Skip to content

Commit 63fd447

Browse files
Get-DbaWsfc* - Fix the State property read from an undefined variable, and report the witness path (#10588)
1 parent 117e261 commit 63fd447

13 files changed

Lines changed: 261 additions & 29 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function Get-ResourceGroupState ($state) {
2+
switch ($state) {
3+
-1 { "Unknown" }
4+
0 { "Online" }
5+
1 { "Offline" }
6+
2 { "Failed" }
7+
default { $state }
8+
}
9+
}

private/testing/Get-TestConfig.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ function Get-TestConfig {
1717
# When testing a remote SQL Server instance this must be a network share
1818
# where both the SQL Server instance and the test script can write to.
1919
Temp = 'C:\Temp'
20+
# The Windows failover clusters that the Get-DbaWsfc* tests need. No CI environment has a
21+
# cluster, so these stay empty there and the cluster tests skip themselves. A local
22+
# configuration whose lab has clusters sets them to the cluster names.
23+
# ClusterStorage needs shared storage: one cluster shared volume, and one disk that every
24+
# node can see but that is deliberately not part of the cluster.
25+
ClusterStorage = $null
26+
# ClusterWitness needs a file share witness.
27+
ClusterWitness = $null
2028
}
2129

2230
if (Test-Path $LocalConfigPath) {

public/Get-DbaWsfcAvailableDisk.ps1

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ function Get-DbaWsfcAvailableDisk {
3737
All properties from the MSCluster_AvailableDisk WMI class are returned, including:
3838
3939
Properties added via Add-Member:
40-
- State: Current operational state of the disk
4140
- ClusterName: Name of the cluster
4241
- ClusterFqdn: Fully qualified domain name of the cluster
4342
@@ -80,7 +79,6 @@ function Get-DbaWsfcAvailableDisk {
8079
$disk = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName MSCluster_AvailableDisk
8180

8281
# I don't have an available disk, so I can't see how to clean this up: Passthru
83-
$disk | Add-Member -Force -NotePropertyName State -NotePropertyValue (Get-ResourceState $resource.State)
8482
$disk | Add-Member -Force -NotePropertyName ClusterName -NotePropertyValue $cluster.Name
8583
$disk | Add-Member -Force -NotePropertyName ClusterFqdn -NotePropertyValue $cluster.Fqdn -PassThru
8684
}

public/Get-DbaWsfcCluster.ps1

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ function Get-DbaWsfcCluster {
3939
Default display properties (via Select-DefaultView):
4040
- Name: The name of the cluster
4141
- Fqdn: Fully qualified domain name of the cluster
42-
- State: Current operational state of the cluster (added via NoteProperty)
4342
- DrainOnShutdown: Boolean indicating if nodes drain resources during service shutdown (uint32)
4443
- DynamicQuorumEnabled: Boolean indicating if dynamic quorum adjustment is enabled (uint32)
4544
- EnableSharedVolumes: Boolean indicating if Cluster Shared Volumes feature is enabled (uint32)
4645
- SharedVolumesRoot: The root directory path for Cluster Shared Volumes
4746
- QuorumPath: File system path where quorum files are maintained
4847
- QuorumType: Current quorum type as a string (Majority Node Majority, Node and Disk Majority, No Majority - Disk Only, Node Majority, or Witness)
4948
- QuorumTypeValue: Numeric identifier representing the quorum type (uint32)
49+
- WitnessPath: Location of the witness, whichever kind it is - the UNC path of the share for a file share witness, the value of QuorumPath for a disk witness, and empty for a cluster with no witness (added via NoteProperty)
5050
- RequestReplyTimeout: Timeout period in milliseconds for request-reply operations (uint32)
5151
5252
Additional properties from MSCluster_Cluster WMI class (accessible via Select-Object *):
@@ -108,8 +108,24 @@ function Get-DbaWsfcCluster {
108108
process {
109109
foreach ($computer in $computername) {
110110
$cluster = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName MSCluster_Cluster
111-
$cluster | Add-Member -Force -NotePropertyName State -NotePropertyValue (Get-ResourceState $resource.State)
112-
$cluster | Select-DefaultView -Property Name, Fqdn, State, DrainOnShutdown, DynamicQuorumEnabled, EnableSharedVolumes, SharedVolumesRoot, QuorumPath, QuorumType, QuorumTypeValue, RequestReplyTimeout
111+
112+
# One property for "where is the witness", whichever kind of witness the cluster uses.
113+
# QuorumPath already holds it for a disk witness and is empty for every other quorum type.
114+
$witnessPath = $cluster.QuorumPath
115+
116+
# A file share witness keeps its path in the private properties of its own resource and
117+
# nowhere else - MSCluster_Cluster does not expose it at all. Ask for that resource only
118+
# when the quorum type says there is one, so no other cluster pays for the extra query.
119+
if ($cluster.QuorumTypeValue -eq 2) {
120+
$witnessQuery = "SELECT * FROM MSCluster_Resource WHERE Type = `"File Share Witness`""
121+
$witnessResource = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -Query $witnessQuery
122+
if ($witnessResource.PrivateProperties.SharePath) {
123+
$witnessPath = $witnessResource.PrivateProperties.SharePath
124+
}
125+
}
126+
127+
$cluster | Add-Member -Force -NotePropertyName WitnessPath -NotePropertyValue $witnessPath
128+
$cluster | Select-DefaultView -Property Name, Fqdn, DrainOnShutdown, DynamicQuorumEnabled, EnableSharedVolumes, SharedVolumesRoot, QuorumPath, QuorumType, QuorumTypeValue, WitnessPath, RequestReplyTimeout
113129
}
114130
}
115131
}

public/Get-DbaWsfcResourceGroup.ps1

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,6 @@ function Get-DbaWsfcResourceGroup {
8181
[string[]]$Name,
8282
[switch]$EnableException
8383
)
84-
begin {
85-
function Get-ResourceGroupState ($state) {
86-
switch ($state) {
87-
-1 { "Unknown" }
88-
0 { "Online" }
89-
1 { "Offline" }
90-
2 { "Failed" }
91-
default { $state }
92-
}
93-
}
94-
}
9584
process {
9685
foreach ($computer in $computername) {
9786
$cluster = Get-DbaWsfcCluster -ComputerName $computer -Credential $Credential

public/Get-DbaWsfcRole.ps1

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function Get-DbaWsfcRole {
4040
- ClusterFqdn: Fully qualified domain name of the cluster
4141
- Name: Name of the resource group (key property), typically the cluster role name (e.g., SQL Server instance name)
4242
- OwnerNode: Name of the node currently hosting this resource group
43-
- State: Current state of the resource group translated to readable format (Online, Offline, Failed, Partial Online, Pending, or Unknown)
43+
- State: Current state of the resource group translated to readable format (Online, Offline, Failed, or Unknown)
4444
4545
Additional properties available (from WMI MSCluster_ResourceGroup object):
4646
- Caption: Short textual description of the resource group
@@ -89,11 +89,13 @@ function Get-DbaWsfcRole {
8989
process {
9090
foreach ($computer in $computername) {
9191
$cluster = Get-DbaWsfcCluster -ComputerName $computer -Credential $Credential
92-
$role = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName MSCluster_ResourceGroup
93-
$role | Add-Member -Force -NotePropertyName State -NotePropertyValue (Get-ResourceState $resource.State)
94-
$role | Add-Member -Force -NotePropertyName ClusterName -NotePropertyValue $cluster.Name
95-
$role | Add-Member -Force -NotePropertyName ClusterFqdn -NotePropertyValue $cluster.Fqdn
96-
$role | Select-DefaultView -Property ClusterName, ClusterFqdn, Name, OwnerNode, State
92+
$roles = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName MSCluster_ResourceGroup
93+
foreach ($role in $roles) {
94+
$role | Add-Member -Force -NotePropertyName State -NotePropertyValue (Get-ResourceGroupState $role.State)
95+
$role | Add-Member -Force -NotePropertyName ClusterName -NotePropertyValue $cluster.Name
96+
$role | Add-Member -Force -NotePropertyName ClusterFqdn -NotePropertyValue $cluster.Fqdn
97+
$role | Select-DefaultView -Property ClusterName, ClusterFqdn, Name, OwnerNode, State
98+
}
9799
}
98100
}
99101
}

public/Get-DbaWsfcSharedVolume.ps1

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ function Get-DbaWsfcSharedVolume {
3333
.OUTPUTS
3434
System.Management.ManagementObject
3535
36-
Returns one ClusterSharedVolume WMI object per shared volume found on the cluster, with three added NoteProperties providing cluster context.
36+
Returns one MSCluster_ClusterSharedVolume WMI object per shared volume found on the cluster, with two added NoteProperties providing cluster context.
3737
38-
Default display properties (ClusterSharedVolume WMI class properties plus):
38+
Default display properties (MSCluster_ClusterSharedVolume WMI class properties plus):
3939
- ClusterName: Name of the Windows Server Failover Cluster
4040
- ClusterFqdn: Fully qualified domain name of the failover cluster
41-
- State: Current state of the cluster shared volume (Online, Offline, Failed, etc.), converted from numeric value
4241
43-
All properties from the underlying ClusterSharedVolume WMI class are accessible using Select-Object *.
42+
The WMI class carries BackupState and FaultState for the volume; there is no single State property.
43+
44+
All properties from the underlying MSCluster_ClusterSharedVolume WMI class are accessible using Select-Object *.
4445
4546
.LINK
4647
https://dbatools.io/Get-DbaWsfcSharedVolume
@@ -60,11 +61,10 @@ function Get-DbaWsfcSharedVolume {
6061
process {
6162
foreach ($computer in $computername) {
6263
$cluster = Get-DbaWsfcCluster -ComputerName $computer -Credential $Credential
63-
$volume = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName ClusterSharedVolume
64+
$volume = Get-DbaCmObject -Computername $computer -Credential $Credential -Namespace root\MSCluster -ClassName MSCluster_ClusterSharedVolume
6465
# I don't have a shared volume, so I can't see how to clean this up: Passthru
6566
$volume | Add-Member -Force -NotePropertyName ClusterName -NotePropertyValue $cluster.Name
66-
$volume | Add-Member -Force -NotePropertyName ClusterFqdn -NotePropertyValue $cluster.Fqdn
67-
$volume | Add-Member -Force -NotePropertyName State -NotePropertyValue (Get-ResourceState $resource.State) -PassThru
67+
$volume | Add-Member -Force -NotePropertyName ClusterFqdn -NotePropertyValue $cluster.Fqdn -PassThru
6868
}
6969
}
7070
}

tests/CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,16 @@ A boundary is an external system such as SQL Server, S3, Azure Storage, SMTP, LD
468468
- A skipped placeholder is not coverage. If a required dependency was not provisioned, the behavioral test and runner setup must fail rather than skip. Skipping remains appropriate when the tested server version does not support the targeted feature.
469469
- Regression integration tests are expected when a bug crosses a boundary. Add the smallest focused test that fails for the old behavior and passes for the fix.
470470

471+
### The one boundary CI does not have: Windows failover clusters
472+
473+
`Get-DbaWsfc*` is the documented exception to "must fail rather than skip". A Windows failover cluster cannot be provisioned on any current runner - not on the container workflow, not on the self-hosted Azure runners - and there is deliberately no `CLUSTER` scenario in `pester.groups.ps1`, because scenarios exist to schedule CI lanes and no CI lane can host a cluster.
474+
475+
So these tests read their cluster from `$TestConfig.ClusterStorage` and `$TestConfig.ClusterWitness`, which are `$null` in `Get-TestConfig` and set only by a local configuration whose lab has clusters. The integration `Context` carries `-Skip:(-not $TestConfig.ClusterStorage)`, so it skips in CI and runs for real against a lab.
476+
477+
The lab those names describe needs more than a bare cluster: one cluster shared volume, one shared disk that every node can see but that is deliberately *not* part of the cluster, and a file share witness. Without them `Get-DbaWsfcSharedVolume` and `Get-DbaWsfcAvailableDisk` return nothing at all and cannot fail, which is exactly how a wrong WMI class name survived in `Get-DbaWsfcSharedVolume` from 2018 until August 2026.
478+
479+
Do not copy this exception to any other command family. It applies where the boundary is a Windows feature that no runner can provide, not where provisioning is merely inconvenient.
480+
471481
## TEST MANAGEMENT GUIDELINES
472482

473483
The dbatools test suite must remain manageable in size while ensuring adequate coverage for important functionality.

tests/Get-DbaWsfcAvailableDisk.Tests.ps1

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,37 @@ Describe $CommandName -Tag UnitTests {
1818
Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty
1919
}
2020
}
21+
}
22+
23+
Describe $CommandName -Tag IntegrationTests {
24+
# These tests need a Windows failover cluster that can see a shared disk which is not part of the
25+
# cluster. No CI environment has one, so $TestConfig.ClusterStorage is empty there and these tests
26+
# skip. A lab that has a cluster sets it, and then a regression has to fail these tests.
27+
Context "Retrieving the available disks" -Skip:(-not $TestConfig.ClusterStorage) {
28+
BeforeAll {
29+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
30+
31+
$availableDisks = @(Get-DbaWsfcAvailableDisk -ComputerName $TestConfig.ClusterStorage)
32+
$wsfcCluster = Get-DbaWsfcCluster -ComputerName $TestConfig.ClusterStorage
33+
34+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
35+
}
36+
37+
It "Returns at least one available disk" {
38+
$availableDisks | Should -Not -BeNullOrEmpty
39+
}
40+
41+
It "Adds the cluster name and fqdn to every disk" {
42+
# The -PassThru that emits these objects used to sit on a NoteProperty that has been
43+
# removed, so this also guards against the command returning nothing at all.
44+
($availableDisks | Where-Object ClusterName -ne $wsfcCluster.Name).Name | Should -BeNullOrEmpty
45+
($availableDisks | Where-Object ClusterFqdn -ne $wsfcCluster.Fqdn).Name | Should -BeNullOrEmpty
46+
}
47+
48+
It "Does not add an empty State property" {
49+
# MSCluster_AvailableDisk has no State property, so the command built its State
50+
# NoteProperty from an undefined variable and it was always empty.
51+
$availableDisks[0].PSObject.Properties.Name | Should -Not -Contain "State"
52+
}
53+
}
2154
}

tests/Get-DbaWsfcCluster.Tests.ps1

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,65 @@ Describe $CommandName -Tag UnitTests {
1818
Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty
1919
}
2020
}
21+
}
22+
23+
Describe $CommandName -Tag IntegrationTests {
24+
# These tests need a Windows failover cluster. No CI environment has one, so
25+
# $TestConfig.ClusterStorage is empty there and these tests skip. A lab that has a cluster sets
26+
# it, and then a regression has to fail these tests.
27+
Context "Retrieving the cluster" -Skip:(-not $TestConfig.ClusterStorage) {
28+
BeforeAll {
29+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
30+
31+
$wsfcCluster = Get-DbaWsfcCluster -ComputerName $TestConfig.ClusterStorage
32+
33+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
34+
}
35+
36+
It "Returns the name and fqdn of the cluster" {
37+
$wsfcCluster.Name | Should -Be $TestConfig.ClusterStorage
38+
$wsfcCluster.Fqdn | Should -BeLike "$($TestConfig.ClusterStorage).*"
39+
}
40+
41+
It "Returns the quorum type as a readable string" {
42+
$wsfcCluster.QuorumType | Should -Not -BeNullOrEmpty
43+
$wsfcCluster.QuorumTypeValue | Should -BeOfType [uint32]
44+
}
45+
46+
It "Does not add an empty State property" {
47+
# MSCluster_Cluster has no State property, so the command built its State NoteProperty
48+
# from an undefined variable and it was always empty, in the object and in the default view.
49+
$wsfcCluster.PSObject.Properties.Name | Should -Not -Contain "State"
50+
}
51+
52+
It "Reports the quorum path as the witness path of a disk witness" {
53+
$wsfcCluster.WitnessPath | Should -Be $wsfcCluster.QuorumPath
54+
}
55+
}
56+
57+
# This needs a cluster whose witness is a file share, which is a different cluster from the one
58+
# with the shared storage. Same rule as above: empty in CI, set by a lab that has one.
59+
Context "Retrieving the witness of a file share witness cluster" -Skip:(-not $TestConfig.ClusterWitness) {
60+
BeforeAll {
61+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
62+
63+
$witnessCluster = Get-DbaWsfcCluster -ComputerName $TestConfig.ClusterWitness
64+
$witnessResource = Get-DbaWsfcResource -ComputerName $TestConfig.ClusterWitness | Where-Object Type -eq "File Share Witness"
65+
66+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
67+
}
68+
69+
It "Uses a file share witness" {
70+
$witnessResource | Should -Not -BeNullOrEmpty -Because "the rest of this context tests what such a cluster reports"
71+
}
72+
73+
It "Returns the share path of the witness" {
74+
# This is what issue #10573 asked for. MSCluster_Cluster has no property that carries it:
75+
# QuorumPath stays empty for a file share witness, and the path only exists in the private
76+
# properties of the witness resource.
77+
$witnessCluster.QuorumPath | Should -BeNullOrEmpty
78+
$witnessCluster.WitnessPath | Should -Be $witnessResource.PrivateProperties.SharePath
79+
$witnessCluster.WitnessPath | Should -BeLike "\\*"
80+
}
81+
}
2182
}

0 commit comments

Comments
 (0)