This article, Automate VHD Offline Defrag for Citrix Provisioning Server, shows you how to defragment your VHD files with PowerShell.
Change Log 14.10.2017: new check in main script to verify the existence of the VHD(X) file. 15.10.2017: 1) fellow Citrix CTA Trentent Tye mentions two important caveats concerning VHD(X) files and block size; 2) this blog post now offers a second script which supports command line arguments (based on a suggestion from Citrix CTP Carl Webster). Thanks a lot for your input guys! 17.11.2017: major issue solved for Windows 2012 R2 and higher: set the Unique Disk ID back to the original one before unmounting the VHD(X) file. When mounting the VHD(X) file on the same machine where it was created, a disk collision occurs as soon as the VHD(X) file is taken online. As soon as the disk is taken online, a new Unique Disk ID is set, which makes the VHDX file unbootable in PVS. |
Table of Contents |
Introduction
Note: for the sake of easy reading, I use the abbreviation VHD in this article, but all information and code also applies 100% to VHDX files. |
When deploying VHD files with Citrix Provisioning Server in combination with RAM Cache with Overflow to Disk, it is very important to defragment your virtual disks. In the article Size Matters: PVS RAM Cache Overflow Sizing, Citrix makes two very clear statements. The first one is:
“Defragmenting the vDisk resulted in write cache savings of up to 30% or more during testing.”
The second statement refers directly to offline defragmentation (as opposed to online defragmentation):
“Defragment the vDisk by mounting the .VHD on the PVS server and running a manual defragmentation on it. This allows for a more robust defragmentation as the OS is not loaded. An additional 15% reduction in the write cache size was seen with this approach over standard defragmentation.”
Defragment a stand-alone VHD file
Fellow Citrix CTA Trentent Tye mentions the following caveats:
|
Manually defragment a stand-alone VHD file
The manual process to defragment a stand-alone VHD file is as follows.
Open the Computer Management console. With a right-mouse click on Disk Management select Attach VHD.
Locate and select your VHD file. You can use a local or network path.
After having attached the VHD, it will show up in the Computer Management console as an additional disk. Modern operating systems have at minimum two partitions; a partition called System Reserved which serves as the boot partition and the C: drive containing the Windows files and folders.
The partition you want to defragment is the one with the Windows files and folders, in our case drive F:. With a right-mouse click on drive F:, select Properties. In the new windows that opens, go to the tab Tools and click the button Optimize.
Another windows opens. Select the F: drive and click the button Optimize. The defragmentation process starts, which may take a while.
The following paragraph describes how to automate this process using PowerShell .
Automatically defragment a stand-alone VHD file with PowerShell
In a fully automated installation process of your master target device, the automatic offline defragmentation of your VHD file should be included as well. This is certainly true if you are using software deployment technologies such as Microsoft SCCM or MDT.
One of the last tasks when creating your master target device is most likely the execution of the p2pvs.exe or imagingwizard.exe to capture your VHD image. This image capture tool is included in the Citrix Provisioning Server Target Device software.
After the VHD file has been created, you can continue, within the same PowerShell script, with the offline defragmentation of the newly created VHD file.
For this purpose, I have prepared a script, complete with detailed logging and error handling. You can run this script by itself, or you can copy the blocks of code you need to your own scripts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
#========================================================================== # # VHD OFFLINE DEFRAGMENTATION # # AUTHOR: Dennis Span (https://dennisspan.com) # DATE : 11.10.2017 # # COMMENT 1: This script mounts a VHD(X) file, defrags it and unmounts it again # COMMENT 2: Please enter your custom variables in lines 75 to 78 # # Change log: # ----------- # 16.10.2017 Dennis Span: added -> check if VHD(X) file exists # 17.11.2017 Dennis Span: added -> set the Unique Disk ID back to the original one before unmounting the VHD(X) file # Note: when mounting the VHD(X) file on the same machine where it was created, a disk collision occurs as soon as the VHD(X) file # is taken online. As soon as the disk is online, a new Unique Disk ID is set, which makes the VHDX file unbootable. #========================================================================== # define Error handling # note: do not change these values $global:ErrorActionPreference = "Stop" if($verbose){ $global:VerbosePreference = "Continue" } # FUNCTION DS_WriteLog #========================================================================== Function DS_WriteLog { <# .SYNOPSIS Write text to this script's log file .DESCRIPTION Write text to this script's log file .PARAMETER InformationType This parameter contains the information type prefix. Possible prefixes and information types are: I = Information S = Success W = Warning E = Error - = No status .PARAMETER Text This parameter contains the text (the line) you want to write to the log file. If text in the parameter is omitted, an empty line is written. .PARAMETER LogFile This parameter contains the full path, the file name and file extension to the log file (e.g. C:\Logs\MyApps\MylogFile.log) .EXAMPLE DS_WriteLog -InformationType "I" -Text "Copy files to C:\Temp" -LogFile "C:\Logs\MylogFile.log" Writes a line containing information to the log file .Example DS_WriteLog -InformationType "E" -Text "An error occurred trying to copy files to C:\Temp (error: $($Error[0]))" -LogFile "C:\Logs\MylogFile.log" Writes a line containing error information to the log file .Example DS_WriteLog -InformationType "-" -Text "" -LogFile "C:\Logs\MylogFile.log" Writes an empty line to the log file #> [CmdletBinding()] Param( [Parameter(Mandatory=$true, Position = 0)][ValidateSet("I","S","W","E","-",IgnoreCase = $True)][String]$InformationType, [Parameter(Mandatory=$true, Position = 1)][AllowEmptyString()][String]$Text, [Parameter(Mandatory=$true, Position = 2)][AllowEmptyString()][String]$LogFile ) $DateTime = (Get-Date -format dd-MM-yyyy) + " " + (Get-Date -format HH:mm:ss) if ( $Text -eq "" ) { Add-Content $LogFile -value ("") # Write an empty line } Else { Add-Content $LogFile -value ($DateTime + " " + $InformationType.ToUpper() + " - " + $Text) } } #========================================================================== ################ # Main section # ################ # Disable File Security $env:SEE_MASK_NOZONECHECKS = 1 # Custom variables [edit] $BaseLogDir = "C:\Logs" # [edit] enter the path to your log directory (e.g. "C:\Logs") $PackageName = "VHDOfflineDefrag" # [edit] enter the package name (the name of the log directory and log file is based on the package name) $VHDFileToDefrag = "C:\Temp\MyVirtualHardDisk.vhdx" # [edit] enter the path and name of the VHD or VHDX file that needs to be defragged (e.g. "c:\Temp\MyVirtualHardDisk.vhdx", "\\MyFileServer\MyFileShare\MyVirtualHardDisk.vhdx") $DefragFile = "defrag.exe" # [edit] enter the path (= optional) and executable name of the defragger program (e.g. 'defrag.exe', 'C:\Temp\df.exe' (= Defraggler), etc.) # Global variables $StartDir = $PSScriptRoot # the directory path of the script currently being executed $LogDir = (Join-Path $BaseLogDir $PackageName).Replace(" ","_") $LogFileName = "Config_$($PackageName).log" $LogFile = Join-path $LogDir $LogFileName # Create the log directory if it does not exist if (!(Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType directory | Out-Null } # Create new log file (overwrite existing one) New-Item $LogFile -ItemType "file" -force | Out-Null DS_WriteLog "I" "START SCRIPT - Config $PackageName" $LogFile DS_WriteLog "-" "" $LogFile ##################################### # Defrag VHD(X) offline # ##################################### DS_WriteLog "I" "==========================" $LogFile DS_WriteLog "I" "Defrag VHD(X) (offline) " $LogFile DS_WriteLog "I" "==========================" $LogFile DS_WriteLog "-" "" $LogFile DS_WriteLog "I" "File to defrag: $VHDFileToDefrag" $LogFile DS_WriteLog "I" "Defrag executable: $DefragFile" $LogFile DS_WriteLog "I" "Log file: $LogFile" $LogFile DS_WriteLog "-" "" $LogFile # Write verbose output Write-Host "Defrag VHD(X) (offline)" Write-Host "=======================" Write-Host "" Write-Host "File to defrag: $VHDFileToDefrag" Write-Host "Defrag executable: $DefragFile" Write-Host "Log file: $LogFile" Write-Host "" # Check if the VHD(X) file exists DS_WriteLog "I" "Check if the VHD(X) file '$VHDFileToDefrag' exists" $LogFile if ( (Test-Path $VHDFileToDefrag ) -eq $True ) { DS_WriteLog "I" "The VHD(X) file '$VHDFileToDefrag' exists" $LogFile } else { DS_WriteLog "E" "The VHD(X) file '$VHDFileToDefrag' does NOT exist or cannot be reached" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } DS_WriteLog "-" "" $LogFile # Retrieve drives before mounting the VHD(X) $DrivesAvailableBeforeVHDMount = (Get-PSDrive -PsProvider FileSystem).Name DS_WriteLog "I" "Retrieve available drives (before mount): $([string]$DrivesAvailableBeforeVHDMount)" $LogFile DS_WriteLog "-" "" $LogFile # Retrieve available disks before VHD(X) mount $DisksAvailableBeforeVHDXMount = (Get-WmiObject Win32_DiskDrive).Index | sort-object DS_WriteLog "I" "Retrieve available disks (before mount): $([string]$DisksAvailableBeforeVHDXMount)" $LogFile DS_WriteLog "-" "" $LogFile # Retrieve the disk number of the disk with the active partition $DiskNumber = ((Get-WmiObject -Class Win32_DiskPartition | Where-Object { $_.Bootable -eq $True }).DeviceID).SubString(6,1) DS_WriteLog "I" "Retrieve the disk number of the disk with the active partition: $($DiskNumber)" $LogFile # Retrieve the unique disk ID (= signature) of the disk with the active partition $UniqueDiskIDDec = (Get-WmiObject Win32_DiskDrive | Where-Object { $_.Index -eq $DiskNumber }).Signature DS_WriteLog "I" "Retrieve the unique disk ID (= signature) of the disk with the active partition (decimal value): $($UniqueDiskIDDec)" $LogFile # Convert the unique ID (= signature), which is returned in decimal notation, to hexidecimal notation $UniqueDiskIDHex = '{0:X}' -f $UniqueDiskIDDec DS_WriteLog "I" "Convert decimal value of the unique disk ID to hexidecimal: $($UniqueDiskIDHex)" $LogFile DS_WriteLog "-" "" $LogFile # Create temporary diskpart configuration file (to be able to mount the VHD(X)) $DiskpartFile = "$env:TEMP\diskpart_mount.txt" $Line1 = "SELECT VDISK FILE=""$($VHDFileToDefrag)""" $Line2 = "ATTACH VDISK" $Line3 = "ONLINE DISK" DS_WriteLog "I" "Create temporary diskpart file to mount the VHD(X) file ($DiskpartFile)" $LogFile try { New-Item $DiskpartFile -type file -force | Out-Null DS_WriteLog "I" "Temporary diskpart file was created successfully" $LogFile Add-Content $DiskpartFile $Line1 DS_WriteLog "I" "The line $Line1 was added successfully" $LogFile Add-Content $DiskpartFile $Line2 DS_WriteLog "I" "The line $Line2 was added successfully" $LogFile Add-Content $DiskpartFile $Line3 DS_WriteLog "I" "The line $Line3 was added successfully" $LogFile } Catch { DS_WriteLog "E" "An error occurred trying to create the diskpart configuration file (error: $($Error[0]))" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } DS_WriteLog "-" "" $LogFile # Mount VHD(X) (using diskpart) DS_WriteLog "I" "Mount (attach) the VHD(X) (using diskpart.exe)" $LogFile $Process = Start-Process -WindowStyle hidden -FilePath 'diskpart.exe' -ArgumentList "/S $DiskpartFile" -Wait -PassThru $ProcessExitCode = $Process.ExitCode switch ($ProcessExitCode) { 0 { DS_WriteLog "I" "The diskpart command executed successfully" $LogFile } -2147024809 { DS_WriteLog "E" "An error occurred. Either the virtual disk is already attached or the file or directory is corrupted!" $LogFile DS_WriteLog "E" "Please detach the VHD(X) file and run this script again or try to attach the VHD(X) file manually in the Computer Management console" $LogFile DS_WriteLog "E" "Please make sure you are using a valid VHD(X) file containing a Windows operating system (Windows 7/Windows Server 2008 R2 or higher)" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } default { DS_WriteLog "E" "An error occurred executing the diskpart command (error: $ProcessExitCode)" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } } Write-Host "The VHD(X) file was mounted successfully" DS_WriteLog "-" "" $LogFile # Retrieve drives after mounting the VHD(X) $DrivesAvailableAfterVHDMount = (Get-PSDrive -PsProvider FileSystem).Name DS_WriteLog "I" "Retrieve available drives (after mount): $([string]$DrivesAvailableAfterVHDMount)" $LogFile DS_WriteLog "-" "" $LogFile # Check which drive letter or driver letters were added after mounting the VHD(X) file (drive letters are written without a colon; e.g. "D" instead of "D:") try { [array]$Drives = ((Compare-Object $DrivesAvailableBeforeVHDMount $DrivesAvailableAfterVHDMount).InputObject).ToUpper() } catch { DS_WriteLog "E" "No additional drives were detected. It is possible that the VHD(X) file was mounted, but that no drive letter could be assigned" $LogFile DS_WriteLog "E" "Please make sure you are using a valid VHD(X) file containing a Windows operating system (Windows 7/Windows Server 2008 R2 or higher)" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } # Continue check which drive letter or driver letters were added after mounting the VHD(X) file (drive letters are written without a colon; e.g. "D" instead of "D:") $DriveCount = $Drives.Count switch ($DriveCount) { 0 { DS_WriteLog "E" "No new drives were added. Apparently the VHD(X) mount did not succeed" $LogFile DS_WriteLog "E" "This script will now quit." $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } 1 { # One additional drive will be found when capturing an operating system WITHOUT a 'System Reserved' boot partition (e.g. Windows 7, Windows Server 2008 R2) [string]$DriveLetterToDefrag = "$($Drives):" DS_WriteLog "S" "One new drive was added: $DriveLetterToDefrag" $LogFile } 2 { # Two additional drives will be found when capturing an operating system WITH a 'System Reserved' boot partition (e.g. Windows 10, Windows Server 2016) DS_WriteLog "I" "Two new drives were added. Checking which drive requires offline defragmentation" $LogFile foreach ( $Drive in $Drives ) { DS_WriteLog "I" "Checking drive $($Drive):" $LogFile if ( (Get-WmiObject -Class win32_volume -Filter "DriveLetter = '$($Drive):'").Label -like "*Reserved*") { DS_WriteLog "I" " Drive $($Drive): is the 'System Reserved' drive. This one does not require offline defragmentation" $LogFile } else { DS_WriteLog "I" " Drive $($Drive): is the primary partition and requires offline defragmentation" $LogFile [string]$DriveLetterToDefrag = "$($Drive):" } } } default { DS_WriteLog "E" "More than two new drives were added. This script is not able to verify which drive needs offline defragmentation" $LogFile DS_WriteLog "E" "This script will now quit." $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } } DS_WriteLog "-" "" $LogFile # Retrieve available disks after VHD(X) mount $DisksAvailableAfterVHDXMount = (Get-WmiObject Win32_DiskDrive).Index | sort-object DS_WriteLog "I" "Retrieve available disks (after mount): $([string]$DisksAvailableAfterVHDXMount)" $LogFile DS_WriteLog "-" "" $LogFile # Check which disks were added after mounting the VHD(X) file try { [array]$Disks = (Compare-Object $DisksAvailableBeforeVHDXMount $DisksAvailableAfterVHDXMount).InputObject } catch { DS_WriteLog "E" "No additional disks were detected. Please check if the VHD(X) file was properly mounted" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } # Continue check which disks were added after mounting the VHD(X) file and determine the one that needs to have its unique ID reset $DiskCount = $Disks.Count switch ($DiskCount) { 0 { DS_WriteLog "E" "No new disks were added. Apparently the VHD(X) mount did not succeed" $LogFile DS_WriteLog "E" "This script will now quit." $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } 1 { # One additional disk was found [string]$NewDisk = "$($Disks)" DS_WriteLog "I" "One new disk was added. Disk number: $NewDisk" $LogFile } default { DS_WriteLog "E" "More than one new disks were added. This script is not able to verify which disk needs to have its unique ID reset" $LogFile DS_WriteLog "E" "This script will now quit." $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } } DS_WriteLog "-" "" $LogFile # Defrag VHD(X) Write-Host "Defragging the VHD(X) mounted to drive $DriveLetterToDefrag..." DS_WriteLog "I" "Defrag the VHD(X) mounted to drive $DriveLetterToDefrag" $LogFile DS_WriteLog "I" "Execute command: Start-Process -WindowStyle hidden -FilePath ""$DefragFile"" -ArgumentList $DriveLetterToDefrag -Wait -PassThru" $LogFile $Process = Start-Process -WindowStyle hidden -FilePath $DefragFile -ArgumentList $DriveLetterToDefrag -Wait -PassThru $ProcessExitCode = $Process.ExitCode if ($ProcessExitCode -eq 0 ) { DS_WriteLog "I" "Defrag drive $DriveLetterToDefrag completed successfully" $LogFile Write-Host "Defragging drive $DriveLetterToDefrag completed successfully" } else { DS_WriteLog "E" "An error occurred while attempting to defrag drive $DriveLetterToDefrag (error: $ProcessExitCode)" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } DS_WriteLog "-" "" $LogFile # Create second temporary diskpart configuration file (to be able to un-mount the VHD(X)) $DiskpartFile = "$env:TEMP\diskpart_unmount.txt" $Line1 = "SELECT DISK=""$($NewDisk)""" $Line2 = "UNIQUEID DISK ID=""$($UniqueDiskIDHex)""" $Line3 = "SELECT VDISK FILE=""$($VHDFileToDefrag)""" $Line4 = "DETACH VDISK" DS_WriteLog "I" "Create temporary diskpart file to unmount the VHD(X) file ($DiskpartFile)" $LogFile try { New-Item $DiskpartFile -type file -force | Out-Null DS_WriteLog "I" "Temporary diskpart file was created successfully" $LogFile Add-Content $DiskpartFile $Line1 DS_WriteLog "I" "The line $Line1 was added successfully" $LogFile Add-Content $DiskpartFile $Line2 DS_WriteLog "I" "The line $Line2 was added successfully" $LogFile Add-Content $DiskpartFile $Line3 DS_WriteLog "I" "The line $Line3 was added successfully" $LogFile Add-Content $DiskpartFile $Line4 DS_WriteLog "I" "The line $Line4 was added successfully" $LogFile } Catch { DS_WriteLog "E" "An error occurred trying to create the diskpart configuration file (error: $($Error[0]))" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } DS_WriteLog "-" "" $LogFile # (Re)set unique disk ID and un-mount VHD(X) (using diskpart) DS_WriteLog "I" "(Re)set unique disk ID and un-mount (detach) the VHD(X) (using diskpart.exe)" $LogFile $Process = Start-Process -WindowStyle hidden -FilePath 'diskpart.exe' -ArgumentList "/S $DiskpartFile" -Wait -PassThru $ProcessExitCode = $Process.ExitCode if ($ProcessExitCode -eq 0 ) { DS_WriteLog "I" "The diskpart command executed successfully" $LogFile Write-Host "The VHD(X) file was unmounted successfully" } else { DS_WriteLog "E" "An error occurred executing the diskpart command (error: $ProcessExitCode)" $LogFile Write-Host "The script ended in an error. Please see the log file '$LogFile'" Exit 1 } # Enable File Security Remove-Item env:\SEE_MASK_NOZONECHECKS DS_WriteLog "-" "" $LogFile DS_WriteLog "I" "End of script" $LogFile Write-Host "End of script" |
Note: many thanks to Salim Hurjuk (@salimhurjuk) for his help testing this script! |
The above script can be used as follows:
- Prepare a VHD file. Any VHD file from any operating system will do.
- Copy the VHD file to an installation directory, either on the local computer or on a network share (UNC path). For example: C:\Temp\MyVirtualHardDisk.vhdx or \\MyServer\MyShare\MyVirtualHardDisk.vhdx.
- Copy the above PowerShell script to a new PS1 file (e.g. VHDOfflineDefrag.ps1) and add this file to a directory on the server where you would like to execute it.
- Edit the PowerShell script and modify lines 78 to 81:
- $BaseLogDir: this variable contains the path to your log directory.
- $PackageName: this variable contains the package name (the name of the log directory and log file is based on the package name).
- $VHDFileToDefrag: this variable contains the path and name of the VHD file that needs to be defragmented. Enter the path to the installation directory you created in one of the previous steps.
- $DefragFile: this variable contains the executable name of the defragmentation program you would like to use. The default defragmentation tool included in Windows is defrag.exe. Another possible defragmentation tool is df.exe (= Defraggler) from CCLeaner (previously Piroform) (the same company as from CCleaner).
- Execute the PowerShell script:
powershell.exe -file “C:\Temp\VHDOfflineDefrag.ps1”
Note: in case of an error you may have to execute the PowerShell script as follows: powershell.exe -executionpolicy unrestricted -file “C:\Temp\VHDOfflineDefrag.ps1” |
Let’s take a closer look at the contents of the script:
- Up to line 120, the code mainly consists of variables, functions and logging.
- Enter your custom variables in lines 78 to 81.
- Lines 119 to 128 check if the VHD file specified in the variable $VHDFileToDefrag in line 78 exists. If not, an error is thrown.
- In line 133, the currently mapped drives are retrieved. We will need this information later in the script, to compare the currently mapped drives with the new one(s) created after mounting the VHD file.
- Lines 138 to 141 retrieve the currently available disks. We will need this information later in the script, to compare the currently available disks with the new one(s) created after mounting the VHD file.
- Lines 143 to 151 retrieve the Unique Disk ID from the disk containing the boot partition. We need this ID later in the script. When mounting the VHD(X) file on the same machine where it was created, a disk collision occurs as soon as the VHD(X) file is taken online. As soon as the disk is taken online, a new Unique Disk ID is set, which makes the VHDX file unbootable in PVS. Before unmounting the VHD file the original Unique Disk ID needs to be set once again. If this action is not taken and the Unique Disk ID is overwritten, the boot process fails and results in error 0xc000000e:
- Lines 155 to 174 contain the code to create the file diskpart_mount.txt in the current user’s %Temp% folder. This file contains the commands required by DiskPart to mount the VHD file:
- SELECT VDISK FILE=C:\Temp\MyVirtualHardDisk.vhdx
- ATTACH VDISK
- ONLINE DISK
- Lines 178 to 199 contain the code to actually mount the VHD file, using diskpart.exe and the DiskPart configuration file diskpart_mount.txt. DiskPart is launched using the PowerShell cmdlet Start-Process.
- In line 205, the currently mapped drives are retrieved (again), which will now include the drive(s) of the mapped VHD file.
- Lines 209 to 253 contain the code to retrieve the drive letter that needs to be defragmented. In case of new operating systems such as Windows 10 and Windows Server 2016 (all versions and builds), the VHD file contains two partitions: a boot partition with the name System Reserved and the “C:” drive containing all Windows files and folders. The PowerShell script automatically checks if one or two new drives were added and determines which drive needs to be defragmented.
- In line 258, the currently available disks are retrieved (again), which will now include the disk of the mapped VHD file.
- Lines 262 to 292 contain the code to determine the disk number of the newly attached VHD file. The original Unique Disk ID is (re)set to this disk number (= VHD file).
- Lines 296 to 309 is where the actual defragmentation of the drive is executed, using the PowerShell cmdlet Start-Process. The defragmentation tool used is the one defined in the variable $DefragFile in line 81.
- Lines 313 to 335 contain the code to create the file diskpart_unmount.txt in the current user’s %Temp% folder. This file contains the commands required by DiskPart to unmount the VHD file:
- SELECT DISK=3
- UNIQUEID DISK ID=2A23B239
- SELECT VDISK FILE=C:\Temp\MyVirtualHardDisk.vhdx
- DETACH VDISK
- In the last part of the script, lines 339 to 350, the Unique Disk ID is set and the VHD file is unmounted, using diskpart.exe and the DiskPart configuration file diskpart_unmount.txt.
Now you can copy the VHD file to your Provisioning Server store and create your vDisk.
Note: in case you would rather parse the parameters at the command line (instead of changing the values directly in the script), I have created a copy of the above script and added command line arguments.
The following arguments are available.
Examples:
You can download the script here. |
Defragment a Provisioning Server vDisk
In case you already imported your VHD file to Provisioning Server, you cannot simply start an offline defragmentation. You need to make sure that the file is not in use. To put it differently, you have to make sure that no target devices are connected to the vDisk you want to defragment.
If you want to use the PowerShell script in the previous section, you will have to add a few additional lines to check for any open connections to the vDisk. You can use the Get-PvsDiskInfo cmdlet to check if your vDisk is locked. Please make sure to install the Provisioning Server PowerShell snap-in as described in the article Citrix Provisioning Server unattended installation beforehand. Here is an example of how to check if your vDisk is locked or not.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$SiteName = "MySite" $StoreName = "MyStore" $vDiskName = "MyvDiskName" If ( ((Get-PvsDiskInfo -SiteName $SiteName -StoreName $StoreName -Name $vDiskName ).Locked) -eq $True) { Write-Host "The vDisk is currently locked. Disconnect all target devices and rerun this script" } else { Write-Host "The vDisk is currently unlocked and can be defragmented" } |
It is not important if the vDisk is set to Standard or Private mode. These modes are only relevant for interactive logons. In our case, we want the vDisk to be completely offline.
Provisioning Server mounting tool CVhdMount.exe
Both Provisioning Server as well as the Provisioning Server Target Device software includes it own mounting tool, the executable CVhdMount.exe in the directory C:\Program Files\Citrix\Provisioning Services.
This tool is used when mounting a vDisk using the Provisioning Server console.
You can also use this tool on the command line. The parameters to mount the vDisk are: CVhdMount.exe -p 1 C:\PVSMyHomeStore\MyVirtualHardDisk.vhdx
The -p switch should be greater than 0. As far as I can tell, this serial number is only required for the CVhdMount tool to keep track of the mounted vDisk or vDisks. This serial number is not to be confused with the serial number of the actual vDisk.
1 2 |
Add-PsSnapin citrix* (Get-PvsDiskInfo -SiteName "Site" -StoreName "MyHomeStore" -Name "MyVirtualHardDisk").SerialNumber |
In most cases, you can simply use number 1 as the serial number. Let me demonstrate. In the following screen shot, I try to mount two different vDisk with the same serial number (in both cases I use number 1). The second vDisk cannot be mounted due to a conflicting serial number. When I assign number 2 as a serial number, the second vDisk can be mounted without any problem.
To unmount the vDisk, use the following command: CVhdMount.exe -u 1. The switch -u requires the same serial number as the one you used when mounting the vDisk.
If you so choose, you could replace the diskpart.exe with the cVHDMount.exe in the PowerShell script presented in the first section of this article. Personally, I would stick to DiskPart.
Dennis Span works as a Lead Account Technology Strategist at Cloud Software Group in Vienna, Austria. He holds multiple Citrix certifications (CCE-V). Dennis has been a Citrix Technology Advocate (CTA) since 2017 (+ one year as Citrix Technology Professional, CTP). Besides his interest in virtualization technologies and blogging, he loves spending time with his family as well as snowboarding, playing basketball and rowing. He is fluent in Dutch, English, German and Slovak and speaks some Spanish.
Great stuff Dennis! Some caveats I’ve experienced:
1) if you create a VHD file with 32MB block size, ONLY cvhdmount.exe will mount it. All other formats can be done with diskpart.
2) You can’t mount VHD files if they reside on 4K block storage, they will need to be moved to 512b block storage.
Thanks Trentent. I will add your comments to the article!
Performing the “Manually defragment a stand-alone VHD file” procedure, the degragment status of the drive before starting the defragmentation is: OK (100% space efficiency).
Pingback: EUC Weekly Digest – October 14, 2017 – Carl Stalhood
Pingback: How to configure and run BIS-F in an SCCM task sequence - Dennis Span
Pingback: How I run a Defraf a PVS Vdisk. » Citrixirc.com
Hello, where can I find the automatic installation script for the main target device
Hello, where can I find the automation script for target device installation
HI, you can find the script here: https://dennisspan.com/scripting-the-complete-list-of-citrix-components-with-powershell/#PVSTargetDevice
Hello, about whether the image wizard after the installation of the target device supports script unattended
Hi, I am sorry, but I still do not quite understand your question. Are you asking if the imaging wizard process itself can be automated or if a post-config script can run after the imaging wizard or perhaps at first boot?
I’m sorry I wasn’t very clear.What I want to express is that the “image wizard” will continue to be configured after the steps of “Target device Installation” are completed according to the process of the interface. Can the configuration of “Image Wizard” be realized through scripts.
Hi,
Yes, the imaging wizard can be executed using scripts. In all honestly, if you really want to create a correctly configured image for your Citrix environment, I recommend that you use the free BISF framework (https://eucweb.com/download-bis-f). This PowerShell framework includes everything you will ever need, including an automatic trigger of the PVS Imaging Wizard. I wrote an article how to use BISF together with SCCM, perhaps it can be of help to you: https://dennisspan.com/how-to-configure-and-run-bis-f-in-an-sccm-task-sequence/
Sorry, I didn’t make myself clear. I hope you can help me. It means a lot to me.
I mean, after you install the target device, you need to create the boot partition and other partitions through the boot Image wizard. The operations include adding sites, creating virtual disks, adding target devices, creating virtual disks, creating mirrors, and optimizing disks. Is it possible to use scripts to do this?
Looking forward to your reply. Thank you very much.
Hi, I think that you are mixing two different things. There is the image itself that needs to be prepared (which can be 100% automated) and there is the configuration of the PVS servers, which can also be 100% automated. I wrote an article on this website how to install and configure the PVS servers using PowerShell (https://dennisspan.com/citrix-provisioning-server-unattended-installation/#ConfigureFarmAndLocalHost). My friend Chris Twiest wrote an article about automating the vDisk within PVS (https://workspace-guru.com/2017/10/21/scripting-citrix-provisioning-services-pvs-powershell-commandline/). The main point is that all steps can be automated. I hope this information helps.
Big guy, I would like to implement the following functions of the script and I hope you can help.
https://docs.citrix.com/en-us/provisioning/current-release/install/vdisks-image-wizard.html
Thank you very much