Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add Apis -- Exec and Get Command Output #55

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/TopoMojo.Api/Features/Vm/VmController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,53 @@ await GetVmIsolationTag(id)
return Ok(opt);
}

/// <summary>
/// Start a vm command execution.
/// </summary>
/// <param name="id">Vm Id</param>
/// <param name="commandList">Commands to execute</param>
/// <returns>The pid of the started command execution process.</returns>
[HttpPost("api/vm/{id}/exec")]
[SwaggerOperation(OperationId = "ExecVmCommand")]
[Authorize(AppConstants.AnyUserPolicy)]
public async Task<ActionResult<int>> ExecVmCommand(string id, [FromBody] string[] commandList)
{
if (!AuthorizeAny(
() => CanManageVm(id, Actor).Result
)) return Forbid();

try {
return Ok(
await podService.ExecCommand(id, commandList)
);
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}

/// <summary>
/// Get a vm command execution output.
/// </summary>
/// <param name="id">Vm Id</param>
/// <param name="pid">Command pid @see ExecVmCommand</param>
/// <returns>The output of the command execution.</returns>
[HttpGet("api/vm/{id}/exec/{pid}")]
[SwaggerOperation(OperationId = "GetVmCommandOutput")]
[Authorize(AppConstants.AnyUserPolicy)]
public async Task<ActionResult<VmExecResponse>> GetVmCommandOutput(string id, int pid)
{
if (!AuthorizeAny(
() => CanManageVm(id, Actor).Result
)) return Forbid();

try {
return Ok(
await podService.GetCommandOutput(id, pid)
);
} catch (Exception ex) {
return BadRequest(ex.Message);
}
}
/// <summary>
/// Request a vm console access ticket.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/TopoMojo.Hypervisor/IHypervisorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public interface IHypervisorService
// Task<TemplateOptions> GetTemplateOptions(string key);
Task<VmOptions> GetVmIsoOptions(string key);
Task<VmOptions> GetVmNetOptions(string key);
Task<int> ExecCommand(string id, string[] command);
Task<VmExecResponse> GetCommandOutput(string id, int pid);
string Version { get; }
Task ReloadHost(string host);

Expand Down
46 changes: 46 additions & 0 deletions src/TopoMojo.Hypervisor/Proxmox/ProxmoxClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,52 @@ public async Task<Vm> Save(string id)
return vm;
}

public async Task<int> ExecCommand(string id, string[] command)
{
var vm = _vmCache[id];
var task = await _pveClient.Nodes[vm.Host].Qemu[vm.Id].Agent.Exec.Exec(command);
await _pveClient.WaitForTaskToFinish(task);

if (task.IsSuccessStatusCode)
{
return (int)task.Response.data.pid;
}
else
{
throw new Exception(task.ReasonPhrase);
}
}

public async Task<VmExecResponse> GetCommandOutput(string id, int pid)
{
var vm = _vmCache[id];
var task = await _pveClient.Nodes[vm.Host].Qemu[vm.GetId()].Agent.ExecStatus.ExecStatus(pid);
await _pveClient.WaitForTaskToFinish(task);

if (task.IsSuccessStatusCode)
{
var responseData = (IDictionary<string, object>)task.Response.data;

// Create a new VmExecResponse object and populate it from the ExpandoObject
var execResponse = new VmExecResponse
{
ErrData = responseData.ContainsKey("err-data") ? (string)responseData["err-data"] : null,
ErrTruncated = responseData.ContainsKey("err-truncated") ? (bool?)responseData["err-truncated"] : false,
ExitCode = responseData.ContainsKey("exitcode") ? Convert.ToInt32(responseData["exitcode"]) : (int?)null,
Exited = Convert.ToInt32(responseData["exited"]) != 0,
OutData = responseData.ContainsKey("out-data") ? (string)responseData["out-data"] : null,
OutTruncated = responseData.ContainsKey("out-truncated") ? (bool?)responseData["out-truncated"] : false,
Signal = responseData.ContainsKey("signal") ? Convert.ToInt32(responseData["signal"]) : (int?)null
};

return execResponse;
}
else
{
throw new Exception(task.ReasonPhrase);
}
}

private async Task CompleteSave(Result task, string oldId, int nextId, Vm template, string vmId)
{
try
Expand Down
10 changes: 10 additions & 0 deletions src/TopoMojo.Hypervisor/Proxmox/ProxmoxHypervisorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,16 @@ public async Task<VmOptions> GetVmIsoOptions(string key)
};
}

public async Task<int> ExecCommand(string id, string[] command)
{
return await _pveClient.ExecCommand(id, command);
}

public async Task<VmExecResponse> GetCommandOutput(string id, int pid)
{
return await _pveClient.GetCommandOutput(id, pid);
}

public Task ReloadHost(string host)
{
throw new NotImplementedException();
Expand Down
12 changes: 12 additions & 0 deletions src/TopoMojo.Hypervisor/Vm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ public enum VmOperationType
Reset
}


public class VmExecResponse
{
public string ErrData { get; set; } // Optional, stderr of the process
public bool? ErrTruncated { get; set; } // Optional, true if stderr was not fully captured
public int? ExitCode { get; set; } // Optional, process exit code if it was normally terminated
public bool Exited { get; set; } // Required, tells if the command has exited yet
public string OutData { get; set; } // Optional, stdout of the process
public bool? OutTruncated { get; set; } // Optional, true if stdout was not fully captured
public int? Signal { get; set; } // Optional, signal number or exception code if the process was abnormally terminated
}

public class VmConsole
{
public string Id { get; set; }
Expand Down
12 changes: 12 additions & 0 deletions src/TopoMojo.Hypervisor/vMock/MockHypervisorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,18 @@ public Task StopAll(string target)
throw new NotImplementedException();
}

public async Task<int> ExecCommand(string id, string[] command)
{
await Task.Delay(0);
return 0;
}

public async Task<VmExecResponse> GetCommandOutput(string id, int pid)
{
await Task.Delay(0);
return new VmExecResponse();
}

private void NormalizeOptions(HypervisorServiceConfiguration options)
{
var regex = new Regex("(]|/)$");
Expand Down
13 changes: 13 additions & 0 deletions src/TopoMojo.Hypervisor/vSphere/vSphereHypervisorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,19 @@ public async Task<VmOptions> GetVmNetOptions(string id)
Net = _vlanman.FindNetworks(id)
};
}

public async Task<int> ExecCommand(string id, string[] command)
{
// TODO: implement
throw new NotImplementedException();
}

public async Task<VmExecResponse> GetCommandOutput(string id, int pid)
{
// TODO: implement
throw new NotImplementedException();
}

public string Version
{
get
Expand Down