modules/Cluster/Cluster.psm1
|
<# Cluster.psm1 - failover cluster discovery (read-only). Role-gated. Never alters cluster state. #> function Get-DiscoveryModuleMetadata { [pscustomobject]@{ ModuleName='Cluster'; DisplayName='Failover Cluster'; Category='Virtualization'; Version='1.0.0' DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$true; RequiresDomainContext=$false RequiresRole='Failover-Clustering'; EstimatedImpact='Low'; CanRunAsSystem=$true ProducesDatasets=@('ClusterDiscovery','ClusterResources','ClusterNodes','ClusterNetworks','ClusterGroups','ClusterSharedVolumes') ProducesRisks=$true; ProducesFollowUpQuestions=$false; SupportsDeepMode=$true; SupportsComplianceLens=$false } } function Test-DiscoveryPrerequisites { param([object]$Context) $present = (Get-CommandAvailable -Name 'Get-Cluster') -or (Get-Service -Name 'ClusSvc' -ErrorAction SilentlyContinue) if ($present) { return [pscustomobject]@{ ModuleName='Cluster'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() } } [pscustomobject]@{ ModuleName='Cluster'; CanRun=$false; Status='NotApplicable'; Reason='Failover Clustering (Get-Cluster / ClusSvc) not present.'; Limitations=@() } } function Invoke-DiscoveryCollection { param([object]$Context) $disc=[System.Collections.Generic.List[object]]::new(); $res=[System.Collections.Generic.List[object]]::new() $cnodes=[System.Collections.Generic.List[object]]::new(); $cnets=[System.Collections.Generic.List[object]]::new(); $cgroups=[System.Collections.Generic.List[object]]::new(); $ccsv=[System.Collections.Generic.List[object]]::new() if (-not (Get-CommandAvailable -Name 'Get-Cluster')) { Add-Limitation -Context $Context -Module 'Cluster' -Message 'FailoverClusters module not available.' | Out-Null; return ,@{ ClusterDiscovery=@(); ClusterResources=@(); ClusterNodes=@(); ClusterNetworks=@(); ClusterGroups=@(); ClusterSharedVolumes=@() } } try { $cl = Get-Cluster -ErrorAction SilentlyContinue if ($cl) { $nodes = ''; $quorum=''; $witness=''; $nets='' try { $nodes = ((Get-ClusterNode -ErrorAction SilentlyContinue | ForEach-Object { $_.Name }) -join ', ') } catch { } try { $q = Get-ClusterQuorum -ErrorAction SilentlyContinue; $quorum = [string]$q.QuorumType; $witness = [string]$q.QuorumResource } catch { } try { $nets = ((Get-ClusterNetwork -ErrorAction SilentlyContinue | ForEach-Object { $_.Name }) -join ', ') } catch { } $disc.Add([pscustomobject]@{ ClusterName=$cl.Name; Nodes=$nodes; QuorumType=$quorum; WitnessResource=$witness; Networks=$nets }) try { foreach ($r in (Get-ClusterResource -ErrorAction SilentlyContinue)) { $res.Add([pscustomobject]@{ Name=$r.Name; State=[string]$r.State; OwnerGroup=[string]$r.OwnerGroup; ResourceType=[string]$r.ResourceType; OwnerNode=[string]$r.OwnerNode }) } } catch { } # Health and workload detail: a node or network that is not Up, and the roles (VMs, file servers) # and CSVs that have to be moved, are the facts a migration or retirement plan is built on. try { foreach ($n in (Get-ClusterNode -ErrorAction SilentlyContinue)) { $cnodes.Add([pscustomobject]@{ Name=$n.Name; State=[string]$n.State; NodeWeight=$n.NodeWeight; DynamicWeight=$n.DynamicWeight }) } } catch { } try { foreach ($n in (Get-ClusterNetwork -ErrorAction SilentlyContinue)) { $cnets.Add([pscustomobject]@{ Name=$n.Name; Address=$n.Address; AddressMask=$n.AddressMask; Role=[string]$n.Role; State=[string]$n.State }) } } catch { } try { foreach ($g in (Get-ClusterGroup -ErrorAction SilentlyContinue)) { $cgroups.Add([pscustomobject]@{ Name=$g.Name; GroupType=[string]$g.GroupType; OwnerNode=[string]$g.OwnerNode; State=[string]$g.State }) } } catch { } try { foreach ($v in (Get-ClusterSharedVolume -ErrorAction SilentlyContinue)) { $ccsv.Add([pscustomobject]@{ Name=$v.Name; OwnerNode=[string]$v.OwnerNode; State=[string]$v.State; Path=[string]$v.SharedVolumeInfo.FriendlyVolumeName }) } } catch { } } } catch { Add-Limitation -Context $Context -Module 'Cluster' -Message 'Cluster query failed.' -Reason $_.Exception.Message | Out-Null } return ,@{ ClusterDiscovery=@($disc); ClusterResources=@($res); ClusterNodes=@($cnodes); ClusterNetworks=@($cnets); ClusterGroups=@($cgroups); ClusterSharedVolumes=@($ccsv) } } function ConvertTo-DiscoveryDatasets { param([object]$Context, $RawData) if (-not $RawData) { $RawData = @{} } $get = { param($k) if ($RawData[$k]) { @($RawData[$k]) } else { @() } } Add-DataSet -Context $Context -Name 'ClusterDiscovery' -Description 'Failover cluster summary.' -Rows (& $get 'ClusterDiscovery') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null Add-DataSet -Context $Context -Name 'ClusterResources' -Description 'Cluster resources.' -Rows (& $get 'ClusterResources') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null Add-DataSet -Context $Context -Name 'ClusterNodes' -Description 'Cluster nodes with state and quorum weight.' -Rows (& $get 'ClusterNodes') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null Add-DataSet -Context $Context -Name 'ClusterNetworks' -Description 'Cluster networks with role and state.' -Rows (& $get 'ClusterNetworks') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null Add-DataSet -Context $Context -Name 'ClusterGroups' -Description 'Cluster roles (VMs, file servers, ...).' -Rows (& $get 'ClusterGroups') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null Add-DataSet -Context $Context -Name 'ClusterSharedVolumes' -Description 'Cluster Shared Volumes.' -Rows (& $get 'ClusterSharedVolumes') -Visibility 'Internal' -SourceModule 'Cluster' | Out-Null } Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets' |