mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user