diff --git a/CHANGELOG.md b/CHANGELOG.md index be4be481..2d6d1b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Disk: + - BREAKING CHANGE: Added support for volumes to be formatted as Dev Drives - Fixes [Issue #276](https://github.com/dsccommunity/StorageDsc/issues/276) + ## [5.1.0] - 2023-02-22 ### Changed diff --git a/source/DSCResources/DSC_Disk/DSC_Disk.psm1 b/source/DSCResources/DSC_Disk/DSC_Disk.psm1 index 6ba3c59c..da3c73b4 100644 --- a/source/DSCResources/DSC_Disk/DSC_Disk.psm1 +++ b/source/DSCResources/DSC_Disk/DSC_Disk.psm1 @@ -51,6 +51,9 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' Specifies if the disks partition schema should be removed entirely, even if data and OEM partitions are present. Only possible with AllowDestructive enabled. This parameter is not used in Get-TargetResource. + + .PARAMETER DevDrive + Specifies if the volume is formatted as a Dev Drive. #> function Get-TargetResource { @@ -99,7 +102,11 @@ function Get-TargetResource [Parameter()] [System.Boolean] - $ClearDisk + $ClearDisk, + + [Parameter()] + [System.Boolean] + $DevDrive ) Write-Verbose -Message ( @( @@ -127,6 +134,14 @@ function Get-TargetResource -Query "SELECT BlockSize from Win32_Volume WHERE DriveLetter = '$($DriveLetter):'" ` -ErrorAction SilentlyContinue).BlockSize + $DevDrive = $false + if ($volume.UniqueId) + { + $DevDrive = Test-DevDriveVolume ` + -VolumeGuidPath $volume.UniqueId ` + -ErrorAction SilentlyContinue + } + return @{ DiskId = $DiskId DiskIdType = $DiskIdType @@ -136,6 +151,7 @@ function Get-TargetResource FSLabel = $volume.FileSystemLabel AllocationUnitSize = $blockSize FSFormat = $volume.FileSystem + DevDrive = $DevDrive } } # Get-TargetResource @@ -173,6 +189,9 @@ function Get-TargetResource .PARAMETER ClearDisk Specifies if the disks partition schema should be removed entirely, even if data and OEM partitions are present. Only possible with AllowDestructive enabled. + + .PARAMETER DevDrive + Specifies if the volume should be formatted as a Dev Drive. #> function Set-TargetResource { @@ -222,7 +241,11 @@ function Set-TargetResource [Parameter()] [System.Boolean] - $ClearDisk + $ClearDisk, + + [Parameter()] + [System.Boolean] + $DevDrive ) Write-Verbose -Message ( @( @@ -288,6 +311,11 @@ function Set-TargetResource ) -join '' ) $disk | Initialize-Disk -PartitionStyle $PartitionStyle + + # Requery the disk + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType } else { @@ -310,6 +338,24 @@ function Set-TargetResource } } + <# + Check Dev Drive assertions. + #> + if ($DevDrive) + { + Assert-DevDriveFeatureAvailable + Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue -FSFormat $FSFormat + + <# + We validate the case where the user does not specify a size later on, should we need to create a new + partition. + #> + if ($Size) + { + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $Size + } + } + # Get the partitions on the disk $partition = $disk | Get-Partition -ErrorAction SilentlyContinue @@ -317,6 +363,12 @@ function Set-TargetResource $assignedPartition = $partition | Where-Object -Property DriveLetter -eq $DriveLetter + <# + Get the current max unallocated space in bytes. Round up to nearest Gb so we can do better comparisons + with the Size parameter. + #> + $currentMaxUnallocatedSpaceInBytes = [Math]::Round($disk.LargestFreeExtent / 1GB, 2) * 1GB + # Check if existing partition already has file system on it if ($null -eq $assignedPartition) { @@ -333,10 +385,94 @@ function Set-TargetResource # There are partitions defined - identify if one matches the size required if ($Size) { - # Find the first basic partition matching the size - $partition = $partition | - Where-Object -FilterScript { $_.Type -eq 'Basic' -and $_.Size -eq $Size } | - Select-Object -First 1 + if ($DevDrive) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AttemptingToFindAPartitionToResizeForDevDrive) + ) -join '' ) + + <# + Find the first partition whose max - min supported partition size is greater than or equal + to the size the user wants so we can resize it later. The max size also includes any + unallocated space next to the partition. + #> + $partitionToResizeForDevDriveScenario = $null + $amountToDecreasePartitionBy = 0 + $isResizeNeeded = $true + foreach ($tempPartition in $partition) + { + $shouldNotBeResized = ($tempPartition.Type -in 'System','Reserved','Recovery') + + $doesNotHaveDriveLetter = (-not $tempPartition.DriveLetter -or ` + $tempPartition.DriveLetter -eq '') + + if ($shouldNotBeResized -or $doesNotHaveDriveLetter) + { + continue + } + + $supportedSize = Get-PartitionSupportedSize -DriveLetter $tempPartition.DriveLetter + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingIfPartitionCanBeResizedForDevDrive -f $tempPartition.DriveLetter) + ) -join '' ) + + if (($supportedSize.SizeMax - $supportedSize.SizeMin) -ge $Size) + { + $unallocatedSpaceNextToPartition = $supportedSize.SizeMax - $tempPartition.Size + + if ($unallocatedSpaceNextToPartition -ge $Size) + { + <# + The size of the unallocated space next to the partition is already big enough + to create a Dev Drive volume on, so we don't need to resize any partitions. + #> + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NoPartitionResizeNeededForDevDrive) + ) -join '' ) + + $isResizeNeeded = $false + break + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PartitionFoundThatCanBeResizedForDevDrive -f $tempPartition.DriveLetter) + ) -join '' ) + + + $partitionToResizeForDevDriveScenario = $tempPartition + $amountToDecreasePartitionBy = $Size - $unallocatedSpaceNextToPartition + break + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PartitionCantBeResizedForDevDrive -f $tempPartition.DriveLetter) + ) -join '' ) + } + + $partition = $partitionToResizeForDevDriveScenario + + if ($isResizeNeeded -and (-not $partition) -and $currentMaxUnallocatedSpaceInBytes -lt $Size) + { + $SizeInGb = [Math]::Round($Size / 1GB, 2) + throw ($script:localizedData.FoundNoPartitionsThatCanResizedForDevDrive -f $SizeInGb) + } + } + else + { + <# + When not in the Dev Drive scenario we attempt to find the first basic partition matching + the size and use that as the partition. + #> + $partition = $partition | + Where-Object -FilterScript { $_.Type -eq 'Basic' -and $_.Size -eq $Size } | + Select-Object -First 1 + } if ($partition) { @@ -395,7 +531,51 @@ function Set-TargetResource } # if } # if - # Do we need to create a new partition? + <# + We can't find a partition with the required drive letter, so we may need to make a new one. + First we need to check if there is already enough unallocated space on the disk. + #> + $enoughUnallocatedSpace = ($currentMaxUnallocatedSpaceInBytes -ge $Size) + + if ($DevDrive -and $Size -and $partition -and (-not $enoughUnallocatedSpace)) + { + <# + Resize the partition that has the largest max - min supported size. This will create + enough new unallocated space for the Dev Drive volume to be created on. + #> + if ($AllowDestructive) + { + $newPartitionSize = ($partition.Size - $amountToDecreasePartitionBy) + $newPartitionSizeInGb = [Math]::Round($newPartitionSize / 1GB, 2) + $SizeInGb = [Math]::Round($Size / 1GB, 2) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ResizingPartitionToMakeSpaceForDevDriveVolume ` + -f $partition.DriveLetter, $newPartitionSizeInGb, $SizeInGb) + ) -join '' ) + + $partition | Resize-Partition -Size $newPartitionSize + + # Requery the disk since we resized a partition + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + } + else + { + # Allow Destructive is not set to true, so throw an exception. + throw ($script:localizedData.AllowDestructiveNeededForDevDriveOperation -f $partition.DriveLetter) + } + + # We no longer need to use the resized partition as we now have enough unallocated space. + $partition = $null + } + + <# + We enter here if there are no partitions on the disk or when we need to create a new partition + using unallocated space. + #> if (-not $partition) { # Attempt to create a new partition @@ -412,6 +592,21 @@ function Set-TargetResource -f $DiskIdType, $DiskId, $DriveLetter, "$($Size/1KB) KB") ) -join '' ) + if ($DevDrive) + { + <# + If size is slightly larger in bytes due to low level rounding differences than the max + free extent there won't be any capacity to create the new partition. So if the values + are the same in GB after rounding, we'll update size to be the max free extent + #> + if (Compare-SizeUsingGB -SizeAInBytes $Size -SizeBInBytes $disk.LargestFreeExtent) + { + $Size = $disk.LargestFreeExtent + } + + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $Size + } + $partitionParams['Size'] = $Size } else @@ -423,6 +618,11 @@ function Set-TargetResource -f $DiskIdType, $DiskId, $DriveLetter, 'all free space') ) -join '' ) + if ($DevDrive) + { + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $currentMaxUnallocatedSpaceInBytes + } + $partitionParams['UseMaximumSize'] = $true } # if @@ -477,7 +677,7 @@ function Set-TargetResource $supportedSize = $assignedPartition | Get-PartitionSupportedSize <# - If the parition size was not specified then try and make the partition + If the partition size was not specified then try and make the partition use all possible space on the disk. #> if (-not ($PSBoundParameters.ContainsKey('Size'))) @@ -487,7 +687,7 @@ function Set-TargetResource if ($assignedPartition.Size -ne $Size) { - # A patition resize is required + # A partition resize is required if ($AllowDestructive) { if ($FSFormat -eq 'ReFS') @@ -531,6 +731,18 @@ function Set-TargetResource } } + <# + If the Set-TargetResource function is run as a standalone function, and $assignedPartition is not null + and there are multiple partitions in $partition, then '$partition | Get-Volume', will give $volume back + the first volume on the first partition. If $assignedPartition is after that one, then we could + potentially format a different volume. So we need to make sure that $partition is equal to + $assignedPartition before we call Get-Volume. + #> + if ($assignedPartition) + { + $partition = $assignedPartition + } + # Get the Volume on the partition $volume = $partition | Get-Volume @@ -560,6 +772,14 @@ function Set-TargetResource $($script:localizedData.FormattingVolumeMessage -f $formatVolumeParameters.FileSystem) ) -join '' ) + if ($DevDrive) + { + # Confirm that the partition size meets the minimum requirements for a Dev Drive volume. + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $partition.Size + + $formatVolumeParameters['DevDrive'] = $DevDrive + } + # Format the volume $volume = $partition | Format-Volume @formatVolumeParameters } @@ -597,7 +817,16 @@ function Set-TargetResource $formatParam.Add('AllocationUnitSize', $AllocationUnitSize) } - $Volume | Format-Volume @formatParam + if ($DevDrive) + { + # Confirm that the volume size meets the minimum requirements for a Dev Drive volume. + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $volume.Size + + $formatParam.Add('DevDrive', $DevDrive) + } + + # Update the volume with the new format information. + $volume = $volume | Format-Volume @formatParam } } # if } # if @@ -635,6 +864,29 @@ function Set-TargetResource $($script:localizedData.SuccessfullyInitializedMessage -f $DriveLetter) ) -join '' ) } # if + + # Confirm that the volume is now actually formatted as a Dev Drive volume. + if ($DevDrive) + { + $isDevDriveVolume = Test-DevDriveVolume -VolumeGuidPath $volume.UniqueId -ErrorAction SilentlyContinue + + if ($isDevDriveVolume) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SuccessfullyConfiguredDevDriveVolume ` + -f $volume.UniqueId, $volume.DriveLetter) + ) -join '' ) + } + else + { + throw ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FailedToConfigureDevDriveVolume ` + -f $volume.UniqueId, $volume.DriveLetter) + ) -join '' ) + } + } } # Set-TargetResource <# @@ -671,6 +923,9 @@ function Set-TargetResource .PARAMETER ClearDisk Specifies if the disks partition schema should be removed entirely, even if data and OEM partitions are present. Only possible with AllowDestructive enabled. + + .PARAMETER DevDrive + Specifies if the volume should be formatted as a Dev Drive. #> function Test-TargetResource { @@ -719,7 +974,11 @@ function Test-TargetResource [Parameter()] [System.Boolean] - $ClearDisk + $ClearDisk, + + [Parameter()] + [System.Boolean] + $DevDrive ) Write-Verbose -Message ( @( @@ -841,7 +1100,40 @@ function Test-TargetResource -f $DriveLetter, $Partition.Size, $Size) ) -join '' ) - return $false + if ($DevDrive) + { + <# + In the Dev Drive scenario we may resize a partition to create new unallocated space, so + that we can create a new Dev Drive. When this is done we create a new partition on the + largest free extent on the disk. However, Though the value is equivalent they aren't + always the same. E.g 150Gb in powershell cmdline is 161061273600 bytes. + But $disk.LargestFreeExtent could be 161060225024 bytes. Which is different but they are + both 150Gb when you convert them. + + See the 'Size' parameter in: + https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/createpartition-msft-disk + for more information. But to some it up, what the user enters and what the New-Partition + cmdlet is able to allocate can be different in bytes. So to keep idempotence, we only + return false when they arent the same in GB. Also if the volume already is formatted as + a ReFS, there is no point in returning false for size mismatches since they can't be + resized with resize-partition. + #> + + $partitionSizeEqualToSizeParam = Compare-SizeUsingGB ` + -SizeAInBytes $Size ` + -SizeBInBytes $partition.Size + + $volume = $partition | Get-Volume + + if ((-not $partitionSizeEqualToSizeParam) -and $volume.FileSystem -ne 'ReFS') + { + return $false + } + } + else + { + return $false + } } else { @@ -852,6 +1144,11 @@ function Test-TargetResource ) -join '' ) } } # if + + if ($DevDrive) + { + Assert-SizeMeetsMinimumDevDriveRequirement -UserDesiredSize $Size + } } # if $blockSize = (Get-CimInstance ` @@ -917,6 +1214,39 @@ function Test-TargetResource } # if } # if + if ($DevDrive) + { + # User requested to configure the volume as a Dev Drive volume. So we check that the assertions are met. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingDevDriveAssertions) + ) -join '' ) + + Assert-DevDriveFeatureAvailable + Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue -FSFormat $FSFormat + + $isDevDriveVolume = Test-DevDriveVolume -VolumeGuidPath $volume.UniqueId -ErrorAction SilentlyContinue + + if ($isDevDriveVolume) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TheVolumeIsCurrentlyConfiguredAsADevDriveVolume ` + -f $volume.UniqueId, $volume.DriveLetter) + ) -join '' ) + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TheVolumeIsNotConfiguredAsADevDriveVolume ` + -f $volume.UniqueId, $volume.DriveLetter) + ) -join '' ) + + return $false + } + } + return $true } # Test-TargetResource diff --git a/source/DSCResources/DSC_Disk/DSC_Disk.schema.mof b/source/DSCResources/DSC_Disk/DSC_Disk.schema.mof index 01341025..7d06a357 100644 --- a/source/DSCResources/DSC_Disk/DSC_Disk.schema.mof +++ b/source/DSCResources/DSC_Disk/DSC_Disk.schema.mof @@ -12,4 +12,5 @@ class DSC_Disk : OMI_BaseResource [Write, Description("Specifies the file system format of the new volume."), ValueMap{"NTFS","ReFS"}, Values{"NTFS","ReFS"}] String FSFormat; [Write, Description("Specifies if potentially destructive operations may occur.")] Boolean AllowDestructive; [Write, Description("Specifies if the disks partition schema should be removed entirely, even if data and OEM partitions are present. Only possible with AllowDestructive enabled.")] Boolean ClearDisk; + [Write, Description("Specifies if the volume should be formatted as a Dev Drive.")] Boolean DevDrive; }; diff --git a/source/DSCResources/DSC_Disk/README.md b/source/DSCResources/DSC_Disk/README.md index 4f6d1ed4..696b05d6 100644 --- a/source/DSCResources/DSC_Disk/README.md +++ b/source/DSCResources/DSC_Disk/README.md @@ -23,10 +23,51 @@ identifier for disks with `GPT` partition table format. If the disk is `RAW` be used. This is because the _Guid_ for a disk is only assigned once the partition table for the disk has been created. +## Dev Drive + +The Dev Drive feature is currently available on Windows 11 in builds 10.0.22621.2338 or later. See [the Dev Drive documentation for the latest in formation](https://learn.microsoft.com/en-us/windows/dev-drive/). + +### What is a Dev Drive volume and how is it different from regular volumes? + +Dev Drive volumes from a storage perspective are just like regular ReFS volumes on a Windows machine. The difference However, is that most of the filter drivers except the antivirus filter will not attach to the volume at boot time by default. This is a low-level concept that most users will never need to interact with but for further reading, see the documentation [here](https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-configure-additional-filters-on-dev-drive) for further reading. + +### What is the default state of the Dev Drive flag in this resource? + +By default, the Dev Drive flag is set to **false**. This means that a Dev Drive volume will not be created with the inputted parameters. This is used to create/reconfigure non Dev Drive volumes. Setting the flag to **true** will attempt to create/reconfigure a volume as a Dev Drive volume using the users' inputted parameters. + +### Can more than one Dev Drive be mounted at a time? + +Yes, more than one Dev Drive volume can be mounted at a time. You can have as many Dev Drive volumes as the physical storage amount on the disk permits. Though, it should be noted, that the `minimum size` for a single Dev Drive volume is `50 Gb`. + +### If I have a non Dev Drive volume that is 50 Gb or more can it be reformatted as a Dev Drive volume? + +Yes, since the Dev Drive volume is just like any other volume storage wise to the Windows operating system, a non Dev Drive ReFS volume can be reformatted as a Dev Drive volume. An NTFS volume can also be reformatted as a Dev Drive volume. Note, the Disk resource will throw an exception, should you also attempt to resize a ReFS volume while attempting to reformat it as a Dev Drive volume since ReFS volumes cannot be resized. As Dev Drive volumes are also ReFS volumes, they carry the same restrictions, see: [Resilient File System (ReFS) overview | Microsoft Learn](https://learn.microsoft.com/en-us/windows-server/storage/refs/refs-overview) + +### If I don't have any unallocated space available to create a Dev Drive volume, what will happen? + +The Disk resource uses the Get-PartitionSupportedSize cmdlet to know which volume can be be resized to a safe size to create enough unallocated space for the Dev Drive volume to be created. As long as the size parameter is used, the Disk resource will shrink the first non ReFS Drive whose (MaxSize - MinSize) is greater than or equal to the size entered in the size parameter. + +If unallocated space exists but isn't enough to create a Dev Drive volume with, The Disk Resource will only shrink the volume noted above by the minimum size needed, to add to the existing unallocated space so it can be equal to the size parameter. For example, if you wanted to create a new 50 Gb Dev Drive volume on disk 0, and let's say on disk 0 there was only a 'C' drive that was 800 Gb in size. Next to the 'C' drive there was only 40 Gb of free contiguous unallocated space. The Disk resource would shrink the 'C' drive by 10 Gb, creating an addition 10 Gb of unallocated space. Now the unallocated space would be 50 Gb in size. The disk resource would then create a new partition and create the Dev Drive volume into this new partition. + +**Note: if no size is entered the disk resource will throw an error stating that size is 0 gb, so no partitions can be resized.** + +### Dev Drive requirements for this resource + +There are only five requirements: + +1. The Dev Drive feature must be available on the machine. We assert that this is true in order to format a Dev Drive volume onto a partition. +2. The Dev Drive feature is enabled on the machine. Note: the feature could be disabled by either a group or system policy, so if ran in an enterprise environment this should be checked. Note, once a Dev Drive volume is created, its functionality will not change and will not be affected should the feature become disabled afterwards. Disablement would only prevent new Dev Drive volumes from being created. However, this could affect the `idempotence` for the Drive. For example, changes to this drive after disablement (e.g., reformatting the volume as an NTFS volume) would not be corrected by rerunning the configuration. Since the feature is disabled, attempting reformat the volume as a Dev Drive volume will throw an error advising you that it is not possible due to the feature being disabled. +3. If the `size` parameter is entered, the value must be greater than or equal to 50 Gb in size. We assert that this is true in order to format a Dev Drive volume onto a partition. +4. Currently today, if the `size` parameter is not entered then the Disk resource will use the maximum space available on the Disk. When the `DevDrive` flag is set to `$true`, then we assert that the maximum available free unallocated space on the Disk should be `50 Gb or more in size`. This assertion only comes into play if the volume doesn't already exist. +5. The `FSformat` parameter must be set to 'ReFS', when the `DevDrive` flag is set to true. We assert that this is true and throw an exception if it is not. + # Testing Note: Integration tests are not run for the Disk resource when SerialNumber is used since the virtual disk that is created does not have a serial number. +There are no Dev Drive integration tests as the feature is not available in Server +2019 and 2022. + ## Known Issues ### Defragsvc Conflict diff --git a/source/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 b/source/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 index 257c42da..ed227739 100644 --- a/source/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 +++ b/source/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 @@ -39,4 +39,17 @@ FreeSpaceViolationError = Attempted to resize partition {0} from {1} to {2} while maximum allowed size was {3}. ResizeRefsNotPossibleMessage = Skipping resize of {0} from {1} to {2}. Resizing ReFS partitions is currently not possible. ResizeNotAllowedMessage = Skipping resize of {0} from {1} to {2}. AllowDestructive is not set to $true. + AllowDestructiveNeededForDevDriveOperation = The partition with drive letter '{0}' was found to be big enough to be resized to create enough space for a new Dev Drive volume. However the 'AllowDestructive' flag was not set to $true. + ResizingPartitionToMakeSpaceForDevDriveVolume = Resizing partition with drive letter '{0}' to size '{1} Gb', so we can make enough unallocated space for a Dev Drive volume of size '{2} Gb'. + SuccessfullyConfiguredDevDriveVolume = The volume with path '{0}' on drive '{1}:\\' has been successfully configured as a Dev Drive volume. + FailedToConfigureDevDriveVolume = Failed to configure the volume with path '{0}' on drive '{1}:\\' as a Dev Drive volume. + AttemptingToFindAPartitionToResizeForDevDrive = Attempting to find a partition that we can resize, in order to create enough unallocated space for a new Dev Drive volume. + CheckingIfPartitionCanBeResizedForDevDrive = Checking if partition with drive letter '{0}' can be resized to create enough unallocated space for the new Dev Drive volume. + PartitionCantBeResizedForDevDrive = Partition with drive letter '{0}' cannot be resized to create the new Dev Drive volume. Continuing search... + PartitionFoundThatCanBeResizedForDevDrive = Found partition with drive letter '{0}' which can be resized to create a new Dev Drive volume. + FoundNoPartitionsThatCanResizedForDevDrive = There is no unallocated space available to create the Dev Drive volume. We found no partitions that could be resized to create '{0} Gb' of unallocated space. + NoPartitionResizeNeededForDevDrive = We found enough unallocated space available on the disk. No partition resize is needed to create the Dev Drive volume. + CheckingDevDriveAssertions = Checking system meets requirements for the Dev Drive feature. + TheVolumeIsNotConfiguredAsADevDriveVolume = The volume with path '{0}' and Drive letter '{1}' is not configured as a Dev Drive volume. + TheVolumeIsCurrentlyConfiguredAsADevDriveVolume = The volume with path '{0}' and Drive letter '{1}' is currently configured as a Dev Drive volume. '@ diff --git a/source/Examples/Resources/Disk/3-Disk_CreateDevDriveOnDiskWithExistingPartitions.ps1 b/source/Examples/Resources/Disk/3-Disk_CreateDevDriveOnDiskWithExistingPartitions.ps1 new file mode 100644 index 00000000..d2258537 --- /dev/null +++ b/source/Examples/Resources/Disk/3-Disk_CreateDevDriveOnDiskWithExistingPartitions.ps1 @@ -0,0 +1,86 @@ +<#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 + For this scenario we want to create two 60 Gb Dev Drive volumes. We know that disk 2 has 3 existing + NTFS volumes and we prefer not to remove them. At most we only want the disk DSC resource to shrink any + of them should there not be enough space for any of the Dev Drive volumes to be created. We also know that the + the 3 existing volumes are 100Gb, 200Gb and 300Gb in size and disk 2 is 600 Gb in size. Since all the space + is being used by the existing volumes, The Disk Dsc resource will resize the existing volumes to create + space for our new Dev Drive volumes. An example of what could happen is the Disk resource could resize the + 300Gb volume to 240Gb for the first Dev Drive volume and then resize the 240Gb volume again to 180Gb for the second. + Thats just one combination, the disk Dsc resource uses the Get-PartitionSupportedSize cmdlet to know which volume + can be be resized to a safe size to create enough unallocated space for the Dev Drive volume to be created. Note: + ReFS volumes cannot be resized, so if the existing volumes were all ReFS volumes, the Disk Dsc resource would not be able + to resize any volumes and would instead throw an exception. + + This configuration below will wait for disk 2 to become available, and then create two new 60 Gb Dev Drive volumes, + 'E' and 'F'. The volumes will be formatted as ReFS volumes and labeled 'Dev Drive 1' and 'Dev Drive 2' respectively. + Note: setting 'AllowDestructive' to $true will not cause the disk to be cleared, as the flag is only used when there + is a need to resize an existing partition. It is used as confirmation that you agree to the resizing which will + create the necessary space for the Dev Drive volume. This flag is **NOT** needed if you already know there is enough + unallocated space on the disk to create the Dev Drive volume. If this flag is not used and there is not enough space + to create the Dev Drive volume an error will be thrown and the Dev Drive will not be created. Its important to be very + careful not to add the 'ClearDisk' flag while using the 'AllowDestructive' flag, as this will cause the disk to be cleared, + and all data lost on the disk (even existing volumes). +#> +Configuration Disk_CreateDevDriveOnDiskWithExistingPartitions +{ + Import-DSCResource -ModuleName StorageDsc + + Node localhost + { + WaitForDisk Disk2 + { + DiskId = '5E1E50A401000000001517FFFF0AEB84' # Disk 2 + DiskIdType = 'UniqueId' + RetryIntervalSec = 60 + RetryCount = 60 + } + + # Will create a Dev Drive volume of 60 Gb called Dev Drive 1. + Disk DevDrive1 + { + DiskId = '5E1E50A401000000001517FFFF0AEB84' + DiskIdType = 'UniqueId' + DriveLetter = 'E' + FSFormat = 'ReFS' + FSLabel = 'DevDrive 1' + DevDrive = $true + AllowDestructive = $true + Size = 60Gb + DependsOn = '[WaitForDisk]Disk2' + } + + # Will create a Dev Drive volume of 60 Gb called Dev Drive 2. + Disk DevDrive2 + { + DiskId = '5E1E50A401000000001517FFFF0AEB84' + DiskIdType = 'UniqueId' + DriveLetter = 'F' + FSFormat = 'ReFS' + FSLabel = 'DevDrive 2' + DevDrive = $true + AllowDestructive = $true + Size = 60Gb + DependsOn = '[Disk]DevDrive1' + } + } +} diff --git a/source/Examples/Resources/Disk/4-Disk_InitializeDiskWithMultipleDrivesIncludingDevDrives.ps1 b/source/Examples/Resources/Disk/4-Disk_InitializeDiskWithMultipleDrivesIncludingDevDrives.ps1 new file mode 100644 index 00000000..53e72892 --- /dev/null +++ b/source/Examples/Resources/Disk/4-Disk_InitializeDiskWithMultipleDrivesIncludingDevDrives.ps1 @@ -0,0 +1,101 @@ +<#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 + For this scenario we want to create 2 Non Dev Drive volumes and 2 Dev Drive volumes on a new 1 Tb disk + (disk 1) with no partitions. The first non Dev Drive volume will be an NTFS volume of 100 Gb called 'Data'. + The second non Dev Drive volume will be a ReFS volume of 200 Gb called 'Logs'. The first Dev Drive volume + will be a ReFS volume of 300 Gb called 'Dev Drive 1'. The second Dev Drive volume will be a ReFS volume of + 400 Gb called 'Dev Drive 2'. Note: The Dev Drive volumes will be created after the non Dev Drive volumes are + created but the order does not matter, we could have created the Dev Drive volumes first and then the non Dev + Drive volumes or even interleave them. Since this is a new disk and we know there are no existing partitions, + we do not need to set the 'AllowDestructive' flag for the Dev Drive volumes like in + 3-Disk_CreateDevDriveOnDiskWithExistingPartitions.ps1. + + This configuration below will wait for disk 1 to become available, and then create two new non Dev Drive volumes + called Data and Logs with Drive letters G and J respectively. The D drive is an NTFS drive and the J drive is an + ReFS drive. It also create two new Dev Drive volumes which are assigned drive letters K and L respectively. + The Dev Drive volumes are formatted as ReFS volumes and labeled 'Dev Drive 1' and 'Dev Drive 2' respectively. +#> +Configuration Disk_InitializeDiskWithMultipleDrivesIncludingDevDrives +{ + Import-DSCResource -ModuleName StorageDsc + + Node localhost + { + WaitForDisk Disk1 + { + DiskId = '5E1E50A401000000001517FFFF0AEB81' # Disk 1 + DiskIdType = 'UniqueId' + RetryIntervalSec = 60 + RetryCount = 60 + } + + # Will create a NTFS volume of 100 Gb called Data. + Disk DataVolume + { + DiskId = '5E1E50A401000000001517FFFF0AEB81' + DiskIdType = 'UniqueId' + DriveLetter = 'G' + FSFormat = 'NTFS' + FSLabel = 'Data' + Size = 100Gb + DependsOn = '[WaitForDisk]Disk1' + } + + # Will create a ReFS volume of 200 Gb called Logs. + Disk LogsVolume + { + DiskId = '5E1E50A401000000001517FFFF0AEB81' + DiskIdType = 'UniqueId' + DriveLetter = 'J' + FSFormat = 'ReFS' + FSLabel = 'Logs' + Size = 200Gb + DependsOn = '[Disk]DataVolume' + } + + # Will create a Dev Drive volume of 300 Gb called Dev Drive 1. + Disk DevDrive1 + { + DiskId = '5E1E50A401000000001517FFFF0AEB81' + DiskIdType = 'UniqueId' + DriveLetter = 'K' + FSFormat = 'ReFS' + FSLabel = 'DevDrive 1' + DevDrive = $true + Size = 300Gb + DependsOn = '[Disk]LogsVolume' + } + + # Will create a Dev Drive volume of 400 Gb called Dev Drive 2. + Disk DevDrive2 + { + DiskId = '5E1E50A401000000001517FFFF0AEB81' + DiskIdType = 'UniqueId' + DriveLetter = 'L' + FSFormat = 'ReFS' + FSLabel = 'DevDrive 2' + DevDrive = $true + Size = 400Gb + DependsOn = '[Disk]DevDrive1' + } + } +} diff --git a/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 b/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 index 87eb8c64..718f353a 100644 --- a/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 +++ b/source/Modules/StorageDsc.Common/StorageDsc.Common.psm1 @@ -222,10 +222,425 @@ function Test-AccessPathAssignedToLocal return $accessPathAssigned } # end function Test-AccessPathLocal +<# + .SYNOPSIS + Returns C# code that will be used to call Dev Drive related Win32 apis. +#> +function Get-DevDriveWin32HelperScript +{ + [OutputType([System.Type])] + [CmdletBinding()] + param + () + + $DevDriveHelperDefinitions = @' + + // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-developer_drive_enablement_state + public enum DEVELOPER_DRIVE_ENABLEMENT_STATE + { + DeveloperDriveEnablementStateError = 0, + DeveloperDriveEnabled = 1, + DeveloperDriveDisabledBySystemPolicy = 2, + DeveloperDriveDisabledByGroupPolicy = 3, + } + + // https://learn.microsoft.com/en-us/windows/win32/api/apiquery2/nf-apiquery2-isapisetimplemented + [DllImport("api-ms-win-core-apiquery-l2-1-0.dll", ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern bool IsApiSetImplemented(string Contract); + + // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getdeveloperdriveenablementstate + [DllImport("api-ms-win-core-sysinfo-l1-2-6.dll")] + public static extern DEVELOPER_DRIVE_ENABLEMENT_STATE GetDeveloperDriveEnablementState(); + + + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern SafeFileHandle CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + // https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DeviceIoControl( + SafeFileHandle hDevice, + uint dwIoControlCode, + IntPtr lpInBuffer, + uint nInBufferSize, + IntPtr lpOutBuffer, + uint nOutBufferSize, + out uint lpBytesReturned, + IntPtr lpOverlapped); + + // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_fs_persistent_volume_information + [StructLayout(LayoutKind.Sequential)] + public struct FILE_FS_PERSISTENT_VOLUME_INFORMATION + { + public uint VolumeFlags; + public uint FlagMask; + public uint Version; + public uint Reserved; + } + + // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_fs_persistent_volume_information + public const uint FSCTL_QUERY_PERSISTENT_VOLUME_STATE = 590396U; + public const uint PERSISTENT_VOLUME_STATE_DEV_VOLUME = 0x00002000; + + // https://learn.microsoft.com/en-us/windows/win32/fileio/creating-and-opening-files + public const uint FILE_READ_ATTRIBUTES = 0x0080; + public const uint FILE_WRITE_ATTRIBUTES = 0x0100; + public const uint FILE_SHARE_READ = 0x00000001; + public const uint FILE_SHARE_WRITE = 0x00000002; + public const uint OPEN_EXISTING = 3; + public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + + // To call the win32 function without having to allocate memory in powershell + public static bool DeviceIoControlWrapperForDevDriveQuery(string volumeGuidPath) + { + uint notUsedSize = 0; + var outputVolumeInfo = new FILE_FS_PERSISTENT_VOLUME_INFORMATION { }; + var inputVolumeInfo = new FILE_FS_PERSISTENT_VOLUME_INFORMATION { }; + inputVolumeInfo.FlagMask = PERSISTENT_VOLUME_STATE_DEV_VOLUME; + inputVolumeInfo.Version = 1; + + var volumeFileHandle = CreateFile( + volumeGuidPath, + FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE, + IntPtr.Zero, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + if (volumeFileHandle.IsInvalid) + { + // Handle is invalid. + throw new Exception("CreateFile unable to get file handle for volume to check if its a Dev Drive volume", + new Win32Exception(Marshal.GetLastWin32Error())); + } + + // We need to allocated memory for the structures so we can marshal and unmarshal them. + IntPtr inputVolptr = Marshal.AllocHGlobal(Marshal.SizeOf(inputVolumeInfo)); + IntPtr outputVolptr = Marshal.AllocHGlobal(Marshal.SizeOf(outputVolumeInfo)); + + try + { + Marshal.StructureToPtr(inputVolumeInfo, inputVolptr, false); + + var result = DeviceIoControl( + volumeFileHandle, + FSCTL_QUERY_PERSISTENT_VOLUME_STATE, + inputVolptr, + (uint)Marshal.SizeOf(inputVolumeInfo), + outputVolptr, + (uint)Marshal.SizeOf(outputVolumeInfo), + out notUsedSize, + IntPtr.Zero); + + if (!result) + { + // Can't query volume. + throw new Exception("DeviceIoControl unable to query if volume is a Dev Drive volume", + new Win32Exception(Marshal.GetLastWin32Error())); + } + + // Unmarshal the output structure + outputVolumeInfo = (FILE_FS_PERSISTENT_VOLUME_INFORMATION) Marshal.PtrToStructure( + outputVolptr, + typeof(FILE_FS_PERSISTENT_VOLUME_INFORMATION) + ); + + // Check that the output flag is set to Dev Drive volume. + if ((outputVolumeInfo.VolumeFlags & PERSISTENT_VOLUME_STATE_DEV_VOLUME) > 0) + { + // Volume is a Dev Drive volume. + return true; + } + + return false; + } + finally + { + // Free the memory we allocated. + Marshal.FreeHGlobal(inputVolptr); + Marshal.FreeHGlobal(outputVolptr); + volumeFileHandle.Close(); + } + } +'@ + if (([System.Management.Automation.PSTypeName]'DevDrive.DevDriveHelper').Type) + { + $script:DevDriveWin32Helper = ([System.Management.Automation.PSTypeName]'DevDrive.DevDriveHelper').Type + } + else + { + # Note: when recompiling changes to the C# code above you'll need to close the powershell session and reopen a new one. + $script:DevDriveWin32Helper = Add-Type ` + -Namespace 'DevDrive' ` + -Name 'DevDriveHelper' ` + -MemberDefinition $DevDriveHelperDefinitions ` + -UsingNamespace ` + 'System.ComponentModel', + 'Microsoft.Win32.SafeHandles' + } + + return $script:DevDriveWin32Helper +} # end function Get-DevDriveWin32HelperScript + +<# + .SYNOPSIS + Invokes win32 IsApiSetImplemented function. + + .PARAMETER Contract + Specifies the contract string for the dll that houses the win32 function. +#> +function Invoke-IsApiSetImplemented +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Contract + ) + + $helper = Get-DevDriveWin32HelperScript + return $helper::IsApiSetImplemented($Contract) +} # end function Invoke-IsApiSetImplemented + +<# + .SYNOPSIS + Invokes win32 GetDeveloperDriveEnablementState function. +#> +function Get-DevDriveEnablementState +{ + [CmdletBinding()] + [OutputType([System.Enum])] + param + () + + $helper = Get-DevDriveWin32HelperScript + return $helper::GetDeveloperDriveEnablementState() +} # end function Get-DevDriveEnablementState + +<# + .SYNOPSIS + Validates whether the Dev Drive feature is available and enabled on the system. +#> +function Assert-DevDriveFeatureAvailable +{ + [CmdletBinding()] + [OutputType([System.Void])] + param + () + + $devDriveHelper = Get-DevDriveWin32HelperScript + Write-Verbose -Message ($script:localizedData.CheckingDevDriveEnablementMessage) + + $IsApiSetImplemented = Invoke-IsApiSetImplemented('api-ms-win-core-sysinfo-l1-2-6') + $DevDriveEnablementType = [DevDrive.DevDriveHelper+DEVELOPER_DRIVE_ENABLEMENT_STATE] + + if ($IsApiSetImplemented) + { + try + { + # Based on the enablement result we will throw an error or return without doing anything. + switch (Get-DevDriveEnablementState) + { + ($DevDriveEnablementType::DeveloperDriveEnablementStateError) + { + throw $script:localizedData.DevDriveEnablementUnknownError + } + ($DevDriveEnablementType::DeveloperDriveDisabledBySystemPolicy) + { + throw $script:localizedData.DevDriveDisabledBySystemPolicyError + } + ($DevDriveEnablementType::DeveloperDriveDisabledByGroupPolicy) + { + throw $script:localizedData.DevDriveDisabledByGroupPolicyError + } + ($DevDriveEnablementType::DeveloperDriveEnabled) + { + Write-Verbose -Message ($script:localizedData.DevDriveEnabledMessage) + return + } + default + { + throw $script:localizedData.DevDriveEnablementUnknownError + } + } + } + # function may not exist in some versions of Windows in the apiset dll. + catch [System.EntryPointNotFoundException] + { + Write-Verbose $_.Exception.Message + } + } + + <# + If apiset isn't implemented or we get the EntryPointNotFoundException we should throw + since the feature isn't available here. + #> + throw $script:localizedData.DevDriveFeatureNotImplementedError +} # end function Assert-DevDriveFeatureAvailable + +<# + .SYNOPSIS + Validates that ReFs is supplied when attempting to format a volume as a Dev Drive. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. +#> +function Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FSFormat + ) + + if ($FSFormat -ne 'ReFS') + { + New-InvalidArgumentException ` + -Message $($script:localizedData.FSFormatNotReFSWhenDevDriveFlagIsTrueError -f 'ReFS', $FSFormat) ` + -ArgumentName 'FSFormat' + } + +} # end function Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue + +<# + .SYNOPSIS + Validates that the user entered a size greater than the minimum for Dev Drive volumes. + (The minimum is 50 Gb) + + .PARAMETER UserDesiredSize + Specifies the size the user wants to create the Dev Drive volume with. +#> +function Assert-SizeMeetsMinimumDevDriveRequirement +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.UInt64] + $UserDesiredSize + ) + + # 50 Gb is the minimum size for Dev Drive volumes. + $UserDesiredSizeInGb = [Math]::Round($UserDesiredSize / 1GB, 2) + $minimumSizeForDevDriveInGb = 50 + + if ($UserDesiredSizeInGb -lt $minimumSizeForDevDriveInGb) + { + throw ($script:localizedData.MinimumSizeNeededToCreateDevDriveVolumeError -f $UserDesiredSizeInGb ) + } + +} # end function Assert-SizeMeetsMinimumDevDriveRequirement + +<# +.SYNOPSIS + Invokes the wrapper for the DeviceIoControl Win32 API function. + +.PARAMETER VolumeGuidPath + The guid path of the volume that will be queried. +#> +function Invoke-DeviceIoControlWrapperForDevDriveQuery +{ + [CmdletBinding()] + [OutputType([System.boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VolumeGuidPath + ) + + $devDriveHelper = Get-DevDriveWin32HelperScript + + return $devDriveHelper::DeviceIoControlWrapperForDevDriveQuery($VolumeGuidPath) + +}# end function Invoke-DeviceIoControlWrapperForDevDriveQuery + +<# + .SYNOPSIS + Validates that a volume is a Dev Drive volume. This is temporary until a way to do + this is added to the Storage Powershell library to query whether the volume is a Dev Drive volume + or not. + + .PARAMETER VolumeGuidPath + The guid path of the volume that will be queried. +#> +function Test-DevDriveVolume +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VolumeGuidPath + ) + + $devDriveHelper = Get-DevDriveWin32HelperScript + + return Invoke-DeviceIoControlWrapperForDevDriveQuery -VolumeGuidPath $VolumeGuidPath +}# end function Test-DevDriveVolume + +<# + .SYNOPSIS + Compares two values that are in bytes to see whether they are equal when converted to gigabytes. + We return true if they are equal and false if they are not. + + .PARAMETER SizeAInBytes + The size of the first value in bytes. + + .PARAMETER SizeBInBytes + The size of the second value in bytes. +#> +function Compare-SizeUsingGB +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.UInt64] + $SizeAInBytes, + + [Parameter(Mandatory = $true)] + [System.UInt64] + $SizeBInBytes + ) + + $SizeAInGb = [Math]::Round($SizeAInBytes / 1GB, 2) + $SizeBInGb = [Math]::Round($SizeBInBytes / 1GB, 2) + + return $SizeAInGb -eq $SizeBInGb + +}# end function Compare-SizeUsingGB + Export-ModuleMember -Function @( 'Restart-ServiceIfExists', 'Assert-DriveLetterValid', 'Assert-AccessPathValid', 'Get-DiskByIdentifier', - 'Test-AccessPathAssignedToLocal' + 'Test-AccessPathAssignedToLocal', + 'Assert-DevDriveFeatureAvailable', + 'Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue', + 'Assert-SizeMeetsMinimumDevDriveRequirement', + 'Get-DevDriveWin32HelperScript', + 'Invoke-IsApiSetImplemented', + 'Get-DevDriveEnablementState', + 'Test-DevDriveVolume', + 'Invoke-DeviceIoControlWrapperForDevDriveQuery', + 'Compare-SizeUsingGB' ) diff --git a/source/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 b/source/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 index bb3dde13..8af13fea 100644 --- a/source/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 +++ b/source/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 @@ -4,4 +4,12 @@ ConvertFrom-StringData @' UnknownService = Unable to find the desired service. InvalidDriveLetterFormatError = Drive Letter format '{0}' is not valid. InvalidAccessPathError = Access Path '{0}' is not found. + DevDriveEnablementUnknownError = Unable to get Dev Drive enablement status. + DevDriveDisabledBySystemPolicyError = Dev Drive feature disabled due to system policy. + DevDriveDisabledByGroupPolicyError = Dev Drive feature disabled due to group policy. + DevDriveEnabledMessage = Dev Drive feature is enabled on this system. + CheckingDevDriveEnablementMessage = Checking if the Dev Drive feature is available and enabled on the system. + DevDriveFeatureNotImplementedError = Dev Drive feature is not implemented on this system. + MinimumSizeNeededToCreateDevDriveVolumeError = To configure a volume as a Dev Drive volume the size parameter must be 50 Gb or more. Size of '{0} Gb' was specified. + FSFormatNotReFSWhenDevDriveFlagIsTrueError = Only the 'ReFS' file system can be used with FSFormat when the Dev Drive flag is set to true. '@ diff --git a/source/StorageDsc.psd1 b/source/StorageDsc.psd1 index 2b9632f2..c1beb719 100644 --- a/source/StorageDsc.psd1 +++ b/source/StorageDsc.psd1 @@ -52,7 +52,7 @@ Prerelease = '' # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Disk', 'Storage', 'Partition', 'Volume') + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Disk', 'Storage', 'Partition', 'Volume', 'Dev Drive') # A URL to the license for this module. LicenseUri = 'https://github.com/dsccommunity/StorageDsc/blob/main/LICENSE' diff --git a/tests/Unit/DSC_Disk.Tests.ps1 b/tests/Unit/DSC_Disk.Tests.ps1 index 00cc64b2..9ffddcac 100644 --- a/tests/Unit/DSC_Disk.Tests.ps1 +++ b/tests/Unit/DSC_Disk.Tests.ps1 @@ -19,6 +19,7 @@ function Invoke-TestSetup -TestType 'Unit' Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + $modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' } function Invoke-TestCleanup @@ -32,7 +33,10 @@ Invoke-TestSetup try { InModuleScope $script:dscResourceName { - $script:testDriveLetter = 'G' + $script:testDriveLetterG = 'G' + $script:testDriveLetterH = 'H' + $script:testDriveLetterK = 'K' + $script:testDriveLetterT = 'T' $script:testDiskNumber = 1 $script:testDiskUniqueId = 'TESTDISKUNIQUEID' $script:testDiskFriendlyName = 'TESTDISKFRIENDLYNAME' @@ -105,6 +109,81 @@ try PartitionStyle = 'GPT' } + <# + Used in the scenario where a user wants to create a Dev Drive volume + and there is sufficient unallocated space available. + #> + $script:mockedDisk0GptForDevDriveResizeNotNeededScenario = [pscustomobject] @{ + Number = $script:testDiskNumber + UniqueId = $script:testDiskUniqueId + FriendlyName = $script:testDiskFriendlyName + SerialNumber = $script:testDiskSerialNumber + Guid = $script:testDiskGptGuid + IsOffline = $false + IsReadOnly = $false + PartitionStyle = 'GPT' + Size = 100Gb + } + + <# + Used in the scenario where a user wants to create a Dev Drive volume but there + is insufficient unallocated space available and a resize of any partition is not possibile. + #> + $script:mockedDisk0GptForDevDriveResizeNotPossibleScenario = [pscustomobject] @{ + Number = $script:testDiskNumber + UniqueId = $script:testDiskUniqueId + FriendlyName = $script:testDiskFriendlyName + SerialNumber = $script:testDiskSerialNumber + Guid = $script:testDiskGptGuid + IsOffline = $false + IsReadOnly = $false + PartitionStyle = 'GPT' + Size = 60Gb + } + + <# + Used in the scenario where a user wants to create a Dev Drive volume but there + is insufficient unallocated space available. However a resize of a partition possibile. + which will create new unallocated space for the new partition. + #> + $script:mockedDisk0GptForDevDriveResizePossibleScenario = [pscustomobject] @{ + Number = $script:testDiskNumber + UniqueId = $script:testDiskUniqueId + FriendlyName = $script:testDiskFriendlyName + SerialNumber = $script:testDiskSerialNumber + Guid = $script:testDiskGptGuid + IsOffline = $false + IsReadOnly = $false + PartitionStyle = 'GPT' + Size = 100Gb + } + + $script:mockedDisk0GptForDevDriveAfterResize = [pscustomobject] @{ + Number = $script:testDiskNumber + UniqueId = $script:testDiskUniqueId + FriendlyName = $script:testDiskFriendlyName + SerialNumber = $script:testDiskSerialNumber + Guid = $script:testDiskGptGuid + IsOffline = $false + IsReadOnly = $false + PartitionStyle = 'GPT' + Size = 100Gb + LargestFreeExtent = 50Gb + } + + $script:mockedDisk0RawForDevDrive = [pscustomobject] @{ + Number = $script:testDiskNumber + UniqueId = $script:testDiskUniqueId + FriendlyName = $script:testDiskFriendlyName + SerialNumber = $script:testDiskSerialNumber + Guid = '' + IsOffline = $false + IsReadOnly = $false + PartitionStyle = 'RAW' + Size = 80Gb + LargestFreeExtent = 0 + } + $script:mockedCim = [pscustomobject] @{ BlockSize = 4096 } @@ -112,25 +191,99 @@ try $script:mockedPartitionSize = 1GB $script:mockedPartition = [pscustomobject] @{ - DriveLetter = [System.Char] $script:testDriveLetter + DriveLetter = [System.Char] $script:testDriveLetterG Size = $script:mockedPartitionSize PartitionNumber = 1 Type = 'Basic' } + $script:mockedPartitionSize40Gb = 40GB + + $script:mockedPartitionSize50Gb = 50GB + + $script:mockedPartitionSize70Gb = 70GB + + $script:mockedPartitionSize100Gb = 100GB + + $script:mockedPartitionWithTDriveLetter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterT + Size = $script:mockedPartitionSize50Gb + PartitionNumber = 1 + Type = 'Basic' + } + + $script:mockedPartitionSupportedSizeForTDriveletter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterT + SizeMax = $script:mockedPartitionSize100Gb + SizeMin = $script:mockedPartitionSize10Gb + } + + $script:mockedPartitionWithGDriveletter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterG + Size = $script:mockedPartitionSize50Gb + PartitionNumber = 1 + Type = 'Basic' + } + + $script:mockedPartitionSupportedSizeForGDriveletter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterG + SizeMax = $script:mockedPartitionSize50Gb + SizeMin = $script:mockedPartitionSize50Gb + } + + $script:mockedPartitionWithHDriveLetter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterH + Size = $script:mockedPartitionSize50Gb + PartitionNumber = 1 + Type = 'Basic' + } + + $script:mockedPartitionSupportedSizeForHDriveletter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterH + SizeMax = $script:mockedPartitionSize100Gb + SizeMin = $script:mockedPartitionSize10Gb + } + + $script:mockedPartitionWithKDriveLetter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterK + Size = $script:mockedPartitionSize70Gb + PartitionNumber = 1 + Type = 'Basic' + } + + $script:mockedPartitionSupportedSizeForKDriveletter = [pscustomobject] @{ + DriveLetter = [System.Char] $script:testDriveLetterK + SizeMax = $script:mockedPartitionSize100Gb + SizeMin = $script:mockedPartitionSize + } + + $script:mockedPartitionListForResizeNotPossibleScenario = @( + $script:mockedPartitionWithGDriveletter + ) + + $script:mockedPartitionListForResizeNotNeededScenario = @( + $script:mockedPartitionWithGDriveletter, + $script:mockedPartitionWithHDriveLetter + ) + + $script:mockedPartitionListForResizePossibleScenario = @( + $script:mockedPartitionWithGDriveletter, + $script:mockedPartitionWithKDriveLetter + ) + <# This condition seems to occur in some systems where the same partition is reported twice with the same drive letter. #> $script:mockedPartitionMultiple = @( [pscustomobject] @{ - DriveLetter = [System.Char] $script:testDriveLetter + DriveLetter = [System.Char] $script:testDriveLetterG Size = $script:mockedPartitionSize PartitionNumber = 1 Type = 'Basic' }, [pscustomobject] @{ - DriveLetter = [System.Char] $script:testDriveLetter + DriveLetter = [System.Char] $script:testDriveLetterG Size = $script:mockedPartitionSize PartitionNumber = 1 Type = 'Basic' @@ -145,6 +298,46 @@ try IsReadOnly = $false } + $script:mockedPartitionNoDriveLetter50Gb = [pscustomobject] @{ + DriveLetter = [System.Char] $null + Size = $script:mockedPartitionSize50Gb + PartitionNumber = 1 + Type = 'Basic' + IsReadOnly = $false + } + + $script:mockedPartitionGDriveLetter40Gb = [pscustomobject] @{ + DriveLetter = [System.Char] $testDriveLetterG + Size = $script:mockedPartitionSize40Gb + PartitionNumber = 1 + Type = 'Basic' + IsReadOnly = $false + } + + $script:mockedPartitionGDriveLetter50Gb = [pscustomobject] @{ + DriveLetter = [System.Char] $testDriveLetterG + Size = $script:mockedPartitionSize50Gb + PartitionNumber = 1 + Type = 'Basic' + IsReadOnly = $false + } + + $script:mockedPartitionGDriveLetterAlternatePartition150Gb = [pscustomobject] @{ + DriveLetter = [System.Char] $testDriveLetterG + Size = $script:partitionFormattedByWindows150Gb + PartitionNumber = 1 + Type = 'Basic' + IsReadOnly = $false + } + + $script:mockedPartitionGDriveLetter150Gb = [pscustomobject] @{ + DriveLetter = [System.Char] $testDriveLetterG + Size = $script:userDesiredSize150Gb + PartitionNumber = 1 + Type = 'Basic' + IsReadOnly = $false + } + $script:mockedPartitionNoDriveLetterReadOnly = [pscustomobject] @{ DriveLetter = [System.Char] $null Size = $script:mockedPartitionSize @@ -156,7 +349,7 @@ try $script:mockedVolume = [pscustomobject] @{ FileSystemLabel = 'myLabel' FileSystem = 'NTFS' - DriveLetter = $script:testDriveLetter + DriveLetter = $script:testDriveLetterG } $script:mockedVolumeUnformatted = [pscustomobject] @{ @@ -174,13 +367,87 @@ try $script:mockedVolumeReFS = [pscustomobject] @{ FileSystemLabel = 'myLabel' FileSystem = 'ReFS' - DriveLetter = $script:testDriveLetter + DriveLetter = $script:testDriveLetterG + } + + $script:mockedVolumeDevDrive = [pscustomobject] @{ + FileSystemLabel = 'myLabel' + FileSystem = 'ReFS' + DriveLetter = $script:testDriveLetterG + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + } + + $script:mockedVolumeCreatedAfterNewPartiton = [pscustomobject] @{ + FileSystemLabel = '' + FileSystem = '' + DriveLetter = $script:testDriveLetterT + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + } + + $script:mockedVolumeThatExistPriorToConfiguration = [pscustomobject] @{ + FileSystemLabel = 'myLabel' + FileSystem = 'NTFS' + DriveLetter = $script:testDriveLetterT + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + Size = $script:mockedPartitionSize50Gb + } + + $script:mockedVolumeThatExistPriorToConfigurationReFS = [pscustomobject] @{ + FileSystemLabel = 'myLabel' + FileSystem = 'ReFS' + DriveLetter = $script:testDriveLetterT + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + Size = $script:mockedPartitionSize50Gb + } + + $script:mockedVolumeThatExistPriorToConfigurationNtfs150Gb = [pscustomobject] @{ + FileSystemLabel = 'myLabel' + FileSystem = 'NTFS' + DriveLetter = $script:testDriveLetterT + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + Size = $script:userDesiredSize150Gb + } + + $script:mockedVolumeThatExistPriorToConfigurationRefs150Gb = [pscustomobject] @{ + FileSystemLabel = 'myLabel' + FileSystem = 'ReFS' + DriveLetter = $script:testDriveLetterT + UniqueId = '\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\' + Size = $script:userDesiredSize150Gb } $script:parameterFilter_MockedDisk0Number = { $DiskId -eq $script:mockedDisk0Gpt.Number -and $DiskIdType -eq 'Number' } + $script:userDesiredSize150Gb = 150Gb + + # Alternate value in bytes that can represent a 150 Gb partition in a physical hard disk that has been formatted by Windows. + $script:partitionFormattedByWindows150Gb = 161060225024 + + $script:userDesiredSize50Gb = 50Gb + + $script:userDesiredSize40Gb = 40Gb + + $script:amountOfTimesGetDiskByIdentifierIsCalled = 0 + + function Get-PartitionSupportedSizeForDevDriveScenarios + { + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $DriveLetter + ) + + switch ($DriveLetter) { + 'G' { $script:mockedPartitionSupportedSizeForGDriveletter } + 'H' { $script:mockedPartitionSupportedSizeForHDriveletter } + 'K' { $script:mockedPartitionSupportedSizeForKDriveletter } + } + } + <# These functions are required to be able to mock functions where values are passed in via the pipeline. @@ -337,7 +604,11 @@ try [Parameter()] [Switch] - $Force + $Force, + + [Parameter()] + [System.Boolean] + $DevDrive ) } @@ -399,6 +670,46 @@ try ) } + function Get-IsApiSetImplemented + { + [CmdletBinding()] + Param + ( + [OutputType([System.Boolean])] + [String] + $Contract + ) + } + + function Get-DeveloperDriveEnablementState + { + [CmdletBinding()] + [OutputType([System.Enum])] + Param + () + } + + function Test-DevDriveVolume + { + [CmdletBinding()] + param + ( + [string] + $VolumeGuidPath + ) + } + + function Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FSFormat + ) + } + Describe 'DSC_Disk\Get-TargetResource' { Context 'When online GPT disk with a partition/volume and correct Drive Letter assigned using Disk Number' { # verifiable (should be called) mocks @@ -425,7 +736,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.Number)" { @@ -436,8 +747,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return size $($script:mockedPartition.Size)" { @@ -491,7 +802,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.Number)" { @@ -502,8 +813,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return size $($script:mockedPartition.Size)" { @@ -558,7 +869,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.UniqueId ` -DiskIdType 'UniqueId' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.UniqueId)" { @@ -569,8 +880,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return size $($script:mockedPartition.Size)" { @@ -625,7 +936,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.FriendlyName ` -DiskIdType 'FriendlyName' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.FriendlyName)" { @@ -636,8 +947,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return size $($script:mockedPartition.Size)" { @@ -692,7 +1003,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.SerialNumber ` -DiskIdType 'SerialNumber' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.SerialNumber)" { @@ -703,8 +1014,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return size $($script:mockedPartition.Size)" { @@ -759,7 +1070,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.Guid ` -DiskIdType 'Guid' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.Guid)" { @@ -770,8 +1081,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return Size $($script:mockedPartition.Size)" { @@ -826,7 +1137,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.Guid ` -DiskIdType 'Guid' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.Guid)" { @@ -837,8 +1148,8 @@ try $resource.PartitionStyle | Should -Be $script:mockedDisk0Gpt.PartitionStyle } - It "Should return DriveLetter $($script:testDriveLetter)" { - $resource.DriveLetter | Should -Be $script:testDriveLetter + It "Should return DriveLetter $($script:testDriveLetterG)" { + $resource.DriveLetter | Should -Be $script:testDriveLetterG } It "Should return Size $($script:mockedPartition.Size)" { @@ -889,7 +1200,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Gpt.Number)" { @@ -952,7 +1263,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Mbr.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Mbr.Number)" { @@ -1015,7 +1326,7 @@ try $resource = Get-TargetResource ` -DiskId $script:mockedDisk0Raw.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose It "Should return DiskId $($script:mockedDisk0Raw.Number)" { @@ -1055,9 +1366,108 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly 1 } } + + Context 'When volume on partition is a Dev Drive volume' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-CimInstance ` + -MockWith { $script:mockedCim } ` + -Verifiable + + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartition } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeDevDrive } ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $true } ` + -Verifiable + + $resource = Get-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -Verbose + + It "Should return DevDrive as $($true)" { + $resource.DevDrive | Should -BeTrue + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly 1 + Assert-MockCalled -CommandName Get-Volume -Exactly 1 + Assert-MockCalled -CommandName Test-DevDriveVolume -Exactly 1 + } + } + + Context 'When volume on partition is not a Dev Drive volume' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-CimInstance ` + -MockWith { $script:mockedCim } ` + -Verifiable + + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartition } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfiguration } ` + -Verifiable + + Mock -CommandName Test-DevDriveVolume ` + -MockWith { $false } ` + -Verifiable + + $resource = Get-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -Verbose + + It "Should return DevDrive as $($false)" { + $resource.DevDrive | Should -BeFalse + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly 1 + Assert-MockCalled -CommandName Get-Volume -Exactly 1 + Assert-MockCalled -CommandName Test-DevDriveVolume -Exactly 1 + } + } } Describe 'DSC_Disk\Set-TargetResource' { + BeforeAll { + $localizedCommonStrings = Get-LocalizedData -BaseDirectory "$modulePath\StorageDsc.Common" -FileName "StorageDsc.Common.strings.psd1" + } + Context 'When offline GPT disk using Disk Number' { # verifiable (should be called) mocks Mock ` @@ -1076,7 +1486,7 @@ try Mock ` -CommandName New-Partition ` - -ParameterFilter { $DriveLetter -eq $script:testDriveLetter } ` + -ParameterFilter { $DriveLetter -eq $script:testDriveLetterG} ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1100,7 +1510,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0GptOffline.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1114,7 +1524,7 @@ try Assert-MockCalled -CommandName Get-Partition -Exactly -Times 4 Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` - -ParameterFilter { $DriveLetter -eq $script:testDriveLetter } + -ParameterFilter { $DriveLetter -eq $script:testDriveLetterG} Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 } @@ -1139,7 +1549,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1165,7 +1575,7 @@ try Set-TargetResource ` -DiskId $script:mockedDisk0GptOffline.UniqueId ` -DiskIdType 'UniqueId' ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1180,7 +1590,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1206,7 +1616,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1232,7 +1642,7 @@ try Set-TargetResource ` -DiskId $script:mockedDisk0GptOffline.FriendlyName ` -DiskIdType 'FriendlyName' ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1247,7 +1657,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1273,7 +1683,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1299,7 +1709,7 @@ try Set-TargetResource ` -DiskId $script:mockedDisk0GptOffline.SerialNumber ` -DiskIdType 'SerialNumber' ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1314,7 +1724,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1340,7 +1750,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1366,7 +1776,7 @@ try Set-TargetResource ` -DiskId $script:mockedDisk0GptOffline.Guid ` -DiskIdType 'Guid' ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1381,7 +1791,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1407,7 +1817,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1432,7 +1842,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0GptReadonly.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1447,7 +1857,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1477,7 +1887,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1499,14 +1909,14 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0RawOffline.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } It 'Should call the correct mocks' { Assert-VerifiableMock - Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 2 ` -ParameterFilter $script:parameterFilter_MockedDisk0Number Assert-MockCalled -CommandName Set-Disk -Exactly -Times 1 Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 1 @@ -1514,7 +1924,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1540,7 +1950,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1565,7 +1975,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Raw.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size $script:mockedPartitionSize ` -AllocationUnitSize 64 ` -FSLabel 'MyDisk' ` @@ -1575,7 +1985,7 @@ try It 'Should call the correct mocks' { Assert-VerifiableMock - Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 2 ` -ParameterFilter $script:parameterFilter_MockedDisk0Number Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 1 @@ -1583,7 +1993,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1605,7 +2015,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1631,7 +2041,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1646,7 +2056,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 Assert-MockCalled -CommandName Set-Partition -Exactly -Times 1 @@ -1668,7 +2078,7 @@ try Mock ` -CommandName New-Partition ` - -ParameterFilter {$DriveLetter -eq $script:testDriveLetter} ` + -ParameterFilter {$DriveLetter -eq $script:testDriveLetterG} ` -MockWith { $script:mockedPartitionNoDriveLetterReadOnly } ` -Verifiable @@ -1690,7 +2100,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Throw $errorRecord } @@ -1718,7 +2128,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 0 Assert-MockCalled -CommandName Set-Volume -Exactly -Times 0 @@ -1741,7 +2151,7 @@ try Mock ` -CommandName New-Partition ` - -ParameterFilter {$DriveLetter -eq $script:testDriveLetter} ` + -ParameterFilter {$DriveLetter -eq $script:testDriveLetterG} ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -1759,7 +2169,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1780,7 +2190,7 @@ try Assert-MockCalled -CommandName Get-Volume -Exactly -Times 2 Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } Assert-MockCalled -CommandName Format-Volume -Exactly -Times 0 Assert-MockCalled -CommandName Set-Volume -Exactly -Times 0 @@ -1813,7 +2223,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Mbr.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Throw $errorRecord } @@ -1857,7 +2267,7 @@ try Set-TargetResource ` -DiskId $script:mockedDisk0Mbr.UniqueId ` -DiskIdType 'UniqueId' ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Verbose } | Should -Throw $errorRecord } @@ -1904,7 +2314,7 @@ try { Set-targetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Verbose } | Should -Not -Throw } @@ -1955,7 +2365,7 @@ try { Set-targetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -Size $script:mockedPartitionSize ` -Verbose } | Should -Not -Throw @@ -2118,7 +2528,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -FSLabel 'NewLabel' ` -Verbose } | Should -Not -Throw @@ -2155,7 +2565,7 @@ try Mock ` -CommandName New-Partition ` -ParameterFilter { - $DriveLetter -eq $script:testDriveLetter + $DriveLetter -eq $script:testDriveLetterG } ` -MockWith { $script:mockedPartitionNoDriveLetter } ` -Verifiable @@ -2178,7 +2588,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size ($script:mockedPartitionSize + 1024) ` -AllowDestructive $true ` -FSLabel 'NewLabel' ` @@ -2243,7 +2653,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size ($script:mockedPartitionSize + 1024) ` -AllowDestructive $true ` -FSLabel 'NewLabel' ` @@ -2315,7 +2725,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -AllowDestructive $true ` -FSLabel 'NewLabel' ` -Verbose @@ -2384,7 +2794,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size ($script:mockedPartitionSize + 1024) ` -AllowDestructive $true ` -FSLabel 'NewLabel' ` @@ -2434,6 +2844,7 @@ try Mock ` -CommandName Format-Volume ` + -MockWith { $script:mockedVolume } ` -Verifiable # mocks that should not be called @@ -2446,7 +2857,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size $script:mockedPartitionSize ` -FSFormat 'ReFS' ` -FSLabel 'NewLabel' ` @@ -2506,7 +2917,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size $script:mockedPartitionSize ` -FSLabel 'NewLabel' ` -AllowDestructive $true ` @@ -2598,7 +3009,7 @@ try { Set-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -Driveletter $script:testDriveLetter ` + -Driveletter $script:testDriveLetterG ` -Size $script:mockedPartitionSize ` -FSLabel 'NewLabel' ` -AllowDestructive $true ` @@ -2609,7 +3020,7 @@ try It 'Should call the correct mocks' { Assert-VerifiableMock - Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 2 ` + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 3 ` -ParameterFilter $script:parameterFilter_MockedDisk0Number Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 1 @@ -2622,35 +3033,445 @@ try Assert-MockCalled -CommandName Clear-Disk -Exactly -Times 1 } } - } - - Describe 'DSC_Disk\Test-TargetResource' { - Mock ` - -CommandName Get-CimInstance ` - -MockWith { $script:mockedCim } - Context 'When testing disk does not exist using Disk Number' { + Context 'When the DevDrive flag is true, the AllowDestructive flag is false and there is not enough space on the disk to create the partition' { # verifiable (should be called) mocks Mock ` -CommandName Get-DiskByIdentifier ` -ParameterFilter $script:parameterFilter_MockedDisk0Number ` - -MockWith { $script:mockedDisk0GptOffline } ` + -MockWith { $script:mockedDisk0GptForDevDriveResizeNotPossibleScenario } ` -Verifiable - # mocks that should not be called - Mock -CommandName Get-Volume - Mock -CommandName Get-Partition - Mock -CommandName Get-CimInstance + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionListForResizeNotPossibleScenario } ` + -Verifiable - $script:result = $null + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable - It 'Should not throw an exception' { - { - $script:result = Test-TargetResource ` - -DiskId $script:mockedDisk0GptOffline.Number ` - -DriveLetter $script:testDriveLetter ` - -AllocationUnitSize 4096 ` - -Verbose + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { & Get-PartitionSupportedSizeForDevDriveScenarios -DriveLetter $DriveLetter } ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + + $userDesiredSizeInGb = [Math]::Round($script:mockedPartitionSize50Gb / 1GB, 2) + + It 'Should throw an exception' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -Size $script:mockedPartitionSize50Gb ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -Verbose + } | Should -Throw -ExpectedMessage ($script:localizedData.FoundNoPartitionsThatCanResizedForDevDrive -f $userDesiredSizeInGb) + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, AllowDestructive is false and there is enough space on the disk to create the partition' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0GptForDevDriveResizeNotNeededScenario } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionListForResizeNotNeededScenario } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $true } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeCreatedAfterNewPartiton } ` + -Verifiable + + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { & Get-PartitionSupportedSizeForDevDriveScenarios -DriveLetter $DriveLetter } ` + -Verifiable + + Mock ` + -CommandName New-Partition ` + -MockWith { $script:mockedPartitionWithTDriveLetter } ` + -Verifiable + + Mock ` + -CommandName Format-Volume ` + -MockWith { $script:mockedVolumeCreatedAfterNewPartiton } ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + + It 'Should not throw an exception' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -Size $script:mockedPartitionSize50Gb ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 4 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 + Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 ` + -ParameterFilter { + $DevDrive -eq $true + } + } + } + + Context 'When the DevDrive flag is true, AllowDestructive flag is false and there is not enough unallocated disk space but a resize of a partition is possible to create new space' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0GptForDevDriveResizePossibleScenario } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionListForResizePossibleScenario } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { & Get-PartitionSupportedSizeForDevDriveScenarios -DriveLetter $DriveLetter } ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + + It 'Should throw an exception stating that AllowDestructive flag needs to be set to resize existing partition for DevDrive' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -Size $script:mockedPartitionSize50Gb ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -Verbose + } | Should -Throw -ExpectedMessage ($script:localizedData.AllowDestructiveNeededForDevDriveOperation -f $script:testDriveLetterK) + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, AllowDestructive flag is true and there is not enough unallocated disk space but a resize of a partition is possible to create new space' { + # verifiable (should be called) mocks + + $script:amountOfTimesGetDiskByIdentifierIsCalled = 0 + + # For resize scenario we need to call Get-DiskByIdentifier twice. After the resize a disk.FreeLargestExtent is updated. + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { + $script:amountOfTimesGetDiskByIdentifierIsCalled++ + + if ($script:amountOfTimesGetDiskByIdentifierIsCalled -eq 1) { + + $script:mockedDisk0GptForDevDriveResizePossibleScenario + } + elseif ($script:amountOfTimesGetDiskByIdentifierIsCalled -eq 2) { + + $script:mockedDisk0GptForDevDriveAfterResize + } + else { + $script:mockedDisk0GptForDevDriveResizePossibleScenario + } + } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionListForResizePossibleScenario } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { & Get-PartitionSupportedSizeForDevDriveScenarios -DriveLetter $DriveLetter } ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $true } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeCreatedAfterNewPartiton } ` + -Verifiable + + Mock ` + -CommandName New-Partition ` + -MockWith { $script:mockedPartitionWithTDriveLetter } ` + -Verifiable + + Mock ` + -CommandName Resize-Partition ` + -Verifiable + + Mock ` + -CommandName Format-Volume ` + -MockWith { $script:mockedVolumeCreatedAfterNewPartiton } ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + + It 'Should not throw an exception' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -Size $script:mockedPartitionSize50Gb ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 2 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 4 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 + Assert-MockCalled -CommandName Resize-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 ` + -ParameterFilter { + $DevDrive -eq $true + } + Assert-MockCalled -CommandName New-Partition -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, AllowDestructive is true, and a Partition that matches the users drive letter exists' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0GptForDevDriveResizeNotNeededScenario } ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $true } ` + -Verifiable + + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { $script:mockedPartitionSupportedSizeForTDriveletter } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionWithTDriveletter } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfiguration } ` + -Verifiable + + Mock ` + -CommandName Format-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfiguration } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + + It 'Should not throw an exception and overwrite the existing partition' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 + Assert-MockCalled -CommandName Assert-DevDriveFeatureAvailable -Exactly -Times 1 + Assert-MockCalled -CommandName Format-Volume -Exactly -Times 1 ` + -ParameterFilter { + $DevDrive -eq $true + } + } + } + + Context 'When the DevDrive flag is true, AllowDestructive is false, and a Partition that matches the users drive letter exists' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0GptForDevDriveResizeNotNeededScenario } ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $false } ` + -Verifiable + + Mock ` + -CommandName Get-PartitionSupportedSize ` + -MockWith { $script:mockedPartitionSupportedSizeForTDriveletter } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionWithTDriveletter } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfiguration } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Set-Disk + Mock -CommandName Initialize-Disk + Mock -CommandName Format-Volume + + It 'Should throw an exception advising that the volume was not formatted as a Dev Drive volume' { + { + Set-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -Driveletter $script:testDriveLetterT ` + -FSLabel 'NewLabel' ` + -FSFormat 'ReFS' ` + -DevDrive $true ` + -Verbose + } | Should -Throw -ExpectedMessage ($script:localizedData.FailedToConfigureDevDriveVolume ` + -f $script:mockedVolumeThatExistPriorToConfiguration.UniqueId, $script:testDriveLetterT) + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Set-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Initialize-Disk -Exactly -Times 0 + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 + Assert-MockCalled -CommandName Assert-DevDriveFeatureAvailable -Exactly -Times 1 + Assert-MockCalled -CommandName Format-Volume -Exactly -Times 0 ` + -ParameterFilter { + $DevDrive -eq $true + } + } + } + } + + Describe 'DSC_Disk\Test-TargetResource' { + Mock ` + -CommandName Get-CimInstance ` + -MockWith { $script:mockedCim } + + Context 'When testing disk does not exist using Disk Number' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0GptOffline } ` + -Verifiable + + # mocks that should not be called + Mock -CommandName Get-Volume + Mock -CommandName Get-Partition + Mock -CommandName Get-CimInstance + + $script:result = $null + + It 'Should not throw an exception' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0GptOffline.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Verbose } | Should -Not -Throw } @@ -2687,7 +3508,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptOffline.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2727,7 +3548,7 @@ try $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptOffline.UniqueId ` -DiskIdType 'UniqueId' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2767,7 +3588,7 @@ try $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptOffline.FriendlyName ` -DiskIdType 'FriendlyName' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2807,7 +3628,7 @@ try $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptOffline.SerialNumber ` -DiskIdType 'SerialNumber' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2847,7 +3668,7 @@ try $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptOffline.Guid ` -DiskIdType 'Guid' ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2886,7 +3707,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0GptReadonly.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2925,7 +3746,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Raw.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -2966,7 +3787,7 @@ try { Test-TargetResource ` -DiskId $script:mockedDisk0Mbr.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Throw $errorRecord @@ -3003,7 +3824,7 @@ try { Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -PartitionStyle 'MBR' ` -Verbose @@ -3039,7 +3860,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -PartitionStyle 'MBR' ` -AllowDestructive $true ` @@ -3091,7 +3912,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Size ($script:mockedPartitionSize + 1MB) ` -Verbose @@ -3136,7 +3957,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Size ($script:mockedPartitionSize + 1MB) ` -AllowDestructive $true ` @@ -3199,7 +4020,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -3260,7 +4081,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Verbose } | Should -Not -Throw @@ -3315,7 +4136,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -AllowDestructive $true ` -Verbose @@ -3368,7 +4189,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -AllowDestructive $true ` -Verbose @@ -3416,7 +4237,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4097 ` -AllowDestructive $true ` -Verbose @@ -3465,7 +4286,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -FSFormat 'ReFS' ` -Verbose } | Should -Not -Throw @@ -3514,7 +4335,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -FSFormat 'ReFS' ` -AllowDestructive $true ` -Verbose @@ -3564,7 +4385,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -FSLabel 'NewLabel' ` -Verbose } | Should -Not -Throw @@ -3659,7 +4480,7 @@ try { $script:result = Test-TargetResource ` -DiskId $script:mockedDisk0Gpt.Number ` - -DriveLetter $script:testDriveLetter ` + -DriveLetter $script:testDriveLetterG ` -AllocationUnitSize 4096 ` -Size $script:mockedPartition.Size ` -FSLabel $script:mockedVolume.FileSystemLabel ` @@ -3681,6 +4502,270 @@ try Assert-MockCalled -CommandName Get-CimInstance -Exactly -Times 1 } } + + Context 'When the DevDrive flag is true, and Size parameter is less than minimum required size for Dev Drive (50 Gb)' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $mockedPartitionGDriveLetter40Gb } ` + -Verifiable + + $userDesiredSizeInGb = [Math]::Round($script:userDesiredSize40Gb / 1GB, 2) + + It 'Should throw an exception as size does not meet minimum requirement' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Size $script:userDesiredSize40Gb ` + -FSLabel $script:mockedVolume.FileSystemLabel ` + -FSFormat $script:mockedVolumeReFS.FileSystem ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Throw -ExpectedMessage ($script:localizedCommonStrings.MinimumSizeNeededToCreateDevDriveVolumeError -f $userDesiredSizeInGb) + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, but the partition is effectively the same size as user inputted size and volume is NTFS' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionGDriveLetterAlternatePartition150Gb } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfigurationNtfs150Gb } ` + -Verifiable + + $script:result = $null + + It 'Should not throw an exception' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Size $script:userDesiredSize50Gb ` + -FSLabel $script:mockedVolume.FileSystemLabel ` + -FSFormat $script:mockedVolumeReFS.FileSystem ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should be false' { + $script:result | Should -BeFalse + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, but the partition is not the same size as user inputted size, volume is ReFS formatted but not Dev Drive volume' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionGDriveLetterAlternatePartition150Gb } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfigurationReFS } ` + -Verifiable + + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $false } ` + -Verifiable + + $script:result = $null + + It 'Should not throw an exception' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Size $script:userDesiredSize50Gb ` + -FSLabel $script:mockedVolume.FileSystemLabel ` + -FSFormat $script:mockedVolumeReFS.FileSystem ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should be false' { + $script:result | Should -BeFalse + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 2 + Assert-MockCalled -CommandName Test-DevDriveVolume -Exactly -Times 1 + Assert-MockCalled -CommandName Assert-DevDriveFeatureAvailable -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, but the partition is effectively the same size as user inputted size, volume is ReFS formatted and is Dev Drive volume' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionGDriveLetterAlternatePartition150Gb } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeDevDrive } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $true } ` + -Verifiable + + $script:result = $null + + It 'Should not throw an exception' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Size $script:userDesiredSize50Gb ` + -FSLabel $script:mockedVolume.FileSystemLabel ` + -FSFormat $script:mockedVolumeReFS.FileSystem ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should be true' { + $script:result | Should -BeTrue + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 2 + Assert-MockCalled -CommandName Test-DevDriveVolume -Exactly -Times 1 + Assert-MockCalled -CommandName Assert-DevDriveFeatureAvailable -Exactly -Times 1 + } + } + + Context 'When the DevDrive flag is true, but the partition is effectively the same size as user inputted size, volume is ReFS formatted and is not Dev Drive volume' { + # verifiable (should be called) mocks + Mock ` + -CommandName Get-DiskByIdentifier ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number ` + -MockWith { $script:mockedDisk0Gpt } ` + -Verifiable + + Mock ` + -CommandName Get-Partition ` + -MockWith { $script:mockedPartitionGDriveLetterAlternatePartition150Gb } ` + -Verifiable + + Mock ` + -CommandName Get-Volume ` + -MockWith { $script:mockedVolumeThatExistPriorToConfigurationRefs150Gb } ` + -Verifiable + + Mock ` + -CommandName Assert-DevDriveFeatureAvailable ` + -Verifiable + + Mock ` + -CommandName Test-DevDriveVolume ` + -MockWith { $false } ` + -Verifiable + + $script:result = $null + + It 'Should not throw an exception' { + { + $script:result = Test-TargetResource ` + -DiskId $script:mockedDisk0Gpt.Number ` + -DriveLetter $script:testDriveLetterG ` + -AllocationUnitSize 4096 ` + -Size $script:userDesiredSize50Gb ` + -FSLabel $script:mockedVolume.FileSystemLabel ` + -FSFormat $script:mockedVolumeReFS.FileSystem ` + -DevDrive $true ` + -AllowDestructive $true ` + -Verbose + } | Should -Not -Throw + } + + It 'Should be false' { + $script:result | Should -BeFalse + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-DiskByIdentifier -Exactly -Times 1 ` + -ParameterFilter $script:parameterFilter_MockedDisk0Number + Assert-MockCalled -CommandName Get-Partition -Exactly -Times 1 + Assert-MockCalled -CommandName Get-Volume -Exactly -Times 2 + Assert-MockCalled -CommandName Test-DevDriveVolume -Exactly -Times 1 + Assert-MockCalled -CommandName Assert-DevDriveFeatureAvailable -Exactly -Times 1 + } + } } } } diff --git a/tests/Unit/StorageDsc.Common.Tests.ps1 b/tests/Unit/StorageDsc.Common.Tests.ps1 index a83de198..bfe959b5 100644 --- a/tests/Unit/StorageDsc.Common.Tests.ps1 +++ b/tests/Unit/StorageDsc.Common.Tests.ps1 @@ -25,6 +25,25 @@ Import-Module $script:subModuleFile -Force -ErrorAction Stop Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') InModuleScope $script:subModuleName { + + $script:mockedSizesForDevDriveScenario = [pscustomobject] @{ + UserDesired0Gb = 0Gb + UserDesired10Gb = 10Gb + UserDesired50Gb = 50Gb + UserDesired60Gb = 60Gb + CurrentDiskFreeSpace40Gb = 40Gb + CurrentDiskFreeSpace50Gb = 50Gb + CurrentDiskFreeSpace60Gb = 60Gb + + # Alternate value in bytes that can represent a 150 Gb partition in a physical hard disk that has been formatted by Windows. + SizeAEffectively150GbInBytes = 161060225024 + + SizeB150GbInBytes = 150Gb + SizeBInBytesFor49Point99Scenario = 49.99Gb + } + + $script:mockedDiskNumber = 1 + function Get-Disk { [CmdletBinding()] @@ -512,4 +531,283 @@ InModuleScope $script:subModuleName { } } } + + Describe 'StorageDsc.Common\Assert-DevDriveFeatureAvailable' -Tag 'Assert-DevDriveFeatureAvailable' { + Context 'When testing the Dev Drive enablement state and the dev drive feature not implemented' { + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $false } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should throw with DevDriveFeatureNotImplementedError' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Throw -ExpectedMessage $LocalizedData.DevDriveFeatureNotImplementedError + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + } + } + + Context 'When testing the Dev Drive enablement state returns an enablement state not defined in the enum' { + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + Mock ` + -CommandName Get-DevDriveEnablementState ` + -MockWith { $null } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should throw with DevDriveEnablementUnknownError' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Throw -ExpectedMessage $LocalizedData.DevDriveEnablementUnknownError + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DevDriveEnablementState -Exactly -Times 1 + } + } + + Context 'When testing the Dev Drive enablement state and the dev drive feature is disabled by group policy' { + Get-DevDriveWin32HelperScript + $DevDriveEnablementType = [DevDrive.DevDriveHelper+DEVELOPER_DRIVE_ENABLEMENT_STATE] + + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + Mock ` + -CommandName Get-DevDriveEnablementState ` + -MockWith { $DevDriveEnablementType::DeveloperDriveDisabledByGroupPolicy } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should throw with DevDriveDisabledByGroupPolicyError' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Throw -ExpectedMessage $LocalizedData.DevDriveDisabledByGroupPolicyError + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DevDriveEnablementState -Exactly -Times 1 + } + } + + Context 'When testing the Dev Drive enablement state and the dev drive feature is disabled by system policy' { + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + Mock ` + -CommandName Get-DevDriveEnablementState ` + -MockWith { $DevDriveEnablementType::DeveloperDriveDisabledBySystemPolicy } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should throw with DeveloperDriveDisabledBySystemPolicy' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Throw -ExpectedMessage $LocalizedData.DeveloperDriveDisabledBySystemPolicy + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DevDriveEnablementState -Exactly -Times 1 + } + } + + Context 'When testing the Dev Drive enablement state and the enablement state is unknown' { + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + Mock ` + -CommandName Get-DevDriveEnablementState ` + -MockWith { $DevDriveEnablementType::DeveloperDriveEnablementStateError } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should throw with DevDriveEnablementUnknownError' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Throw -ExpectedMessage $LocalizedData.DevDriveEnablementUnknownError + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DevDriveEnablementState -Exactly -Times 1 + } + } + + Context 'When testing Dev Drive enablement state and the enablement state is set to enabled' { + Mock ` + -CommandName Invoke-IsApiSetImplemented ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + Mock ` + -CommandName Get-DevDriveEnablementState ` + -MockWith { $DevDriveEnablementType::DeveloperDriveEnabled } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + It 'Should not throw' { + { + Assert-DevDriveFeatureAvailable -Verbose + } | Should -Not -Throw + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-IsApiSetImplemented -Exactly -Times 1 + Assert-MockCalled -CommandName Get-DevDriveEnablementState -Exactly -Times 1 + } + } + } + + Describe 'StorageDsc.Common\Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue' -Tag 'Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue' { + Context 'When testing that only the ReFS file system is allowed' { + + $errorRecord = Get-InvalidArgumentRecord ` + -Message ($script:localizedData.FSFormatNotReFSWhenDevDriveFlagIsTrueError ) ` + -ArgumentName 'FSFormat' + + It 'Should throw invalid argument error if a filesystem other than ReFS is passed in' { + { + Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue -FSFormat "test" -Verbose + } | Should -Throw $errorRecord + } + } + + Context 'When testing Exception not thrown in Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue when ReFS file system passed in' { + + It 'Should not throw invalid argument error if ReFS filesystem is passed in' { + { + Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue -FSFormat "ReFS" -Verbose + } | Should -Not -Throw + } + } + } + + Describe 'StorageDsc.Common\Assert-SizeMeetsMinimumDevDriveRequirement' -Tag 'Assert-SizeMeetsMinimumDevDriveRequirement' { + Context 'When UserDesiredSize does not meet the minimum size for Dev Drive volumes' { + + $UserDesiredSizeInGb = [Math]::Round($mockedSizesForDevDriveScenario.UserDesired10Gb / 1GB, 2) + It 'Should throw invalid argument error' { + { + Assert-SizeMeetsMinimumDevDriveRequirement ` + -UserDesiredSize $mockedSizesForDevDriveScenario.UserDesired10Gb ` + -Verbose + } | Should -Throw -ExpectedMessage ($script:localizedCommonStrings.MinimumSizeNeededToCreateDevDriveVolumeError -f $UserDesiredSizeInGb) + } + } + + Context 'When UserDesiredSize meets the minimum size for Dev Drive volumes' { + + It 'Should not throw invalid argument error' { + { + Assert-SizeMeetsMinimumDevDriveRequirement ` + -UserDesiredSize $mockedSizesForDevDriveScenario.UserDesired50Gb ` + -Verbose + } | Should -Not -Throw + } + } + } + + Describe 'StorageDsc.Common\Test-DevDriveVolume' -Tag 'Test-DevDriveVolume' { + Context 'When testing whether a volume is a Dev Drive volume and the volume is a Dev Drive volume' { + + Mock ` + -CommandName Invoke-DeviceIoControlWrapperForDevDriveQuery ` + -MockWith { $true } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + $result = Test-DevDriveVolume -VolumeGuidPath "\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7924}\" -Verbose + It 'Should return true' { + $result | Should -Be $true + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-DeviceIoControlWrapperForDevDriveQuery -Exactly -Times 1 + } + + } + + Context 'When testing whether a volume is a Dev Drive volume and the volume is not a Dev Drive volume' { + + Mock ` + -CommandName Invoke-DeviceIoControlWrapperForDevDriveQuery ` + -MockWith { $false } ` + -ModuleName StorageDsc.Common ` + -Verifiable + + $result = Test-DevDriveVolume -VolumeGuidPath "\\?\Volume{3a244a32-efba-4b7e-9a19-7293fc7c7923}\" -Verbose + It 'Should return false' { + $result | Should -Be $false + } + + It 'Should call the correct mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Invoke-DeviceIoControlWrapperForDevDriveQuery -Exactly -Times 1 + } + } + } + + Describe 'StorageDsc.Common\Compare-SizeUsingGB' -Tag 'Compare-SizeUsingGB' { + Context 'When comparing whether SizeAInBytes is equal to SizeBInBytes when SizeAInBytes is effectively equal to SizeBInBytes when both are converted to Gb' { + + $result = Compare-SizeUsingGB ` + -SizeAInBytes $mockedSizesForDevDriveScenario.SizeB150GbInBytes ` + -SizeBInBytes $mockedSizesForDevDriveScenario.SizeAEffectively150GbInBytes + + It 'Should return true' { + $result | Should -BeTrue + } + } + + Context 'When comparing whether SizeAInBytes is equal to SizeBInBytes when SizeAInBytes is equal to SizeBInBytes when both are converted to Gb' { + + $result = Compare-SizeUsingGB ` + -SizeAInBytes $mockedSizesForDevDriveScenario.UserDesired50Gb ` + -SizeBInBytes $mockedSizesForDevDriveScenario.CurrentDiskFreeSpace50Gb + + It 'Should return true' { + $result | Should -BeTrue + } + } + + Context 'When comparing whether SizeAInBytes is equal to SizeBInBytes when SizeAInBytes is not equal to SizeBInBytes when both are converted to Gb' { + + $result = Compare-SizeUsingGB ` + -SizeAInBytes $mockedSizesForDevDriveScenario.UserDesired50Gb ` + -SizeBInBytes $mockedSizesForDevDriveScenario.SizeBInBytesFor49Point99Scenario + + It 'Should return false' { + $result | Should -BeFalse + } + } + } }