From df4cae01bc133649a26b6853ac1b0a4edef9c7ef Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Sat, 21 Mar 2026 09:55:19 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20close=20API=20coverage=20gaps=20?= =?UTF-8?q?=E2=80=94=2051=20new=20cmdlets=20across=206=20areas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 13 + README.md | 81 ++++ docs/PVE_API_COVERAGE.md | 27 +- .../Models/Cluster/PveClusterResource.cs | 129 ++++++ .../Models/Cluster/PvePool.cs | 40 ++ .../Containers/PveContainerInterface.cs | 37 ++ .../Models/Storage/PveStorageStatus.cs | 68 +++ .../Models/Users/PveDomain.cs | 42 ++ src/PSProxmoxVE.Core/Models/Users/PveGroup.cs | 36 ++ .../Models/Vms/PveGuestFsInfo.cs | 49 ++ .../Models/Vms/PveGuestOsInfo.cs | 68 +++ .../Services/BackupService.cs | 18 + .../Services/ClusterService.cs | 19 + .../Services/ContainerService.cs | 98 ++++ .../Services/NetworkService.cs | 102 +++++ src/PSProxmoxVE.Core/Services/NodeService.cs | 115 +++++ src/PSProxmoxVE.Core/Services/PoolService.cs | 86 ++++ .../Services/StorageService.cs | 99 ++++ src/PSProxmoxVE.Core/Services/TaskService.cs | 52 +++ src/PSProxmoxVE.Core/Services/UserService.cs | 176 ++++++++ src/PSProxmoxVE.Core/Services/VmService.cs | 157 +++++++ .../Cmdlets/Backup/GetPveBackupInfoCmdlet.cs | 39 ++ .../Cluster/GetPveClusterResourceCmdlet.cs | 50 +++ .../GetPveContainerInterfaceCmdlet.cs | 39 ++ .../MovePveContainerVolumeCmdlet.cs | 76 ++++ .../NewPveContainerTemplateCmdlet.cs | 48 ++ .../ResizePveContainerDiskCmdlet.cs | 61 +++ .../Containers/ResumePveContainerCmdlet.cs | 62 +++ .../Containers/SuspendPveContainerCmdlet.cs | 62 +++ .../Network/InvokePveSdnApplyCmdlet.cs | 29 ++ .../Network/SetPveSdnControllerCmdlet.cs | 53 +++ .../Cmdlets/Network/SetPveSdnDnsCmdlet.cs | 59 +++ .../Cmdlets/Network/SetPveSdnIpamCmdlet.cs | 53 +++ .../Cmdlets/Network/SetPveSdnSubnetCmdlet.cs | 62 +++ .../Cmdlets/Network/SetPveSdnVnetCmdlet.cs | 59 +++ .../Cmdlets/Network/SetPveSdnZoneCmdlet.cs | 52 +++ .../Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs | 39 ++ .../Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs | 39 ++ .../Cmdlets/Nodes/SetPveNodeConfigCmdlet.cs | 46 ++ .../Cmdlets/Nodes/SetPveNodeDnsCmdlet.cs | 59 +++ .../Cmdlets/Nodes/StartPveNodeVmsCmdlet.cs | 45 ++ .../Cmdlets/Nodes/StopPveNodeVmsCmdlet.cs | 52 +++ .../Cmdlets/Pools/GetPvePoolCmdlet.cs | 46 ++ .../Cmdlets/Pools/NewPvePoolCmdlet.cs | 40 ++ .../Cmdlets/Pools/RemovePvePoolCmdlet.cs | 36 ++ .../Cmdlets/Pools/SetPvePoolCmdlet.cs | 61 +++ .../Storage/GetPveStorageStatusCmdlet.cs | 41 ++ .../Storage/NewPveStorageDiskCmdlet.cs | 66 +++ .../Storage/RemovePveStorageContentCmdlet.cs | 45 ++ .../Cmdlets/Storage/SetPveStorageCmdlet.cs | 66 +++ .../Storage/SetPveStorageContentCmdlet.cs | 51 +++ .../Cmdlets/Tasks/GetPveTaskListCmdlet.cs | 65 +++ .../Cmdlets/Tasks/StopPveTaskCmdlet.cs | 42 ++ .../Cmdlets/Users/GetPveDomainCmdlet.cs | 42 ++ .../Cmdlets/Users/GetPveGroupCmdlet.cs | 42 ++ .../Cmdlets/Users/NewPveDomainCmdlet.cs | 64 +++ .../Cmdlets/Users/NewPveGroupCmdlet.cs | 44 ++ .../Cmdlets/Users/RemovePveDomainCmdlet.cs | 35 ++ .../Cmdlets/Users/RemovePveGroupCmdlet.cs | 34 ++ .../Cmdlets/Users/SetPveApiTokenCmdlet.cs | 55 +++ .../Cmdlets/Users/SetPveDomainCmdlet.cs | 46 ++ .../Cmdlets/Users/SetPveGroupCmdlet.cs | 40 ++ .../Cmdlets/Users/SetPvePasswordCmdlet.cs | 49 ++ .../Cmdlets/Users/SetPveRoleCmdlet.cs | 35 ++ .../Cmdlets/Vms/GetPveVmGuestFsInfoCmdlet.cs | 40 ++ .../Cmdlets/Vms/GetPveVmGuestOsInfoCmdlet.cs | 40 ++ .../Vms/InvokePveVmGuestFsTrimCmdlet.cs | 38 ++ .../Cmdlets/Vms/MovePveVmDiskCmdlet.cs | 83 ++++ .../Cmdlets/Vms/ReadPveVmGuestFileCmdlet.cs | 44 ++ .../Cmdlets/Vms/RemovePveVmDiskCmdlet.cs | 55 +++ .../Vms/SetPveVmGuestPasswordCmdlet.cs | 56 +++ .../Cmdlets/Vms/WritePveVmGuestFileCmdlet.cs | 49 ++ src/PSProxmoxVE/PSProxmoxVE.format.ps1xml | 412 +++++++++++++++++ src/PSProxmoxVE/PSProxmoxVE.psd1 | 81 +++- .../Backup/BackupInfoCmdlets.Tests.ps1 | 58 +++ .../Cluster/ClusterCmdlets.Tests.ps1 | 66 +++ .../Containers/ContainerGapCmdlets.Tests.ps1 | 342 ++++++++++++++ .../Network/SdnSetCmdlets.Tests.ps1 | 424 ++++++++++++++++++ .../Nodes/NodeOpsCmdlets.Tests.ps1 | 281 ++++++++++++ .../Pools/PoolCmdlets.Tests.ps1 | 182 ++++++++ .../Storage/SetPveStorageCmdlets.Tests.ps1 | 287 ++++++++++++ .../Tasks/TaskListCmdlets.Tests.ps1 | 127 ++++++ .../Users/AccessManagementCmdlets.Tests.ps1 | 400 +++++++++++++++++ .../Vms/GuestAgentExtCmdlets.Tests.ps1 | 331 ++++++++++++++ .../Vms/VmDiskOpsCmdlets.Tests.ps1 | 143 ++++++ 85 files changed, 7172 insertions(+), 8 deletions(-) create mode 100644 src/PSProxmoxVE.Core/Models/Cluster/PveClusterResource.cs create mode 100644 src/PSProxmoxVE.Core/Models/Cluster/PvePool.cs create mode 100644 src/PSProxmoxVE.Core/Models/Containers/PveContainerInterface.cs create mode 100644 src/PSProxmoxVE.Core/Models/Storage/PveStorageStatus.cs create mode 100644 src/PSProxmoxVE.Core/Models/Users/PveDomain.cs create mode 100644 src/PSProxmoxVE.Core/Models/Users/PveGroup.cs create mode 100644 src/PSProxmoxVE.Core/Models/Vms/PveGuestFsInfo.cs create mode 100644 src/PSProxmoxVE.Core/Models/Vms/PveGuestOsInfo.cs create mode 100644 src/PSProxmoxVE.Core/Services/PoolService.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/GetPveContainerInterfaceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerVolumeCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerTemplateCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/ResizePveContainerDiskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/ResumePveContainerCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/InvokePveSdnApplyCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnControllerCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnDnsCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnIpamCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnVnetCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Network/SetPveSdnZoneCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeConfigCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeDnsCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/StartPveNodeVmsCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Nodes/StopPveNodeVmsCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Pools/GetPvePoolCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Pools/NewPvePoolCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Pools/RemovePvePoolCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Pools/SetPvePoolCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageStatusCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageDiskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageContentCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageContentCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskListCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Tasks/StopPveTaskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/GetPveDomainCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/GetPveGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/NewPveDomainCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/NewPveGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/RemovePveDomainCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/RemovePveGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPveApiTokenCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPveDomainCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPveGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPvePasswordCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Users/SetPveRoleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestFsInfoCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestOsInfoCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestFsTrimCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/MovePveVmDiskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/ReadPveVmGuestFileCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/RemovePveVmDiskCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/SetPveVmGuestPasswordCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Vms/WritePveVmGuestFileCmdlet.cs create mode 100644 tests/PSProxmoxVE.Tests/Backup/BackupInfoCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Cluster/ClusterCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Containers/ContainerGapCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Network/SdnSetCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Nodes/NodeOpsCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Pools/PoolCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Storage/SetPveStorageCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Tasks/TaskListCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Users/AccessManagementCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Vms/GuestAgentExtCmdlets.Tests.ps1 create mode 100644 tests/PSProxmoxVE.Tests/Vms/VmDiskOpsCmdlets.Tests.ps1 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())); + } + } + WriteObject(pso); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterResourceCmdlet.cs new file mode 100644 index 0000000..b897d20 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterResourceCmdlet.cs @@ -0,0 +1,50 @@ +using System; +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Cluster; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Cluster +{ + /// + /// Lists resources across the Proxmox VE cluster. + /// + /// Returns cluster-wide resources (VMs, containers, nodes, storage, SDN). + /// Optionally filter by resource type or node name. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveClusterResource")] + [OutputType(typeof(PveClusterResource))] + public sealed class GetPveClusterResourceCmdlet : PveCmdletBase + { + /// + /// Filter by resource type. + /// + [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; } + + /// + /// Filter results to a specific node name. + /// + [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); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/GetPveContainerInterfaceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/GetPveContainerInterfaceCmdlet.cs new file mode 100644 index 0000000..1bec648 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/GetPveContainerInterfaceCmdlet.cs @@ -0,0 +1,39 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Containers; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Gets network interface information for an LXC container. + /// + /// Retrieves network interface information for the specified LXC container + /// via the Proxmox VE API, including interface names, MAC addresses, and IP addresses. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveContainerInterface")] + [OutputType(typeof(PveContainerInterface))] + public sealed class GetPveContainerInterfaceCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The container identifier. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerVolumeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerVolumeCmdlet.cs new file mode 100644 index 0000000..90924c7 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerVolumeCmdlet.cs @@ -0,0 +1,76 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Moves a container volume to a different storage. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Move, "PveContainerVolume", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class MovePveContainerVolumeCmdlet : PveCmdletBase + { + /// + /// The node on which the container resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the container whose volume should be moved. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The volume to move (e.g., "rootfs", "mp0"). + /// + [Parameter(Mandatory = true, HelpMessage = "The volume to move (e.g. rootfs, mp0).")] + public string Volume { get; set; } = string.Empty; + + /// + /// The target storage to move the volume to. + /// + [Parameter(Mandatory = true, HelpMessage = "The target storage identifier.")] + public string Storage { get; set; } = string.Empty; + + /// + /// When specified, deletes the original volume after a successful move. Default is true. + /// + [Parameter(Mandatory = false, HelpMessage = "Delete the original volume after move (default true).")] + public SwitchParameter Delete { get; set; } = true; + + /// + /// When specified, waits for the move task to complete before returning. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerTemplateCmdlet.cs new file mode 100644 index 0000000..5c60e6c --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerTemplateCmdlet.cs @@ -0,0 +1,48 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Converts an LXC container to a template. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.New, "PveContainerTemplate", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(PveTask))] + public sealed class NewPveContainerTemplateCmdlet : PveCmdletBase + { + /// + /// The node on which the container resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the container to convert to a template. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/ResizePveContainerDiskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/ResizePveContainerDiskCmdlet.cs new file mode 100644 index 0000000..0571b55 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/ResizePveContainerDiskCmdlet.cs @@ -0,0 +1,61 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Resizes a disk attached to an LXC container. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Resize, "PveContainerDisk", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class ResizePveContainerDiskCmdlet : PveCmdletBase + { + /// + /// The node on which the container resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the container whose disk should be resized. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The disk/volume to resize (e.g., "rootfs", "mp0"). + /// + [Parameter(Mandatory = true, HelpMessage = "The disk/volume to resize (e.g. rootfs, mp0).")] + public string Disk { get; set; } = string.Empty; + + /// + /// + /// 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. + /// + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/ResumePveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/ResumePveContainerCmdlet.cs new file mode 100644 index 0000000..67f3a1b --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/ResumePveContainerCmdlet.cs @@ -0,0 +1,62 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Resumes a suspended LXC container on a Proxmox VE node. + /// + /// Sends a resume command to the specified LXC container via the Proxmox VE API. + /// Use -Wait to block until the resume task completes. + /// + /// + [Cmdlet(VerbsLifecycle.Resume, "PveContainer", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class ResumePveContainerCmdlet : PveCmdletBase + { + /// + /// The node on which the container resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the container to resume. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// When specified, waits for the resume task to complete before returning. + /// + [Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")] + public SwitchParameter Wait { get; set; } + + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs new file mode 100644 index 0000000..b5f2542 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs @@ -0,0 +1,62 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Suspends an LXC container on a Proxmox VE node. + /// + /// Sends a suspend command to the specified LXC container via the Proxmox VE API. + /// Use -Wait to block until the suspend task completes. + /// + /// + [Cmdlet(VerbsLifecycle.Suspend, "PveContainer", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class SuspendPveContainerCmdlet : PveCmdletBase + { + /// + /// The node on which the container resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the container to suspend. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// When specified, waits for the suspend task to complete before returning. + /// + [Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")] + public SwitchParameter Wait { get; set; } + + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/InvokePveSdnApplyCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/InvokePveSdnApplyCmdlet.cs new file mode 100644 index 0000000..ebb1314 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/InvokePveSdnApplyCmdlet.cs @@ -0,0 +1,29 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Applies pending SDN configuration changes in Proxmox VE. + /// + /// Applies all pending Software-Defined Networking configuration changes cluster-wide. + /// Run this after making changes with Set-PveSdnZone, Set-PveSdnVnet, etc. + /// + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnControllerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnControllerCmdlet.cs new file mode 100644 index 0000000..3d62992 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnControllerCmdlet.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN controller configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking controller configuration. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnController", SupportsShouldProcess = true)] + public class SetPveSdnControllerCmdlet : PveCmdletBase + { + /// The controller identifier. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN controller identifier.")] + public string Controller { get; set; } = string.Empty; + + /// The Autonomous System Number for BGP/EVPN. + [Parameter(Mandatory = false, HelpMessage = "The Autonomous System Number for BGP/EVPN.")] + public int? Asn { get; set; } + + /// Comma-separated list of BGP peer addresses. + [Parameter(Mandatory = false, HelpMessage = "Comma-separated list of BGP peer addresses.")] + public string? Peers { get; set; } + + /// The node this controller is configured on. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnDnsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnDnsCmdlet.cs new file mode 100644 index 0000000..ccb4dbf --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnDnsCmdlet.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN DNS plugin configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking DNS plugin configuration. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnDns", SupportsShouldProcess = true)] + public class SetPveSdnDnsCmdlet : PveCmdletBase + { + /// The DNS plugin identifier. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN DNS plugin identifier.")] + public string Dns { get; set; } = string.Empty; + + /// The DNS server URL. + [Parameter(Mandatory = false, HelpMessage = "The DNS server URL.")] + public string? Url { get; set; } + + /// The TSIG key for DNS updates. + [Parameter(Mandatory = false, HelpMessage = "The TSIG key for DNS updates.")] + public string? Key { get; set; } + + /// The IPv6 reverse DNS mask length. + [Parameter(Mandatory = false, HelpMessage = "The IPv6 reverse DNS mask length.")] + public int? ReverseMaskV6 { get; set; } + + /// The TTL for DNS records. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnIpamCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnIpamCmdlet.cs new file mode 100644 index 0000000..b21f768 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnIpamCmdlet.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN IPAM plugin configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking IPAM plugin configuration. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnIpam", SupportsShouldProcess = true)] + public class SetPveSdnIpamCmdlet : PveCmdletBase + { + /// The IPAM plugin identifier. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN IPAM plugin identifier.")] + public string Ipam { get; set; } = string.Empty; + + /// The IPAM server URL. + [Parameter(Mandatory = false, HelpMessage = "The IPAM server URL.")] + public string? Url { get; set; } + + /// The API token for the IPAM server. + [Parameter(Mandatory = false, HelpMessage = "The API token for the IPAM server.")] + public string? Token { get; set; } + + /// The section ID in the IPAM server. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs new file mode 100644 index 0000000..4c7120d --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnSubnetCmdlet.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN subnet configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking subnet configuration on a VNet. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnSubnet", SupportsShouldProcess = true)] + public class SetPveSdnSubnetCmdlet : PveCmdletBase + { + /// The VNet this subnet belongs to. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN VNet name.")] + public string Vnet { get; set; } = string.Empty; + + /// The subnet CIDR. + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The subnet CIDR.")] + public string Subnet { get; set; } = string.Empty; + + /// Gateway address for this subnet. + [Parameter(Mandatory = false, HelpMessage = "Gateway address for this subnet.")] + public string? Gateway { get; set; } + + /// Enable SNAT for this subnet. + [Parameter(Mandatory = false, HelpMessage = "Enable SNAT for this subnet.")] + public SwitchParameter Snat { get; set; } + + /// DNS zone prefix for automatic registration. + [Parameter(Mandatory = false, HelpMessage = "DNS zone prefix for automatic registration.")] + public string? DnsZonePrefix { get; set; } + + /// DHCP range for this subnet. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnVnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnVnetCmdlet.cs new file mode 100644 index 0000000..1cd37c4 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnVnetCmdlet.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN VNet configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking VNet configuration. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnVnet", SupportsShouldProcess = true)] + public class SetPveSdnVnetCmdlet : PveCmdletBase + { + /// The VNet identifier. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN VNet name.")] + public string Vnet { get; set; } = string.Empty; + + /// An alias for the VNet. + [Parameter(Mandatory = false, HelpMessage = "An alias for the VNet.")] + public string? Alias { get; set; } + + /// The zone this VNet belongs to. + [Parameter(Mandatory = false, HelpMessage = "The zone this VNet belongs to.")] + public string? Zone { get; set; } + + /// VLAN tag for this VNet. + [Parameter(Mandatory = false, HelpMessage = "VLAN tag for this VNet.")] + public int? Tag { get; set; } + + /// Enable VLAN-aware bridging. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnZoneCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnZoneCmdlet.cs new file mode 100644 index 0000000..71bb789 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveSdnZoneCmdlet.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Network +{ + /// + /// Updates an SDN zone configuration in Proxmox VE. + /// + /// Modifies the specified Software-Defined Networking zone configuration. + /// Changes are pending until Invoke-PveSdnApply is called. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveSdnZone", SupportsShouldProcess = true)] + public class SetPveSdnZoneCmdlet : PveCmdletBase + { + /// The zone identifier. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN zone name.")] + public string Zone { get; set; } = string.Empty; + + /// DNS server for automatic DNS registration. + [Parameter(Mandatory = false, HelpMessage = "DNS server for automatic registration.")] + public string? Dns { get; set; } + + /// Reverse DNS server. + [Parameter(Mandatory = false, HelpMessage = "Reverse DNS server.")] + public string? Reversedns { get; set; } + + /// DNS zone name for registration. + [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(); + + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs new file mode 100644 index 0000000..262c54f --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs @@ -0,0 +1,39 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Nodes +{ + /// + /// Returns the configuration of a Proxmox VE node. + /// + /// Retrieves the node configuration from the /nodes/{node}/config endpoint, + /// including description, wakeonlan settings and other node-level properties. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveNodeConfig")] + [OutputType(typeof(PSObject))] + public class GetPveNodeConfigCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs new file mode 100644 index 0000000..16f9157 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs @@ -0,0 +1,39 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Nodes +{ + /// + /// Returns the DNS configuration of a Proxmox VE node. + /// + /// Retrieves the DNS settings (search domain and nameservers) for the specified + /// node from the /nodes/{node}/dns endpoint. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveNodeDns")] + [OutputType(typeof(PSObject))] + public class GetPveNodeDnsCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeConfigCmdlet.cs new file mode 100644 index 0000000..9ed030e --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeConfigCmdlet.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Nodes +{ + /// + /// Updates the configuration of a Proxmox VE node. + /// + /// Modifies node-level configuration properties such as description and wake-on-LAN + /// settings via the /nodes/{node}/config endpoint. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveNodeConfig", SupportsShouldProcess = true)] + public class SetPveNodeConfigCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// Node description/comment. + [Parameter(Mandatory = false, HelpMessage = "Node description or comment.")] + public string? Description { get; set; } + + /// Wake-on-LAN MAC address or configuration. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeDnsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeDnsCmdlet.cs new file mode 100644 index 0000000..78a9a14 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/SetPveNodeDnsCmdlet.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Nodes +{ + /// + /// Updates the DNS configuration of a Proxmox VE node. + /// + /// Modifies the DNS settings (search domain and nameservers) for the specified + /// node via the /nodes/{node}/dns endpoint. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveNodeDns", SupportsShouldProcess = true)] + public class SetPveNodeDnsCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// DNS search domain. + [Parameter(Mandatory = true, Position = 1, HelpMessage = "The DNS search domain.")] + [ValidateNotNullOrEmpty] + public string Search { get; set; } = string.Empty; + + /// Primary DNS server. + [Parameter(Mandatory = false, HelpMessage = "Primary DNS server address.")] + public string? Dns1 { get; set; } + + /// Secondary DNS server. + [Parameter(Mandatory = false, HelpMessage = "Secondary DNS server address.")] + public string? Dns2 { get; set; } + + /// Tertiary DNS server. + [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 + { + ["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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/StartPveNodeVmsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/StartPveNodeVmsCmdlet.cs new file mode 100644 index 0000000..9d0d3ca --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/StartPveNodeVmsCmdlet.cs @@ -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 +{ + /// + /// Starts all VMs and containers on a Proxmox VE node. + /// + /// 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. + /// + /// + [Cmdlet(VerbsLifecycle.Start, "PveNodeVms", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public class StartPveNodeVmsCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// Comma-separated list of VM IDs to start. If omitted, all VMs are started. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/StopPveNodeVmsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/StopPveNodeVmsCmdlet.cs new file mode 100644 index 0000000..c3bc890 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Nodes/StopPveNodeVmsCmdlet.cs @@ -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 +{ + /// + /// Stops all VMs and containers on a Proxmox VE node. + /// + /// 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. + /// + /// + [Cmdlet(VerbsLifecycle.Stop, "PveNodeVms", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(PveTask))] + public class StopPveNodeVmsCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// Comma-separated list of VM IDs to stop. If omitted, all VMs are stopped. + [Parameter(Mandatory = false, HelpMessage = "Comma-separated list of VM IDs to stop.")] + public string? VmIds { get; set; } + + /// Force stop VMs (do not wait for graceful shutdown). + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Pools/GetPvePoolCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Pools/GetPvePoolCmdlet.cs new file mode 100644 index 0000000..ddaf285 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Pools/GetPvePoolCmdlet.cs @@ -0,0 +1,46 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Cluster; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Pools +{ + /// + /// Lists Proxmox VE resource pools. + /// + /// Returns resource pool configurations from the Proxmox VE cluster. + /// Optionally filter by pool ID to retrieve a specific pool with its members. + /// + /// + [Cmdlet(VerbsCommon.Get, "PvePool")] + [OutputType(typeof(PvePool))] + public sealed class GetPvePoolCmdlet : PveCmdletBase + { + /// + /// The pool ID to retrieve. When omitted, all pools are returned. + /// + [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); + } + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Pools/NewPvePoolCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Pools/NewPvePoolCmdlet.cs new file mode 100644 index 0000000..1e8a06b --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Pools/NewPvePoolCmdlet.cs @@ -0,0 +1,40 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Pools +{ + /// + /// Creates a new resource pool in Proxmox VE. + /// + /// Creates a new resource pool in the Proxmox VE cluster with the specified ID + /// and optional comment. + /// + /// + [Cmdlet(VerbsCommon.New, "PvePool", SupportsShouldProcess = true)] + public sealed class NewPvePoolCmdlet : PveCmdletBase + { + /// + /// The pool ID to create. + /// + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The pool ID to create.")] + public string PoolId { get; set; } = string.Empty; + + /// + /// An optional comment or description for the pool. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Pools/RemovePvePoolCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Pools/RemovePvePoolCmdlet.cs new file mode 100644 index 0000000..40cff8b --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Pools/RemovePvePoolCmdlet.cs @@ -0,0 +1,36 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Pools +{ + /// + /// Removes a resource pool from Proxmox VE. + /// + /// Deletes a resource pool from the Proxmox VE cluster configuration. + /// This operation is destructive and requires confirmation unless -Force is specified. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PvePool", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public sealed class RemovePvePoolCmdlet : PveCmdletBase + { + /// + /// The pool ID to remove. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Pools/SetPvePoolCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Pools/SetPvePoolCmdlet.cs new file mode 100644 index 0000000..1872b72 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Pools/SetPvePoolCmdlet.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Pools +{ + /// + /// Updates a resource pool in Proxmox VE. + /// + /// Modifies an existing resource pool configuration. You can update the comment, + /// add members, or remove members. + /// + /// + [Cmdlet(VerbsCommon.Set, "PvePool", SupportsShouldProcess = true)] + public sealed class SetPvePoolCmdlet : PveCmdletBase + { + /// + /// The pool ID to update. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The pool ID to update.")] + public string PoolId { get; set; } = string.Empty; + + /// + /// An optional comment or description for the pool. + /// + [Parameter(Mandatory = false, HelpMessage = "An optional comment or description for the pool.")] + public string? Comment { get; set; } + + /// + /// Comma-separated list of members to add (e.g., "100,200,storage1"). + /// + [Parameter(Mandatory = false, HelpMessage = "Comma-separated list of members to add (e.g., '100,200,storage1').")] + public string? Members { get; set; } + + /// + /// Comma-separated list of members to remove. + /// + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageStatusCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageStatusCmdlet.cs new file mode 100644 index 0000000..f9e0da3 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageStatusCmdlet.cs @@ -0,0 +1,41 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Storage; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Returns the status of a Proxmox VE storage on a node. + /// + /// Retrieves capacity, usage and activation status for the specified storage + /// on the given node from the /nodes/{node}/storage/{storage}/status endpoint. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveStorageStatus")] + [OutputType(typeof(PveStorageStatus))] + public class GetPveStorageStatusCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// The storage identifier. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageDiskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageDiskCmdlet.cs new file mode 100644 index 0000000..f2827e4 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageDiskCmdlet.cs @@ -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 +{ + /// + /// Allocates a new disk image on a Proxmox VE storage. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.New, "PveStorageDisk", SupportsShouldProcess = true)] + [OutputType(typeof(string))] + public class NewPveStorageDiskCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// The storage identifier. + [Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")] + [ValidateNotNullOrEmpty] + public string Storage { get; set; } = string.Empty; + + /// The filename for the new disk (e.g. "vm-100-disk-1"). + [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; + + /// The size of the disk (e.g. "32G", "100M"). + [Parameter(Mandatory = true, Position = 3, HelpMessage = "The size of the disk (e.g. 32G, 100M).")] + [ValidateNotNullOrEmpty] + public string Size { get; set; } = string.Empty; + + /// The disk image format. + [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 + { + ["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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageContentCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageContentCmdlet.cs new file mode 100644 index 0000000..f897ea7 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageContentCmdlet.cs @@ -0,0 +1,45 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Removes a content volume from a Proxmox VE storage. + /// + /// Deletes the specified volume (disk image, ISO, template, backup, etc.) from the + /// storage on the given node. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PveStorageContent", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public class RemovePveStorageContentCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// The storage identifier. + [Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")] + [ValidateNotNullOrEmpty] + public string Storage { get; set; } = string.Empty; + + /// The volume identifier to remove (e.g. "local:iso/ubuntu.iso"). + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageCmdlet.cs new file mode 100644 index 0000000..90caf2b --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageCmdlet.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Updates a Proxmox VE storage definition. + /// + /// Modifies the configuration of an existing storage definition. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveStorage", SupportsShouldProcess = true)] + public sealed class SetPveStorageCmdlet : PveCmdletBase + { + /// The storage identifier to update. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The storage identifier.")] + public string Storage { get; set; } = string.Empty; + + /// Comma-separated list of content types (e.g. 'images,iso,backup'). + [Parameter(Mandatory = false, HelpMessage = "Comma-separated content types (e.g. 'images,iso,backup').")] + public string? Content { get; set; } + + /// Comma-separated list of nodes that can access this storage. + [Parameter(Mandatory = false, HelpMessage = "Comma-separated list of nodes.")] + public string? Nodes { get; set; } + + /// Whether the storage is enabled. + [Parameter(Mandatory = false, HelpMessage = "Whether the storage is enabled.")] + public SwitchParameter Enabled { get; set; } + + /// Whether the storage is shared across nodes. + [Parameter(Mandatory = false, HelpMessage = "Whether the storage is shared.")] + public SwitchParameter Shared { get; set; } + + /// Additional configuration as a hashtable for parameters not covered by named parameters. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageContentCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageContentCmdlet.cs new file mode 100644 index 0000000..b227cb2 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/SetPveStorageContentCmdlet.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Updates properties of a content volume in Proxmox VE storage. + /// + /// Modifies metadata (such as notes/description) of an existing volume in the specified + /// storage on the given node. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveStorageContent", SupportsShouldProcess = true)] + public class SetPveStorageContentCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + [ValidateNotNullOrEmpty] + public string Node { get; set; } = string.Empty; + + /// The storage identifier. + [Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")] + [ValidateNotNullOrEmpty] + public string Storage { get; set; } = string.Empty; + + /// The volume identifier to update. + [Parameter(Mandatory = true, Position = 2, ValueFromPipelineByPropertyName = true, HelpMessage = "The volume identifier to update.")] + [ValidateNotNullOrEmpty] + public string Volume { get; set; } = string.Empty; + + /// Notes/description to set on the volume. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskListCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskListCmdlet.cs new file mode 100644 index 0000000..97767a7 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskListCmdlet.cs @@ -0,0 +1,65 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Tasks +{ + /// + /// Lists tasks on a Proxmox VE node. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveTaskList")] + [OutputType(typeof(PveTask))] + public sealed class GetPveTaskListCmdlet : PveCmdletBase + { + /// + /// The node to list tasks from. + /// + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// Filter tasks by VM ID. + /// + [Parameter(Mandatory = false, HelpMessage = "Filter tasks by VM ID.")] + public int? VmId { get; set; } + + /// + /// Filter by task source: all or active. + /// + [Parameter(Mandatory = false, HelpMessage = "Filter by task source: all or active.")] + [ValidateSet("all", "active")] + public string? Source { get; set; } + + /// + /// Filter by task type (e.g., qmstart, vzdump). + /// + [Parameter(Mandatory = false, HelpMessage = "Filter by task type (e.g., qmstart, vzdump).")] + public string? TypeFilter { get; set; } + + /// + /// Maximum number of tasks to return. Defaults to 50. + /// + [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); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Tasks/StopPveTaskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Tasks/StopPveTaskCmdlet.cs new file mode 100644 index 0000000..64d5cd9 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Tasks/StopPveTaskCmdlet.cs @@ -0,0 +1,42 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Tasks +{ + /// + /// Stops a running task on a Proxmox VE node. + /// + /// Cancels a running task identified by its UPID on the specified node. + /// This is a destructive operation and requires confirmation. + /// + /// + [Cmdlet(VerbsLifecycle.Stop, "PveTask", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public sealed class StopPveTaskCmdlet : PveCmdletBase + { + /// + /// The node on which the task is running. + /// + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The UPID of the task to stop. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveDomainCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveDomainCmdlet.cs new file mode 100644 index 0000000..00adc2a --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveDomainCmdlet.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Users; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Lists Proxmox VE authentication domains/realms. + /// + /// Returns all authentication domains (realms) from the Proxmox VE access management + /// system. Optionally filter by a specific realm name. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveDomain")] + [OutputType(typeof(PveDomain))] + public class GetPveDomainCmdlet : PveCmdletBase + { + /// Optional realm name to filter by. + [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); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveGroupCmdlet.cs new file mode 100644 index 0000000..0b7f5de --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveGroupCmdlet.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Users; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Lists Proxmox VE groups. + /// + /// Returns all groups from the Proxmox VE access management system. Optionally + /// filter by a specific group ID. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveGroup")] + [OutputType(typeof(PveGroup))] + public class GetPveGroupCmdlet : PveCmdletBase + { + /// Optional group ID to filter by. + [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); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/NewPveDomainCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/NewPveDomainCmdlet.cs new file mode 100644 index 0000000..d0e4872 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/NewPveDomainCmdlet.cs @@ -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 +{ + /// + /// Creates a new Proxmox VE authentication domain/realm. + /// + /// Adds a new authentication domain (realm) to the Proxmox VE access management system. + /// + /// + [Cmdlet(VerbsCommon.New, "PveDomain", SupportsShouldProcess = true)] + [OutputType(typeof(PveDomain))] + public class NewPveDomainCmdlet : PveCmdletBase + { + /// The realm identifier. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The realm identifier.")] + [ValidateNotNullOrEmpty] + public string Realm { get; set; } = string.Empty; + + /// The domain type. + [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; + + /// Optional comment/description for the domain. + [Parameter(Mandatory = false, HelpMessage = "Comment or description for the domain.")] + public string? Comment { get; set; } + + /// Set this realm as the default authentication domain. + [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 + { + ["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 + }); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/NewPveGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/NewPveGroupCmdlet.cs new file mode 100644 index 0000000..d90901f --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/NewPveGroupCmdlet.cs @@ -0,0 +1,44 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Users; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Creates a new Proxmox VE group. + /// + /// Adds a new group to the Proxmox VE access management system. + /// + /// + [Cmdlet(VerbsCommon.New, "PveGroup", SupportsShouldProcess = true)] + [OutputType(typeof(PveGroup))] + public class NewPveGroupCmdlet : PveCmdletBase + { + /// The group identifier. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The group identifier.")] + [ValidateNotNullOrEmpty] + public string GroupId { get; set; } = string.Empty; + + /// Optional comment/description for the group. + [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 + }); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveDomainCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveDomainCmdlet.cs new file mode 100644 index 0000000..7f45350 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveDomainCmdlet.cs @@ -0,0 +1,35 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Removes a Proxmox VE authentication domain/realm. + /// + /// Deletes the specified authentication domain (realm) from the Proxmox VE + /// access management system. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PveDomain", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public class RemovePveDomainCmdlet : PveCmdletBase + { + /// The realm identifier to remove. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveGroupCmdlet.cs new file mode 100644 index 0000000..e9513fe --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveGroupCmdlet.cs @@ -0,0 +1,34 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Removes a Proxmox VE group. + /// + /// Deletes the specified group from the Proxmox VE access management system. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PveGroup", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public class RemovePveGroupCmdlet : PveCmdletBase + { + /// The group identifier to remove. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPveApiTokenCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPveApiTokenCmdlet.cs new file mode 100644 index 0000000..30f7abc --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPveApiTokenCmdlet.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Updates a Proxmox VE API token configuration. + /// + /// Modifies properties of an existing API token such as its comment, + /// expiration, or privilege separation setting. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveApiToken", SupportsShouldProcess = true)] + public sealed class SetPveApiTokenCmdlet : PveCmdletBase + { + /// The user ID that owns the token (e.g. 'user@realm'). + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The user ID (e.g. 'user@realm').")] + public string UserId { get; set; } = string.Empty; + + /// The token identifier. + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The token identifier.")] + public string TokenId { get; set; } = string.Empty; + + /// Optional comment for the token. + [Parameter(Mandatory = false, HelpMessage = "Comment describing the token.")] + public string? Comment { get; set; } + + /// Token expiration as a Unix epoch timestamp. Set to 0 for no expiration. + [Parameter(Mandatory = false, HelpMessage = "Expiration as Unix epoch (0 = no expiration).")] + public long? Expire { get; set; } + + /// Whether the token uses privilege separation. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPveDomainCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPveDomainCmdlet.cs new file mode 100644 index 0000000..b4bdbb6 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPveDomainCmdlet.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Updates a Proxmox VE authentication domain/realm. + /// + /// Modifies properties of an existing authentication domain (realm) in the + /// Proxmox VE access management system. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveDomain", SupportsShouldProcess = true)] + public class SetPveDomainCmdlet : PveCmdletBase + { + /// The realm identifier to update. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The realm identifier.")] + [ValidateNotNullOrEmpty] + public string Realm { get; set; } = string.Empty; + + /// Updated comment/description for the domain. + [Parameter(Mandatory = false, HelpMessage = "Updated comment or description for the domain.")] + public string? Comment { get; set; } + + /// Set this realm as the default authentication domain. + [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(); + 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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPveGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPveGroupCmdlet.cs new file mode 100644 index 0000000..5910f1b --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPveGroupCmdlet.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Updates a Proxmox VE group. + /// + /// Modifies properties of an existing group in the Proxmox VE access management system. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveGroup", SupportsShouldProcess = true)] + public class SetPveGroupCmdlet : PveCmdletBase + { + /// The group identifier to update. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The group identifier.")] + [ValidateNotNullOrEmpty] + public string GroupId { get; set; } = string.Empty; + + /// Updated comment/description for the group. + [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(); + if (Comment != null) config["comment"] = Comment; + + var session = GetSession(); + var service = new UserService(); + + WriteVerbose($"Updating group '{GroupId}'..."); + service.UpdateGroup(session, GroupId, config); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPvePasswordCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPvePasswordCmdlet.cs new file mode 100644 index 0000000..1416d0a --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPvePasswordCmdlet.cs @@ -0,0 +1,49 @@ +using System.Management.Automation; +using System.Runtime.InteropServices; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Changes a Proxmox VE user's password. + /// + /// Updates the password for the specified user via the /access/password endpoint. + /// The password is accepted as a SecureString for safe handling in PowerShell. + /// + /// + [Cmdlet(VerbsCommon.Set, "PvePassword", SupportsShouldProcess = true)] + public class SetPvePasswordCmdlet : PveCmdletBase + { + /// The user identifier in "user@realm" format. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The user ID in user@realm format.")] + [ValidateNotNullOrEmpty] + public string UserId { get; set; } = string.Empty; + + /// The new password for the user. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPveRoleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPveRoleCmdlet.cs new file mode 100644 index 0000000..8d10294 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPveRoleCmdlet.cs @@ -0,0 +1,35 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Users +{ + /// + /// Updates a Proxmox VE role's privileges. + /// + /// Modifies the privilege set for an existing custom role. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveRole", SupportsShouldProcess = true)] + public sealed class SetPveRoleCmdlet : PveCmdletBase + { + /// The role identifier to update. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The role identifier.")] + public string RoleId { get; set; } = string.Empty; + + /// Comma-separated list of privileges to assign to the role. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestFsInfoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestFsInfoCmdlet.cs new file mode 100644 index 0000000..00c21ad --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestFsInfoCmdlet.cs @@ -0,0 +1,40 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Gets filesystem information from the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveVmGuestFsInfo")] + [OutputType(typeof(PveGuestFsInfo))] + public sealed class GetPveVmGuestFsInfoCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestOsInfoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestOsInfoCmdlet.cs new file mode 100644 index 0000000..7c557e6 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmGuestOsInfoCmdlet.cs @@ -0,0 +1,40 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Gets OS information from the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveVmGuestOsInfo")] + [OutputType(typeof(PveGuestOsInfo))] + public sealed class GetPveVmGuestOsInfoCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestFsTrimCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestFsTrimCmdlet.cs new file mode 100644 index 0000000..ab23675 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestFsTrimCmdlet.cs @@ -0,0 +1,38 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Invokes an fstrim operation inside the guest via the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsLifecycle.Invoke, "PveVmGuestFsTrim", SupportsShouldProcess = true)] + public sealed class InvokePveVmGuestFsTrimCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/MovePveVmDiskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/MovePveVmDiskCmdlet.cs new file mode 100644 index 0000000..427af7a --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/MovePveVmDiskCmdlet.cs @@ -0,0 +1,83 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Moves a VM disk to a different storage. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Move, "PveVmDisk", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class MovePveVmDiskCmdlet : PveCmdletBase + { + /// + /// The node on which the VM resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the VM whose disk should be moved. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The disk slot to move (e.g., "scsi0", "virtio0"). + /// + [Parameter(Mandatory = true, HelpMessage = "The disk slot to move (e.g. scsi0, virtio0).")] + public string Disk { get; set; } = string.Empty; + + /// + /// The target storage to move the disk to. + /// + [Parameter(Mandatory = true, HelpMessage = "The target storage identifier.")] + public string Storage { get; set; } = string.Empty; + + /// + /// The target disk format. + /// + [Parameter(Mandatory = false, HelpMessage = "Target disk format (raw, qcow2, vmdk).")] + [ValidateSet("raw", "qcow2", "vmdk")] + public string? Format { get; set; } + + /// + /// When specified, deletes the original disk after a successful move. Default is true. + /// + [Parameter(Mandatory = false, HelpMessage = "Delete the original disk after move (default true).")] + public SwitchParameter Delete { get; set; } = true; + + /// + /// When specified, waits for the move task to complete before returning. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/ReadPveVmGuestFileCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/ReadPveVmGuestFileCmdlet.cs new file mode 100644 index 0000000..775c128 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/ReadPveVmGuestFileCmdlet.cs @@ -0,0 +1,44 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Reads a file from the guest filesystem via the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommunications.Read, "PveVmGuestFile")] + [OutputType(typeof(string))] + public sealed class ReadPveVmGuestFileCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The path to the file inside the guest to read. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/RemovePveVmDiskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/RemovePveVmDiskCmdlet.cs new file mode 100644 index 0000000..26d3285 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/RemovePveVmDiskCmdlet.cs @@ -0,0 +1,55 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Removes (unlinks) disks from a QEMU/KVM virtual machine. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PveVmDisk", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public sealed class RemovePveVmDiskCmdlet : PveCmdletBase + { + /// + /// The node on which the VM resides. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// + /// The ID of the VM whose disks should be removed. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// Comma-separated list of disk IDs to unlink (e.g., "scsi0,scsi1"). + /// + [Parameter(Mandatory = true, HelpMessage = "Comma-separated list of disk IDs to unlink (e.g. scsi0,scsi1).")] + public string IdList { get; set; } = string.Empty; + + /// + /// When specified, forces removal of the disk images from storage. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmGuestPasswordCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmGuestPasswordCmdlet.cs new file mode 100644 index 0000000..f3104ed --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/SetPveVmGuestPasswordCmdlet.cs @@ -0,0 +1,56 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Sets a user password inside the guest via the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Set, "PveVmGuestPassword", SupportsShouldProcess = true)] + public sealed class SetPveVmGuestPasswordCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The username whose password should be changed. + /// + [Parameter(Mandatory = true, HelpMessage = "The username whose password to set.")] + public string Username { get; set; } = string.Empty; + + /// + /// The new password for the user. + /// + [Parameter(Mandatory = true, HelpMessage = "The new password.")] + public string Password { get; set; } = string.Empty; + + /// + /// When specified, indicates the password is already in crypted/hashed format. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Vms/WritePveVmGuestFileCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/WritePveVmGuestFileCmdlet.cs new file mode 100644 index 0000000..eb2c2e6 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Vms/WritePveVmGuestFileCmdlet.cs @@ -0,0 +1,49 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Vms +{ + /// + /// Writes content to a file on the guest filesystem via the QEMU guest agent. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommunications.Write, "PveVmGuestFile", SupportsShouldProcess = true)] + public sealed class WritePveVmGuestFileCmdlet : PveCmdletBase + { + /// The Proxmox VE node name. + [Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The VM identifier. + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// + /// The path to the file inside the guest to write. + /// + [Parameter(Mandatory = true, HelpMessage = "The file path inside the guest to write.")] + public string File { get; set; } = string.Empty; + + /// + /// The content to write to the file. + /// + [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); + } + } +} diff --git a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml index c0c2c79..39d60c8 100644 --- a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml +++ b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml @@ -944,5 +944,417 @@ + + + PSProxmoxVE.Core.Models.Cluster.PveClusterResource + + PSProxmoxVE.Core.Models.Cluster.PveClusterResource + + + + + + 8 + Left + + + + 20 + Left + + + + 10 + Left + + + + 15 + Left + + + + 8 + Right + + + + 8 + Right + + + + 12 + Right + + + + 15 + Left + + + + + + + Type + + + Name + + + Status + + + Node + + + VmId + + + if ($_.Cpu -ne $null) { [math]::Round($_.Cpu * 100, 1).ToString() + '%' } else { 'N/A' } + + + if ($_.MaxMem) { [math]::Round($_.MaxMem / 1GB, 1).ToString() + 'G' } else { 'N/A' } + + + Pool + + + + + + + + + + PSProxmoxVE.Core.Models.Cluster.PvePool + + PSProxmoxVE.Core.Models.Cluster.PvePool + + + + + + 25 + Left + + + + Left + + + + + + + PoolId + + + Comment + + + + + + + + + + PSProxmoxVE.Core.Models.Vms.PveGuestOsInfo + + PSProxmoxVE.Core.Models.Vms.PveGuestOsInfo + + + + + + 25 + Left + + + + 25 + Left + + + + 15 + Left + + + + + + + Name + + + KernelRelease + + + Machine + + + + + + + + + + PSProxmoxVE.Core.Models.Vms.PveGuestFsInfo + + PSProxmoxVE.Core.Models.Vms.PveGuestFsInfo + + + + + + 15 + Left + + + + 20 + Left + + + + 10 + Left + + + + 15 + Right + + + + 15 + Right + + + + + + + Name + + + MountPoint + + + Type + + + if ($_.TotalBytes) { [math]::Round($_.TotalBytes / 1GB, 1).ToString() + ' GB' } else { 'N/A' } + + + if ($_.UsedBytes) { [math]::Round($_.UsedBytes / 1GB, 1).ToString() + ' GB' } else { 'N/A' } + + + + + + + + + + PSProxmoxVE.Core.Models.Containers.PveContainerInterface + + PSProxmoxVE.Core.Models.Containers.PveContainerInterface + + + + + + 10 + Left + + + + 20 + Left + + + + 20 + Left + + + + 30 + Left + + + + + + + Name + + + HwAddr + + + Inet + + + Inet6 + + + + + + + + + + PSProxmoxVE.Core.Models.Storage.PveStorageStatus + + PSProxmoxVE.Core.Models.Storage.PveStorageStatus + + + + + + 10 + Left + + + + 15 + Right + + + + 15 + Right + + + + 15 + Right + + + + 8 + Left + + + + + + + Type + + + if ($_.Total) { [math]::Round($_.Total / 1GB, 1).ToString() + ' GB' } else { 'N/A' } + + + if ($_.Used) { [math]::Round($_.Used / 1GB, 1).ToString() + ' GB' } else { 'N/A' } + + + if ($_.Available) { [math]::Round($_.Available / 1GB, 1).ToString() + ' GB' } else { 'N/A' } + + + if ($_.Active) { 'Yes' } else { 'No' } + + + + + + + + + + PSProxmoxVE.Core.Models.Users.PveGroup + + PSProxmoxVE.Core.Models.Users.PveGroup + + + + + + 25 + Left + + + + Left + + + + Left + + + + + + + GroupId + + + Comment + + + Users + + + + + + + + + + PSProxmoxVE.Core.Models.Users.PveDomain + + PSProxmoxVE.Core.Models.Users.PveDomain + + + + + + 15 + Left + + + + 10 + Left + + + + Left + + + + 8 + Left + + + + + + + Realm + + + Type + + + Comment + + + if ($_.Default) { 'Yes' } else { 'No' } + + + + + + + diff --git a/src/PSProxmoxVE/PSProxmoxVE.psd1 b/src/PSProxmoxVE/PSProxmoxVE.psd1 index 6bcc6fa..9a25937 100644 --- a/src/PSProxmoxVE/PSProxmoxVE.psd1 +++ b/src/PSProxmoxVE/PSProxmoxVE.psd1 @@ -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 diff --git a/tests/PSProxmoxVE.Tests/Backup/BackupInfoCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Backup/BackupInfoCmdlets.Tests.ps1 new file mode 100644 index 0000000..98436c9 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Backup/BackupInfoCmdlets.Tests.ps1 @@ -0,0 +1,58 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for backup info cmdlets: + Get-PveBackupInfo. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Get-PveBackupInfo')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveBackupInfo +# --------------------------------------------------------------------------- +Describe 'Get-PveBackupInfo' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveBackupInfo' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveBackupInfo' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveBackupInfo' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Should have Session parameter' { + Skip-IfMissing 'Get-PveBackupInfo' + $script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveBackupInfo' + { Get-PveBackupInfo -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Cluster/ClusterCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Cluster/ClusterCmdlets.Tests.ps1 new file mode 100644 index 0000000..5b59777 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Cluster/ClusterCmdlets.Tests.ps1 @@ -0,0 +1,66 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for cluster cmdlets: + Get-PveClusterResource. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Get-PveClusterResource')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveClusterResource +# --------------------------------------------------------------------------- +Describe 'Get-PveClusterResource' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveClusterResource' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveClusterResource' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveClusterResource' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Should have optional Type parameter' { + Skip-IfMissing 'Get-PveClusterResource' + $script:Cmd.Parameters.ContainsKey('Type') | Should -BeTrue + } + It 'Should have optional Node parameter' { + Skip-IfMissing 'Get-PveClusterResource' + $script:Cmd.Parameters.ContainsKey('Node') | Should -BeTrue + } + It 'Should have Session parameter' { + Skip-IfMissing 'Get-PveClusterResource' + $script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveClusterResource' + { Get-PveClusterResource -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Containers/ContainerGapCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Containers/ContainerGapCmdlets.Tests.ps1 new file mode 100644 index 0000000..470c011 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Containers/ContainerGapCmdlets.Tests.ps1 @@ -0,0 +1,342 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for container gap cmdlets: + Suspend-PveContainer, Resume-PveContainer, Resize-PveContainerDisk, + New-PveContainerTemplate, Move-PveContainerVolume, Get-PveContainerInterface. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Suspend-PveContainer', 'Resume-PveContainer', 'Resize-PveContainerDisk', + 'New-PveContainerTemplate', 'Move-PveContainerVolume', 'Get-PveContainerInterface' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Suspend-PveContainer +# --------------------------------------------------------------------------- +Describe 'Suspend-PveContainer' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Suspend-PveContainer' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Suspend-PveContainer' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Suspend-PveContainer' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Suspend-PveContainer' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Suspend-PveContainer' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Suspend-PveContainer' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Suspend-PveContainer' + { Suspend-PveContainer -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Resume-PveContainer +# --------------------------------------------------------------------------- +Describe 'Resume-PveContainer' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Resume-PveContainer' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Resume-PveContainer' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Resume-PveContainer' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Resume-PveContainer' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Resume-PveContainer' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Resume-PveContainer' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Resume-PveContainer' + { Resume-PveContainer -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Resize-PveContainerDisk +# --------------------------------------------------------------------------- +Describe 'Resize-PveContainerDisk' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Resize-PveContainerDisk' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Resize-PveContainerDisk' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Resize-PveContainerDisk' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Resize-PveContainerDisk' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Resize-PveContainerDisk' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Resize-PveContainerDisk' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Disk should be Mandatory' { + Skip-IfMissing 'Resize-PveContainerDisk' + $isMandatory = $script:Cmd.Parameters['Disk'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Size should be Mandatory' { + Skip-IfMissing 'Resize-PveContainerDisk' + $isMandatory = $script:Cmd.Parameters['Size'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Resize-PveContainerDisk' + { Resize-PveContainerDisk -Node 'pve' -VmId 100 -Disk 'rootfs' -Size '+5G' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# New-PveContainerTemplate +# --------------------------------------------------------------------------- +Describe 'New-PveContainerTemplate' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'New-PveContainerTemplate' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'New-PveContainerTemplate' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'New-PveContainerTemplate' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'New-PveContainerTemplate' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'New-PveContainerTemplate' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'New-PveContainerTemplate' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'New-PveContainerTemplate' + { New-PveContainerTemplate -Node 'pve' -VmId 100 -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Move-PveContainerVolume +# --------------------------------------------------------------------------- +Describe 'Move-PveContainerVolume' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Move-PveContainerVolume' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Move-PveContainerVolume' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Move-PveContainerVolume' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Move-PveContainerVolume' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Move-PveContainerVolume' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Move-PveContainerVolume' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Volume should be Mandatory' { + Skip-IfMissing 'Move-PveContainerVolume' + $isMandatory = $script:Cmd.Parameters['Volume'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'Move-PveContainerVolume' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Move-PveContainerVolume' + { Move-PveContainerVolume -Node 'pve' -VmId 100 -Volume 'rootfs' -Storage 'local-lvm' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveContainerInterface +# --------------------------------------------------------------------------- +Describe 'Get-PveContainerInterface' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveContainerInterface' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveContainerInterface' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveContainerInterface' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveContainerInterface' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Get-PveContainerInterface' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveContainerInterface' + { Get-PveContainerInterface -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Network/SdnSetCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Network/SdnSetCmdlets.Tests.ps1 new file mode 100644 index 0000000..dacebdd --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Network/SdnSetCmdlets.Tests.ps1 @@ -0,0 +1,424 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for SDN Set cmdlets and related mutating cmdlets: + Set-PveSdnZone, Set-PveSdnVnet, Set-PveSdnSubnet, Set-PveSdnController, + Set-PveSdnIpam, Set-PveSdnDns, Invoke-PveSdnApply, Set-PveRole, Set-PveApiToken. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Set-PveSdnZone', 'Set-PveSdnVnet', 'Set-PveSdnSubnet', 'Set-PveSdnController', + 'Set-PveSdnIpam', 'Set-PveSdnDns', 'Invoke-PveSdnApply', 'Set-PveRole', 'Set-PveApiToken' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnZone +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnZone' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnZone' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnZone' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnZone' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnZone' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Zone should be Mandatory' { + Skip-IfMissing 'Set-PveSdnZone' + $isMandatory = $script:Cmd.Parameters['Zone'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnZone' + { Set-PveSdnZone -Zone 'testzone' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnVnet +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnVnet' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnVnet' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnVnet' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnVnet' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnVnet' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Vnet should be Mandatory' { + Skip-IfMissing 'Set-PveSdnVnet' + $isMandatory = $script:Cmd.Parameters['Vnet'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnVnet' + { Set-PveSdnVnet -Vnet 'testvnet' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnSubnet +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnSubnet' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnSubnet' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnSubnet' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnSubnet' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnSubnet' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Vnet should be Mandatory' { + Skip-IfMissing 'Set-PveSdnSubnet' + $isMandatory = $script:Cmd.Parameters['Vnet'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Subnet should be Mandatory' { + Skip-IfMissing 'Set-PveSdnSubnet' + $isMandatory = $script:Cmd.Parameters['Subnet'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnSubnet' + { Set-PveSdnSubnet -Vnet 'testvnet' -Subnet '10.0.0.0/24' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnController +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnController' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnController' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnController' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnController' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnController' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Controller should be Mandatory' { + Skip-IfMissing 'Set-PveSdnController' + $isMandatory = $script:Cmd.Parameters['Controller'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnController' + { Set-PveSdnController -Controller 'testctrl' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnIpam +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnIpam' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnIpam' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnIpam' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnIpam' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnIpam' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Ipam should be Mandatory' { + Skip-IfMissing 'Set-PveSdnIpam' + $isMandatory = $script:Cmd.Parameters['Ipam'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnIpam' + { Set-PveSdnIpam -Ipam 'testipam' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveSdnDns +# --------------------------------------------------------------------------- +Describe 'Set-PveSdnDns' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveSdnDns' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveSdnDns' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveSdnDns' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveSdnDns' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Dns should be Mandatory' { + Skip-IfMissing 'Set-PveSdnDns' + $isMandatory = $script:Cmd.Parameters['Dns'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveSdnDns' + { Set-PveSdnDns -Dns 'testdns' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Invoke-PveSdnApply +# --------------------------------------------------------------------------- +Describe 'Invoke-PveSdnApply' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Invoke-PveSdnApply' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Invoke-PveSdnApply' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Invoke-PveSdnApply' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Invoke-PveSdnApply' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Invoke-PveSdnApply' + { Invoke-PveSdnApply -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveRole +# --------------------------------------------------------------------------- +Describe 'Set-PveRole' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveRole' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveRole' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveRole' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveRole' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'RoleId should be Mandatory' { + Skip-IfMissing 'Set-PveRole' + $isMandatory = $script:Cmd.Parameters['RoleId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Privileges should be Mandatory' { + Skip-IfMissing 'Set-PveRole' + $isMandatory = $script:Cmd.Parameters['Privileges'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveRole' + { Set-PveRole -RoleId 'testrole' -Privileges 'VM.Audit' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveApiToken +# --------------------------------------------------------------------------- +Describe 'Set-PveApiToken' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveApiToken' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveApiToken' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveApiToken' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveApiToken' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'UserId should be Mandatory' { + Skip-IfMissing 'Set-PveApiToken' + $isMandatory = $script:Cmd.Parameters['UserId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'TokenId should be Mandatory' { + Skip-IfMissing 'Set-PveApiToken' + $isMandatory = $script:Cmd.Parameters['TokenId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveApiToken' + { Set-PveApiToken -UserId 'user@pam' -TokenId 'testtoken' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Nodes/NodeOpsCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Nodes/NodeOpsCmdlets.Tests.ps1 new file mode 100644 index 0000000..50024f1 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Nodes/NodeOpsCmdlets.Tests.ps1 @@ -0,0 +1,281 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for node operation cmdlets: + Get-PveNodeConfig, Set-PveNodeConfig, Get-PveNodeDns, Set-PveNodeDns, + Start-PveNodeVms, Stop-PveNodeVms. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Get-PveNodeConfig', 'Set-PveNodeConfig', 'Get-PveNodeDns', 'Set-PveNodeDns', + 'Start-PveNodeVms', 'Stop-PveNodeVms' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveNodeConfig +# --------------------------------------------------------------------------- +Describe 'Get-PveNodeConfig' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveNodeConfig' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveNodeConfig' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveNodeConfig' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveNodeConfig' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveNodeConfig' + { Get-PveNodeConfig -Node 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveNodeConfig +# --------------------------------------------------------------------------- +Describe 'Set-PveNodeConfig' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveNodeConfig' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveNodeConfig' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveNodeConfig' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveNodeConfig' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Set-PveNodeConfig' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveNodeConfig' + { Set-PveNodeConfig -Node 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveNodeDns +# --------------------------------------------------------------------------- +Describe 'Get-PveNodeDns' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveNodeDns' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveNodeDns' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveNodeDns' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveNodeDns' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveNodeDns' + { Get-PveNodeDns -Node 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveNodeDns +# --------------------------------------------------------------------------- +Describe 'Set-PveNodeDns' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveNodeDns' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveNodeDns' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveNodeDns' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveNodeDns' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Set-PveNodeDns' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Search should be Mandatory' { + Skip-IfMissing 'Set-PveNodeDns' + $isMandatory = $script:Cmd.Parameters['Search'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveNodeDns' + { Set-PveNodeDns -Node 'pve' -Search 'example.com' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Start-PveNodeVms +# --------------------------------------------------------------------------- +Describe 'Start-PveNodeVms' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Start-PveNodeVms' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Start-PveNodeVms' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Start-PveNodeVms' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Start-PveNodeVms' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Start-PveNodeVms' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Start-PveNodeVms' + { Start-PveNodeVms -Node 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Stop-PveNodeVms +# --------------------------------------------------------------------------- +Describe 'Stop-PveNodeVms' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Stop-PveNodeVms' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Stop-PveNodeVms' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Stop-PveNodeVms' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Stop-PveNodeVms' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Stop-PveNodeVms' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Stop-PveNodeVms' + { Stop-PveNodeVms -Node 'pve' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Pools/PoolCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Pools/PoolCmdlets.Tests.ps1 new file mode 100644 index 0000000..3657ee7 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Pools/PoolCmdlets.Tests.ps1 @@ -0,0 +1,182 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for pool cmdlets: + Get-PvePool, New-PvePool, Set-PvePool, Remove-PvePool. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Get-PvePool', 'New-PvePool', 'Set-PvePool', 'Remove-PvePool')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PvePool +# --------------------------------------------------------------------------- +Describe 'Get-PvePool' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PvePool' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PvePool' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PvePool' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Should have optional PoolId parameter' { + Skip-IfMissing 'Get-PvePool' + $script:Cmd.Parameters.ContainsKey('PoolId') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PvePool' + { Get-PvePool -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# New-PvePool +# --------------------------------------------------------------------------- +Describe 'New-PvePool' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'New-PvePool' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'New-PvePool' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'New-PvePool' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'PoolId should be Mandatory' { + Skip-IfMissing 'New-PvePool' + $isMandatory = $script:Cmd.Parameters['PoolId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'New-PvePool' + { New-PvePool -PoolId 'testpool' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PvePool +# --------------------------------------------------------------------------- +Describe 'Set-PvePool' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PvePool' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PvePool' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PvePool' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'PoolId should be Mandatory' { + Skip-IfMissing 'Set-PvePool' + $isMandatory = $script:Cmd.Parameters['PoolId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PvePool' + { Set-PvePool -PoolId 'testpool' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Remove-PvePool +# --------------------------------------------------------------------------- +Describe 'Remove-PvePool' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Remove-PvePool' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Remove-PvePool' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Remove-PvePool' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Remove-PvePool' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'PoolId should be Mandatory' { + Skip-IfMissing 'Remove-PvePool' + $isMandatory = $script:Cmd.Parameters['PoolId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Remove-PvePool' + { Remove-PvePool -PoolId 'testpool' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Storage/SetPveStorageCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Storage/SetPveStorageCmdlets.Tests.ps1 new file mode 100644 index 0000000..11b0deb --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Storage/SetPveStorageCmdlets.Tests.ps1 @@ -0,0 +1,287 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for storage management cmdlets: + Set-PveStorage, Get-PveStorageStatus, Remove-PveStorageContent, + Set-PveStorageContent, New-PveStorageDisk. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Set-PveStorage', 'Get-PveStorageStatus', 'Remove-PveStorageContent', + 'Set-PveStorageContent', 'New-PveStorageDisk' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveStorage +# --------------------------------------------------------------------------- +Describe 'Set-PveStorage' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveStorage' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveStorage' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveStorage' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveStorage' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Storage should be Mandatory' { + Skip-IfMissing 'Set-PveStorage' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveStorage' + { Set-PveStorage -Storage 'local' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveStorageStatus +# --------------------------------------------------------------------------- +Describe 'Get-PveStorageStatus' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveStorageStatus' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveStorageStatus' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveStorageStatus' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveStorageStatus' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'Get-PveStorageStatus' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveStorageStatus' + { Get-PveStorageStatus -Node 'pve' -Storage 'local' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Remove-PveStorageContent +# --------------------------------------------------------------------------- +Describe 'Remove-PveStorageContent' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Remove-PveStorageContent' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Remove-PveStorageContent' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Remove-PveStorageContent' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Remove-PveStorageContent' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Remove-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'Remove-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Volume should be Mandatory' { + Skip-IfMissing 'Remove-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Volume'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Remove-PveStorageContent' + { Remove-PveStorageContent -Node 'pve' -Storage 'local' -Volume 'local:iso/test.iso' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveStorageContent +# --------------------------------------------------------------------------- +Describe 'Set-PveStorageContent' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveStorageContent' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveStorageContent' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveStorageContent' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveStorageContent' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Set-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'Set-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Volume should be Mandatory' { + Skip-IfMissing 'Set-PveStorageContent' + $isMandatory = $script:Cmd.Parameters['Volume'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveStorageContent' + { Set-PveStorageContent -Node 'pve' -Storage 'local' -Volume 'local:iso/test.iso' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# New-PveStorageDisk +# --------------------------------------------------------------------------- +Describe 'New-PveStorageDisk' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'New-PveStorageDisk' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'New-PveStorageDisk' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'New-PveStorageDisk' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'New-PveStorageDisk' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'New-PveStorageDisk' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'New-PveStorageDisk' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Filename should be Mandatory' { + Skip-IfMissing 'New-PveStorageDisk' + $isMandatory = $script:Cmd.Parameters['Filename'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Size should be Mandatory' { + Skip-IfMissing 'New-PveStorageDisk' + $isMandatory = $script:Cmd.Parameters['Size'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'New-PveStorageDisk' + { New-PveStorageDisk -Node 'pve' -Storage 'local' -Filename 'vm-100-disk-1' -Size '32G' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Tasks/TaskListCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Tasks/TaskListCmdlets.Tests.ps1 new file mode 100644 index 0000000..3aec602 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Tasks/TaskListCmdlets.Tests.ps1 @@ -0,0 +1,127 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for task list cmdlets: + Get-PveTaskList, Stop-PveTask. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Get-PveTaskList', 'Stop-PveTask')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveTaskList +# --------------------------------------------------------------------------- +Describe 'Get-PveTaskList' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveTaskList' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveTaskList' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveTaskList' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveTaskList' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Parameter metadata' { + It 'Should have optional VmId parameter' { + Skip-IfMissing 'Get-PveTaskList' + $script:Cmd.Parameters.ContainsKey('VmId') | Should -BeTrue + } + It 'Should have optional Limit parameter' { + Skip-IfMissing 'Get-PveTaskList' + $script:Cmd.Parameters.ContainsKey('Limit') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveTaskList' + { Get-PveTaskList -Node 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Stop-PveTask +# --------------------------------------------------------------------------- +Describe 'Stop-PveTask' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Stop-PveTask' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Stop-PveTask' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Stop-PveTask' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Stop-PveTask' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Stop-PveTask' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Stop-PveTask' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Upid should be Mandatory' { + Skip-IfMissing 'Stop-PveTask' + $isMandatory = $script:Cmd.Parameters['Upid'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Stop-PveTask' + { Stop-PveTask -Node 'pve' -Upid 'UPID:pve:00000001:00000000:00000000:test::root@pam:' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Users/AccessManagementCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Users/AccessManagementCmdlets.Tests.ps1 new file mode 100644 index 0000000..a2704c9 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Users/AccessManagementCmdlets.Tests.ps1 @@ -0,0 +1,400 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for access management cmdlets: + Get-PveGroup, New-PveGroup, Set-PveGroup, Remove-PveGroup, + Get-PveDomain, New-PveDomain, Set-PveDomain, Remove-PveDomain, Set-PvePassword. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Get-PveGroup', 'New-PveGroup', 'Set-PveGroup', 'Remove-PveGroup', + 'Get-PveDomain', 'New-PveDomain', 'Set-PveDomain', 'Remove-PveDomain', 'Set-PvePassword' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveGroup +# --------------------------------------------------------------------------- +Describe 'Get-PveGroup' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveGroup' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveGroup' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveGroup' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Should have optional GroupId parameter' { + Skip-IfMissing 'Get-PveGroup' + $script:Cmd.Parameters.ContainsKey('GroupId') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveGroup' + { Get-PveGroup -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# New-PveGroup +# --------------------------------------------------------------------------- +Describe 'New-PveGroup' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'New-PveGroup' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'New-PveGroup' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'New-PveGroup' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'GroupId should be Mandatory' { + Skip-IfMissing 'New-PveGroup' + $isMandatory = $script:Cmd.Parameters['GroupId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'New-PveGroup' + { New-PveGroup -GroupId 'testgroup' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveGroup +# --------------------------------------------------------------------------- +Describe 'Set-PveGroup' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveGroup' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveGroup' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveGroup' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'GroupId should be Mandatory' { + Skip-IfMissing 'Set-PveGroup' + $isMandatory = $script:Cmd.Parameters['GroupId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveGroup' + { Set-PveGroup -GroupId 'testgroup' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Remove-PveGroup +# --------------------------------------------------------------------------- +Describe 'Remove-PveGroup' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Remove-PveGroup' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Remove-PveGroup' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Remove-PveGroup' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Remove-PveGroup' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'GroupId should be Mandatory' { + Skip-IfMissing 'Remove-PveGroup' + $isMandatory = $script:Cmd.Parameters['GroupId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Remove-PveGroup' + { Remove-PveGroup -GroupId 'testgroup' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveDomain +# --------------------------------------------------------------------------- +Describe 'Get-PveDomain' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveDomain' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveDomain' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveDomain' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Should have optional Realm parameter' { + Skip-IfMissing 'Get-PveDomain' + $script:Cmd.Parameters.ContainsKey('Realm') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveDomain' + { Get-PveDomain -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# New-PveDomain +# --------------------------------------------------------------------------- +Describe 'New-PveDomain' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'New-PveDomain' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'New-PveDomain' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'New-PveDomain' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Realm should be Mandatory' { + Skip-IfMissing 'New-PveDomain' + $isMandatory = $script:Cmd.Parameters['Realm'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Type should be Mandatory' { + Skip-IfMissing 'New-PveDomain' + $isMandatory = $script:Cmd.Parameters['Type'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'New-PveDomain' + { New-PveDomain -Realm 'testrealm' -Type 'pve' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveDomain +# --------------------------------------------------------------------------- +Describe 'Set-PveDomain' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveDomain' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveDomain' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveDomain' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Realm should be Mandatory' { + Skip-IfMissing 'Set-PveDomain' + $isMandatory = $script:Cmd.Parameters['Realm'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveDomain' + { Set-PveDomain -Realm 'testrealm' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Remove-PveDomain +# --------------------------------------------------------------------------- +Describe 'Remove-PveDomain' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Remove-PveDomain' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Remove-PveDomain' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Remove-PveDomain' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Remove-PveDomain' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Realm should be Mandatory' { + Skip-IfMissing 'Remove-PveDomain' + $isMandatory = $script:Cmd.Parameters['Realm'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Remove-PveDomain' + { Remove-PveDomain -Realm 'testrealm' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PvePassword +# --------------------------------------------------------------------------- +Describe 'Set-PvePassword' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PvePassword' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PvePassword' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PvePassword' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PvePassword' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'UserId should be Mandatory' { + Skip-IfMissing 'Set-PvePassword' + $isMandatory = $script:Cmd.Parameters['UserId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Password should be Mandatory' { + Skip-IfMissing 'Set-PvePassword' + $isMandatory = $script:Cmd.Parameters['Password'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PvePassword' + $secPw = ConvertTo-SecureString 'dummypassword' -AsPlainText -Force + { Set-PvePassword -UserId 'root@pam' -Password $secPw -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Vms/GuestAgentExtCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/GuestAgentExtCmdlets.Tests.ps1 new file mode 100644 index 0000000..f394654 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Vms/GuestAgentExtCmdlets.Tests.ps1 @@ -0,0 +1,331 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for extended guest agent cmdlets: + Get-PveVmGuestOsInfo, Get-PveVmGuestFsInfo, Read-PveVmGuestFile, + Write-PveVmGuestFile, Set-PveVmGuestPassword, Invoke-PveVmGuestFsTrim. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @( + 'Get-PveVmGuestOsInfo', 'Get-PveVmGuestFsInfo', 'Read-PveVmGuestFile', + 'Write-PveVmGuestFile', 'Set-PveVmGuestPassword', 'Invoke-PveVmGuestFsTrim' + )) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveVmGuestOsInfo +# --------------------------------------------------------------------------- +Describe 'Get-PveVmGuestOsInfo' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveVmGuestOsInfo' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveVmGuestOsInfo' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveVmGuestOsInfo' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestOsInfo' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestOsInfo' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveVmGuestOsInfo' + { Get-PveVmGuestOsInfo -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveVmGuestFsInfo +# --------------------------------------------------------------------------- +Describe 'Get-PveVmGuestFsInfo' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveVmGuestFsInfo' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveVmGuestFsInfo' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveVmGuestFsInfo' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestFsInfo' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestFsInfo' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveVmGuestFsInfo' + { Get-PveVmGuestFsInfo -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Read-PveVmGuestFile +# --------------------------------------------------------------------------- +Describe 'Read-PveVmGuestFile' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Read-PveVmGuestFile' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Read-PveVmGuestFile' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Read-PveVmGuestFile' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Read-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Read-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'File should be Mandatory' { + Skip-IfMissing 'Read-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['File'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Read-PveVmGuestFile' + { Read-PveVmGuestFile -Node 'pve' -VmId 100 -File '/etc/hostname' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Write-PveVmGuestFile +# --------------------------------------------------------------------------- +Describe 'Write-PveVmGuestFile' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Write-PveVmGuestFile' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Write-PveVmGuestFile' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Write-PveVmGuestFile' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Write-PveVmGuestFile' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Write-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Write-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'File should be Mandatory' { + Skip-IfMissing 'Write-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['File'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Content should be Mandatory' { + Skip-IfMissing 'Write-PveVmGuestFile' + $isMandatory = $script:Cmd.Parameters['Content'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Write-PveVmGuestFile' + { Write-PveVmGuestFile -Node 'pve' -VmId 100 -File '/tmp/test.txt' -Content 'hello' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Set-PveVmGuestPassword +# --------------------------------------------------------------------------- +Describe 'Set-PveVmGuestPassword' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Set-PveVmGuestPassword' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Username should be Mandatory' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $isMandatory = $script:Cmd.Parameters['Username'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Password should be Mandatory' { + Skip-IfMissing 'Set-PveVmGuestPassword' + $isMandatory = $script:Cmd.Parameters['Password'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Set-PveVmGuestPassword' + { Set-PveVmGuestPassword -Node 'pve' -VmId 100 -Username 'root' -Password 'dummy' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Invoke-PveVmGuestFsTrim +# --------------------------------------------------------------------------- +Describe 'Invoke-PveVmGuestFsTrim' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Invoke-PveVmGuestFsTrim' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Invoke-PveVmGuestFsTrim' + { Invoke-PveVmGuestFsTrim -Node 'pve' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Vms/VmDiskOpsCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/VmDiskOpsCmdlets.Tests.ps1 new file mode 100644 index 0000000..f7c7fc3 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Vms/VmDiskOpsCmdlets.Tests.ps1 @@ -0,0 +1,143 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for VM disk operation cmdlets: + Move-PveVmDisk, Remove-PveVmDisk. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Move-PveVmDisk', 'Remove-PveVmDisk')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Move-PveVmDisk +# --------------------------------------------------------------------------- +Describe 'Move-PveVmDisk' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Move-PveVmDisk' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Move-PveVmDisk' + $script:Cmd | Should -Not -BeNullOrEmpty + } + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Move-PveVmDisk' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Move-PveVmDisk' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Move-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Move-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Disk should be Mandatory' { + Skip-IfMissing 'Move-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['Disk'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'Storage should be Mandatory' { + Skip-IfMissing 'Move-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Move-PveVmDisk' + { Move-PveVmDisk -Node 'pve' -VmId 100 -Disk 'scsi0' -Storage 'local-lvm' -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Remove-PveVmDisk +# --------------------------------------------------------------------------- +Describe 'Remove-PveVmDisk' -Tag 'Unit' { + + BeforeAll { $script:Cmd = Get-Command 'Remove-PveVmDisk' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Remove-PveVmDisk' + $script:Cmd | Should -Not -BeNullOrEmpty + } + } + + Context 'ShouldProcess / ConfirmImpact' { + It 'Should support WhatIf' { + Skip-IfMissing 'Remove-PveVmDisk' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + It 'Should declare ConfirmImpact High' { + Skip-IfMissing 'Remove-PveVmDisk' + $attr = $script:Cmd.ImplementingType.GetCustomAttributes( + [System.Management.Automation.CmdletAttribute], $false) | + Select-Object -First 1 + $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High) + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Remove-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'VmId should be Mandatory' { + Skip-IfMissing 'Remove-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + It 'IdList should be Mandatory' { + Skip-IfMissing 'Remove-PveVmDisk' + $isMandatory = $script:Cmd.Parameters['IdList'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Remove-PveVmDisk' + { Remove-PveVmDisk -Node 'pve' -VmId 100 -IdList 'scsi0' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +}