diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e6a66..8615e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed minor markdown linting issues in README.md and used to issue BREAKING CHANGE commit - Fixes [Issue #282](https://github.com/dsccommunity/StorageDsc/issues/282). - Updated tag from 'Dev Drive' to 'DevDrive' in manifest file - Fixes [Issue #280](https://github.com/dsccommunity/StorageDsc/issues/280). +- Added DSC_VirtualHardDisk resource for creating virtual disks and tests - Fixes [Issue #277](https://github.com/dsccommunity/StorageDsc/issues/277) ### Changed diff --git a/README.md b/README.md index b8d6e2a..bca9bfe 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The **StorageDsc** module contains the following resources: disk drive (e.g. a CDROM or DVD drive). This resource ignores mounted ISOs. - **WaitForDisk** wait for a disk to become available. - **WaitForVolume** wait for a drive to be mounted and become available. +- **VirtualHardDisk** used to create and attach a virtual hard disk. This project has adopted [this code of conduct](CODE_OF_CONDUCT.md). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 312a423..fbbca69 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -27,7 +27,7 @@ stages: vmImage: 'windows-latest' steps: - pwsh: | - dotnet tool install --global GitVersion.Tool + dotnet tool install --global GitVersion.Tool --version 5.* $gitVersionObject = dotnet-gitversion | ConvertFrom-Json $gitVersionObject.PSObject.Properties.ForEach{ Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." diff --git a/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.psm1 b/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.psm1 new file mode 100644 index 0000000..551b763 --- /dev/null +++ b/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.psm1 @@ -0,0 +1,393 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +# Import the VirtualHardDisk Win32Helpers Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.VirtualHardDisk.Win32Helpers' ` + -ChildPath 'StorageDsc.VirtualHardDisk.Win32Helpers.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the virtual hard disk. + + .PARAMETER FilePath + Specifies the complete path to the virtual hard disk file. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilePath + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingVirtualHardDisk -f $FilePath) + ) -join '' ) + + $diskImage = Get-DiskImage -ImagePath $FilePath -ErrorAction SilentlyContinue + $ensure = 'Present' + + if (-not $diskImage) + { + $ensure = 'Absent' + } + + # Get the virtual hard disk info using its path on the system + return @{ + FilePath = $diskImage.ImagePath + Attached = $diskImage.Attached + DiskSize = $diskImage.Size + DiskNumber = $diskImage.DiskNumber + Ensure = $ensure + } +} # function Get-TargetResource + +<# + .SYNOPSIS + Returns the current state of the virtual hard disk. + + .PARAMETER FilePath + Specifies the complete path to the virtual hard disk file. + + .PARAMETER DiskSize + Specifies the size of new virtual hard disk. + + .PARAMETER DiskFormat + Specifies the supported virtual hard disk format. Currently only the vhd and vhdx formats are supported. + + .PARAMETER DiskType + Specifies the supported virtual hard disk type. + + .PARAMETER Ensure + Determines whether the setting should be applied or removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilePath, + + [Parameter()] + [ValidateScript({ $_ -gt 0 })] + [System.UInt64] + $DiskSize, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat, + + [Parameter()] + [ValidateSet('Fixed', 'Dynamic')] + [System.String] + $DiskType = 'Dynamic', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-ParametersValid -FilePath $FilePath -DiskSize $DiskSize -DiskFormat $DiskFormat + + try + { + Assert-ElevatedUser + } + catch + { + # Use a user friendly error message specific to the virtual disk dsc resource. + throw $script:localizedData.VirtualDiskAdminError + } + + $currentState = Get-TargetResource -FilePath $FilePath + + if ($Ensure -eq 'Present') + { + # Disk doesn't exist + if (-not $currentState.FilePath) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskDoesNotExistCreatingNow -f $FilePath) + ) -join '' ) + + $folderPath = Split-Path -Parent $FilePath + $wasLocationCreated = $false + + try + { + # Create the location if it doesn't exist. + if (-not (Test-Path -PathType Container $folderPath)) + { + New-Item -ItemType Directory -Path $folderPath + $wasLocationCreated = $true + } + + New-SimpleVirtualDisk -VirtualDiskPath $FilePath -DiskFormat $DiskFormat -DiskType $DiskType -DiskSizeInBytes $DiskSize + } + catch + { + # Remove file if we created it but were unable to attach it. No handles are open when this happens. + if (Test-Path -Path $FilePath -PathType Leaf) + { + Write-Verbose -Message ($script:localizedData.RemovingCreatedVirtualHardDiskFile -f $FilePath) + Remove-Item -LiteralPath $FilePath -Verbose -Force + } + + if ($wasLocationCreated) + { + Remove-Item -LiteralPath $folderPath -Verbose -Force + } + + # Rethrow the exception + throw + } + + } + elseif (-not $currentState.Attached) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualDiskNotMounted -f $FilePath) + ) -join '' ) + + # Virtual hard disk file exists so lets attempt to attach it to the system. + Add-SimpleVirtualDisk -VirtualDiskPath $FilePath -DiskFormat $DiskFormat + } + } + else + { + # Detach the virtual hard disk if its not supposed to be attached. + if ($currentState.Attached) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskDetachingImage ` + -f $FilePath) + ) -join '' ) + + Dismount-DiskImage -ImagePath $FilePath + } + } +} # function Set-TargetResource + +<# + .SYNOPSIS + Returns the current state of the virtual hard disk. + + .PARAMETER FilePath + Specifies the complete path to the virtual hard disk file. + + .PARAMETER DiskSize + Specifies the size of new virtual hard disk. + + .PARAMETER DiskFormat + Specifies the supported virtual hard disk format. Currently only the vhd and vhdx formats are supported. + + .PARAMETER DiskType + Specifies the supported virtual hard disk type. + + .PARAMETER Ensure + Determines whether the setting should be applied or removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilePath, + + [Parameter()] + [ValidateScript({ $_ -gt 0 })] + [System.UInt64] + $DiskSize, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat, + + [Parameter()] + [ValidateSet('Fixed', 'Dynamic')] + [System.String] + $DiskType = 'Dynamic', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-ParametersValid -FilePath $FilePath -DiskSize $DiskSize -DiskFormat $DiskFormat + + $currentState = Get-TargetResource -FilePath $FilePath + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingVirtualDiskExists -f $FilePath) + ) -join '' ) + + if ($Ensure -eq 'Present') + { + # Found the virtual hard disk and confirmed its attached to the system. + if ($currentState.Attached) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskCurrentlyMounted -f $FilePath) + ) -join '' ) + + return $true + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskMayNotExistOrNotMounted -f $FilePath) + ) -join '' ) + + return $false + } + else + { + # Found the virtual hard disk and confirmed its attached to the system but ensure variable set to 'Absent'. + if ($currentState.Attached) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskCurrentlyMountedButShouldNotBe -f $FilePath) + ) -join '' ) + + return $false + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VirtualHardDiskMayNotExistOrNotMounted -f $FilePath) + ) -join '' ) + + return $true + } +} # function Test-TargetResource + +<# + .SYNOPSIS + Validates parameters for both set and test operations. + + .PARAMETER FilePath + Specifies the complete path to the virtual hard disk file. + + .PARAMETER DiskSize + Specifies the size of new virtual hard disk. + + .PARAMETER DiskFormat + Specifies the supported virtual hard disk format. Currently only the vhd and vhdx formats are supported. +#> +function Assert-ParametersValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilePath, + + [Parameter(Mandatory = $true)] + [ValidateScript({ $_ -gt 0 })] + [System.UInt64] + $DiskSize, + + [Parameter(Mandatory = $true)] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat + ) + + # We'll only support local paths with drive letters. + if ($FilePath -notmatch '[a-zA-Z]:\\') + { + # AccessPath is invalid + New-InvalidArgumentException ` + -Message $($script:localizedData.VirtualHardDiskPathError -f $FilePath) ` + -ArgumentName 'FilePath' + } + + $extension = [System.IO.Path]::GetExtension($FilePath).TrimStart('.') + if ($extension) + { + if (($extension -ne 'vhd') -and ($extension -ne 'vhdx')) + { + New-InvalidArgumentException ` + -Message $($script:localizedData.VirtualHardDiskUnsupportedFileType -f $extension) ` + -ArgumentName 'FilePath' + } + elseif ($extension -ne $DiskFormat) + { + New-InvalidArgumentException ` + -Message $($script:localizedData.VirtualHardDiskExtensionAndFormatMismatchError -f $FilePath, $extension, $DiskFormat) ` + -ArgumentName 'FilePath' + } + } + else + { + New-InvalidArgumentException ` + -Message $($script:localizedData.VirtualHardDiskNoExtensionError -f $FilePath) ` + -ArgumentName 'FilePath' + } + + <# + Validate DiskFormat values. Minimum value for GPT is around ~10MB and the maximum value for + the vhd format in 2040GB. Maximum for the vhdx format is 64TB. + #> + $isVhdxFormat = $DiskFormat -eq 'Vhdx' + $isInValidSizeForVhdFormat = ($DiskSize -lt 10MB -bor $DiskSize -gt 2040GB) + $isInValidSizeForVhdxFormat = ($DiskSize -lt 10MB -bor $DiskSize -gt 64TB) + if ((-not $isVhdxFormat -and $isInValidSizeForVhdFormat) -bor + ($isVhdxFormat -and $isInValidSizeForVhdxFormat)) + { + if ($DiskSize -lt 1GB) + { + $diskSizeString = ($DiskSize / 1MB).ToString('0.00MB') + } + else + { + $diskSizeString = ($DiskSize / 1TB).ToString('0.00TB') + } + + $invalidSizeMsg = $script:localizedData.VhdFormatDiskSizeInvalid + if ($isVhdxFormat) + { + $invalidSizeMsg = $script:localizedData.VhdxFormatDiskSizeInvalid + } + + New-InvalidArgumentException ` + -Message $($invalidSizeMsg -f $diskSizeString) ` + -ArgumentName 'DiskSize' + } +} # Assert-ParametersValid + +Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.schema.mof b/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.schema.mof new file mode 100644 index 0000000..9e442fb --- /dev/null +++ b/source/DSCResources/DSC_VirtualHardDisk/DSC_VirtualHardDisk.schema.mof @@ -0,0 +1,12 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("VirtualHardDisk")] +class DSC_VirtualHardDisk : OMI_BaseResource +{ + [Key, Description("Specifies the full path to the virtual hard disk file that will be created and attached. This must include the extension, and the extension must match the disk format.")] String FilePath; + [Write, Description("Specifies the size of virtual hard disk to create if it doesn't exist and Ensure is present.")] Uint64 DiskSize; + [Write, Description("Specifies the disk type of virtual hard disk to create if it doesn't exist and Ensure is present."), ValueMap{"Fixed","Dynamic"}, Values{"Fixed","Dynamic"}] String DiskType; + [Write, Description("Specifies the disk format the virtual hard disk should use or create if it does not exist and Ensure is present. Defaults to Vhdx."), ValueMap{"Vhd","Vhdx"}, Values{"Vhd","Vhdx"}] String DiskFormat; + [Write, Description("Determines whether the virtual hard disk should be created and attached or should be detached if it exists."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Read, Description("Returns whether or not the virtual hard disk is mounted to the system.")] Boolean Attached; + [Read, Description("Returns the disk number of the virtual hard disk if it is mounted to the system.")] UInt32 DiskNumber; +}; diff --git a/source/DSCResources/DSC_VirtualHardDisk/README.md b/source/DSCResources/DSC_VirtualHardDisk/README.md new file mode 100644 index 0000000..c2d9e37 --- /dev/null +++ b/source/DSCResources/DSC_VirtualHardDisk/README.md @@ -0,0 +1,80 @@ +# Description + +The resource is used to create a virtual hard disk and attach it to the system. + +There are 2 high level scenarios, one where the user uses the 'Present' flag +and the second when the user uses the 'Absent' flag in the resource. + +1. When the 'Present' flag is used the resource checks if the virtual hard disk +is on the machine using the file path that is entered. + 1. If the parameters are valid but the file path does not exist. The + resource will attempt to create and attach the virtual hard disk to + the system. Note: only paths with drive letters are accepted e.g + `C:\Myfolder\newVirtFile.vhdx`, or `D:\newVirtFile.vhd`, etc. + 1. If the file path is confirmed to be the location of a real virtual hard + disk file, and it is attached to the system the resource will do nothing. + 1. If the file path is confirmed to be the location of a real virtual hard + disk file, but is not attached, the resource will attach the virtual hard + disk to the system. + 1. If the file path does exist but is not a real file path to a virtual + hard disk file, the resource will throw an error. +1. When the 'Absent' flag is used the resource checks if the virtual disk is on +the machine using the file path that is entered. + 1. If the file path is confirmed to be the location of a real virtual hard + disk file. The resource will check if the virtual hard disk is attached. + If it is attached, the resource will detach the virtual hard disk from + the system. + 1. If the location is a real virtual hard disk but the virtual hard disk + is detached from the system, the resource will do nothing. + 1. If the parameters are valid but the file path is a path to a virtual + hard disk file that doesn't exist, the resource will do nothing. + 1. The resource **does not** delete a virtual hard disk file, that + **already** exists on the system prior to the `Set-TargetResource` + function being called. It will only detach the virtual hard disk + from the system. The file will remain, this is to prevent accidental deletions. + +Note: If the file path to the `.vhd` or `.vhdx` file is real and exists but is +not attached. The resource will simply attach it. Even if the `DiskSize` and +`DiskType` parameters are different. These values are currently only useful for +creation. Additional functionality would need to be added to [resize](https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-resizevirtualdisk) +or change +the virtual disk's type after creation. + +See the [Limitations](#limitations) section for more limitations. + +## How does this differ from the [DSC_VHD](https://github.com/dsccommunity/HyperVDsc/tree/main/source/DSCResources/DSC_VHD) in the Hyper-V Dsc? + +This DSC_VirtualHardDisk resource does not rely on the Hyper-V Windows feature +being enabled to allow users to use the resource. Unlike the DSC_VHD, users can +use this resource right out of the box assuming they are running at least +`Windows 8.1 / Windows Server 2008 R2 or later`. The resource uses the publicly +available Win32 virtual disk apis, to create and attach a virtual hard disk file +(`.vhd` and `.vhdx`) to the system. See more information about the apis [here](https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/). + +Warning: Using both the DSC_VirtualHardDisk and DSC_VHD resources in the same +config/machine could result in an invalid config. + +## Limitations + +1. The resource only supports `.vhd` and `.vhdx` files. No other virtual hard disk +file extension is supported at this time. +1. The ability to `expand` the max size of the virtual hard disk after its creation is not currently included in this resource. +1. The ability to `shrink` the max size of the virtual hard disk after its creation +is not currently included in this resource. +1. The resource uses default values internally to create the virtual hard disk +file that are not provided in the Get\Test\Set Methods. Values such as: + 1. The ability to set the block size of the virtual hard disk in bytes. + 1. The ability to set the sector size of the virtual hard disk in bytes. + 1. The ability to associate the new virtual hard disk with an existing virtual + hard disk. + 1. The ability to specify a resiliency guid for the virtual hard disk. + 1. The ability to set a unique Id for the virtual hard disk and not use one + generated by the system. + 1. The ability to prepopulate a new virtual disk with data from an existing + virtual disk. + +See [virtdisk.h](https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/) +to read about what the Win32 api supports. This resource could in theory support +all of these, as the parameters just need to be passed to the the [CreateVirtualDisk](https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-createvirtualdisk) +Win32 function call in the resource. So additional help from the community if any +of these are wanted, is welcomed. diff --git a/source/DSCResources/DSC_VirtualHardDisk/en-US/DSC_VirtualHardDisk.strings.psd1 b/source/DSCResources/DSC_VirtualHardDisk/en-US/DSC_VirtualHardDisk.strings.psd1 new file mode 100644 index 0000000..77bf0a2 --- /dev/null +++ b/source/DSCResources/DSC_VirtualHardDisk/en-US/DSC_VirtualHardDisk.strings.psd1 @@ -0,0 +1,18 @@ +ConvertFrom-StringData @' + CheckingVirtualDiskExists = Checking virtual hard disk at location '{0}' exists and is mounted. + VirtualHardDiskMayNotExistOrNotMounted = The virtual hard disk at location '{0}' does not exist or is not mounted. + VirtualHardDiskDoesNotExistCreatingNow = The virtual hard disk at location '{0}' does not exist. Creating virtual hard disk now. + VirtualHardDiskCurrentlyMounted = The virtual hard disk at location '{0}' was found and is mounted to the system. + VirtualHardDiskCurrentlyMountedButShouldNotBe = The virtual hard disk at location '{0}' was found and is mounted to the system but it should not be. + VhdFormatDiskSizeInvalid = The virtual hard disk size '{0}' was invalid for the 'vhd' format. Min supported value is 10 Mb and max supported value 2040 Gb. + VhdxFormatDiskSizeInvalid = The virtual hard disk size '{0}' was invalid for the 'vhdx' format. Min supported value is 10 Mb and max supported value 64 Tb. + VirtualDiskNotMounted = The virtual hard disk at location '{0}' is not mounted. Mounting virtual hard disk now. + VirtualHardDiskDetachingImage = The virtual hard disk located at '{0}' is detaching. + VirtualHardDiskUnsupportedFileType = The file type .{0} is not supported. Only .vhd and .vhdx file types are supported. + VirtualHardDiskPathError = The path '{0}' must be a fully qualified path that starts with a Drive Letter. + VirtualHardDiskExtensionAndFormatMismatchError = The path you entered '{0}' has extension '{1}' but the disk format entered is '{2}'. Both the extension and format must match. + VirtualHardDiskNoExtensionError = The path '{0}' does not contain an extension. Supported extension types are '.vhd' and '.vhdx'. + GettingVirtualHardDisk = Getting virtual hard disk information for virtual hard disk located at '{0}. + RemovingCreatedVirtualHardDiskFile = The virtual hard disk file at location '{0}' is being removed due to an error while attempting to mount it to the system. + VirtualDiskAdminError = Creating and mounting a virtual disk requires running local Administrator permissions. Please ensure this resource is being applied with an account with local Administrator permissions. +'@ diff --git a/source/Examples/Resources/VirtualHardDisk/1-VirtualHardDisk_CreateFixedSizedVirtualDisk.ps1 b/source/Examples/Resources/VirtualHardDisk/1-VirtualHardDisk_CreateFixedSizedVirtualDisk.ps1 new file mode 100644 index 0000000..2535d73 --- /dev/null +++ b/source/Examples/Resources/VirtualHardDisk/1-VirtualHardDisk_CreateFixedSizedVirtualDisk.ps1 @@ -0,0 +1,53 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 3f629ab7-358f-4d82-8c0a-556e32514e3e +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/StorageDsc/blob/main/LICENSE +.PROJECTURI https://github.com/dsccommunity/StorageDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module StorageDsc + +<# + .DESCRIPTION + This configuration will create a fixed sized virtual disk that is 40Gb in size and will format a + NTFS volume named 'new volume' that uses the drive letter E. If the folder path in the FilePath + property does not exist, it will be created. +#> +Configuration VirtualHardDisk_CreateFixedSizedVirtualDisk +{ + Import-DSCResource -ModuleName StorageDsc + + Node localhost + { + # Create new virtual disk + VirtualHardDisk newVhd + { + FilePath = 'C:\myVhds\virtDisk1.vhd' + DiskSize = 40Gb + DiskFormat = 'Vhd' + DiskType = 'Fixed' + Ensure = 'Present' + } + + # Create new volume onto the new virtual disk + Disk Volume1 + { + DiskId = 'C:\myVhds\virtDisk1.vhd' + DiskIdType = 'Location' + DriveLetter = 'E' + FSLabel = 'new volume' + Size = 20Gb + DependsOn = '[VirtualHardDisk]newVhd' + } + } +} diff --git a/source/Examples/Resources/VirtualHardDisk/2-VirtualHardDisk_CreateDynamicallyExpandingVirtualDisk.ps1 b/source/Examples/Resources/VirtualHardDisk/2-VirtualHardDisk_CreateDynamicallyExpandingVirtualDisk.ps1 new file mode 100644 index 0000000..aa50908 --- /dev/null +++ b/source/Examples/Resources/VirtualHardDisk/2-VirtualHardDisk_CreateDynamicallyExpandingVirtualDisk.ps1 @@ -0,0 +1,54 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 56cbc9fc-4168-4662-9dec-12addcfb82da +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/StorageDsc/blob/main/LICENSE +.PROJECTURI https://github.com/dsccommunity/StorageDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module StorageDsc + +<# + .DESCRIPTION + This configuration will create a dynamically sized virtual hard disk that has a max size of 80Gb and will format a + RefS volume named 'new volume 2' that uses the drive letter F, onto the newly created virtual hard disk. If the + folder path in the FilePath property does not exist, it will be created. +#> +Configuration VirtualHardDisk_CreateDynamicallyExpandingVirtualDisk +{ + Import-DSCResource -ModuleName StorageDsc + + Node localhost + { + # Create new virtual disk + VirtualHardDisk newVhd2 + { + FilePath = 'C:\myVhds\virtDisk2.vhdx' + DiskSize = 80Gb + DiskFormat = 'Vhdx' + DiskType = 'Dynamic' + Ensure = 'Present' + } + + # Create new volume onto the new virtual disk + Disk Volume1 + { + DiskId = 'C:\myVhds\virtDisk2.vhdx' + DiskIdType = 'Location' + DriveLetter = 'F' + FSLabel = 'new volume 2' + FSFormat = 'ReFS' + Size = 20Gb + DependsOn = '[VirtualHardDisk]newVhd2' + } + } +} diff --git a/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 b/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 index 718f353..5ae97c1 100644 --- a/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 +++ b/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 @@ -160,7 +160,7 @@ function Get-DiskByIdentifier $DiskId, [Parameter()] - [ValidateSet('Number','UniqueId','Guid','Location','FriendlyName','SerialNumber')] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location', 'FriendlyName', 'SerialNumber')] [System.String] $DiskIdType = 'Number' ) @@ -182,7 +182,7 @@ function Get-DiskByIdentifier default # for filters requiring Where-Object { $disk = Get-Disk -ErrorAction SilentlyContinue | - Where-Object -Property $DiskIdType -EQ $DiskId + Where-Object -Property $DiskIdType -EQ $DiskId } } @@ -233,7 +233,7 @@ function Get-DevDriveWin32HelperScript param () - $DevDriveHelperDefinitions = @' + $DevDriveHelperDefinitions = @' // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-developer_drive_enablement_state public enum DEVELOPER_DRIVE_ENABLEMENT_STATE @@ -385,8 +385,8 @@ function Get-DevDriveWin32HelperScript -Name 'DevDriveHelper' ` -MemberDefinition $DevDriveHelperDefinitions ` -UsingNamespace ` - 'System.ComponentModel', - 'Microsoft.Win32.SafeHandles' + 'System.ComponentModel', + 'Microsoft.Win32.SafeHandles' } return $script:DevDriveWin32Helper diff --git a/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/StorageDsc.VirtualHardDisk.Win32Helpers.psm1 b/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/StorageDsc.VirtualHardDisk.Win32Helpers.psm1 new file mode 100644 index 0000000..245d619 --- /dev/null +++ b/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/StorageDsc.VirtualHardDisk.Win32Helpers.psm1 @@ -0,0 +1,684 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns C# code that will be used to call Dev Drive related Win32 apis +#> +function Get-VirtDiskWin32HelperScript +{ + [CmdletBinding()] + [OutputType([System.Void])] + param + () + + $virtDiskDefinitions = @' + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-virtual_storage_type + [StructLayout(LayoutKind.Sequential)] + public struct VIRTUAL_STORAGE_TYPE + { + public UInt32 DeviceId; + public Guid VendorId; + } + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-create_virtual_disk_parameters + [StructLayout(LayoutKind.Sequential)] + public struct CREATE_VIRTUAL_DISK_PARAMETERS + { + public UInt32 Version; + public Guid UniqueId; + public UInt64 MaximumSize; + public UInt32 BlockSizeInBytes; + public UInt32 SectorSizeInBytes; + [MarshalAs(UnmanagedType.LPWStr)] + public string ParentPath; + [MarshalAs(UnmanagedType.LPWStr)] + public string SourcePath; + } + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-attach_virtual_disk_parameters + [StructLayout(LayoutKind.Sequential)] + public struct ATTACH_VIRTUAL_DISK_PARAMETERS + { + public UInt32 Version; + public UInt32 Reserved; + } + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-open_virtual_disk_parameters + [StructLayout(LayoutKind.Sequential)] + public struct OPEN_VIRTUAL_DISK_PARAMETERS + { + public UInt32 Version; + public UInt32 RWDepth; + } + + // Define structures and constants for creating a virtual disk. + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-create_virtual_disk_version + public static uint CREATE_VIRTUAL_DISK_VERSION_2 = 2; + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-virtual_disk_access_mask-r1 + public static uint VIRTUAL_DISK_ACCESS_NONE = 0; + public static uint VIRTUAL_DISK_ACCESS_ALL = 0x003f0000; + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-create_virtual_disk_flag + public static uint CREATE_VIRTUAL_DISK_FLAG_NONE = 0x0; + public static uint CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION = 0x1; + + // Define structures and constants for attaching a virtual disk. + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-attach_virtual_disk_flag + public static uint ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME = 0x00000004; + public static uint ATTACH_VIRTUAL_DISK_FLAG_AT_BOOT = 0x00000400; + + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-attach_virtual_disk_version + public static uint ATTACH_VIRTUAL_DISK_VERSION_1 = 1; + + // Define structures and constants for opening a virtual disk. + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-open_virtual_disk_version + public static uint OPEN_VIRTUAL_DISK_VERSION_1 = 1; + + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ne-virtdisk-open_virtual_disk_flag + public static uint OPEN_VIRTUAL_DISK_FLAG_NONE = 0x0; + + // Constants found in virtdisk.h + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/ns-virtdisk-virtual_storage_type + public static uint VIRTUAL_STORAGE_TYPE_DEVICE_VHD = 2U; + public static uint VIRTUAL_STORAGE_TYPE_DEVICE_VHDX = 3U; + public static Guid VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT = new Guid(0xEC984AEC, 0xA0F9, 0x47E9, 0x90, 0x1F, 0x71, 0x41, 0x5A, 0x66, 0x34, 0x5B); + + // Declare method to create a virtual disk + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-createvirtualdisk + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern Int32 CreateVirtualDisk( + ref VIRTUAL_STORAGE_TYPE VirtualStorageType, + string Path, + UInt32 VirtualDiskAccessMask, + IntPtr SecurityDescriptor, + UInt32 Flags, + UInt32 ProviderSpecificFlags, + ref CREATE_VIRTUAL_DISK_PARAMETERS Parameters, + IntPtr Overlapped, + out SafeFileHandle Handle + ); + + // Declare method to attach a virtual disk + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-attachvirtualdisk + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern Int32 AttachVirtualDisk( + SafeFileHandle VirtualDiskHandle, + IntPtr SecurityDescriptor, + UInt32 Flags, + UInt32 ProviderSpecificFlags, + ref ATTACH_VIRTUAL_DISK_PARAMETERS Parameters, + IntPtr Overlapped + ); + + // Declare function to open a handle to a virtual disk + // https://learn.microsoft.com/en-us/windows/win32/api/virtdisk/nf-virtdisk-openvirtualdisk + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern Int32 OpenVirtualDisk( + ref VIRTUAL_STORAGE_TYPE VirtualStorageType, + string Path, + UInt32 VirtualDiskAccessMask, + UInt32 Flags, + ref OPEN_VIRTUAL_DISK_PARAMETERS Parameters, + out SafeFileHandle Handle + ); +'@ + if (([System.Management.Automation.PSTypeName]'VirtDisk.Helper').Type) + { + $script:VirtDiskHelper = ([System.Management.Automation.PSTypeName]'VirtDisk.Helper').Type + } + else + { + $script:VirtDiskHelper = Add-Type ` + -Namespace 'VirtDisk' ` + -Name 'Helper' ` + -MemberDefinition $virtDiskDefinitions ` + -UsingNamespace ` + 'System.ComponentModel', + 'Microsoft.Win32.SafeHandles' + } + + return $script:VirtDiskHelper +} # end function Get-VirtDiskWin32HelperScript + +<# + .SYNOPSIS + Creates a new virtual disk. + + .DESCRIPTION + Calls the CreateVirtualDisk Win32 api to create a new virtual disk. + This is used so we can mock this call easier. + + .PARAMETER VirtualStorageType + Specifies the type and provider (vendor) of the virtual storage device. + + .PARAMETER VirtualDiskPath + Specifies the whole path to the virtual disk file. + + .PARAMETER AccessMask + Specifies the bitmask for specifying access rights to a virtual hard disk. + + .PARAMETER SecurityDescriptor + Specifies the security information associated with the virtual disk. + + .PARAMETER Flags + Specifies creation flags for the virtual disk. + + .PARAMETER ProviderSpecificFlags + Specifies flags specific to the type of virtual disk being created. + + .PARAMETER CreateVirtualDiskParameters + Specifies the virtual hard disk creation parameters, providing control over, + and information about, the newly created virtual disk. + + .PARAMETER Overlapped + Specifies the reference to an overlapped structure for asynchronous calls. + + .PARAMETER Handle + Specifies the reference to handle object that represents the newly created virtual disk. +#> +function New-VirtualDiskUsingWin32 +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [ref] + $VirtualStorageType, + + [Parameter(Mandatory = $true)] + [ValidateScript({ -not (Test-Path $_) })] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [UInt32] + $AccessMask, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $SecurityDescriptor, + + [Parameter(Mandatory = $true)] + [UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $ProviderSpecificFlags, + + [Parameter(Mandatory = $true)] + [ref] + $CreateVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $Overlapped, + + [Parameter(Mandatory = $true)] + [ref] + $Handle + ) + + $helper = Get-VirtDiskWin32HelperScript + + return $helper::CreateVirtualDisk( + $virtualStorageType, + $VirtualDiskPath, + $AccessMask, + $SecurityDescriptor, + $Flags, + $ProviderSpecificFlags, + $CreateVirtualDiskParameters, + $Overlapped, + $Handle) +} # end function New-VirtualDiskUsingWin32 + +<# + .SYNOPSIS + Mounts an existing virtual disk to the system. + + .DESCRIPTION + Calls the AttachVirtualDisk Win32 api to mount an existing virtual disk + to the system. This is used so we can mock this call easier. + + .PARAMETER Handle + Specifies the reference to a handle to a virtual disk file. + + .PARAMETER SecurityDescriptor + Specifies the security information associated with the virtual disk. + + .PARAMETER Flags + Specifies attachment flags for the virtual disk. + + .PARAMETER ProviderSpecificFlags + Specifies flags specific to the type of virtual disk being created. + + .PARAMETER AttachVirtualDiskParameters + Specifies the virtual hard disk attach request parameters. + + .PARAMETER Overlapped + Specifies the reference to an overlapped structure for asynchronous calls. + +#> +function Add-VirtualDiskUsingWin32 +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [ref] + $Handle, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $SecurityDescriptor, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [System.Int32] + $ProviderSpecificFlags, + + [Parameter(Mandatory = $true)] + [ref] + $AttachVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $Overlapped + ) + + $helper = Get-VirtDiskWin32HelperScript + + return $helper::AttachVirtualDisk( + $Handle.Value, + $SecurityDescriptor, + $Flags, + $ProviderSpecificFlags, + $AttachVirtualDiskParameters, + $Overlapped) +} # end function Add-VirtualDiskUsingWin32 + +<# + .SYNOPSIS + Opens an existing virtual disk. + + .DESCRIPTION + Calls the OpenVirtualDisk Win32 api to open an existing virtual disk. + This is used so we can mock this call easier. + + .PARAMETER VirtualStorageType + Specifies the type and provider (vendor) of the virtual storage device. + + .PARAMETER VirtualDiskPath + Specifies the whole path to the virtual disk file. + + .PARAMETER AccessMask + Specifies the bitmask for specifying access rights to a virtual hard disk. + + .PARAMETER Flags + Specifies Open virtual disk flags for the virtual disk. + + .PARAMETER CreateVirtualDiskParameters + Specifies the virtual hard disk open request parameters. + + .PARAMETER Handle + Specifies the reference to handle object that represents the a virtual disk file. +#> +function Get-VirtualDiskUsingWin32 +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [ref] + $VirtualStorageType, + + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path $_ })] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $AccessMask, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [ref] + $OpenVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [ref] + $Handle + ) + + $helper = Get-VirtDiskWin32HelperScript + + return $helper::OpenVirtualDisk( + $VirtualStorageType, + $VirtualDiskPath, + $AccessMask, + $Flags, + $OpenVirtualDiskParameters, + $Handle) +} # end function Get-VirtualDiskUsingWin32 + +<# + .SYNOPSIS + Creates and mounts a virtual disk to the system. + + .PARAMETER VirtualDiskPath + Specifies the whole path to the virtual disk file. + + .PARAMETER DiskSizeInBytes + Specifies the size of new virtual disk in bytes. + + .PARAMETER DiskFormat + Specifies the supported virtual disk format. + + .PARAMETER DiskType + Specifies the supported virtual disk type. +#> +function New-SimpleVirtualDisk +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [System.UInt64] + $DiskSizeInBytes, + + [Parameter(Mandatory = $true)] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat, + + [Parameter(Mandatory = $true)] + [ValidateSet('Fixed', 'Dynamic')] + [System.String] + $DiskType + ) + + Write-Verbose -Message ($script:localizedData.CreatingVirtualDiskMessage -f $VirtualDiskPath) + $vDiskHelper = Get-VirtDiskWin32HelperScript + + # Get parameters for CreateVirtualDisk function + [ref]$virtualStorageType = Get-VirtualStorageType -DiskFormat $DiskFormat + [ref]$createVirtualDiskParameters = New-Object -TypeName VirtDisk.Helper+CREATE_VIRTUAL_DISK_PARAMETERS + $createVirtualDiskParameters.Value.Version = [VirtDisk.Helper]::CREATE_VIRTUAL_DISK_VERSION_2 + $createVirtualDiskParameters.Value.MaximumSize = $DiskSizeInBytes + $securityDescriptor = [System.IntPtr]::Zero + $accessMask = [VirtDisk.Helper]::VIRTUAL_DISK_ACCESS_NONE + $providerSpecificFlags = 0 + + # Handle to the new virtual disk + [ref]$handle = [Microsoft.Win32.SafeHandles.SafeFileHandle]::Zero + + # Virtual disk will be dynamically expanding, up to the size of $DiskSizeInBytes on the parent disk + $flags = [VirtDisk.Helper]::CREATE_VIRTUAL_DISK_FLAG_NONE + + if ($DiskType -eq 'Fixed') + { + # Virtual disk will be fixed, and will take up the up the full size of $DiskSizeInBytes on the parent disk after creation + $flags = [VirtDisk.Helper]::CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION + } + + try + { + # create the virtual disk + $result = New-VirtualDiskUsingWin32 ` + -VirtualStorageType $virtualStorageType ` + -VirtualDiskPath $VirtualDiskPath ` + -AccessMask $accessMask ` + -SecurityDescriptor $securityDescriptor ` + -Flags $flags ` + -ProviderSpecificFlags $providerSpecificFlags ` + -CreateVirtualDiskParameters $createVirtualDiskParameters ` + -Overlapped ([System.IntPtr]::Zero) ` + -Handle $handle + + if ($result -ne 0) + { + $win32Error = [System.ComponentModel.Win32Exception]::new($result) + throw [System.Exception]::new( ` + ($script:localizedData.CreateVirtualDiskError -f $VirtualDiskPath, $win32Error.Message), ` + $win32Error) + } + + Write-Verbose -Message ($script:localizedData.VirtualDiskCreatedSuccessfully -f $VirtualDiskPath) + + # Mount the newly created virtual disk + Add-SimpleVirtualDisk ` + -VirtualDiskPath $VirtualDiskPath ` + -DiskFormat $DiskFormat ` + -Handle $handle + } + finally + { + # Close handle + if ($handle.Value) + { + $handle.Value.Close() + } + } +} # function New-SimpleVirtualDisk + +<# + .SYNOPSIS + Mounts a virtual disk to the system. + + .PARAMETER VirtualDiskPath + Specifies the whole path to the virtual disk file. + + .PARAMETER DiskFormat + Specifies the supported virtual disk format. + + .PARAMETER Handle + Specifies a reference to a win32 handle that points to a virtual disk +#> +function Add-SimpleVirtualDisk +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat, + + [Parameter()] + [ref] + $Handle + ) + try + { + Write-Verbose -Message ($script:localizedData.MountingVirtualDiskMessage -f $VirtualDiskPath) + + $vDiskHelper = Get-VirtDiskWin32HelperScript + + # No handle passed in so we need to open the virtual disk first using $virtualDiskPath to get the handle. + if ($null -eq $Handle) + { + $Handle = Get-VirtualDiskHandle -VirtualDiskPath $VirtualDiskPath -DiskFormat $DiskFormat + } + + # Build parameters for AttachVirtualDisk function. + [ref]$attachVirtualDiskParameters = New-Object -TypeName VirtDisk.Helper+ATTACH_VIRTUAL_DISK_PARAMETERS + $attachVirtualDiskParameters.Value.Version = [VirtDisk.Helper]::ATTACH_VIRTUAL_DISK_VERSION_1 + $securityDescriptor = [System.IntPtr]::Zero + $providerSpecificFlags = 0 + $result = 0 + + <# + Some builds of Windows may not have the ATTACH_VIRTUAL_DISK_FLAG_AT_BOOT flag. So we attempt to mount the virtual + disk with the flag first. If this fails we mount the virtual disk without the flag. The flag allows the + virtual disk to be auto-mounted by the system at boot time. + #> + $combinedFlags = [VirtDisk.Helper]::ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME -bor [VirtDisk.Helper]::ATTACH_VIRTUAL_DISK_FLAG_AT_BOOT + $attemptFlagValues = @($combinedFlags, [VirtDisk.Helper]::ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME) + + foreach ($flags in $attemptFlagValues) + { + $result = Add-VirtualDiskUsingWin32 ` + -Handle $Handle ` + -SecurityDescriptor $securityDescriptor ` + -Flags $flags ` + -ProviderSpecificFlags $providerSpecificFlags ` + -AttachVirtualDiskParameters $attachVirtualDiskParameters ` + -Overlapped ([System.IntPtr]::Zero) + + if ($result -eq 0) + { + break + } + } + + if ($result -ne 0) + { + $win32Error = [System.ComponentModel.Win32Exception]::new($result) + throw [System.Exception]::new( ` + ($script:localizedData.MountVirtualDiskError -f $VirtualDiskPath, $win32Error.Message), ` + $win32Error) + } + + Write-Verbose -Message ($script:localizedData.VirtualDiskMountedSuccessfully -f $VirtualDiskPath) + } + finally + { + # Close handle + if ($handle.Value) + { + $handle.Value.Close() + } + } + +} # function Add-SimpleVirtualDisk + +<# + .SYNOPSIS + Opens a handle to a virtual disk on the system. + + .PARAMETER VirtualDiskPath + Specifies the whole path to the virtual disk file. + + .PARAMETER DiskFormat + Specifies the supported virtual disk format. +#> +function Get-VirtualDiskHandle +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat + ) + + Write-Verbose -Message ($script:localizedData.OpeningVirtualBeforeMountingMessage) + $vDiskHelper = Get-VirtDiskWin32HelperScript + + # Get parameters for OpenVirtualDisk function. + [ref]$virtualStorageType = Get-VirtualStorageType -DiskFormat $DiskFormat + [ref]$openVirtualDiskParameters = New-Object -TypeName VirtDisk.Helper+OPEN_VIRTUAL_DISK_PARAMETERS + $openVirtualDiskParameters.Value.Version = [VirtDisk.Helper]::OPEN_VIRTUAL_DISK_VERSION_1 + $accessMask = [VirtDisk.Helper]::VIRTUAL_DISK_ACCESS_ALL + $flags = [VirtDisk.Helper]::OPEN_VIRTUAL_DISK_FLAG_NONE + + # Handle to the virtual disk. + [ref]$handle = [Microsoft.Win32.SafeHandles.SafeFileHandle]::Zero + + $result = Get-VirtualDiskUsingWin32 ` + -VirtualStorageType $virtualStorageType ` + -VirtualDiskPath $VirtualDiskPath ` + -AccessMask $accessMask ` + -Flags $flags ` + -OpenVirtualDiskParameters $openVirtualDiskParameters ` + -Handle $handle + + if ($result -ne 0) + { + $win32Error = [System.ComponentModel.Win32Exception]::new($result) + throw [System.Exception]::new( ` + ($script:localizedData.OpenVirtualDiskError -f $VirtualDiskPath, $win32Error.Message), ` + $win32Error) + } + + Write-Verbose -Message ($script:localizedData.VirtualDiskOpenedSuccessfully -f $VirtualDiskPath) + + return $handle +} # function Get-VirtualDiskHandle + +<# + .SYNOPSIS + Gets the storage type based on the disk format. + + .PARAMETER DiskFormat + Specifies the supported virtual disk format. +#> +function Get-VirtualStorageType +{ + [CmdletBinding()] + [OutputType([VirtDisk.Helper+VIRTUAL_STORAGE_TYPE])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Vhd', 'Vhdx')] + [System.String] + $DiskFormat + ) + + # Create VIRTUAL_STORAGE_TYPE structure. + $virtualStorageType = New-Object -TypeName VirtDisk.Helper+VIRTUAL_STORAGE_TYPE + + # Default to the vhdx file format. + $virtualStorageType.VendorId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT + $virtualStorageType.DeviceId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_DEVICE_VHDX + + if ($DiskFormat -eq 'Vhd') + { + $virtualStorageType.DeviceId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_DEVICE_VHD + } + + return $virtualStorageType +} # function Get-VirtualStorageType + +Export-ModuleMember -Function @( + 'New-SimpleVirtualDisk', + 'Add-SimpleVirtualDisk', + 'Get-VirtualDiskHandle', + 'Get-VirtualStorageType', + 'Get-VirtDiskWin32HelperScript', + 'New-VirtualDiskUsingWin32', + 'Add-VirtualDiskUsingWin32', + 'Get-VirtualDiskUsingWin32' +) diff --git a/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/en-US/StorageDsc.VirtualHardDisk.Win32Helpers.strings.psd1 b/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/en-US/StorageDsc.VirtualHardDisk.Win32Helpers.strings.psd1 new file mode 100644 index 0000000..5df5109 --- /dev/null +++ b/source/Modules/StorageDsc.VirtualHardDisk.Win32Helpers/en-US/StorageDsc.VirtualHardDisk.Win32Helpers.strings.psd1 @@ -0,0 +1,11 @@ +ConvertFrom-StringData @' + CreatingVirtualDiskMessage = Creating virtual hard disk at location '{0}'. + CreateVirtualDiskError = Unable to create virtual hard disk at location '{0}' due to error '{1}'. + VirtualDiskCreatedSuccessfully = Virtual hard disk created successfully at location: '{0}'. + MountingVirtualDiskMessage = Mounting virtual hard disk at location '{0}'. + MountVirtualDiskError = Unable to mount virtual hard disk at location '{0}' due to error '{1}'. + VirtualDiskMountedSuccessfully = Virtual hard disk mounted successfully at location '{0}'. + OpeningVirtualBeforeMountingMessage = Attempting to open handle to virtual hard disk at location '{0}. + OpenVirtualDiskError = Unable to open virtual hard disk handle for location '{0}' due to error '{1}'. + VirtualDiskOpenedSuccessfully = Virtual hard disk handle for location '{0}' opened successfully. +'@ diff --git a/source/StorageDsc.psd1 b/source/StorageDsc.psd1 index d6d1edb..4a0bbfd 100644 --- a/source/StorageDsc.psd1 +++ b/source/StorageDsc.psd1 @@ -42,7 +42,8 @@ 'OpticalDiskDriveLetter', 'WaitForDisk', 'WaitForVolume', - 'Disk' + 'Disk', + 'VirtualHardDisk' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. @@ -52,7 +53,7 @@ Prerelease = '' # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Disk', 'Storage', 'Partition', 'Volume', 'DevDrive') + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Disk', 'Storage', 'Partition', 'Volume', 'DevDrive', 'VirtualHardDisk', 'VHD') # A URL to the license for this module. LicenseUri = 'https://github.com/dsccommunity/StorageDsc/blob/main/LICENSE' diff --git a/tests/Integration/DSC_VirtualHardDisk.Integration.Tests.ps1 b/tests/Integration/DSC_VirtualHardDisk.Integration.Tests.ps1 new file mode 100644 index 0000000..23c7af4 --- /dev/null +++ b/tests/Integration/DSC_VirtualHardDisk.Integration.Tests.ps1 @@ -0,0 +1,143 @@ +$script:dscModuleName = 'StorageDsc' +$script:dscResourceName = 'DSC_VirtualHardDisk' + +try +{ + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' +} +catch [System.IO.FileNotFoundException] +{ + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' +} + +$script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + +Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile -Verbose -ErrorAction Stop + + Describe "$($script:dscResourceName)_CreateAndAttachFixedVhd_Integration" { + Context 'Create and attach a fixed virtual disk' { + $configData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + FilePath = "$($pwd.drive.name):\newTestFixedVhd.vhd" + Attached = $true + DiskSize = 5GB + DiskFormat = 'Vhd' + DiskType = 'Fixed' + Ensure = 'Present' + } + ) + } + + It 'Should compile the MOF without throwing' { + { + & "$($script:dscResourceName)_CreateAndAttachFixedVhd_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $configData + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $currentState = Get-DscConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq "$($script:dscResourceName)_CreateAndAttachFixedVhd_Config" + } + $currentState.FilePath | Should -Be $configData.AllNodes.FilePath + $currentState.DiskSize | Should -Be $configData.AllNodes.DiskSize + $currentState.Attached | Should -Be $configData.AllNodes.Attached + $currentState.Ensure | Should -Be $configData.AllNodes.Ensure + } + + AfterAll { + Dismount-DiskImage -ImagePath $TestFixedVirtualHardDiskVhdPath -StorageType VHD + Remove-Item -Path $TestFixedVirtualHardDiskVhdPath -Force + } + } + } + + Describe "$($script:dscResourceName)_CreateAndAttachDynamicallyExpandingVhdx_Integration" { + Context 'Create and attach a dynamically expanding virtual disk' { + $configData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + FilePath = "$($pwd.drive.name):\newTestDynamicVhdx.vhdx" + Attached = $true + DiskSize = 10GB + DiskFormat = 'Vhdx' + DiskType = 'Dynamic' + Ensure = 'Present' + } + ) + } + + It 'Should compile the MOF without throwing' { + { + & "$($script:dscResourceName)_CreateAndAttachDynamicallyExpandingVhdx_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $configData + } | Should -Not -Throw + } + + It 'Should apply the MOF without throwing' { + { + Start-DscConfiguration ` + -Path $TestDrive ` + -ComputerName localhost ` + -Wait ` + -Verbose ` + -Force ` + -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $currentState = Get-DscConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq "$($script:dscResourceName)_CreateAndAttachDynamicallyExpandingVhdx_Config" + } + $currentState.FilePath | Should -Be $configData.AllNodes.FilePath + $currentState.DiskSize | Should -Be $configData.AllNodes.DiskSize + $currentState.Attached | Should -Be $configData.AllNodes.Attached + $currentState.Ensure | Should -Be $configData.AllNodes.Ensure + } + + AfterAll { + Dismount-DiskImage -ImagePath $TestDynamicVirtualHardDiskVhdx -StorageType VHDX + Remove-Item -Path $TestDynamicVirtualHardDiskVhdx -Force + } + } + } +} +finally +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/tests/Integration/DSC_VirtualHardDisk.config.ps1 b/tests/Integration/DSC_VirtualHardDisk.config.ps1 new file mode 100644 index 0000000..2d323f8 --- /dev/null +++ b/tests/Integration/DSC_VirtualHardDisk.config.ps1 @@ -0,0 +1,28 @@ +$TestFixedVirtualHardDiskVhdPath = "$($pwd.drive.name):\newTestFixedVhd.vhd" +$TestDynamicVirtualHardDiskVhdx = "$($pwd.drive.name):\newTestDynamicVhdx.vhdx" + +configuration DSC_VirtualHardDisk_CreateAndAttachFixedVhd_Config { + Import-DscResource -ModuleName StorageDsc + node localhost { + VirtualHardDisk Integration_Test { + FilePath = $TestFixedVirtualHardDiskVhdPath + DiskSize = 5GB + DiskFormat = 'Vhd' + DiskType = 'Fixed' + Ensure = 'Present' + } + } +} + +configuration DSC_VirtualHardDisk_CreateAndAttachDynamicallyExpandingVhdx_Config { + Import-DscResource -ModuleName StorageDsc + node localhost { + VirtualHardDisk Integration_Test { + FilePath = $TestDynamicVirtualHardDiskVhdx + DiskSize = 10GB + DiskFormat = 'Vhdx' + DiskType = 'Dynamic' + Ensure = 'Present' + } + } +} diff --git a/tests/Unit/DSC_VirtualHardDisk.Tests.ps1 b/tests/Unit/DSC_VirtualHardDisk.Tests.ps1 new file mode 100644 index 0000000..238ac73 --- /dev/null +++ b/tests/Unit/DSC_VirtualHardDisk.Tests.ps1 @@ -0,0 +1,745 @@ +$script:dscModuleName = 'StorageDsc' +$script:dscResourceName = 'DSC_VirtualHardDisk' + +function Invoke-TestSetup +{ + try + { + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} + +Invoke-TestSetup + +# Begin Testing +try +{ + InModuleScope $script:dscResourceName { + $script:DiskImageGoodVhdxPath = 'C:\test.vhdx' + $script:DiskImageBadPath = '\\test.vhdx' + $script:DiskImageGoodVhdPath = 'C:\test.vhd' + $script:DiskImageNonVirtDiskPath = 'C:\test.text' + $script:DiskImageVirtDiskPathWithoutExtension = 'C:\test' + $script:DiskImageSizeBelowVirtDiskMinimum = 9Mb + $script:DiskImageSizeAboveVhdMaximum = 2041Gb + $script:DiskImageSizeAboveVhdxMaximum = 65Tb + $script:DiskImageSize65Gb = 65Gb + $script:MockTestPathCount = 0 + + $script:mockedDiskImageMountedVhdx = [pscustomobject] @{ + Attached = $true + ImagePath = $script:DiskImageGoodVhdxPath + Size = 100GB + DiskNumber = 2 + } + + $script:mockedDiskImageMountedVhd = [pscustomobject] @{ + Attached = $true + ImagePath = $script:DiskImageGoodVhdPath + Size = 100GB + DiskNumber = 2 + } + + $script:mockedDiskImageNotMountedVhdx = [pscustomobject] @{ + Attached = $false + ImagePath = $script:DiskImageGoodVhdxPath + Size = 100GB + DiskNumber = 2 + } + + $script:mockedDiskImageNotMountedVhd = [pscustomobject] @{ + Attached = $false + ImagePath = $script:DiskImageGoodVhdPath + Size = 100GB + DiskNumber = 2 + } + + $script:GetTargetOutputWhenBadPath = [pscustomobject] @{ + FilePath = $null + Attached = $null + Size = $null + DiskNumber = $null + Ensure = 'Absent' + } + + $script:GetTargetOutputWhenPathGood = [pscustomobject] @{ + FilePath = $mockedDiskImageMountedVhdx.ImagePath + Attached = $mockedDiskImageMountedVhdx.Attached + Size = $mockedDiskImageMountedVhdx.Size + DiskNumber = $mockedDiskImageMountedVhdx.DiskNumber + Ensure = 'Present' + } + + $script:mockedDiskImageEmpty = $null + + function Add-SimpleVirtualDisk + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [ValidateSet('vhd', 'vhdx')] + [System.String] + $DiskFormat, + + [Parameter()] + [ref] + $Handle + ) + } + + function New-SimpleVirtualDisk + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [System.UInt64] + $DiskSizeInBytes, + + [Parameter(Mandatory = $true)] + [ValidateSet('vhd', 'vhdx')] + [System.String] + $DiskFormat, + + [Parameter(Mandatory = $true)] + [ValidateSet('fixed', 'dynamic')] + [System.String] + $DiskType + ) + } + + Describe 'DSC_VirtualHardDisk\Get-TargetResource' { + Context 'When file path does not exist or was never mounted' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageEmpty } ` + -Verifiable + + $currentState = Get-TargetResource -FilePath $script:DiskImageBadPath -Verbose + + It "Should return DiskNumber $($script:GetTargetOutputWhenBadPath.DiskNumber)" { + $currentState.DiskNumber | Should -Be $script:GetTargetOutputWhenBadPath.DiskNumber + } + + It "Should return FilePath $($script:GetTargetOutputWhenBadPath.FilePath)" { + $currentState.FilePath | Should -Be $script:GetTargetOutputWhenBadPath.FilePath + } + + It "Should return Mounted $($script:GetTargetOutputWhenBadPath.Attached)" { + $currentState.Attached | Should -Be $script:GetTargetOutputWhenBadPath.Attached + } + + It "Should return Size $($script:GetTargetOutputWhenBadPath.Size)" { + $currentState.DiskSize | Should -Be $script:GetTargetOutputWhenBadPath.Size + } + + It "Should return Ensure $($script:GetTargetOutputWhenBadPath.Ensure)" { + $currentState.Ensure | Should -Be $script:GetTargetOutputWhenBadPath.Ensure + } + } + + Context 'When file path does exist and is currently mounted' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageMountedVhdx } ` + -Verifiable + + $currentState = Get-TargetResource -FilePath $script:DiskImageGoodVhdxPath -Verbose + + It "Should return DiskNumber $($script:GetTargetOutputWhenPathGood.DiskNumber)" { + $currentState.DiskNumber | Should -Be $script:GetTargetOutputWhenPathGood.DiskNumber + } + + It "Should return FilePath $($script:GetTargetOutputWhenPathGood.FilePath)" { + $currentState.FilePath | Should -Be $script:GetTargetOutputWhenPathGood.FilePath + } + + It "Should return Mounted $($script:GetTargetOutputWhenPathGood.Attached)" { + $currentState.Attached | Should -Be $script:GetTargetOutputWhenPathGood.Attached + } + + It "Should return Size $($script:GetTargetOutputWhenPathGood.Size)" { + $currentState.DiskSize | Should -Be $script:GetTargetOutputWhenPathGood.Size + } + + It "Should return Ensure $($script:GetTargetOutputWhenPathGood.Ensure)" { + $currentState.Ensure | Should -Be $script:GetTargetOutputWhenPathGood.Ensure + } + } + } + + Describe 'DSC_VirtualHardDisk\Set-TargetResource' { + Context 'When file path is not fully qualified' { + Mock ` + -CommandName Assert-ElevatedUser + + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VirtualHardDiskPathError -f ` + $DiskImageBadPath) ` + -ArgumentName 'FilePath' + + It 'Should throw invalid argument error when path is not fully qualified' { + { + Set-TargetResource ` + -FilePath $DiskImageBadPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When not running as administrator' { + Mock ` + -CommandName Assert-ElevatedUser ` + -MockWith { throw [System.Exception]::new('User not elevated.')} ` + -Verifiable + + $exception = [System.Exception]::new($script:localizedData.VirtualDiskAdminError) + + It 'Should throw an error message that the user should run resource as admin' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhd' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw -ExpectedMessage $exception.Message + } + } + + Context 'When file extension is not .vhd or .vhdx' { + Mock ` + -CommandName Assert-ElevatedUser + + $extension = [System.IO.Path]::GetExtension($DiskImageNonVirtDiskPath).TrimStart('.') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VirtualHardDiskUnsupportedFileType -f ` + $extension) ` + -ArgumentName 'FilePath' + + It 'Should throw invalid argument error when the file type is not supported' { + { + Set-TargetResource ` + -FilePath $DiskImageNonVirtDiskPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When file extension does not match the disk format' { + Mock ` + -CommandName Assert-ElevatedUser + + $extension = [System.IO.Path]::GetExtension($DiskImageGoodVhdPath).TrimStart('.') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VirtualHardDiskExtensionAndFormatMismatchError -f ` + $DiskImageGoodVhdPath, $extension, 'vhdx') ` + -ArgumentName 'FilePath' + + It 'Should throw invalid argument error when the file type and filepath extension do not match' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When file extension is not present in the file path' { + Mock ` + -CommandName Assert-ElevatedUser + + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VirtualHardDiskNoExtensionError -f ` + $script:DiskImageVirtDiskPathWithoutExtension) ` + -ArgumentName 'FilePath' + + It 'Should throw invalid argument error when the file type and filepath extension do not match' { + { + Set-TargetResource ` + -FilePath $script:DiskImageVirtDiskPathWithoutExtension ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When size provided is less than the minimum size for the vhd format' { + Mock ` + -CommandName Assert-ElevatedUser + + $minSizeInMbString = ($DiskImageSizeBelowVirtDiskMinimum / 1MB).ToString('0.00MB') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VhdFormatDiskSizeInvalid -f ` + $minSizeInMbString) ` + -ArgumentName 'DiskSize' + + It 'Should throw invalid argument error when the provided size is below the minimum for the vhd format' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdPath ` + -DiskSize $DiskImageSizeBelowVirtDiskMinimum ` + -DiskFormat 'vhd' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When size provided is less than the minimum size for the vhdx format' { + Mock ` + -CommandName Assert-ElevatedUser + + $minSizeInMbString = ($DiskImageSizeBelowVirtDiskMinimum / 1MB).ToString('0.00MB') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VhdxFormatDiskSizeInvalid -f ` + $minSizeInMbString) ` + -ArgumentName 'DiskSize' + + It 'Should throw invalid argument error when the provided size is below the minimum for the vhdx format' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdxPath ` + -DiskSize $DiskImageSizeBelowVirtDiskMinimum ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When size provided is greater than the maximum size for the vhd format' { + Mock ` + -CommandName Assert-ElevatedUser + + $maxSizeInTbString = ($DiskImageSizeAboveVhdMaximum / 1TB).ToString('0.00TB') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VhdFormatDiskSizeInvalid -f ` + $maxSizeInTbString) ` + -ArgumentName 'DiskSize' + + It 'Should throw invalid argument error when the provided size is above the maximum for the vhd format' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdPath ` + -DiskSize $DiskImageSizeAboveVhdMaximum ` + -DiskFormat 'vhd' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When size provided is greater than the maximum size for the vhdx format' { + Mock ` + -CommandName Assert-ElevatedUser + + $maxSizeInTbString = ($DiskImageSizeAboveVhdxMaximum / 1TB).ToString('0.00TB') + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.VhdxFormatDiskSizeInvalid -f ` + $maxSizeInTbString) ` + -ArgumentName 'DiskSize' + + It 'Should throw invalid argument error when the provided size is above the maximum for the vhdx format' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdxPath ` + -DiskSize $DiskImageSizeAboveVhdxMaximum ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When file path to vhdx file is fully qualified' { + Mock ` + -CommandName Assert-ElevatedUser + + It 'Should not throw invalid argument error when path is fully qualified' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdxPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhdx' ` + -Ensure 'Present' ` + -Verbose + } | Should -Not -Throw + } + } + + Context 'When file path to vhd is fully qualified' { + Mock ` + -CommandName Assert-ElevatedUser + + It 'Should not throw invalid argument error when path is fully qualified' { + { + Set-TargetResource ` + -FilePath $DiskImageGoodVhdPath ` + -DiskSize $DiskImageSize65Gb ` + -DiskFormat 'vhd' ` + -Ensure 'Present' ` + -Verbose + } | Should -Not -Throw + } + } + + Context 'Virtual disk is mounted and ensure set to present' { + Mock ` + -CommandName Assert-ElevatedUser + + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageMountedVhdx } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should not throw an exception' { + { + Set-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk is mounted and ensure set to absent, so it should be dismounted' { + Mock ` + -CommandName Assert-ElevatedUser + + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageMountedVhdx } ` + -Verifiable + + Mock ` + -CommandName Dismount-DiskImage ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should dismount the virtual disk' { + { + Set-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Absent' ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + Assert-MockCalled -CommandName Dismount-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk is dismounted and ensure set to present, so it should be re-mounted' { + Mock ` + -CommandName Assert-ElevatedUser + + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageNotMountedVhdx } ` + -Verifiable + + Mock ` + -CommandName Add-SimpleVirtualDisk ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should Not throw exception' { + { + Set-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + Assert-MockCalled -CommandName Add-SimpleVirtualDisk -Exactly 1 + } + } + + Context 'Virtual disk does not exist and ensure set to present, so a new one should be created and mounted' { + Mock ` + -CommandName Assert-ElevatedUser + + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageEmpty } ` + -Verifiable + + Mock ` + -CommandName New-SimpleVirtualDisk ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should not throw an exception' { + { + Set-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + Assert-MockCalled -CommandName New-SimpleVirtualDisk -Exactly 1 + } + } + + Context 'When folder does not exist in user provided path but an exception occurs after creating the virtual disk' { + Mock ` + -CommandName Assert-ElevatedUser + + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageEmpty } ` + -Verifiable + + # Folder does not exist on system so return false to go into if block that creates the folder + Mock ` + -CommandName Test-Path ` + -ParameterFilter { $script:MockTestPathCount -eq 0 } ` + -MockWith { $script:MockTestPathCount++; $false } ` + -Verifiable + + # File was created and exists on system so return true to go into if block that deletes file + Mock ` + -CommandName Test-Path ` + -ParameterFilter { $script:MockTestPathCount -eq 1 } ` + -MockWith { $true } ` + -Verifiable + + Mock ` + -CommandName New-SimpleVirtualDisk ` + -MockWith { throw [System.ComponentModel.Win32Exception]::new($script:AccessDeniedWin32Error) } ` + -Verifiable + + Mock ` + -CommandName New-Item ` + -Verifiable + + Mock ` + -CommandName Remove-Item ` + -Verifiable + + $script:MockTestPathCount = 0 + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + $exception = [System.ComponentModel.Win32Exception]::new($script:AccessDeniedWin32Error) + It 'Should not let exception escape and new folder and file should be deleted' { + { + Set-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose + } | Should -Throw -ExpectedMessage $exception.Message + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + Assert-MockCalled -CommandName New-Item -Exactly 1 + Assert-MockCalled -CommandName Test-Path -Exactly 2 + Assert-MockCalled -CommandName Remove-Item -Exactly 2 + } + } + } + + Describe 'DSC_VirtualHardDisk\Test-TargetResource' { + Context 'Virtual disk does not exist and ensure set to present' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageEmpty } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return false.' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose | Should -BeFalse + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk exists but is not mounted while ensure set to present' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageNotMountedVhdx } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return false.' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose | Should -BeFalse + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk does not exist and ensure set to absent' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageEmpty } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return true' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Absent' ` + -Verbose | Should -BeTrue + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk exists, is mounted and ensure set to present' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageMountedVhdx } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return true' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'Present' ` + -Verbose | Should -BeTrue + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk exists but is mounted while ensure set to absent' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageMountedVhdx } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return false.' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'absent' ` + -Verbose | Should -BeFalse + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + + Context 'Virtual disk exists but is not mounted while ensure set to absent' { + Mock ` + -CommandName Get-DiskImage ` + -MockWith { $script:mockedDiskImageNotMountedVhdx } ` + -Verifiable + + $extension = [System.IO.Path]::GetExtension($script:mockedDiskImageMountedVhdx.ImagePath).TrimStart('.') + It 'Should return true.' { + Test-TargetResource ` + -FilePath $script:mockedDiskImageMountedVhdx.ImagePath ` + -DiskSize $script:mockedDiskImageMountedVhdx.Size ` + -DiskFormat $extension ` + -Ensure 'absent' ` + -Verbose | Should -BeTrue + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskImage -Exactly 1 + } + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/tests/Unit/StorageDsc.VirtualHardDisk.Win32Helpers.Tests.ps1 b/tests/Unit/StorageDsc.VirtualHardDisk.Win32Helpers.Tests.ps1 new file mode 100644 index 0000000..e91c127 --- /dev/null +++ b/tests/Unit/StorageDsc.VirtualHardDisk.Win32Helpers.Tests.ps1 @@ -0,0 +1,404 @@ +#region HEADER, boilerplate used from StorageDSC.Common.Tests +$script:projectPath = "$PSScriptRoot\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:parentModule = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +$script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' +Remove-Module -Name $script:parentModule -Force -ErrorAction 'SilentlyContinue' + +$script:subModuleName = (Split-Path -Path $PSCommandPath -Leaf) -replace '\.Tests.ps1' +$script:subModuleFile = Join-Path -Path $script:subModulesFolder -ChildPath "$($script:subModuleName)/$($script:subModuleName).psm1" + +Import-Module $script:subModuleFile -Force -ErrorAction Stop +#endregion HEADER + +Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + +# Begin Testing +InModuleScope $script:subModuleName { + function New-VirtualDiskUsingWin32 + { + [CmdletBinding()] + [OutputType([System.Int32])] + Param + ( + [Parameter(Mandatory = $true)] + [ref] + $VirtualStorageType, + + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [UInt32] + $AccessMask, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $SecurityDescriptor, + + [Parameter(Mandatory = $true)] + [UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $ProviderSpecificFlags, + + [Parameter(Mandatory = $true)] + [ref] + $CreateVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $Overlapped, + + [Parameter(Mandatory = $true)] + [ref] + $Handle + ) + } + + function Add-VirtualDiskUsingWin32 + { + [CmdletBinding()] + [OutputType([System.Int32])] + Param + ( + [Parameter(Mandatory = $true)] + [ref] + $Handle, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $SecurityDescriptor, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [System.Int32] + $ProviderSpecificFlags, + + [Parameter(Mandatory = $true)] + [ref] + $AttachVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [System.IntPtr] + $Overlapped + ) + } + + function Get-VirtualDiskUsingWin32 + { + [CmdletBinding()] + [OutputType([System.Int32])] + Param + ( + [Parameter(Mandatory = $true)] + [ref] + $VirtualStorageType, + + [Parameter(Mandatory = $true)] + [System.String] + $VirtualDiskPath, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $AccessMask, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Flags, + + [Parameter(Mandatory = $true)] + [ref] + $OpenVirtualDiskParameters, + + [Parameter(Mandatory = $true)] + [ref] + $Handle + ) + } + + $script:DiskImageGoodVhdxPath = 'C:\test.vhdx' + $script:AccessDeniedWin32Error = 5 + $script:vhdDiskFormat = 'vhd' + [ref]$script:TestHandle = [Microsoft.Win32.SafeHandles.SafeFileHandle]::Zero + + $script:mockedParams = [pscustomobject] @{ + DiskSizeInBytes = 65Gb + VirtualDiskPath = $script:DiskImageGoodVhdxPath + DiskType = 'dynamic' + DiskFormat = 'vhdx' + } + + $script:mockedVhdParams = [pscustomobject] @{ + DiskSizeInBytes = 65Gb + VirtualDiskPath = $script:DiskImageGoodVhdxPath + DiskType = 'dynamic' + DiskFormat = 'vhd' + } + + Describe 'StorageDsc.VirtualHardDisk.Win32Helpers\New-SimpleVirtualDisk' -Tag 'New-SimpleVirtualDisk' { + Context 'Creating and attaching a new virtual disk (vhdx) successfully' { + Mock ` + -CommandName New-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + Mock ` + -CommandName Add-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + It 'Should not throw an exception' { + { + New-SimpleVirtualDisk ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskSizeInBytes $script:mockedParams.DiskSizeInBytes ` + -DiskFormat $script:mockedParams.DiskFormat ` + -DiskType $script:mockedParams.DiskType` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName New-VirtualDiskUsingWin32 -Exactly 1 + Assert-MockCalled -CommandName Add-VirtualDiskUsingWin32 -Exactly 1 + } + } + + Context 'Creating and attaching a new virtual disk (vhd) successfully' { + Mock ` + -CommandName New-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + Mock ` + -CommandName Add-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + It 'Should not throw an exception' { + { + New-SimpleVirtualDisk ` + -VirtualDiskPath $script:mockedVhdParams.VirtualDiskPath ` + -DiskSizeInBytes $script:mockedVhdParams.DiskSizeInBytes ` + -DiskFormat $script:mockedVhdParams.DiskFormat ` + -DiskType $script:mockedVhdParams.DiskType` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName New-VirtualDiskUsingWin32 -Exactly 1 + Assert-MockCalled -CommandName Add-VirtualDiskUsingWin32 -Exactly 1 + } + } + + Context 'Creating a new virtual disk failed due to exception' { + Mock ` + -CommandName New-VirtualDiskUsingWin32 ` + -MockWith { $script:AccessDeniedWin32Error } ` + -Verifiable + + $win32Error = [System.ComponentModel.Win32Exception]::new($script:AccessDeniedWin32Error) + $exception = [System.Exception]::new( ` + ($script:localizedData.CreateVirtualDiskError -f $script:mockedParams.VirtualDiskPath, $win32Error.Message), ` + $win32Error) + + It 'Should throw an exception in creation method' { + { + New-SimpleVirtualDisk ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskSizeInBytes $script:mockedParams.DiskSizeInBytes ` + -DiskFormat $script:mockedParams.DiskFormat ` + -DiskType $script:mockedParams.DiskType` + -Verbose + } | Should -Throw -ExpectedMessage $exception.Message + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName New-VirtualDiskUsingWin32 -Exactly 1 + } + } + } + + Describe 'StorageDsc.VirtualHardDisk.Win32Helpers\Add-SimpleVirtualDisk' -Tag 'Add-SimpleVirtualDisk' { + Context 'Attaching a virtual disk failed due to exception' { + + Mock ` + -CommandName Get-VirtualDiskHandle ` + -MockWith { $script:TestHandle } ` + -Verifiable + + Mock ` + -CommandName Add-VirtualDiskUsingWin32 ` + -MockWith { $script:AccessDeniedWin32Error } ` + -Verifiable + + $win32Error = [System.ComponentModel.Win32Exception]::new($script:AccessDeniedWin32Error) + $exception = [System.Exception]::new( ` + ($script:localizedData.MountVirtualDiskError -f $script:mockedParams.VirtualDiskPath, $win32Error.Message), ` + $win32Error) + + It 'Should throw an exception during attach function' { + { + Add-SimpleVirtualDisk ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskFormat $script:mockedParams.DiskFormat ` + -Verbose + } | Should -Throw -ExpectedMessage $exception.Message + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Add-VirtualDiskUsingWin32 -Exactly 2 + Assert-MockCalled -CommandName Get-VirtualDiskHandle -Exactly 1 + } + } + + Context 'Attaching a virtual disk successfully' { + Mock ` + -CommandName Add-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + Mock ` + -CommandName Get-VirtualDiskHandle ` + -MockWith { $script:TestHandle } ` + -Verifiable + + It 'Should not throw an exception' { + { + Add-SimpleVirtualDisk ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskFormat $script:mockedParams.DiskFormat ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-VirtualDiskHandle -Exactly 1 + Assert-MockCalled -CommandName Add-VirtualDiskUsingWin32 -Exactly 1 + } + } + } + + Describe 'StorageDsc.VirtualHardDisk.Win32Helpers\Get-VirtualDiskHandle' -Tag 'Get-VirtualDiskHandle' { + Context 'Opening a virtual disk file failed due to exception' { + + Mock ` + -CommandName Get-VirtualDiskUsingWin32 ` + -MockWith { $script:AccessDeniedWin32Error } ` + -Verifiable + + $win32Error = [System.ComponentModel.Win32Exception]::new($script:AccessDeniedWin32Error) + $exception = [System.Exception]::new( ` + ($script:localizedData.OpenVirtualDiskError -f $script:mockedParams.VirtualDiskPath, $win32Error.Message), ` + $win32Error) + + It 'Should throw an exception while attempting to open virtual disk file' { + { + Get-VirtualDiskHandle ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskFormat $script:mockedParams.DiskFormat ` + -Verbose + } | Should -Throw -ExpectedMessage $exception.Message + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-VirtualDiskUsingWin32 -Exactly 1 + } + } + + Context 'Opening a virtual disk file successfully' { + Mock ` + -CommandName Get-VirtualDiskUsingWin32 ` + -MockWith { 0 } ` + -Verifiable + + It 'Should not throw an exception' { + { + Get-VirtualDiskHandle ` + -VirtualDiskPath $script:mockedParams.VirtualDiskPath ` + -DiskFormat $script:mockedParams.DiskFormat ` + -Verbose + } | Should -Not -Throw + } + + It 'Should only call required mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-VirtualDiskUsingWin32 -Exactly 1 + } + } + } + + Describe 'StorageDsc.VirtualHardDisk.Win32Helpers\Get-VirtualStorageType' -Tag 'Get-VirtualStorageType' { + Context 'Storage type requested for vhd disk format' { + $result = Get-VirtualStorageType -DiskFormat $script:vhdDiskFormat + It 'Should not throw an exception' { + { + Get-VirtualStorageType ` + -DiskFormat $script:vhdDiskFormat ` + -Verbose + } | Should -Not -Throw + } + Get-VirtDiskWin32HelperScript + $virtualStorageType = New-Object -TypeName VirtDisk.Helper+VIRTUAL_STORAGE_TYPE + $virtualStorageType.VendorId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT + $virtualStorageType.DeviceId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_DEVICE_VHD + + It "Should return vendorId $($virtualStorageType.VendorId)" { + $result.VendorId | Should -Be $virtualStorageType.VendorId + } + + It "Should return DeviceId $($virtualStorageType.DeviceId)" { + $result.DeviceId | Should -Be $virtualStorageType.DeviceId + } + } + + Context 'Storage type requested for vhdx disk format' { + $result = Get-VirtualStorageType -DiskFormat $script:mockedParams.DiskFormat + It 'Should not throw an exception' { + { + Get-VirtualStorageType ` + -DiskFormat $script:mockedParams.DiskFormat ` + -Verbose + } | Should -Not -Throw + } + Get-VirtDiskWin32HelperScript + $virtualStorageType = New-Object -TypeName VirtDisk.Helper+VIRTUAL_STORAGE_TYPE + $virtualStorageType.VendorId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT + $virtualStorageType.DeviceId = [VirtDisk.Helper]::VIRTUAL_STORAGE_TYPE_DEVICE_VHDX + + It "Should return vendorId $($virtualStorageType.VendorId)" { + $result.VendorId | Should -Be $virtualStorageType.VendorId + } + + It "Should return DeviceId $($virtualStorageType.DeviceId)" { + $result.DeviceId | Should -Be $virtualStorageType.DeviceId + } + } + + } +}