diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveGuestIpAddress.cs b/src/PSProxmoxVE.Core/Models/Vms/PveGuestIpAddress.cs
new file mode 100644
index 0000000..ce645d4
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Vms/PveGuestIpAddress.cs
@@ -0,0 +1,27 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Vms;
+
+///
+/// Represents an IP address reported by the QEMU guest agent.
+///
+public class PveGuestIpAddress
+{
+ /// The IP address string (e.g., "192.168.1.100" or "fe80::1").
+ [JsonPropertyName("ip-address")]
+ [JsonProperty("ip-address")]
+ public string Address { get; set; } = string.Empty;
+
+ /// The address type: "ipv4" or "ipv6".
+ [JsonPropertyName("ip-address-type")]
+ [JsonProperty("ip-address-type")]
+ public string Type { get; set; } = string.Empty;
+
+ /// The network prefix length (e.g., 24 for a /24 subnet).
+ [JsonPropertyName("prefix")]
+ [JsonProperty("prefix")]
+ public int Prefix { get; set; }
+
+ public override string ToString() => $"{Address}/{Prefix} ({Type})";
+}
diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveGuestNetworkInterface.cs b/src/PSProxmoxVE.Core/Models/Vms/PveGuestNetworkInterface.cs
new file mode 100644
index 0000000..63410e3
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Vms/PveGuestNetworkInterface.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Vms;
+
+///
+/// Represents a network interface reported by the QEMU guest agent.
+///
+public class PveGuestNetworkInterface
+{
+ /// The interface name (e.g., "eth0", "ens18", "lo").
+ [JsonPropertyName("name")]
+ [JsonProperty("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// The hardware (MAC) address of the interface.
+ [JsonPropertyName("hardware-address")]
+ [JsonProperty("hardware-address")]
+ public string? HardwareAddress { get; set; }
+
+ /// The IP addresses assigned to this interface.
+ [JsonPropertyName("ip-addresses")]
+ [JsonProperty("ip-addresses")]
+ public PveGuestIpAddress[] IpAddresses { get; set; } = System.Array.Empty();
+
+ 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}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs
index 826a203..c041d7c 100644
--- a/src/PSProxmoxVE.Core/Services/VmService.cs
+++ b/src/PSProxmoxVE.Core/Services/VmService.cs
@@ -353,5 +353,89 @@ namespace PSProxmoxVE.Core.Services
task.Node = node;
return task;
}
+
+ // -------------------------------------------------------------------------
+ // QEMU Guest Agent
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Pings the QEMU guest agent on the specified VM. Returns true if responsive.
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Retrieves network interface information from the QEMU guest agent.
+ ///
+ 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() ?? Array.Empty();
+ }
+
+ ///
+ /// Executes a command inside the guest via the QEMU guest agent.
+ /// Returns the PID of the spawned process.
+ ///
+ 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
+ {
+ ["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() ?? 0;
+ return pid;
+ }
+
+ ///
+ /// Gets the status/result of a guest agent exec command by PID.
+ ///
+ 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();
+ }
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Containers/SetPveContainerConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/SetPveContainerConfigCmdlet.cs
index bb1fc2a..822e0a2 100644
--- a/src/PSProxmoxVE/Cmdlets/Containers/SetPveContainerConfigCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Containers/SetPveContainerConfigCmdlet.cs
@@ -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; }
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ [Parameter(Mandatory = false)]
+ public Hashtable? AdditionalConfig { get; set; }
+
+ ///
+ ///
+ /// Comma-separated list of configuration keys to delete.
+ /// Maps to the PVE API "delete" parameter.
+ ///
+ ///
+ [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);
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestNetworkCmdlet.cs
new file mode 100644
index 0000000..c39fb4e
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestNetworkCmdlet.cs
@@ -0,0 +1,37 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Vms
+{
+ ///
+ /// Gets network interface information from the QEMU guest agent.
+ ///
+ /// 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.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveVmGuestNetwork")]
+ [OutputType(typeof(PveGuestNetworkInterface))]
+ public sealed class GetPveVmGuestNetworkCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier.
+ [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);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs
new file mode 100644
index 0000000..057ee93
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs
@@ -0,0 +1,72 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Vms
+{
+ ///
+ /// Executes a command inside a VM via the QEMU guest agent.
+ ///
+ /// 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.
+ ///
+ ///
+ [Cmdlet(VerbsLifecycle.Invoke, "PveVmGuestExec")]
+ [OutputType(typeof(PSObject))]
+ public sealed class InvokePveVmGuestExecCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier.
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// The command to execute inside the guest.
+ [Parameter(Mandatory = true, Position = 2)]
+ public string Command { get; set; } = string.Empty;
+
+ /// Optional arguments to pass to the command.
+ [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() != 1);
+
+ var output = new PSObject();
+ output.Properties.Add(new PSNoteProperty("ExitCode", result["exitcode"]?.ToObject() ?? -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!;
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmConfigCmdlet.cs
index 1a16aa5..5f09d48 100644
--- a/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmConfigCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmConfigCmdlet.cs
@@ -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; }
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ [Parameter(Mandatory = false)]
+ public Hashtable? AdditionalConfig { get; set; }
+
+ ///
+ ///
+ /// Comma-separated list of configuration keys to delete (e.g., "ide2,args").
+ /// Maps to the PVE API "delete" parameter.
+ ///
+ ///
+ [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);
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/TestPveVmGuestAgentCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/TestPveVmGuestAgentCmdlet.cs
new file mode 100644
index 0000000..07aa4de
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Vms/TestPveVmGuestAgentCmdlet.cs
@@ -0,0 +1,32 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Vms
+{
+ ///
+ /// Tests whether the QEMU guest agent is responding on a VM.
+ ///
+ /// Pings the QEMU guest agent on the specified virtual machine.
+ /// Returns $true if the agent responds, $false otherwise.
+ ///
+ ///
+ [Cmdlet(VerbsDiagnostic.Test, "PveVmGuestAgent")]
+ [OutputType(typeof(bool))]
+ public sealed class TestPveVmGuestAgentCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier.
+ [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));
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/PSProxmoxVE.psd1 b/src/PSProxmoxVE/PSProxmoxVE.psd1
index 87d4f14..7fef605 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.psd1
+++ b/src/PSProxmoxVE/PSProxmoxVE.psd1
@@ -75,6 +75,11 @@
'Set-PveVmConfig',
'Resize-PveVmDisk',
+ # QEMU Guest Agent
+ 'Test-PveVmGuestAgent',
+ 'Get-PveVmGuestNetwork',
+ 'Invoke-PveVmGuestExec',
+
# Containers
'Get-PveContainer',
'New-PveContainer',
diff --git a/tests/PSProxmoxVE.Tests/Vms/VmConfigCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/VmConfigCmdlets.Tests.ps1
index a8a2937..2e64127 100644
--- a/tests/PSProxmoxVE.Tests/Vms/VmConfigCmdlets.Tests.ps1
+++ b/tests/PSProxmoxVE.Tests/Vms/VmConfigCmdlets.Tests.ps1
@@ -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
}