Overview
Citrix App Layering Publishes new versions of PVS images to a PVS Server as a Full vDisk. Some customers prefer to manage vDisk changes using PVS versioning. This sample script will convert App Layering Published image vDisks into a versioning format on the PVS server using the Image Name as the vDisk Name.
When using this script, your vDisk Store will resemble this, showing a single entry for each App Layering Image that you publish to PVS:
Then, if you open versioning, each App Layered vDisk will be shown as a version with a type of “Merged base”.
This then allows administrators to update targets by just promoting the new version.
The script is designed to run as necessary, and it will process all vDisks only by modifying vDisks named using the App Layering Date_Time format that is also not assigned to a device or locked.
Therefore, the following vDisk:
Win10PVSUser_2022-01-20_18-17-58
Would become a version of a VDisk named:
Win10PVSUser
This script should work fine for customers that previously used the Citrix App Layering 4.x: PVS Connector Script to Convert VHD to VHDX script to perform the same process along with VHD to VHDX conversion.
This provides a mechanism to support versioning now that connector scripts have been removed from App Layering.
Limitations
To use this script, there are several important limitations to be aware of:
The full path of the vDisk cannot be longer than 127 characters. This includes the store path.
Warning
Citrix Solution Architects provide this example script as a useful extension to Citrix App Layering. This script has been tested in our labs but not by our QA department, and it is not supported as part of the Citrix App Layering Product. Please test thoroughly and implement with care.
Be aware that the script is designed to process all vDisks on a PVS Server that conforms to the App Layering standard for naming. For example:
Win2016PVS_2023-12-15_08-57-35
The disk will be processed if the vDisk name matches the Date_Time format. The script will check to ensure no device targets have the disk assigned. If there is an assignment, the vDisk will be skipped,
LEGAL DISCLAIMER
This software/sample code is provided to you “AS IS” with no representations, warranties, or conditions of any kind. You may use, modify, and distribute it at your own risk. CITRIX DISCLAIMS ALL WARRANTIES WHATSOEVER, EXPRESS, IMPLIED, WRITTEN, ORAL OR STATUTORY, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NONINFRINGEMENT. Without limiting the generality of the foregoing, you acknowledge and agree that (a) the software/sample code may exhibit errors, design flaws or other problems, possibly resulting in loss of data or damage to property; (b) it may not be possible to make the software/sample code fully functional; and (c) Citrix may, without notice or liability to you, cease to make available the current version and/or any future versions of the software/sample code. In no event should the software/code be used to support of ultra-hazardous activities, including but not limited to life support or blasting activities. NEITHER CITRIX NOR ITS AFFILIATES OR AGENTS WILL BE LIABLE, UNDER BREACH OF CONTRACT OR ANY OTHER THEORY OF LIABILITY, FOR ANY DAMAGES WHATSOEVER ARISING FROM USE OF THE SOFTWARE / SAMPLE CODE, INCLUDING WITHOUT LIMITATION DIRECT, SPECIAL, INCIDENTAL, PUNITIVE, CONSEQUENTIAL OR OTHER DAMAGES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Although the copyright in the software/code belongs to Citrix, any distribution of the code should include only your own standard copyright attribution and not that of Citrix. You agree to indemnify and defend Citrix against any and all claims arising from your use, modification or distribution of the code.
Infrastructure Requirements
The following infrastructure components/configurations are required:
PowerShell Execution Policy must be set to remote-signed or unrestricted.
The account used to run the script must be a local administrator and PVS administrator, and it must have modified rights to the script folder and the PVS store.
Setting Up the Script
The first step when setting up the script is to create a folder on the PVS server. A folder off the root with no spaces in the name is recommended but not required.
If the script file is blocked, unblock it.
Test
Testing the script on test vDisks on a test PVSServer is critical. Test all the way to deploying targets using the versioned vDisk.
The script creates a detailed log. Review the log after each test.
Summary of Script Logic
The following high-level logic is used in the script
Load the PVS PowerShell Module
Get All The vDisks on the Server (Disk Locators)
Process each vDisk
Get Disk Information and any Device Targets with the disk assigned
If the disk is assigned or locked, then skip it
Figure out if it’s a VHD or VHDX
Determine the Image Name from the vDisk name using REGEX
"_([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})$"
Look for an Existing vDisk name matching our Image Name
If there is none, then create one using this first vDisk
Capture the vDisk properties
Remove the vDisk from the console (Do not delete the vDisk)
If it can't be removed, skip it
Delete any pvp and lok files for this vDisk
Rename the VHD file to the format
ImageName.vhd/vhdx
Make sure the rename worked
If not, skip this vDisk
Create a new Disk Locator (vDisk in Console) for the ImageName vDisk
If this fails, skip this vDisk
Set the saved properties on the new vDisk
Check to make sure the settings were saved and log the results
If there was already a vDisk for the ImageName, then add this new vDisk as a new version
Export the new vDisks info
This creates a manifest xml file
Get the vDisk’s current versions
Find the next available version number
Modify the manifest with the new disk name and set the settings to Merged Base and Test
Copy existing vDisk Settings
Remove the vDisk
Format ImageName.VersionNumber.vhd/vhdx
If the remove fails skip to next vDisk
Delete the lok,PVP, and XML files for the old name of the vDisk
Rename the vDisk to the new format
Add a PVS Versions using the new vDisk name (ImageName)
This is the merge
If the add fails skip this vDisk
Delete the manifest file
Log any PowerShell Errors found during processing
I have provided the script here below so that it can be copied and saved for your use.
#Script file vdisk_merge.ps1
#This script was developed as an example script by Citrix Solution Architect Rob Zylowski
#This script allows app layered images published to PVS to be merged as versions within a PVS vDisk
#This allows admins to manage new versions of vDisks as PVS versions
<# ***************************************** LEGAL DISCLAIMER *****************************************
This software / sample code is provided to you “AS IS” with no representations, warranties or conditions of any kind.
You may use, modify and distribute it at your own risk. CITRIX DISCLAIMS ALL WARRANTIES WHATSOEVER, EXPRESS, IMPLIED,
WRITTEN, ORAL OR STATUTORY, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
TITLE AND NONINFRINGEMENT. Without limiting the generality of the foregoing, you acknowledge and agree that (a) the
software / sample code may exhibit errors, design flaws or other problems, possibly resulting in loss of data or damage to
property; (b) it may not be possible to make the software / sample code fully functional; and (c) Citrix may, without notice
or liability to you, cease to make available the current version and/or any future versions of the software / sample code.
In no event should the software / code be used to support of ultra-hazardous activities, including but not limited to life
support or blasting activities. NEITHER CITRIX NOR ITS AFFILIATES OR AGENTS WILL BE LIABLE, UNDER BREACH OF CONTRACT OR ANY
OTHER THEORY OF LIABILITY, FOR ANY DAMAGES WHATSOEVER ARISING FROM USE OF THE SOFTWARE / SAMPLE CODE, INCLUDING WITHOUT
LIMITATION DIRECT, SPECIAL, INCIDENTAL, PUNITIVE, CONSEQUENTIAL OR OTHER DAMAGES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. Although the copyright in the software / code belongs to Citrix, any distribution of the code should include only
your own standard copyright attribution, and not that of Citrix. You agree to indemnify and defend Citrix against any and all
claims arising from your use, modification or distribution of the code.
********************************************************************************************************** #>
#Version 1.0 2-1-2024
function Get-ScriptDirectory
{
$Invocation = (Get-Variable MyInvocation -Scope 1).Value
Split-Path $Invocation.MyCommand.Path
}
Function LogLine($strLine)
{
Write-Host $strLine
$global:StrTime = Get-Date -Format "MM-dd-yyyy-HH-mm-ss-tt"
"$global:StrTime - $strLine " | Out-file -FilePath $LogFile -Encoding ASCII -Append
}
Function FormatErrors()
{
$NumToProcess = $Error.Count - 1
For ($i = $NumToProcess; $i -gt -1; $i-- )
{
If (!($Error[$i] -Contains "disklocatorTest"))
{
logline "========================================================================"
$MyError = "Error: " + $Error[$i].InvocationInfo.PositionMessage + "Exception: " + $Error[$i]
logline $MyError
}
}
$Error.Clear()
}
function IsDateTime {
param(
[string]$vdiskname
)
$pattern = '\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}'
if ($vdiskname -match $pattern) {
$returnvalue = $true
} else {
$returnvalue = $false
}
$returnvalue
}
#Get the directory where the script is installed
$ScriptSource = Get-ScriptDirectory
#Create a logline folder and file
$LogFileQualifier = Get-Date -Format "MM-dd-yyyy-HH-mm-tt"
$LogFolder = "$ScriptSource\Logs"
If (!(Test-Path "$LogFolder"))
{
mkdir "$LogFolder" >$null
}
$LogFile = "$LogFolder\vDiskMerge-$LogFileQualifier-log.txt"
logline "========================================================================"
logline " Running vDisk Merge Script Version 1.0"
logline "========================================================================"
logline ""
$FormatedErrors = ""
#=========================================================================
#Import PowerShell Module
#=========================================================================
$PVS_Server = $env:computername
Logline "Script is running on PVS Server [$PVS_Server]"
Try{
Import-Module 'C:\Program Files\Citrix\Provisioning Services console\Citrix.PVS.SnapIn.dll' -ErrorAction Stop
} Catch {
logline "The vDisk Merge script has failed because the PVS PowerShell module could not be loaded"
logline "PVS PowerShell Snap-In Not Found on $env:computername. Script terminated."
exit
}
#Now Lets get all the disk locators on the server so we can look for vDisks with the App Layering Format
Logline "Getting all the vDisks on this server"
$VHDDiskLocators = Get-PVSDisklocator -ServerName $PVS_Server
Logline "The number of vDsks found was [$($VHDDiskLocators.count)]"
Foreach ($MyVHDDiskLocator in $VHDDiskLocators)
{
#First lets determine if this vDisk is in the App Layeirng Format. This means it has a date_time label on the vdisk.
#If it is not app layerd format we will skip
$vDiskName = $MyVHDDiskLocator.Name
if(!(isDateTime $vdiskName))
{
Logline "vDisk named [$vdiskName] does not need to be processed and will be skipped"
continue
}
logline "Processing vDisk [$vdiskName]"
$VHDX_DiskLocator = $MyVHDDiskLocator.diskLocatorId
Logline "Using DiskLocatorId [$VHDX_DiskLocator]"
#Lets make sure ths vDisk has not already been assigned. If it has we can do nothing with it.
$DiskInfo = Get-PvsDiskInfo -DiskLocatorId $VHDX_DiskLocator
$AssignedDevices = Get-PVSDevice -DiskLocatorId $VHDX_DiskLocator.guid
if (($AssignedDevices.Count -gt 0) -or ($DiskInfo.Locked))
{
Logline "vDisk named [$vdiskName] is assigned to at least one device and will be skipped"
continue
}
else
{
Logline "vDisk named [$vdiskName] is NOT assigned and it will be merged"
}
#Is it a vhd or vhdx
if ($DiskInfo.VHDX -eq $true)
{
$DiskType="vhdx"
}
else
{
$DiskType="vhd"
}
Logline "vDisk type is [$DiskType]"
#Lets Get the image Name from the disk file name
$dateTimeRegex = "_([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})$"
$ImageName = $vDiskName -replace $dateTimeRegex,""
Logline "The Image Name is [$ImageName]"
$storePath = (Get-PvsStore -StoreId $MyVHDDiskLocator.StoreId).Path
logline "Changing to the store directory: $storePath"
Push-Location $storePath -StackName $LOCATION_STACK_NAME
#First lets see if there is a vDisk disklocator with our image name already
$siteId = $MyVHDDiskLocator.SiteId
$storID = $MyVHDDiskLocator.StoreId
$disklocatorTest = $null
$bvDiskIsFirstImage = $false
#If we cant get the ImageName as a vDisk we assume its the first time we are trying to add the image
Try {
$disklocatorTest = Get-PvsDiskLocator -DiskLocatorName "$imageName" -SiteId $siteId -StoreId $storID -ErrorAction:Ignore
}
Catch {
$bvDiskIsFirstImage = $true
}
#======================
#If this is the first vDisk for this image Template we need to convert to the Image Template and set the first version
#======================
if ($bvDiskIsFirstImage)
{
logline "No vDisk found matching our image name [$imageName]. This will be the first version."
#Now need to remove the vDisk, rename the vhd and add it back in to PVS.
#First we capture the vDisk settings so we can put them back
$bRebaseVersion = $false
$dl_Name = $imageName
$dl_Site = $MyVHDDiskLocator.SiteName
$dl_Store = $MyVHDDiskLocator.StoreName
$dl_Description = $MyVHDDiskLocatorr.Description
$dl_MenuText = $MyVHDDiskLocator.MenuText
$dl_ServerName = $MyVHDDiskLocator.ServerName
$dl_RebalanceEnabled = $MyVHDDiskLocator.RebalanceEnabled
$dl_RebalanceEnabledPercent = $MyVHDDiskLocator.RebalanceTriggerPercent
$dl_SubnetAffinity = $MyVHDDiskLocator.SubnetAffinity
#Delete the original disklocator
logline "Removing the Disk Locator with ID $VHDX_DiskLocator"
Try {
Remove-PvsDiskLocator -DiskLocatorId $VHDX_DiskLocator -Confirm:$false
}
Catch
{
Logline "We coudl not remove the existing vDsik Disklocator"
Logline $error[0]
Logline "Skipping this vDisk"
Continue
}
#Delete the PVP and lok files
$delFileBase = $MyVHDDiskLocator.DiskLocatorName
if (test-path "$storePath\$delFileBase.lok")
{
logline "Removing $storePath\$delFileBase.lok"
Remove-Item "$storePath\$delFileBase.lok"
}
if (test-path "$storePath\$delFileBase.pvp")
{
logline "Removing $storePath\$delFileBase.pvp"
Remove-Item "$storePath\$delFileBase.pvp"
}
#Now rename the vhd file to imagename.vhd or imagename.vhdx
logline "Changing to directory: $storePath"
Push-Location $storePath -StackName $LOCATION_STACK_NAME
$MyVHDXpath = "$storePath\$vdiskName.$DiskType"
$MyNewDiskPath = "$storePath\$imageName.$DiskType"
logline "Renaming the disk file from [$MyVHDXpath] to [$MyNewDiskPath]"
ren $MyVHDXpath $MyNewDiskPath
#Make sure the rename worked
if (!(Test-Path $MyNewDiskPath))
{
logline "***Warning the rename failed. skipping this vDisk"
continue
}
#Now add the new one
logline "Creating a disk locator for our VHD/VHDX file"
if ($diskType -eq "vhdx")
{
if ($dl_MenuText -eq $null){$dl_MenuText=""}
if ($dl_Description -eq $null){$dl_Description=""}
if ($dl_ServerName -eq $null){$dl_ServerName=""}
if ($dl_ServerName -eq "")
{
New-PvsDiskLocator `
-DiskLocatorName "$dl_Name" `
-SiteName "$dl_Site" `
-StoreName "$dl_Store" `
-VHDX
}
else
{
New-PvsDiskLocator `
-DiskLocatorName "$dl_Name" `
-SiteName "$dl_Site" `
-StoreName "$dl_Store" `
-ServerName "$dl_ServerName" `
-VHDX
}
}
else
{
if ($dl_MenuText -eq $null){$dl_MenuText=""}
if ($dl_Description -eq $null){$dl_Description=""}
if ($dl_ServerName -eq $null){$dl_ServerName=""}
if ($dl_ServerName -eq "")
{
New-PvsDiskLocator `
-DiskLocatorName "$dl_Name" `
-SiteName "$dl_Site" `
-StoreName "$dl_Store" `
}
else
{
New-PvsDiskLocator `
-DiskLocatorName "$dl_Name" `
-SiteName "$dl_Site" `
-StoreName "$dl_Store" `
-ServerName "$dl_ServerName" `
}
}
if (!($?))
{
logline "***Warning the new disk created for our image was could not be added as a vDisk. Exiting Script"
logline "using settings dl_name:$dl_Name dl_site:$dl_Site dl_store:$dl_store dl_server:$dl_ServerName"
continue
}
$NewDiskLocator = Get-PvsDiskLocator -DiskLocatorName $dl_Name -SiteName $dl_Site -StoreName $dl_Store
$newDiskLocatorId = $NewDiskLocator.DiskLocatorId
logline "The new DiskLocator Id for the renamed image [$imageName] is $newDiskLocatorId"
$storePath = (Get-PvsStore -StoreId $NewDiskLocator.StoreId).Path
#Now lets put back the settings we saved
$o = Get-PvsDisk -DiskLocatorId $newDiskLocatorId
$o.ActivationDateEnabled = $VD_ActivationEnabled
$o.HaEnabled = $VD_HAEnabled
$o.AdPasswordEnabled = $VD_ADPasswordEnabled
$o.PrinterManagementEnabled = $VD_PrinterManagementEnabled
$o.WriteCacheType = $VD_WritecacheType
$o.WriteCacheSize = $VD_WriteCacheSize
$o.LicenseMode = $VD_LicenseMode
#WriteCacheType = 9 is standard mode. use 0 for private mode
$o.WriteCacheType = 9
if ($DiskType -eq "vhdx")
{
$o.VHDX = $true
}
#get the vhd/vhdx path
$OriginalFileO = $o.OriginalFile
if ($OriginalFileO -eq $null){$OriginalFileO = ""}
if ($OriginalFileO.Contains("."))
{
$MyVHDXPath = $OriginalFileO
logline "The new VHDX path for our vDisk is [$MyVHDXPath]"
}
else
{
logline "We were not able to find the imported vDisk after the VHDX conversion -- skipping to next vDisk"
logline "========================================================================"
continue
}
logline "We will now update the vDisk settings to match previous settings"
#Change the disk to use the new disk locator
Set-PvsDisk $o
#Check to make sure it changed
$NewDiskAfterChange2 = Get-PvsDisk -DiskLocatorId $NewDiskLocator.DiskLocatorId
logline "Checking new path for VHDXPath [$MyVHDXPath]"
$OriginalFileNew2 = $NewDiskAfterChange2.OriginalFile
if ($OriginalFileNew2 -eq $null){$OriginalFileNew2 = ""}
if (!($OriginalFileNew2.Contains(".")))
{
logline "We were not able to find the vDisk after converting to VHDX -- skipping to next vDisk"
logline "========================================================================"
continue
}
logline "Internal Name:$OriginalFileNew2"
if ($OriginalFileNew2 -ne $MyVHDXPath)
{
#We failed
logline "The vDisk was imported as $OriginalFileNew2. Please remove the vDisk and add it back in"
continue
}
else
{
#We succeeded
logline "Success - The new vDisk was imported as $ImageName."
}
}
else
{
$bRebaseVersion = $true
}
#=======================
#If we already hasd the image version we need to add this vDisk to it as a new version
#=======================
if ($bRebaseVersion)
{
Logline "Rebasing vDisk [$vDiskName] to ImageName [$ImageName]"
Export-PvsDisk -DiskLocatorId $MyVHDDiskLocator.DiskLocatorId
$existingDiskLoc = Get-PvsDiskLocator -SiteId $MyVHDDiskLocator.SiteId -StoreId $MyVHDDiskLocator.StoreId -DiskLocatorName $ImageName
logline "Getting existing Disk Versions"
$ExistingDiskBaseName = $ImageName
$existingDiskVersions = Get-PvsDiskVersion -DiskLocatorId $existingDiskLoc.DiskLocatorId
$nextVersionNum = ($existingDiskVersions | Measure -Property Version -Maximum).Maximum + 1
#Edit the disk manifest for our versiosn import of the new version
$ManifestFile = "$($MyVHDDiskLocator.Name).xml"
logline "Editing disk manifest file [$ManifestFile] to use version [$NextVersionNum]"
[xml]$manifest = Get-Content -Path "$ManifestFile"
$newDesc = "Created from published disk $($manifest.versionManifest.version.diskFileName)"
$manifest.versionManifest.version.type = "4" # Merged Base - necessary so previous versions can be deleted
$manifest.versionManifest.version.access = "7" # Test - as opposed to Production (0) or Maintenance
$manifest.versionManifest.version.versionNumber = "$NextVersionNum"
if ($DiskType -eq "vhdx")
{
$manifest.versionManifest.version.diskFileName = "$ExistingDiskBaseName.$NextVersionNum.vhdx"
}
else
{
$manifest.versionManifest.version.diskFileName = "$ExistingDiskBaseName.$NextVersionNum.vhd"
}
$manifest.versionManifest.version.description = "$newDesc"
$outPath = "$(Convert-Path -Path ".")\$ImageName.xml"
logline "Writing new disk manifest file '$outPath'"
$manifest.Save($outPath)
# Copy settings from the vDisk
$diskLocks = Get-PvsDiskLocatorLock -DiskLocatorId $existingDiskLoc.DiskLocatorId
if ($diskLocks.Length -le 0) {
logline "Copying configuration settings to existing vDisk: $existingDiskBaseName"
$newDisk = Get-PvsDisk -DiskLocatorId $MyVHDDiskLocator.DiskLocatorId
$existingDisk = Get-PvsDisk -DiskLocatorId $existingDiskLoc.DiskLocatorId
$existingDisk.PrinterManagementEnabled = $newDisk.PrinterManagementEnabled
$existingDisk.AdPasswordEnabled = $newDisk.AdPasswordEnabled
$existingDisk.WriteCacheType = $newDisk.WriteCacheType
$existingDisk.LicenseMode = $newDisk.LicenseMode
$OriginalFileED = $existingDisk.OriginalFile
if ($OriginalFileED -eq $null){$OriginalFileED = $existingDisk.OriginalFile}
if ($OriginalFileED -eq $null){$OriginalFileED = ""}
if (!($OriginalFileED.Contains(".")))
{
logline "We were not able to find the vDisk file name for versioning -- skipping vDisk"
continue
}
} else {
logline "WARNING: vDisk '$existingDiskBaseName' is locked, configuration settings will not be changed!"
}
logline "Removing the disk locator with id [" + $MyVHDDiskLocator.DiskLocatorId + "]"
Try {
Remove-PvsDiskLocator -DiskLocatorId $MyVHDDiskLocator.DiskLocatorId
}
Catch
{
logline "We could not remove the vDisk with disk locator [$($MyVHDDiskLocator.DiskLocatorId)] -- skipping vDisk"
Logline $error[0]
continue
}
#Now lets rename the pvp and vhdx to our version numbering naming scheme
$NewDiskBaseName = $MyVHDDiskLocator.Name
logline "Renaming vDisk files to use version number [$NextVersionNum]"
if (Test-Path "$NewDiskBaseName.lok")
{
Remove-Item "$NewDiskBaseName.lok"
}
if (Test-Path "$NewDiskBaseName.xml")
{
Remove-Item "$NewDiskBaseName.xml"
}
if (Test-Path "$NewDiskBaseName.pvp")
{
copy-Item "$NewDiskBaseName.pvp" "$ExistingDiskBaseName.$NextVersionNum.pvp" -Force
remove-item "$NewDiskBaseName.pvp"
}
if (Test-Path "$NewDiskBaseName.$DiskType")
{
if (!(Test-Path "$ExistingDiskBaseName.$NextVersionNum.$DiskType"))
{
rename-Item "$NewDiskBaseName.$DiskType" "$ExistingDiskBaseName.$NextVersionNum.$DiskType"
}
}
Try {
Add-PvsDiskVersion -DiskLocatorId $existingDiskLoc.DiskLocatorId.guid
}
Catch
{
logline "vDisk $($MyVHDDiskLocator.Name) could not be added with version $nextVersionNum of vDisk $existingDiskBaseName"
}
logline "vDisk $($MyVHDDiskLocator.Name) successfully converted to version $nextVersionNum of vDisk $existingDiskBaseName"
Remove-Item "$existingDiskBaseName.xml" -ErrorAction Continue
}
}
Push-Location -Path $ScriptSource
logline "========================================================================"
logline "Errors During Script"
logline "========================================================================"
FormatErrors
logline "========================================================================"