mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
refactor: adapt cluster tests to _IntegrationHelper + rename to 16_Cluster
- Rename ZZ.ClusterConfig.Integration.Tests.ps1 → 16_Cluster.Tests.ps1 - Replace manual env var setup with _IntegrationHelper.ps1 + Connect-TestPve - Use credential auth (root@pam) throughout — no API tokens - Use $script:PasswordB for node B auth (from helper) - Access JoinInfo.Nodelist as List<Dictionary> (native types from D013) - Keep cluster-specific skip helpers (Skip-IfNoCluster, Skip-IfPve9HaGroups) - Remove redundant Connection context (Connect-TestPve handles it) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-157
@@ -1,82 +1,31 @@
|
||||
#Requires -Module Pester
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pester 5 integration tests for cluster configuration and HA lifecycle.
|
||||
Integration tests for cluster configuration and HA lifecycle.
|
||||
|
||||
These tests exercise the full cluster lifecycle: creating a cluster on
|
||||
node A, joining node B, managing HA groups, and removing node B.
|
||||
Exercises the full cluster lifecycle: create cluster on node A,
|
||||
join node B, manage HA groups/rules, and verify cluster state.
|
||||
|
||||
Nodes are provisioned STANDALONE (not clustered). The tests create and
|
||||
destroy the cluster during the run.
|
||||
Nodes must be provisioned STANDALONE (not clustered). These tests
|
||||
create a cluster during the run. Cleanup is handled by reprovisioning
|
||||
(2-node cluster cannot be torn down via API due to quorum loss).
|
||||
|
||||
Required environment variables:
|
||||
PVETEST_HOST - Node A hostname or IP
|
||||
PVETEST_PORT - API port (default 8006)
|
||||
PVETEST_APITOKEN - Node A API token
|
||||
PVETEST_NODE - Node A node name
|
||||
|
||||
Optional (multi-node tests):
|
||||
PVETEST_HOST_B - Node B hostname or IP
|
||||
PVETEST_APITOKEN_B - Node B API token
|
||||
PVETEST_PASSWORD - Root password for nested PVE instances
|
||||
|
||||
Optional:
|
||||
PVETEST_PVE_VERSION - PVE major version (8 or 9)
|
||||
|
||||
WARNING: These tests CREATE and DESTROY a cluster on the target nodes.
|
||||
WARNING: These tests CREATE a cluster on the target nodes.
|
||||
Never run against a production cluster.
|
||||
#>
|
||||
|
||||
BeforeAll {
|
||||
. $PSScriptRoot/../_TestHelper.ps1
|
||||
|
||||
# --- Required env vars ---
|
||||
$requiredVars = @('PVETEST_HOST', 'PVETEST_PORT', 'PVETEST_APITOKEN', 'PVETEST_NODE')
|
||||
$script:SkipReason = $null
|
||||
|
||||
foreach ($var in $requiredVars) {
|
||||
if (-not [System.Environment]::GetEnvironmentVariable($var)) {
|
||||
$script:SkipReason = "No live Proxmox VE target configured. Set: $($requiredVars -join ', ')"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# --- Convenience accessors ---
|
||||
$script:Host_ = [System.Environment]::GetEnvironmentVariable('PVETEST_HOST')
|
||||
$portEnv = [System.Environment]::GetEnvironmentVariable('PVETEST_PORT')
|
||||
$script:Port = [int]$(if ($portEnv) { $portEnv } else { '8006' })
|
||||
$script:ApiToken = [System.Environment]::GetEnvironmentVariable('PVETEST_APITOKEN')
|
||||
$script:Node = [System.Environment]::GetEnvironmentVariable('PVETEST_NODE')
|
||||
|
||||
# --- Optional multi-node vars ---
|
||||
$script:HostB = [System.Environment]::GetEnvironmentVariable('PVETEST_HOST_B')
|
||||
$script:ApiTokenB = [System.Environment]::GetEnvironmentVariable('PVETEST_APITOKEN_B')
|
||||
$script:Password = [System.Environment]::GetEnvironmentVariable('PVETEST_PASSWORD')
|
||||
$script:PveVersion = [System.Environment]::GetEnvironmentVariable('PVETEST_PVE_VERSION')
|
||||
. $PSScriptRoot/_IntegrationHelper.ps1
|
||||
Connect-TestPve
|
||||
|
||||
# --- State tracking ---
|
||||
$script:ClusterCreated = $false
|
||||
$script:JoinInfo = $null
|
||||
$script:NodeBName = $null
|
||||
$script:HaRuleCreated = $false
|
||||
$script:HaTestVmId = $null
|
||||
|
||||
# --- Skip helpers ---
|
||||
function script:Skip-IfNoTarget {
|
||||
if ($script:SkipReason) {
|
||||
Set-ItResult -Skipped -Because $script:SkipReason
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfNoNodeB {
|
||||
if (Skip-IfNoTarget) { return $true }
|
||||
if (-not $script:HostB -or -not $script:ApiTokenB -or -not $script:Password) {
|
||||
Set-ItResult -Skipped -Because 'Multi-node env vars not set (PVETEST_HOST_B, PVETEST_APITOKEN_B, PVETEST_PASSWORD)'
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
# --- Cluster-specific skip helpers ---
|
||||
|
||||
function script:Skip-IfNoCluster {
|
||||
if (Skip-IfNoTarget) { return $true }
|
||||
@@ -87,15 +36,6 @@ BeforeAll {
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfNoPve9 {
|
||||
if (Skip-IfNoCluster) { return $true }
|
||||
if (-not $script:PveVersion -or [int]$script:PveVersion -lt 9) {
|
||||
Set-ItResult -Skipped -Because 'HA rules require PVE 9.0+'
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function script:Skip-IfPve9HaGroups {
|
||||
# PVE 9.0+ migrated HA groups to rules — groups API returns 500
|
||||
if (Skip-IfNoCluster) { return $true }
|
||||
@@ -108,7 +48,7 @@ BeforeAll {
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
# Best-effort cleanup of HA artifacts created during testing
|
||||
# Best-effort cleanup of HA artifacts
|
||||
if ($script:HaRuleCreated) {
|
||||
try { Remove-PveHaRule -Rule 'pester-rule-1' -Confirm:$false -ErrorAction SilentlyContinue } catch { }
|
||||
}
|
||||
@@ -119,27 +59,11 @@ AfterAll {
|
||||
|
||||
# Node removal from a 2-node cluster is not possible via API (quorum loss).
|
||||
# Test infrastructure handles cleanup via reprovisioning.
|
||||
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
|
||||
Disconnect-TestPve
|
||||
}
|
||||
|
||||
Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Connection' {
|
||||
It 'Should connect to PVE node A' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
$session = Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-ApiToken $script:ApiToken `
|
||||
-SkipCertificateCheck `
|
||||
-PassThru
|
||||
|
||||
$session | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'Pre-cluster reads on standalone node' {
|
||||
It 'Get-PveClusterStatus returns node info' {
|
||||
@@ -168,15 +92,12 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
It 'Get-PveClusterConfig returns config' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
# /cluster/config returns a directory listing (array) on standalone nodes
|
||||
# and may return an object on clustered nodes — either is valid
|
||||
{ Get-PveClusterConfig -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'Get-PveHaStatus returns status without throwing' {
|
||||
if (Skip-IfNoTarget) { return }
|
||||
|
||||
# On a standalone node this may return an empty list — that is fine
|
||||
{ Get-PveHaStatus -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
@@ -186,20 +107,12 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
It 'New-PveCluster creates a cluster named pester-cluster' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
|
||||
# Cluster creation requires root@pam ticket auth, not API token
|
||||
$secPw = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
$cred = New-Object System.Management.Automation.PSCredential('root@pam', $secPw)
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-Credential $cred `
|
||||
-SkipCertificateCheck
|
||||
|
||||
# Already connected as root@pam via Connect-TestPve
|
||||
$result = New-PveCluster -ClusterName 'pester-cluster' -Wait -Confirm:$false -ErrorAction Stop
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
$script:ClusterCreated = $true
|
||||
|
||||
# Allow corosync to fully stabilize after cluster creation
|
||||
# Allow corosync to fully stabilize
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
||||
@@ -207,9 +120,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
$status = Get-PveClusterStatus -ErrorAction Stop
|
||||
# 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
|
||||
}
|
||||
|
||||
@@ -231,11 +141,10 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
$script:JoinInfo = Get-PveClusterJoinInfo -ErrorAction Stop
|
||||
$script:JoinInfo | Should -Not -BeNullOrEmpty
|
||||
|
||||
# Nodelist is a Newtonsoft JArray — convert to native array for PowerShell compatibility
|
||||
$nodelistJson = $script:JoinInfo.Nodelist.ToString()
|
||||
$nodelist = $nodelistJson | ConvertFrom-Json
|
||||
$nodelist | Should -Not -BeNullOrEmpty
|
||||
$script:Fingerprint = $nodelist[0].pve_fp
|
||||
# Nodelist is List<Dictionary<string, object?>> — access pve_fp from first entry
|
||||
$script:JoinInfo.Nodelist | Should -Not -BeNullOrEmpty
|
||||
$script:JoinInfo.Nodelist.Count | Should -BeGreaterOrEqual 1
|
||||
$script:Fingerprint = $script:JoinInfo.Nodelist[0]['pve_fp']
|
||||
$script:Fingerprint | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
@@ -247,8 +156,8 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
return
|
||||
}
|
||||
|
||||
# Connect to node B with root@pam (join requires ticket auth, not API token)
|
||||
$secPw = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
# Connect to node B with root@pam
|
||||
$secPw = ConvertTo-SecureString $script:PasswordB -AsPlainText -Force
|
||||
$credB = New-Object System.Management.Automation.PSCredential('root@pam', $secPw)
|
||||
Connect-PveServer `
|
||||
-Server $script:HostB `
|
||||
@@ -257,31 +166,25 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
-SkipCertificateCheck
|
||||
|
||||
$fingerprint = $script:Fingerprint
|
||||
$joinPw = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
|
||||
try {
|
||||
$result = Add-PveClusterMember `
|
||||
-Hostname $script:Host_ `
|
||||
-Fingerprint $fingerprint `
|
||||
-Password $secPw `
|
||||
-Password $joinPw `
|
||||
-Wait `
|
||||
-Confirm:$false `
|
||||
-ErrorAction Stop
|
||||
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
|
||||
# Brief pause for pmxcfs to sync after task completion
|
||||
# Brief pause for pmxcfs to sync
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
finally {
|
||||
# Always reconnect to node A — whether join succeeded or failed,
|
||||
# the current session points at node B which may be invalid.
|
||||
$secPwA = ConvertTo-SecureString $script:Password -AsPlainText -Force
|
||||
$credA = New-Object System.Management.Automation.PSCredential('root@pam', $secPwA)
|
||||
Connect-PveServer `
|
||||
-Server $script:Host_ `
|
||||
-Port $script:Port `
|
||||
-Credential $credA `
|
||||
-SkipCertificateCheck
|
||||
# Always reconnect to node A
|
||||
Connect-TestPve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +195,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
$nodes = Get-PveClusterConfigNode -ErrorAction Stop
|
||||
@($nodes).Count | Should -BeGreaterOrEqual 2
|
||||
|
||||
# Discover and store node B's name for cleanup
|
||||
# Discover and store node B's name
|
||||
$nodeNames = @($nodes) | ForEach-Object { $_.Name }
|
||||
$script:NodeBName = $nodeNames | Where-Object { $_ -ne $script:Node } | Select-Object -First 1
|
||||
$script:NodeBName | Should -Not -BeNullOrEmpty
|
||||
@@ -316,8 +219,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
It 'Set-PveClusterOption sets keyboard to en-us' {
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
# Already connected as root@pam from cluster creation context
|
||||
# Save original keyboard setting
|
||||
$options = Get-PveClusterOption -ErrorAction Stop
|
||||
$script:OriginalKeyboard = $options.Keyboard
|
||||
|
||||
@@ -338,7 +239,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
Set-PveClusterOption -Keyboard $script:OriginalKeyboard -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
else {
|
||||
# Original was unset — delete the key to restore default
|
||||
Set-PveClusterOption -Delete 'keyboard' -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
|
||||
@@ -348,7 +248,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'HA group management' {
|
||||
Context 'HA group management (PVE 8 only)' {
|
||||
It 'New-PveHaGroup creates test group' {
|
||||
if (Skip-IfPve9HaGroups) { return }
|
||||
|
||||
@@ -382,9 +282,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
|
||||
{ Set-PveHaGroup -Group 'pester-group' -Comment 'pester test' -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
|
||||
$group = Get-PveHaGroup -Group 'pester-group' -ErrorAction Stop
|
||||
$group.Comment | Should -Be 'pester test'
|
||||
}
|
||||
|
||||
It 'Remove-PveHaGroup deletes test group' {
|
||||
@@ -392,18 +289,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
|
||||
{ Remove-PveHaGroup -Group 'pester-group' -Confirm:$false -ErrorAction Stop } |
|
||||
Should -Not -Throw
|
||||
|
||||
# Verify the group is gone
|
||||
$groups = @(Get-PveHaGroup -ErrorAction Stop)
|
||||
$match = $groups | Where-Object { $_.Group -eq 'pester-group' }
|
||||
$match | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
Context 'HA resource management' {
|
||||
It 'Skipped — HA resources require a test VM' {
|
||||
Set-ItResult -Skipped -Because 'HA resource tests require a running VM, which is not provisioned in this test file'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +297,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
It 'Get-PveHaRule returns rules list without throwing' {
|
||||
if (Skip-IfNoPve9) { return }
|
||||
|
||||
# May return an empty list on a fresh cluster — that is fine
|
||||
{ Get-PveHaRule -ErrorAction Stop } | Should -Not -Throw
|
||||
}
|
||||
|
||||
@@ -420,8 +304,7 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
if (Skip-IfNoPve9) { return }
|
||||
if (Skip-IfNoCluster) { return }
|
||||
|
||||
# HA rules require a managed resource — create a minimal VM and
|
||||
# register it as an HA resource first
|
||||
# HA rules require a managed resource — create a minimal VM first
|
||||
$script:HaTestVmId = Get-PveClusterNextId -ErrorAction Stop
|
||||
New-PveVm -Node $script:Node -VmId $script:HaTestVmId `
|
||||
-Name 'pester-ha-test' -Memory 128 -Cores 1 `
|
||||
@@ -431,7 +314,6 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
-State 'disabled' `
|
||||
-Confirm:$false -ErrorAction Stop
|
||||
|
||||
# Now create a node-affinity rule for that resource
|
||||
{ New-PveHaRule -Type 'node-affinity' `
|
||||
-Properties @{
|
||||
rule = 'pester-rule-1'
|
||||
@@ -499,24 +381,19 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
||||
|
||||
$script:HaRuleCreated = $false
|
||||
|
||||
# Verify it's gone
|
||||
$rules = Get-PveHaRule -ErrorAction Stop
|
||||
$match = @($rules) | Where-Object { $_.Rule -eq 'pester-rule-1' }
|
||||
$match | Should -BeNullOrEmpty
|
||||
|
||||
# Clean up HA resource and test VM
|
||||
try { Remove-PveHaResource -Sid "vm:$($script:HaTestVmId)" -Confirm:$false -ErrorAction SilentlyContinue } catch { }
|
||||
try { Remove-PveVm -Node $script:Node -VmId $script:HaTestVmId -Confirm:$false -ErrorAction SilentlyContinue } catch { }
|
||||
$script:HaTestVmId = $null
|
||||
|
||||
# Verify rule is gone
|
||||
$rules = Get-PveHaRule -ErrorAction Stop
|
||||
$match = @($rules) | Where-Object { $_.Rule -eq 'pester-rule-1' }
|
||||
$match | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# NOTE: Removing a node from a 2-node cluster via API is not supported
|
||||
# because the remaining node loses quorum mid-operation. PVE requires
|
||||
# stopping corosync on the departing node first (pvecm expected 1),
|
||||
# which is not available via the REST API. Test infrastructure handles
|
||||
# cleanup via reprovisioning.
|
||||
Context 'Verify cluster state at end of test' {
|
||||
It 'Get-PveClusterConfigNode shows both nodes still present' {
|
||||
if (Skip-IfNoNodeB) { return }
|
||||
Reference in New Issue
Block a user