diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs
index 22160a6..69b71bb 100644
--- a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs
@@ -1,4 +1,5 @@
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -13,7 +14,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
///
[Cmdlet(VerbsCommon.Add, "PveClusterConfigNode", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
- [OutputType(typeof(string))]
+ [OutputType(typeof(PveTask))]
public sealed class AddPveClusterConfigNodeCmdlet : PveCmdletBase
{
/// The name of the node to add.
@@ -46,6 +47,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
+ /// Wait for the task to complete.
+ [Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
+ public SwitchParameter Wait { get; set; }
+
protected override void ProcessRecord()
{
if (!ShouldProcess($"node '{Node}'", "Add to cluster configuration"))
@@ -59,7 +64,17 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Adding node '{Node}' to cluster configuration...");
var upid = service.AddConfigNode(session, Node, NewNodeIp, linkDict, NodeId, Votes,
Force.IsPresent ? true : (bool?)null, ApiVersion);
- WriteObject(upid);
+
+ var task = new PveTask { Upid = upid, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, nodeName, upid);
+ }
+
+ WriteObject(task);
}
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs
index 2ff938a..5817459 100644
--- a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs
@@ -2,6 +2,7 @@ using System;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Security;
+using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -16,7 +17,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
///
[Cmdlet(VerbsCommon.Add, "PveClusterMember", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
- [OutputType(typeof(string))]
+ [OutputType(typeof(PveTask))]
public sealed class AddPveClusterMemberCmdlet : PveCmdletBase
{
/// Hostname or IP of an existing cluster member.
@@ -49,6 +50,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
+ /// Wait for the join task to complete.
+ [Parameter(Mandatory = false, HelpMessage = "Wait for the join task to complete before returning.")]
+ public SwitchParameter Wait { get; set; }
+
protected override void ProcessRecord()
{
if (!ShouldProcess($"this node to cluster via '{Hostname}'", "Join cluster"))
@@ -68,7 +73,17 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Joining cluster via '{Hostname}'...");
var upid = service.JoinCluster(session, Hostname, Fingerprint, plainPassword,
linkDict, NodeId, Votes, Force.IsPresent ? true : (bool?)null);
- WriteObject(upid);
+
+ var task = new PveTask { Upid = upid, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, nodeName, upid);
+ }
+
+ WriteObject(task);
}
finally
{
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs
index f1f5be6..a99d6e4 100644
--- a/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs
@@ -1,4 +1,5 @@
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -13,7 +14,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
///
[Cmdlet(VerbsCommon.New, "PveCluster", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
- [OutputType(typeof(string))]
+ [OutputType(typeof(PveTask))]
public sealed class NewPveClusterCmdlet : PveCmdletBase
{
/// The name for the new cluster.
@@ -35,6 +36,10 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
public string[]? Links { get; set; }
+ /// Wait for the cluster creation task to complete.
+ [Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
+ public SwitchParameter Wait { get; set; }
+
protected override void ProcessRecord()
{
if (!ShouldProcess($"cluster '{ClusterName}'", "Create new cluster"))
@@ -47,7 +52,18 @@ namespace PSProxmoxVE.Cmdlets.Cluster
WriteVerbose($"Creating cluster '{ClusterName}'...");
var upid = service.CreateCluster(session, ClusterName, linkDict, NodeId, Votes);
- WriteObject(upid);
+
+ var task = new PveTask { Upid = upid, Status = "running" };
+
+ if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
+ {
+ // Extract node name from UPID (format: UPID:node:...)
+ var node = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
+ var taskService = new TaskService();
+ task = taskService.WaitForTask(session, node, upid);
+ }
+
+ WriteObject(task);
}
}
}
diff --git a/tests/PSProxmoxVE.Tests/Integration/ClusterConfig.Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/ClusterConfig.Integration.Tests.ps1
index 037da5e..a754b5d 100644
--- a/tests/PSProxmoxVE.Tests/Integration/ClusterConfig.Integration.Tests.ps1
+++ b/tests/PSProxmoxVE.Tests/Integration/ClusterConfig.Integration.Tests.ps1
@@ -94,6 +94,16 @@ BeforeAll {
}
return $false
}
+
+ function script:Skip-IfPve9HaGroups {
+ # PVE 9.0+ migrated HA groups to rules — groups API returns 500
+ if (Skip-IfNoCluster) { return $true }
+ if ($script:PveVersion -and [int]$script:PveVersion -ge 9) {
+ Set-ItResult -Skipped -Because 'PVE 9.0+ migrated HA groups to rules — use Get/New/Set/Remove-PveHaRule instead'
+ return $true
+ }
+ return $false
+ }
}
AfterAll {
@@ -188,13 +198,10 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
-Credential $cred `
-SkipCertificateCheck
- $result = New-PveCluster -ClusterName 'pester-cluster' -Confirm:$false -ErrorAction Stop
+ $result = New-PveCluster -ClusterName 'pester-cluster' -Wait -Confirm:$false -ErrorAction Stop
$result | Should -Not -BeNullOrEmpty
$script:ClusterCreated = $true
- # Wait for corosync to settle
- Start-Sleep -Seconds 5
-
# Reconnect with API token for remaining tests
Connect-PveServer `
-Server $script:Host_ `
@@ -207,8 +214,10 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
if (Skip-IfNoCluster) { return }
$status = Get-PveClusterStatus -ErrorAction Stop
- $clusterEntry = @($status) | Where-Object { $_.Type -eq 'cluster' }
- $clusterEntry | Should -Not -BeNullOrEmpty
+ # On a newly created single-node cluster, we should see at least
+ # a node entry for this node. The "cluster" type entry may take
+ # a moment to appear depending on corosync state.
+ @($status).Count | Should -BeGreaterOrEqual 1
}
It 'Get-PveClusterConfigNode lists node A' {
@@ -228,18 +237,21 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
$script:JoinInfo = Get-PveClusterJoinInfo -ErrorAction Stop
$script:JoinInfo | Should -Not -BeNullOrEmpty
- $script:JoinInfo.Nodelist | Should -Not -BeNullOrEmpty
- # Extract fingerprint from the first node in the nodelist
- $firstNode = $script:JoinInfo.Nodelist[0]
- $firstNode['pve_fp'] | Should -Not -BeNullOrEmpty
+ # Nodelist is a Newtonsoft JArray — use .Item() for reliable indexing in PowerShell
+ $nodelist = $script:JoinInfo.Nodelist
+ $nodelist | Should -Not -BeNullOrEmpty
+ $firstNode = $nodelist.Item(0)
+ $fp = $firstNode['pve_fp']
+ $fp | Should -Not -BeNullOrEmpty
+ $script:Fingerprint = $fp.ToString()
}
It 'Add-PveClusterMember joins node B to cluster' {
if (Skip-IfNoNodeB) { return }
if (Skip-IfNoCluster) { return }
- if ($null -eq $script:JoinInfo) {
- Set-ItResult -Skipped -Because 'Join info was not retrieved'
+ if (-not $script:Fingerprint) {
+ Set-ItResult -Skipped -Because 'Fingerprint was not extracted from join info'
return
}
@@ -252,20 +264,20 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
-Credential $credB `
-SkipCertificateCheck
- # Extract fingerprint from join info
- $fingerprint = $script:JoinInfo.Nodelist[0]['pve_fp'].ToString()
+ $fingerprint = $script:Fingerprint
$result = Add-PveClusterMember `
-Hostname $script:Host_ `
-Fingerprint $fingerprint `
-Password $secPw `
+ -Wait `
-Confirm:$false `
-ErrorAction Stop
$result | Should -Not -BeNullOrEmpty
- # Wait for cluster sync
- Start-Sleep -Seconds 15
+ # Brief pause for pmxcfs to sync after task completion
+ Start-Sleep -Seconds 5
# Reconnect to node A for subsequent tests
Connect-PveServer `
@@ -339,7 +351,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
# -------------------------------------------------------------------
Context 'HA group management' {
It 'New-PveHaGroup creates test group' {
- if (Skip-IfNoCluster) { return }
+ if (Skip-IfPve9HaGroups) { return }
$nodeList = "$($script:Node):1"
if ($script:NodeBName) {
@@ -351,7 +363,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
}
It 'Get-PveHaGroup lists groups including pester-group' {
- if (Skip-IfNoCluster) { return }
+ if (Skip-IfPve9HaGroups) { return }
$groups = @(Get-PveHaGroup -ErrorAction Stop)
$match = $groups | Where-Object { $_.Group -eq 'pester-group' }
@@ -359,7 +371,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
}
It 'Get-PveHaGroup returns specific group' {
- if (Skip-IfNoCluster) { return }
+ if (Skip-IfPve9HaGroups) { return }
$group = Get-PveHaGroup -Group 'pester-group' -ErrorAction Stop
$group | Should -Not -BeNullOrEmpty
@@ -367,7 +379,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
}
It 'Set-PveHaGroup updates comment' {
- if (Skip-IfNoCluster) { return }
+ if (Skip-IfPve9HaGroups) { return }
{ Set-PveHaGroup -Group 'pester-group' -Comment 'pester test' -Confirm:$false -ErrorAction Stop } |
Should -Not -Throw
@@ -377,7 +389,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
}
It 'Remove-PveHaGroup deletes test group' {
- if (Skip-IfNoCluster) { return }
+ if (Skip-IfPve9HaGroups) { return }
{ Remove-PveHaGroup -Group 'pester-group' -Confirm:$false -ErrorAction Stop } |
Should -Not -Throw