Hyper-V上でコンテナを作成、操作する方法を紹介します。内容の詳細は下記に記載いたしますのでご確認ください。
前提
モジュール名は「InvokeVContainer」とします。前回配置した「C:\Users\<ユーザー名>\Documents\WindowsPowerShell\Modules\ InvokeVContainer.psm1」のモジュールファイルのコマンドを追加していきます。
最終的な完成版は、GitHub(https://github.com/InvokeV/InvokeV-Container)に掲載しています。
公開しているモジュールInvokeVContainer.psm1のコードは以下となります。
$RootPath = "D:\InvokeVContainer"
Function Get-InvokeVContanerRoot(){
     Return $RootPath
}
Function Import-ContainerImage($FilePath, $ImageName) { 
    $File = Get-ChildItem $FilePath
    If ($File.Extension -eq ".vhdx") {
        If ($File -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}" -eq $True){
            Copy-Item $File -Destination ("$RootPath\Images\" + $File.Name)
        }Else{
            If($ImageName){$ImageName = $File.BaseName}
            Copy-Item $File -Destination ("$RootPath\Images\" + $ImageName + "_" + [Guid]::NewGuid() + ".vhdx") 
        }
    }Else{
        Expand-Archive -Path $FilePath -DestinationPath $RootPath\Images -Force
    } 
}
Function Get-ContainerImage { 
    Get-ChildItem $RootPath\Images *.vhdx | Where-Object {$_.Name -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"} | Select `
    @{Label="Name"; Expression={($_.BaseName.Substring(0, $_.BaseName.Length - ($_.BaseName.Split("_")[$_.BaseName.Split("_").Length - 1].Length) - 1))}}, `
    @{Label="Path"; Expression={($_.FullName)}}, 
    @{Label="Size(MB)"; Expression={($_.Length /1024/1024)}}, `
    @{Label="Created"; Expression={($_.LastWriteTime)}}, `
    @{Label="ParentPath"; Expression={(Get-VHD $_.FullName).ParentPath}} | Where-Object {$_.Name -ne ""}
}
Function Export-ContainerImage([String]$ImageName, [String]$ExportPath, [Switch]$Tree){
    [Reflection.Assembly]::LoadWithPartialName( "System.IO.Compression.FileSystem" ) | Out-Null
    $Archive = [System.IO.Compression.ZipFile]::Open($ExportPath, "Update")
    $CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $Image = Get-ChildItem (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
    [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $Image.FullName, $Image.Name, $CompressionLevel) | Out-Null  
    If($Tree){ 
        $ParentFile = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).ParentPath  
        While ($ParentFile -ne ""){
            If($ParentFile -ne ""){
                [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $ParentFile, (Get-ChildItem $ParentFile).Name, $CompressionLevel) | Out-Null
                $ParentFile = (Get-ContainerImage | Where {$_.Path -eq "$ParentFile"}).ParentPath
            }
        }        
    }
    $Archive.Dispose()
}
Function New-ContainerImage([String]$ContainerName, [String]$ImageName) {    
    $ImageName = $ImageName + "_" + [Guid]::NewGuid() 
    $VM = Get-VM $ContainerName
    $Disk = Get-VMHardDiskDrive $VM   
    If($VM.State -eq "Off"){
        Get-VMHardDiskDrive $VM | Copy-Item -Destination "$RootPath\Images\$ImageName.vhdx"
     }Else{
        Checkpoint-VM $VM -SnapshotName "$ContainerName"
        Copy-Item (Get-VHD (Get-VMHardDiskDrive $VM).Path).ParentPath -Destination "$RootPath\Images\$ImageName.vhdx"
        Remove-VMSnapshot $VM –Name "$ContainerName" 
    }   
}
Function Merge-ContainerImage([String]$ImageName, [String]$NewImageName, [Switch]$Del) { 
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path  
    $ParentPath = (Get-VHD "$ImagePath").ParentPath
    $NewImageID = $NewImageName + "_" + [Guid]::NewGuid() 
    Copy-Item "$ParentPath" -Destination "$RootPath\Images\$NewImageID.vhdx" 
    Copy-Item "$ImagePath" -Destination "$RootPath\Images\$NewImageID.avhdx"
    Set-VHD -Path "$RootPath\Images\$NewImageID.avhdx" –ParentPath "$RootPath\Images\$NewImageID.vhdx"
    Merge-VHD –Path "$RootPath\Images\$NewImageID.avhdx" –DestinationPath "$RootPath\Images\$NewImageID.vhdx"  
    If($Del){ Remove-Item $ImagePath }
}
Function Remove-ContainerImage[String]$ImageName, [Switch]$Tree) { 
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
    If($ImagePath) {
        $ChildItems = Get-ChildItem -Path "$RootPath" -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq "$ImagePath"}
        If($ChildItems.Count -eq 0){
            Remove-Item $ImagePath -Recurse
        }Else{   
            If($Tree){
                ForEach ($ChildItem in $ChildItems) {
                    If($ChildItem.Path.ToString().ToUpper().StartsWith("$RootPath\Images".ToString().ToUpper())){
                        $ChildImageName = (Get-ChildItem ($ChildItem.Path)).Name
                        $ImageName = ($ChildImageName.Substring(0, $ChildImageName.Length - ($ChildImageName.Split("_")[$ChildImageName.Split("_").Length - 1].Length) - 1)) 
                        Remove-ContainerImage $ImageName -Tree
                    }Else{
                        $ContainerName =  (Get-ChildItem ($ChildItem.Path)).BaseName
                        Remove-Container($ContainerName)
                    }
                }            
                Remove-Item $ImagePath -Recurse
            }Else{
                Write-Host """$ImageName"" is used other container images or containers." -ForegroundColor Red
            }
        }
    }Else{
         Write-Host """$ImageName"" is not container image name." -ForegroundColor Red
    }
}
Function New-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName) {
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path 
    $VHD = New-VHD -Path "$RootPath\Containers\$ContainerName\$ContainerName.vhdx" -Differencing -ParentPath "$ImagePath"
    $VM = New-VM -Name "$ContainerName" -Generation 2 -MemoryStartupByte $Memory -VHDPath $VHD.Path -Path "$RootPath\Containers"
    Set-VM $VM -DynamicMemory -MemoryMaximumBytes $Memory -ProcessorCount $Processer
    If($SwitchName -ne ""){
        Get-VMNetworkAdapter $VM | Connect-VMNetworkAdapter –SwitchName $SwitchName
    }
    Set-VMFirmware $VM -EnableSecureBoot Off
    Set-VMProcessor $VM -ExposeVirtualizationExtensions $True
} 
Function Get-Container { 
    Get-VM | Where-Object {Test-Path ("$RootPath\Containers\" + $_.Name)} | Select `
    @{Label="Name"; Expression={$_.Name}}, 
    State, 
    @{Label="Path"; Expression={((Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path)}}, `
    @{Label="ParentPath"; Expression={(Get-VHD(Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path).ParentPath}}
}
Function Start-Container([String]$ContainerName) {   
    Start-VM $ContainerName
}
Function Stop-Container([String]$ContainerName) {   
    Stop-VM $ContainerName -Force
}
Function Remove-Container([String]$ContainerName) { 
    $VM = Get-VM "$ContainerName"
    If($VM.State -eq "Running"){
        Stop-VM $VM -TurnOff
    }  
    Remove-VM $VM -Force
    Remove-Item "$RootPath\Containers\$ContainerName" -Recurse
}
Function Run-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName, [String]$IPAddress, [String]$Subnet, [String]$Gateway, [String]$DNS = @()){
    New-Container -ContainerName "$ContainerName" -ImageName "$ImageName" -Memory $Memory -Processer $Processer -SwitchName "$SwitchName"
    Start-Container "$ContainerName"
    Wait-ContainerBoot "$ContainerName"
    Set-ContainerIPConfig -ContainerName "$ContainerName" -IPAddress $IPAddress -Subnet $Subnet -Gateway $Gateway -DNS $DNS
}
Function Wait-ContainerBoot([String]$ContainerName){
    $Flg = $False
    $TimeCount = 0
    Do
    { 
        If((Get-ContainerIPAddress $ContainerName) -ne ""){
            $Flg = $True 
        }Else{
            $TimeCount = $TimeCount + 1
            If($TimeCount -eq 180){
                 $Flg = $True 
            }
        }
        Start-Sleep -s 1
    }
    While ($Flg -eq $False)
}
Function Get-ContainerIPAddress([String]$ContainerName){
    $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService 
    $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'" 
    $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }    
    $NetworkAdapters = $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData') 
    $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0] 
    $NetworkSettings = @()
    ForEach ($NetworkAdapter in $NetworkAdapters) {
        If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {
            $NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")
        }
    }  
    If($NetworkSettings.Length -eq 0){
        Return ""
    }Else{
        If( $NetworkSettings[0].IPAddresses.Length -eq 0){
            Return ""
        }Else{
            Return $NetworkSettings[0].IPAddresses[0]    
        }        
    }
}
Function Set-ContainerIPConfig([String]$ContainerName, [String]$IPAddress = @(), [String]$Subnet = @(), [String]$Gateway = @(), [String]$DNS = @()){
    $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService 
    $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'" 
    $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }    
    $NetworkAdapters = $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData') 
    $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0] 
    $NetworkSettings = @()
    ForEach ($NetworkAdapter in $NetworkAdapters) {
        If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {
            $NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")
        }
    }
    $NetworkSettings[0].DHCPEnabled = $False
    $NetworkSettings[0].IPAddresses = $IPAddress
    $NetworkSettings[0].Subnets = $Subnet
    $NetworkSettings[0].DefaultGateways = $Gateway
    $NetworkSettings[0].DNSServers = $DNS
    $NetworkSettings[0].ProtocolIFType = 4096  
    $ManagementService.SetGuestNetworkAdapterConfiguration($ComputerSystem.Path, $NetworkSettings.GetText(1)) | Out-Null
    #Write-Host "IP was configured."
}
Function Get-TreeView() { 
    $Global:Tree = "`r`n"
    $RootFiles = Get-ChildItem $RootPath\Images *.vhdx | Get-VHD | Where {$_.ParentPath -eq ""}
    ForEach($File in $RootFiles){ 
        Set-TreeView $File.Path 0    
    }
    Write-Host $Global:Tree
}
Function Set-TreeView([String]$ParentFile, [Int]$Level) { 
    $Files = Get-ChildItem -Path $RootPath -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq $ParentFile} 
    $BaseName = (Get-ChildItem $ParentFile).BaseName
    If($BaseName -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"){
        $Name = ($BaseName.Substring(0, $BaseName.Length - ($BaseName.Split("_")[$BaseName.Split("_").Length - 1].Length) - 1))
    }Else{
        $Name = $BaseName
    }
    If((Get-ChildItem $ParentFile).FullName.ToUpper().StartsWith(("$RootPath\Images").ToUpper())){
        $Name = "[" + $Name + "]"
    }Else{
        $Name = " " + $Name    
    }
    $Space = ""
    For ( $i = 0; $i -lt (($Level - 1) * 12); $i++ ){ 
        $Space += " "
    }
    If($Level -ne 0){
        $Space += " |"
        For ( $i = 0; $i -lt (10 - $Nam.Length) ; $i++ ){ 
            $Space += "-"
        }
    }
    $Global:Tree += $Space + $Name +  "`r`n`r`n"
    If($Files.Count -eq 0){  
    }Else{    
        ForEach($File in $Files){     
            Set-TreeView $File.Path ($Level + 1)    
        }
    }
}
Function Correct-ContainerImage() { 
    $Images = Get-ChildItem -Path "$RootPath\Images" -Include "*.vhdx" -Recurse
    ForEach($Image in $Images){ 
        $ParentPath = (Get-VHD $Image).ParentPath
        If ($ParentPath){  
            Set-VHD -Path $Image.FullName –ParentPath $ParentPath -IgnoreIdMismatch  
            Write-Host $Image.Name ">" (Get-ChildItem $ParentPath).Name
        }
    }
}
今回は主にコンテナの操作について紹介します。
コンテナの操作
コンテナの作成
InvokeVコンテナの相対は、Hyper-V上の仮想マシンです。従ってコンテナの作成は仮想マシンの作成と同じ方法で行います。ポイントは、親となるコンテナイメージの仮想ハードディスクの差分ファイルを作成して、コンテナのハードディスクとして利用するという点です。
New-Container
Function New-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName) {  $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path$VHD = New-VHD -Path "$RootPath\Containers\$ContainerName\$ContainerName.vhdx" -Differencing -ParentPath "$ImagePath"  $VM = New-VM -Name "$ContainerName" -Generation 2 -MemoryStartupByte $Memory -VHDPath $VHD.Path -Path "$RootPath\Containers"  Set-VM $VM -DynamicMemory -MemoryMaximumBytes $Memory -ProcessorCount $Processer  If($SwitchName -ne ""){    Get-VMNetworkAdapter $VM | Connect-VMNetworkAdapter –SwitchName $SwitchName  }  Set-VMFirmware $VM -EnableSecureBoot Off  Set-VMProcessor $VM -ExposeVirtualizationExtensions $True}
引数として、コンテナ名、親のコンテナイメージ名、メモリ、プロセッサ数、仮想スイッチ名を指定します。メモリは1024MB、プロセッサ数は1つを既定として設定しています(引数で変更可)。コンテナイメージ名からGet-ContainerImageコマンドでコンテナイメージのvhdxファイルパスを取得します。New-VHDコマンドでコンテナ用の差分仮想ハードディスクファイルを作成します。親ファイルはコンテナイメージのvhdx-ファイルとなります。New-VMコマンドで仮想マシンとしてコンテナを作成します。仮想スイッチ名が指定されている場合は、接続します。LinuxなどWindows以外のコンテナを想定して、セキュアブートをOFFにしておきます。Nested Hyper-Vとしてのコンテナ利用を想定して、Nestedを有効にしておきます。
コンテナの詳細取得
コンテナの詳細として、仮想マシンとしての名前と状態の情報に加え、仮想ハードディスクファイルのパスと差分仮想ハードディスクファイルの親ファイルのパスを表示しています。
Get-Container
Function Get-Container {  Get-VM | Where-Object {Test-Path ("$RootPath\Containers\" + $_.Name)} | Select `  @{Label="Name"; Expression={$_.Name}}, State, @{Label="Path"; Expression={((Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path)}}, `  @{Label="ParentPath"; Expression={(Get-VHD(Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path).ParentPath}}}
Get-VMコマンドで名前と状態を取得します。Get-VMHardDiskDriveコマンドで仮想ハードディスクファイルのパスを取得します。Get-VHDコマンドで差分仮想ハードディスクファイルの親ファイルのパスを取得します。
コンテナの起動
Start-Container
Function Start-Container([String]$ContainerName) {  Start-VM $ContainerName}
Start-VMコマンドを利用してコンテナを起動します。
コンテナの停止
Stop-Container
Function Stop-Container([String]$ContainerName) {  Stop-VM $ContainerName -Force}
Stop-VMコマンドを利用してコンテナを停止(シャットダウン)します。
コンテナの削除
コンテナの削除はコンテナイメージの削除とは異なり、仮想マシンとして作成されているコンテナを削除するとともに、仮想ハードディスクファイルや仮想マシンの構成ファイルなども削除する必要があります。
Remove-Container
Function Remove-Container([String]$ContainerName) {  $VM = Get-VM "$ContainerName"  If($VM.State -eq "Running"){    Stop-VM $VM -TurnOff  }  Remove-VM $VM -Force  Remove-Item "$RootPath\Containers\$ContainerName" -Recurse}
Get-VMコマンドで仮想マシンを取得します。もし仮想マシンが実行中の場合は、停止します。Remove-VMコマンドで仮想マシンを削除します。Hyper-V上からは削除されましたが、構成ファイルや仮想ハードディスクファイルが残っているので、フォルダごと削除します。
コンテナの作成~起動~IP設定
コンテナイメージ自体にIPの設定がされていない場合、コンテナは作成された時点では同様にIPの設定が行われておらず、ネットワークに接続できません。Hyper-V上の仮想マシンであるコンテナは、起動状態であれば外部からリモートパワーシェルやWMIを利用してIPの設定を行うことが可能です(現時点でUbunt16~uは除くCentOS7~は可)。コンテナ作成~起動~IP設定をひとまとめにしたコマンドがRun-Containerとなります。Run-Containerコマンド自体もいくつかのコマンドを組み合わせて作成しています。
手順としては、以下です。
コンテナの作成コンテナの起動コンテナのブート完了まで待機 IPアドレスの設定
1つずつ解説していきましょう。
Run-Container
Function Run-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName, [String]$IPAddress, [String]$Subnet, [String]$Gateway, [String]$DNS = @()){  New-Container -ContainerName "$ContainerName" -ImageName "$ImageName" -Memory $Memory -Processer $Processer -SwitchName "$SwitchName"  Start-Container "$ContainerName"  Wait-ContainerBoot "$ContainerName"  Set-ContainerIPConfig -ContainerName "$ContainerName" -IPAddress $IPAddress -Subnet $Subnet -Gateway $Gateway -DNS $DNS}
New-Containerコマンドに必要な引数に加えて、IPアドレス情報も引数として設定します。New-Containerコマンド(既出)でコンテナを作成します。Start-Containerコマンド(既出)でコンテナを起動します。Wait-ContainerBootコマンドでコンテナの起動~ブートが完了するまで待機します。IP設定はブートが完全に終了するまでできません。Set-ContainerIPConfigコマンドでIP設定を行います。
Wait-ContainerBoot
Function Wait-ContainerBoot([String]$ContainerName){  $Flg = $False  $TimeCount = 0  Do  {    If((Get-ContainerIPAddress $ContainerName) -ne ""){      $Flg = $True    }Else{      $TimeCount = $TimeCount + 1      If($TimeCount -eq 180){        $Flg = $True      }    }    Start-Sleep -s 1  }  While ($Flg -eq $False)}
ブート完了を監視するコンテナ名を引数で指定します。Do~Whileのループを抜けるためのフラグと、タイムアウトのカウンタを定義します。Get-ContainerIPAddressコマンド(後出)で、コンテナのIPアドレスが取得できるまでループします。ループしている間に1秒ごとにタイムアウトカウンタを追加して、180秒でタイムアウトします。フラグがTrueとなったらループを抜けます(=コンテナのIPが取得できた or タイムアウトした)。IP設定前であっても169.254.X.XというIPアドレスが自動的に割り当てられるので、このアドレスが取得できたらブート完了と判断しています。
Get-ContainerIPAddress
Function Get-ContainerIPAddress([String]$ContainerName){  $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService  $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'"  $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }  $NetworkAdapters = $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData')  $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0]  $NetworkSettings = @()  ForEach ($NetworkAdapter in $NetworkAdapters) {    If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {$NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")    }  }  If($NetworkSettings.Length -eq 0){    Return ""  }Else{    If( $NetworkSettings[0].IPAddresses.Length -eq 0){      Return ""    }Else{      Return $NetworkSettings[0].IPAddresses[0]    }  }}
コンテナ名を引数とします。Get-WmiObjectコマンドで、WMIのクラスを利用して起動中のコンテナのIP情報を取得します。WMIの利用方法は、ここでは細かい説明は省略します。Get-VMNetworkAdapterコマンドで取得したコンテナの、ネットワークアダプターのMacAddressと一致したWMIのネットワーク情報があった場合に、そのアダプタに設定されたIPアドレスを取得します。
Set-ContainerIPConfig
Function Set-ContainerIPConfig([String]$ContainerName, [String]$IPAddress = @(), [String]$Subnet = @(), [String]$Gateway = @(), [String]$DNS = @()){  $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService  $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'"  $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }  $NetworkAdapters =     $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData')  $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0]  $NetworkSettings = @()  ForEach ($NetworkAdapter in $NetworkAdapters) {    If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {$NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")    }  }  $NetworkSettings[0].DHCPEnabled = $False  $NetworkSettings[0].IPAddresses = $IPAddress  $NetworkSettings[0].Subnets = $Subnet  $NetworkSettings[0].DefaultGateways = $Gateway  $NetworkSettings[0].DNSServers = $DNS  $NetworkSettings[0].ProtocolIFType = 4096$ManagementService.SetGuestNetworkAdapterConfiguration($ComputerSystem.Path, $NetworkSettings.GetText(1)) | Out-Null#Write-Host "IP was configured."}
コンテナ名、IPアドレス、サブネットマスク、ゲートウェイアドレス、DNSアドレスを引数として設定します。WMIを利用して起動中のコンテナのIP設定を行います。Get-VMNetworkAdapterコマンドで取得したコンテナの、ネットワークアダプターのMacAddressと一致したWMIのネットワーク情報があった場合に、ネットワークの各設定を行います。ネットワークの各設定値は複数設定することが可能なので、配列に格納して割り当てる必要があります。
その他
ツリー表示
Get-ContainerImageやGet-Containerコマンドでは、一覧は参照できますが、その親子関係の把握が難しくなっています。そこで、簡易的ではありますが、ツリー構造をPowerShellで表示しようというおまけ的なコマンドを追加しました。
Get-TreeView
Function Get-TreeView() {  $Global:Tree = "`r`n"  $RootFiles = Get-ChildItem $RootPath\Images *.vhdx | Get-VHD | Where {$_.ParentPath -eq ""}  ForEach($File in $RootFiles){    Set-TreeView $File.Path 0  }  Write-Host $Global:Tree}
ルートフォルダ内でvhdxファイルを検索して、差分ハードディスクファイルとして親ファイルが無いもの=大元のイメージを抽出します。それぞれのイメージに対してSet-TreeVieコマンドで紐づいている子ファイルを抽出しています。
Set-TreeView
Function Set-TreeView([String]$ParentFile, [Int]$Level) {  $Files = Get-ChildItem -Path $RootPath -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq $ParentFile}  $BaseName = (Get-ChildItem $ParentFile).BaseName  If($BaseName -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"){    $Name = ($BaseName.Substring(0, $BaseName.Length - ($BaseName.Split("_")[$BaseName.Split("_").Length - 1].Length) - 1))  }Else{    $Name = $BaseName  }If((Get-ChildItem $ParentFile).FullName.ToUpper().StartsWith(("$RootPath\Images").ToUpper())){$Name = "[" + $Name + "]"}Else{$Name = " " + $Name}  $Space = ""  For ( $i = 0; $i -lt (($Level - 1) * 12); $i++ ){    $Space += " "  }  If($Level -ne 0){    $Space += " |"    For ( $i = 0; $i -lt (10 - $Nam.Length) ; $i++ ){      $Space += "-"    }  }  $Global:Tree += $Space + $Name + "`r`n`r`n"  If($Files.Count -eq 0){  }Else{     ForEach($File in $Files){       Set-TreeView $File.Path ($Level + 1)     }  }}
差分ハードディスクファイルとしての親ファイルパスと、階層レベルを引数として設定します。InvokeVのルートフォルダ以下で親ファイルを参照している子ファイルを、Get-ChildItemコマンドとGet-VHDコマンドを使って抽出します。BaseNameとして拡張子を除いたファイル名を抽出します。ファイル名にGUIDが含まれている場合は、名前部分だけを抽出します。GUIDが含まれていない場合は、そのままファイル名となります。InvokeVのイメージフォルダに対象ファイルがある場合は、イメージファイルと判断して名前に[ ]を追加します。コンテナファイルの場合は、名前の前にスペースを追加します。引数で設定されたレベルの分だけ階層の深さをあらわすために、スペースを追加して表示を整えます。引数で渡された子ファイルに対して、さらにSet-TreeViewコマンドを実行し、描画を参照ファイルがなくなるまで続けます。再度Set-TreeViewコマンドを呼び出す場合は、レベルを-1して引数とします。
Get-TreeViewの実行結果
PS C:\> Get-TreeView[centos_7] |---------- centos7_01[nanoiis] |---------- nanoiis_01 |----------[nanoiis_img02]       |---------- nanoiis_02       |---------- nanoiis_03       |----------[nanoiis_img03][ubuntu_16.04.1] |---------- ubuntu_01[win2016] |---------- win2016_01 |----------[win2016_hyper-v][win2016_core] |---------- win2016_core_01
親ファイルとの再接続
InvokeVコンテナは差分仮想ハードディスクを利用して実現しています。インポートやエクスポート、コンテナイメージやコンテナの削除を繰り返していくうちに、もし差分仮想ハードディスクの親子関係が壊れてしまった場合、vhdxファイルの親ファイルの属性を再設定することで、修復できる場合があります。万が一のためのコマンドとして作成しましたが、1つずつSet-VHDコマンドを実行することでも修復は可能です。ただし、親ファイルに何か大きな変更があった場合などは、再設定しても子ファイルが修復されない場合もあります。
Correct-ContainerImage
Function Correct-ContainerImage() {  $Images = Get-ChildItem -Path "$RootPath\Images" -Include "*.vhdx" -Recurse  ForEach($Image in $Images){    $ParentPath = (Get-VHD $Image).ParentPath    If ($ParentPath){      Set-VHD -Path $Image.FullName –ParentPath $ParentPath -IgnoreIdMismatchWrite-Host $Image.Name ">" (Get-ChildItem $ParentPath).Name    }  }}
イメージフォルダ以下のvhdxファイルを取得します。各イメージファイルの差分仮想ハードディスクファイルとして、属性で親ファイルがある場合、Set-VHDコマンドのIgnoreIdMismatchオプションスイッチを追加して強制的に親子関係を再設定します。再設定されたファイルを表示します。
以上が、InvokeVContainer.psm1モジュールコマンド一覧となります。GitHub(https://github.com/InvokeV/InvokeV-Container)にはこれらのコマンドの使い方サンプルSample.ps1が付属していますので参考にしていただけたらと思います。
InvokeV Container Manager
GitHub(https://github.com/InvokeV/InvokeV-Container)には、この他にも、「InvokeV Container Manger」(InvokeVContainerManger.exe)というツールがアップされています。これは、PowerShellベースのInvokeVコンテナをGUIツールで利用できるようにしたものです。コンテナの主要な操作はすべてこれまで解説してきたモジュールInvokeVContainer.psm1のコマンドを利用しています。基本的な操作はマウスの右クリックからとなっており、簡単に操作できるようになっています。実行した操作は全てコマンドウィンドウにPowerShellのコマンドがそのまま表示されるので、InvokeVコンテナのコマンドのサンプルとしても利用できます。
01 InvokeV Container Manger
以上、5回に渡って紹介しました「InvokeVコンテナ」、いかがだったでしょうか。紹介した内容を参考に、モジュールをカスタマイズして独自のコマンドを作成したり、これらのコマンドを組み合わせてInvokeVコンテナを自動的に展開、操作することができるようになります。InvokeVコンテナは、「より速く」「より小さく」「より使いやすく」をHyper-Vで実現する、新しい仮想マシンの使い方を提案します。