mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-12 23:26:53 +00:00
Issue #3 — Set-PveVmConfig / Set-PveContainerConfig: - Add -AdditionalConfig [Hashtable] for arbitrary PVE config keys (scsi0, boot, agent, net0, ide2, cicustom, etc.) - Add -Delete [string] for removing config keys - Unit tests for both new parameters Issue #2 — QEMU Guest Agent cmdlets: - Test-PveVmGuestAgent: pings guest agent, returns bool - Get-PveVmGuestNetwork: returns guest network interfaces with IPs - Invoke-PveVmGuestExec: executes command in guest, returns output - New models: PveGuestNetworkInterface, PveGuestIpAddress - Service methods in VmService for all agent operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an IP address reported by the QEMU guest agent.
|
||||
/// </summary>
|
||||
public class PveGuestIpAddress
|
||||
{
|
||||
/// <summary>The IP address string (e.g., "192.168.1.100" or "fe80::1").</summary>
|
||||
[JsonPropertyName("ip-address")]
|
||||
[JsonProperty("ip-address")]
|
||||
public string Address { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The address type: "ipv4" or "ipv6".</summary>
|
||||
[JsonPropertyName("ip-address-type")]
|
||||
[JsonProperty("ip-address-type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The network prefix length (e.g., 24 for a /24 subnet).</summary>
|
||||
[JsonPropertyName("prefix")]
|
||||
[JsonProperty("prefix")]
|
||||
public int Prefix { get; set; }
|
||||
|
||||
public override string ToString() => $"{Address}/{Prefix} ({Type})";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a network interface reported by the QEMU guest agent.
|
||||
/// </summary>
|
||||
public class PveGuestNetworkInterface
|
||||
{
|
||||
/// <summary>The interface name (e.g., "eth0", "ens18", "lo").</summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The hardware (MAC) address of the interface.</summary>
|
||||
[JsonPropertyName("hardware-address")]
|
||||
[JsonProperty("hardware-address")]
|
||||
public string? HardwareAddress { get; set; }
|
||||
|
||||
/// <summary>The IP addresses assigned to this interface.</summary>
|
||||
[JsonPropertyName("ip-addresses")]
|
||||
[JsonProperty("ip-addresses")]
|
||||
public PveGuestIpAddress[] IpAddresses { get; set; } = System.Array.Empty<PveGuestIpAddress>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var ips = IpAddresses.Length > 0
|
||||
? string.Join(", ", System.Array.ConvertAll(IpAddresses, ip => ip.ToString()))
|
||||
: "no addresses";
|
||||
return $"{Name} ({HardwareAddress ?? "N/A"}): {ips}";
|
||||
}
|
||||
}
|
||||
@@ -353,5 +353,89 @@ namespace PSProxmoxVE.Core.Services
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// QEMU Guest Agent
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Pings the QEMU guest agent on the specified VM. Returns true if responsive.
|
||||
/// </summary>
|
||||
public bool PingGuestAgent(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/ping").GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves network interface information from the QEMU guest agent.
|
||||
/// </summary>
|
||||
public PveGuestNetworkInterface[] GetGuestNetworkInterfaces(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/agent/network-get-interfaces")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
var result = data?["result"];
|
||||
return result?.ToObject<PveGuestNetworkInterface[]>() ?? Array.Empty<PveGuestNetworkInterface>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a command inside the guest via the QEMU guest agent.
|
||||
/// Returns the PID of the spawned process.
|
||||
/// </summary>
|
||||
public int ExecuteGuestCommand(PveSession session, string node, int vmid,
|
||||
string command, string[]? args = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(command)) throw new ArgumentNullException(nameof(command));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["command"] = command
|
||||
};
|
||||
|
||||
if (args != null && args.Length > 0)
|
||||
{
|
||||
// PVE expects input-data for arguments passed as a JSON-encoded string array
|
||||
var argsJson = Newtonsoft.Json.JsonConvert.SerializeObject(args);
|
||||
data["input-data"] = argsJson;
|
||||
}
|
||||
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/exec", data)
|
||||
.GetAwaiter().GetResult();
|
||||
var pid = JObject.Parse(response)["data"]?["pid"]?.ToObject<int>() ?? 0;
|
||||
return pid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status/result of a guest agent exec command by PID.
|
||||
/// </summary>
|
||||
public JObject GetGuestExecStatus(PveSession session, string node, int vmid, int pid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/agent/exec-status?pid={pid}")
|
||||
.GetAwaiter().GetResult();
|
||||
return JObject.Parse(response)["data"] as JObject ?? new JObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
@@ -77,6 +78,25 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? SearchDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// Hashtable of additional configuration keys to set. Use this for any PVE config
|
||||
/// option not exposed as a named parameter (e.g., net0, mp0, rootfs).
|
||||
/// Values are merged after named parameters and can override them.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public Hashtable? AdditionalConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// Comma-separated list of configuration keys to delete.
|
||||
/// Maps to the PVE API "delete" parameter.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Delete { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "Set-PveContainerConfig"))
|
||||
@@ -104,6 +124,15 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
if (!string.IsNullOrEmpty(SearchDomain))
|
||||
config["searchdomain"] = SearchDomain!;
|
||||
|
||||
if (AdditionalConfig != null)
|
||||
{
|
||||
foreach (DictionaryEntry entry in AdditionalConfig)
|
||||
config[entry.Key.ToString()!] = entry.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Delete))
|
||||
config["delete"] = Delete!;
|
||||
|
||||
containerService.SetContainerConfig(session, Node, VmId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Vms
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Gets network interface information from the QEMU guest agent.</para>
|
||||
/// <para type="description">
|
||||
/// Queries the QEMU guest agent running inside the specified VM for its network
|
||||
/// interface configuration, including interface names, MAC addresses, and IP addresses.
|
||||
/// The guest agent must be installed and running inside the VM.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveVmGuestNetwork")]
|
||||
[OutputType(typeof(PveGuestNetworkInterface))]
|
||||
public sealed class GetPveVmGuestNetworkCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The VM identifier.</summary>
|
||||
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
|
||||
public int VmId { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = GetSession();
|
||||
var service = new VmService();
|
||||
var interfaces = service.GetGuestNetworkInterfaces(session, Node, VmId);
|
||||
|
||||
foreach (var iface in interfaces)
|
||||
WriteObject(iface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Vms
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Executes a command inside a VM via the QEMU guest agent.</para>
|
||||
/// <para type="description">
|
||||
/// Sends a command to the QEMU guest agent running inside the specified VM for execution.
|
||||
/// Returns the result including stdout, stderr, and exit code. The guest agent must be
|
||||
/// installed and running inside the VM.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsLifecycle.Invoke, "PveVmGuestExec")]
|
||||
[OutputType(typeof(PSObject))]
|
||||
public sealed class InvokePveVmGuestExecCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The VM identifier.</summary>
|
||||
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
|
||||
public int VmId { get; set; }
|
||||
|
||||
/// <summary>The command to execute inside the guest.</summary>
|
||||
[Parameter(Mandatory = true, Position = 2)]
|
||||
public string Command { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Optional arguments to pass to the command.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string[]? Args { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = GetSession();
|
||||
var service = new VmService();
|
||||
|
||||
var pid = service.ExecuteGuestCommand(session, Node, VmId, Command, Args);
|
||||
|
||||
// Poll for completion
|
||||
Newtonsoft.Json.Linq.JObject result;
|
||||
do
|
||||
{
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
result = service.GetGuestExecStatus(session, Node, VmId, pid);
|
||||
} while (result["exited"]?.ToObject<int>() != 1);
|
||||
|
||||
var output = new PSObject();
|
||||
output.Properties.Add(new PSNoteProperty("ExitCode", result["exitcode"]?.ToObject<int>() ?? -1));
|
||||
output.Properties.Add(new PSNoteProperty("Stdout", DecodeBase64(result["out-data"]?.ToString())));
|
||||
output.Properties.Add(new PSNoteProperty("Stderr", DecodeBase64(result["err-data"]?.ToString())));
|
||||
output.Properties.Add(new PSNoteProperty("Pid", pid));
|
||||
|
||||
WriteObject(output);
|
||||
}
|
||||
|
||||
private static string DecodeBase64(string? encoded)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encoded)) return string.Empty;
|
||||
try
|
||||
{
|
||||
var bytes = System.Convert.FromBase64String(encoded);
|
||||
return System.Text.Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return encoded!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
@@ -83,6 +84,25 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? OsType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// Hashtable of additional configuration keys to set. Use this for any PVE config
|
||||
/// option not exposed as a named parameter (e.g., scsi0, boot, agent, net0, ide2).
|
||||
/// Values are merged after named parameters and can override them.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public Hashtable? AdditionalConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// Comma-separated list of configuration keys to delete (e.g., "ide2,args").
|
||||
/// Maps to the PVE API "delete" parameter.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Delete { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Set-PveVmConfig"))
|
||||
@@ -112,6 +132,15 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
if (!string.IsNullOrEmpty(OsType))
|
||||
config["ostype"] = OsType!;
|
||||
|
||||
if (AdditionalConfig != null)
|
||||
{
|
||||
foreach (DictionaryEntry entry in AdditionalConfig)
|
||||
config[entry.Key.ToString()!] = entry.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Delete))
|
||||
config["delete"] = Delete!;
|
||||
|
||||
vmService.SetVmConfig(session, Node, VmId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Vms
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Tests whether the QEMU guest agent is responding on a VM.</para>
|
||||
/// <para type="description">
|
||||
/// Pings the QEMU guest agent on the specified virtual machine.
|
||||
/// Returns $true if the agent responds, $false otherwise.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsDiagnostic.Test, "PveVmGuestAgent")]
|
||||
[OutputType(typeof(bool))]
|
||||
public sealed class TestPveVmGuestAgentCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The VM identifier.</summary>
|
||||
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
|
||||
public int VmId { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = GetSession();
|
||||
var service = new VmService();
|
||||
WriteObject(service.PingGuestAgent(session, Node, VmId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,11 @@
|
||||
'Set-PveVmConfig',
|
||||
'Resize-PveVmDisk',
|
||||
|
||||
# QEMU Guest Agent
|
||||
'Test-PveVmGuestAgent',
|
||||
'Get-PveVmGuestNetwork',
|
||||
'Invoke-PveVmGuestExec',
|
||||
|
||||
# Containers
|
||||
'Get-PveContainer',
|
||||
'New-PveContainer',
|
||||
|
||||
@@ -212,6 +212,22 @@ Describe 'Set-PveVmConfig' {
|
||||
$script:Cmd.Parameters['OsType'].ParameterType | Should -Be ([string])
|
||||
}
|
||||
|
||||
It 'Should have AdditionalConfig parameter (optional, Hashtable)' {
|
||||
$script:Cmd.Parameters.ContainsKey('AdditionalConfig') | Should -BeTrue
|
||||
$script:Cmd.Parameters['AdditionalConfig'].ParameterType | Should -Be ([hashtable])
|
||||
$isMandatory = $script:Cmd.Parameters['AdditionalConfig'].ParameterSets.Values |
|
||||
Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Should have Delete parameter (optional, String)' {
|
||||
$script:Cmd.Parameters.ContainsKey('Delete') | Should -BeTrue
|
||||
$script:Cmd.Parameters['Delete'].ParameterType | Should -Be ([string])
|
||||
$isMandatory = $script:Cmd.Parameters['Delete'].ParameterSets.Values |
|
||||
Where-Object { $_.IsMandatory }
|
||||
$isMandatory | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Should have Session parameter (inherited from PveCmdletBase)' {
|
||||
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user