Public/Invoke-ProfileMigration.ps1
<#
.SYNOPSIS Migrates a user profile to a target location, optionally creating a VHD. .DESCRIPTION The `Invoke-ProfileMigration` function migrates a user profile to a specified target location. It supports creating a VHD, copying profile data, setting NTFS permissions, and updating registry configurations. .PARAMETER ProfilePath The path to the profile to be migrated. .PARAMETER HomePath The path to the home directory. .PARAMETER Target The target path for the migrated profile. .PARAMETER VHDMaxSizeGB The maximum size of the VHD in GB. .PARAMETER VHDLogicalSectorSize The logical sector size of the VHD. Valid values are '4K' and '512'. .PARAMETER SearchRoots An array of search root paths to search in. .PARAMETER LogPath (Optional) The path to the log file where log messages will be written. .PARAMETER RegistryPaths (Optional) An array of registry paths to remove. .PARAMETER FilestoRemove (Optional) An array of files to remove. .PARAMETER VHD (Optional) A switch to create a VHD. .PARAMETER IncludeRobocopyDetail (Optional) A switch to include detailed Robocopy logs. .EXAMPLE PS C:\> Invoke-ProfileMigration -ProfilePath "C:\Users\jdoe" -HomePath "H:\jdoe" -Target "E:\MigratedProfiles" -VHDMaxSizeGB 100 -VHDLogicalSectorSize "4K" -SearchRoots @("GC://dc=test,dc=LOCAL", "GC://dc=testing,dc=LOCAL") -LogPath "C:\Logs\migration.log" .EXAMPLE PS C:\> Invoke-ProfileMigration -ProfilePath "C:\Users\jdoe" -HomePath "H:\jdoe" -Target "E:\MigratedProfiles" -VHDMaxSizeGB 100 -VHDLogicalSectorSize "512" -SearchRoots @("GC://dc=test,dc=LOCAL") -RegistryPaths @("Software\MyApp\Settings", "Software\MyApp\Data") -FilestoRemove @("*.tmp", "*.log") -VHD -IncludeRobocopyDetail -LogPath "C:\Logs\migration.log" .NOTES Author: Sundeep Eswarawaka Date: 2024-05-16 This function requires administrator privileges to execute. #> function Check-NeededFeatures { [CmdletBinding()] param ( [Parameter(Mandatory = $false, HelpMessage = "The name of the first feature to check.")] [string]$FeatureName1 = "Hyper-V", [Parameter(Mandatory = $false, HelpMessage = "The name of the second optional feature to check.")] [string]$FeatureName2 = "Microsoft-Hyper-V-Management-PowerShell", [Parameter(Mandatory = $false, HelpMessage = "The name of the third feature to check.")] [string]$FeatureName3 = "RSAT-Hyper-V-Tools", [Parameter(Mandatory = $false, HelpMessage = "Optional path to a log file for logging messages.")] [string]$LogPath ) function Write-Log { param ( [string]$Message, [string]$LogPath ) if ($LogPath) { Add-Content -Path $LogPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" } else { Write-Host $Message } } # Check if the OS is Windows Server $os = Get-WmiObject Win32_OperatingSystem if ($os.Caption -notmatch "Windows Server") { $errorMsg = "This script needs to be run on a Windows Server operating system." Write-Log -Message $errorMsg -LogPath $LogPath throw $errorMsg } Write-Log -Message "Checking if the prerequisites are installed before starting the migration." -LogPath $LogPath Write-Log -Message "Checking if the $FeatureName1, $FeatureName2, and $FeatureName3 features are installed or enabled." -LogPath $LogPath $feature1 = Get-WindowsFeature -Name $FeatureName1 if (-not $feature1.Installed) { $errorMsg1 = "The $FeatureName1 feature is not installed. Please install it using the following command before running the migration: `Install-WindowsFeature -Name $FeatureName1`." Write-Log -Message $errorMsg1 -LogPath $LogPath throw $errorMsg1 } else { Write-Log -Message "The $FeatureName1 feature is installed." -LogPath $LogPath } $feature2 = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName2 if ($feature2.State -ne 'Enabled') { $errorMsg2 = "The $FeatureName2 optional feature is not enabled. Please enable it using the following command before running the migration: `Enable-WindowsOptionalFeature -Online -FeatureName $FeatureName2`." Write-Log -Message $errorMsg2 -LogPath $LogPath throw $errorMsg2 } else { Write-Log -Message "The $FeatureName2 optional feature is enabled." -LogPath $LogPath } $feature3 = Get-WindowsFeature -Name $FeatureName3 if (-not $feature3.Installed) { $errorMsg3 = "The $FeatureName3 feature is not installed. Please install it using the following command before running the migration: `Install-WindowsFeature -Name $FeatureName3`." Write-Log -Message $errorMsg3 -LogPath $LogPath throw $errorMsg3 } else { Write-Log -Message "The $FeatureName3 feature is installed." -LogPath $LogPath } $service = Get-Service -Name "vmms" if ($service.Status -ne 'Running') { $errorMsg4 = "The Hyper-V Virtual Machine Management service (vmms) is not running. Please ensure it is installed and running before running the migration." Write-Log -Message $errorMsg4 -LogPath $LogPath throw $errorMsg4 } else { Write-Log -Message "The Hyper-V Virtual Machine Management service (vmms) is running." -LogPath $LogPath } Write-Log -Message "All prerequisites are installed for the migration." -LogPath $LogPath } function Invoke-ProfileMigration { [CmdletBinding(SupportsShouldProcess = $True)] Param ( [Parameter(Mandatory = $True, HelpMessage = "Path to the profile to be migrated.")] [string]$ProfilePath, [Parameter(Mandatory = $false, HelpMessage = "Path to the home directory.")] [string]$HomePath, [Parameter(Mandatory = $True, HelpMessage = "Target path for the migrated profile.")] [string]$Target, [Parameter(Mandatory = $True, HelpMessage = "Maximum size of the VHD in GB.")] [uint64]$VHDMaxSizeGB, [Parameter(Mandatory = $True, HelpMessage = "Logical sector size of the VHD.")] [ValidateSet('4K', '512')] [string]$VHDLogicalSectorSize, [Parameter(Mandatory = $true, HelpMessage = "Array of search root paths to search in.")] [string[]]$SearchRoots, [Parameter(HelpMessage = "Path to the log file where log messages will be written.")] [string]$LogPath, [Parameter(Mandatory = $false, HelpMessage = "Array of registry paths to remove.")] [string[]] $RegistryPaths, [Parameter(Mandatory = $false, HelpMessage = "Array of files to remove.")] [string[]] $FilestoRemove, [Parameter(HelpMessage = "Switch to create a VHD.")] [switch]$VHD, [Parameter(HelpMessage = "Switch to include detailed Robocopy logs.")] [switch]$IncludeRobocopyDetail ) # Check prerequisites try { Check-NeededFeatures -LogPath $LogPath } catch { Write-Host $_ return } $SuccessProfileList = @() $FailedProfileList = @() $SkippedProfileList = @() $CopyParams = @{ } $Success = 0 $Skipped = 0 function Write-Log { param ( [string]$Message, [string]$LogPath ) Add-Content -Path $LogPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" } if ($VHD) { $Params = @{ 'VHD' = $true } } else { $Params = @{ } } try { $BatchObject = Get-ProfileSource -ProfilePath $ProfilePath -ErrorAction Stop | New-MigrationObject -Target $Target @Params -ErrorAction Stop } catch { Write-Log -Message "Cannot create batch object" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath return } $BatchStartTime = Get-Date foreach ($P in $BatchObject) { Write-Log -Message "-----------------------------------------------------------------------------" -LogPath $LogPath Write-Log -Message "Beginning Migration of $($P.ProfilePath)" -LogPath $LogPath Write-Log -Message "-----------------------------------------------------------------------------" -LogPath $LogPath if ($P.Target -ne "Cannot Copy") { $ProfileStartTime = Get-Date if (-not (Test-Path ($P.Target.Substring(0, $P.Target.LastIndexOf('.')) + "*"))) { try { $Drive = (New-UserProfileDisk -ProfilePath $P.ProfilePath -Target $P.Target -Username $P.Username -Size $VHDMaxSizeGB -SectorSize $VHDLogicalSectorSize -LogPath $LogPath -ErrorAction Stop).Drive } catch { Write-Log -Message "Could not create or mount Profile Disk" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath continue } if ($Drive) { $CopyParams = @{ } if ($IncludeRobocopyDetail) { $CopyParams["IncludeRobocopyDetail"] = $True } try { $changeinpath = Join-Path -Path $P.ProfilePath -ChildPath "UPM_Profile" Invoke-CopyProfileData -Drive $Drive -ProfilePath $changeinpath -LogPath $LogPath @CopyParams if ($null -ne $HomePath) { Invoke-CopyProfileData -Drive $Drive -ProfilePath $HomePath -LogPath $LogPath @CopyParams } else { Write-Log -Message "Skipping HomePath,Since it is null" -LogPath $LogPath } } catch { Write-Log -Message "Could not copy" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath continue } $Destination = "$Drive`Profile" $samAccountName = $P.Username # First attempt to find the SID in the primary domain $Domain = Get-UserDomain -SamAccountName $samAccountName -SearchRoots $SearchRoots -LogPath $LogPath -ErrorAction SilentlyContinue try { icacls $Destination /setowner "$Domain\$samAccountName" /T /C | Out-Null icacls $Destination /reset /T | Out-Null $sidvalue = (New-Object System.Security.Principal.NTAccount($samAccountName)).Translate([System.Security.Principal.SecurityIdentifier]).Value # First attempt to find the SID in the primary domain New-UserProfileRegistry -UserSID $sidvalue -Drive $Drive -SearchRoots $SearchRoots -LogPath $LogPath -ErrorAction SilentlyContinue Write-Log -Message "Adding User and System NTFS Permissions" -LogPath $LogPath } catch { Write-Log -Message "Cannot create Registry File" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath continue } try { icacls $Destination /grant "Administrators:(OI)(CI)F" /T | Out-Null icacls $Destination /grant "$domain\$samAccountName`:(OI)(CI)F" /T | Out-Null icacls $Destination /grant "SYSTEM:(OI)(CI)F" /T | Out-Null icacls ($P.Target | Split-Path) /setowner "$Domain\$($P.Username)" /T /C | Out-Null icacls (($P).Target | Split-Path) /grant $domain\$(($P).Username)`:`(OI`)`(CI`)F /T | Out-Null } catch { Write-Log -Message "Could not Add Permissions to Disk" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath continue } Remove-UnwantedFiles -UserProfilePath $Destination -LogPath $LogPath # Example usage $params1 = @{ LogPath = $LogPath ProfileDesktopPath = "$Destination\Desktop" AppDataRoamingPath = "$Destination\AppData\Roaming\Microsoft\Windows\SendTo" LocalStatePath = "$Destination\AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" StartMenuProgramsPath = "$Destination\AppData\Roaming\Microsoft\windows\Start Menu\Programs\Windows PowerShell" FilesToRemove = $FilestoRemove StartBinPath = "C:\Temp\start2.bin" DesktopShortcutPath = "C:\Temp\Desktop (create shortcut).DeskLink" } Update-UserProfile -Parameters $params1 Set-RegistryConfiguration -LogPath $LogPath -NtuserDatPath "$Destination\NTUSER.DAT" $folderPath = "$Destination\AppData" # Set the hidden attribute on the folder Set-ItemProperty -Path $folderPath -Name Attributes -Value ([System.IO.FileAttributes]::Hidden) Write-Log -Message "Dismounting $($P.Target)" -LogPath $LogPath try { Dismount-VHD $P.Target -ErrorAction Stop } catch { Write-Log -Message "Could not dismount drive" -LogPath $LogPath Write-Log -Message $_ -LogPath $LogPath continue } $ProfileEndTime = Get-Date $ProfileDuration = "{0:hh\:mm\:ss}" -f ($ProfileEndTime - $ProfileStartTime) Write-Log -Message "$($P.ProfilePath) Migrated. Duration: $ProfileDuration" -LogPath $LogPath Write-Output "$($P.ProfilePath) Migrated. Duration: $ProfileDuration" if (Test-Path $P.Target) { $Success++ $SuccessProfileList += $P.ProfilePath } } else { Write-Log -Message "Could not create or mount target drive." -LogPath $LogPath Write-Error "Could not create or mount target drive." } } else { Write-Log -Message "Profile $($P.Target.Substring(0, $P.Target.LastIndexOf('.'))) already exists. Skipping." -LogPath $LogPath Write-Warning "Profile $($P.Target.Substring(0, $P.Target.LastIndexOf('.'))) already exists. Skipping." $Skipped++ $SkippedProfileList += $P.ProfilePath } } elseif ($P.Target -eq "Cannot Copy") { Write-Log -Message "Profile $($P.ProfilePath) Could not resolve to AD User. Cannot copy." -LogPath $LogPath Write-Warning "Profile $($P.ProfilePath) Could not resolve to AD User. Cannot copy." $FailedProfileList += $P.ProfilePath } } $BatchEndTime = Get-Date $duration = $BatchEndTime - $BatchStartTime $BatchDuration = "{0:hh\:mm\:ss}" -f $duration Write-Log -Message "Total duration: $BatchDuration" -LogPath $LogPath Write-Output " ----------------------------------------------------- Profile Migration Completed. Source: $ProfilePath Target: $Target Start time: $BatchStartTime End time: $BatchEndTime Duration: $BatchDuration Total Profiles: $(($batchObject | Measure-Object).count) Eligible Profiles: $(($batchObject | Where-Object Target -NE "Cannot Copy" | Measure-Object).count) Successful Migrations: $Success Skipped Migrations: $Skipped Failed Migrations: $($(($batchobject | Measure-Object).count) - $($Success) - $($Skipped))" if (($SuccessProfileList | Measure-Object).count -gt 0) { Write-Output " Successful Migration List:" $SuccessProfileList } if (($SkippedProfileList | Measure-Object).count -gt 0) { Write-Output " Skipped Migration List:" $SkippedProfileList } if (($FailedProfileList | Measure-Object).count -gt 0) { Write-Output " Failed Migration List:" $FailedProfileList } Write-Output "-----------------------------------------------------" if ($LogPath) { Add-Content -Path $LogPath -Value "`n" Add-Content -Path $LogPath -Value "***************************************************************************************************" Add-Content -Path $LogPath -Value "$([DateTime]::Now) - Finished processing" Add-Content -Path $LogPath -Value "***************************************************************************************************" } } |