前回から、VMを資産として管理するための一覧を作成しています。VMの名前や配置先、CPU、メモリなどの情報を整理し、保有しているVMとその構成を一覧で把握できるようにすることが目的です。この一覧は、作成する都度、Hyper-V環境から必要な情報を取得する方針とします。前回作成した一覧にさらに項目を追加し、CSV出力機能も実装して、スクリプトを完成させます。
前回までのスクリプト
前回までに作成したVM一覧を作成するスクリプトを以下に再掲します。このスクリプトは、Get-VMの実行結果から取得した重要なプロパティを軸に、Get-VMFirmware、Get-VMSecurity、Get-VMProcessor、Get-VMHardDiskDrive、Get-VMDVDDrive、Get-VMNetworkAdapterの実行結果を加工して、新たなプロパティとして追加しました(青字部分)。
スクリプトの実行結果が格納される$report変数 には、各VMの情報をプロパティとその値にまとめたカカスタムオブジェクト(PSCustomObject《System.Management.Automation.PSCustomObject》)のコレクションが格納されます。プロパティ数がかなり多くなったため、そのまま一覧表示すると、コンソールのウィンドウ幅によっては列が省略されたり、値が途中で切れたりします。そこで、$report | Out-GridView *1を使うと、GUIのグリッドビューの別ウィンドウで列幅の調整や横スクロールができ、確認しやすくなります。また、今回実装することになるCSVファイルへの出力も簡単です。
*1 ※PowerShell 7.6.6のOut-GridViewには不具合が存在し、応答が返ってこない問題が確認されています(→ OutGridView freezes indefinitely in 7.6.6 #27994|PowerShell Issue GitHub) 。この問題は、PowerShell 7.6.7以降で解消されています。
$report = Get-VM | ForEach-Object {
$vm = $_
if ($vm.Generation -eq "2") {
$firmware = Get-VMFirmware -VM $vm
$security = Get-VMSecurity -VM $vm
$secureboot = $firmware.SecureBoot
$tpmenabled = $security.TpmEnabled
$osType = if ($firmware.SecureBootTemplate -eq "MicrosoftWindows") {
"Windows"
} else {
"Linux"
}
} else {
$secureboot = $null
$tpmenabled = $null
$osType = $null
}
$disks = @(Get-VMHardDiskDrive -VM $vm)
$dvds = @(Get-VMDVDDrive -VM $vm)
$nics = @(Get-VMNetworkAdapter -VM $vm)
$osDisk = $null
$controllerType = switch ($vm.Generation) {
2 { 'SCSI' }
1 { 'IDE' }
default { $null }
}
if ($null -ne $controllerType) {
$osDisk = $disks |
Where-Object { $_.ControllerType -eq $controllerType } |
Sort-Object ControllerNumber, ControllerLocation |
Select-Object -First 1
}
$osDiskSizeGB = if ($osDisk.Path) {
try {
[math]::Round((Get-VHD -Path $osDisk.Path -ErrorAction Stop).Size / 1GB, 1)
}
catch {
$null
}
} else {
"(nodisk)"
}
[pscustomobject]@{
VMName = $vm.Name
Generation = $vm.Generation
ConfigPath = $vm.ConfigurationLocation
VMId = $vm.VMId
## Firmware
## Security
SecureBoot = $secureboot
TPMEnabled = $tpmenabled
OSType = $ostype
## Processor
vCPUs = $vm.ProcessorCount
NestedVirt = (Get-VMProcessor -VM $vm).ExposeVirtualizationExtensions
## Memory
# MemoryAssigned = $vm.MemoryAssigned
DynamicMemory = $vm.DynamicMemoryEnabled
RAMStartupGB = [math]::Round($vm.MemoryStartup / 1GB, 1)
RAMMinimumGB = [math]::Round($vm.MemoryMinimum / 1GB, 1)
RAMMaximumGB = [math]::Round($vm.MemoryMaximum / 1GB, 1)
## HardDrives
OSDiskPath = if ($null -ne $osDisk) { $osDisk.Path } else { "(nodisk)" }
OSDiskGB = $osDiskSizeGB
#DataDisks = [math]::Max($disks.Count - 1, 0)
DataDisks = $disks.Count - $(if ($null -ne $osDisk) { 1 } else { 0 })
## DVDDrives
DVDs = $dvds.Count
## NetoworkAdapters
NICs = $nics.Count
Version = $vm.Version
CreationTime = $vm.CreationTime
# isClustered = $vm.isClustered
}
}
$report
(プレーンテキストで表示/ダウンロード)
VMの設定項目をさらに追加する
前回までのVM一覧で資産管理に必要な項目は十分カバーしていると思いますが、今回はこの一覧に、VMのゲストOSからでなければ取得できない情報をカスタムオブジェクトのプロパティとして追加してみましょう。
スクリプトを簡単にするため、以下の記事で紹介した、「get-kvpdata.ps1」と「get-vmnicinfo.ps1」を使用することにします。「get-kvpdata.ps1」は、KVP(キー値ペア)交換サービスを通じて、VMのゲストの情報をホスト側から取得します。「get-vmnicinfo.ps1」は、VMに割り当てられたネットワークアダプターのIPアドレス情報を取得するスクリプトです。「get-kvpdata.ps1」の結果にもIPアドレスの情報は含まれますが、Windows 10以降およびWindows Server 2016以降はIPアドレス情報を返さなくなったため、その代替手段として作成したものです。
vol.168 知ってる? 使ってる? Hyper-VのKVP|Windows Server 2016 EOSまであと364日
vol.169 KVPでIPアドレスが取得できなくなったのは仕様|Windows Server 2016 EOSまであと362日
この2つのスクリプトを使用して、VMの一覧にOperatingSystem(OS名)、OSVersion(Windowsの場合はOSビルド、Linuxの場合はバージョン)、IPAddress0(ネットワークアダプターの最初のIPv4アドレス)を追加します。
追加したコード(青字部分)を含むスクリプトは、以下のようになります。「get-kvpdata.ps1」と「get-vmnicinfo.ps1」がカレントディレクトリに存在していることを前提としています。
$report = Get-VM | ForEach-Object {
$vm = $_
if ($vm.Generation -eq "2") {
$firmware = Get-VMFirmware -VM $vm
$security = Get-VMSecurity -VM $vm
$secureboot = $firmware.SecureBoot
$tpmenabled = $security.TpmEnabled
$osType = if ($firmware.SecureBootTemplate -eq "MicrosoftWindows") {
"Windows"
} else {
"Linux"
}
} else {
$secureboot = $null
$tpmenabled = $null
$osType = $null
}
$disks = @(Get-VMHardDiskDrive -VM $vm)
$dvds = @(Get-VMDVDDrive -VM $vm)
$nics = @(Get-VMNetworkAdapter -VM $vm)
$osDisk = $null
$controllerType = switch ($vm.Generation) {
2 { 'SCSI' }
1 { 'IDE' }
default { $null }
}
if ($null -ne $controllerType) {
$osDisk = $disks |
Where-Object { $_.ControllerType -eq $controllerType } |
Sort-Object ControllerNumber, ControllerLocation |
Select-Object -First 1
}
$osDiskSizeGB = if ($osDisk.Path) {
try {
[math]::Round((Get-VHD -Path $osDisk.Path -ErrorAction Stop).Size / 1GB, 1)
}
catch {
$null
}
} else {
"(nodisk)"
}
$os = $null
$osver = $null
$vmip = $null
if ($vm.State -eq 'Running') {
$kvp = & ".\get-kvpdata.ps1" -VMName $vm.Name
$os = $kvp | Where-Object Name -eq 'OSName' |
Select-Object -ExpandProperty Data
$osver = $kvp | Where-Object Name -eq 'OSVersion' |
Select-Object -ExpandProperty Data
$vmip = & ".\get-vmnicinfo.ps1" -VMName $vm.Name |
Select-Object -First 1 |
Select -ExpandProperty IPAddresses |
Select -First 1
}
[pscustomobject]@{
VMName = $vm.Name
Generation = $vm.Generation
ConfigPath = $vm.ConfigurationLocation
VMId = $vm.VMId
## Firmware
## Security
SecureBoot = $secureboot
TPMEnabled = $tpmenabled
OSType = $ostype
OperatingSystem = $os
OSVersion = $osver
## Processor
vCPUs = $vm.ProcessorCount
NestedVirt = (Get-VMProcessor -VM $vm).ExposeVirtualizationExtensions
## Memory
# MemoryAssigned = $vm.MemoryAssigned
DynamicMemory = $vm.DynamicMemoryEnabled
RAMStartupGB = [math]::Round($vm.MemoryStartup / 1GB, 1)
RAMMinimumGB = [math]::Round($vm.MemoryMinimum / 1GB, 1)
RAMMaximumGB = [math]::Round($vm.MemoryMaximum / 1GB, 1)
## HardDrives
OSDiskPath = if ($null -ne $osDisk) { $osDisk.Path } else { "(nodisk)" }
OSDiskGB = $osDiskSizeGB
#DataDisks = [math]::Max($disks.Count - 1, 0)
DataDisks = $disks.Count - $(if ($null -ne $osDisk) { 1 } else { 0 })
## DVDDrives
DVDs = $dvds.Count
## NetoworkAdapters
NICs = $nics.Count
IPAddress0 = $vmip
Version = $vm.Version
CreationTime = $vm.CreationTime
# isClustered = $vm.isClustered
}
}
$report
(プレーンテキストで表示/ダウンロード)
CSVファイルに出力する
$report編集に格納されたカスタムオブジェクトのコレクションは、次のようにExport-Csvを使用して簡単にCSV形式のファイルとして出力できます(画面1)。以下の例では、カレントディレクトリに「VMListReport-コンピューター名-yyyyMMdd-HHmmss.csv」というファイル名でCSVファイルを出力します。
$report = Get-VM | ForEach-Object {
(中略)
}
$fileName = '.\VMListReport-{0}-{1}.csv' -f $env:COMPUTERNAME, (Get-Date -Format 'yyyyMMdd-HHmmss')
$report | Export-Csv -Path $fileName -NoTypeInformation -Encoding UTF8
$report

画面1 VM一覧をCSVファイルに出力して参照
過去データも利用して、スクリプトは完成
CSVファイルに出力するように変更したことで、スクリプトは完成としたいところでしたが、もう少し手を加えてみます。せっかくCSVファイルに出力するようにしたので、それを活用しようと思います。
最後に追加したVM一覧のOperatingSystem、OSVersion、IPAddress0は、スクリプト実行時に実行中(Running)のVMだけを対象としており、実行中でなければ値なし($null)になります。過去に出力された「VMListReport-コンピューター名-yyyyMMdd-HHmmss.csv」の最新のものを読み込めば、VMが実行中でない場合でも過去の値を反映することができます(青字部分)。完成した以下のスクリプト「Get-VMList.ps1」では、実行中でないVMで過去の情報が最新のCSVファイルに存在する場合は、「(値)」のように括弧で囲んで出力します(画面2)。これにより、スクリプトを実行するたびにOperatingSystem、OSVersion、IPAddress0の情報が埋められていくはずです。
[Get-VMList.ps1](プレーンテキストで表示/ダウンロード)
$latestCsv = Get-ChildItem -Path . -Filter "VMListReport-$($env:COMPUTERNAME)-*.csv" -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
$oldvmList = if ($latestCsv) {
@(Import-Csv -LiteralPath $latestCsv.FullName)
} else {
$null
}
$report = Get-VM | ForEach-Object {
(中略)
$os = $null
$osver = $null
$vmip = $null
if ($vm.State -eq 'Running') {
$kvp = & ".\get-kvpdata.ps1" -VMName $vm.Name
$os = $kvp | Where-Object Name -eq 'OSName' |
Select-Object -ExpandProperty Data
if ($os -like '*Windows*') {
$osver = $kvp | Where-Object Name -eq 'OSVersion' |
Select-Object -ExpandProperty Data
} else {
$osver = $kvp | Where-Object Name -eq 'OSMajorVersion' |
Select-Object -ExpandProperty Data
}
$vmip = & ".\get-vmnicinfo.ps1" -VMName $vm.Name |
Select-Object -First 1 |
Select -ExpandProperty IPAddresses |
Select -First 1
} else {
if ($oldvmList) {
foreach ($oldvm in $oldvmList) {
if ($oldvm.vmid -eq $vm.VMId){
if($oldvm.OperatingSystem) {
$os = "($($oldvm.OperatingSystem))"
$os = $os.Replace('((', '(').Replace('))', ')')
}
if($oldvm.OSVersion) {
$osver = "($($oldvm.OSVersion))"
$osver = $osver.Replace('((', '(').Replace('))', ')')
}
if($oldvm.IPAddress0) {
$vmip = "($($oldvm.IPAddress0))"
$vmip = $vmip.Replace('((', '(').Replace('))', ')')
}
}
}
}
}
[pscustomobject]@{
VMName = $vm.Name
Generation = $vm.Generation
ConfigPath = $vm.ConfigurationLocation
VMId = $vm.VMId
## Firmware
## Security
SecureBoot = $secureboot
TPMEnabled = $tpmenabled
OSType = $ostype
OperatingSystem = $os
OSVersion = $osver
## Processor
vCPUs = $vm.ProcessorCount
NestedVirt = (Get-VMProcessor -VM $vm).ExposeVirtualizationExtensions
## Memory
# MemoryAssigned = $vm.MemoryAssigned
DynamicMemory = $vm.DynamicMemoryEnabled
RAMStartupGB = [math]::Round($vm.MemoryStartup / 1GB, 1)
RAMMinimumGB = [math]::Round($vm.MemoryMinimum / 1GB, 1)
RAMMaximumGB = [math]::Round($vm.MemoryMaximum / 1GB, 1)
## HardDrives
OSDiskPath = if ($null -ne $osDisk) { $osDisk.Path } else { "(nodisk)" }
OSDiskGB = $osDiskSizeGB
#DataDisks = [math]::Max($disks.Count - 1, 0)
DataDisks = $disks.Count - $(if ($null -ne $osDisk) { 1 } else { 0 })
## DVDDrives
DVDs = $dvds.Count
## NetoworkAdapters
NICs = $nics.Count
IPAddress0 = $vmip
Version = $vm.Version
CreationTime = $vm.CreationTime
# isClustered = $vm.isClustered
}
}
$fileName = '.\VMListReport-{0}-{1}.csv' -f $env:COMPUTERNAME, (Get-Date -Format 'yyyyMMdd-HHmmss')
$report | Export-Csv -Path $fileName -NoTypeInformation -Encoding UTF8
Write-Host "CSV out to: $($fileName)"
$report

画面2 実行中でないVMのOperatingSystem、OSVersion、IPAddress0は、最新のCSVファイルから読み込んで「(値)」のように表示
リファレンス(Microsoft Learn):
Get-VM(Hyper-V)|Get-VMProcessor(Hyper-V)|Get-VMFirmware(Hyper-V)|Get-VMSecurity(Hyper-V)|Get-VMNetworkAdapter(Hyper-V)|Get-VMHardDiskDrive(Hyper-V)|Get-VMHardDiskDrive(Hyper-V)|Export-CSV(PowerShell Utility)|about_PSCustomObject(PowerShell)
Hyper-V 小技・裏技集 目次|(1) |(2)|(3)|(4)|(5)|(6)|(7)