From 76c2ad4946a14c0cae8f58392054faf87d8eb2c2 Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Tue, 17 Mar 2026 15:46:38 -0500 Subject: [PATCH] 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) --- .../CloudInit/GetPveCloudInitConfigCmdlet.cs | 41 ++++++ .../InvokePveCloudInitRegenerateCmdlet.cs | 46 ++++++ .../CloudInit/SetPveCloudInitConfigCmdlet.cs | 110 +++++++++++++++ .../Cmdlets/Network/GetPveNetworkCmdlet.cs | 54 +++++++ .../Cmdlets/Network/GetPveSdnVnetCmdlet.cs | 52 +++++++ .../Cmdlets/Network/GetPveSdnZoneCmdlet.cs | 42 ++++++ .../Network/InvokePveNetworkApplyCmdlet.cs | 66 +++++++++ .../Cmdlets/Network/NewPveNetworkCmdlet.cs | 94 +++++++++++++ .../Cmdlets/Network/NewPveSdnVnetCmdlet.cs | 57 ++++++++ .../Cmdlets/Network/NewPveSdnZoneCmdlet.cs | 78 +++++++++++ .../Cmdlets/Network/RemovePveNetworkCmdlet.cs | 35 +++++ .../Cmdlets/Network/RemovePveSdnVnetCmdlet.cs | 30 ++++ .../Cmdlets/Network/RemovePveSdnZoneCmdlet.cs | 31 ++++ .../Cmdlets/Network/SetPveNetworkCmdlet.cs | 94 +++++++++++++ .../Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs | 43 ++++++ .../Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs | 89 ++++++++++++ .../Snapshots/RemovePveSnapshotCmdlet.cs | 75 ++++++++++ .../Snapshots/RestorePveSnapshotCmdlet.cs | 78 +++++++++++ .../Cmdlets/Tasks/GetPveTaskCmdlet.cs | 35 +++++ .../Cmdlets/Tasks/WaitPveTaskCmdlet.cs | 132 ++++++++++++++++++ .../Cmdlets/Templates/GetPveTemplateCmdlet.cs | 83 +++++++++++ .../Cmdlets/Templates/NewPveTemplateCmdlet.cs | 49 +++++++ .../Templates/NewPveVmFromTemplateCmdlet.cs | 80 +++++++++++ .../Templates/RemovePveTemplateCmdlet.cs | 54 +++++++ .../Cmdlets/Users/GetPvePermissionCmdlet.cs | 48 +++++++ .../Cmdlets/Users/GetPveRoleCmdlet.cs | 42 ++++++ .../Cmdlets/Users/GetPveUserCmdlet.cs | 58 ++++++++ .../Cmdlets/Users/NewPveRoleCmdlet.cs | 48 +++++++ .../Cmdlets/Users/NewPveUserCmdlet.cs | 90 ++++++++++++ .../Cmdlets/Users/RemovePveRoleCmdlet.cs | 30 ++++ .../Cmdlets/Users/RemovePveUserCmdlet.cs | 29 ++++ .../Cmdlets/Users/SetPvePermissionCmdlet.cs | 68 +++++++++ .../Cmdlets/Users/SetPveUserCmdlet.cs | 81 +++++++++++ 33 files changed, 2042 insertions(+) create mode 100644 src/PSProxmoxVE/Cmdlets/CloudInit/GetPveCloudInitConfigCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/GetPveSdnVnetCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/GetPveSdnZoneCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/InvokePveNetworkApplyCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/NewPveSdnVnetCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/NewPveSdnZoneCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Templates/GetPveTemplateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Templates/RemovePveTemplateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/GetPveRoleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/NewPveRoleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/NewPveUserCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/RemovePveRoleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPveUserCmdlet.cs 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); + } + } +}