feat: close API coverage gaps — 51 new cmdlets across 6 areas

Phase 1 — Missing Update/Set operations (10 cmdlets):
- SDN: Set-PveSdnZone/Vnet/Subnet/Controller/Ipam/Dns + Invoke-PveSdnApply
- Set-PveRole, Set-PveStorage, Set-PveApiToken

Phase 2 — Cluster operations (8 cmdlets):
- Get-PveClusterResource (cluster-wide inventory)
- Get-PveTaskList, Stop-PveTask
- Get/New/Set/Remove-PvePool
- Get-PveBackupInfo (unprotected VMs)

Phase 3 — VM & container gaps (14 cmdlets):
- Move-PveVmDisk, Remove-PveVmDisk
- Guest agent: Get-PveVmGuestOsInfo/FsInfo, Read/Write-PveVmGuestFile,
  Set-PveVmGuestPassword, Invoke-PveVmGuestFsTrim
- Suspend/Resume-PveContainer, Resize-PveContainerDisk,
  New-PveContainerTemplate, Move-PveContainerVolume,
  Get-PveContainerInterface

Phase 4 — Storage content management (4 cmdlets):
- Get-PveStorageStatus, Remove/Set-PveStorageContent, New-PveStorageDisk

Phase 5 — Node operations (6 cmdlets):
- Get/Set-PveNodeConfig, Get/Set-PveNodeDns, Start/Stop-PveNodeVms

Phase 6 — Access management (9 cmdlets):
- Get/New/Set/Remove-PveGroup, Get/New/Set/Remove-PveDomain, Set-PvePassword

Total: 169 cmdlets (was 118). Includes models, services, format views,
Pester unit tests, manifest updates, README, CHANGELOG, API coverage docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-21 09:55:19 -05:00
parent ca4585f134
commit df4cae01bc
85 changed files with 7172 additions and 8 deletions
@@ -0,0 +1,129 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Cluster;
/// <summary>
/// Represents a resource entry returned by the /cluster/resources endpoint.
/// Resources can be VMs, containers, nodes, storage, or SDN objects.
/// </summary>
public class PveClusterResource
{
/// <summary>
/// The unique resource identifier (e.g., "node/pve1", "qemu/100", "storage/local").
/// </summary>
[JsonPropertyName("id")]
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// The resource type: "vm" (QEMU), "lxc" (container), "node", "storage", or "sdn".
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string? Type { get; set; }
/// <summary>
/// The current status of the resource (e.g., "running", "stopped", "online").
/// </summary>
[JsonPropertyName("status")]
[JsonProperty("status")]
public string? Status { get; set; }
/// <summary>
/// The display name of the resource.
/// </summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>
/// The node on which this resource resides.
/// </summary>
[JsonPropertyName("node")]
[JsonProperty("node")]
public string? Node { get; set; }
/// <summary>
/// The pool this resource belongs to, if any.
/// </summary>
[JsonPropertyName("pool")]
[JsonProperty("pool")]
public string? Pool { get; set; }
/// <summary>
/// Uptime in seconds.
/// </summary>
[JsonPropertyName("uptime")]
[JsonProperty("uptime")]
public long? Uptime { get; set; }
/// <summary>
/// Maximum number of CPUs/cores available to this resource.
/// </summary>
[JsonPropertyName("maxcpu")]
[JsonProperty("maxcpu")]
public double? MaxCpu { get; set; }
/// <summary>
/// Current CPU usage as a fraction (0.0 to 1.0).
/// </summary>
[JsonPropertyName("cpu")]
[JsonProperty("cpu")]
public double? Cpu { get; set; }
/// <summary>
/// Maximum memory in bytes.
/// </summary>
[JsonPropertyName("maxmem")]
[JsonProperty("maxmem")]
public long? MaxMem { get; set; }
/// <summary>
/// Current memory usage in bytes.
/// </summary>
[JsonPropertyName("mem")]
[JsonProperty("mem")]
public long? Mem { get; set; }
/// <summary>
/// Maximum disk space in bytes.
/// </summary>
[JsonPropertyName("maxdisk")]
[JsonProperty("maxdisk")]
public long? MaxDisk { get; set; }
/// <summary>
/// Current disk usage in bytes.
/// </summary>
[JsonPropertyName("disk")]
[JsonProperty("disk")]
public long? Disk { get; set; }
/// <summary>
/// Whether this resource is a template (1) or not (0). Only for VM/LXC types.
/// </summary>
[JsonPropertyName("template")]
[JsonProperty("template")]
public int? Template { get; set; }
/// <summary>
/// The VM or container ID. Only for VM/LXC types.
/// </summary>
[JsonPropertyName("vmid")]
[JsonProperty("vmid")]
public int? VmId { get; set; }
/// <summary>
/// The HA (High Availability) state of the resource, if managed by HA.
/// </summary>
[JsonPropertyName("hastate")]
[JsonProperty("hastate")]
public string? HaState { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{Type ?? "unknown"}: {Name ?? Id ?? "N/A"} | Node: {Node ?? "N/A"} | Status: {Status ?? "N/A"}";
}
}
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PSProxmoxVE.Core.Models.Cluster;
/// <summary>
/// Represents a Proxmox VE resource pool as returned by the /pools endpoint.
/// </summary>
public class PvePool
{
/// <summary>
/// The pool identifier.
/// </summary>
[JsonPropertyName("poolid")]
[JsonProperty("poolid")]
public string? PoolId { get; set; }
/// <summary>
/// An optional comment or description for the pool.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// The pool members (VMs, containers, storage). Returned as a raw array
/// when querying a specific pool.
/// </summary>
[JsonPropertyName("members")]
[JsonProperty("members")]
public JArray? Members { get; set; }
/// <inheritdoc />
public override string ToString()
{
var memberCount = Members?.Count ?? 0;
return $"Pool: {PoolId ?? "N/A"} | Comment: {Comment ?? "-"} | Members: {memberCount}";
}
}
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Containers;
/// <summary>
/// Represents a network interface on an LXC container.
/// </summary>
public class PveContainerInterface
{
/// <summary>The interface name (e.g., "eth0", "lo").</summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>The hardware (MAC) address of the interface.</summary>
[JsonPropertyName("hwaddr")]
[JsonProperty("hwaddr")]
public string? HwAddr { get; set; }
/// <summary>The IPv4 address assigned to this interface.</summary>
[JsonPropertyName("inet")]
[JsonProperty("inet")]
public string? Inet { get; set; }
/// <summary>The IPv6 address assigned to this interface.</summary>
[JsonPropertyName("inet6")]
[JsonProperty("inet6")]
public string? Inet6 { get; set; }
/// <inheritdoc />
public override string ToString()
{
var addr = Inet ?? Inet6 ?? "no address";
return $"{Name ?? "Unknown"} ({HwAddr ?? "N/A"}): {addr}";
}
}
@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Storage;
/// <summary>
/// Represents the status of a Proxmox VE storage on a specific node,
/// as returned by the /nodes/{node}/storage/{storage}/status endpoint.
/// </summary>
public class PveStorageStatus
{
/// <summary>Total capacity in bytes.</summary>
[JsonPropertyName("total")]
[JsonProperty("total")]
public long? Total { get; set; }
/// <summary>Used capacity in bytes.</summary>
[JsonPropertyName("used")]
[JsonProperty("used")]
public long? Used { get; set; }
/// <summary>Available (free) capacity in bytes.</summary>
[JsonPropertyName("avail")]
[JsonProperty("avail")]
public long? Available { get; set; }
/// <summary>Whether the storage is currently active (1) or not (0).</summary>
[JsonPropertyName("active")]
[JsonProperty("active")]
public int? Active { get; set; }
/// <summary>Comma-separated list of supported content types.</summary>
[JsonPropertyName("content")]
[JsonProperty("content")]
public string? Content { get; set; }
/// <summary>Whether the storage is enabled (1) or disabled (0).</summary>
[JsonPropertyName("enabled")]
[JsonProperty("enabled")]
public int? Enabled { get; set; }
/// <summary>Whether the storage is shared across cluster nodes (1) or node-local (0).</summary>
[JsonPropertyName("shared")]
[JsonProperty("shared")]
public int? Shared { get; set; }
/// <summary>The storage backend type.</summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string? Type { get; set; }
/// <summary>The storage identifier. Populated by the cmdlet after retrieval.</summary>
public string? Storage { get; set; }
/// <summary>The node name. Populated by the cmdlet after retrieval.</summary>
public string? Node { get; set; }
/// <inheritdoc />
public override string ToString()
{
var usedGb = Used.HasValue ? $"{Used.Value / 1024.0 / 1024 / 1024:F1} GB" : "N/A";
var totalGb = Total.HasValue ? $"{Total.Value / 1024.0 / 1024 / 1024:F1} GB" : "N/A";
var availGb = Available.HasValue ? $"{Available.Value / 1024.0 / 1024 / 1024:F1} GB" : "N/A";
var activeStr = Active is 1 ? "active" : "inactive";
return $"Storage: {Storage ?? "N/A"} | Type: {Type ?? "N/A"} | {activeStr} | "
+ $"Used: {usedGb}/{totalGb} | Free: {availGb}";
}
}
@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Users;
/// <summary>
/// Represents a Proxmox VE authentication domain/realm as returned by the /access/domains endpoint.
/// </summary>
public class PveDomain
{
/// <summary>The realm identifier (e.g., "pam", "pve", "myldap").</summary>
[JsonPropertyName("realm")]
[JsonProperty("realm")]
public string Realm { get; set; } = string.Empty;
/// <summary>The domain type (e.g., "pam", "pve", "ad", "ldap", "openid").</summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>Optional comment/description for the domain.</summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>Whether this is the default realm (1) or not (0).</summary>
[JsonPropertyName("default")]
[JsonProperty("default")]
public int? Default { get; set; }
/// <summary>TFA configuration string, if any.</summary>
[JsonPropertyName("tfa")]
[JsonProperty("tfa")]
public string? Tfa { get; set; }
/// <inheritdoc />
public override string ToString()
{
var defaultStr = Default is 1 ? " [default]" : string.Empty;
return $"Realm: {Realm}{defaultStr} | Type: {Type} | Comment: {Comment ?? "N/A"}";
}
}
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Users;
/// <summary>
/// Represents a Proxmox VE group as returned by the /access/groups endpoint.
/// </summary>
public class PveGroup
{
/// <summary>The unique group identifier.</summary>
[JsonPropertyName("groupid")]
[JsonProperty("groupid")]
public string GroupId { get; set; } = string.Empty;
/// <summary>Optional comment/description for the group.</summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>Comma-separated list of user IDs that belong to this group.</summary>
[JsonPropertyName("users")]
[JsonProperty("users")]
public string? Users { get; set; }
/// <summary>Array of member user IDs. Populated when available.</summary>
[JsonPropertyName("members")]
[JsonProperty("members")]
public string[]? Members { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"Group: {GroupId} | Comment: {Comment ?? "N/A"} | Users: {Users ?? "none"}";
}
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Vms;
/// <summary>
/// Represents filesystem information reported by the QEMU guest agent.
/// </summary>
public class PveGuestFsInfo
{
/// <summary>The filesystem name/device.</summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>The mount point path.</summary>
[JsonPropertyName("mountpoint")]
[JsonProperty("mountpoint")]
public string? MountPoint { get; set; }
/// <summary>The filesystem type (e.g., "ext4", "ntfs").</summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string? Type { get; set; }
/// <summary>Total size of the filesystem in bytes.</summary>
[JsonPropertyName("total-bytes")]
[JsonProperty("total-bytes")]
public long? TotalBytes { get; set; }
/// <summary>Used space on the filesystem in bytes.</summary>
[JsonPropertyName("used-bytes")]
[JsonProperty("used-bytes")]
public long? UsedBytes { get; set; }
/// <summary>List of disk devices backing this filesystem.</summary>
[JsonPropertyName("disk")]
[JsonProperty("disk")]
public string[]? DiskList { get; set; }
/// <inheritdoc />
public override string ToString()
{
var used = UsedBytes.HasValue && TotalBytes.HasValue && TotalBytes.Value > 0
? $" ({UsedBytes.Value * 100 / TotalBytes.Value}% used)"
: string.Empty;
return $"{MountPoint ?? Name ?? "Unknown"} [{Type ?? "?"}]{used}";
}
}
@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Vms;
/// <summary>
/// Represents OS information reported by the QEMU guest agent.
/// </summary>
public class PveGuestOsInfo
{
/// <summary>The OS identifier (e.g., "mswindows", "linux").</summary>
[JsonPropertyName("id")]
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>The OS name (e.g., "Microsoft Windows 10 Enterprise").</summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>The kernel release string.</summary>
[JsonPropertyName("kernel-release")]
[JsonProperty("kernel-release")]
public string? KernelRelease { get; set; }
/// <summary>The kernel version string.</summary>
[JsonPropertyName("kernel-version")]
[JsonProperty("kernel-version")]
public string? KernelVersion { get; set; }
/// <summary>The machine hardware name.</summary>
[JsonPropertyName("machine")]
[JsonProperty("machine")]
public string? Machine { get; set; }
/// <summary>The OS version details.</summary>
[JsonPropertyName("version")]
[JsonProperty("version")]
public PveGuestOsVersion? Version { get; set; }
/// <inheritdoc />
public override string ToString()
{
var ver = Version != null ? $" {Version.Major}.{Version.Minor}" : string.Empty;
return $"{Name ?? Id ?? "Unknown"}{ver} ({KernelRelease ?? "N/A"})";
}
}
/// <summary>
/// Represents a guest OS version with major and minor components.
/// </summary>
public class PveGuestOsVersion
{
/// <summary>The version identifier string.</summary>
[JsonPropertyName("id")]
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>The major version number.</summary>
[JsonPropertyName("major")]
[JsonProperty("major")]
public int? Major { get; set; }
/// <summary>The minor version number.</summary>
[JsonPropertyName("minor")]
[JsonProperty("minor")]
public int? Minor { get; set; }
}
@@ -106,6 +106,24 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Backup compliance info
// -------------------------------------------------------------------------
/// <summary>
/// Returns the list of guests not covered by any backup job.
/// </summary>
public JArray GetNotBackedUp(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/backup-info/not-backed-up")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data as JArray ?? new JArray();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@@ -24,5 +24,24 @@ namespace PSProxmoxVE.Core.Services
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
}
/// <summary>
/// Returns cluster resources, optionally filtered by type.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="type">Optional resource type filter (vm, lxc, node, storage, sdn).</param>
public PveClusterResource[] GetClusterResources(PveSession session, string? type = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var resource = "cluster/resources";
if (!string.IsNullOrEmpty(type))
resource += $"?type={Uri.EscapeDataString(type!)}";
var response = client.GetAsync(resource).GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveClusterResource[]>() ?? Array.Empty<PveClusterResource>();
}
}
}
@@ -318,6 +318,104 @@ namespace PSProxmoxVE.Core.Services
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Suspend / Resume
// -------------------------------------------------------------------------
/// <summary>Suspends a container. Returns the task UPID.</summary>
public PveTask SuspendContainer(PveSession session, string node, int vmid)
=> PostStatus(session, node, vmid, "suspend");
/// <summary>Resumes a suspended container. Returns the task UPID.</summary>
public PveTask ResumeContainer(PveSession session, string node, int vmid)
=> PostStatus(session, node, vmid, "resume");
// -------------------------------------------------------------------------
// Disk / volume operations
// -------------------------------------------------------------------------
/// <summary>
/// Resizes a container disk/volume. Returns the task UPID.
/// </summary>
public PveTask ResizeContainerDisk(PveSession session, string node, int vmid, string disk, string size)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(disk)) throw new ArgumentNullException(nameof(disk));
if (string.IsNullOrWhiteSpace(size)) throw new ArgumentNullException(nameof(size));
var formData = new Dictionary<string, string>
{
["disk"] = disk,
["size"] = size
};
using var client = new PveHttpClient(session);
var response = client.PutAsync($"nodes/{node}/lxc/{vmid}/resize", formData)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Moves a container volume to a different storage. Returns the task UPID.
/// </summary>
public PveTask MoveVolume(PveSession session, string node, int vmid, string volume, string storage, bool delete = true)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
var formData = new Dictionary<string, string>
{
["volume"] = volume,
["storage"] = storage,
["delete"] = delete ? "1" : "0"
};
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/move_volume", formData)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Template
// -------------------------------------------------------------------------
/// <summary>
/// Converts a container to a template. This is irreversible. Returns the task UPID.
/// </summary>
public PveTask ConvertToTemplate(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/template")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Interfaces
// -------------------------------------------------------------------------
/// <summary>
/// Returns network interface information for a container.
/// </summary>
public PveContainerInterface[] GetInterfaces(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/lxc/{vmid}/interfaces")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveContainerInterface[]>() ?? Array.Empty<PveContainerInterface>();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@@ -438,6 +438,108 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// SDN Update operations
// -------------------------------------------------------------------------
/// <summary>
/// Applies pending SDN configuration changes cluster-wide.
/// </summary>
public void ApplySdn(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
client.PutAsync("cluster/sdn", new Dictionary<string, string>())
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN zone configuration.
/// </summary>
public void UpdateSdnZone(PveSession session, string zone, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN VNet configuration.
/// </summary>
public void UpdateSdnVnet(PveSession session, string vnet, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN subnet configuration.
/// </summary>
public void UpdateSdnSubnet(PveSession session, string vnet, string subnet, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync(
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}",
config).GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN controller configuration.
/// </summary>
public void UpdateSdnController(PveSession session, string controller, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN IPAM plugin configuration.
/// </summary>
public void UpdateSdnIpam(PveSession session, string ipam, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an SDN DNS plugin configuration.
/// </summary>
public void UpdateSdnDns(PveSession session, string dns, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}", config)
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Nodes;
using PSProxmoxVE.Core.Models.Vms;
namespace PSProxmoxVE.Core.Services
{
@@ -39,6 +41,104 @@ namespace PSProxmoxVE.Core.Services
return data?.ToObject<PveNodeStatus>() ?? new PveNodeStatus();
}
/// <summary>
/// Returns the configuration of a specific node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
public JObject GetNodeConfig(PveSession session, string node)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/config").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data as JObject ?? new JObject();
}
/// <summary>
/// Updates the configuration of a specific node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="config">Configuration parameters to update.</param>
public void SetNodeConfig(PveSession session, string node, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"nodes/{node}/config", config).GetAwaiter().GetResult();
}
/// <summary>
/// Returns the DNS configuration of a specific node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
public JObject GetNodeDns(PveSession session, string node)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/dns").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data as JObject ?? new JObject();
}
/// <summary>
/// Updates the DNS configuration of a specific node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="config">DNS configuration parameters to update.</param>
public void SetNodeDns(PveSession session, string node, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"nodes/{node}/dns", config).GetAwaiter().GetResult();
}
/// <summary>
/// Starts all VMs and containers on a node. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="config">Optional parameters (e.g. vms to limit which VMs start).</param>
public PveTask StartAll(PveSession session, string node, Dictionary<string, string>? config = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
var formData = config ?? new Dictionary<string, string>();
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/startall", formData).GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Stops all VMs and containers on a node. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="config">Optional parameters (e.g. vms, force-stop).</param>
public PveTask StopAll(PveSession session, string node, Dictionary<string, string>? config = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
var formData = config ?? new Dictionary<string, string>();
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/stopall", formData).GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Returns the Proxmox VE version running on the server.
/// </summary>
@@ -54,5 +154,20 @@ namespace PSProxmoxVE.Core.Services
throw new InvalidOperationException("Failed to retrieve PVE version from API response.");
return PveVersion.Parse(versionStr!);
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
private static PveTask ParseTask(string response, string node)
{
var data = JObject.Parse(response)["data"];
if (data?.Type == JTokenType.String)
return new PveTask { Upid = data.ToString(), Node = node };
var task = data?.ToObject<PveTask>() ?? new PveTask();
task.Node = node;
return task;
}
}
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Cluster;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Service for Proxmox VE resource pool API operations.
/// </summary>
public class PoolService
{
/// <summary>
/// Returns all resource pools.
/// </summary>
public PvePool[] GetPools(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("pools").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PvePool[]>() ?? Array.Empty<PvePool>();
}
/// <summary>
/// Returns a single resource pool by ID.
/// </summary>
public PvePool? GetPool(PveSession session, string poolId)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"pools/{Uri.EscapeDataString(poolId)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PvePool>();
}
/// <summary>
/// Creates a new resource pool.
/// </summary>
public void CreatePool(PveSession session, string poolId, string? comment = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
using var client = new PveHttpClient(session);
var config = new Dictionary<string, string> { { "poolid", poolId } };
if (!string.IsNullOrEmpty(comment))
config["comment"] = comment!;
client.PostAsync("pools", config).GetAwaiter().GetResult();
}
/// <summary>
/// Updates an existing resource pool.
/// </summary>
public void UpdatePool(PveSession session, string poolId, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"pools/{Uri.EscapeDataString(poolId)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Removes a resource pool.
/// </summary>
public void RemovePool(PveSession session, string poolId)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
using var client = new PveHttpClient(session);
client.DeleteAsync($"pools/{Uri.EscapeDataString(poolId)}")
.GetAwaiter().GetResult();
}
}
}
@@ -187,6 +187,105 @@ namespace PSProxmoxVE.Core.Services
client.DeleteAsync($"storage/{storage}").GetAwaiter().GetResult();
}
/// <summary>
/// Updates a storage definition.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="storage">The storage identifier to update.</param>
/// <param name="config">Configuration parameters to update.</param>
public void UpdateStorage(PveSession session, string storage, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"storage/{Uri.EscapeDataString(storage)}", config)
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Storage status & content management
// -------------------------------------------------------------------------
/// <summary>
/// Returns the status of a specific storage on a node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="storage">The storage identifier.</param>
public PveStorageStatus GetStorageStatus(PveSession session, string node, string storage)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/storage/{storage}/status").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveStorageStatus>() ?? new PveStorageStatus();
}
/// <summary>
/// Removes a content volume from a storage on a node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="storage">The storage identifier.</param>
/// <param name="volume">The volume identifier to remove.</param>
public void RemoveContent(PveSession session, string node, string storage, string volume)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
using var client = new PveHttpClient(session);
client.DeleteAsync($"nodes/{node}/storage/{storage}/content/{Uri.EscapeDataString(volume)}")
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates properties (e.g. notes) of a content volume on a storage.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="storage">The storage identifier.</param>
/// <param name="volume">The volume identifier to update.</param>
/// <param name="config">Configuration parameters to update.</param>
public void UpdateContent(PveSession session, string node, string storage, string volume, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"nodes/{node}/storage/{storage}/content/{Uri.EscapeDataString(volume)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Allocates a new disk image on a storage. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="storage">The storage identifier.</param>
/// <param name="config">Allocation parameters (filename, size, format).</param>
public PveTask AllocateDisk(PveSession session, string node, string storage, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/storage/{storage}/content", config)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@@ -103,5 +103,57 @@ namespace PSProxmoxVE.Core.Services
Thread.Sleep(effectivePoll);
}
}
/// <summary>
/// Returns a list of tasks on the specified node, with optional filters.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The node name.</param>
/// <param name="vmid">Optional VM ID filter.</param>
/// <param name="source">Optional source filter: "all" or "active".</param>
/// <param name="typeFilter">Optional task type filter (e.g., "qmstart").</param>
/// <param name="limit">Maximum number of tasks to return. Defaults to 50.</param>
public PveTask[] GetTasks(PveSession session, string node, int? vmid = null,
string? source = null, string? typeFilter = null, int limit = 50)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var queryParts = new List<string> { $"limit={limit}" };
if (vmid.HasValue)
queryParts.Add($"vmid={vmid.Value}");
if (!string.IsNullOrEmpty(source))
queryParts.Add($"source={Uri.EscapeDataString(source!)}");
if (!string.IsNullOrEmpty(typeFilter))
queryParts.Add($"typefilter={Uri.EscapeDataString(typeFilter!)}");
var query = string.Join("&", queryParts);
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/tasks?{query}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
var tasks = data?.ToObject<PveTask[]>() ?? Array.Empty<PveTask>();
foreach (var t in tasks)
t.Node ??= node;
return tasks;
}
/// <summary>
/// Stops (cancels) a running task on the specified node.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The node name.</param>
/// <param name="upid">The UPID of the task to stop.</param>
public void StopTask(PveSession session, string node, string upid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
using var client = new PveHttpClient(session);
var encodedUpid = Uri.EscapeDataString(upid);
client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}")
.GetAwaiter().GetResult();
}
}
}
@@ -189,6 +189,23 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an API token's configuration.
/// </summary>
public void UpdateApiToken(PveSession session, string userId, string tokenId, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
var encodedUser = Uri.EscapeDataString(userId);
var encodedToken = Uri.EscapeDataString(tokenId);
client.PutAsync($"access/users/{encodedUser}/token/{encodedToken}", config)
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Roles
// -------------------------------------------------------------------------
@@ -235,6 +252,165 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates a role's privileges.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="roleId">The role identifier to update.</param>
/// <param name="privileges">Comma-separated list of privileges.</param>
public void UpdateRole(PveSession session, string roleId, string privileges)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId));
if (string.IsNullOrWhiteSpace(privileges)) throw new ArgumentNullException(nameof(privileges));
var formData = new Dictionary<string, string> { ["privs"] = privileges };
using var client = new PveHttpClient(session);
client.PutAsync($"access/roles/{Uri.EscapeDataString(roleId)}", formData)
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Groups
// -------------------------------------------------------------------------
/// <summary>Returns all groups.</summary>
/// <param name="session">The authenticated PVE session.</param>
public PveGroup[] GetGroups(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("access/groups").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveGroup[]>() ?? Array.Empty<PveGroup>();
}
/// <summary>Creates a new group.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="groupId">The group identifier.</param>
/// <param name="comment">Optional comment/description.</param>
public void CreateGroup(PveSession session, string groupId, string? comment = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
var formData = new Dictionary<string, string> { ["groupid"] = groupId };
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PostAsync("access/groups", formData).GetAwaiter().GetResult();
}
/// <summary>Updates a group's properties.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="groupId">The group identifier to update.</param>
/// <param name="config">Configuration parameters to update.</param>
public void UpdateGroup(PveSession session, string groupId, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"access/groups/{Uri.EscapeDataString(groupId)}", config)
.GetAwaiter().GetResult();
}
/// <summary>Removes a group.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="groupId">The group identifier to remove.</param>
public void RemoveGroup(PveSession session, string groupId)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
using var client = new PveHttpClient(session);
client.DeleteAsync($"access/groups/{Uri.EscapeDataString(groupId)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Domains / Realms
// -------------------------------------------------------------------------
/// <summary>Returns all authentication domains/realms.</summary>
/// <param name="session">The authenticated PVE session.</param>
public PveDomain[] GetDomains(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("access/domains").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveDomain[]>() ?? Array.Empty<PveDomain>();
}
/// <summary>Creates a new authentication domain/realm.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="config">Domain configuration parameters (must include realm and type).</param>
public void CreateDomain(PveSession session, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PostAsync("access/domains", config).GetAwaiter().GetResult();
}
/// <summary>Updates an authentication domain/realm.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="realm">The realm identifier to update.</param>
/// <param name="config">Configuration parameters to update.</param>
public void UpdateDomain(PveSession session, string realm, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"access/domains/{Uri.EscapeDataString(realm)}", config)
.GetAwaiter().GetResult();
}
/// <summary>Removes an authentication domain/realm.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="realm">The realm identifier to remove.</param>
public void RemoveDomain(PveSession session, string realm)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
using var client = new PveHttpClient(session);
client.DeleteAsync($"access/domains/{Uri.EscapeDataString(realm)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Password
// -------------------------------------------------------------------------
/// <summary>Changes a user's password.</summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="userId">The user ID in "username@realm" format.</param>
/// <param name="password">The new password.</param>
public void ChangePassword(PveSession session, string userId, string password)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentNullException(nameof(password));
var formData = new Dictionary<string, string>
{
["userid"] = userId,
["password"] = password
};
using var client = new PveHttpClient(session);
client.PutAsync("access/password", formData).GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Permissions / ACLs
// -------------------------------------------------------------------------
+157
View File
@@ -521,6 +521,163 @@ namespace PSProxmoxVE.Core.Services
return JObject.Parse(response)["data"] as JObject ?? new JObject();
}
// -------------------------------------------------------------------------
// Disk operations
// -------------------------------------------------------------------------
/// <summary>
/// Moves a VM disk to a different storage. Returns the task UPID.
/// </summary>
public PveTask MoveDisk(PveSession session, string node, int vmid, string disk, string storage, string? format = null, bool delete = true)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(disk)) throw new ArgumentNullException(nameof(disk));
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
var formData = new Dictionary<string, string>
{
["disk"] = disk,
["storage"] = storage,
["delete"] = delete ? "1" : "0"
};
if (!string.IsNullOrEmpty(format))
formData["format"] = format!;
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/move_disk", formData)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Unlinks (detaches) disks from a VM.
/// </summary>
public void UnlinkDisk(PveSession session, string node, int vmid, string idlist, bool force = false)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(idlist)) throw new ArgumentNullException(nameof(idlist));
var formData = new Dictionary<string, string>
{
["idlist"] = idlist
};
if (force)
formData["force"] = "1";
using var client = new PveHttpClient(session);
client.PutAsync($"nodes/{node}/qemu/{vmid}/unlink", formData)
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Guest agent — extended operations
// -------------------------------------------------------------------------
/// <summary>
/// Retrieves OS information from the QEMU guest agent.
/// </summary>
public PveGuestOsInfo? GetGuestOsInfo(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/agent/get-osinfo")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
var result = data?["result"];
return result?.ToObject<PveGuestOsInfo>();
}
/// <summary>
/// Retrieves filesystem information from the QEMU guest agent.
/// </summary>
public PveGuestFsInfo[] GetGuestFsInfo(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/agent/get-fsinfo")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
var result = data?["result"];
return result?.ToObject<PveGuestFsInfo[]>() ?? Array.Empty<PveGuestFsInfo>();
}
/// <summary>
/// Reads a file from the guest filesystem via the QEMU guest agent.
/// </summary>
public string ReadGuestFile(PveSession session, string node, int vmid, string file)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(file)) throw new ArgumentNullException(nameof(file));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/agent/file-read?file={Uri.EscapeDataString(file)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?["content"]?.ToString() ?? string.Empty;
}
/// <summary>
/// Writes content to a file on the guest filesystem via the QEMU guest agent.
/// </summary>
public void WriteGuestFile(PveSession session, string node, int vmid, string file, string content)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(file)) throw new ArgumentNullException(nameof(file));
var formData = new Dictionary<string, string>
{
["file"] = file,
["content"] = content
};
using var client = new PveHttpClient(session);
client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/file-write", formData)
.GetAwaiter().GetResult();
}
/// <summary>
/// Sets a user password inside the guest via the QEMU guest agent.
/// </summary>
public void SetGuestPassword(PveSession session, string node, int vmid, string username, string password, bool crypted = false)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullException(nameof(username));
var formData = new Dictionary<string, string>
{
["username"] = username,
["password"] = password
};
if (crypted)
formData["crypted"] = "1";
using var client = new PveHttpClient(session);
client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/set-user-password", formData)
.GetAwaiter().GetResult();
}
/// <summary>
/// Triggers an fstrim operation inside the guest via the QEMU guest agent.
/// </summary>
public void GuestFsTrim(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
using var client = new PveHttpClient(session);
client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/fstrim")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// OVA Upload
// -------------------------------------------------------------------------
@@ -0,0 +1,39 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Lists guests not covered by any backup job.</para>
/// <para type="description">
/// Returns information about VMs and containers that are not included in any
/// scheduled backup job. Useful for backup compliance auditing.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveBackupInfo")]
[OutputType(typeof(PSObject))]
public sealed class GetPveBackupInfoCmdlet : PveCmdletBase
{
protected override void ProcessRecord()
{
var session = GetSession();
var service = new BackupService();
WriteVerbose("Getting guests not covered by backup jobs...");
var items = service.GetNotBackedUp(session);
foreach (var item in items)
{
var pso = new PSObject();
if (item is Newtonsoft.Json.Linq.JObject jObj)
{
foreach (var prop in jObj.Properties())
{
pso.Properties.Add(new PSNoteProperty(prop.Name, prop.Value?.ToObject<object>()));
}
}
WriteObject(pso);
}
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Lists resources across the Proxmox VE cluster.</para>
/// <para type="description">
/// Returns cluster-wide resources (VMs, containers, nodes, storage, SDN).
/// Optionally filter by resource type or node name.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveClusterResource")]
[OutputType(typeof(PveClusterResource))]
public sealed class GetPveClusterResourceCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">Filter by resource type.</para>
/// </summary>
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by resource type (vm, lxc, node, storage, sdn).")]
[ValidateSet("vm", "lxc", "node", "storage", "sdn")]
public string? Type { get; set; }
/// <summary>
/// <para type="description">Filter results to a specific node name.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Filter results to a specific node name.")]
public string? Node { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new ClusterService();
WriteVerbose($"Getting cluster resources{(Type != null ? $" of type '{Type}'" : "")}...");
var resources = service.GetClusterResources(session, Type);
foreach (var resource in resources)
{
if (!string.IsNullOrEmpty(Node) &&
!string.Equals(resource.Node, Node, StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(resource);
}
}
}
}
@@ -0,0 +1,39 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Containers;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Gets network interface information for an LXC container.</para>
/// <para type="description">
/// Retrieves network interface information for the specified LXC container
/// via the Proxmox VE API, including interface names, MAC addresses, and IP addresses.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveContainerInterface")]
[OutputType(typeof(PveContainerInterface))]
public sealed class GetPveContainerInterfaceCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The container identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
WriteVerbose($"Getting network interfaces for container {VmId}...");
var service = new ContainerService();
var interfaces = service.GetInterfaces(session, Node, VmId);
foreach (var iface in interfaces)
WriteObject(iface);
}
}
}
@@ -0,0 +1,76 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Moves a container volume to a different storage.</para>
/// <para type="description">
/// Moves the specified volume on an LXC container to a different storage backend
/// via the Proxmox VE API. Optionally deletes the original volume after a successful move.
/// Use -Wait to block until the move task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Move, "PveContainerVolume", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class MovePveContainerVolumeCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the container resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the container whose volume should be moved.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The volume to move (e.g., "rootfs", "mp0").</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The volume to move (e.g. rootfs, mp0).")]
public string Volume { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The target storage to move the volume to.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The target storage identifier.")]
public string Storage { get; set; } = string.Empty;
/// <summary>
/// <para type="description">When specified, deletes the original volume after a successful move. Default is true.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Delete the original volume after move (default true).")]
public SwitchParameter Delete { get; set; } = true;
/// <summary>
/// <para type="description">When specified, waits for the move task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"Volume '{Volume}' on container {VmId} (node '{Node}') to storage '{Storage}'", "Move-PveContainerVolume"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Moving volume '{Volume}' on container {VmId} to storage '{Storage}'...");
var task = containerService.MoveVolume(session, Node, VmId, Volume, Storage, Delete.IsPresent);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
}
WriteObject(task);
}
}
}
@@ -0,0 +1,48 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Converts an LXC container to a template.</para>
/// <para type="description">
/// Converts the specified LXC container into a template via the Proxmox VE API.
/// This operation is irreversible — once converted, the container cannot be started
/// and can only be used as a base for cloning new containers.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveContainerTemplate",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(PveTask))]
public sealed class NewPveContainerTemplateCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the container resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the container to convert to a template.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "New-PveContainerTemplate"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Converting container {VmId} to template on node '{Node}'...");
var task = containerService.ConvertToTemplate(session, Node, VmId);
WriteObject(task);
}
}
}
@@ -0,0 +1,61 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Resizes a disk attached to an LXC container.</para>
/// <para type="description">
/// Resizes the specified disk/volume on an LXC container via the Proxmox VE API.
/// Use an absolute size (e.g., "50G") to set a fixed size, or a relative size
/// prefixed with "+" (e.g., "+5G") to grow the disk by that amount.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Resize, "PveContainerDisk", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class ResizePveContainerDiskCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the container resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the container whose disk should be resized.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The disk/volume to resize (e.g., "rootfs", "mp0").</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The disk/volume to resize (e.g. rootfs, mp0).")]
public string Disk { get; set; } = string.Empty;
/// <summary>
/// <para type="description">
/// The new size for the disk. Use an absolute value (e.g., "50G") to set a specific size,
/// or a "+" prefix (e.g., "+5G") to grow the disk by the specified amount.
/// </para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "New disk size (e.g. 50G or +5G to grow).")]
public string Size { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"Disk '{Disk}' on container {VmId} (node '{Node}') to size '{Size}'", "Resize-PveContainerDisk"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Resizing disk '{Disk}' on container {VmId}...");
var task = containerService.ResizeContainerDisk(session, Node, VmId, Disk, Size);
WriteObject(task);
}
}
}
@@ -0,0 +1,62 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Resumes a suspended LXC container on a Proxmox VE node.</para>
/// <para type="description">
/// Sends a resume command to the specified LXC container via the Proxmox VE API.
/// Use -Wait to block until the resume task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Resume, "PveContainer", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class ResumePveContainerCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the container resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the container to resume.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">When specified, waits for the resume task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "Resume-PveContainer"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Resuming container {VmId} on node '{Node}'...");
var task = containerService.ResumeContainer(session, Node, VmId);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
}
WriteObject(task);
}
}
}
@@ -0,0 +1,62 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Suspends an LXC container on a Proxmox VE node.</para>
/// <para type="description">
/// Sends a suspend command to the specified LXC container via the Proxmox VE API.
/// Use -Wait to block until the suspend task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Suspend, "PveContainer", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class SuspendPveContainerCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the container resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the container to suspend.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">When specified, waits for the suspend task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "Suspend-PveContainer"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Suspending container {VmId} on node '{Node}'...");
var task = containerService.SuspendContainer(session, Node, VmId);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
}
WriteObject(task);
}
}
}
@@ -0,0 +1,29 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Applies pending SDN configuration changes in Proxmox VE.</para>
/// <para type="description">
/// Applies all pending Software-Defined Networking configuration changes cluster-wide.
/// Run this after making changes with Set-PveSdnZone, Set-PveSdnVnet, etc.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "PveSdnApply", SupportsShouldProcess = true)]
public class InvokePveSdnApplyCmdlet : PveCmdletBase
{
protected override void ProcessRecord()
{
if (!ShouldProcess("SDN configuration", "Apply pending changes"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose("Applying pending SDN configuration changes...");
service.ApplySdn(session);
}
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN controller configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking controller configuration.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnController", SupportsShouldProcess = true)]
public class SetPveSdnControllerCmdlet : PveCmdletBase
{
/// <summary>The controller identifier.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN controller identifier.")]
public string Controller { get; set; } = string.Empty;
/// <summary>The Autonomous System Number for BGP/EVPN.</summary>
[Parameter(Mandatory = false, HelpMessage = "The Autonomous System Number for BGP/EVPN.")]
public int? Asn { get; set; }
/// <summary>Comma-separated list of BGP peer addresses.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of BGP peer addresses.")]
public string? Peers { get; set; }
/// <summary>The node this controller is configured on.</summary>
[Parameter(Mandatory = false, HelpMessage = "The node this controller is configured on.")]
public string? Node { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Controller, "Set PVE SDN Controller"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN controller '{Controller}'...");
var config = new Dictionary<string, string>();
if (MyInvocation.BoundParameters.ContainsKey("Asn"))
config["asn"] = Asn!.Value.ToString();
if (!string.IsNullOrEmpty(Peers)) config["peers"] = Peers!;
if (!string.IsNullOrEmpty(Node)) config["node"] = Node!;
service.UpdateSdnController(session, Controller, config);
}
}
}
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN DNS plugin configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking DNS plugin configuration.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnDns", SupportsShouldProcess = true)]
public class SetPveSdnDnsCmdlet : PveCmdletBase
{
/// <summary>The DNS plugin identifier.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN DNS plugin identifier.")]
public string Dns { get; set; } = string.Empty;
/// <summary>The DNS server URL.</summary>
[Parameter(Mandatory = false, HelpMessage = "The DNS server URL.")]
public string? Url { get; set; }
/// <summary>The TSIG key for DNS updates.</summary>
[Parameter(Mandatory = false, HelpMessage = "The TSIG key for DNS updates.")]
public string? Key { get; set; }
/// <summary>The IPv6 reverse DNS mask length.</summary>
[Parameter(Mandatory = false, HelpMessage = "The IPv6 reverse DNS mask length.")]
public int? ReverseMaskV6 { get; set; }
/// <summary>The TTL for DNS records.</summary>
[Parameter(Mandatory = false, HelpMessage = "The TTL for DNS records.")]
public int? Ttl { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Dns, "Set PVE SDN DNS"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN DNS plugin '{Dns}'...");
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Url)) config["url"] = Url!;
if (!string.IsNullOrEmpty(Key)) config["key"] = Key!;
if (MyInvocation.BoundParameters.ContainsKey("ReverseMaskV6"))
config["reversemaskv6"] = ReverseMaskV6!.Value.ToString();
if (MyInvocation.BoundParameters.ContainsKey("Ttl"))
config["ttl"] = Ttl!.Value.ToString();
service.UpdateSdnDns(session, Dns, config);
}
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN IPAM plugin configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking IPAM plugin configuration.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnIpam", SupportsShouldProcess = true)]
public class SetPveSdnIpamCmdlet : PveCmdletBase
{
/// <summary>The IPAM plugin identifier.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN IPAM plugin identifier.")]
public string Ipam { get; set; } = string.Empty;
/// <summary>The IPAM server URL.</summary>
[Parameter(Mandatory = false, HelpMessage = "The IPAM server URL.")]
public string? Url { get; set; }
/// <summary>The API token for the IPAM server.</summary>
[Parameter(Mandatory = false, HelpMessage = "The API token for the IPAM server.")]
public string? Token { get; set; }
/// <summary>The section ID in the IPAM server.</summary>
[Parameter(Mandatory = false, HelpMessage = "The section ID in the IPAM server.")]
public int? Section { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Ipam, "Set PVE SDN IPAM"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN IPAM plugin '{Ipam}'...");
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Url)) config["url"] = Url!;
if (!string.IsNullOrEmpty(Token)) config["token"] = Token!;
if (MyInvocation.BoundParameters.ContainsKey("Section"))
config["section"] = Section!.Value.ToString();
service.UpdateSdnIpam(session, Ipam, config);
}
}
}
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN subnet configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking subnet configuration on a VNet.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnSubnet", SupportsShouldProcess = true)]
public class SetPveSdnSubnetCmdlet : PveCmdletBase
{
/// <summary>The VNet this subnet belongs to.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN VNet name.")]
public string Vnet { get; set; } = string.Empty;
/// <summary>The subnet CIDR.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The subnet CIDR.")]
public string Subnet { get; set; } = string.Empty;
/// <summary>Gateway address for this subnet.</summary>
[Parameter(Mandatory = false, HelpMessage = "Gateway address for this subnet.")]
public string? Gateway { get; set; }
/// <summary>Enable SNAT for this subnet.</summary>
[Parameter(Mandatory = false, HelpMessage = "Enable SNAT for this subnet.")]
public SwitchParameter Snat { get; set; }
/// <summary>DNS zone prefix for automatic registration.</summary>
[Parameter(Mandatory = false, HelpMessage = "DNS zone prefix for automatic registration.")]
public string? DnsZonePrefix { get; set; }
/// <summary>DHCP range for this subnet.</summary>
[Parameter(Mandatory = false, HelpMessage = "DHCP range for this subnet.")]
public string? DhcpRange { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Subnet, "Set PVE SDN Subnet"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN subnet '{Subnet}' on VNet '{Vnet}'...");
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Gateway)) config["gateway"] = Gateway!;
if (MyInvocation.BoundParameters.ContainsKey("Snat"))
config["snat"] = Snat.IsPresent ? "1" : "0";
if (!string.IsNullOrEmpty(DnsZonePrefix)) config["dnszoneprefix"] = DnsZonePrefix!;
if (!string.IsNullOrEmpty(DhcpRange)) config["dhcp-range"] = DhcpRange!;
service.UpdateSdnSubnet(session, Vnet, Subnet, config);
}
}
}
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN VNet configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking VNet configuration.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnVnet", SupportsShouldProcess = true)]
public class SetPveSdnVnetCmdlet : PveCmdletBase
{
/// <summary>The VNet identifier.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN VNet name.")]
public string Vnet { get; set; } = string.Empty;
/// <summary>An alias for the VNet.</summary>
[Parameter(Mandatory = false, HelpMessage = "An alias for the VNet.")]
public string? Alias { get; set; }
/// <summary>The zone this VNet belongs to.</summary>
[Parameter(Mandatory = false, HelpMessage = "The zone this VNet belongs to.")]
public string? Zone { get; set; }
/// <summary>VLAN tag for this VNet.</summary>
[Parameter(Mandatory = false, HelpMessage = "VLAN tag for this VNet.")]
public int? Tag { get; set; }
/// <summary>Enable VLAN-aware bridging.</summary>
[Parameter(Mandatory = false, HelpMessage = "Enable VLAN-aware bridging.")]
public SwitchParameter VlanAware { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Vnet, "Set PVE SDN VNet"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN VNet '{Vnet}'...");
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Alias)) config["alias"] = Alias!;
if (!string.IsNullOrEmpty(Zone)) config["zone"] = Zone!;
if (MyInvocation.BoundParameters.ContainsKey("Tag"))
config["tag"] = Tag!.Value.ToString();
if (MyInvocation.BoundParameters.ContainsKey("VlanAware"))
config["vlanaware"] = VlanAware.IsPresent ? "1" : "0";
service.UpdateSdnVnet(session, Vnet, config);
}
}
}
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Updates an SDN zone configuration in Proxmox VE.</para>
/// <para type="description">
/// Modifies the specified Software-Defined Networking zone configuration.
/// Changes are pending until Invoke-PveSdnApply is called.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveSdnZone", SupportsShouldProcess = true)]
public class SetPveSdnZoneCmdlet : PveCmdletBase
{
/// <summary>The zone identifier.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN zone name.")]
public string Zone { get; set; } = string.Empty;
/// <summary>DNS server for automatic DNS registration.</summary>
[Parameter(Mandatory = false, HelpMessage = "DNS server for automatic registration.")]
public string? Dns { get; set; }
/// <summary>Reverse DNS server.</summary>
[Parameter(Mandatory = false, HelpMessage = "Reverse DNS server.")]
public string? Reversedns { get; set; }
/// <summary>DNS zone name for registration.</summary>
[Parameter(Mandatory = false, HelpMessage = "DNS zone name for registration.")]
public string? DnsZone { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Zone, "Set PVE SDN Zone"))
return;
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
var service = new NetworkService();
WriteVerbose($"Updating SDN zone '{Zone}'...");
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Dns)) config["dns"] = Dns!;
if (!string.IsNullOrEmpty(Reversedns)) config["reversedns"] = Reversedns!;
if (!string.IsNullOrEmpty(DnsZone)) config["dnszone"] = DnsZone!;
service.UpdateSdnZone(session, Zone, config);
}
}
}
@@ -0,0 +1,39 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Returns the configuration of a Proxmox VE node.</para>
/// <para type="description">
/// Retrieves the node configuration from the /nodes/{node}/config endpoint,
/// including description, wakeonlan settings and other node-level properties.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveNodeConfig")]
[OutputType(typeof(PSObject))]
public class GetPveNodeConfigCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Getting configuration for node '{Node}'...");
var config = service.GetNodeConfig(session, Node);
var psObj = new PSObject();
foreach (var prop in config.Properties())
{
psObj.Properties.Add(new PSNoteProperty(prop.Name, prop.Value.ToString()));
}
WriteObject(psObj);
}
}
}
@@ -0,0 +1,39 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Returns the DNS configuration of a Proxmox VE node.</para>
/// <para type="description">
/// Retrieves the DNS settings (search domain and nameservers) for the specified
/// node from the /nodes/{node}/dns endpoint.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveNodeDns")]
[OutputType(typeof(PSObject))]
public class GetPveNodeDnsCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Getting DNS configuration for node '{Node}'...");
var dns = service.GetNodeDns(session, Node);
var psObj = new PSObject();
foreach (var prop in dns.Properties())
{
psObj.Properties.Add(new PSNoteProperty(prop.Name, prop.Value.ToString()));
}
WriteObject(psObj);
}
}
}
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Updates the configuration of a Proxmox VE node.</para>
/// <para type="description">
/// Modifies node-level configuration properties such as description and wake-on-LAN
/// settings via the /nodes/{node}/config endpoint.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveNodeConfig", SupportsShouldProcess = true)]
public class SetPveNodeConfigCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>Node description/comment.</summary>
[Parameter(Mandatory = false, HelpMessage = "Node description or comment.")]
public string? Description { get; set; }
/// <summary>Wake-on-LAN MAC address or configuration.</summary>
[Parameter(Mandatory = false, HelpMessage = "Wake-on-LAN configuration.")]
public string? Wakeonlan { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Node, "Set PVE Node Config"))
return;
var config = new Dictionary<string, string>();
if (Description != null) config["description"] = Description;
if (!string.IsNullOrEmpty(Wakeonlan)) config["wakeonlan"] = Wakeonlan!;
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Updating configuration for node '{Node}'...");
service.SetNodeConfig(session, Node, config);
}
}
}
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Updates the DNS configuration of a Proxmox VE node.</para>
/// <para type="description">
/// Modifies the DNS settings (search domain and nameservers) for the specified
/// node via the /nodes/{node}/dns endpoint.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveNodeDns", SupportsShouldProcess = true)]
public class SetPveNodeDnsCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>DNS search domain.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The DNS search domain.")]
[ValidateNotNullOrEmpty]
public string Search { get; set; } = string.Empty;
/// <summary>Primary DNS server.</summary>
[Parameter(Mandatory = false, HelpMessage = "Primary DNS server address.")]
public string? Dns1 { get; set; }
/// <summary>Secondary DNS server.</summary>
[Parameter(Mandatory = false, HelpMessage = "Secondary DNS server address.")]
public string? Dns2 { get; set; }
/// <summary>Tertiary DNS server.</summary>
[Parameter(Mandatory = false, HelpMessage = "Tertiary DNS server address.")]
public string? Dns3 { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Node, "Set PVE Node DNS"))
return;
var config = new Dictionary<string, string>
{
["search"] = Search
};
if (!string.IsNullOrEmpty(Dns1)) config["dns1"] = Dns1!;
if (!string.IsNullOrEmpty(Dns2)) config["dns2"] = Dns2!;
if (!string.IsNullOrEmpty(Dns3)) config["dns3"] = Dns3!;
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Updating DNS configuration for node '{Node}'...");
service.SetNodeDns(session, Node, config);
}
}
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Starts all VMs and containers on a Proxmox VE node.</para>
/// <para type="description">
/// Triggers the start-all operation on the specified node, which starts all VMs and
/// containers that are configured to auto-start. Optionally limit to specific VM IDs.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Start, "PveNodeVms", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class StartPveNodeVmsCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>Comma-separated list of VM IDs to start. If omitted, all VMs are started.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of VM IDs to start.")]
public string? VmIds { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Node, "Start all VMs"))
return;
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(VmIds)) config["vms"] = VmIds!;
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Starting all VMs on node '{Node}'...");
var task = service.StartAll(session, Node, config);
WriteObject(task);
}
}
}
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
{
/// <summary>
/// <para type="synopsis">Stops all VMs and containers on a Proxmox VE node.</para>
/// <para type="description">
/// Triggers the stop-all operation on the specified node, which stops all running VMs
/// and containers. Optionally limit to specific VM IDs or force-stop.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Stop, "PveNodeVms",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(PveTask))]
public class StopPveNodeVmsCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>Comma-separated list of VM IDs to stop. If omitted, all VMs are stopped.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of VM IDs to stop.")]
public string? VmIds { get; set; }
/// <summary>Force stop VMs (do not wait for graceful shutdown).</summary>
[Parameter(Mandatory = false, HelpMessage = "Force stop VMs without waiting for graceful shutdown.")]
public SwitchParameter ForceStop { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Node, "Stop all VMs"))
return;
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(VmIds)) config["vms"] = VmIds!;
if (ForceStop.IsPresent) config["force-stop"] = "1";
var session = GetSession();
var service = new NodeService();
WriteVerbose($"Stopping all VMs on node '{Node}'...");
var task = service.StopAll(session, Node, config);
WriteObject(task);
}
}
}
@@ -0,0 +1,46 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Pools
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE resource pools.</para>
/// <para type="description">
/// Returns resource pool configurations from the Proxmox VE cluster.
/// Optionally filter by pool ID to retrieve a specific pool with its members.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PvePool")]
[OutputType(typeof(PvePool))]
public sealed class GetPvePoolCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The pool ID to retrieve. When omitted, all pools are returned.</para>
/// </summary>
[Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The pool ID to retrieve.")]
public string? PoolId { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new PoolService();
if (!string.IsNullOrEmpty(PoolId))
{
WriteVerbose($"Getting pool '{PoolId}'...");
var pool = service.GetPool(session, PoolId!);
WriteObject(pool);
}
else
{
WriteVerbose("Getting all pools...");
var pools = service.GetPools(session);
foreach (var pool in pools)
{
WriteObject(pool);
}
}
}
}
}
@@ -0,0 +1,40 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Pools
{
/// <summary>
/// <para type="synopsis">Creates a new resource pool in Proxmox VE.</para>
/// <para type="description">
/// Creates a new resource pool in the Proxmox VE cluster with the specified ID
/// and optional comment.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PvePool", SupportsShouldProcess = true)]
public sealed class NewPvePoolCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The pool ID to create.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The pool ID to create.")]
public string PoolId { get; set; } = string.Empty;
/// <summary>
/// <para type="description">An optional comment or description for the pool.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "An optional comment or description for the pool.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"pool '{PoolId}'", "New-PvePool"))
return;
var session = GetSession();
var service = new PoolService();
WriteVerbose($"Creating pool '{PoolId}'...");
service.CreatePool(session, PoolId, Comment);
}
}
}
@@ -0,0 +1,36 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Pools
{
/// <summary>
/// <para type="synopsis">Removes a resource pool from Proxmox VE.</para>
/// <para type="description">
/// Deletes a resource pool from the Proxmox VE cluster configuration.
/// This operation is destructive and requires confirmation unless -Force is specified.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PvePool",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public sealed class RemovePvePoolCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The pool ID to remove.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The pool ID to remove.")]
public string PoolId { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"pool '{PoolId}'", "Remove-PvePool"))
return;
var session = GetSession();
var service = new PoolService();
WriteVerbose($"Removing pool '{PoolId}'...");
service.RemovePool(session, PoolId);
}
}
}
@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Pools
{
/// <summary>
/// <para type="synopsis">Updates a resource pool in Proxmox VE.</para>
/// <para type="description">
/// Modifies an existing resource pool configuration. You can update the comment,
/// add members, or remove members.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PvePool", SupportsShouldProcess = true)]
public sealed class SetPvePoolCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The pool ID to update.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The pool ID to update.")]
public string PoolId { get; set; } = string.Empty;
/// <summary>
/// <para type="description">An optional comment or description for the pool.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "An optional comment or description for the pool.")]
public string? Comment { get; set; }
/// <summary>
/// <para type="description">Comma-separated list of members to add (e.g., "100,200,storage1").</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of members to add (e.g., '100,200,storage1').")]
public string? Members { get; set; }
/// <summary>
/// <para type="description">Comma-separated list of members to remove.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of members to remove.")]
public string? Delete { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"pool '{PoolId}'", "Set-PvePool"))
return;
var session = GetSession();
var service = new PoolService();
var config = new Dictionary<string, string>();
if (Comment != null)
config["comment"] = Comment;
if (!string.IsNullOrEmpty(Members))
config["members"] = Members!;
if (!string.IsNullOrEmpty(Delete))
config["delete"] = Delete!;
WriteVerbose($"Updating pool '{PoolId}'...");
service.UpdatePool(session, PoolId, config);
}
}
}
@@ -0,0 +1,41 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Storage;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Returns the status of a Proxmox VE storage on a node.</para>
/// <para type="description">
/// Retrieves capacity, usage and activation status for the specified storage
/// on the given node from the /nodes/{node}/storage/{storage}/status endpoint.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveStorageStatus")]
[OutputType(typeof(PveStorageStatus))]
public class GetPveStorageStatusCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>The storage identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The storage pool name.")]
[ValidateNotNullOrEmpty]
public string Storage { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var session = GetSession();
var service = new StorageService();
WriteVerbose($"Getting status for storage '{Storage}' on node '{Node}'...");
var status = service.GetStorageStatus(session, Node, Storage);
status.Storage = Storage;
status.Node = Node;
WriteObject(status);
}
}
}
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Allocates a new disk image on a Proxmox VE storage.</para>
/// <para type="description">
/// Creates a new disk image with the specified filename, size and optional format
/// on the given storage and node. Returns the volume ID of the newly created disk.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveStorageDisk", SupportsShouldProcess = true)]
[OutputType(typeof(string))]
public class NewPveStorageDiskCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>The storage identifier.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")]
[ValidateNotNullOrEmpty]
public string Storage { get; set; } = string.Empty;
/// <summary>The filename for the new disk (e.g. "vm-100-disk-1").</summary>
[Parameter(Mandatory = true, Position = 2, HelpMessage = "The filename for the new disk (e.g. vm-100-disk-1).")]
[ValidateNotNullOrEmpty]
public string Filename { get; set; } = string.Empty;
/// <summary>The size of the disk (e.g. "32G", "100M").</summary>
[Parameter(Mandatory = true, Position = 3, HelpMessage = "The size of the disk (e.g. 32G, 100M).")]
[ValidateNotNullOrEmpty]
public string Size { get; set; } = string.Empty;
/// <summary>The disk image format.</summary>
[Parameter(Mandatory = false, HelpMessage = "The disk image format (raw, qcow2, vmdk).")]
[ValidateSet("raw", "qcow2", "vmdk", IgnoreCase = true)]
public string? Format { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"{Filename} ({Size})", "New PVE Storage Disk"))
return;
var config = new Dictionary<string, string>
{
["filename"] = Filename,
["size"] = Size
};
if (!string.IsNullOrEmpty(Format))
config["format"] = Format!;
var session = GetSession();
var service = new StorageService();
WriteVerbose($"Allocating disk '{Filename}' ({Size}) on storage '{Storage}' on node '{Node}'...");
var task = service.AllocateDisk(session, Node, Storage, config);
WriteObject(task.Upid ?? string.Empty);
}
}
}
@@ -0,0 +1,45 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Removes a content volume from a Proxmox VE storage.</para>
/// <para type="description">
/// Deletes the specified volume (disk image, ISO, template, backup, etc.) from the
/// storage on the given node.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveStorageContent",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public class RemovePveStorageContentCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>The storage identifier.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")]
[ValidateNotNullOrEmpty]
public string Storage { get; set; } = string.Empty;
/// <summary>The volume identifier to remove (e.g. "local:iso/ubuntu.iso").</summary>
[Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true, HelpMessage = "The volume identifier to remove.")]
[ValidateNotNullOrEmpty]
public string Volume { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess(Volume, "Remove PVE Storage Content"))
return;
var session = GetSession();
var service = new StorageService();
WriteVerbose($"Removing volume '{Volume}' from storage '{Storage}' on node '{Node}'...");
service.RemoveContent(session, Node, Storage, Volume);
}
}
}
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE storage definition.</para>
/// <para type="description">
/// Modifies the configuration of an existing storage definition.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveStorage", SupportsShouldProcess = true)]
public sealed class SetPveStorageCmdlet : PveCmdletBase
{
/// <summary>The storage identifier to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The storage identifier.")]
public string Storage { get; set; } = string.Empty;
/// <summary>Comma-separated list of content types (e.g. 'images,iso,backup').</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated content types (e.g. 'images,iso,backup').")]
public string? Content { get; set; }
/// <summary>Comma-separated list of nodes that can access this storage.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of nodes.")]
public string? Nodes { get; set; }
/// <summary>Whether the storage is enabled.</summary>
[Parameter(Mandatory = false, HelpMessage = "Whether the storage is enabled.")]
public SwitchParameter Enabled { get; set; }
/// <summary>Whether the storage is shared across nodes.</summary>
[Parameter(Mandatory = false, HelpMessage = "Whether the storage is shared.")]
public SwitchParameter Shared { get; set; }
/// <summary>Additional configuration as a hashtable for parameters not covered by named parameters.</summary>
[Parameter(Mandatory = false, HelpMessage = "Additional configuration parameters as a hashtable.")]
public System.Collections.Hashtable? AdditionalConfig { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"storage '{Storage}'", "Set"))
return;
var session = GetSession();
var service = new StorageService();
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Content)) config["content"] = Content!;
if (!string.IsNullOrEmpty(Nodes)) config["nodes"] = Nodes!;
if (MyInvocation.BoundParameters.ContainsKey("Enabled"))
config["disable"] = Enabled.IsPresent ? "0" : "1";
if (MyInvocation.BoundParameters.ContainsKey("Shared"))
config["shared"] = Shared.IsPresent ? "1" : "0";
if (AdditionalConfig != null)
{
foreach (var key in AdditionalConfig.Keys)
config[key.ToString()!] = AdditionalConfig[key]?.ToString() ?? string.Empty;
}
WriteVerbose($"Updating storage '{Storage}'...");
service.UpdateStorage(session, Storage, config);
}
}
}
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Updates properties of a content volume in Proxmox VE storage.</para>
/// <para type="description">
/// Modifies metadata (such as notes/description) of an existing volume in the specified
/// storage on the given node.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveStorageContent", SupportsShouldProcess = true)]
public class SetPveStorageContentCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
[ValidateNotNullOrEmpty]
public string Node { get; set; } = string.Empty;
/// <summary>The storage identifier.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")]
[ValidateNotNullOrEmpty]
public string Storage { get; set; } = string.Empty;
/// <summary>The volume identifier to update.</summary>
[Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true, HelpMessage = "The volume identifier to update.")]
[ValidateNotNullOrEmpty]
public string Volume { get; set; } = string.Empty;
/// <summary>Notes/description to set on the volume.</summary>
[Parameter(Mandatory = false, HelpMessage = "Notes or description for the volume.")]
public string? Notes { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Volume, "Set PVE Storage Content"))
return;
var config = new Dictionary<string, string>();
if (Notes != null) config["notes"] = Notes;
var session = GetSession();
var service = new StorageService();
WriteVerbose($"Updating volume '{Volume}' on storage '{Storage}' on node '{Node}'...");
service.UpdateContent(session, Node, Storage, Volume, config);
}
}
}
@@ -0,0 +1,65 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Tasks
{
/// <summary>
/// <para type="synopsis">Lists tasks on a Proxmox VE node.</para>
/// <para type="description">
/// Returns a list of recent tasks on the specified node. Use optional parameters
/// to filter by VM ID, source, or task type. Unlike Get-PveTask which retrieves
/// a single task by UPID, this cmdlet lists multiple tasks.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveTaskList")]
[OutputType(typeof(PveTask))]
public sealed class GetPveTaskListCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node to list tasks from.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">Filter tasks by VM ID.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Filter tasks by VM ID.")]
public int? VmId { get; set; }
/// <summary>
/// <para type="description">Filter by task source: all or active.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Filter by task source: all or active.")]
[ValidateSet("all", "active")]
public string? Source { get; set; }
/// <summary>
/// <para type="description">Filter by task type (e.g., qmstart, vzdump).</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Filter by task type (e.g., qmstart, vzdump).")]
public string? TypeFilter { get; set; }
/// <summary>
/// <para type="description">Maximum number of tasks to return. Defaults to 50.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum number of tasks to return. Defaults to 50.")]
[ValidateRange(1, 10000)]
public int Limit { get; set; } = 50;
protected override void ProcessRecord()
{
var session = GetSession();
var service = new TaskService();
WriteVerbose($"Getting tasks on node '{Node}' (limit={Limit})...");
var tasks = service.GetTasks(session, Node, VmId, Source, TypeFilter, Limit);
foreach (var task in tasks)
{
WriteObject(task);
}
}
}
}
@@ -0,0 +1,42 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Tasks
{
/// <summary>
/// <para type="synopsis">Stops a running task on a Proxmox VE node.</para>
/// <para type="description">
/// Cancels a running task identified by its UPID on the specified node.
/// This is a destructive operation and requires confirmation.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Stop, "PveTask",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public sealed class StopPveTaskCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the task is running.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The UPID of the task to stop.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The task UPID.")]
public string Upid { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"task '{Upid}' on node '{Node}'", "Stop-PveTask"))
return;
var session = GetSession();
var service = new TaskService();
WriteVerbose($"Stopping task '{Upid}' on node '{Node}'...");
service.StopTask(session, Node, Upid);
}
}
}
@@ -0,0 +1,42 @@
using System.Linq;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Users;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE authentication domains/realms.</para>
/// <para type="description">
/// Returns all authentication domains (realms) from the Proxmox VE access management
/// system. Optionally filter by a specific realm name.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveDomain")]
[OutputType(typeof(PveDomain))]
public class GetPveDomainCmdlet : PveCmdletBase
{
/// <summary>Optional realm name to filter by.</summary>
[Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Optional realm name to filter results.")]
public string? Realm { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new UserService();
WriteVerbose("Getting authentication domains...");
var domains = service.GetDomains(session);
if (!string.IsNullOrEmpty(Realm))
{
domains = domains.Where(d => d.Realm == Realm).ToArray();
}
foreach (var domain in domains)
{
WriteObject(domain);
}
}
}
}
@@ -0,0 +1,42 @@
using System.Linq;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Users;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE groups.</para>
/// <para type="description">
/// Returns all groups from the Proxmox VE access management system. Optionally
/// filter by a specific group ID.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveGroup")]
[OutputType(typeof(PveGroup))]
public class GetPveGroupCmdlet : PveCmdletBase
{
/// <summary>Optional group ID to filter by.</summary>
[Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "Optional group ID to filter results.")]
public string? GroupId { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new UserService();
WriteVerbose("Getting groups...");
var groups = service.GetGroups(session);
if (!string.IsNullOrEmpty(GroupId))
{
groups = groups.Where(g => g.GroupId == GroupId).ToArray();
}
foreach (var group in groups)
{
WriteObject(group);
}
}
}
}
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Users;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Creates a new Proxmox VE authentication domain/realm.</para>
/// <para type="description">
/// Adds a new authentication domain (realm) to the Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveDomain", SupportsShouldProcess = true)]
[OutputType(typeof(PveDomain))]
public class NewPveDomainCmdlet : PveCmdletBase
{
/// <summary>The realm identifier.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The realm identifier.")]
[ValidateNotNullOrEmpty]
public string Realm { get; set; } = string.Empty;
/// <summary>The domain type.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The domain type (pam, pve, ad, ldap, openid).")]
[ValidateSet("pam", "pve", "ad", "ldap", "openid", IgnoreCase = true)]
public string Type { get; set; } = string.Empty;
/// <summary>Optional comment/description for the domain.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comment or description for the domain.")]
public string? Comment { get; set; }
/// <summary>Set this realm as the default authentication domain.</summary>
[Parameter(Mandatory = false, HelpMessage = "Set as the default authentication domain.")]
public SwitchParameter Default { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Realm, "Create PVE Domain"))
return;
var config = new Dictionary<string, string>
{
["realm"] = Realm,
["type"] = Type
};
if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment!;
if (Default.IsPresent) config["default"] = "1";
var session = GetSession();
var service = new UserService();
WriteVerbose($"Creating domain '{Realm}' of type '{Type}'...");
service.CreateDomain(session, config);
WriteObject(new PveDomain
{
Realm = Realm,
Type = Type,
Comment = Comment,
Default = Default.IsPresent ? 1 : 0
});
}
}
}
@@ -0,0 +1,44 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Users;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Creates a new Proxmox VE group.</para>
/// <para type="description">
/// Adds a new group to the Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveGroup", SupportsShouldProcess = true)]
[OutputType(typeof(PveGroup))]
public class NewPveGroupCmdlet : PveCmdletBase
{
/// <summary>The group identifier.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The group identifier.")]
[ValidateNotNullOrEmpty]
public string GroupId { get; set; } = string.Empty;
/// <summary>Optional comment/description for the group.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comment or description for the group.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(GroupId, "Create PVE Group"))
return;
var session = GetSession();
var service = new UserService();
WriteVerbose($"Creating group '{GroupId}'...");
service.CreateGroup(session, GroupId, Comment);
WriteObject(new PveGroup
{
GroupId = GroupId,
Comment = Comment
});
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Removes a Proxmox VE authentication domain/realm.</para>
/// <para type="description">
/// Deletes the specified authentication domain (realm) from the Proxmox VE
/// access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveDomain",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public class RemovePveDomainCmdlet : PveCmdletBase
{
/// <summary>The realm identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The realm identifier to remove.")]
[ValidateNotNullOrEmpty]
public string Realm { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess(Realm, "Remove PVE Domain"))
return;
var session = GetSession();
var service = new UserService();
WriteVerbose($"Removing domain '{Realm}'...");
service.RemoveDomain(session, Realm);
}
}
}
@@ -0,0 +1,34 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Removes a Proxmox VE group.</para>
/// <para type="description">
/// Deletes the specified group from the Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveGroup",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public class RemovePveGroupCmdlet : PveCmdletBase
{
/// <summary>The group identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The group identifier to remove.")]
[ValidateNotNullOrEmpty]
public string GroupId { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess(GroupId, "Remove PVE Group"))
return;
var session = GetSession();
var service = new UserService();
WriteVerbose($"Removing group '{GroupId}'...");
service.RemoveGroup(session, GroupId);
}
}
}
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE API token configuration.</para>
/// <para type="description">
/// Modifies properties of an existing API token such as its comment,
/// expiration, or privilege separation setting.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveApiToken", SupportsShouldProcess = true)]
public sealed class SetPveApiTokenCmdlet : PveCmdletBase
{
/// <summary>The user ID that owns the token (e.g. 'user@realm').</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The user ID (e.g. 'user@realm').")]
public string UserId { get; set; } = string.Empty;
/// <summary>The token identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The token identifier.")]
public string TokenId { get; set; } = string.Empty;
/// <summary>Optional comment for the token.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comment describing the token.")]
public string? Comment { get; set; }
/// <summary>Token expiration as a Unix epoch timestamp. Set to 0 for no expiration.</summary>
[Parameter(Mandatory = false, HelpMessage = "Expiration as Unix epoch (0 = no expiration).")]
public long? Expire { get; set; }
/// <summary>Whether the token uses privilege separation.</summary>
[Parameter(Mandatory = false, HelpMessage = "Whether the token uses privilege separation.")]
public SwitchParameter PrivilegeSeparation { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"API token '{UserId}!{TokenId}'", "Set"))
return;
var session = GetSession();
var service = new UserService();
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment!;
if (Expire.HasValue) config["expire"] = Expire.Value.ToString();
if (MyInvocation.BoundParameters.ContainsKey("PrivilegeSeparation"))
config["privsep"] = PrivilegeSeparation.IsPresent ? "1" : "0";
WriteVerbose($"Updating API token '{UserId}!{TokenId}'...");
service.UpdateApiToken(session, UserId, TokenId, config);
}
}
}
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE authentication domain/realm.</para>
/// <para type="description">
/// Modifies properties of an existing authentication domain (realm) in the
/// Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveDomain", SupportsShouldProcess = true)]
public class SetPveDomainCmdlet : PveCmdletBase
{
/// <summary>The realm identifier to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The realm identifier.")]
[ValidateNotNullOrEmpty]
public string Realm { get; set; } = string.Empty;
/// <summary>Updated comment/description for the domain.</summary>
[Parameter(Mandatory = false, HelpMessage = "Updated comment or description for the domain.")]
public string? Comment { get; set; }
/// <summary>Set this realm as the default authentication domain.</summary>
[Parameter(Mandatory = false, HelpMessage = "Set as the default authentication domain.")]
public SwitchParameter Default { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(Realm, "Set PVE Domain"))
return;
var config = new Dictionary<string, string>();
if (Comment != null) config["comment"] = Comment;
if (Default.IsPresent) config["default"] = "1";
var session = GetSession();
var service = new UserService();
WriteVerbose($"Updating domain '{Realm}'...");
service.UpdateDomain(session, Realm, config);
}
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE group.</para>
/// <para type="description">
/// Modifies properties of an existing group in the Proxmox VE access management system.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveGroup", SupportsShouldProcess = true)]
public class SetPveGroupCmdlet : PveCmdletBase
{
/// <summary>The group identifier to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The group identifier.")]
[ValidateNotNullOrEmpty]
public string GroupId { get; set; } = string.Empty;
/// <summary>Updated comment/description for the group.</summary>
[Parameter(Mandatory = false, HelpMessage = "Updated comment or description for the group.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess(GroupId, "Set PVE Group"))
return;
var config = new Dictionary<string, string>();
if (Comment != null) config["comment"] = Comment;
var session = GetSession();
var service = new UserService();
WriteVerbose($"Updating group '{GroupId}'...");
service.UpdateGroup(session, GroupId, config);
}
}
}
@@ -0,0 +1,49 @@
using System.Management.Automation;
using System.Runtime.InteropServices;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Changes a Proxmox VE user's password.</para>
/// <para type="description">
/// Updates the password for the specified user via the /access/password endpoint.
/// The password is accepted as a SecureString for safe handling in PowerShell.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PvePassword", SupportsShouldProcess = true)]
public class SetPvePasswordCmdlet : PveCmdletBase
{
/// <summary>The user identifier in "user@realm" format.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The user ID in user@realm format.")]
[ValidateNotNullOrEmpty]
public string UserId { get; set; } = string.Empty;
/// <summary>The new password for the user.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The new password for the user.")]
public System.Security.SecureString Password { get; set; } = new System.Security.SecureString();
protected override void ProcessRecord()
{
if (!ShouldProcess(UserId, "Set PVE Password"))
return;
var ptr = Marshal.SecureStringToGlobalAllocUnicode(Password);
string plainPassword;
try
{
plainPassword = Marshal.PtrToStringUni(ptr) ?? string.Empty;
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(ptr);
}
var session = GetSession();
var service = new UserService();
WriteVerbose($"Changing password for user '{UserId}'...");
service.ChangePassword(session, UserId, plainPassword);
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Users
{
/// <summary>
/// <para type="synopsis">Updates a Proxmox VE role's privileges.</para>
/// <para type="description">
/// Modifies the privilege set for an existing custom role.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveRole", SupportsShouldProcess = true)]
public sealed class SetPveRoleCmdlet : PveCmdletBase
{
/// <summary>The role identifier to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The role identifier.")]
public string RoleId { get; set; } = string.Empty;
/// <summary>Comma-separated list of privileges to assign to the role.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "Comma-separated list of privileges (e.g. 'VM.Audit,VM.Console').")]
public string Privileges { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"role '{RoleId}'", "Set"))
return;
var session = GetSession();
var service = new UserService();
WriteVerbose($"Updating role '{RoleId}'...");
service.UpdateRole(session, RoleId, Privileges);
}
}
}
@@ -0,0 +1,40 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Gets filesystem information from the QEMU guest agent.</para>
/// <para type="description">
/// Queries the QEMU guest agent running inside the specified VM for its filesystem
/// information, including mount points, filesystem types, and usage statistics.
/// The guest agent must be installed and running inside the VM.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveVmGuestFsInfo")]
[OutputType(typeof(PveGuestFsInfo))]
public sealed class GetPveVmGuestFsInfoCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
WriteVerbose($"Getting guest filesystem info for VM {VmId}...");
var service = new VmService();
var fsInfos = service.GetGuestFsInfo(session, Node, VmId);
foreach (var fs in fsInfos)
WriteObject(fs);
}
}
}
@@ -0,0 +1,40 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Gets OS information from the QEMU guest agent.</para>
/// <para type="description">
/// Queries the QEMU guest agent running inside the specified VM for its operating system
/// information, including OS name, kernel version, and machine type.
/// The guest agent must be installed and running inside the VM.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveVmGuestOsInfo")]
[OutputType(typeof(PveGuestOsInfo))]
public sealed class GetPveVmGuestOsInfoCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
WriteVerbose($"Getting guest OS info for VM {VmId}...");
var service = new VmService();
var osInfo = service.GetGuestOsInfo(session, Node, VmId);
if (osInfo != null)
WriteObject(osInfo);
}
}
}
@@ -0,0 +1,38 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Invokes an fstrim operation inside the guest via the QEMU guest agent.</para>
/// <para type="description">
/// Triggers an fstrim (filesystem trim) operation inside the guest operating system
/// using the QEMU guest agent. This reclaims unused space on thin-provisioned storage.
/// The guest agent must be installed and running inside the VM.
/// </para>
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "PveVmGuestFsTrim", SupportsShouldProcess = true)]
public sealed class InvokePveVmGuestFsTrimCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Invoke-PveVmGuestFsTrim"))
return;
var session = GetSession();
WriteVerbose($"Invoking fstrim on VM {VmId} via guest agent...");
var service = new VmService();
service.GuestFsTrim(session, Node, VmId);
}
}
}
@@ -0,0 +1,83 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Moves a VM disk to a different storage.</para>
/// <para type="description">
/// Moves the specified disk on a QEMU/KVM virtual machine to a different storage backend
/// via the Proxmox VE API. Optionally deletes the original disk after a successful move.
/// Use -Wait to block until the move task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Move, "PveVmDisk", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class MovePveVmDiskCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the VM resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the VM whose disk should be moved.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The disk slot to move (e.g., "scsi0", "virtio0").</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The disk slot to move (e.g. scsi0, virtio0).")]
public string Disk { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The target storage to move the disk to.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The target storage identifier.")]
public string Storage { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The target disk format.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Target disk format (raw, qcow2, vmdk).")]
[ValidateSet("raw", "qcow2", "vmdk")]
public string? Format { get; set; }
/// <summary>
/// <para type="description">When specified, deletes the original disk after a successful move. Default is true.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Delete the original disk after move (default true).")]
public SwitchParameter Delete { get; set; } = true;
/// <summary>
/// <para type="description">When specified, waits for the move task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"Disk '{Disk}' on VM {VmId} (node '{Node}') to storage '{Storage}'", "Move-PveVmDisk"))
return;
var session = GetSession();
var vmService = new VmService();
WriteVerbose($"Moving disk '{Disk}' on VM {VmId} to storage '{Storage}'...");
var task = vmService.MoveDisk(session, Node, VmId, Disk, Storage, Format, Delete.IsPresent);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
}
WriteObject(task);
}
}
}
@@ -0,0 +1,44 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Reads a file from the guest filesystem via the QEMU guest agent.</para>
/// <para type="description">
/// Reads the contents of a file inside the guest operating system using the QEMU guest agent.
/// The guest agent must be installed and running inside the VM.
/// Returns the file content as a string.
/// </para>
/// </summary>
[Cmdlet(VerbsCommunications.Read, "PveVmGuestFile")]
[OutputType(typeof(string))]
public sealed class ReadPveVmGuestFileCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The path to the file inside the guest to read.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The file path inside the guest to read.")]
public string File { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var session = GetSession();
WriteVerbose($"Reading file '{File}' from VM {VmId} via guest agent...");
var service = new VmService();
var content = service.ReadGuestFile(session, Node, VmId, File);
WriteObject(content);
}
}
}
@@ -0,0 +1,55 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Removes (unlinks) disks from a QEMU/KVM virtual machine.</para>
/// <para type="description">
/// Detaches and optionally destroys one or more disks from a virtual machine
/// via the Proxmox VE API. This operation is destructive when -Force is used.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveVmDisk",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public sealed class RemovePveVmDiskCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the VM resides.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the VM whose disks should be removed.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">Comma-separated list of disk IDs to unlink (e.g., "scsi0,scsi1").</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "Comma-separated list of disk IDs to unlink (e.g. scsi0,scsi1).")]
public string IdList { get; set; } = string.Empty;
/// <summary>
/// <para type="description">When specified, forces removal of the disk images from storage.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Force removal of disk images from storage.")]
public SwitchParameter Force { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"Disks '{IdList}' on VM {VmId} (node '{Node}')", "Remove-PveVmDisk"))
return;
var session = GetSession();
var vmService = new VmService();
WriteVerbose($"Unlinking disks '{IdList}' from VM {VmId}...");
vmService.UnlinkDisk(session, Node, VmId, IdList, Force.IsPresent);
}
}
}
@@ -0,0 +1,56 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Sets a user password inside the guest via the QEMU guest agent.</para>
/// <para type="description">
/// Changes a user's password inside the guest operating system using the QEMU guest agent.
/// The guest agent must be installed and running inside the VM.
/// Use -Crypted if the password is already in a hashed/crypted format.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveVmGuestPassword", SupportsShouldProcess = true)]
public sealed class SetPveVmGuestPasswordCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The username whose password should be changed.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The username whose password to set.")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The new password for the user.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The new password.")]
public string Password { get; set; } = string.Empty;
/// <summary>
/// <para type="description">When specified, indicates the password is already in crypted/hashed format.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Indicates the password is already crypted/hashed.")]
public SwitchParameter Crypted { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"User '{Username}' on VM {VmId} (node '{Node}')", "Set-PveVmGuestPassword"))
return;
var session = GetSession();
WriteVerbose($"Setting password for user '{Username}' on VM {VmId} via guest agent...");
var service = new VmService();
service.SetGuestPassword(session, Node, VmId, Username, Password, Crypted.IsPresent);
}
}
}
@@ -0,0 +1,49 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Vms
{
/// <summary>
/// <para type="synopsis">Writes content to a file on the guest filesystem via the QEMU guest agent.</para>
/// <para type="description">
/// Writes the specified content to a file inside the guest operating system using the QEMU guest agent.
/// The guest agent must be installed and running inside the VM.
/// </para>
/// </summary>
[Cmdlet(VerbsCommunications.Write, "PveVmGuestFile", SupportsShouldProcess = true)]
public sealed class WritePveVmGuestFileCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The VM identifier.</summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The path to the file inside the guest to write.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The file path inside the guest to write.")]
public string File { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The content to write to the file.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The content to write to the file.")]
public string Content { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"File '{File}' on VM {VmId} (node '{Node}')", "Write-PveVmGuestFile"))
return;
var session = GetSession();
WriteVerbose($"Writing to file '{File}' on VM {VmId} via guest agent...");
var service = new VmService();
service.WriteGuestFile(session, Node, VmId, File, Content);
}
}
}
+412
View File
@@ -944,5 +944,417 @@
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Cluster.PveClusterResource -->
<View>
<Name>PSProxmoxVE.Core.Models.Cluster.PveClusterResource</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Cluster.PveClusterResource</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Type</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Name</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Status</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Node</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>VmId</Label>
<Width>8</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Cpu</Label>
<Width>8</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>MaxMem</Label>
<Width>12</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Pool</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Status</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Node</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>VmId</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Cpu -ne $null) { [math]::Round($_.Cpu * 100, 1).ToString() + '%' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.MaxMem) { [math]::Round($_.MaxMem / 1GB, 1).ToString() + 'G' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Pool</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Cluster.PvePool -->
<View>
<Name>PSProxmoxVE.Core.Models.Cluster.PvePool</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Cluster.PvePool</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>PoolId</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>PoolId</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Vms.PveGuestOsInfo -->
<View>
<Name>PSProxmoxVE.Core.Models.Vms.PveGuestOsInfo</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Vms.PveGuestOsInfo</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>KernelRelease</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Machine</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>KernelRelease</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Machine</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Vms.PveGuestFsInfo -->
<View>
<Name>PSProxmoxVE.Core.Models.Vms.PveGuestFsInfo</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Vms.PveGuestFsInfo</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>MountPoint</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>TotalBytes</Label>
<Width>15</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>UsedBytes</Label>
<Width>15</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>MountPoint</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.TotalBytes) { [math]::Round($_.TotalBytes / 1GB, 1).ToString() + ' GB' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.UsedBytes) { [math]::Round($_.UsedBytes / 1GB, 1).ToString() + ' GB' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Containers.PveContainerInterface -->
<View>
<Name>PSProxmoxVE.Core.Models.Containers.PveContainerInterface</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Containers.PveContainerInterface</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>HwAddr</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Inet</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Inet6</Label>
<Width>30</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>HwAddr</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Inet</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Inet6</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Storage.PveStorageStatus -->
<View>
<Name>PSProxmoxVE.Core.Models.Storage.PveStorageStatus</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Storage.PveStorageStatus</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Type</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Total</Label>
<Width>15</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Used</Label>
<Width>15</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Available</Label>
<Width>15</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Active</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Total) { [math]::Round($_.Total / 1GB, 1).ToString() + ' GB' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Used) { [math]::Round($_.Used / 1GB, 1).ToString() + ' GB' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Available) { [math]::Round($_.Available / 1GB, 1).ToString() + ' GB' } else { 'N/A' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Active) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Users.PveGroup -->
<View>
<Name>PSProxmoxVE.Core.Models.Users.PveGroup</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Users.PveGroup</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>GroupId</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Users</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>GroupId</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Users</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Users.PveDomain -->
<View>
<Name>PSProxmoxVE.Core.Models.Users.PveDomain</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Users.PveDomain</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Realm</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Default</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Realm</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Default) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
+80 -1
View File
@@ -215,7 +215,86 @@
# SDN — Controller
'Get-PveSdnController',
'New-PveSdnController',
'Remove-PveSdnController'
'Remove-PveSdnController',
# SDN — Update / Apply
'Set-PveSdnZone',
'Set-PveSdnVnet',
'Set-PveSdnSubnet',
'Set-PveSdnController',
'Set-PveSdnIpam',
'Set-PveSdnDns',
'Invoke-PveSdnApply',
# Role / Storage / Token — Update
'Set-PveRole',
'Set-PveStorage',
'Set-PveApiToken',
# Cluster
'Get-PveClusterResource',
# Tasks
'Get-PveTaskList',
'Stop-PveTask',
# Pools
'Get-PvePool',
'New-PvePool',
'Set-PvePool',
'Remove-PvePool',
# Backup Compliance
'Get-PveBackupInfo',
# VM Disk Operations
'Move-PveVmDisk',
'Remove-PveVmDisk',
# VM Guest Agent Extensions
'Get-PveVmGuestOsInfo',
'Get-PveVmGuestFsInfo',
'Read-PveVmGuestFile',
'Write-PveVmGuestFile',
'Set-PveVmGuestPassword',
'Invoke-PveVmGuestFsTrim',
# Container Gaps
'Suspend-PveContainer',
'Resume-PveContainer',
'Resize-PveContainerDisk',
'New-PveContainerTemplate',
'Move-PveContainerVolume',
'Get-PveContainerInterface',
# Storage Content Management
'Get-PveStorageStatus',
'Remove-PveStorageContent',
'Set-PveStorageContent',
'New-PveStorageDisk',
# Node Operations
'Get-PveNodeConfig',
'Set-PveNodeConfig',
'Get-PveNodeDns',
'Set-PveNodeDns',
'Start-PveNodeVms',
'Stop-PveNodeVms',
# Access — Groups
'Get-PveGroup',
'New-PveGroup',
'Set-PveGroup',
'Remove-PveGroup',
# Access — Domains / Realms
'Get-PveDomain',
'New-PveDomain',
'Set-PveDomain',
'Remove-PveDomain',
# Access — Password
'Set-PvePassword'
)
# Variables to export from this module