Skip to content

Commit d52ad84

Browse files
committed
Fixes MPIO plugin to properly map MPIO drives to correct disks
1 parent 310f5be commit d52ad84

4 files changed

Lines changed: 166 additions & 154 deletions

File tree

doc/31-Changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ documentation before upgrading to a new release.
77

88
Released closed milestones can be found on [GitHub](https://github.com/Icinga/icinga-powershell-plugins/milestones?state=closed).
99

10+
## 1.15.0 (2026-06-30)
11+
12+
[Issue and PRs](https://github.com/Icinga/icinga-powershell-plugins/milestone/24)
13+
14+
### Bugfixes
15+
16+
* [#476](https://github.com/Icinga/icinga-powershell-plugins/pull/476) Fixes mapping of MPIO drives to the correct LUN on the host for `Invoke-IcingaCheckMPIO`
17+
1018
## 1.14.1 (2026-03-31)
1119

1220
[Issue and PRs](https://github.com/Icinga/icinga-powershell-plugins/milestone/23)

plugins/Invoke-IcingaCheckMPIO.psm1

Lines changed: 122 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,20 @@
2626
2727
* Root\WMI
2828
.EXAMPLE
29-
PS> Invoke-IcingaCheckMPIO -NumberOfPathWarning 'LUN002=9:','LUN003=4';
30-
31-
[WARNING] Multipath-IO Package: 1 Warning [WARNING] ROOT\MPIO\0000_0 Package
32-
\_ [WARNING] ROOT\MPIO\0000_0 Package
33-
\_ [WARNING] ROOT\MPIO\0000_0 Drivers Package
34-
\_ [WARNING] LUN002 Number Paths: Value 8c is lower than threshold 9
35-
\_ [WARNING] LUN003 Number Paths: Value 8c is greater than threshold 4
36-
| lun002::ifw_mpio::numberofpaths=8c;9:;;; lun003::ifw_mpio::numberofpaths=8c;4;;; nolabel::ifw_mpio::numberofpaths=8c;;;; windows::ifw_mpio::numberofpaths=8c;;;; windowsos::ifw_mpio::numberofpaths=8c;;;; rootmpio0000_0::ifw_mpio::numberofdrives=6c;;;;
29+
PS> Invoke-IcingaCheckMPIO -NumberOfPathWarning 'LUN002=9:','LUN003=4' -Verbosity 1;
30+
31+
[WARNING] Multipath-IO Package: 1 Ok 3 Warning [WARNING] MPIO Disk 2 [WARNING] MPIO Disk 3 (All must be [OK])
32+
\_ [INFO] ROOT\MPIO\0000_0 Active: 1
33+
\_ [WARNING] ROOT\MPIO\0000_0 Drives Package (All must be [OK])
34+
\_ [WARNING] MPIO Disk 2 (All must be [OK])
35+
\_ [INFO] Assigned Letters: None
36+
\_ [INFO] Assigned Volumes: LUN002
37+
\_ [WARNING] Number Paths: Value 8c is lower than threshold 9
38+
\_ [WARNING] MPIO Disk 3 (All must be [OK])
39+
\_ [INFO] Assigned Letters: None
40+
\_ [INFO] Assigned Volumes: LUN003
41+
\_ [WARNING] Number Paths: Value 8c is greater than threshold 4
42+
| windows::ifw_mpio::numberofpaths=8c;;;; lun001::ifw_mpio::numberofpaths=4c;;;; lun002::ifw_mpio::numberofpaths=8c;9:;;; lun003::ifw_mpio::numberofpaths=8c;4:;;;
3743
#>
3844
function Invoke-IcingaCheckMPIO()
3945
{
@@ -48,99 +54,131 @@ function Invoke-IcingaCheckMPIO()
4854
);
4955

5056
$CheckPackage = New-IcingaCheckPackage -Name 'Multipath-IO Package' -OperatorAnd -Verbose $Verbosity -AddSummaryHeader;
51-
$MpioData = Get-IcingaMPIOData;
52-
[hashtable]$MpioPackages = @{ };
53-
54-
if ($MpioData -is [array]) {
55-
foreach ($mpio in $MpioData) {
56-
[string]$MpioInstance = $mpio.InstanceName;
57-
58-
# Create a new MPIO Package if it does not exist yet for each instance
59-
if (-Not $MpioPackages.ContainsKey($MpioInstance)) {
60-
$MpioPackages.Add(
61-
$MpioInstance,
62-
@{
63-
'MpioPackage' = New-IcingaCheckPackage -Name ([string]::Format('{0} Package', $MpioInstance)) ` -OperatorAnd -Verbose $Verbosity;
64-
'DrivePackage' = New-IcingaCheckPackage -Name ([string]::Format('{0} Drivers Package', $MpioInstance)) ` -OperatorAnd -Verbose $Verbosity;
65-
}
66-
);
67-
68-
# Add instance specific checks (rquired only once, because they are identical for all drives of an instance)
69-
$MpioPackages[$MpioInstance]['MpioPackage'].AddCheck(
70-
(
71-
New-IcingaCheck `
72-
-Name ([string]::Format('{0} Active', $MpioInstance)) `
73-
-Value $mpio.Active `
74-
-NoPerfData
75-
)
76-
);
77-
78-
$MpioPackages[$MpioInstance]['MpioPackage'].AddCheck(
79-
(
80-
New-IcingaCheck `
81-
-Name ([string]::Format('{0} NumberDrives', $MpioInstance)) `
82-
-Value $mpio.NumberDrives `
83-
-Unit 'c' `
84-
-MetricIndex $MpioInstance `
85-
-MetricName 'numberofdrives'
86-
)
87-
);
88-
}
57+
$DrivePackage = $null;
58+
$DiskData = Join-IcingaPhysicalDiskDataPerfCounter;
59+
[bool]$AddedBasePackages = $false;
60+
[bool]$FoundMpioData = $false;
61+
62+
foreach ($DiskPart in $DiskData.Keys) {
63+
$DiskObjects = $DiskData[$DiskPart];
64+
65+
if ($null -eq $DiskObjects.Data -or $DiskObjects.Data.DiskId -eq '_Total') {
66+
continue;
67+
}
68+
69+
$MpioWarningThreshold = $null;
70+
$MpioCriticalThreshold = $null;
71+
$VolumeNames = 'None';
72+
$VolumeIndex = [string]::Format('MPIO Disk {0}', $DiskObjects.Data.DiskId);
73+
$MpioInstance = '';
74+
$DriveReferences = 'None';
75+
76+
# Don't add non-mpio disks to the check package, but continue with the next one, as there might be multiple disks and only some of them are MPIO enabled
77+
if ($null -eq $DiskObjects.Data.MPIO) {
78+
continue;
79+
}
8980

90-
$MpioWarningThreshold = $null;
91-
$MpioCriticalThreshold = $null;
81+
$FoundMpioData = $true;
82+
$MpioInstance = $DiskObjects.Data.MPIO.InstanceName;
9283

93-
foreach ($entry in $NumberOfPathWarning) {
94-
$parts = $entry.Split('=', 2);
95-
if ($mpio.Volume -like $parts[0]) {
84+
if ($null -ne $DiskObjects.Data.VolumeNames -and $DiskObjects.Data.VolumeNames.Count -gt 0) {
85+
$VolumeIndex = $DiskObjects.Data.VolumeNames[0];
86+
$VolumeNames = $DiskObjects.Data.VolumeNames -join '; ';
87+
}
88+
89+
if ($null -ne $DiskObjects.Data.DriveReference -and $DiskObjects.Data.DriveReference.Count -gt 0) {
90+
$DriveReferences = $DiskObjects.Data.DriveReference.Keys -join '; ';
91+
}
92+
93+
# Only add these once, as the data is identical for every disk
94+
if (-not $AddedBasePackages) {
95+
$DrivePackage = New-IcingaCheckPackage -Name ([string]::Format('{0} Drives Package', $MpioInstance)) -OperatorAnd -Verbose $Verbosity;
96+
97+
$IsActive = New-IcingaCheck `
98+
-Name ([string]::Format('{0} Active', $MpioInstance)) `
99+
-Value $DiskObjects.Data.MPIO.Active `
100+
-NoPerfData;
101+
102+
$NumberOfDrives = New-IcingaCheck `
103+
-Name ([string]::Format('{0} NumberDrives', $MpioInstance)) `
104+
-Value $DiskObjects.Data.MPIO.NumberDrives `
105+
-Unit 'c' `
106+
-MetricIndex $MpioInstance `
107+
-MetricName 'numberofdrives';
108+
109+
$CheckPackage.AddCheck($IsActive);
110+
$CheckPackage.AddCheck($NumberOfDrives);
111+
$AddedBasePackages = $true;
112+
}
113+
114+
foreach ($entry in $NumberOfPathWarning) {
115+
$parts = $entry.Split('=', 2);
116+
[bool]$VolumeMatch = $false;
117+
118+
foreach ($assignedVolume in $DiskObjects.Data.VolumeNames) {
119+
if ($assignedVolume -like $parts[0]) {
96120
$MpioWarningThreshold = $parts[1];
121+
$VolumeMatch = $true;
97122
break;
98123
}
99124
}
100125

101-
foreach ($entry in $NumberOfPathCritical) {
102-
$parts = $entry.Split('=', 2);
103-
if ($mpio.Volume -like $parts[0]) {
126+
if ($VolumeMatch) {
127+
break;
128+
}
129+
}
130+
131+
foreach ($entry in $NumberOfPathCritical) {
132+
$parts = $entry.Split('=', 2);
133+
[bool]$VolumeMatch = $false;
134+
135+
foreach ($assignedVolume in $DiskObjects.Data.VolumeNames) {
136+
if ($assignedVolume -like $parts[0]) {
104137
$MpioCriticalThreshold = $parts[1];
138+
$VolumeMatch = $true;
105139
break;
106140
}
107141
}
108142

109-
# Add all drive specific checks from the MPIO instance
110-
$MpioPackages[$MpioInstance]['DrivePackage'].AddCheck(
111-
(
112-
New-IcingaCheck `
113-
-Name ([string]::Format('{0} Number Paths', $mpio.Volume)) `
114-
-Value $mpio.NumberOfPaths `
115-
-Unit 'c' `
116-
-MetricIndex $mpio.Volume `
117-
-MetricName 'numberofpaths'
118-
).WarnOutOfRange(
119-
$MpioWarningThreshold
120-
).CritOutOfRange(
121-
$MpioCriticalThreshold
122-
)
123-
);
143+
if ($VolumeMatch) {
144+
break;
145+
}
124146
}
125147

126-
foreach ($check in $MpioPackages.Keys) {
127-
$MpioCheckPackage = $MpioPackages[$check]['MpioPackage'];
128-
$DriverPackage = $MpioPackages[$check]['DrivePackage'];
148+
$MPIODiskPackage = New-IcingaCheckPackage -Name ([string]::Format('MPIO Disk {0}', $DiskObjects.Data.DiskId)) -OperatorAnd -Verbose $Verbosity;
129149

130-
if ($MpioCheckPackage.HasChecks()) {
131-
# Add Driver Package to MPIO Package
132-
$MpioCheckPackage.AddCheck($DriverPackage);
133-
}
150+
$MpioDrive = New-IcingaCheck `
151+
-Name 'Number Paths' `
152+
-Value $DiskObjects.Data.MPIO.NumberPaths `
153+
-Unit 'c' `
154+
-MetricIndex $VolumeIndex `
155+
-MetricName 'numberofpaths';
134156

135-
$CheckPackage.AddCheck($MpioCheckPackage);
136-
}
157+
$MpioDrive.WarnOutOfRange($MpioWarningThreshold).CritOutOfRange($MpioCriticalThreshold) | Out-Null;
158+
159+
$MpioVolumes = New-IcingaCheck `
160+
-Name 'Assigned Volumes' `
161+
-Value $VolumeNames `
162+
-NoPerfData;
163+
164+
$MpioLetters = New-IcingaCheck `
165+
-Name 'Assigned Letters' `
166+
-Value $DriveReferences `
167+
-NoPerfData;
168+
169+
$MPIODiskPackage.AddCheck($MpioDrive);
170+
$MPIODiskPackage.AddCheck($MpioVolumes);
171+
$MPIODiskPackage.AddCheck($MpioLetters);
172+
$DrivePackage.AddCheck($MPIODiskPackage);
173+
}
174+
175+
if (-not $FoundMpioData) {
176+
$Check = New-IcingaCheck -Name 'MultiPath-IO Check Status' -NoPerfData;
177+
$Check.SetCritical('No MPIO devices were found on this machine', $TRUE) | Out-Null;
178+
# Enforce the checks to critical in case we get an exception
179+
$CheckPackage.AddCheck($Check);
137180
} else {
138-
if ($MpioData -is [hashtable] -and $MpioData.ContainsKey('Exception')) {
139-
$Check = New-IcingaCheck -Name 'MultiPath-IO Check Status' -NoPerfData;
140-
$Check.SetCritical($TestIcingaWindowsInfoEnums.TestIcingaWindowsInfoText[[int]$MpioData.Exception], $TRUE) | Out-Null;
141-
# Enforce the checks to critical in case we get an exception
142-
$CheckPackage.AddCheck($Check);
143-
}
181+
$CheckPackage.AddCheck($DrivePackage);
144182
}
145183

146184
return (New-IcingaCheckResult -Check $CheckPackage -NoPerfData $NoPerfData -Compile);

provider/disks/Get-IcingaPhysicalDiskInfo.psm1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function Global:Get-IcingaPhysicalDiskInfo()
3838
$PhysicalDiskData = @{ };
3939
$PartitionMapping = @{ };
4040
$VolumeData = Get-Volume;
41+
[array]$MPIOData = Get-IcingaMPIOData;
4142

4243
# This will allow us to map the label of a volume to a certain disk later on
4344
foreach ($volume in $VolumeData) {
@@ -75,6 +76,7 @@ function Global:Get-IcingaPhysicalDiskInfo()
7576
}
7677

7778
$DiskInfo = @{
79+
'DiskId' = $DiskId;
7880
'PartitionStyle' = ''; # Set later on partition check
7981
'PartitionLayout' = @{ }; # Set later on partition check
8082
'DriveReference' = @{ };
@@ -135,6 +137,22 @@ function Global:Get-IcingaPhysicalDiskInfo()
135137
'SectorsPerTrack' = $physical_disk.SectorsPerTrack;
136138
};
137139

140+
if ($null -ne $MPIOData) {
141+
foreach ($mpio in $MPIOData.DriveInfo) {
142+
if ([int]($mpio.Name.Replace('MPIO Disk', '').Trim()) -eq [int]$DiskId) {
143+
$DiskInfo.Add(
144+
'MPIO', @{
145+
'InstanceName' = $MPIOData.InstanceName;
146+
'NumberDrives' = $MPIOData.NumberDrives;
147+
'Active' = $MPIOData.Active;
148+
'NumberPaths' = $mpio.NumberPaths;
149+
}
150+
);
151+
break;
152+
}
153+
}
154+
}
155+
138156
$Partitions = Get-CimAssociatedInstance -InputObject $physical_disk -ResultClass Win32_DiskPartition;
139157
$MaxBlocks = 0;
140158

provider/mpio/Get-IcingaMPIOData.psm1

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,37 @@
66
.OUTPUTS
77
System.Collections.Hashtable
88
#>
9-
109
function Get-IcingaMPIOData()
1110
{
12-
if (-Not (Test-IcingaMPIOInstalled)) {
13-
Exit-IcingaThrowException -ExceptionType 'Custom' -CustomMessage 'MPIO not installed' -InputString 'The Multipath-IO feature is not installed on this system.' -Force;
11+
if (-not (Test-IcingaMPIOInstalled)) {
12+
return $null;
13+
# Test Data for debugging
14+
<#
15+
return [PSCustomObject]@{
16+
'DriveInfo' = [PSCustomObject]@{
17+
'Name' = 'MPIO Disk 0';
18+
'NumberPaths' = 8;
19+
};
20+
'InstanceName' = 'Test Instance';
21+
'NumberDrives' = 1;
22+
'Active' = $true;
23+
}
24+
#>
1425
}
1526

1627
# Check whether MPIO_DISK_INFO exists on the targeted system
1728
$TestClasses = Test-IcingaWindowsInformation -ClassName 'MPIO_DISK_INFO' -NameSpace 'Root\WMI';
1829
# Check for error Ids with Binary operators
1930
$BitWiseCheck = Test-IcingaBinaryOperator -Value $TestClasses -Compare @($TestIcingaWindowsInfoEnums.TestIcingaWindowsInfoExceptionType.Values) -Namespace $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo;
20-
# Get the lasth throw exception id
21-
$ExceptionId = Get-IcingaLastExceptionId;
2231

2332
# We return a empty hashtable if for some reason no data from the WMI classes can be retrieved
2433
if ($BitWiseCheck) {
25-
if ($TestClasses -ne $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok) {
26-
return @{'Exception' = $TestClasses; };
27-
}
28-
}
29-
30-
# Throw an exception when the exception ID is not OK, NotSpecified and PermissionError
31-
if ($TestClasses -ne $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok) {
32-
Exit-IcingaThrowException `
33-
-CustomMessage ($TestIcingaWindowsInfoEnums.TestIcingaWindowsInfoExceptionType[[int]$TestClasses]) `
34-
-InputString ($TestIcingaWindowsInfoEnums.TestIcingaWindowsInfoText[[int]$TestClasses]) `
35-
-ExceptionType Custom `
36-
-Force;
37-
}
38-
39-
$VolumeData = Get-Volume;
40-
$PartitionData = Get-Partition;
41-
$MpioData = Get-IcingaWindowsInformation -ClassName MPIO_DISK_INFO -Namespace 'Root\WMI';
42-
$VolumeObject = @();
43-
44-
foreach ($vol in $VolumeData) {
45-
[string]$VolumeName = $vol.FileSystemLabel;
46-
47-
if ([string]::IsNullOrEmpty($VolumeName)) {
48-
$VolumeName = 'NoLabel';
49-
}
50-
51-
$VolumeObject += [PSCustomObject]@{
52-
'Volume' = $VolumeName;
53-
'DriveLetter' = $vol.DriveLetter;
54-
'FileSystem' = $vol.FileSystem;
55-
'Health' = $vol.HealthStatus;
56-
'SizeRemaining' = $vol.SizeRemaining;
57-
'InstanceName' = '';
58-
'DiskNumber' = -1;
59-
'Partition' = -1;
60-
'Size' = 0;
61-
'NumberOfPaths' = 0;
62-
'NumberDrives' = 0;
63-
'Active' = 0;
64-
'Type' = '';
65-
'DriveType' = $vol.DriveType;
66-
};
67-
68-
foreach ($partition in $PartitionData) {
69-
if ($partition.AccessPaths -contains $vol.Path) {
70-
$VolumeObject[-1].DiskNumber = $partition.DiskNumber;
71-
$VolumeObject[-1].Partition = $partition.PartitionNumber;
72-
$VolumeObject[-1].Size = $partition.Size;
73-
$VolumeObject[-1].Type = $partition.Type;
74-
}
75-
}
76-
}
34+
if ($TestClasses -eq $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok) {
35+
$MPIOData = Get-IcingaWindowsInformation -ClassName MPIO_DISK_INFO -Namespace 'Root\WMI';
7736

78-
foreach ($obj in $VolumeObject) {
79-
$diskId = $obj.DiskNumber;
80-
foreach ($MpioInstance in $MpioData) {
81-
foreach ($drive in $MpioInstance.DriveInfo) {
82-
if ($drive.Name.replace('MPIO Disk', '') -eq $diskid) {
83-
$obj.InstanceName = $MpioInstance.InstanceName;
84-
$obj.NumberDrives = $MpioInstance.NumberDrives;
85-
$obj.Active = $MpioInstance.Active;
86-
$obj.NumberOfPaths = $drive.NumberPaths;
87-
break;
88-
}
89-
}
37+
return $MPIOData;
9038
}
9139
}
9240

93-
return $VolumeObject;
41+
return $null;
9442
}

0 commit comments

Comments
 (0)