Modules/StorageDsc.Common/StorageDsc.Common.psm1
$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') # Import Localization Strings $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' <# .SYNOPSIS Restarts a System Service .PARAMETER Name Name of the service to be restarted. #> function Restart-ServiceIfExists { [CmdletBinding()] param ( [Parameter()] [System.String] $Name ) Write-Verbose -Message ($script:localizedData.GetServiceInformation -f $Name) -Verbose $servicesService = Get-Service @PSBoundParameters -ErrorAction Continue if ($servicesService) { Write-Verbose -Message ($script:localizedData.RestartService -f $Name) -Verbose $servicesService | Restart-Service -Force -ErrorAction Stop -Verbose } else { Write-Verbose -Message ($script:localizedData.UnknownService -f $Name) -Verbose } } <# .SYNOPSIS Validates a Drive Letter, removing or adding the trailing colon if required. .PARAMETER DriveLetter The Drive Letter string to validate. .PARAMETER Colon Will ensure the returned string will include or exclude a colon. #> function Assert-DriveLetterValid { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DriveLetter, [Parameter()] [Switch] $Colon ) $matches = @([regex]::matches($DriveLetter, '^([A-Za-z]):?$', 'IgnoreCase')) if (-not $matches) { # DriveLetter format is invalid New-InvalidArgumentException ` -Message $($script:localizedData.InvalidDriveLetterFormatError -f $DriveLetter) ` -ArgumentName 'DriveLetter' } # This is the drive letter without a colon $DriveLetter = $matches.Groups[1].Value if ($Colon) { $DriveLetter = $DriveLetter + ':' } # if return $DriveLetter } # end function Assert-DriveLetterValid <# .SYNOPSIS Validates an Access Path, removing or adding the trailing slash if required. If the Access Path does not exist or is not a folder then an exception will be thrown. .PARAMETER AccessPath The Access Path string to validate. .PARAMETER Slash Will ensure the returned path will include or exclude a slash. #> function Assert-AccessPathValid { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $AccessPath, [Parameter()] [Switch] $Slash ) if (-not (Test-Path -Path $AccessPath -PathType Container)) { # AccessPath is invalid New-InvalidArgumentException ` -Message $($script:localizedData.InvalidAccessPathError -f $AccessPath) ` -ArgumentName 'AccessPath' } # if # Remove or Add the trailing slash if ($AccessPath.EndsWith('\')) { if (-not $Slash) { $AccessPath = $AccessPath.TrimEnd('\') } # if } else { if ($Slash) { $AccessPath = "$AccessPath\" } # if } # if return $AccessPath } # end function Assert-AccessPathValid <# .SYNOPSIS Retrieves a Disk object matching the disk Id and Id type provided. .PARAMETER DiskId Specifies the disk identifier for the disk to retrieve. .PARAMETER DiskIdType Specifies the identifier type the DiskId contains. Defaults to Number. #> function Get-DiskByIdentifier { [CmdletBinding()] [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true)] [System.String] $DiskId, [Parameter()] [ValidateSet('Number', 'UniqueId', 'Guid', 'Location', 'FriendlyName', 'SerialNumber')] [System.String] $DiskIdType = 'Number' ) switch -regex ($DiskIdType) { 'Number|UniqueId|FriendlyName|SerialNumber' # for filters supported by the Get-Disk CmdLet { $diskIdParameter = @{ $DiskIdType = $DiskId } $disk = Get-Disk ` @diskIdParameter ` -ErrorAction SilentlyContinue break } default # for filters requiring Where-Object { $disk = Get-Disk -ErrorAction SilentlyContinue | Where-Object -Property $DiskIdType -EQ $DiskId } } return $disk } # end function Get-DiskByIdentifier <# .SYNOPSIS Tests if any of the access paths from a partition are assigned to a local path. .PARAMETER AccessPath Specifies the access paths that are assigned to the partition. #> function Test-AccessPathAssignedToLocal { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String[]] $AccessPath ) $accessPathAssigned = $false foreach ($path in $AccessPath) { if ($path -match '[a-zA-Z]:\\') { $accessPathAssigned = $true break } } 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', 'Assert-DevDriveFeatureAvailable', 'Assert-FSFormatIsReFsWhenDevDriveFlagSetToTrue', 'Assert-SizeMeetsMinimumDevDriveRequirement', 'Get-DevDriveWin32HelperScript', 'Invoke-IsApiSetImplemented', 'Get-DevDriveEnablementState', 'Test-DevDriveVolume', 'Invoke-DeviceIoControlWrapperForDevDriveQuery', 'Compare-SizeUsingGB' ) |