diff --git a/src/TopoMojo.Api/Features/Vm/VmController.cs b/src/TopoMojo.Api/Features/Vm/VmController.cs
index cfe13b0..5ba788d 100644
--- a/src/TopoMojo.Api/Features/Vm/VmController.cs
+++ b/src/TopoMojo.Api/Features/Vm/VmController.cs
@@ -240,6 +240,53 @@ await GetVmIsolationTag(id)
return Ok(opt);
}
+ ///
+ /// Start a vm command execution.
+ ///
+ /// Vm Id
+ /// Commands to execute
+ /// The pid of the started command execution process.
+ [HttpPost("api/vm/{id}/exec")]
+ [SwaggerOperation(OperationId = "ExecVmCommand")]
+ [Authorize(AppConstants.AnyUserPolicy)]
+ public async Task> 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);
+ }
+ }
+
+ ///
+ /// Get a vm command execution output.
+ ///
+ /// Vm Id
+ /// Command pid @see ExecVmCommand
+ /// The output of the command execution.
+ [HttpGet("api/vm/{id}/exec/{pid}")]
+ [SwaggerOperation(OperationId = "GetVmCommandOutput")]
+ [Authorize(AppConstants.AnyUserPolicy)]
+ public async Task> 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);
+ }
+ }
///
/// Request a vm console access ticket.
///
diff --git a/src/TopoMojo.Hypervisor/IHypervisorService.cs b/src/TopoMojo.Hypervisor/IHypervisorService.cs
index 8df7074..9b10c85 100755
--- a/src/TopoMojo.Hypervisor/IHypervisorService.cs
+++ b/src/TopoMojo.Hypervisor/IHypervisorService.cs
@@ -30,6 +30,8 @@ public interface IHypervisorService
// Task GetTemplateOptions(string key);
Task GetVmIsoOptions(string key);
Task GetVmNetOptions(string key);
+ Task ExecCommand(string id, string[] command);
+ Task GetCommandOutput(string id, int pid);
string Version { get; }
Task ReloadHost(string host);
diff --git a/src/TopoMojo.Hypervisor/Proxmox/ProxmoxClient.cs b/src/TopoMojo.Hypervisor/Proxmox/ProxmoxClient.cs
index bfba684..2539b89 100644
--- a/src/TopoMojo.Hypervisor/Proxmox/ProxmoxClient.cs
+++ b/src/TopoMojo.Hypervisor/Proxmox/ProxmoxClient.cs
@@ -504,6 +504,52 @@ public async Task Save(string id)
return vm;
}
+ public async Task 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 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)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
diff --git a/src/TopoMojo.Hypervisor/Proxmox/ProxmoxHypervisorService.cs b/src/TopoMojo.Hypervisor/Proxmox/ProxmoxHypervisorService.cs
index b1996a9..d385768 100644
--- a/src/TopoMojo.Hypervisor/Proxmox/ProxmoxHypervisorService.cs
+++ b/src/TopoMojo.Hypervisor/Proxmox/ProxmoxHypervisorService.cs
@@ -486,6 +486,16 @@ public async Task GetVmIsoOptions(string key)
};
}
+ public async Task ExecCommand(string id, string[] command)
+ {
+ return await _pveClient.ExecCommand(id, command);
+ }
+
+ public async Task GetCommandOutput(string id, int pid)
+ {
+ return await _pveClient.GetCommandOutput(id, pid);
+ }
+
public Task ReloadHost(string host)
{
throw new NotImplementedException();
diff --git a/src/TopoMojo.Hypervisor/Vm.cs b/src/TopoMojo.Hypervisor/Vm.cs
index 6374f95..4ceed49 100644
--- a/src/TopoMojo.Hypervisor/Vm.cs
+++ b/src/TopoMojo.Hypervisor/Vm.cs
@@ -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; }
diff --git a/src/TopoMojo.Hypervisor/vMock/MockHypervisorService.cs b/src/TopoMojo.Hypervisor/vMock/MockHypervisorService.cs
index 7449f96..b5e4c58 100644
--- a/src/TopoMojo.Hypervisor/vMock/MockHypervisorService.cs
+++ b/src/TopoMojo.Hypervisor/vMock/MockHypervisorService.cs
@@ -514,6 +514,18 @@ public Task StopAll(string target)
throw new NotImplementedException();
}
+ public async Task ExecCommand(string id, string[] command)
+ {
+ await Task.Delay(0);
+ return 0;
+ }
+
+ public async Task GetCommandOutput(string id, int pid)
+ {
+ await Task.Delay(0);
+ return new VmExecResponse();
+ }
+
private void NormalizeOptions(HypervisorServiceConfiguration options)
{
var regex = new Regex("(]|/)$");
diff --git a/src/TopoMojo.Hypervisor/vSphere/vSphereHypervisorService.cs b/src/TopoMojo.Hypervisor/vSphere/vSphereHypervisorService.cs
index c582436..7378e0d 100644
--- a/src/TopoMojo.Hypervisor/vSphere/vSphereHypervisorService.cs
+++ b/src/TopoMojo.Hypervisor/vSphere/vSphereHypervisorService.cs
@@ -475,6 +475,19 @@ public async Task GetVmNetOptions(string id)
Net = _vlanman.FindNetworks(id)
};
}
+
+ public async Task ExecCommand(string id, string[] command)
+ {
+ // TODO: implement
+ throw new NotImplementedException();
+ }
+
+ public async Task GetCommandOutput(string id, int pid)
+ {
+ // TODO: implement
+ throw new NotImplementedException();
+ }
+
public string Version
{
get