前回までの「Hyper-V小技・裏技集」シリーズでは、Hyper-VやHyper-Vホストクラスター上の個々のVMに焦点を当て、PowerShellを使用した自動化テクニックを紹介してきました。今回から「Hyper-V健康診断」シリーズと名前を変えて、PowerShellによるHyper-Vのプラットフォーム側の管理の効率化を目指していきます。
Hyper-Vの仮想化プラットフォームを長期間運用していると、導入当初からさまざまな設定変更が加えられていきます。仮想マシン(VM)の追加や削除、CPUやメモリの増設、VMへのリソースの割り当て変更、仮想スイッチやネットワーク設定、ストレージの変更など、その内容は多岐にわたります。フェールオーバークラスターを構成している場合は、クラスターやクラスター共有ボリューム(CSV)の構成も把握しておく必要があります。
こうした構成情報は、障害発生時の状況確認や、メンテナンス前後の設定比較にも役立ちます。しかし、Hyper-Vマネージャーやフェールオーバークラスターマネージャー、その他のWindowsの管理ツールを開いて一つずつ確認し、記録していくのは大変です。
そこで今回は、PowerShellを使ってHyper-Vホストの構成情報をまとめて取得する「棚卸スクリプト」を作成しようと思います。棚卸スクリプトで保存されたデータは、Hyper-Vプラットフォームの構成や状態を把握し、変更点や異常を見つけ、その健全性を維持するための、いわば「カルテ」になります。
棚卸スクリプトで現在の構成をファイルに保存しておけば、後から読み込んで過去の設定を確認したり、最新の構成と比較したり、取得時期の異なるファイル同士を比較して構成変更を検出したりできるようになります。Hyper-Vホストやホストクラスターの「現在の構成を記録する」だけでなく、「以前と比べて何が変わったのか」をPowerShellで確認できるようにすることも、今回作成する棚卸スクリプトの目的です。差分比較の方法については、次回以降で取り上げる予定です。
さまざまなコマンドレットの実行結果を1ファイルに
Windowsサーバーの基本構成情報やHyper-Vなどの特定の役割の設定情報の多くは、Windows PowerShell 5.1やPowerShell 6/7標準のコマンドレットや、役割に対応したPowerShellモジュールのコマンドレットで取得することができます。対応するコマンドレットがない場合でも、コマンドラインツールの実行結果やレジストリを参照することで不足をカバーできます。
例えば、コンピューターの基本情報(Get-ComputerInfo、Get-WindowsFeatureなど)、Hyper-Vの設定(Get-VMHost、Get-Clusterなど)、ネットワーク(Get-NetAdapter、Get-VMSwitch、Get-NetIPConfigurationなど)、ストレージ(Get-PhysicalDisk、Get-Volume、Get-ClusterSharedVolumeなど)が返すオブジェクトやコレクションの各オブジェクトには、設定値や現在の状態を含む多数のプロパティが含まれているため、これらのコマンドレットで取得したオブジェクトのコレクションを一括で保存しておけば、後から読み込んで調査するのに役立てることができます。
PowerShellはXML(ConvertTo-Xml/ConvertFrom-Xml)、CSV(Export-Csv/Import-Csv、ConvertTo-Csv/ConvertFrom-Csv)、JSON(ConvertTo-Json/ConvertFrom-Json)といった主要な構造化ファイル形式の扱いを得意としています。今回は、Export-Clixml/Import-Clixmlコマンドレットを使用して、取得した構成情報をCLIXML(Common Language Infrastructure XML)形式で保存することにします。
PowerShell以外の他のツールと組み合わせる場合はXMLやJSON、CSVが適していますが、PowerShellでの利用を前提とした場合、CLIXMLが適しています。*1 CLIXMLは、PowerShellオブジェクトをシリアライズして保存するための形式であり、配列や入れ子になったオブジェクトなどの構造に加えて、プロパティ値の型に関する情報もある程度保持できます。そのため、Get-ComputerInfoやGet-VM、Get-VMHostなど、構造の異なるさまざまなオブジェクトをまとめて1つのファイルに保存する今回の用途に適しています。Export-Clixmlで保存した構成情報は、Import-Clixmlを使用して再びPowerShellオブジェクトとして読み込み、参照や比較などに利用できます。
*1 Export-Clixml/Import-Clixmlの利用例については、「スクリプトから平文パスワードを排除する簡単な方法」の記事で資格情報の安全なエクスポート/インポートについて紹介しました。
ただし、Import-Clixmlで完全に元のオブジェクトを復元できるわけではない点には注意が必要です。読み込んだオブジェクトは、多くの場合Deserialized.*オブジェクト(元のオブジェクトの状態を保存したコピー、スナップショットのようなもの)となり、メソッドやライブオブジェクトとしての動作は失われます。
次のPowerShellスクリプト「get-hypervhostinventory.ps1」は、Hyper-VホストまたはHyper-Vホストクラスターのノードで実行することを想定し、Windowsのシステム情報とHyper-Vに関連するホスト、VM、ネットワーク、ストレージ、および過去24時間に記録された重要な(重大/エラー/警告)イベントを1つのCLIXMLファイル(.xml)に保存する棚卸スクリプトです。ファイルは、カレントディレクトリのファイル名「HyperVHost-Inventory-コンピューター名-yyyyMMdd-HHmmss.xml」に保存されます。
「get-hypervhostinventory.ps1」ではファイル出力後にもう1つのPowerShellスクリプト「get-hypervhostsummary.ps1」を実行して保存された情報の概要をコンソールに出力します(画面1)。「get-hypervhostsummary.ps1」は単体で実行することもできます。それには、引数にCLIXMLファイル(.xml)のパスを指定します。
[get-hypervhostinventory.ps1](プレーンテキストで表示/ダウンロード)
$ComputerInfo = Get-ComputerInfo
$cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
# クラスター構成を確認
$cluster = $false
try {
$cluster = Get-Cluster -ErrorAction Stop
}
catch {
$cluster = $false
}
# HA VM の一覧
$haVMNames = @()
if ($cluster) {
$haVMNames = @(
Get-ClusterGroup |
Where-Object GroupType -eq 'VirtualMachine' |
Select-Object -ExpandProperty Name
)
$replicaBroker = Get-ClusterGroup |
Where-Object GroupType -eq 'VMReplicaBroker'
}
# VM一覧
$VirtualMachines = @(
Get-VM | ForEach-Object {
$vm = $_
$vm |
Select-Object *,
@{
Name = 'HA'
Expression = {
$_.Name -in $haVMNames
}
},
@{
Name = 'VCPU'
Expression = {
Get-VMProcessor -VM $_
}
},
@{
Name = 'VRAM'
Expression = {
Get-VMMemory -VM $_
}
},
@{
Name = 'VMFirmware'
Expression = {
if ($_.Generation -eq 2) {
Get-VMFirmware -VM $_
}
}
},
@{
Name = 'VMSecurity'
Expression = {
Get-VMSecurity -VM $_
}
},
@{
Name = 'VNIC'
Expression = {
@(Get-VMNetworkAdapter -VM $_)
}
},
@{
Name = 'VHD'
Expression = {
@(Get-VMHardDiskDrive -VM $_)
}
},
@{
Name = 'VMSnapshot'
Expression = {
@(Get-VMSnapshot -VM $_ -ErrorAction SilentlyContinue)
}
}
}
)
# Hyper-V / Cluster 関連サービス
$services = @(
'vmms'
'vmcompute'
'hvhost'
'clussvc'
)
$HyperVServices = @(
Get-Service -Name $services -ErrorAction SilentlyContinue
)
# Hyper-V関連のイベントログ(重大/エラー/警告)
$start = (Get-Date).AddHours(-24)
$HyperVLogs = @(
'System'
'Application'
'Security'
Get-WinEvent -ListLog 'Microsoft-Windows-Hyper-V-*' |
Where-Object {
$_.IsEnabled -and $_.RecordCount -gt 0
} |
Select-Object -ExpandProperty LogName
if ($cluster) {
Get-WinEvent -ListLog 'Microsoft-Windows-FailoverClustering-*' |
Where-Object {
$_.IsEnabled -and $_.RecordCount -gt 0
} |
Select-Object -ExpandProperty LogName
}
)
$HyperVEvents = foreach ($log in $HyperVLogs) {
Get-WinEvent -FilterHashtable @{
LogName = $log
StartTime = $start
Level = 1, 2, 3 # Critical, Error, Warning
} -ErrorAction SilentlyContinue
}
$inventory = [pscustomobject]@{
CollectedAt = Get-Date
OSInfo = [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
OperatingSystem = $ComputerInfo.OSName
OSBuild = "$($cv.CurrentBuild).$($cv.UBR)"
isCluster = if ($cluster) { $true } else { $false }
ClusterName = if ($cluster) { $cluster.Name } else { $null }
HotFix = @(Get-HotFix)
WindowsFeatures = @(Get-WindowsFeature)
}
ComputerInfo = $ComputerInfo
HyperV = [pscustomobject]@{
Services = $HyperVServices
NumaNodes = @(Get-VMHostNumaNode)
VMHost = Get-VMHost
VMReplicationServer = if($cluster) {
if ($replicaBroker) {
Get-VMReplicationServer -ComputerName $replicaBroker.Name
} else {
$null
}
} else {
Get-VMReplicationServer
}
VMReplication = @(Get-VMReplication)
}
Network = [pscustomobject]@{
NetAdapters = @(
Get-NetAdapter
)
VMSwitches = @(
Get-VMSwitch
)
NetNats = @(
Get-NetNat
)
NetNatStaticMappings = @(
Get-NetNatStaticMapping
)
ManagementOSAdapters = @(
Get-VMNetworkAdapter -ManagementOS
)
IPAddresses = @(
Get-NetIPConfiguration
)
FirewallRules = @(
Get-NetFirewallRule -PolicyStore ActiveStore
)
}
VirtualMachines = @(
$VirtualMachines
)
Storage = [pscustomobject]@{
PhysicalDisks = @(
Get-PhysicalDisk
)
Volumes = @(
Get-Volume
)
ClusterSharedVolumes = @(
if ($cluster) {
Get-ClusterSharedVolume
}
)
S2D = if ($cluster) {
$s2dPool = @(Get-StoragePool -FriendlyName 'S2D*' -ErrorAction SilentlyContinue)
if ($s2dPool) {
[pscustomobject]@{
StoragePools = $s2dPool
PhysicalDisks = @(
Get-StorageSubsystem -FriendlyName 'Cluster*' |
Get-PhysicalDisk
)
VirtualDisks = @(
$s2dPool | Get-VirtualDisk
)
}
} else {
$null
}
} else {
$null
}
}
HyperVEvents = @(
$HyperVEvents |
Sort-Object TimeCreated |
Select-Object TimeCreated, LogName, Id, Level, LevelDisplayName, Message
)
}
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$inventory | Export-Clixml ".\HyperVHost-Inventory-$env:COMPUTERNAME-$timestamp.xml"
& .\get-hypervhostsummary.ps1 ".\HyperVHost-Inventory-$env:COMPUTERNAME-$timestamp.xml"
Write-Host "`nOutput to: .\HyperVHost-Inventory-$env:COMPUTERNAME-$timestamp.xml"
[get-hypervhostsummary.ps1](プレーンテキストで表示/ダウンロード、Hyper-Vホストクラスターでの出力サンプル)
param(
[Parameter(Mandatory)]
[string]$Path
)
$inventory = Import-Clixml $Path
$computer = $inventory.ComputerInfo
$roles = $inventory.OSInfo.WindowsFeatures
$services = @($inventory.HyperV.Services)
$vms = @($inventory.VirtualMachines)
$nics = @($inventory.Network.NetAdapters)
$mgmtNics = @($inventory.Network.ManagementOSAdapters)
$switches = @($inventory.Network.VMSwitches)
$volumes = @($inventory.Storage.Volumes)
Write-Host "`n### Hyper-V Host Summary ###"
[pscustomobject]@{
# OS / Host
ComputerName = $inventory.OSInfo.ComputerName
OperatingSystem = $inventory.OSInfo.OperatingSystem
OSBuild = $inventory.OSInfo.OSBuild
CollectedAt = $inventory.CollectedAt.DateTime
# ComputerInfo
Manufacturer = $computer.CsManufacturer
Model = $computer.CsModel
Processor = $computer.CsProcessors.Name
PhysicalProcessors = @($computer.CsProcessors).Count
LogicalProcessors = $computer.CsNumberOfLogicalProcessors
NUMANodes = @($inventory.NumaNodes).Count
MemoryGB = [math]::Round(
$computer.CsTotalPhysicalMemory / 1GB, 1
)
BIOSVersion = $computer.BiosSMBIOSBIOSVersion
# Cluster
ClusterName = $inventory.OSInfo.ClusterName
# Role and Features
InstalledRoles = ($roles |
Where-Object {$_.Installed -and $_.FeatureType -like 'Role*'}).Count
InstalledFeatures = ($roles |
Where-Object {$_.Installed -and $_.FeatureType -like 'Feature*'}).Count
# Services
HyperVServices = $services.Count
RunningServices = @($services |
Where-Object Status -like 'Running'
).Count
# VM
VMCount = $vms.Count
RunningVMs = @($vms | Where-Object State -like 'Running').Count
StoppedVMs = @($vms | Where-Object State -like 'Off').Count
HAVMs = @($vms | Where-Object HA -eq $true).Count
# Network
NetAdapters = $nics.Count
UpNetAdapters = @($nics | Where-Object Status -like 'Up').Count
ManagementOSNICs = $mgmtNics.Count
VMSwitches = $switches.Count
# Storage
Volumes = $volumes.Count
}
Write-Host "`n### WindowsFeatures (Get-WindowsFeature) ###"
$roles |
Where-Object {$_.Installed} |
Format-Table -AutoSize
Write-Host "`n### Services (Get-Service) ###"
$services |
Format-Table Name, Status, StartType
Write-Host "`n=== VMHost (Get-VMHost) ==="
$inventory.HyperV.VMHost |
Select LogicalProcessorCount, NumaSpanningEnabled, MemoryCapacity, VirtualMachineMigrationEnabled,VirtualHardDiskPath, VirtualMachinePath |
Format-List
Write-Host "`n### Virtual Machines (Get-VM) ###"
$vms |
Select-Object Name, State, Generation, ProcessorCount, HA |
Format-Table -AutoSize
Write-Host "`n=== VMReplicationServer (Get-VMReplicationServer) ==="
$inventory.HyperV.VMReplicationServer |
Format-Table -AutoSize
Write-Host "`n=== VMReplication (Get-VMReplication) ==="
$inventory.HyperV.VMReplication |
Format-Table -AutoSize
Write-Host "`n### Network ###"
Write-Host "`n=== NetAdapters (Get-NetAdapter) ==="
$nics |
Select-Object Name, Status, LinkSpeed |
Format-Table -AutoSize
Write-Host "`n=== VMSwitches (Get-VMSwitch) ==="
$switches |
Select Name,SwitchType,AllowManagementOS,EmbeddedTeamingEnabled,AllowNetLbfoTeams |
Format-Table -AutoSize
Write-Host "`n=== IPAddresses (Get-NetIPConfiguration) ==="
$inventory.Network.IPAddresses |
ForEach-Object {
[pscustomobject]@{
IfAlias = $_.InterfaceAlias
IfIndex = $_.InterfaceIndex
MacAddress = $_.NetAdapter.MacAddress
IPv4Address = @(
$_.IPv4Address | ForEach-Object {
[pscustomobject]@{
IPAddress = $_.IPAddress
PrefixLength = $_.PrefixLength
}
}
)
IPv6Address = @(
$_.IPv6Address | ForEach-Object {
[pscustomobject]@{
IPAddress = $_.IPAddress
PrefixLength = $_.PrefixLength
}
}
)
IPv4Gateway = @(
$_.IPv4DefaultGateway.NextHop
)
IPv6Gateway = @(
$_.IPv6DefaultGateway.NextHop
)
DNSServer = @(
$_.DNSServer.ServerAddresses
)
}
} |
Select-Object IfAlias,IfIndex, MacAddress,
@{N='IPv4Address';E={
($_.IPv4Address | ForEach-Object {
"$($_.IPAddress)/$($_.PrefixLength)"
}) -join ', '
}} |Format-Table -AutoSize
Write-Host "`n### Storage ###"
Write-Host "`n=== Volumes (Get-Volume) ==="
$volumes |
Select-Object DriveLetter, FileSystemLabel,
@{N='SizeGB';E={[math]::Round($_.Size / 1GB,1)}},
@{N='FreeGB';E={[math]::Round($_.SizeRemaining / 1GB,1)}} |
Format-Table -AutoSize
Write-Host "`n=== PhysicalDisks (Get-PhysicalDisk) ==="
$inventory.Storage.PhysicalDisks | Format-Table -AutoSize
Write-Host "`n=== ClusterSharedVolumes (Get-ClusterSharedVolume) ==="
$inventory.Storage.ClusterSharedVolumes |Select Name, OwnerNode, State, @{Name='Path'; Expression={$_.SharedVolumeInfo.FriendlyVolumeName}} | Format-Table -AutoSize
Write-Host "`n=== S2D.StoragePools (Get-StoragePool) ==="
$inventory.Storage.S2D.StoragePools|Format-Table -AutoSize
Write-Host "`n=== S2D.PhysicalDisks (Get-PhysicalDisk) ==="
$inventory.Storage.S2D.PhysicalDisks|Format-Table -AutoSize
Write-Host "`n=== S2D.VirtualDisks (Get-VirtualDisk) ==="
$inventory.Storage.S2D.VirtualDisks|Format-Table -AutoSize
Write-Host "`n### EventLog (Get-WinEvent, Critical/Error in the past 24h) ###"
$Inventory.HyperVEvents |
Where-Object LevelDisplayName -in '重大','エラー' |
Group-Object LogName |
ForEach-Object {
Write-Host "`n=== $($_.Name) ==="
$_.Group |
Sort-Object TimeCreated -Descending |
Format-Table TimeCreated, Id, LevelDisplayName, Message -AutoSize -Wrap
}

画面1 棚卸スクリプトは、スタンドアロンのHyper-VホストとHyper-Vホストクラスターの両方に対応(クラスターでの出力サンプルはこちら)
次回はこの棚卸スクリプトで保存したCLIXMLファイル(.xml)を読み込んで、Hyper-Vホストの構成データを参照する方法を紹介します。今回のPowerShellスクリプト「Get-hypervhostsummary.ps1」はその一例です。なお、棚卸スクリプトは次回以降に説明する内容に対応させるために項目を追加していった結果、かなり複雑で大きなデータになってしまいました。棚卸データで保存されるデータの構造については、次回説明します。
サーバー設定仕様書の自動生成サービス、SSD-assistanceのご紹介
弊社の主力製品の1つ設定仕様書自動生成サービス「SSD-assistance」は、設定仕様書作成のためのデータ収集やドキュメント作成の手間と時間を省き、高品質で標準化された設定仕様書を自動生成するサービスです。SSD-assistanceは、「Hyper-V ホスト 設定仕様書」の作成に対応している上、「差分比較リスト」で構成の変更差分を視覚的に強調表示させることができます。今回、そして次回以降の記事でやろうとしていることと同等のことは、実はSSD-assistanceを利用すれば誰でも、簡単に、すぐに実現することができます。ただこの連載記事では、SSD-assistanceでは手の届かない、Hyper-Vの深くて、細かいところまで手を伸ばせればと考えています。
Hyper-Vホスト設定仕様書のサンプル(クリックで拡大)
|
差分比較リストのサンプル(クリックで拡大)
|
|
リファレンス(Microsoft Learn):
Get-ComputerInfo(PowerShell Management)|Get-Service(PowerShell Management)|Get-HotFix(PowerShell Management)|Get-VMHost(Hyper-V)|Get-VMHostNumaNode(Hyper-V)|Get-VM(Hyper-V)|Get-VMProcessor(Hyper-V)|Get-VMMemory(Hyper-V)|Get-VMFirmware(Hyper-V)|Get-VMSecurity(Hyper-V)|Get-VMNetworkAdapter(Hyper-V)|Get-VMHardDiskDrive(Hyper-V)|Get-VMSnapshot(Hyper-V)|Get-VMReplicationServer(Hyper-V)|Get-VMReplication(Hyper-V)|Get-VMSwitch(Hyper-V)|Get-Cluster(FailoverClusters)|Get-ClusterGroup(FailoverClusters)|Get-ClusterSharedVolume(FailoverClusters)|Get-NetAdapter(NetAdapter)|Get-PhysicalDisk(Storage)|Get-Volume(Storage)|Get-WinEvent(PowerShell Dignostics)|Export-Clixml(PowerShell Utility)|Import-Clixml(PowerShell Utility)|ConvertTo-JSON(PowerShell Utility)|ConvertFrom-JSON(PowerShell Utility)|Compare-Object(PowerShell Utility)
Hyper-V 小技・裏技集 目次|(1) | (2)