├── Convert-UPDtoFSLogix.ps1 └── README.md /Convert-UPDtoFSLogix.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Convert regular UPD VHDX files to the content format and folder structure for FSLogix 4 | .NOTES 5 | Script assumes SAM_SID folder structure as this is easier to use when browsing for profiles in a folder. FSLogix default format is SID_SAM 6 | Script assumes VHDX file extension. FSLogix default is VHD 7 | Options to configure this are documented here: https://docs.microsoft.com/en-us/fslogix/configure-profile-container-tutorial 8 | .REQUIREMENTS 9 | AD Module 10 | #> 11 | 12 | ##################################################### 13 | ################ Specify paths here ################# 14 | ##################################################### 15 | 16 | 17 | # Source path containg UVHD-SID.VHDX UPD files 18 | # Where are the current UPD VHDX files? 19 | $updroot = "\\filesever01\uvhd_profiles" 20 | 21 | # Root path for FSLogix data. 22 | # Where will the VHDX files for FSLogix go? 23 | $fslogixroot = "\\fileserver01\fslogix_profiles" 24 | 25 | # Value in seconds to pause when mounting and dismounting the VHDX. 26 | # Helps make sure the process has completed. Allow time for drive letter allocation after mounting source vhdx 27 | $delay = 3 28 | 29 | # File use to record any vhdx files which fail to mount. 30 | # These files are left in the original location and are not modified in anyway 31 | $errorlogfolder = "\\filserver01\uvhd_profiles" 32 | $errorlogfilename = "vhdxmountfailed.txt" 33 | $errorlogresults = $errorlogfolder+'\'+$errorlogfilename 34 | 35 | 36 | ##################################################### 37 | ########## No need to edit below this line ########## 38 | ##################################################### 39 | 40 | # Outputs the current HHmmss value when called. Used to prefix log and console entries 41 | Function ThisHHmmss() { 42 | (Get-Date).ToString("HH:mm:ss") 43 | } 44 | 45 | # Stop if the AD module cannot be loaded 46 | If (!(Get-module ActiveDirectory)) { 47 | Import-Module ActiveDirectory -ErrorAction Stop 48 | Write-Host (ThisHHmmss) "AD PowerShell Module not found or could not be loaded" -ForegroundColor Red 49 | } 50 | 51 | 52 | # If the VHDX fails to mount then this function is called and there is additional code to deal with what action to take next 53 | Function MountError ($ErrorRecord=$Error[0]) 54 | { 55 | $ErrorRecord | Format-List * -Force 56 | $ErrorRecord.InvocationInfo |Format-List * 57 | $Exception = $ErrorRecord.Exception 58 | for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException)) 59 | { "$i" * 80 60 | $Exception |Format-List * -Force 61 | } 62 | } 63 | 64 | 65 | 66 | 67 | 68 | # Create the log folder and file for any VHDX files which fail to mount 69 | # Create folder if missing 70 | If(!(Test-Path $errorlogfolder)) 71 | { 72 | New-Item -ItemType Directory -Force -Path $fslogixroot 73 | Write-Host (ThisHHmmss) "Created $fslogixroot" -ForegroundColor Yellow 74 | } 75 | # Create file if missing 76 | If(!(Test-Path $errorlogresults)) 77 | { 78 | New-Item -Path $errorlogfolder -ItemType File -Name $errorlogfilename 79 | Write-Host (ThisHHmmss) "Created $errorlogresults" -ForegroundColor Yellow 80 | } 81 | 82 | 83 | # VHDX work begins here 84 | 85 | # Create the FSLogix root path if it does not exist 86 | If(!(Test-Path $fslogixroot)) 87 | { 88 | New-Item -ItemType Directory -Force -Path $fslogixroot 89 | } 90 | 91 | 92 | # Index the UPD VHDX files 93 | $files = Get-ChildItem -Path $updroot -File -Filter UVHD-S*.vhdx | Sort Name 94 | 95 | # Convert VHDX filename AD account information and add to the array $results 96 | ForEach ($file in $files) { 97 | # Obtain the SID in the filename by removing the UVHD- prefix 98 | $sid = ($file.Basename).Substring(5) 99 | If 100 | ( 101 | # Only proceed with this file if there is an AD user with this SID 102 | (Get-ADUser -Filter { SID -eq $sid }) -ne $null 103 | ) { 104 | # Obtain Name and SAM values from the user SID 105 | $userinfo = Get-ADUser -Filter { SID -eq $sid } | Select Name, SamAccountName, UserPrincipalName, SID 106 | $name = ($userinfo.Name).ToString() 107 | $sam = ($userinfo.SamAccountName).ToString() 108 | Write-Host (ThisHHmmss) "Processing account: $name ($sam)" -ForegroundColor Green 109 | 110 | # Source UPD VHDX 111 | $sourcevhdx = $file.FullName 112 | # Unique user SID_SAM user folder name to store FSLogix VHDX 113 | $sam_sid = "$sid"+"_"+"$sam" 114 | # Full folder path to store VHDX 115 | $fslogixuserpath = "$fslogixroot" + "\" + "$sam_sid" 116 | 117 | # Mount the source VHDX and obtain the drive mapping 118 | Write-Host (ThisHHmmss) "Mounting VHDX: $sourcevhdx" -ForegroundColor Green 119 | # Stop the script is mounting fails 120 | Mount-DiskImage -ImagePath $sourcevhdx -ErrorAction SilentlyContinue -ErrorVariable MountError | Out-Null; 121 | 122 | # If the VHDX failed to mount then add the filename to the error log 123 | If ($MountError){ 124 | Write-Host "Failed to mount" $sourcevhdx -ForegroundColor Yellow 125 | Add-Content -Path $errorlogresults -Value $sourcevhdx 126 | } 127 | 128 | # Small delay to ensure VHDX has been mounted 129 | Write-Host (ThisHHmmss) "$delay Second delay after mounting $sourcevhdx" -ForegroundColor Green 130 | Start-Sleep -Seconds $delay 131 | # Get drive letter 132 | $mountletter = (Get-DiskImage -ImagePath $sourcevhdx | Get-Disk | Get-Partition).DriveLetter 133 | $mountpath = ($mountletter + ':\') 134 | 135 | 136 | 137 | # Note that the mount letter is null becauase the VHDX failed to mount 138 | If ($mountletter -eq $null){ 139 | Write-Host "Path is blank" -ForegroundColor Yellow 140 | } 141 | 142 | #region mountsuccess 143 | If ($mountletter -ne $null){ 144 | 145 | 146 | #region internalVHDX 147 | ##################################################### 148 | ##### These changes occur within the VHDX itself #### 149 | ##################################################### 150 | 151 | ## Create a folder called Profile in the root of the mounted VHDX 152 | # Define path in the profile disk 153 | $ProfileDir = 'Profile' 154 | $vhdxprofiledir = Join-Path -Path $mountpath -ChildPath $ProfileDir 155 | # Create path in the profile disk 156 | If (!(Test-Path $vhdxprofiledir)) { 157 | Write-Output "Create Folder: $vhdxprofiledir" 158 | New-Item $vhdxprofiledir -ItemType Directory | Out-Null 159 | } 160 | 161 | ## Move the user content into the new Profile folder 162 | # Defining the files and folders that should not be moved 163 | $Excludes = @("Profile", "Uvhd-Binding", "`$RECYCLE.BIN", "System Volume Information") 164 | 165 | # Copy profile disk content to the new profile folder 166 | $Content = Get-ChildItem $mountpath -Force 167 | ForEach ($C in $Content) { 168 | If ($Excludes -notcontains $C.Name) { 169 | Write-Output ('Move: ' + $C.FullName) 170 | Try { Move-Item $C.FullName -Destination $vhdxprofiledir -Force -ErrorAction Stop } 171 | Catch { Write-Warning "Error: $_" } 172 | } 173 | 174 | } 175 | 176 | ## Create the .reg file containing the FSLogix information for the profile 177 | 178 | # Defining the registry file 179 | $regtext = "Windows Registry Editor Version 5.00 180 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$SID] 181 | `"ProfileImagePath`"=`"C:\\Users\\$SAM`" 182 | `"Flags`"=dword:00000000 183 | `"State`"=dword:00000000 184 | `"ProfileLoadTimeLow`"=dword:00000000 185 | `"ProfileLoadTimeHigh`"=dword:00000000 186 | `"RefCount`"=dword:00000000 187 | `"RunLogonScriptSync`"=dword:00000001 188 | " 189 | 190 | # Create the folder and registry file 191 | Write-Output "Create Reg: $vhdxprofiledir\AppData\Local\FSLogix\ProfileData.reg" 192 | If (!(Test-Path "$vhdxprofiledir\AppData\Local\FSLogix")) { 193 | New-Item -Path "$vhdxprofiledir\AppData\Local\FSLogix" -ItemType directory | Out-Null 194 | } 195 | If (!(Test-Path "$vhdxprofiledir\AppData\Local\FSLogix\ProfileData.reg")) { 196 | $regtext | Out-File "$vhdxprofiledir\AppData\Local\FSLogix\ProfileData.reg" -Encoding ascii 197 | } 198 | 199 | ##################################################### 200 | ########### End of internal VHDX changes ############ 201 | ##################################################### 202 | #endregion internalVHDX 203 | 204 | # Dismount source VHDX 205 | Dismount-DiskImage -ImagePath $sourcevhdx 206 | # Small delay after dismounting the VHDX file to ensure it and the drive letter are free 207 | Write-Host (ThisHHmmss) "$delay Second delay after dismounting $sourcevhdx" -ForegroundColor Green 208 | Start-Sleep -Seconds $delay 209 | 210 | ### Moving and renaming the VHDX happens here ### 211 | 212 | # Create the new SAM_SID user folder in the FSLogix root path 213 | Write-Host (ThisHHmmss) "Creating new folder $fslogixuserpath" -ForegroundColor Green 214 | New-Item -Path $fslogixroot -Name $sam_sid -ItemType Directory | Out-Null 215 | 216 | # Move the source UPD VHDX to the fsLogix path 217 | Write-Host (ThisHHmmss) "Moving original VHDX to new FSLogix location" -ForegroundColor Green 218 | Move-Item -Path $sourcevhdx -Destination $fslogixuserpath 219 | 220 | # Rename the VHDX file from the UPD format to the fsLogix format 221 | $updvhdx = "$fslogixuserpath" + "\" + "$file" 222 | $fslogixvhdx = "Profile_" + "$sam" + ".vhdx" 223 | Rename-Item $updvhdx -NewName $fslogixvhdx 224 | 225 | # This is the full filepath of the new VHDX file 226 | $newUVHD = "$fslogixuserpath" + "\" + "$fslogixvhdx" 227 | 228 | # Update NTFS permission to give the user RW access 229 | & icacls $fslogixuserpath /setowner "$env:userdomain\$sam" /T /C | Out-Null 230 | & icacls $fslogixuserpath /grant $env:userdomain\$sam`:`(OI`)`(CI`)F /T | Out-Null 231 | & icacls $newUVHD /grant $env:userdomain\$sam`:`(OI`)`(CI`)F /T /inheritance:E | Out-Null 232 | 233 | Write-Host (ThisHHmmss) "Finished processing account $Name" -ForegroundColor Green 234 | 235 | } 236 | #endregion mountsuccess 237 | 238 | 239 | 240 | # Clear user variables to be safe 241 | Clear-Variable file, sid, userinfo, name, sam, sourcevhdx, sam_sid, fslogixuserpath, mountletter, mountpath, vhdxprofiledir, Content, regtext, updvhdx, fslogixvhdx, newUVHD 242 | Write-Host "#######################################################" 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrate UPD Profiles to FSLogix 2 | 3 | ## Pre-requesites 4 | 5 | AD Powershell Modules 6 | 7 | ```powershell 8 | Install-WindowsFeature RSAT-AD-PowerShell 9 | ``` 10 | 11 | ## Migrate UPD VHDs to FSLogix VHDs 12 | 13 | 1. Copy UVHDs to new folder and share folder e.g to `\\fileserver01\uvhd_profiles` 14 | 2. Create new folder and share folder e.g. at `\\fileserver01\fslogix_profiles` 15 | 3. Adjust variables in [Convert-UPDtoFSLogix.ps1](Convert-UPDtoFSLogix.ps1) 16 | ```powershell 17 | set $updroot = "\\fileserver01\uvhd_profiles" 18 | set $fslogixroot = "\\fileserver01\fslogix_profiles" 19 | set $errorlogfolder = "\\fileserver01\uvhd_profiles" 20 | ``` 21 | 4. Run [Convert-UPDtoFSLogix.ps1](Convert-UPDtoFSLogix.ps1) in ISE as Administrator 22 | 23 | ## Disable User Profile Disks on all RDSH 24 | 25 | Use Server Manager to disable User Profile Disks on all RDSH. 26 | 27 | Or use powershell 28 | 29 | Example: 30 | ```powershell 31 | Set-RDSessionCollectionConfiguration -CollectionName "Session Collection 02" -DisableUserProfileDisk -ConnectionBroker "RDCB.Contoso.com" 32 | ``` 33 | 34 | ## Install and Configure FSLogix 35 | 36 | Download and install FSLogix 37 | [https://aka.ms/fslogix_download](https://aka.ms/fslogix_download) 38 | 39 | 40 | ### Configure FSLogix on all RDSH 41 | 42 | Example: 43 | ``` 44 | [HKEY_LOCAL_MACHINE\SOFTWARE\FSLogix\Profiles] 45 | "Enabled"=dword:00000001 46 | "VHDLocations"="\\fileserver01\fslogix_profiles" 47 | "VolumeType"="VHDX" 48 | "SizeInMBs"=dword:00000c00 49 | "IsDynamic"=dword:00000001 50 | "LockedRetryCount"=dword:00000018 51 | "LockedRetryInterval"=dword:00000006 52 | ``` 53 | 54 | [FSLogix Documentation](https://docs.microsoft.com/en-us/fslogix/) 55 | 56 | 57 | ## Optional - Convert VHD to VHDX 58 | 59 | 60 | 61 | ### Pre-Requesites 62 | 63 | Check exisiting 64 | ```powershell 65 | Get-WindowsFeature *hyper-v* 66 | ``` 67 | 68 | Hyper-V-Powershell 69 | ```powershell 70 | Install-WindowsFeature -Name Hyper-V-PowerShell 71 | ``` 72 | 73 | Convert VHD to VHDX 74 | ```powershell 75 | Convert-VHD TestVHD.vhd -VHDFormat VHDX -DestinationPath C:\temp\VHDs\TestVHDX.vhdx -DeleteSource 76 | ``` 77 | 78 | ## Reference 79 | 80 | The script is a modified version from Roger Critz from the [Microsoft Tech Community Post](https://techcommunity.microsoft.com/t5/windows-virtual-desktop/convert-upd-to-fslogix-container/m-p/927214) --------------------------------------------------------------------------------