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