feat(cmdlets): implement snapshot, network, SDN, user, template, cloud-init cmdlets

Add cmdlets for snapshots (Get/New/Remove/Restore-PveSnapshot), network
(Get/New/Set/Remove-PveNetwork, Invoke-PveNetworkApply), SDN zones and
VNets (PVE 8.0+ version guard), users/roles/permissions, templates
(Get/New/Remove-PveTemplate, New-PveVmFromTemplate), cloud-init config,
and task management (Get-PveTask, Wait-PveTask).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-17 15:46:38 -05:00
parent 0c11bc4b1b
commit 76c2ad4946
33 changed files with 2042 additions and 0 deletions
@@ -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
{
/// <summary>
/// <para type="synopsis">Retrieves cloud-init configuration for a Proxmox VE virtual machine.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveCloudInitConfig")]
[OutputType(typeof(PveVmConfig))]
public class GetPveCloudInitConfigCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[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<PveVmConfig>() ?? new PveVmConfig();
WriteObject(config);
}
}
}
@@ -0,0 +1,46 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.CloudInit
{
/// <summary>
/// <para type="synopsis">Regenerates the cloud-init drive on a Proxmox VE virtual machine.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "PveCloudInitRegenerate", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class InvokePveCloudInitRegenerateCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[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);
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Sets cloud-init configuration on a Proxmox VE virtual machine.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveCloudInitConfig", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class SetPveCloudInitConfigCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public int VmId { get; set; }
/// <summary>Cloud-init hostname override.</summary>
[Parameter(Mandatory = false)]
public string? Hostname { get; set; }
/// <summary>Cloud-init default username.</summary>
[Parameter(Mandatory = false)]
public string? User { get; set; }
/// <summary>Cloud-init default user password. Accepts a SecureString.</summary>
[Parameter(Mandatory = false)]
public System.Security.SecureString? Password { get; set; }
/// <summary>SSH public keys to inject (one key per element).</summary>
[Parameter(Mandatory = false)]
public string[]? SshKeys { get; set; }
/// <summary>
/// IP configuration string (e.g., "ip=192.168.1.50/24,gw=192.168.1.1").
/// Maps to ipconfig0 on the VM.
/// </summary>
[Parameter(Mandatory = false)]
public string? IpConfig { get; set; }
/// <summary>DNS nameserver(s) to inject (space or comma separated).</summary>
[Parameter(Mandatory = false)]
public string? Dns { get; set; }
/// <summary>DNS search domain to inject.</summary>
[Parameter(Mandatory = false)]
public string? SearchDomain { get; set; }
/// <summary>When specified, waits for the config update task to complete before returning.</summary>
[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<string, object>();
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);
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists network interfaces configured on a Proxmox VE node.</para>
/// <para type="description">
/// Returns network interface definitions from the specified node.
/// Optionally filter by interface type. Node accepts pipeline input from Get-PveNode.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveNetwork")]
[OutputType(typeof(PveNetwork))]
public class GetPveNetworkCmdlet : PveCmdletBase
{
/// <summary>
/// The Proxmox VE node name. Accepts pipeline input from Get-PveNode (PveNode.Name).
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("NodeName")]
public string Node { get; set; } = string.Empty;
/// <summary>Filter by interface type (e.g., "bridge", "bond", "eth", "vlan").</summary>
[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<PveNetwork>()!;
network.Node = Node;
WriteObject(network);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists SDN VNets defined in Proxmox VE.</para>
/// <para type="description">
/// Returns Software-Defined Networking VNet definitions from the cluster SDN configuration.
/// Optionally filter by zone.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSdnVnet")]
[OutputType(typeof(PveSdnVnet))]
public class GetPveSdnVnetCmdlet : PveCmdletBase
{
/// <summary>Filter VNets to a specific zone.</summary>
[Parameter(Mandatory = false, Position = 0)]
public string? Zone { get; set; }
/// <summary>Optional VNet identifier to retrieve a specific VNet.</summary>
[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<PveSdnVnet>()!;
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);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists SDN zones defined in Proxmox VE.</para>
/// <para type="description">
/// Returns Software-Defined Networking zone definitions from the cluster SDN configuration.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSdnZone")]
[OutputType(typeof(PveSdnZone))]
public class GetPveSdnZoneCmdlet : PveCmdletBase
{
/// <summary>Optional zone identifier to retrieve a specific zone.</summary>
[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<PveSdnZone>()!;
if (!string.IsNullOrEmpty(Zone) &&
!string.Equals(zone.Zone, Zone, System.StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(zone);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Applies pending network configuration changes on a Proxmox VE node.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "PveNetworkApply", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class InvokePveNetworkApplyCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node on which to apply network changes.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>When specified, waits for the apply task to complete before returning.</summary>
[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() };
}
}
}
}
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new network interface on a Proxmox VE node.</para>
/// <para type="description">
/// Adds a network interface definition to the specified node. After creation, use
/// Invoke-PveNetworkApply to apply pending changes to the running system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveNetwork", SupportsShouldProcess = true)]
public class NewPveNetworkCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The interface name (e.g., "vmbr1", "bond0").</summary>
[Parameter(Mandatory = true, Position = 1)]
public string Interface { get; set; } = string.Empty;
/// <summary>The interface type.</summary>
[Parameter(Mandatory = true, Position = 2)]
[ValidateSet("bridge", "bond", "eth", "alias", "vlan", "OVSBridge", "OVSBond",
"OVSPort", "OVSIntPort", IgnoreCase = true)]
public string Type { get; set; } = string.Empty;
/// <summary>IPv4 address for the interface.</summary>
[Parameter(Mandatory = false)]
public string? Address { get; set; }
/// <summary>IPv4 subnet mask.</summary>
[Parameter(Mandatory = false)]
public string? Netmask { get; set; }
/// <summary>IPv4 gateway address.</summary>
[Parameter(Mandatory = false)]
public string? Gateway { get; set; }
/// <summary>Bridge ports (space-separated interface names, for bridge type).</summary>
[Parameter(Mandatory = false)]
public string? BridgePorts { get; set; }
/// <summary>Bond slave interfaces (space-separated names, for bond type).</summary>
[Parameter(Mandatory = false)]
public string? BondSlaves { get; set; }
/// <summary>VLAN tag ID (for vlan type).</summary>
[Parameter(Mandatory = false)]
public int? VlanId { get; set; }
/// <summary>MTU override.</summary>
[Parameter(Mandatory = false)]
public int? Mtu { get; set; }
/// <summary>Configure this interface to start automatically at boot.</summary>
[Parameter(Mandatory = false)]
public SwitchParameter Autostart { get; set; }
/// <summary>Optional comments/notes for this interface.</summary>
[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<string, string>
{
["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();
}
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new SDN VNet in Proxmox VE.</para>
/// <para type="description">
/// Adds a new Software-Defined Networking VNet (virtual network) to the specified SDN zone.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSdnVnet", SupportsShouldProcess = true)]
public class NewPveSdnVnetCmdlet : PveCmdletBase
{
/// <summary>The VNet identifier (alphanumeric, up to 8 characters).</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Vnet { get; set; } = string.Empty;
/// <summary>The SDN zone this VNet belongs to.</summary>
[Parameter(Mandatory = true, Position = 1)]
public string Zone { get; set; } = string.Empty;
/// <summary>VLAN tag for VLAN-type zones.</summary>
[Parameter(Mandatory = false)]
public int? Tag { get; set; }
/// <summary>Optional alias/description for the VNet.</summary>
[Parameter(Mandatory = false)]
public string? Alias { get; set; }
/// <summary>Enable VLAN awareness on this VNet.</summary>
[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<string, string>
{
["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();
}
}
}
@@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new SDN zone in Proxmox VE.</para>
/// <para type="description">
/// Adds a new Software-Defined Networking zone to the Proxmox VE cluster configuration.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSdnZone", SupportsShouldProcess = true)]
public class NewPveSdnZoneCmdlet : PveCmdletBase
{
/// <summary>The zone identifier (alphanumeric, hyphens allowed).</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Zone { get; set; } = string.Empty;
/// <summary>The zone type.</summary>
[Parameter(Mandatory = true, Position = 1)]
[ValidateSet("vlan", "vxlan", "evpn", "simple", "qinq", IgnoreCase = true)]
public string Type { get; set; } = string.Empty;
/// <summary>VXLAN peer list or multicast address (for vxlan/evpn types).</summary>
[Parameter(Mandatory = false)]
public string? Peers { get; set; }
/// <summary>Bridge interface this zone attaches to (for vlan/qinq types).</summary>
[Parameter(Mandatory = false)]
public string? Bridge { get; set; }
/// <summary>MTU override for this zone.</summary>
[Parameter(Mandatory = false)]
public int? Mtu { get; set; }
/// <summary>DNS server for automatic DNS registration.</summary>
[Parameter(Mandatory = false)]
public string? Dns { get; set; }
/// <summary>Reverse DNS server.</summary>
[Parameter(Mandatory = false)]
public string? ReverseDns { get; set; }
/// <summary>DNS zone name for registration.</summary>
[Parameter(Mandatory = false)]
public string? DnsZone { get; set; }
/// <summary>IPAM plugin to use for this zone.</summary>
[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<string, string>
{
["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();
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes a network interface definition from a Proxmox VE node.</para>
/// <para type="description">
/// Deletes the specified network interface configuration from the node. After removing,
/// use Invoke-PveNetworkApply to apply pending changes to the running system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveNetwork", SupportsShouldProcess = true)]
public class RemovePveNetworkCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The interface name to remove.</summary>
[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();
}
}
}
@@ -0,0 +1,30 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes an SDN VNet from Proxmox VE.</para>
/// <para type="description">
/// Deletes the specified Software-Defined Networking VNet from the cluster SDN configuration.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSdnVnet", SupportsShouldProcess = true)]
public class RemovePveSdnVnetCmdlet : PveCmdletBase
{
/// <summary>The VNet identifier to remove.</summary>
[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();
}
}
}
@@ -0,0 +1,31 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes an SDN zone from Proxmox VE.</para>
/// <para type="description">
/// Deletes the specified Software-Defined Networking zone from the cluster configuration.
/// All VNets within this zone must be removed first.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSdnZone", SupportsShouldProcess = true)]
public class RemovePveSdnZoneCmdlet : PveCmdletBase
{
/// <summary>The zone identifier to remove.</summary>
[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();
}
}
}
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates a network interface configuration on a Proxmox VE node.</para>
/// <para type="description">
/// Modifies the specified network interface on the node. After modifying, use
/// Invoke-PveNetworkApply to apply pending changes to the running system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveNetwork", SupportsShouldProcess = true)]
public class SetPveNetworkCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The interface name to modify.</summary>
[Parameter(Mandatory = true, Position = 1)]
public string Interface { get; set; } = string.Empty;
/// <summary>IPv4 address for the interface.</summary>
[Parameter(Mandatory = false)]
public string? Address { get; set; }
/// <summary>IPv4 subnet mask.</summary>
[Parameter(Mandatory = false)]
public string? Netmask { get; set; }
/// <summary>IPv4 gateway address.</summary>
[Parameter(Mandatory = false)]
public string? Gateway { get; set; }
/// <summary>IPv6 address for the interface.</summary>
[Parameter(Mandatory = false)]
public string? Address6 { get; set; }
/// <summary>IPv6 prefix length.</summary>
[Parameter(Mandatory = false)]
public int? Netmask6 { get; set; }
/// <summary>IPv6 gateway address.</summary>
[Parameter(Mandatory = false)]
public string? Gateway6 { get; set; }
/// <summary>Bridge ports (space-separated interface names).</summary>
[Parameter(Mandatory = false)]
public string? BridgePorts { get; set; }
/// <summary>Bond slave interfaces (space-separated names).</summary>
[Parameter(Mandatory = false)]
public string? BondSlaves { get; set; }
/// <summary>MTU override.</summary>
[Parameter(Mandatory = false)]
public int? Mtu { get; set; }
/// <summary>Configure this interface to start automatically at boot.</summary>
[Parameter(Mandatory = false)]
public SwitchParameter Autostart { get; set; }
/// <summary>Optional comments/notes for this interface.</summary>
[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<string, string>();
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();
}
}
}
@@ -0,0 +1,43 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Snapshots
{
/// <summary>
/// <para type="synopsis">Lists snapshots for a Proxmox VE virtual machine.</para>
/// <para type="description">
/// Returns all snapshots for the specified VM on the given node.
/// VmId can be piped from Get-PveVm.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSnapshot")]
[OutputType(typeof(PveSnapshot))]
public class GetPveSnapshotCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>
/// The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).
/// </summary>
[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);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Creates a snapshot of a Proxmox VE virtual machine.</para>
/// <para type="description">
/// Takes a snapshot of the specified VM. Optionally includes the VM RAM state.
/// Returns a PveTask. Use -Wait to block until the snapshot completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSnapshot", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class NewPveSnapshotCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public int VmId { get; set; }
/// <summary>The snapshot name (alphanumeric, hyphens and underscores).</summary>
[Parameter(Mandatory = true, Position = 2)]
public string Name { get; set; } = string.Empty;
/// <summary>Optional human-readable description for the snapshot.</summary>
[Parameter(Mandatory = false)]
public string? Description { get; set; }
/// <summary>When specified, includes the VM memory state in the snapshot.</summary>
[Parameter(Mandatory = false)]
public SwitchParameter IncludeVmState { get; set; }
/// <summary>When specified, waits for the snapshot task to complete before returning.</summary>
[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<string, string>
{
["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() };
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Removes a snapshot from a Proxmox VE virtual machine.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSnapshot", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class RemovePveSnapshotCmdlet : 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)]
public int VmId { get; set; }
/// <summary>
/// The snapshot name to remove. Accepts pipeline input from Get-PveSnapshot (PveSnapshot.Name).
/// </summary>
[Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = string.Empty;
/// <summary>When specified, waits for the removal task to complete before returning.</summary>
[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() };
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Rolls back a Proxmox VE virtual machine to a snapshot.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsData.Restore, "PveSnapshot",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(PveTask))]
public class RestorePveSnapshotCmdlet : 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)]
public int VmId { get; set; }
/// <summary>
/// The snapshot name to roll back to. Accepts pipeline input from Get-PveSnapshot (PveSnapshot.Name).
/// </summary>
[Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = string.Empty;
/// <summary>When specified, waits for the rollback task to complete before returning.</summary>
[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() };
}
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Tasks
{
/// <summary>
/// <para type="synopsis">Gets the status of a Proxmox VE task by UPID.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveTask")]
[OutputType(typeof(PveTask))]
public class GetPveTaskCmdlet : PveCmdletBase
{
/// <summary>The node on which the task ran.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The UPID of the task to query.</summary>
[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);
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Waits for a Proxmox VE task to complete.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Wait, "PveTask")]
[OutputType(typeof(PveTask))]
public class WaitPveTaskCmdlet : PveCmdletBase
{
/// <summary>The node on which the task is running.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>
/// The UPID of the task to wait for. Accepts pipeline input from PveTask (PveTask.Upid).
/// </summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public string Upid { get; set; } = string.Empty;
/// <summary>
/// Maximum time to wait for the task. Defaults to no timeout.
/// Example: -Timeout (New-TimeSpan -Minutes 10)
/// </summary>
[Parameter(Mandatory = false)]
public TimeSpan? Timeout { get; set; }
/// <summary>
/// How frequently to poll the task status. Defaults to 2 seconds.
/// Example: -PollInterval (New-TimeSpan -Seconds 5)
/// </summary>
[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));
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists VM templates on a Proxmox VE node.</para>
/// <para type="description">
/// Returns QEMU virtual machines that are marked as templates on the specified node.
/// Optionally filter by template name pattern.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveTemplate")]
[OutputType(typeof(PveVm))]
public class GetPveTemplateCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name. When omitted, queries all nodes in the cluster.</summary>
[Parameter(Mandatory = false, Position = 0)]
public string? Node { get; set; }
/// <summary>Filter results by template name. Supports wildcard (*) matching.</summary>
[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<string>();
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<PveVm>()!;
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);
}
}
}
}
}
@@ -0,0 +1,49 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Templates
{
/// <summary>
/// <para type="synopsis">Converts a Proxmox VE virtual machine to a template.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveTemplate", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class NewPveTemplateCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier to convert. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public int VmId { get; set; }
/// <summary>When specified, waits for the conversion task to complete before returning.</summary>
[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);
}
}
}
@@ -0,0 +1,80 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Templates
{
/// <summary>
/// <para type="synopsis">Clones a Proxmox VE VM template to create a new virtual machine.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveVmFromTemplate", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class NewPveVmFromTemplateCmdlet : PveCmdletBase
{
/// <summary>The node where the source template resides.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The source template VM ID. Accepts pipeline input from Get-PveTemplate (PveVm.VmId).</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public int VmId { get; set; }
/// <summary>The VM ID for the new cloned VM.</summary>
[Parameter(Mandatory = true, Position = 2)]
public int NewVmId { get; set; }
/// <summary>Optional name for the new VM.</summary>
[Parameter(Mandatory = false)]
public string? NewName { get; set; }
/// <summary>
/// The target node for the new VM. When omitted, the new VM is created on the same node
/// as the template.
/// </summary>
[Parameter(Mandatory = false)]
public string? TargetNode { get; set; }
/// <summary>
/// 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.
/// </summary>
[Parameter(Mandatory = false)]
public SwitchParameter Full { get; set; }
/// <summary>When specified, waits for the clone task to complete before returning.</summary>
[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);
}
}
}
@@ -0,0 +1,54 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Templates
{
/// <summary>
/// <para type="synopsis">Removes a Proxmox VE VM template.</para>
/// <para type="description">
/// Permanently deletes the specified VM template and all its associated disk images.
/// This action is irreversible. Returns a PveTask.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveTemplate",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(PveTask))]
public class RemovePveTemplateCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Node { get; set; } = string.Empty;
/// <summary>The VM/template identifier to remove. Accepts pipeline input from Get-PveVm (PveVm.VmId).</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
public int VmId { get; set; }
/// <summary>When specified, also removes all associated backup files and jobs.</summary>
[Parameter(Mandatory = false)]
public SwitchParameter Purge { get; set; }
/// <summary>When specified, waits for the deletion task to complete before returning.</summary>
[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);
}
}
}
@@ -0,0 +1,48 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Users;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Lists ACL entries (permissions) in Proxmox VE.</para>
/// <para type="description">
/// Returns Access Control List entries from the Proxmox VE access management system.
/// Optionally filter by path or user/group ID.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PvePermission")]
[OutputType(typeof(PvePermission))]
public class GetPvePermissionCmdlet : PveCmdletBase
{
/// <summary>Filter results to a specific resource path (e.g., "/", "/vms/100").</summary>
[Parameter(Mandatory = false, Position = 0)]
public string? Path { get; set; }
/// <summary>Filter results to a specific user or group ID.</summary>
[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);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE roles.</para>
/// <para type="description">
/// Returns role definitions from the Proxmox VE access management system.
/// Roles are named sets of privileges that can be assigned via ACLs.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveRole")]
[OutputType(typeof(PveRole))]
public class GetPveRoleCmdlet : PveCmdletBase
{
/// <summary>Optional role identifier to retrieve a specific role.</summary>
[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<PveRole>()!;
if (!string.IsNullOrEmpty(RoleId) &&
!string.Equals(role.RoleId, RoleId, System.StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(role);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE user accounts.</para>
/// <para type="description">
/// Returns user accounts from the Proxmox VE access management system.
/// Optionally filter by user ID (supports wildcard matching).
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveUser")]
[OutputType(typeof(PveUser))]
public class GetPveUserCmdlet : PveCmdletBase
{
/// <summary>
/// Filter results to a specific user ID or pattern (e.g., "admin@pam", "*@pve").
/// Supports wildcard (*) matching.
/// </summary>
[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<PveUser>()!;
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);
}
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Creates a new Proxmox VE role.</para>
/// <para type="description">
/// Adds a new role to the Proxmox VE access management system with the specified privileges.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveRole", SupportsShouldProcess = true)]
[OutputType(typeof(PveRole))]
public class NewPveRoleCmdlet : PveCmdletBase
{
/// <summary>The role identifier/name.</summary>
[Parameter(Mandatory = true, Position = 0)]
public string RoleId { get; set; } = string.Empty;
/// <summary>
/// Comma-separated list of privileges to grant this role
/// (e.g., "VM.Allocate,VM.Config.CPU,VM.Config.Memory").
/// </summary>
[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<string, string>
{
["roleid"] = RoleId
};
if (!string.IsNullOrEmpty(Privileges)) data["privs"] = Privileges;
client.PostAsync("/access/roles", data).GetAwaiter().GetResult();
WriteObject(new PveRole { RoleId = RoleId, Privileges = Privileges });
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Creates a new Proxmox VE user account.</para>
/// <para type="description">
/// 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").
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveUser", SupportsShouldProcess = true)]
[OutputType(typeof(PveUser))]
public class NewPveUserCmdlet : PveCmdletBase
{
/// <summary>The user identifier in "user@realm" format (e.g., "jdoe@pve").</summary>
[Parameter(Mandatory = true, Position = 0)]
public string UserId { get; set; } = string.Empty;
/// <summary>The user's password (for pve/pam realms). Accepts a SecureString.</summary>
[Parameter(Mandatory = false)]
public System.Security.SecureString? Password { get; set; }
/// <summary>The user's first name.</summary>
[Parameter(Mandatory = false)]
public string? FirstName { get; set; }
/// <summary>The user's last name.</summary>
[Parameter(Mandatory = false)]
public string? LastName { get; set; }
/// <summary>The user's email address.</summary>
[Parameter(Mandatory = false)]
public string? Email { get; set; }
/// <summary>Comma-separated list of groups to add this user to.</summary>
[Parameter(Mandatory = false)]
public string? Groups { get; set; }
/// <summary>Optional comment/notes for this user.</summary>
[Parameter(Mandatory = false)]
public string? Comment { get; set; }
/// <summary>Account expiry as a Unix timestamp. Use 0 for no expiry.</summary>
[Parameter(Mandatory = false)]
public long? Expire { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(UserId, "Create PVE User"))
return;
var config = new Dictionary<string, object>();
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
});
}
}
}
@@ -0,0 +1,30 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Removes a Proxmox VE role.</para>
/// <para type="description">
/// Deletes the specified custom role from the Proxmox VE access management system.
/// Built-in roles cannot be removed.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveRole", SupportsShouldProcess = true)]
public class RemovePveRoleCmdlet : PveCmdletBase
{
/// <summary>The role identifier to remove.</summary>
[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);
}
}
}
@@ -0,0 +1,29 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Removes a Proxmox VE user account.</para>
/// <para type="description">
/// Deletes the specified user from the Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveUser", SupportsShouldProcess = true)]
public class RemovePveUserCmdlet : PveCmdletBase
{
/// <summary>The user identifier to remove (e.g., "jdoe@pve").</summary>
[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);
}
}
}
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Sets or updates ACL entries (permissions) in Proxmox VE.</para>
/// <para type="description">
/// Adds or modifies Access Control List entries in the Proxmox VE access management system.
/// To remove a permission, use the -Delete switch.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PvePermission", SupportsShouldProcess = true)]
public class SetPvePermissionCmdlet : PveCmdletBase
{
/// <summary>The resource path this ACL applies to (e.g., "/", "/vms/100").</summary>
[Parameter(Mandatory = true, Position = 0)]
public string Path { get; set; } = string.Empty;
/// <summary>The user or group identifier (e.g., "jdoe@pve" or "admins").</summary>
[Parameter(Mandatory = true, Position = 1)]
public string UgId { get; set; } = string.Empty;
/// <summary>The role to assign (e.g., "Administrator", "PVEVMUser").</summary>
[Parameter(Mandatory = true, Position = 2)]
public string RoleId { get; set; } = string.Empty;
/// <summary>The ACL entry type: "user" or "group".</summary>
[Parameter(Mandatory = false)]
[ValidateSet("user", "group", IgnoreCase = true)]
public string Type { get; set; } = "user";
/// <summary>Whether to propagate this ACL to child paths.</summary>
[Parameter(Mandatory = false)]
public SwitchParameter Propagate { get; set; }
/// <summary>When specified, removes the ACL entry instead of adding it.</summary>
[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<string, string>
{
["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();
}
}
}
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Management.Automation;
using System.Runtime.InteropServices;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE user account.</para>
/// <para type="description">
/// Modifies properties of an existing Proxmox VE user account. Only specified parameters
/// are updated; omitted parameters retain their current values.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveUser", SupportsShouldProcess = true)]
public class SetPveUserCmdlet : PveCmdletBase
{
/// <summary>The user identifier to update (e.g., "jdoe@pve").</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
public string UserId { get; set; } = string.Empty;
/// <summary>New password for the user. Accepts a SecureString.</summary>
[Parameter(Mandatory = false)]
public System.Security.SecureString? Password { get; set; }
/// <summary>Updated first name.</summary>
[Parameter(Mandatory = false)]
public string? FirstName { get; set; }
/// <summary>Updated last name.</summary>
[Parameter(Mandatory = false)]
public string? LastName { get; set; }
/// <summary>Updated email address.</summary>
[Parameter(Mandatory = false)]
public string? Email { get; set; }
/// <summary>Updated group membership (comma-separated group names).</summary>
[Parameter(Mandatory = false)]
public string? Groups { get; set; }
/// <summary>Updated comment/notes.</summary>
[Parameter(Mandatory = false)]
public string? Comment { get; set; }
/// <summary>Account expiry as a Unix timestamp. Use 0 to remove expiry.</summary>
[Parameter(Mandatory = false)]
public long? Expire { get; set; }
/// <summary>Enable or disable the user account.</summary>
[Parameter(Mandatory = false)]
public bool? Enable { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(UserId, "Set PVE User"))
return;
var config = new Dictionary<string, object>();
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);
}
}
}