diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c1c0dd..f6a1247 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,19 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
- SDN Controller cmdlets (3): Get/New/Remove-PveSdnController for controller management
- PSGallery version badge in README
- Integration tests for firewall rules, aliases, IP sets, backup jobs, and OVA import
+- SDN Update cmdlets (7): Set-PveSdnZone/Vnet/Subnet/Controller/Ipam/Dns + Invoke-PveSdnApply
+- Set-PveRole, Set-PveStorage, Set-PveApiToken for missing update operations
+- Get-PveClusterResource: single-call cluster-wide inventory of all VMs, containers, nodes, storage
+- Task management: Get-PveTaskList (list tasks on node), Stop-PveTask (cancel running tasks)
+- Pool management cmdlets (4): Get/New/Set/Remove-PvePool
+- Get-PveBackupInfo: find VMs/containers not covered by backup jobs
+- VM disk operations: Move-PveVmDisk (storage migration), Remove-PveVmDisk (detach/delete)
+- Guest agent extensions (6): Get-PveVmGuestOsInfo, Get-PveVmGuestFsInfo, Read/Write-PveVmGuestFile, Set-PveVmGuestPassword, Invoke-PveVmGuestFsTrim
+- Container gaps (6): Suspend/Resume-PveContainer, Resize-PveContainerDisk, New-PveContainerTemplate, Move-PveContainerVolume, Get-PveContainerInterface
+- Storage content management (4): Get-PveStorageStatus, Remove/Set-PveStorageContent, New-PveStorageDisk
+- Node operations (6): Get/Set-PveNodeConfig, Get/Set-PveNodeDns, Start/Stop-PveNodeVms
+- Access management (9): Get/New/Set/Remove-PveGroup, Get/New/Set/Remove-PveDomain, Set-PvePassword
+- Two-tier version gating: introduced vs default version with clear user messaging
### Fixed
diff --git a/README.md b/README.md
index b573b1e..738c5ac 100644
--- a/README.md
+++ b/README.md
@@ -359,19 +359,100 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
| `New-PveBackupJob` | Create a scheduled backup job |
| `Set-PveBackupJob` | Update a scheduled backup job |
| `Remove-PveBackupJob` | Delete a scheduled backup job |
+| `Get-PveBackupInfo` | Find VMs/containers not covered by backup jobs |
### SDN — IPAM / DNS / Controllers (PVE 8.0+)
| Cmdlet | Description |
|---|---|
| `Get-PveSdnIpam` | List SDN IPAM plugins |
| `New-PveSdnIpam` | Create an SDN IPAM plugin |
+| `Set-PveSdnIpam` | Update an SDN IPAM plugin |
| `Remove-PveSdnIpam` | Remove an SDN IPAM plugin |
| `Get-PveSdnDns` | List SDN DNS plugins |
| `New-PveSdnDns` | Create an SDN DNS plugin |
+| `Set-PveSdnDns` | Update an SDN DNS plugin |
| `Remove-PveSdnDns` | Remove an SDN DNS plugin |
| `Get-PveSdnController` | List SDN controllers |
| `New-PveSdnController` | Create an SDN controller |
+| `Set-PveSdnController` | Update an SDN controller |
| `Remove-PveSdnController` | Remove an SDN controller |
+| `Invoke-PveSdnApply` | Apply pending SDN configuration changes |
+| `Set-PveSdnZone` | Update an SDN zone |
+| `Set-PveSdnVnet` | Update an SDN VNet |
+| `Set-PveSdnSubnet` | Update an SDN subnet |
+
+### Cluster
+| Cmdlet | Description |
+|---|---|
+| `Get-PveClusterResource` | List all resources (VMs, containers, nodes, storage) cluster-wide |
+
+### Pools
+| Cmdlet | Description |
+|---|---|
+| `Get-PvePool` | List resource pools |
+| `New-PvePool` | Create a resource pool |
+| `Set-PvePool` | Update a resource pool |
+| `Remove-PvePool` | Delete a resource pool |
+
+### VM Disk Operations
+| Cmdlet | Description |
+|---|---|
+| `Move-PveVmDisk` | Move a VM disk to a different storage |
+| `Remove-PveVmDisk` | Detach and optionally delete a VM disk |
+
+### Guest Agent Extensions
+| Cmdlet | Description |
+|---|---|
+| `Get-PveVmGuestOsInfo` | Get guest OS information via QEMU agent |
+| `Get-PveVmGuestFsInfo` | Get guest filesystem information |
+| `Read-PveVmGuestFile` | Read a file from inside a guest VM |
+| `Write-PveVmGuestFile` | Write a file inside a guest VM |
+| `Set-PveVmGuestPassword` | Change a user password inside a guest VM |
+| `Invoke-PveVmGuestFsTrim` | TRIM guest VM filesystems |
+
+### Additional Container Operations
+| Cmdlet | Description |
+|---|---|
+| `Suspend-PveContainer` | Suspend (freeze) a container |
+| `Resume-PveContainer` | Resume a suspended container |
+| `Resize-PveContainerDisk` | Resize a container disk/volume |
+| `New-PveContainerTemplate` | Convert a container to a template |
+| `Move-PveContainerVolume` | Move a container volume to a different storage |
+| `Get-PveContainerInterface` | Get container network interfaces |
+
+### Storage Content
+| Cmdlet | Description |
+|---|---|
+| `Get-PveStorageStatus` | Get storage usage statistics |
+| `Remove-PveStorageContent` | Delete a volume, backup, or ISO from storage |
+| `Set-PveStorageContent` | Update volume notes/properties |
+| `New-PveStorageDisk` | Allocate a new empty disk image |
+
+### Node Operations
+| Cmdlet | Description |
+|---|---|
+| `Get-PveNodeConfig` | Get node configuration |
+| `Set-PveNodeConfig` | Update node configuration |
+| `Get-PveNodeDns` | Get node DNS configuration |
+| `Set-PveNodeDns` | Update node DNS configuration |
+| `Start-PveNodeVms` | Start all VMs on a node |
+| `Stop-PveNodeVms` | Stop all VMs on a node |
+
+### Access — Groups & Domains
+| Cmdlet | Description |
+|---|---|
+| `Get-PveGroup` | List user groups |
+| `New-PveGroup` | Create a user group |
+| `Set-PveGroup` | Update a user group |
+| `Remove-PveGroup` | Delete a user group |
+| `Get-PveDomain` | List authentication realms (PAM, LDAP, AD, OpenID) |
+| `New-PveDomain` | Create an authentication realm |
+| `Set-PveDomain` | Update an authentication realm |
+| `Remove-PveDomain` | Delete an authentication realm |
+| `Set-PvePassword` | Change a user's password |
+| `Set-PveRole` | Update a role's privileges |
+| `Set-PveStorage` | Update a storage definition |
+| `Set-PveApiToken` | Update an API token |
## Known Limitations (v1)
diff --git a/docs/PVE_API_COVERAGE.md b/docs/PVE_API_COVERAGE.md
index 1e204f0..4fa765d 100644
--- a/docs/PVE_API_COVERAGE.md
+++ b/docs/PVE_API_COVERAGE.md
@@ -4,7 +4,7 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
**Last updated:** 2026-03-21
**Module version:** 0.1.0-preview
-**Total cmdlets:** 110
+**Total cmdlets:** 169
## Implemented
@@ -27,12 +27,26 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
| **API Tokens** | 3 | `/access/users/{userid}/tokens/*` (CRUD) |
| **Templates** | 4 | VM template conversion, listing, cloning, removal |
| **Cloud-Init** | 3 | `/nodes/{node}/qemu/{vmid}/config` (cloud-init fields), `/nodes/{node}/qemu/{vmid}/cloudinit/regenerate` |
-| **Tasks** | 2 | `/nodes/{node}/tasks/{upid}/status` (get, wait) |
+| **Tasks** | 4 | `/nodes/{node}/tasks` (list, get status, stop, wait) |
| **Firewall** | 21 | `/cluster/firewall/*`, `/nodes/{node}/firewall/*`, `/nodes/{node}/qemu/{vmid}/firewall/*`, `/nodes/{node}/lxc/{vmid}/firewall/*` (rules, groups, aliases, IP sets, options, refs) |
-| **Backup** | 5 | `/nodes/{node}/vzdump`, `/cluster/backup/*` (ad-hoc backup, scheduled job CRUD) |
-| **SDN IPAM** | 3 | `/cluster/sdn/ipams/*` (CRUD) |
-| **SDN DNS** | 3 | `/cluster/sdn/dns/*` (CRUD) |
-| **SDN Controllers** | 3 | `/cluster/sdn/controllers/*` (CRUD) |
+| **Backup** | 7 | `/nodes/{node}/vzdump`, `/cluster/backup/*`, `/cluster/backup-info/not-backed-up` |
+| **SDN Zones** | 4 | `/cluster/sdn/zones/*` (CRUD + update) |
+| **SDN VNets** | 4 | `/cluster/sdn/vnets/*` (CRUD + update) |
+| **SDN Subnets** | 4 | `/cluster/sdn/vnets/{vnet}/subnets/*` (CRUD + update) |
+| **SDN IPAM** | 4 | `/cluster/sdn/ipams/*` (CRUD + update) |
+| **SDN DNS** | 4 | `/cluster/sdn/dns/*` (CRUD + update) |
+| **SDN Controllers** | 4 | `/cluster/sdn/controllers/*` (CRUD + update) |
+| **SDN Apply** | 1 | `PUT /cluster/sdn` (apply pending changes) |
+| **Cluster** | 1 | `GET /cluster/resources` (cluster-wide inventory) |
+| **Pools** | 4 | `/pools/*` (CRUD) |
+| **VM Disk Ops** | 2 | `move_disk`, `unlink` |
+| **Guest Agent (ext)** | 6 | `get-osinfo`, `get-fsinfo`, `file-read`, `file-write`, `set-user-password`, `fstrim` |
+| **Container Ops** | 6 | `suspend`, `resume`, `resize`, `template`, `move_volume`, `interfaces` |
+| **Storage Content** | 4 | `status`, `content` DELETE/PUT/POST |
+| **Node Ops** | 6 | `config`, `dns`, `startall`, `stopall` |
+| **Access Groups** | 4 | `/access/groups/*` (CRUD) |
+| **Access Domains** | 4 | `/access/domains/*` (CRUD) |
+| **Access Password** | 1 | `PUT /access/password` |
## Not Yet Implemented
@@ -40,7 +54,6 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
| Area | Key Endpoints | Notes | Priority |
|------|--------------|-------|----------|
-| **Pool Management** | `/pools/*` | Multi-tenant environments, resource grouping | Medium |
### Lower Priority Gaps
diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PveClusterResource.cs b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterResource.cs
new file mode 100644
index 0000000..b494c66
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterResource.cs
@@ -0,0 +1,129 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents a resource entry returned by the /cluster/resources endpoint.
+/// Resources can be VMs, containers, nodes, storage, or SDN objects.
+///
+public class PveClusterResource
+{
+ ///
+ /// The unique resource identifier (e.g., "node/pve1", "qemu/100", "storage/local").
+ ///
+ [JsonPropertyName("id")]
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The resource type: "vm" (QEMU), "lxc" (container), "node", "storage", or "sdn".
+ ///
+ [JsonPropertyName("type")]
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// The current status of the resource (e.g., "running", "stopped", "online").
+ ///
+ [JsonPropertyName("status")]
+ [JsonProperty("status")]
+ public string? Status { get; set; }
+
+ ///
+ /// The display name of the resource.
+ ///
+ [JsonPropertyName("name")]
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// The node on which this resource resides.
+ ///
+ [JsonPropertyName("node")]
+ [JsonProperty("node")]
+ public string? Node { get; set; }
+
+ ///
+ /// The pool this resource belongs to, if any.
+ ///
+ [JsonPropertyName("pool")]
+ [JsonProperty("pool")]
+ public string? Pool { get; set; }
+
+ ///
+ /// Uptime in seconds.
+ ///
+ [JsonPropertyName("uptime")]
+ [JsonProperty("uptime")]
+ public long? Uptime { get; set; }
+
+ ///
+ /// Maximum number of CPUs/cores available to this resource.
+ ///
+ [JsonPropertyName("maxcpu")]
+ [JsonProperty("maxcpu")]
+ public double? MaxCpu { get; set; }
+
+ ///
+ /// Current CPU usage as a fraction (0.0 to 1.0).
+ ///
+ [JsonPropertyName("cpu")]
+ [JsonProperty("cpu")]
+ public double? Cpu { get; set; }
+
+ ///
+ /// Maximum memory in bytes.
+ ///
+ [JsonPropertyName("maxmem")]
+ [JsonProperty("maxmem")]
+ public long? MaxMem { get; set; }
+
+ ///
+ /// Current memory usage in bytes.
+ ///
+ [JsonPropertyName("mem")]
+ [JsonProperty("mem")]
+ public long? Mem { get; set; }
+
+ ///
+ /// Maximum disk space in bytes.
+ ///
+ [JsonPropertyName("maxdisk")]
+ [JsonProperty("maxdisk")]
+ public long? MaxDisk { get; set; }
+
+ ///
+ /// Current disk usage in bytes.
+ ///
+ [JsonPropertyName("disk")]
+ [JsonProperty("disk")]
+ public long? Disk { get; set; }
+
+ ///
+ /// Whether this resource is a template (1) or not (0). Only for VM/LXC types.
+ ///
+ [JsonPropertyName("template")]
+ [JsonProperty("template")]
+ public int? Template { get; set; }
+
+ ///
+ /// The VM or container ID. Only for VM/LXC types.
+ ///
+ [JsonPropertyName("vmid")]
+ [JsonProperty("vmid")]
+ public int? VmId { get; set; }
+
+ ///
+ /// The HA (High Availability) state of the resource, if managed by HA.
+ ///
+ [JsonPropertyName("hastate")]
+ [JsonProperty("hastate")]
+ public string? HaState { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"{Type ?? "unknown"}: {Name ?? Id ?? "N/A"} | Node: {Node ?? "N/A"} | Status: {Status ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PvePool.cs b/src/PSProxmoxVE.Core/Models/Cluster/PvePool.cs
new file mode 100644
index 0000000..d424b7b
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PvePool.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents a Proxmox VE resource pool as returned by the /pools endpoint.
+///
+public class PvePool
+{
+ ///
+ /// The pool identifier.
+ ///
+ [JsonPropertyName("poolid")]
+ [JsonProperty("poolid")]
+ public string? PoolId { get; set; }
+
+ ///
+ /// An optional comment or description for the pool.
+ ///
+ [JsonPropertyName("comment")]
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// The pool members (VMs, containers, storage). Returned as a raw array
+ /// when querying a specific pool.
+ ///
+ [JsonPropertyName("members")]
+ [JsonProperty("members")]
+ public JArray? Members { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var memberCount = Members?.Count ?? 0;
+ return $"Pool: {PoolId ?? "N/A"} | Comment: {Comment ?? "-"} | Members: {memberCount}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Containers/PveContainerInterface.cs b/src/PSProxmoxVE.Core/Models/Containers/PveContainerInterface.cs
new file mode 100644
index 0000000..c74329a
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Containers/PveContainerInterface.cs
@@ -0,0 +1,37 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Containers;
+
+///
+/// Represents a network interface on an LXC container.
+///
+public class PveContainerInterface
+{
+ /// The interface name (e.g., "eth0", "lo").
+ [JsonPropertyName("name")]
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ /// The hardware (MAC) address of the interface.
+ [JsonPropertyName("hwaddr")]
+ [JsonProperty("hwaddr")]
+ public string? HwAddr { get; set; }
+
+ /// The IPv4 address assigned to this interface.
+ [JsonPropertyName("inet")]
+ [JsonProperty("inet")]
+ public string? Inet { get; set; }
+
+ /// The IPv6 address assigned to this interface.
+ [JsonPropertyName("inet6")]
+ [JsonProperty("inet6")]
+ public string? Inet6 { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var addr = Inet ?? Inet6 ?? "no address";
+ return $"{Name ?? "Unknown"} ({HwAddr ?? "N/A"}): {addr}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Storage/PveStorageStatus.cs b/src/PSProxmoxVE.Core/Models/Storage/PveStorageStatus.cs
new file mode 100644
index 0000000..13f1b61
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Storage/PveStorageStatus.cs
@@ -0,0 +1,68 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Storage;
+
+///
+/// Represents the status of a Proxmox VE storage on a specific node,
+/// as returned by the /nodes/{node}/storage/{storage}/status endpoint.
+///
+public class PveStorageStatus
+{
+ /// Total capacity in bytes.
+ [JsonPropertyName("total")]
+ [JsonProperty("total")]
+ public long? Total { get; set; }
+
+ /// Used capacity in bytes.
+ [JsonPropertyName("used")]
+ [JsonProperty("used")]
+ public long? Used { get; set; }
+
+ /// Available (free) capacity in bytes.
+ [JsonPropertyName("avail")]
+ [JsonProperty("avail")]
+ public long? Available { get; set; }
+
+ /// Whether the storage is currently active (1) or not (0).
+ [JsonPropertyName("active")]
+ [JsonProperty("active")]
+ public int? Active { get; set; }
+
+ /// Comma-separated list of supported content types.
+ [JsonPropertyName("content")]
+ [JsonProperty("content")]
+ public string? Content { get; set; }
+
+ /// Whether the storage is enabled (1) or disabled (0).
+ [JsonPropertyName("enabled")]
+ [JsonProperty("enabled")]
+ public int? Enabled { get; set; }
+
+ /// Whether the storage is shared across cluster nodes (1) or node-local (0).
+ [JsonPropertyName("shared")]
+ [JsonProperty("shared")]
+ public int? Shared { get; set; }
+
+ /// The storage backend type.
+ [JsonPropertyName("type")]
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ /// The storage identifier. Populated by the cmdlet after retrieval.
+ public string? Storage { get; set; }
+
+ /// The node name. Populated by the cmdlet after retrieval.
+ public string? Node { get; set; }
+
+ ///
+ 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}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Users/PveDomain.cs b/src/PSProxmoxVE.Core/Models/Users/PveDomain.cs
new file mode 100644
index 0000000..b8f1226
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Users/PveDomain.cs
@@ -0,0 +1,42 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Users;
+
+///
+/// Represents a Proxmox VE authentication domain/realm as returned by the /access/domains endpoint.
+///
+public class PveDomain
+{
+ /// The realm identifier (e.g., "pam", "pve", "myldap").
+ [JsonPropertyName("realm")]
+ [JsonProperty("realm")]
+ public string Realm { get; set; } = string.Empty;
+
+ /// The domain type (e.g., "pam", "pve", "ad", "ldap", "openid").
+ [JsonPropertyName("type")]
+ [JsonProperty("type")]
+ public string Type { get; set; } = string.Empty;
+
+ /// Optional comment/description for the domain.
+ [JsonPropertyName("comment")]
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ /// Whether this is the default realm (1) or not (0).
+ [JsonPropertyName("default")]
+ [JsonProperty("default")]
+ public int? Default { get; set; }
+
+ /// TFA configuration string, if any.
+ [JsonPropertyName("tfa")]
+ [JsonProperty("tfa")]
+ public string? Tfa { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var defaultStr = Default is 1 ? " [default]" : string.Empty;
+ return $"Realm: {Realm}{defaultStr} | Type: {Type} | Comment: {Comment ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Users/PveGroup.cs b/src/PSProxmoxVE.Core/Models/Users/PveGroup.cs
new file mode 100644
index 0000000..4b0d26d
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Users/PveGroup.cs
@@ -0,0 +1,36 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Users;
+
+///
+/// Represents a Proxmox VE group as returned by the /access/groups endpoint.
+///
+public class PveGroup
+{
+ /// The unique group identifier.
+ [JsonPropertyName("groupid")]
+ [JsonProperty("groupid")]
+ public string GroupId { get; set; } = string.Empty;
+
+ /// Optional comment/description for the group.
+ [JsonPropertyName("comment")]
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ /// Comma-separated list of user IDs that belong to this group.
+ [JsonPropertyName("users")]
+ [JsonProperty("users")]
+ public string? Users { get; set; }
+
+ /// Array of member user IDs. Populated when available.
+ [JsonPropertyName("members")]
+ [JsonProperty("members")]
+ public string[]? Members { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"Group: {GroupId} | Comment: {Comment ?? "N/A"} | Users: {Users ?? "none"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveGuestFsInfo.cs b/src/PSProxmoxVE.Core/Models/Vms/PveGuestFsInfo.cs
new file mode 100644
index 0000000..08f6951
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Vms/PveGuestFsInfo.cs
@@ -0,0 +1,49 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Vms;
+
+///
+/// Represents filesystem information reported by the QEMU guest agent.
+///
+public class PveGuestFsInfo
+{
+ /// The filesystem name/device.
+ [JsonPropertyName("name")]
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ /// The mount point path.
+ [JsonPropertyName("mountpoint")]
+ [JsonProperty("mountpoint")]
+ public string? MountPoint { get; set; }
+
+ /// The filesystem type (e.g., "ext4", "ntfs").
+ [JsonPropertyName("type")]
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ /// Total size of the filesystem in bytes.
+ [JsonPropertyName("total-bytes")]
+ [JsonProperty("total-bytes")]
+ public long? TotalBytes { get; set; }
+
+ /// Used space on the filesystem in bytes.
+ [JsonPropertyName("used-bytes")]
+ [JsonProperty("used-bytes")]
+ public long? UsedBytes { get; set; }
+
+ /// List of disk devices backing this filesystem.
+ [JsonPropertyName("disk")]
+ [JsonProperty("disk")]
+ public string[]? DiskList { get; set; }
+
+ ///
+ 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}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveGuestOsInfo.cs b/src/PSProxmoxVE.Core/Models/Vms/PveGuestOsInfo.cs
new file mode 100644
index 0000000..646b7cb
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Vms/PveGuestOsInfo.cs
@@ -0,0 +1,68 @@
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Vms;
+
+///
+/// Represents OS information reported by the QEMU guest agent.
+///
+public class PveGuestOsInfo
+{
+ /// The OS identifier (e.g., "mswindows", "linux").
+ [JsonPropertyName("id")]
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ /// The OS name (e.g., "Microsoft Windows 10 Enterprise").
+ [JsonPropertyName("name")]
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ /// The kernel release string.
+ [JsonPropertyName("kernel-release")]
+ [JsonProperty("kernel-release")]
+ public string? KernelRelease { get; set; }
+
+ /// The kernel version string.
+ [JsonPropertyName("kernel-version")]
+ [JsonProperty("kernel-version")]
+ public string? KernelVersion { get; set; }
+
+ /// The machine hardware name.
+ [JsonPropertyName("machine")]
+ [JsonProperty("machine")]
+ public string? Machine { get; set; }
+
+ /// The OS version details.
+ [JsonPropertyName("version")]
+ [JsonProperty("version")]
+ public PveGuestOsVersion? Version { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var ver = Version != null ? $" {Version.Major}.{Version.Minor}" : string.Empty;
+ return $"{Name ?? Id ?? "Unknown"}{ver} ({KernelRelease ?? "N/A"})";
+ }
+}
+
+///
+/// Represents a guest OS version with major and minor components.
+///
+public class PveGuestOsVersion
+{
+ /// The version identifier string.
+ [JsonPropertyName("id")]
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ /// The major version number.
+ [JsonPropertyName("major")]
+ [JsonProperty("major")]
+ public int? Major { get; set; }
+
+ /// The minor version number.
+ [JsonPropertyName("minor")]
+ [JsonProperty("minor")]
+ public int? Minor { get; set; }
+}
diff --git a/src/PSProxmoxVE.Core/Services/BackupService.cs b/src/PSProxmoxVE.Core/Services/BackupService.cs
index 53a2dc6..b3c8293 100644
--- a/src/PSProxmoxVE.Core/Services/BackupService.cs
+++ b/src/PSProxmoxVE.Core/Services/BackupService.cs
@@ -106,6 +106,24 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
+ // -------------------------------------------------------------------------
+ // Backup compliance info
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns the list of guests not covered by any backup job.
+ ///
+ 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
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE.Core/Services/ClusterService.cs b/src/PSProxmoxVE.Core/Services/ClusterService.cs
index d781651..9b450f3 100644
--- a/src/PSProxmoxVE.Core/Services/ClusterService.cs
+++ b/src/PSProxmoxVE.Core/Services/ClusterService.cs
@@ -24,5 +24,24 @@ namespace PSProxmoxVE.Core.Services
var data = JObject.Parse(response)["data"];
return data?.ToObject() ?? Array.Empty();
}
+
+ ///
+ /// Returns cluster resources, optionally filtered by type.
+ ///
+ /// The authenticated PVE session.
+ /// Optional resource type filter (vm, lxc, node, storage, sdn).
+ 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() ?? Array.Empty();
+ }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/ContainerService.cs b/src/PSProxmoxVE.Core/Services/ContainerService.cs
index 040ce7a..c6d7e23 100644
--- a/src/PSProxmoxVE.Core/Services/ContainerService.cs
+++ b/src/PSProxmoxVE.Core/Services/ContainerService.cs
@@ -318,6 +318,104 @@ namespace PSProxmoxVE.Core.Services
return ParseTask(response, node);
}
+ // -------------------------------------------------------------------------
+ // Suspend / Resume
+ // -------------------------------------------------------------------------
+
+ /// Suspends a container. Returns the task UPID.
+ public PveTask SuspendContainer(PveSession session, string node, int vmid)
+ => PostStatus(session, node, vmid, "suspend");
+
+ /// Resumes a suspended container. Returns the task UPID.
+ public PveTask ResumeContainer(PveSession session, string node, int vmid)
+ => PostStatus(session, node, vmid, "resume");
+
+ // -------------------------------------------------------------------------
+ // Disk / volume operations
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Resizes a container disk/volume. Returns the task UPID.
+ ///
+ 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
+ {
+ ["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);
+ }
+
+ ///
+ /// Moves a container volume to a different storage. Returns the task UPID.
+ ///
+ 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
+ {
+ ["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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Converts a container to a template. This is irreversible. Returns the task UPID.
+ ///
+ 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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns network interface information for a container.
+ ///
+ 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() ?? Array.Empty();
+ }
+
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE.Core/Services/NetworkService.cs b/src/PSProxmoxVE.Core/Services/NetworkService.cs
index 667c1ff..e065e1d 100644
--- a/src/PSProxmoxVE.Core/Services/NetworkService.cs
+++ b/src/PSProxmoxVE.Core/Services/NetworkService.cs
@@ -438,6 +438,108 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
+ // -------------------------------------------------------------------------
+ // SDN Update operations
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Applies pending SDN configuration changes cluster-wide.
+ ///
+ 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())
+ .GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Updates an SDN zone configuration.
+ ///
+ public void UpdateSdnZone(PveSession session, string zone, Dictionary 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();
+ }
+
+ ///
+ /// Updates an SDN VNet configuration.
+ ///
+ public void UpdateSdnVnet(PveSession session, string vnet, Dictionary 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();
+ }
+
+ ///
+ /// Updates an SDN subnet configuration.
+ ///
+ public void UpdateSdnSubnet(PveSession session, string vnet, string subnet, Dictionary 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();
+ }
+
+ ///
+ /// Updates an SDN controller configuration.
+ ///
+ public void UpdateSdnController(PveSession session, string controller, Dictionary 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();
+ }
+
+ ///
+ /// Updates an SDN IPAM plugin configuration.
+ ///
+ public void UpdateSdnIpam(PveSession session, string ipam, Dictionary 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();
+ }
+
+ ///
+ /// Updates an SDN DNS plugin configuration.
+ ///
+ public void UpdateSdnDns(PveSession session, string dns, Dictionary 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
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs
index 1231ded..c1dfad2 100644
--- a/src/PSProxmoxVE.Core/Services/NodeService.cs
+++ b/src/PSProxmoxVE.Core/Services/NodeService.cs
@@ -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() ?? new PveNodeStatus();
}
+ ///
+ /// Returns the configuration of a specific node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ 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();
+ }
+
+ ///
+ /// Updates the configuration of a specific node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// Configuration parameters to update.
+ public void SetNodeConfig(PveSession session, string node, Dictionary 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();
+ }
+
+ ///
+ /// Returns the DNS configuration of a specific node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ 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();
+ }
+
+ ///
+ /// Updates the DNS configuration of a specific node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// DNS configuration parameters to update.
+ public void SetNodeDns(PveSession session, string node, Dictionary 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();
+ }
+
+ ///
+ /// Starts all VMs and containers on a node. Returns the task UPID.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// Optional parameters (e.g. vms to limit which VMs start).
+ public PveTask StartAll(PveSession session, string node, Dictionary? config = null)
+ {
+ if (session == null) throw new ArgumentNullException(nameof(session));
+ if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
+
+ var formData = config ?? new Dictionary();
+ using var client = new PveHttpClient(session);
+ var response = client.PostAsync($"nodes/{node}/startall", formData).GetAwaiter().GetResult();
+ return ParseTask(response, node);
+ }
+
+ ///
+ /// Stops all VMs and containers on a node. Returns the task UPID.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// Optional parameters (e.g. vms, force-stop).
+ public PveTask StopAll(PveSession session, string node, Dictionary? config = null)
+ {
+ if (session == null) throw new ArgumentNullException(nameof(session));
+ if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
+
+ var formData = config ?? new Dictionary();
+ using var client = new PveHttpClient(session);
+ var response = client.PostAsync($"nodes/{node}/stopall", formData).GetAwaiter().GetResult();
+ return ParseTask(response, node);
+ }
+
///
/// Returns the Proxmox VE version running on the server.
///
@@ -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() ?? new PveTask();
+ task.Node = node;
+ return task;
+ }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/PoolService.cs b/src/PSProxmoxVE.Core/Services/PoolService.cs
new file mode 100644
index 0000000..9d0c830
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Services/PoolService.cs
@@ -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
+{
+ ///
+ /// Service for Proxmox VE resource pool API operations.
+ ///
+ public class PoolService
+ {
+ ///
+ /// Returns all resource pools.
+ ///
+ 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() ?? Array.Empty();
+ }
+
+ ///
+ /// Returns a single resource pool by ID.
+ ///
+ 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();
+ }
+
+ ///
+ /// Creates a new resource pool.
+ ///
+ 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 { { "poolid", poolId } };
+ if (!string.IsNullOrEmpty(comment))
+ config["comment"] = comment!;
+
+ client.PostAsync("pools", config).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Updates an existing resource pool.
+ ///
+ public void UpdatePool(PveSession session, string poolId, Dictionary 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();
+ }
+
+ ///
+ /// Removes a resource pool.
+ ///
+ 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();
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/StorageService.cs b/src/PSProxmoxVE.Core/Services/StorageService.cs
index 9fd1ec6..e9f93e0 100644
--- a/src/PSProxmoxVE.Core/Services/StorageService.cs
+++ b/src/PSProxmoxVE.Core/Services/StorageService.cs
@@ -187,6 +187,105 @@ namespace PSProxmoxVE.Core.Services
client.DeleteAsync($"storage/{storage}").GetAwaiter().GetResult();
}
+ ///
+ /// Updates a storage definition.
+ ///
+ /// The authenticated PVE session.
+ /// The storage identifier to update.
+ /// Configuration parameters to update.
+ public void UpdateStorage(PveSession session, string storage, Dictionary 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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns the status of a specific storage on a node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// The storage identifier.
+ 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() ?? new PveStorageStatus();
+ }
+
+ ///
+ /// Removes a content volume from a storage on a node.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// The storage identifier.
+ /// The volume identifier to remove.
+ 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();
+ }
+
+ ///
+ /// Updates properties (e.g. notes) of a content volume on a storage.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// The storage identifier.
+ /// The volume identifier to update.
+ /// Configuration parameters to update.
+ public void UpdateContent(PveSession session, string node, string storage, string volume, Dictionary 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();
+ }
+
+ ///
+ /// Allocates a new disk image on a storage. Returns the task UPID.
+ ///
+ /// The authenticated PVE session.
+ /// The cluster node name.
+ /// The storage identifier.
+ /// Allocation parameters (filename, size, format).
+ public PveTask AllocateDisk(PveSession session, string node, string storage, Dictionary 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
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE.Core/Services/TaskService.cs b/src/PSProxmoxVE.Core/Services/TaskService.cs
index c576916..2a59386 100644
--- a/src/PSProxmoxVE.Core/Services/TaskService.cs
+++ b/src/PSProxmoxVE.Core/Services/TaskService.cs
@@ -103,5 +103,57 @@ namespace PSProxmoxVE.Core.Services
Thread.Sleep(effectivePoll);
}
}
+
+ ///
+ /// Returns a list of tasks on the specified node, with optional filters.
+ ///
+ /// The authenticated PVE session.
+ /// The node name.
+ /// Optional VM ID filter.
+ /// Optional source filter: "all" or "active".
+ /// Optional task type filter (e.g., "qmstart").
+ /// Maximum number of tasks to return. Defaults to 50.
+ 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 { $"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() ?? Array.Empty();
+ foreach (var t in tasks)
+ t.Node ??= node;
+ return tasks;
+ }
+
+ ///
+ /// Stops (cancels) a running task on the specified node.
+ ///
+ /// The authenticated PVE session.
+ /// The node name.
+ /// The UPID of the task to stop.
+ 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();
+ }
}
}
diff --git a/src/PSProxmoxVE.Core/Services/UserService.cs b/src/PSProxmoxVE.Core/Services/UserService.cs
index 6368186..ec22f8c 100644
--- a/src/PSProxmoxVE.Core/Services/UserService.cs
+++ b/src/PSProxmoxVE.Core/Services/UserService.cs
@@ -189,6 +189,23 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
+ ///
+ /// Updates an API token's configuration.
+ ///
+ public void UpdateApiToken(PveSession session, string userId, string tokenId, Dictionary 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();
}
+ ///
+ /// Updates a role's privileges.
+ ///
+ /// The authenticated PVE session.
+ /// The role identifier to update.
+ /// Comma-separated list of privileges.
+ 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 { ["privs"] = privileges };
+ using var client = new PveHttpClient(session);
+ client.PutAsync($"access/roles/{Uri.EscapeDataString(roleId)}", formData)
+ .GetAwaiter().GetResult();
+ }
+
+ // -------------------------------------------------------------------------
+ // Groups
+ // -------------------------------------------------------------------------
+
+ /// Returns all groups.
+ /// The authenticated PVE session.
+ 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() ?? Array.Empty();
+ }
+
+ /// Creates a new group.
+ /// The authenticated PVE session.
+ /// The group identifier.
+ /// Optional comment/description.
+ 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 { ["groupid"] = groupId };
+ if (!string.IsNullOrEmpty(comment))
+ formData["comment"] = comment!;
+
+ using var client = new PveHttpClient(session);
+ client.PostAsync("access/groups", formData).GetAwaiter().GetResult();
+ }
+
+ /// Updates a group's properties.
+ /// The authenticated PVE session.
+ /// The group identifier to update.
+ /// Configuration parameters to update.
+ public void UpdateGroup(PveSession session, string groupId, Dictionary 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();
+ }
+
+ /// Removes a group.
+ /// The authenticated PVE session.
+ /// The group identifier to remove.
+ 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
+ // -------------------------------------------------------------------------
+
+ /// Returns all authentication domains/realms.
+ /// The authenticated PVE session.
+ 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() ?? Array.Empty();
+ }
+
+ /// Creates a new authentication domain/realm.
+ /// The authenticated PVE session.
+ /// Domain configuration parameters (must include realm and type).
+ public void CreateDomain(PveSession session, Dictionary 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();
+ }
+
+ /// Updates an authentication domain/realm.
+ /// The authenticated PVE session.
+ /// The realm identifier to update.
+ /// Configuration parameters to update.
+ public void UpdateDomain(PveSession session, string realm, Dictionary 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();
+ }
+
+ /// Removes an authentication domain/realm.
+ /// The authenticated PVE session.
+ /// The realm identifier to remove.
+ 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
+ // -------------------------------------------------------------------------
+
+ /// Changes a user's password.
+ /// The authenticated PVE session.
+ /// The user ID in "username@realm" format.
+ /// The new password.
+ 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
+ {
+ ["userid"] = userId,
+ ["password"] = password
+ };
+
+ using var client = new PveHttpClient(session);
+ client.PutAsync("access/password", formData).GetAwaiter().GetResult();
+ }
+
// -------------------------------------------------------------------------
// Permissions / ACLs
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs
index 39c8e0c..35f39a2 100644
--- a/src/PSProxmoxVE.Core/Services/VmService.cs
+++ b/src/PSProxmoxVE.Core/Services/VmService.cs
@@ -521,6 +521,163 @@ namespace PSProxmoxVE.Core.Services
return JObject.Parse(response)["data"] as JObject ?? new JObject();
}
+ // -------------------------------------------------------------------------
+ // Disk operations
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Moves a VM disk to a different storage. Returns the task UPID.
+ ///
+ 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
+ {
+ ["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);
+ }
+
+ ///
+ /// Unlinks (detaches) disks from a VM.
+ ///
+ 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
+ {
+ ["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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Retrieves OS information from the QEMU guest agent.
+ ///
+ 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();
+ }
+
+ ///
+ /// Retrieves filesystem information from the QEMU guest agent.
+ ///
+ 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() ?? Array.Empty();
+ }
+
+ ///
+ /// Reads a file from the guest filesystem via the QEMU guest agent.
+ ///
+ 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;
+ }
+
+ ///
+ /// Writes content to a file on the guest filesystem via the QEMU guest agent.
+ ///
+ 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
+ {
+ ["file"] = file,
+ ["content"] = content
+ };
+
+ using var client = new PveHttpClient(session);
+ client.PostAsync($"nodes/{node}/qemu/{vmid}/agent/file-write", formData)
+ .GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Sets a user password inside the guest via the QEMU guest agent.
+ ///
+ 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
+ {
+ ["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();
+ }
+
+ ///
+ /// Triggers an fstrim operation inside the guest via the QEMU guest agent.
+ ///
+ 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
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs
new file mode 100644
index 0000000..0ea3a9e
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs
@@ -0,0 +1,39 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Backup
+{
+ ///
+ /// Lists guests not covered by any backup job.
+ ///
+ /// Returns information about VMs and containers that are not included in any
+ /// scheduled backup job. Useful for backup compliance auditing.
+ ///
+ ///
+ [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