Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD's
This is by far the longest post both in length and the time it took to figure it out I have ever done - this is also one of the most enjoyable posts/topics since the challenge was pretty high hope it helps.
A lot of people have asked me how they can reliably mount a VHD, online the disk, format and assign it a drive letter. Typically the answer has been to script diskpart – but that is far from elegant or reliable… The real answer is to call the Virtual Disk Service API’s, however those API’s are not scriptable and even calling them from C# is not easy. I originally tried to call the VDS API’s from Powershell by marshalling the VDS COM API’s – which while possible would have been a lot of work and really really ugly. Lucky I stumbled across the Microsoft.Storage.Vds.dll library – this seems to be a new library added as part of Server 2008 it's not documented or supported and subject to change I am sure (if you Google/Live Search for it you only get about 20 results). Using .Net Reflector you can see that it’s a near complete .Net wrapper for the VDS API’s. It doesn’t do a lot of abstraction so you still have to deal with the VDS API model which isn’t that pleasant – but here’s some help…
In future posts I will show how to mount VHD's that already have volumes and use mount points...
The End Game
1) Mount the VHD
2) Find the Virtual Disk Service Disk Object
3) Online the Disk
4) Create a New Volume
5) Format the Volume and Assign a Dive Letter
$mountedDisk = MountVhd "c:\disk.vhd" $vdsDisk = GetVDSDiskObjectFromMountedStorageImage $mountedDisk $vdsDiskPack = OnlineDisk $vdsDisk $vdsVolume = CreateVolume $vdsDisk FormatVolumeAndAssignDriveLetter $vdsVolume "Z" |
Mount The VHD
Note that I am using the ProcessWMIJob function from Hyper-V WMI: Rich Error Messages for Non-Zero ReturnValue (no more 32773, 32768, 32700…)
Using the Msvm_ImageManagmentService to mount a pre-created VHD File and then getting the associated Msvm_MountedStorageImage from the completed job.
function MountVhd { param( [String]$VhdPath ) $Msvm_ImgMgmtService = Get-WmiObject -Namespace "root\virtualization" -Class "Msvm_ImageManagementService" $diskMountJob = [WMI]($Msvm_ImgMgmtService.Mount($VhdPath) | ProcessWMIJob $Msvm_ImgMgmtService "Mount").Job return Get-WmiObject -Namespace "root\virtualization" -Query "Associators of {$diskMountJob} Where AssocClass=Msvm_AffectedStorageJobElement ResultClass=Msvm_MountedStorageImage" } |
Find the Virtual Disk Service Disk Object
This is the start of the magic... First we need to load the 'Microsoft.Storage.Vds' dll which is in the GAC on Server 2008 - I then make a string that matches the format of DiskAddress used by VDS, this is why we need the Msvm_MountedStorageImage. We now create a new ServiceLoader object and call the LoadService(...) API which returns a VdsService object, we need to wait for it to be ready. Now we can iterate over the UnallocatedDisks property to find the VHD we mounted earlier by matching the DiskAddress property.
function GetVDSDiskObjectFromMountedStorageImage { param( [WMI]$Msvm_MountedStorageImage ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $formatedDiskAddress = "Port" + $Msvm_MountedStorageImage.PortNumber + ` "Path" + $Msvm_MountedStorageImage.PathId + ` "Target" + $Msvm_MountedStorageImage.TargetId + ` "Lun" + $Msvm_MountedStorageImage.Lun $vdsServiceLoader = New-Object Microsoft.Storage.Vds.ServiceLoader $vdsService = $vdsServiceLoader.LoadService($null) $vdsService.WaitForServiceReady() $vdsService.UnallocatedDisks | foreach { $currentDisk = [Microsoft.Storage.Vds.Advanced.AdvancedDisk]$_ if ($currentDisk.DiskAddress -ilike $formatedDiskAddress) { return $currentDisk } } } |
Online the Disk and Initialize It
This function does a bit more than just online the disk - it also adds it to a Pack which is a VDS concept that most people don't and shouldn't need to ever know or worry about... Again we load the 'Microsoft.Storage.Vds' dll and load the service. Then we call the Online function to online the disk - the next part took me a while to figure out but it turns out that the disk is read only when it first comes on line so I have to clear the ReadOnly flag. The next line has the GUID for the Basic VDS software provider - there are two software providers on the system by default the Basic provider and the Dynamic provider, the dynamic provider is for things like USB/1394 disks so we want the Basic provider. Now I tell the VdsService I am only interested in SoftwareProvidors and then iterate over the providers until I find the Basic provider. I now call the CreatePack() API to create a new disk pack, and use the AddDisk(...) API to add and Initialize the Disk.
function OnlineDisk { param( $VdsAdvancedDisk ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $vdsServiceLoader = New-Object Microsoft.Storage.Vds.ServiceLoader $vdsService = $vdsServiceLoader.LoadService($null) $vdsService.WaitForServiceReady()
$vdsDisk.Online() $vdsDisk.ClearFlags([Microsoft.Storage.Vds.DiskFlags]::ReadOnly) $vdsBasicProvidorGuid = "ca7de14f-5bc8-48fd-93de-a19527b0459e" $vdsService.HardwareProvider = $false $vdsService.SoftwareProvider = $true $vdsService.Providers| foreach{ if ($vdsBasicProvidorGuid -ieq $_.Id) { $vdsDiskPack = $_.CreatePack() $vdsDiskPack.AddDisk($vdsDisk.Id, [Microsoft.Storage.Vds.PartitionStyle]::Mbr, $false) return $vdsDiskPack } } } |
Create a New Volume
We can now create a new volume on the disk... Again we load the 'Microsoft.Storage.Vds' dll - we don't need the service this time so no need to load it the object we are passed is assumed to be valid and thus a service is already loaded. We have to create an InputDisk object and set the ID and Size property - there is some goofiness with the Size you can't set it to the max size of the disk this is the value in bytes that worked with my 120GB VHD I am still looking into how this number gets calculated... The API actually takes an array of InputDisk objects (so you can create raid's and the like) since I only have one disk I create an array of one... We can now call the BeginCreateVolume(...) API with the VolumeType, array of InputDisk Objects, the StripSize (if a raid) and null for both the async callback and state object. We do need to wait for the creation to complete so we can call the EndCreateVolume(...) API to get our created volume.
function CreateVolume { param( $VdsAdvancedDisk ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $vdsInputDiskObject = New-Object Microsoft.Storage.Vds.InputDisk $vdsInputDiskObject.DiskId = $VdsAdvancedDisk.Id $vdsInputDiskObject.Size = ($VdsAdvancedDisk.Size - 1080832) $vdsInputDiskObjects = [Microsoft.Storage.Vds.InputDisk[]]@($vdsInputDiskObject) $volumeCreationEvent = $vdsDiskPack.BeginCreateVolume([Microsoft.Storage.Vds.VolumeType]::Simple, $vdsInputDiskObjects, [UInt32]0, $null, $null) while ($volumeCreationEvent.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100} return $vdsDiskPack.EndCreateVolume($volumeCreationEvent) } |
Format the Volume and Assign a Dive Letter
Finally we have an online, initialized disk with a volume on it... Now we just need to format it and assign a drive letter. We again load the 'Microsoft.Storage.Vds' dll and again we don't need to Service so we skip that. Now we call the BeginFormat(...) API with the file system, volume label, allocation size, true for force, true for quick format, false for compression and null for the callback and state objects. We again need to wait for the creation to complete. Now we call the EndFormat to complete the operation and assign a drive letter.
function FormatVolumeAndAssignDriveLetter { param( $VdsVolume, [String]$DriveLetter ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $formatCreationEvent = $VdsVolume.BeginFormat([Microsoft.Storage.Vds.FileSystemType]::Ntfs, "MyVolumeLabel", 512, $true, $true, $false, $null, $null) while ($formatCreationEvent.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100} $VdsVolume.EndFormat($formatCreationEvent) $VdsVolume.DriveLetter = [Char]::Parse($DriveLetter) } |
The Complete Script...
Here's the whole the final product... - ENJOY!
filter ProcessWMIJob { param ( [string]$WmiClassPath = $null, [string]$MethodName = $null ) $errorCode = 0
if ($_.ReturnValue -eq 4096) { $Job = [WMI]$_.Job
while ($Job.JobState -eq 4) { Write-Progress $Job.Caption "% Complete" -PercentComplete $Job.PercentComplete Start-Sleep -seconds 1 $Job.PSBase.Get() } if ($Job.JobState -ne 7) { if ($Job.ErrorDescription -ne "") { Write-Error $Job.ErrorDescription Throw $Job.ErrorDescription } else { $errorCode = $Job.ErrorCode } } Write-Progress $Job.Caption "Completed" -Completed $TRUE } elseif($_.ReturnValue -ne 0) { $errorCode = $_.ReturnValue } if ($errorCode -ne 0) { Write-Error "Hyper-V WMI Job Failed!" if ($WmiClassPath -and $MethodName) { $psWmiClass = [WmiClass]$WmiClassPath $psWmiClass.PSBase.Options.UseAmendedQualifiers = $TRUE $MethodQualifiers = $psWmiClass.PSBase.Methods[$MethodName].Qualifiers $indexOfError = [System.Array]::IndexOf($MethodQualifiers["ValueMap"].Value, [string]$errorCode) if ($indexOfError -ne "-1") { Throw "ReturnCode: ", $errorCode, " ErrorMessage: '", $MethodQualifiers["Values"].Value[$indexOfError], "' - when calling $MethodName" } else { Throw "ReturnCode: ", $errorCode, " ErrorMessage: 'MessageNotFound' - when calling $MethodName" } } else { Throw "ReturnCode: ", $errorCode, "When calling $MethodName - for rich error messages provide classpath and method name." } } return $_ }
function MountVhd { param( [String]$VhdPath ) $Msvm_ImgMgmtService = Get-WmiObject -Namespace "root\virtualization" -Class "Msvm_ImageManagementService" $diskMountJob = [WMI]($Msvm_ImgMgmtService.Mount($VhdPath) | ProcessWMIJob $Msvm_ImgMgmtService "Mount").Job return Get-WmiObject -Namespace "root\virtualization" -Query "Associators of {$diskMountJob} Where AssocClass=Msvm_AffectedStorageJobElement ResultClass=Msvm_MountedStorageImage" }
function GetVDSDiskObjectFromMountedStorageImage { param( [WMI]$Msvm_MountedStorageImage ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $formatedDiskAddress = "Port" + $Msvm_MountedStorageImage.PortNumber + ` "Path" + $Msvm_MountedStorageImage.PathId + ` "Target" + $Msvm_MountedStorageImage.TargetId + ` "Lun" + $Msvm_MountedStorageImage.Lun $vdsServiceLoader = New-Object Microsoft.Storage.Vds.ServiceLoader $vdsService = $vdsServiceLoader.LoadService($null) $vdsService.WaitForServiceReady()
$vdsService.UnallocatedDisks | foreach { $currentDisk = [Microsoft.Storage.Vds.Advanced.AdvancedDisk]$_ if ($currentDisk.DiskAddress -ilike $formatedDiskAddress) { return $currentDisk } } }
function OnlineDisk { param( $VdsAdvancedDisk ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $vdsDisk.Online() $vdsDisk.ClearFlags([Microsoft.Storage.Vds.DiskFlags]::ReadOnly) $vdsBasicProvidorGuid = "ca7de14f-5bc8-48fd-93de-a19527b0459e" $vdsServiceLoader = New-Object Microsoft.Storage.Vds.ServiceLoader $vdsService = $vdsServiceLoader.LoadService($null) $vdsService.WaitForServiceReady() $vdsService.HardwareProvider = $false $vdsService.SoftwareProvider = $true $vdsService.Providers| foreach{ if ($vdsBasicProvidorGuid -ieq $_.Id) { $vdsDiskPack = $_.CreatePack() $vdsDiskPack.AddDisk($vdsDisk.Id, [Microsoft.Storage.Vds.PartitionStyle]::Mbr, $false) return $vdsDiskPack } } }
function CreateVolume { param( $VdsAdvancedDisk ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $vdsInputDiskObject = New-Object Microsoft.Storage.Vds.InputDisk $vdsInputDiskObject.DiskId = $VdsAdvancedDisk.Id $vdsInputDiskObject.Size = ($VdsAdvancedDisk.Size - 1080832) $vdsInputDiskObjects = [Microsoft.Storage.Vds.InputDisk[]]@($vdsInputDiskObject)
$volumeCreationEvent = $vdsDiskPack.BeginCreateVolume([Microsoft.Storage.Vds.VolumeType]::Simple, $vdsInputDiskObjects, [UInt32]0, $null, $null) while ($volumeCreationEvent.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100} return $vdsDiskPack.EndCreateVolume($volumeCreationEvent) }
function FormatVolumeAndAssignDriveLetter { param( $VdsVolume, [String]$DriveLetter ) [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Storage.Vds") | Out-Null $formatCreationEvent = $VdsVolume.BeginFormat([Microsoft.Storage.Vds.FileSystemType]::Ntfs, "MyVolumeLabel", 512, $true, $true, $false, $null, $null) while ($formatCreationEvent.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100} $VdsVolume.EndFormat($formatCreationEvent)
$VdsVolume.DriveLetter = [Char]::Parse($DriveLetter) }
$mountedDisk = MountVhd "c:\disk.vhd" $vdsDisk = GetVDSDiskObjectFromMountedStorageImage $mountedDisk $vdsDiskPack = OnlineDisk $vdsDisk $vdsVolume = CreateVolume $vdsDisk FormatVolumeAndAssignDriveLetter $vdsVolume "Z" |
Taylor Brown
Hyper-V Integration Test Lead
http://blogs.msdn.com/taylorb
