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 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" # Performs the profile migration .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") -FilestoRemove @("*.tmp", "*.log") -VHD -IncludeRobocopyDetail -LogPath "C:\Logs\migration.log" # Includes Registry and detailed Robocopy logs .NOTES This function requires administrator privileges to execute. #> 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 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 { Test-NeededFeature -LogPath $LogPath } catch { Write-Log $_ return } $SuccessProfileList = @() $FailedProfileList = @() $SkippedProfileList = @() $CopyParams = @{ } $Success = 0 $Skipped = 0 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 -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" Hide-Folder -Destination $Destination -LogPath $LogPath $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-UnwantedFile -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" FilesToRemove = $FilestoRemove StartBinPath = "C:\Temp\start2.bin" DesktopShortcutPath = "C:\Temp\Desktop (create shortcut).DeskLink" } Update-UserProfile -Parameters $params1 $StartMenuProgramsPath = "$Destination\AppData\Roaming\Microsoft\windows\Start Menu\Programs\Windows PowerShell" Test-CreateShortcut -StartMenuProgramsPath $StartMenuProgramsPath -LogPath $LogPath Set-RegistryConfiguration -LogPath $LogPath -NtuserDatPath "$Destination\NTUSER.DAT" 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 "***************************************************************************************************" } } |