diff --git a/src/PSProxmoxVE/Cmdlets/CloudInit/GetPveCloudInitConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/CloudInit/GetPveCloudInitConfigCmdlet.cs
new file mode 100644
index 0000000..3146978
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/CloudInit/GetPveCloudInitConfigCmdlet.cs
@@ -0,0 +1,41 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.CloudInit
+{
+ ///
+ /// Retrieves cloud-init configuration for a Proxmox VE virtual machine.
+ ///
+ /// Returns the VM configuration with a focus on cloud-init fields: ciuser, cipassword,
+ /// sshkeys, ipconfig*, nameserver, and searchdomain. Returns the full PveVmConfig object
+ /// so all other config fields are available as well. VmId can be piped from Get-PveVm.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveCloudInitConfig")]
+ [OutputType(typeof(PveVmConfig))]
+ public class GetPveCloudInitConfigCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.GetAsync($"/nodes/{Node}/qemu/{VmId}/config").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"];
+
+ var config = data?.ToObject() ?? new PveVmConfig();
+ WriteObject(config);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs
new file mode 100644
index 0000000..4e202e2
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs
@@ -0,0 +1,46 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.CloudInit
+{
+ ///
+ /// Regenerates the cloud-init drive on a Proxmox VE virtual machine.
+ ///
+ /// Forces regeneration of the cloud-init ISO drive on the specified VM, ensuring that
+ /// the latest cloud-init configuration is applied on the next boot. This is useful
+ /// after making changes via Set-PveCloudInitConfig. Returns a PveTask.
+ ///
+ ///
+ [Cmdlet(VerbsLifecycle.Invoke, "PveCloudInitRegenerate", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class InvokePveCloudInitRegenerateCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} on {Node}", "Regenerate PVE Cloud-Init Drive"))
+ return;
+
+ var session = GetSession();
+ var service = new CloudInitService();
+ service.RegenerateCloudInitImage(session, Node, VmId);
+
+ var task = new PveTask
+ {
+ Node = Node,
+ Status = "stopped",
+ ExitStatus = "OK"
+ };
+
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs
new file mode 100644
index 0000000..6db1fc7
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Runtime.InteropServices;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.CloudInit
+{
+ ///
+ /// Sets cloud-init configuration on a Proxmox VE virtual machine.
+ ///
+ /// Updates cloud-init settings for the specified VM. Changes take effect on the next
+ /// VM boot after the cloud-init drive is regenerated. Only specified parameters are
+ /// updated. Use Invoke-PveCloudInitRegenerate to force drive regeneration.
+ /// Returns a PveTask. Use -Wait to block until the update completes.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveCloudInitConfig", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class SetPveCloudInitConfigCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// Cloud-init hostname override.
+ [Parameter(Mandatory = false)]
+ public string? Hostname { get; set; }
+
+ /// Cloud-init default username.
+ [Parameter(Mandatory = false)]
+ public string? User { get; set; }
+
+ /// Cloud-init default user password. Accepts a SecureString.
+ [Parameter(Mandatory = false)]
+ public System.Security.SecureString? Password { get; set; }
+
+ /// SSH public keys to inject (one key per element).
+ [Parameter(Mandatory = false)]
+ public string[]? SshKeys { get; set; }
+
+ ///
+ /// IP configuration string (e.g., "ip=192.168.1.50/24,gw=192.168.1.1").
+ /// Maps to ipconfig0 on the VM.
+ ///
+ [Parameter(Mandatory = false)]
+ public string? IpConfig { get; set; }
+
+ /// DNS nameserver(s) to inject (space or comma separated).
+ [Parameter(Mandatory = false)]
+ public string? Dns { get; set; }
+
+ /// DNS search domain to inject.
+ [Parameter(Mandatory = false)]
+ public string? SearchDomain { get; set; }
+
+ /// When specified, waits for the config update task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} on {Node}", "Set PVE Cloud-Init Config"))
+ return;
+
+ var config = new Dictionary();
+
+ if (!string.IsNullOrEmpty(User)) config["ciuser"] = User;
+ if (!string.IsNullOrEmpty(Hostname)) config["name"] = Hostname;
+ if (!string.IsNullOrEmpty(IpConfig)) config["ipconfig0"] = IpConfig;
+ if (!string.IsNullOrEmpty(Dns)) config["nameserver"] = Dns;
+ if (!string.IsNullOrEmpty(SearchDomain)) config["searchdomain"] = SearchDomain;
+
+ if (Password != null)
+ {
+ var ptr = Marshal.SecureStringToGlobalAllocUnicode(Password);
+ try { config["cipassword"] = Marshal.PtrToStringUni(ptr) ?? string.Empty; }
+ finally { Marshal.ZeroFreeGlobalAllocUnicode(ptr); }
+ }
+
+ if (SshKeys != null && SshKeys.Length > 0)
+ {
+ // PVE expects URL-encoded newline-delimited keys
+ var combined = string.Join("\n", SshKeys);
+ config["sshkeys"] = Uri.EscapeDataString(combined);
+ }
+
+ if (config.Count == 0)
+ {
+ WriteWarning("No cloud-init parameters specified. Nothing to update.");
+ return;
+ }
+
+ var session = GetSession();
+ var service = new CloudInitService();
+ service.SetCloudInitConfig(session, Node, VmId, config);
+
+ // The PVE config PUT endpoint may or may not return a task UPID;
+ // surface a minimal task object so callers always get consistent output.
+ var task = new PveTask { Node = Node, Status = "stopped", ExitStatus = "OK" };
+
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs
new file mode 100644
index 0000000..ed2c345
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Network;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Lists network interfaces configured on a Proxmox VE node.
+ ///
+ /// Returns network interface definitions from the specified node.
+ /// Optionally filter by interface type. Node accepts pipeline input from Get-PveNode.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveNetwork")]
+ [OutputType(typeof(PveNetwork))]
+ public class GetPveNetworkCmdlet : PveCmdletBase
+ {
+ ///
+ /// The Proxmox VE node name. Accepts pipeline input from Get-PveNode (PveNode.Name).
+ ///
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("NodeName")]
+ public string Node { get; set; } = string.Empty;
+
+ /// Filter by interface type (e.g., "bridge", "bond", "eth", "vlan").
+ [Parameter(Mandatory = false)]
+ [ValidateSet("bridge", "bond", "eth", "alias", "vlan", "OVSBridge", "OVSBond",
+ "OVSPort", "OVSIntPort", "any_bridge", "any_local_bridge", IgnoreCase = true)]
+ public string? Type { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var resource = $"/nodes/{Node}/network";
+ if (!string.IsNullOrEmpty(Type))
+ resource += $"?type={Uri.EscapeDataString(Type)}";
+
+ var json = client.GetAsync(resource).GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var network = item.ToObject()!;
+ network.Node = Node;
+ WriteObject(network);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnVnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnVnetCmdlet.cs
new file mode 100644
index 0000000..bd77a9a
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnVnetCmdlet.cs
@@ -0,0 +1,52 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Network;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Lists SDN VNets defined in Proxmox VE.
+ ///
+ /// Returns Software-Defined Networking VNet definitions from the cluster SDN configuration.
+ /// Optionally filter by zone.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveSdnVnet")]
+ [OutputType(typeof(PveSdnVnet))]
+ public class GetPveSdnVnetCmdlet : PveCmdletBase
+ {
+ /// Filter VNets to a specific zone.
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? Zone { get; set; }
+
+ /// Optional VNet identifier to retrieve a specific VNet.
+ [Parameter(Mandatory = false)]
+ public string? Vnet { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.GetAsync("/cluster/sdn/vnets").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var vnet = item.ToObject()!;
+
+ if (!string.IsNullOrEmpty(Zone) &&
+ !string.Equals(vnet.Zone, Zone, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ if (!string.IsNullOrEmpty(Vnet) &&
+ !string.Equals(vnet.Vnet, Vnet, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ WriteObject(vnet);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnZoneCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnZoneCmdlet.cs
new file mode 100644
index 0000000..7d3a146
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/GetPveSdnZoneCmdlet.cs
@@ -0,0 +1,42 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Network;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Lists SDN zones defined in Proxmox VE.
+ ///
+ /// Returns Software-Defined Networking zone definitions from the cluster SDN configuration.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveSdnZone")]
+ [OutputType(typeof(PveSdnZone))]
+ public class GetPveSdnZoneCmdlet : PveCmdletBase
+ {
+ /// Optional zone identifier to retrieve a specific zone.
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? Zone { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var resource = "/cluster/sdn/zones";
+ var json = client.GetAsync(resource).GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var zone = item.ToObject()!;
+ if (!string.IsNullOrEmpty(Zone) &&
+ !string.Equals(zone.Zone, Zone, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+ WriteObject(zone);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/InvokePveNetworkApplyCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/InvokePveNetworkApplyCmdlet.cs
new file mode 100644
index 0000000..f5d6b56
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/InvokePveNetworkApplyCmdlet.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Applies pending network configuration changes on a Proxmox VE node.
+ ///
+ /// Reloads the network configuration on the specified node, applying any pending changes
+ /// made via New-PveNetwork, Set-PveNetwork, or Remove-PveNetwork.
+ /// Returns a PveTask. Use -Wait to block until the apply completes.
+ ///
+ ///
+ [Cmdlet(VerbsLifecycle.Invoke, "PveNetworkApply", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class InvokePveNetworkApplyCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node on which to apply network changes.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// When specified, waits for the apply task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Node, "Apply PVE Network Configuration"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.PutAsync($"/nodes/{Node}/network").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var upid = root["data"]?.ToString() ?? string.Empty;
+
+ var task = new PveTask { Upid = upid, Node = Node, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ task = WaitForTask(client, Node, upid);
+ }
+
+ WriteObject(task);
+ }
+
+ private static PveTask WaitForTask(PveHttpClient client, string node, string upid)
+ {
+ var encodedUpid = Uri.EscapeDataString(upid);
+ var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status";
+ while (true)
+ {
+ System.Threading.Thread.Sleep(2000);
+ var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult();
+ var statusRoot = JObject.Parse(statusJson);
+ var d = statusRoot["data"];
+ if (d?["status"]?.ToString() == "stopped")
+ return new PveTask { Upid = upid, Node = node, Status = "stopped", ExitStatus = d["exitstatus"]?.ToString() };
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs
new file mode 100644
index 0000000..38dce08
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Creates a new network interface on a Proxmox VE node.
+ ///
+ /// Adds a network interface definition to the specified node. After creation, use
+ /// Invoke-PveNetworkApply to apply pending changes to the running system.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveNetwork", SupportsShouldProcess = true)]
+ public class NewPveNetworkCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The interface name (e.g., "vmbr1", "bond0").
+ [Parameter(Mandatory = true, Position = 1)]
+ public string Interface { get; set; } = string.Empty;
+
+ /// The interface type.
+ [Parameter(Mandatory = true, Position = 2)]
+ [ValidateSet("bridge", "bond", "eth", "alias", "vlan", "OVSBridge", "OVSBond",
+ "OVSPort", "OVSIntPort", IgnoreCase = true)]
+ public string Type { get; set; } = string.Empty;
+
+ /// IPv4 address for the interface.
+ [Parameter(Mandatory = false)]
+ public string? Address { get; set; }
+
+ /// IPv4 subnet mask.
+ [Parameter(Mandatory = false)]
+ public string? Netmask { get; set; }
+
+ /// IPv4 gateway address.
+ [Parameter(Mandatory = false)]
+ public string? Gateway { get; set; }
+
+ /// Bridge ports (space-separated interface names, for bridge type).
+ [Parameter(Mandatory = false)]
+ public string? BridgePorts { get; set; }
+
+ /// Bond slave interfaces (space-separated names, for bond type).
+ [Parameter(Mandatory = false)]
+ public string? BondSlaves { get; set; }
+
+ /// VLAN tag ID (for vlan type).
+ [Parameter(Mandatory = false)]
+ public int? VlanId { get; set; }
+
+ /// MTU override.
+ [Parameter(Mandatory = false)]
+ public int? Mtu { get; set; }
+
+ /// Configure this interface to start automatically at boot.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Autostart { get; set; }
+
+ /// Optional comments/notes for this interface.
+ [Parameter(Mandatory = false)]
+ public string? Comments { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"{Interface} on {Node}", "Create PVE Network Interface"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["iface"] = Interface,
+ ["type"] = Type
+ };
+
+ if (!string.IsNullOrEmpty(Address)) data["address"] = Address;
+ if (!string.IsNullOrEmpty(Netmask)) data["netmask"] = Netmask;
+ if (!string.IsNullOrEmpty(Gateway)) data["gateway"] = Gateway;
+ if (!string.IsNullOrEmpty(BridgePorts)) data["bridge_ports"] = BridgePorts;
+ if (!string.IsNullOrEmpty(BondSlaves)) data["slaves"] = BondSlaves;
+ if (VlanId.HasValue) data["vlan-id"] = VlanId.Value.ToString();
+ if (Mtu.HasValue) data["mtu"] = Mtu.Value.ToString();
+ if (Autostart.IsPresent) data["autostart"] = "1";
+ if (!string.IsNullOrEmpty(Comments)) data["comments"] = Comments;
+
+ client.PostAsync($"/nodes/{Node}/network", data).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnVnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnVnetCmdlet.cs
new file mode 100644
index 0000000..6654d44
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnVnetCmdlet.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Creates a new SDN VNet in Proxmox VE.
+ ///
+ /// Adds a new Software-Defined Networking VNet (virtual network) to the specified SDN zone.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveSdnVnet", SupportsShouldProcess = true)]
+ public class NewPveSdnVnetCmdlet : PveCmdletBase
+ {
+ /// The VNet identifier (alphanumeric, up to 8 characters).
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Vnet { get; set; } = string.Empty;
+
+ /// The SDN zone this VNet belongs to.
+ [Parameter(Mandatory = true, Position = 1)]
+ public string Zone { get; set; } = string.Empty;
+
+ /// VLAN tag for VLAN-type zones.
+ [Parameter(Mandatory = false)]
+ public int? Tag { get; set; }
+
+ /// Optional alias/description for the VNet.
+ [Parameter(Mandatory = false)]
+ public string? Alias { get; set; }
+
+ /// Enable VLAN awareness on this VNet.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter VlanAware { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"{Vnet} in zone {Zone}", "Create PVE SDN VNet"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["vnet"] = Vnet,
+ ["zone"] = Zone
+ };
+
+ if (Tag.HasValue) data["tag"] = Tag.Value.ToString();
+ if (!string.IsNullOrEmpty(Alias)) data["alias"] = Alias;
+ if (VlanAware.IsPresent) data["vlanaware"] = "1";
+
+ client.PostAsync("/cluster/sdn/vnets", data).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnZoneCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnZoneCmdlet.cs
new file mode 100644
index 0000000..a680f2f
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/NewPveSdnZoneCmdlet.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Creates a new SDN zone in Proxmox VE.
+ ///
+ /// Adds a new Software-Defined Networking zone to the Proxmox VE cluster configuration.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveSdnZone", SupportsShouldProcess = true)]
+ public class NewPveSdnZoneCmdlet : PveCmdletBase
+ {
+ /// The zone identifier (alphanumeric, hyphens allowed).
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Zone { get; set; } = string.Empty;
+
+ /// The zone type.
+ [Parameter(Mandatory = true, Position = 1)]
+ [ValidateSet("vlan", "vxlan", "evpn", "simple", "qinq", IgnoreCase = true)]
+ public string Type { get; set; } = string.Empty;
+
+ /// VXLAN peer list or multicast address (for vxlan/evpn types).
+ [Parameter(Mandatory = false)]
+ public string? Peers { get; set; }
+
+ /// Bridge interface this zone attaches to (for vlan/qinq types).
+ [Parameter(Mandatory = false)]
+ public string? Bridge { get; set; }
+
+ /// MTU override for this zone.
+ [Parameter(Mandatory = false)]
+ public int? Mtu { get; set; }
+
+ /// DNS server for automatic DNS registration.
+ [Parameter(Mandatory = false)]
+ public string? Dns { get; set; }
+
+ /// Reverse DNS server.
+ [Parameter(Mandatory = false)]
+ public string? ReverseDns { get; set; }
+
+ /// DNS zone name for registration.
+ [Parameter(Mandatory = false)]
+ public string? DnsZone { get; set; }
+
+ /// IPAM plugin to use for this zone.
+ [Parameter(Mandatory = false)]
+ public string? Ipam { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Zone, "Create PVE SDN Zone"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["zone"] = Zone,
+ ["type"] = Type
+ };
+
+ if (!string.IsNullOrEmpty(Peers)) data["peers"] = Peers;
+ if (!string.IsNullOrEmpty(Bridge)) data["bridge"] = Bridge;
+ if (Mtu.HasValue) data["mtu"] = Mtu.Value.ToString();
+ if (!string.IsNullOrEmpty(Dns)) data["dns"] = Dns;
+ if (!string.IsNullOrEmpty(ReverseDns)) data["reversedns"] = ReverseDns;
+ if (!string.IsNullOrEmpty(DnsZone)) data["dnszone"] = DnsZone;
+ if (!string.IsNullOrEmpty(Ipam)) data["ipam"] = Ipam;
+
+ client.PostAsync("/cluster/sdn/zones", data).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs
new file mode 100644
index 0000000..208113a
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs
@@ -0,0 +1,35 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Removes a network interface definition from a Proxmox VE node.
+ ///
+ /// Deletes the specified network interface configuration from the node. After removing,
+ /// use Invoke-PveNetworkApply to apply pending changes to the running system.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveNetwork", SupportsShouldProcess = true)]
+ public class RemovePveNetworkCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The interface name to remove.
+ [Parameter(Mandatory = true, Position = 1)]
+ public string Interface { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"{Interface} on {Node}", "Remove PVE Network Interface"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ client.DeleteAsync($"/nodes/{Node}/network/{Interface}").GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs
new file mode 100644
index 0000000..8a02602
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs
@@ -0,0 +1,30 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Removes an SDN VNet from Proxmox VE.
+ ///
+ /// Deletes the specified Software-Defined Networking VNet from the cluster SDN configuration.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveSdnVnet", SupportsShouldProcess = true)]
+ public class RemovePveSdnVnetCmdlet : PveCmdletBase
+ {
+ /// The VNet identifier to remove.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string Vnet { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Vnet, "Remove PVE SDN VNet"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ client.DeleteAsync($"/cluster/sdn/vnets/{Vnet}").GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs
new file mode 100644
index 0000000..6b3bb27
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs
@@ -0,0 +1,31 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Removes an SDN zone from Proxmox VE.
+ ///
+ /// Deletes the specified Software-Defined Networking zone from the cluster configuration.
+ /// All VNets within this zone must be removed first.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveSdnZone", SupportsShouldProcess = true)]
+ public class RemovePveSdnZoneCmdlet : PveCmdletBase
+ {
+ /// The zone identifier to remove.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string Zone { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Zone, "Remove PVE SDN Zone"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ client.DeleteAsync($"/cluster/sdn/zones/{Zone}").GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs
new file mode 100644
index 0000000..1c1d490
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Network
+{
+ ///
+ /// Updates a network interface configuration on a Proxmox VE node.
+ ///
+ /// Modifies the specified network interface on the node. After modifying, use
+ /// Invoke-PveNetworkApply to apply pending changes to the running system.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveNetwork", SupportsShouldProcess = true)]
+ public class SetPveNetworkCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The interface name to modify.
+ [Parameter(Mandatory = true, Position = 1)]
+ public string Interface { get; set; } = string.Empty;
+
+ /// IPv4 address for the interface.
+ [Parameter(Mandatory = false)]
+ public string? Address { get; set; }
+
+ /// IPv4 subnet mask.
+ [Parameter(Mandatory = false)]
+ public string? Netmask { get; set; }
+
+ /// IPv4 gateway address.
+ [Parameter(Mandatory = false)]
+ public string? Gateway { get; set; }
+
+ /// IPv6 address for the interface.
+ [Parameter(Mandatory = false)]
+ public string? Address6 { get; set; }
+
+ /// IPv6 prefix length.
+ [Parameter(Mandatory = false)]
+ public int? Netmask6 { get; set; }
+
+ /// IPv6 gateway address.
+ [Parameter(Mandatory = false)]
+ public string? Gateway6 { get; set; }
+
+ /// Bridge ports (space-separated interface names).
+ [Parameter(Mandatory = false)]
+ public string? BridgePorts { get; set; }
+
+ /// Bond slave interfaces (space-separated names).
+ [Parameter(Mandatory = false)]
+ public string? BondSlaves { get; set; }
+
+ /// MTU override.
+ [Parameter(Mandatory = false)]
+ public int? Mtu { get; set; }
+
+ /// Configure this interface to start automatically at boot.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Autostart { get; set; }
+
+ /// Optional comments/notes for this interface.
+ [Parameter(Mandatory = false)]
+ public string? Comments { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"{Interface} on {Node}", "Set PVE Network Interface"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary();
+
+ if (!string.IsNullOrEmpty(Address)) data["address"] = Address;
+ if (!string.IsNullOrEmpty(Netmask)) data["netmask"] = Netmask;
+ if (!string.IsNullOrEmpty(Gateway)) data["gateway"] = Gateway;
+ if (!string.IsNullOrEmpty(Address6)) data["address6"] = Address6;
+ if (Netmask6.HasValue) data["netmask6"] = Netmask6.Value.ToString();
+ if (!string.IsNullOrEmpty(Gateway6)) data["gateway6"] = Gateway6;
+ if (!string.IsNullOrEmpty(BridgePorts)) data["bridge_ports"] = BridgePorts;
+ if (!string.IsNullOrEmpty(BondSlaves)) data["slaves"] = BondSlaves;
+ if (Mtu.HasValue) data["mtu"] = Mtu.Value.ToString();
+ if (Autostart.IsPresent) data["autostart"] = "1";
+ if (!string.IsNullOrEmpty(Comments)) data["comments"] = Comments;
+
+ client.PutAsync($"/nodes/{Node}/network/{Interface}", data).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs
new file mode 100644
index 0000000..0ba4031
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs
@@ -0,0 +1,43 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Snapshots
+{
+ ///
+ /// Lists snapshots for a Proxmox VE virtual machine.
+ ///
+ /// Returns all snapshots for the specified VM on the given node.
+ /// VmId can be piped from Get-PveVm.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveSnapshot")]
+ [OutputType(typeof(PveSnapshot))]
+ public class GetPveSnapshotCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ ///
+ /// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ ///
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new SnapshotService();
+
+ var snapshots = service.GetSnapshots(session, Node, VmId);
+
+ foreach (var snapshot in snapshots)
+ {
+ snapshot.VmId = VmId;
+ snapshot.Node = Node;
+ WriteObject(snapshot);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs
new file mode 100644
index 0000000..1ecb71c
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Snapshots
+{
+ ///
+ /// Creates a snapshot of a Proxmox VE virtual machine.
+ ///
+ /// Takes a snapshot of the specified VM. Optionally includes the VM RAM state.
+ /// Returns a PveTask. Use -Wait to block until the snapshot completes.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveSnapshot", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class NewPveSnapshotCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// The snapshot name (alphanumeric, hyphens and underscores).
+ [Parameter(Mandatory = true, Position = 2)]
+ public string Name { get; set; } = string.Empty;
+
+ /// Optional human-readable description for the snapshot.
+ [Parameter(Mandatory = false)]
+ public string? Description { get; set; }
+
+ /// When specified, includes the VM memory state in the snapshot.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter IncludeVmState { get; set; }
+
+ /// When specified, waits for the snapshot task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} on {Node}", $"Create snapshot '{Name}'"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["snapname"] = Name
+ };
+ if (!string.IsNullOrEmpty(Description)) data["description"] = Description;
+ if (IncludeVmState.IsPresent) data["vmstate"] = "1";
+
+ var json = client.PostAsync($"/nodes/{Node}/qemu/{VmId}/snapshot", data).GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var upid = root["data"]?.ToString() ?? string.Empty;
+
+ var task = new PveTask { Upid = upid, Node = Node, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ task = WaitForTask(client, Node, upid);
+ }
+
+ WriteObject(task);
+ }
+
+ private static PveTask WaitForTask(PveHttpClient client, string node, string upid)
+ {
+ var encodedUpid = Uri.EscapeDataString(upid);
+ var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status";
+ while (true)
+ {
+ System.Threading.Thread.Sleep(2000);
+ var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult();
+ var statusRoot = JObject.Parse(statusJson);
+ var d = statusRoot["data"];
+ if (d?["status"]?.ToString() == "stopped")
+ return new PveTask { Upid = upid, Node = node, Status = "stopped", ExitStatus = d["exitstatus"]?.ToString() };
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs
new file mode 100644
index 0000000..52f3999
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Snapshots
+{
+ ///
+ /// Removes a snapshot from a Proxmox VE virtual machine.
+ ///
+ /// Deletes the specified snapshot from the VM. Snapshot name can be piped from Get-PveSnapshot.
+ /// Returns a PveTask. Use -Wait to block until removal completes.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveSnapshot", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class RemovePveSnapshotCmdlet : 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)]
+ public int VmId { get; set; }
+
+ ///
+ /// The snapshot name to remove. Accepts pipeline input from Get-PveSnapshot (PveSnapshot.Name).
+ ///
+ [Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true)]
+ public string Name { get; set; } = string.Empty;
+
+ /// When specified, waits for the removal task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} snapshot '{Name}' on {Node}", "Remove PVE Snapshot"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.DeleteAsync($"/nodes/{Node}/qemu/{VmId}/snapshot/{Name}").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var upid = root["data"]?.ToString() ?? string.Empty;
+
+ var task = new PveTask { Upid = upid, Node = Node, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ task = WaitForTask(client, Node, upid);
+ }
+
+ WriteObject(task);
+ }
+
+ private static PveTask WaitForTask(PveHttpClient client, string node, string upid)
+ {
+ var encodedUpid = Uri.EscapeDataString(upid);
+ var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status";
+ while (true)
+ {
+ System.Threading.Thread.Sleep(2000);
+ var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult();
+ var statusRoot = JObject.Parse(statusJson);
+ var d = statusRoot["data"];
+ if (d?["status"]?.ToString() == "stopped")
+ return new PveTask { Upid = upid, Node = node, Status = "stopped", ExitStatus = d["exitstatus"]?.ToString() };
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs
new file mode 100644
index 0000000..5925f91
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Snapshots
+{
+ ///
+ /// Rolls back a Proxmox VE virtual machine to a snapshot.
+ ///
+ /// Restores the VM state to the specified snapshot, discarding all changes made since
+ /// the snapshot was taken. This is a destructive operation — the VM's current state
+ /// will be lost. Returns a PveTask. Use -Wait to block until rollback completes.
+ ///
+ ///
+ [Cmdlet(VerbsData.Restore, "PveSnapshot",
+ SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(PveTask))]
+ public class RestorePveSnapshotCmdlet : 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)]
+ public int VmId { get; set; }
+
+ ///
+ /// The snapshot name to roll back to. Accepts pipeline input from Get-PveSnapshot (PveSnapshot.Name).
+ ///
+ [Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true)]
+ public string Name { get; set; } = string.Empty;
+
+ /// When specified, waits for the rollback task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} on {Node}", $"Restore snapshot '{Name}' (current state will be lost)"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.PostAsync($"/nodes/{Node}/qemu/{VmId}/snapshot/{Name}/rollback").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var upid = root["data"]?.ToString() ?? string.Empty;
+
+ var task = new PveTask { Upid = upid, Node = Node, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ task = WaitForTask(client, Node, upid);
+ }
+
+ WriteObject(task);
+ }
+
+ private static PveTask WaitForTask(PveHttpClient client, string node, string upid)
+ {
+ var encodedUpid = Uri.EscapeDataString(upid);
+ var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status";
+ while (true)
+ {
+ System.Threading.Thread.Sleep(2000);
+ var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult();
+ var statusRoot = JObject.Parse(statusJson);
+ var d = statusRoot["data"];
+ if (d?["status"]?.ToString() == "stopped")
+ return new PveTask { Upid = upid, Node = node, Status = "stopped", ExitStatus = d["exitstatus"]?.ToString() };
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskCmdlet.cs
new file mode 100644
index 0000000..e953709
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskCmdlet.cs
@@ -0,0 +1,35 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Tasks
+{
+ ///
+ /// Gets the status of a Proxmox VE task by UPID.
+ ///
+ /// Retrieves the current status and exit information for a Proxmox VE task identified
+ /// by its UPID (Unique Process Identifier). The Node must be the node that is running
+ /// or ran the task.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveTask")]
+ [OutputType(typeof(PveTask))]
+ public class GetPveTaskCmdlet : PveCmdletBase
+ {
+ /// The node on which the task ran.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The UPID of the task to query.
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public string Upid { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new TaskService();
+ var task = service.GetTask(session, Node, Upid);
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs
new file mode 100644
index 0000000..013477b
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Exceptions;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Tasks
+{
+ ///
+ /// Waits for a Proxmox VE task to complete.
+ ///
+ /// Polls the specified task until it reaches the "stopped" state or the timeout elapses.
+ /// Reports progress via Write-Progress. UPID can be piped from any cmdlet that returns
+ /// a PveTask (PveTask.Upid). Throws PveTaskFailedException if the task exits with an
+ /// error status, and PveTaskTimeoutException if the timeout elapses.
+ ///
+ ///
+ [Cmdlet(VerbsLifecycle.Wait, "PveTask")]
+ [OutputType(typeof(PveTask))]
+ public class WaitPveTaskCmdlet : PveCmdletBase
+ {
+ /// The node on which the task is running.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ ///
+ /// The UPID of the task to wait for. Accepts pipeline input from PveTask (PveTask.Upid).
+ ///
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public string Upid { get; set; } = string.Empty;
+
+ ///
+ /// Maximum time to wait for the task. Defaults to no timeout.
+ /// Example: -Timeout (New-TimeSpan -Minutes 10)
+ ///
+ [Parameter(Mandatory = false)]
+ public TimeSpan? Timeout { get; set; }
+
+ ///
+ /// How frequently to poll the task status. Defaults to 2 seconds.
+ /// Example: -PollInterval (New-TimeSpan -Seconds 5)
+ ///
+ [Parameter(Mandatory = false)]
+ public TimeSpan? PollInterval { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var poll = PollInterval ?? TimeSpan.FromSeconds(2);
+ var deadline = Timeout.HasValue ? DateTime.UtcNow + Timeout.Value : DateTime.MaxValue;
+
+ var encodedUpid = Uri.EscapeDataString(Upid);
+ var statusResource = $"/nodes/{Node}/tasks/{encodedUpid}/status";
+
+ // Derive a short human-readable description from the UPID for progress display
+ var taskDesc = Upid.Length > 50 ? Upid.Substring(0, 47) + "..." : Upid;
+
+ var activityId = Math.Abs(Upid.GetHashCode()) % 1000 + 1;
+ var progressRecord = new ProgressRecord(activityId, $"Waiting for task on {Node}", taskDesc)
+ {
+ PercentComplete = -1 // indeterminate
+ };
+
+ try
+ {
+ while (true)
+ {
+ if (DateTime.UtcNow >= deadline)
+ throw new PveTaskTimeoutException(Upid, Timeout!.Value);
+
+ WriteProgress(progressRecord);
+
+ var elapsed = Timeout.HasValue
+ ? (int)((DateTime.UtcNow - (deadline - Timeout.Value)).TotalSeconds)
+ : -1;
+ if (Timeout.HasValue && elapsed >= 0)
+ {
+ var totalSecs = (int)Timeout.Value.TotalSeconds;
+ progressRecord.PercentComplete = totalSecs > 0
+ ? Math.Min(99, (elapsed * 100) / totalSecs)
+ : -1;
+ progressRecord.SecondsRemaining = Math.Max(0, totalSecs - elapsed);
+ }
+
+ System.Threading.Thread.Sleep((int)poll.TotalMilliseconds);
+
+ var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult();
+ var statusRoot = JObject.Parse(statusJson);
+ var data = statusRoot["data"];
+
+ var status = data?["status"]?.ToString();
+ var exitStatus = data?["exitstatus"]?.ToString();
+
+ if (status == "stopped")
+ {
+ progressRecord.RecordType = ProgressRecordType.Completed;
+ WriteProgress(progressRecord);
+
+ var task = new PveTask
+ {
+ Upid = Upid,
+ Node = Node,
+ Status = status,
+ ExitStatus = exitStatus
+ };
+
+ if (!task.IsSuccessful && !string.IsNullOrEmpty(exitStatus) && exitStatus != "OK")
+ throw new PveTaskFailedException(Upid, exitStatus);
+
+ WriteObject(task);
+ return;
+ }
+ }
+ }
+ catch (PveTaskTimeoutException ex)
+ {
+ progressRecord.RecordType = ProgressRecordType.Completed;
+ WriteProgress(progressRecord);
+ ThrowTerminatingError(new ErrorRecord(
+ ex, "PveTaskTimeout", ErrorCategory.OperationTimeout, Upid));
+ }
+ catch (PveTaskFailedException ex)
+ {
+ ThrowTerminatingError(new ErrorRecord(
+ ex, "PveTaskFailed", ErrorCategory.OperationStopped, Upid));
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Templates/GetPveTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/GetPveTemplateCmdlet.cs
new file mode 100644
index 0000000..b9aab1f
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Templates/GetPveTemplateCmdlet.cs
@@ -0,0 +1,83 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Cmdlets.Templates
+{
+ ///
+ /// Lists VM templates on a Proxmox VE node.
+ ///
+ /// Returns QEMU virtual machines that are marked as templates on the specified node.
+ /// Optionally filter by template name pattern.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveTemplate")]
+ [OutputType(typeof(PveVm))]
+ public class GetPveTemplateCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name. When omitted, queries all nodes in the cluster.
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? Node { get; set; }
+
+ /// Filter results by template name. Supports wildcard (*) matching.
+ [Parameter(Mandatory = false)]
+ public string? Name { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var nodesToQuery = new System.Collections.Generic.List();
+
+ if (!string.IsNullOrEmpty(Node))
+ {
+ nodesToQuery.Add(Node);
+ }
+ else
+ {
+ var nodesJson = client.GetAsync("/nodes").GetAwaiter().GetResult();
+ var nodesRoot = JObject.Parse(nodesJson);
+ var nodesData = nodesRoot["data"] as JArray ?? new JArray();
+ foreach (var n in nodesData)
+ nodesToQuery.Add(n["node"]?.ToString() ?? string.Empty);
+ }
+
+ foreach (var node in nodesToQuery)
+ {
+ if (string.IsNullOrEmpty(node)) continue;
+
+ var json = client.GetAsync($"/nodes/{node}/qemu").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var vm = item.ToObject()!;
+ if (vm.Node == null) vm.Node = node;
+
+ // Only return templates
+ if (vm.Template != 1) continue;
+
+ if (!string.IsNullOrEmpty(Name) && vm.Name != null)
+ {
+ var pattern = Name.Replace("*", "");
+ if (Name.Contains("*"))
+ {
+ if (vm.Name.IndexOf(pattern, System.StringComparison.OrdinalIgnoreCase) < 0)
+ continue;
+ }
+ else
+ {
+ if (!string.Equals(vm.Name, Name, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+ }
+ }
+
+ WriteObject(vm);
+ }
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs
new file mode 100644
index 0000000..377edee
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs
@@ -0,0 +1,49 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Templates
+{
+ ///
+ /// Converts a Proxmox VE virtual machine to a template.
+ ///
+ /// Converts the specified VM into a template. This is a one-way operation — the VM
+ /// will no longer be directly runnable, but can be cloned via New-PveVmFromTemplate.
+ /// The VM must be stopped before converting. Returns a PveTask.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveTemplate", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class NewPveTemplateCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM identifier to convert. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// When specified, waits for the conversion task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"VM {VmId} on {Node}", "Convert to PVE Template (irreversible)"))
+ return;
+
+ var session = GetSession();
+ var service = new TemplateService();
+ var task = service.CreateTemplate(session, Node, VmId);
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid))
+ {
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, Node, task.Upid);
+ }
+
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs
new file mode 100644
index 0000000..c7246c4
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs
@@ -0,0 +1,80 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Templates
+{
+ ///
+ /// Clones a Proxmox VE VM template to create a new virtual machine.
+ ///
+ /// Creates a new VM by cloning the specified template. Supports both linked clones
+ /// (fast, space-efficient, requires template on shared storage) and full clones
+ /// (-Full switch). The source VM/template ID can be piped from Get-PveTemplate.
+ /// Returns a PveTask.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveVmFromTemplate", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveTask))]
+ public class NewPveVmFromTemplateCmdlet : PveCmdletBase
+ {
+ /// The node where the source template resides.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The source template VM ID. Accepts pipeline input from Get-PveTemplate (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// The VM ID for the new cloned VM.
+ [Parameter(Mandatory = true, Position = 2)]
+ public int NewVmId { get; set; }
+
+ /// Optional name for the new VM.
+ [Parameter(Mandatory = false)]
+ public string? NewName { get; set; }
+
+ ///
+ /// The target node for the new VM. When omitted, the new VM is created on the same node
+ /// as the template.
+ ///
+ [Parameter(Mandatory = false)]
+ public string? TargetNode { get; set; }
+
+ ///
+ /// When specified, creates a full clone (independent copy of all disks).
+ /// When omitted, creates a linked clone (shares base disk with template).
+ /// Linked clones require the template to reside on shared storage.
+ ///
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Full { get; set; }
+
+ /// When specified, waits for the clone task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"Template {VmId} -> new VM {NewVmId}", "Clone PVE VM from Template"))
+ return;
+
+ var session = GetSession();
+ var service = new VmService();
+ var task = service.CloneVm(
+ session,
+ Node,
+ VmId,
+ NewVmId,
+ name: NewName,
+ targetNode: TargetNode,
+ full: Full.IsPresent);
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid))
+ {
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, Node, task.Upid);
+ }
+
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Templates/RemovePveTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/RemovePveTemplateCmdlet.cs
new file mode 100644
index 0000000..edf58ca
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Templates/RemovePveTemplateCmdlet.cs
@@ -0,0 +1,54 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Templates
+{
+ ///
+ /// Removes a Proxmox VE VM template.
+ ///
+ /// Permanently deletes the specified VM template and all its associated disk images.
+ /// This action is irreversible. Returns a PveTask.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveTemplate",
+ SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(PveTask))]
+ public class RemovePveTemplateCmdlet : PveCmdletBase
+ {
+ /// The Proxmox VE node name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Node { get; set; } = string.Empty;
+
+ /// The VM/template identifier to remove. Accepts pipeline input from Get-PveVm (PveVm.VmId).
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public int VmId { get; set; }
+
+ /// When specified, also removes all associated backup files and jobs.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Purge { get; set; }
+
+ /// When specified, waits for the deletion task to complete before returning.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Wait { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"Template {VmId} on {Node}", "Remove PVE Template (all disks will be deleted)"))
+ return;
+
+ var session = GetSession();
+ var service = new TemplateService();
+ var task = service.RemoveTemplate(session, Node, VmId, Purge.IsPresent);
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid))
+ {
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, Node, task.Upid);
+ }
+
+ WriteObject(task);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs
new file mode 100644
index 0000000..b280d72
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Users;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Lists ACL entries (permissions) in Proxmox VE.
+ ///
+ /// Returns Access Control List entries from the Proxmox VE access management system.
+ /// Optionally filter by path or user/group ID.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PvePermission")]
+ [OutputType(typeof(PvePermission))]
+ public class GetPvePermissionCmdlet : PveCmdletBase
+ {
+ /// Filter results to a specific resource path (e.g., "/", "/vms/100").
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? Path { get; set; }
+
+ /// Filter results to a specific user or group ID.
+ [Parameter(Mandatory = false)]
+ public string? UgId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new UserService();
+
+ var permissions = service.GetPermissions(session);
+
+ foreach (var perm in permissions)
+ {
+ if (!string.IsNullOrEmpty(Path) &&
+ !string.Equals(perm.Path, Path, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ if (!string.IsNullOrEmpty(UgId) &&
+ !string.Equals(perm.UserId, UgId, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ WriteObject(perm);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveRoleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveRoleCmdlet.cs
new file mode 100644
index 0000000..879740a
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveRoleCmdlet.cs
@@ -0,0 +1,42 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Users;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Lists Proxmox VE roles.
+ ///
+ /// Returns role definitions from the Proxmox VE access management system.
+ /// Roles are named sets of privileges that can be assigned via ACLs.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveRole")]
+ [OutputType(typeof(PveRole))]
+ public class GetPveRoleCmdlet : PveCmdletBase
+ {
+ /// Optional role identifier to retrieve a specific role.
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? RoleId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.GetAsync("/access/roles").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var role = item.ToObject()!;
+ if (!string.IsNullOrEmpty(RoleId) &&
+ !string.Equals(role.RoleId, RoleId, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+ WriteObject(role);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs
new file mode 100644
index 0000000..d2b1f48
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs
@@ -0,0 +1,58 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Users;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Lists Proxmox VE user accounts.
+ ///
+ /// Returns user accounts from the Proxmox VE access management system.
+ /// Optionally filter by user ID (supports wildcard matching).
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveUser")]
+ [OutputType(typeof(PveUser))]
+ public class GetPveUserCmdlet : PveCmdletBase
+ {
+ ///
+ /// Filter results to a specific user ID or pattern (e.g., "admin@pam", "*@pve").
+ /// Supports wildcard (*) matching.
+ ///
+ [Parameter(Mandatory = false, Position = 0)]
+ public string? UserId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var json = client.GetAsync("/access/users").GetAwaiter().GetResult();
+ var root = JObject.Parse(json);
+ var data = root["data"] as JArray ?? new JArray();
+
+ foreach (var item in data)
+ {
+ var user = item.ToObject()!;
+
+ if (!string.IsNullOrEmpty(UserId))
+ {
+ var pattern = UserId.Replace("*", "");
+ if (UserId.Contains("*"))
+ {
+ if (user.UserId.IndexOf(pattern, System.StringComparison.OrdinalIgnoreCase) < 0)
+ continue;
+ }
+ else
+ {
+ if (!string.Equals(user.UserId, UserId, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+ }
+ }
+
+ WriteObject(user);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/NewPveRoleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/NewPveRoleCmdlet.cs
new file mode 100644
index 0000000..631b40d
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/NewPveRoleCmdlet.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+using PSProxmoxVE.Core.Models.Users;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Creates a new Proxmox VE role.
+ ///
+ /// Adds a new role to the Proxmox VE access management system with the specified privileges.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveRole", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveRole))]
+ public class NewPveRoleCmdlet : PveCmdletBase
+ {
+ /// The role identifier/name.
+ [Parameter(Mandatory = true, Position = 0)]
+ public string RoleId { get; set; } = string.Empty;
+
+ ///
+ /// Comma-separated list of privileges to grant this role
+ /// (e.g., "VM.Allocate,VM.Config.CPU,VM.Config.Memory").
+ ///
+ [Parameter(Mandatory = false, Position = 1)]
+ public string? Privileges { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(RoleId, "Create PVE Role"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["roleid"] = RoleId
+ };
+ if (!string.IsNullOrEmpty(Privileges)) data["privs"] = Privileges;
+
+ client.PostAsync("/access/roles", data).GetAwaiter().GetResult();
+
+ WriteObject(new PveRole { RoleId = RoleId, Privileges = Privileges });
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/NewPveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/NewPveUserCmdlet.cs
new file mode 100644
index 0000000..040e439
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/NewPveUserCmdlet.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Runtime.InteropServices;
+using PSProxmoxVE.Core.Models.Users;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Creates a new Proxmox VE user account.
+ ///
+ /// Adds a new user to the Proxmox VE access management system. The user ID must
+ /// include the realm (e.g., "newuser@pve" or "newuser@pam").
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveUser", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveUser))]
+ public class NewPveUserCmdlet : PveCmdletBase
+ {
+ /// The user identifier in "user@realm" format (e.g., "jdoe@pve").
+ [Parameter(Mandatory = true, Position = 0)]
+ public string UserId { get; set; } = string.Empty;
+
+ /// The user's password (for pve/pam realms). Accepts a SecureString.
+ [Parameter(Mandatory = false)]
+ public System.Security.SecureString? Password { get; set; }
+
+ /// The user's first name.
+ [Parameter(Mandatory = false)]
+ public string? FirstName { get; set; }
+
+ /// The user's last name.
+ [Parameter(Mandatory = false)]
+ public string? LastName { get; set; }
+
+ /// The user's email address.
+ [Parameter(Mandatory = false)]
+ public string? Email { get; set; }
+
+ /// Comma-separated list of groups to add this user to.
+ [Parameter(Mandatory = false)]
+ public string? Groups { get; set; }
+
+ /// Optional comment/notes for this user.
+ [Parameter(Mandatory = false)]
+ public string? Comment { get; set; }
+
+ /// Account expiry as a Unix timestamp. Use 0 for no expiry.
+ [Parameter(Mandatory = false)]
+ public long? Expire { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(UserId, "Create PVE User"))
+ return;
+
+ var config = new Dictionary();
+
+ if (Password != null)
+ {
+ var ptr = Marshal.SecureStringToGlobalAllocUnicode(Password);
+ try { config["password"] = Marshal.PtrToStringUni(ptr) ?? string.Empty; }
+ finally { Marshal.ZeroFreeGlobalAllocUnicode(ptr); }
+ }
+
+ if (!string.IsNullOrEmpty(FirstName)) config["firstname"] = FirstName;
+ if (!string.IsNullOrEmpty(LastName)) config["lastname"] = LastName;
+ if (!string.IsNullOrEmpty(Email)) config["email"] = Email;
+ if (!string.IsNullOrEmpty(Groups)) config["groups"] = Groups;
+ if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment;
+ if (Expire.HasValue) config["expire"] = Expire.Value;
+
+ var session = GetSession();
+ var service = new UserService();
+ service.CreateUser(session, UserId, config);
+
+ WriteObject(new PveUser
+ {
+ UserId = UserId,
+ FirstName = FirstName,
+ LastName = LastName,
+ Email = Email,
+ Groups = Groups,
+ Comment = Comment,
+ Expire = Expire,
+ Enabled = 1
+ });
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveRoleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveRoleCmdlet.cs
new file mode 100644
index 0000000..98aaa0d
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveRoleCmdlet.cs
@@ -0,0 +1,30 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Removes a Proxmox VE role.
+ ///
+ /// Deletes the specified custom role from the Proxmox VE access management system.
+ /// Built-in roles cannot be removed.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveRole", SupportsShouldProcess = true)]
+ public class RemovePveRoleCmdlet : PveCmdletBase
+ {
+ /// The role identifier to remove.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string RoleId { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(RoleId, "Remove PVE Role"))
+ return;
+
+ var session = GetSession();
+ var service = new UserService();
+ service.RemoveRole(session, RoleId);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs
new file mode 100644
index 0000000..c6e6c03
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs
@@ -0,0 +1,29 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Removes a Proxmox VE user account.
+ ///
+ /// Deletes the specified user from the Proxmox VE access management system.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveUser", SupportsShouldProcess = true)]
+ public class RemovePveUserCmdlet : PveCmdletBase
+ {
+ /// The user identifier to remove (e.g., "jdoe@pve").
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string UserId { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(UserId, "Remove PVE User"))
+ return;
+
+ var session = GetSession();
+ var service = new UserService();
+ service.RemoveUser(session, UserId);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs
new file mode 100644
index 0000000..7ba5329
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Sets or updates ACL entries (permissions) in Proxmox VE.
+ ///
+ /// Adds or modifies Access Control List entries in the Proxmox VE access management system.
+ /// To remove a permission, use the -Delete switch.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PvePermission", SupportsShouldProcess = true)]
+ public class SetPvePermissionCmdlet : PveCmdletBase
+ {
+ /// The resource path this ACL applies to (e.g., "/", "/vms/100").
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Path { get; set; } = string.Empty;
+
+ /// The user or group identifier (e.g., "jdoe@pve" or "admins").
+ [Parameter(Mandatory = true, Position = 1)]
+ public string UgId { get; set; } = string.Empty;
+
+ /// The role to assign (e.g., "Administrator", "PVEVMUser").
+ [Parameter(Mandatory = true, Position = 2)]
+ public string RoleId { get; set; } = string.Empty;
+
+ /// The ACL entry type: "user" or "group".
+ [Parameter(Mandatory = false)]
+ [ValidateSet("user", "group", IgnoreCase = true)]
+ public string Type { get; set; } = "user";
+
+ /// Whether to propagate this ACL to child paths.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Propagate { get; set; }
+
+ /// When specified, removes the ACL entry instead of adding it.
+ [Parameter(Mandatory = false)]
+ public SwitchParameter Delete { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var action = Delete.IsPresent ? "Remove" : "Set";
+ if (!ShouldProcess($"{Type} '{UgId}' at '{Path}'", $"{action} PVE Permission ({RoleId})"))
+ return;
+
+ var session = GetSession();
+ using var client = new PveHttpClient(session);
+
+ var data = new Dictionary
+ {
+ ["path"] = Path,
+ ["roles"] = RoleId
+ };
+
+ if (string.Equals(Type, "group", System.StringComparison.OrdinalIgnoreCase))
+ data["groups"] = UgId;
+ else
+ data["users"] = UgId;
+
+ if (Propagate.IsPresent) data["propagate"] = "1";
+ if (Delete.IsPresent) data["delete"] = "1";
+
+ client.PutAsync("/access/acl", data).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPveUserCmdlet.cs
new file mode 100644
index 0000000..2260d2e
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/SetPveUserCmdlet.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Runtime.InteropServices;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Updates a Proxmox VE user account.
+ ///
+ /// Modifies properties of an existing Proxmox VE user account. Only specified parameters
+ /// are updated; omitted parameters retain their current values.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveUser", SupportsShouldProcess = true)]
+ public class SetPveUserCmdlet : PveCmdletBase
+ {
+ /// The user identifier to update (e.g., "jdoe@pve").
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string UserId { get; set; } = string.Empty;
+
+ /// New password for the user. Accepts a SecureString.
+ [Parameter(Mandatory = false)]
+ public System.Security.SecureString? Password { get; set; }
+
+ /// Updated first name.
+ [Parameter(Mandatory = false)]
+ public string? FirstName { get; set; }
+
+ /// Updated last name.
+ [Parameter(Mandatory = false)]
+ public string? LastName { get; set; }
+
+ /// Updated email address.
+ [Parameter(Mandatory = false)]
+ public string? Email { get; set; }
+
+ /// Updated group membership (comma-separated group names).
+ [Parameter(Mandatory = false)]
+ public string? Groups { get; set; }
+
+ /// Updated comment/notes.
+ [Parameter(Mandatory = false)]
+ public string? Comment { get; set; }
+
+ /// Account expiry as a Unix timestamp. Use 0 to remove expiry.
+ [Parameter(Mandatory = false)]
+ public long? Expire { get; set; }
+
+ /// Enable or disable the user account.
+ [Parameter(Mandatory = false)]
+ public bool? Enable { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(UserId, "Set PVE User"))
+ return;
+
+ var config = new Dictionary();
+
+ if (Password != null)
+ {
+ var ptr = Marshal.SecureStringToGlobalAllocUnicode(Password);
+ try { config["password"] = Marshal.PtrToStringUni(ptr) ?? string.Empty; }
+ finally { Marshal.ZeroFreeGlobalAllocUnicode(ptr); }
+ }
+
+ if (!string.IsNullOrEmpty(FirstName)) config["firstname"] = FirstName;
+ if (!string.IsNullOrEmpty(LastName)) config["lastname"] = LastName;
+ if (!string.IsNullOrEmpty(Email)) config["email"] = Email;
+ if (!string.IsNullOrEmpty(Groups)) config["groups"] = Groups;
+ if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment;
+ if (Expire.HasValue) config["expire"] = Expire.Value;
+ if (Enable.HasValue) config["enable"] = Enable.Value ? "1" : "0";
+
+ var session = GetSession();
+ var service = new UserService();
+ service.SetUser(session, UserId, config);
+ }
+ }
+}