page hit counter
Welcome to MSDN Blogs Sign in | Join | Help

Taylor Brown's Blog

Test Lead for Windows Core OS Division on the Hyper-V Team.

Syndication

News

Welcome to the professional blog of Taylor Brown.  I am a test lead on the core virtualization team (Hyper-V) at Microsoft.

This blog will contain information about virtualization, Microsoft, Hyper-V, operating systems, testing, and whatever else I end up talking about...

 

Standard Microsoft Disclaimer:
"This posting is provided "AS IS" with no warranties, and confers no rights. You assume all risk for your use."

-Taylor Brown


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

clip_image001

Published Friday, September 19, 2008 11:16 AM by taylorb

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# funny wallpaper » Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD’s @ Friday, September 19, 2008 2:29 PM

PingBack from http://housesfunnywallpaper.cn/?p=6656

funny wallpaper » Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD’s

# re: Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD's @ Monday, September 22, 2008 9:48 AM

Hi,

After formatting and assigning a driver letter, can you then write some files to the VHD? If so, can a process running in the VM see these files once the VM boots up?

thanks,

Marc

 

========================================
Marc,
Yes after formatting the VHD you can write files, copy files what ever you want.  If you then unmount the VHD and attach it to a VM you can then see all of those files in the VM.

 -Taylor

Marc

# re: Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD's @ Thursday, October 02, 2008 11:22 AM

In combination with the VDS provider we are also trying to get the PowerShell Get-ChildItem call to access a GUID based volume, however, it seems to require some magic to reference.

Example:

Get-ChildItem -LiteralPath '\\?\Volume{18471658-8fbf-11dd-96d9-0019b9b4629f}\\'

It will return an illegal character in path, and that the location does not exists.

Any pointers on what is required to reference?

Thank you in advance.

===========================================================
I see the same issue - I have asked the Powershell team and I'll see if I can find a work around for you.

-Taylor

Chris

# re: Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD's @ Saturday, October 04, 2008 3:26 PM

Can you shed some lights on this maginc number?

1080832

which you deducted from the size for the volume. Why not the whole size?

I am working on a c++ version of loading a virtual disk, then partition and format it.

Thanks,

Huihong Luo

# re: Using The Virtual Disk Service (VDS) From Powershell to Mount and Use VHD's @ Tuesday, December 23, 2008 12:27 PM

Hey Taylor,

I was wondering if you ever worked with the volume BeginExtend method?

I'm trying to use the VDS provider through PowerShell to extend a volume (that happens to live in a Hyper-V guest) and I'm getting errors back from the function.

$Volume.BeginExtend($InputDisks, $null, $null)

Exception calling "BeginExtend" with "3" argument(s): "The call to IVdsVolume::Extend(...) failed."

At line:1 char:20

+ $Volume.BeginExtend( <<<< $InputDisks, $null, $null)

Thanks for any help you can provide.

Andy

==========================================================
Andy,
I have not - have you checked the event log?  How much are you trying to extend the partion by?  Try a smaller number - that's how I figure out the "majic number"

-Taylor

Andy Stumph

Leave a Comment

(required) 
required 
(required) 

  
Enter Code Here: Required
Page view tracker