Skip to content

Instantly share code, notes, and snippets.

@automationhaus
Last active December 8, 2016 15:25
Show Gist options
  • Save automationhaus/28f2acefa3582afff7d47ee94c68f68e to your computer and use it in GitHub Desktop.
Save automationhaus/28f2acefa3582afff7d47ee94c68f68e to your computer and use it in GitHub Desktop.
Advanced multi source and destination profile migration function
function Start-ProfileMigration
{
<#
.SYNOPSIS
Advanced data migration function using Robocopy
.DESCRIPTION
Using Robocopy and designed for scheduling multiple incremental data migrations to mirror the source with the destination
.PARAMETER Name
Job name string to use in labeling log files and output
.PARAMETER Source
Data copy source string array
.PARAMETER Destination
Data copy destination string array where, if multiple destinations are specified, the index number of the destination item will correspond to the index number of the source item. Otherwise, the destination for all sources.
.PARAMETER NetBIOSDN
NetBIOS domain name string used to replace profile folder names that use the .NETBIOS syntax and applying destination folder ownership
.PARAMETER SubFolder
Profile subfolder string array designed for migrating profiles from Citrix User Profile Management where the profile data is contained in a UPM_Profile subfolder. The source index association applies.
.PARAMETER CheckLastWriteOn
File item within the the profile folder source to check the last write date on for profile age confirmation. Use a *\<path>\filename.ext syntax to search all subfolders. The source index association applies.
.PARAMETER ExcludeUsers
Username string array of accounts to exclude from the profile migration.
.PARAMETER ExcludeFiles
Robocopy file exclusions array list of strings. Defaults to "*.log","*.tmp","~*","*.dat","*.dat.*","*.bak","*.regtrans-ms","*.blf","*.pol","desktop.ini","ntuser.ini".
.PARAMETER IncludeFolders
Array list of folder name strings to include in the data copy from the source root, or source\subfolder root. This builds a list of Robocopy exclusions not containing these folders. Defaults to "Documents","Desktop","Downloads","Saved Games","Contacts","Favorites","Links","Pictures","Sharepoint","Sharepoint Sites".
.PARAMETER IncludeAppData
Hash table including the source AppData subfolder and underlying folder names to include. This builds a list of Robocopy exclusions not containing these folders. Defaults to @{Local = @("Google");Roaming = @("Google")}.
.PARAMETER ExcludeFolders
Robocopy folder exclusions array list of strings. Defaults to "Temp","Temporary Internet Files","INetCache","INetCookies","WebCache","AppData\LocalLow".
.PARAMETER Age
Oldest profile age integer in days. Checks against last write file and Active Directory. Defaults to 90 days.
.PARAMETER SetPermissions
Boolean option to disable setting the permissions on the destination directory to properly host a profile. Defaults to true.
.PARAMETER SkipExisting
Boolean option to enable skipping the copy of profiles that already exist in the destination. Defaults to false.
.PARAMETER PrimarySource
Boolean option to enable the primary source. This is used to only include profiles in secondary sources that do not exist in the primary source. The primary source is the first source given. Defaults to true.
.EXAMPLE
Basic single source and destination with file type exclusions
PS>Start-DataMigration -Name "UserMigration" -Source "\\SERVER1\Share" -Destination "E:\Share" -NetBIOSDN "CONTOSO" -ExcludeFiles "*.vmdk","*.mp3","*.iso"
.EXAMPLE
Basic multiple sources and a single destination
PS>Start-DataMigration -Name "UserMigration" -Source "\\SERVER1\Share","\\SERVER1\Share2" -Destination "E:\Share" -NetBIOSDN "CONTOSO"
.EXAMPLE
Advanced multiple source and destination with a check file, last use age, includes and excludes
PS>Start-DataMigration -Name "UserMigration"
-Source "\\SERVER1\Share","\\SERVER1\Share2"
-Destination "E:\Share","E:\Share2"
-SubFolder "UPM_Profile",""
-CheckLastWriteOn "*\PmCompatibility.ini","*\AppData\Local\Microsoft\Windows\UsrClass.dat"
-ExcludeUsers "Administrator"
-Age 90
-SetPermissions $true
-NetBIOSDN "CONTOSO"
.NOTES
Britt Thompson
[email protected]
.LINK
https://gist.github.com/amesritter/28f2acefa3582afff7d47ee94c68f68e
#>
[CmdletBinding(SupportsShouldProcess)]
param
(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$true)]
[string[]]$Source,
[Parameter(Mandatory=$true)]
[string[]]$Destination,
[Parameter(Mandatory=$true)]
[string]$NetBIOSDN,
[string[]]$SubFolder,
[string[]]$CheckLastWriteOn,
[string[]]$ExcludeUsers,
[System.Collections.ArrayList]$ExcludeFiles = @("*.log","*.tmp","~*","*.dat","*.dat.*","*.bak","*.regtrans-ms","*.blf","*.pol","desktop.ini","ntuser.ini"),
[System.Collections.ArrayList]$IncludeFolders = @("Documents","Desktop","Downloads","Saved Games","Contacts","Favorites","Links","Pictures","Sharepoint","Sharepoint Sites"),
[System.Collections.Hashtable]$IncludeAppData = @{Local = @("Google");Roaming = @("Google")},
[System.Collections.ArrayList]$ExcludeFolders = @("Temp","Temporary Internet Files","INetCache","INetCookies","WebCache","AppData\LocalLow"),
[int]$Age = 90,
[bool]$SetPermissions = $true,
[bool]$SkipExisting = $false,
[bool]$PrimarySource = $true
)
#region FUNCTIONS
function Write-Tee
{
<#
.SYNOPSIS
Write output to the screen and to file simultaneously
.DESCRIPTION
Simply write your screen output to a file with automatic coloring based with special sytax between square brackets. Enable debugging and produce debugging only ouptut.
.EXAMPLE
Write-Tee "[=] This is the first task in my loop"
.EXAMPLE
Write-Tee @"
Write a long message without a prefix and overwrite your log file
"@ -NoPrefix -Overwrite
.EXAMPLE
Write-Tee "[?] Collect info without a line break" -NoNewLine; Read-Host
#>
[cmdletbinding()]
param
(
[Parameter(Position=0,ValueFromPipeline=$true)]
[string]$msg,
[string]$ForegroundColor = "White",
[string]$OutFile=$LogFile,
[switch]$Overwrite,
[switch]$NoNewLine,
[string]$Prefix=" - ",
[switch]$NoPrefix
)
$Info = "i"; $Task = "="; $Errors = "!"; $Inquiry = "?"; $Debugging = "d"; $Response = "r"
if($ForegroundColor -eq "") { $ForegroundColor = "White" }
$DBG = $False
switch -regex ($msg)
{
"\[$Info\]" { $ForegroundColor = "Yellow" }
"\[$Task\]" { $ForegroundColor = "Cyan"; $Prefix = " " }
"\[$Errors\]" { $ForegroundColor = "Red" }
"\[\$Inquiry\]" { $ForegroundColor = "Magenta" }
"\[$Debugging\]" { $ForegroundColor = "Magenta"; $DBG = $True }
"\[$Response\]" { if($ForegroundColor -eq "White"){ $ForegroundColor = "Green" } }
default { $ForegroundColor = "White" }
}
if(($DBG -eq $False) -or ($DBG -and $Debug)){ $Write = $True }
if(!$NoPrefix)
{
if($Write){ Write-Host $Prefix -NoNewline }
}
if($NoNewLine)
{
if($Write){ Write-Host -ForegroundColor $ForegroundColor $msg -NoNewline }
}
else
{
if($Write){ Write-Host -ForegroundColor $ForegroundColor $msg }
}
if(!$NoPrefix -and $msg -ne ""){ $msg = $Prefix + $msg }
if($OutFile -ne "")
{
if(!$Overwrite)
{
if($NoNewLine)
{
if($Write){ [System.IO.File]::AppendAllText($OutFile, $msg, [System.Text.Encoding]::Unicode) }
}
else
{
if($Write){ $msg | Out-File $OutFile -Append }
}
}
else
{
if($Write){ $msg | Out-File $OutFile -Force }
}
}
}
function Get-Catch
{
<#
.SYNOPSIS
Allows you to use a single line of code for your catch
.DESCRIPTION
Creates consistent repeatable error output in your try / catch
.EXAMPLE
try { Get-Process | ?{$_.ProcessName -match "w3wp"} } catch { Get-Catch }
#>
Write-Tee "[!] $($_.Exception.Message)"
}
#endregion
#region VARS
$ScriptPath = $PSScriptRoot
[string]$DateFormat = $(Get-Date -Format yyyy-MM-dd_HHmm)
$LogPath = "$ScriptPath\Logs"
$LogFile = "$LogPath\$($Name)_$DateFormat.txt"
#endregion
Write-Tee @"
==============================================================================================
Onepath User Profile Migration $(Get-Date)
-------------------------------------------------------------------------------------------
Job Name = $Name
$(if($Age){ "Profile Age = $Age days" })
Script Path = $ScriptPath
Log File = $LogFile
==============================================================================================
"@ -NoPrefix -Overwrite
if ($PSCmdlet.ShouldProcess($Name))
{
$PrimaryProfiles = @()
for ($i=0; $i -lt $Source.Count; $i++)
{
# Profile counter
$X = 1
$Src = $Source[$i]
# Check and see if there's more than 1 destination, subfolder or check file and set the correct destination based on the location in the array
if($Destination.Count -gt 1){ $Dst = $Destination[$i] } else { $Dst = $Destination }
if($SubFolder.Count -gt 1){ $SF = $SubFolder[$i] } else { $SF = $SubFolder }
if($CheckLastWriteOn.Count -gt 1){ $CLW = $CheckLastWriteOn[$i] } else { $CLW = $CheckLastWriteOn }
Write-Tee @"
*****************************************************************************************
Source Number $($i+1) of $($Source.Count) $(Get-Date)
---------------------------------------------------------------------------------------
Source = $Src
Destination = $Dst
Check Last = $CLW
*****************************************************************************************
"@ -NoPrefix
try
{
Write-Tee "[=] Initial settings configuration"
# Create the log file path if it doesn't exist
if(-not (Test-Path $LogPath))
{
$Log = New-Item -Path $ScriptPath -ItemType Directory -Name $(Split-Path $LogPath -Leaf); Write-Tee "[+] Created the log path"
}
# Set an age that will cover all profiles if nothing is set
if(-not $Age){ $Age = 9999; Write-Tee "[i] Profile age set to $Age" }
# Get an array of profile folders in the source path
if($CLW)
{
$Profiles = Get-ChildItem -Path "$Src\$CLW" -Force | ?{ $_.LastWriteTime -ge (Get-Date).AddDays(-$Age) } | %{$_.Directory.FullName}
$SS = (Split-Path $CLW).Substring(1)
Write-Tee "[i] Found $($Profiles.Count) profiles in use within the last $Age days"
}
else
{
$Profiles = Get-ChildItem -Path $Src -Directory -Force | %{$_.FullName}
$SS = $false
Write-Tee "[i] Found $($Profiles.Count) profile folders"
}
# Get the check file path without the wildcard to see if it's deeper than the user folder path
if($SS)
{
# If the check file path is deeper than the user folder path remove the check string from the full directory path and the source string
$Profiles = $Profiles | %{ ($_.Replace($SS,"")).Replace("$Src\","") }
}
else
{
# If the check file path is in the user folder path remove the source string because the full directory name doesn't contain the check file path
$Profiles = $Profiles | %{ ($_).Replace("$Src\","") }
}
$ProfilePaths = @()
# Create an object containing the username and source path
$Profiles | ?{ $PrimaryProfiles.User -notcontains $_ } |
%{
Write-Tee "[d] Check profile folder $_ against AD"
# Remove the NetBIOS domain suffix from the profile folder if it exists
$U = $($_ -replace ".$NetBIOSDN")
# Remove the version suffix from roaming profile folders for the username
if($U -match ".V2"){ $U = $U.Replace(".V2","") }
if($U -match ".V6"){ $U = $U.Replace(".V6","") }
$Obj = New-Object PSObject
# Check if the folder is actually a profile folder for a user in AD
try { $ADUser = Get-ADUser $U -Properties LastLogonDate | ?{$_.LastLogonDate -ge $((Get-Date).AddDays(-$Age))} } catch { Get-Catch }
if($ADUser.Name)
{
$Obj | Add-Member -MemberType NoteProperty -Name User -Value $U
if($SF)
{
$Obj | Add-Member -MemberType NoteProperty -Name Path -Value "$Src\$_\$SF"
}
else
{
$Obj | Add-Member -MemberType NoteProperty -Name Path -Value "$Src\$_"
}
$Obj | Add-Member -MemberType NoteProperty -Name Destination -Value "$Dst\$_"
$ProfilePaths += $Obj
Write-Tee "[+] Added $U"
if($PrimarySource -and $i -eq 0){ $PrimaryProfiles += $Obj; Write-Tee "[+] Added $U to primary profiles list" }
}
$ADUser = $null
}
Write-Tee "[i] $($ProfilePaths.Count) profile paths collected"
} catch { Get-Catch } #try/catch
$DestProfilePaths = Get-Childitem $Dst -Directory -Name
$PPCount = $ProfilePaths.Count
# Compare profiles existing and removing them if skip existing is enabled
if($DestProfilePaths.Count -ne $ProfilePaths.Count -and $SkipExisting)
{
Write-Tee "[i] Removing profiles from the profile path that already exist in the destination"
foreach($DP in $DestProfilePaths)
{
$ProfilePaths = $ProfilePaths | ?{ $_.User -ne $DP }
Write-Tee "[d] Removing $DP"
}
$PPRemoved = $PPCount - $ProfilePaths.Count
Write-Tee "[+] Removed $PPRemoved profile paths from the array"
}
# Remove excluded users from the array
$PPCount = $ProfilePaths.Count
if($ExcludeUsers)
{
Write-Tee "[i] Checking for excluded profiles"
foreach($E in $ExcludeUsers)
{
if($ProfilePaths.User -contains $E)
{
Write-Tee "[d] Removing $E"
$ProfilePaths = $ProfilePaths | ?{ $_.User -ne $E }
}
}
$PPRemoved = $PPCount - $ProfilePaths.Count
Write-Tee "[+] Removed $PPRemoved excluded profiles from the array"
}
# Cycle through each profile path to set permissions on the destination and copy the data
foreach($P in $ProfilePaths)
{
$ErrorActionPreference = "Stop"
$Username = $P.User
$S = $P.Source
$D = $P.Destination
Write-Tee "[=] ($X of $($ProfilePaths.Count)): $Username"
if($SetPermissions)
{
try
{
$CopyAll = $null
# If the destination folder doesn't exist create it so we can set the proper permissions and inheritance
if(!(Test-Path $D)){ $New = New-Item -Path $Dst -ItemType Directory -Name $Username; Write-Tee "[+] Created $Username's destination profile folder" }
Write-Tee "[i] Building ACLs for $Username"
# Create the initial ACL object using the existing path's permissions
$Acl = (Get-Item $S).GetAccessControl("Access")
if($Acl)
{
# Create the access rule with full control for the current user and set inheritance
$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Username,"FullControl","ContainerInherit,ObjectInherit","None","Allow")
# Apply the access rule to the ACL
$Acl.SetAccessRule($Ar)
# Establish the owner object and set the user as the owner
$Owner = New-Object System.Security.Principal.NTAccount("$NetBIOSDN\$Username")
$Acl.SetOwner($Owner)
# Apply the ACL to the destination folder
$SetAcl = Set-Acl -Path $D -AclObject $Acl
Write-Tee "[+] Applied the ACL to $Username's destination profile"
}
} catch { Get-Catch }
} else { $CopyAll = "/COPYALL /SECFIX" }
# If included folders are used create a list of all the folders not contained in the include folders list
$ExCount = $ExcludeFolders.Count
$ExDCount = 0
if($IncludeFolders)
{
try
{
Write-Tee "[i] Building the FOLDER exclusion list from the included folders"
$ExD = Get-ChildItem $S -Directory -Force | ?{ $IncludeFolders -notcontains $_.Name } | %{ $_.FullName }
if($ExD){ $ExD | %{ $ExcludeFolders.Add($_) | Out-Null } }
$ExDCount = $ExcludeFolders.Count - $ExCount
$ExCount = $ExcludeFolders.Count
Write-Tee "[+] $ExDCount folder exclusions created"
} catch { Get-Catch }
}
# If included appdata folders are used create a list of all the folders not contained in the included appdata folders list
if($IncludeAppData)
{
try
{
Write-Tee "[i] Updating the FOLDER exclusion list from the included AppData folders"
# Remove appdata from the excluded folders list in case it wasn't included
if($IncludeFolders -notcontains "AppData"){ $ExcludeFolders.Remove("AppData") }
foreach($F in $IncludeAppData.Keys)
{
$ExDA = Get-ChildItem "$S\AppData\$F" -Directory -Force | ?{ $IncludeAppData.$F -notcontains $_.Name } | %{ $_.FullName }
if($ExDA){ $ExDA | %{ $ExcludeFolders.Add($_) | Out-Null } }
}
$ExcludeFolders.Remove("$S\AppData")
$ExDCount = $ExcludeFolders.Count - $ExCount
Write-Tee "[+] $ExDCount folder exclusions created"
} catch { Get-Catch }
}
$ErrorActionPreference = "Continue"
# Establish a job file for the robocopy process to use in case the excludes are too long for the command
$JobFile = New-Item -ItemType File -Path "$env:TEMP" -Name "$($Name)_$($Username)_$DateFormat.RCJ"
if($ExcludeFiles)
{
# Add the full exclusion settings to the job file
@"
/XF
`t$($ExcludeFiles -join "`r`n`t")
"@ | Add-Content $JobFile -Force
}
if($ExcludeFolders)
{
# Add the full exclusion settings to the job file
@"
/XD
`t$($ExcludeFolders -join "`r`n`t")
"@ | Add-Content $JobFile -Force
}
Write-Tee "[+] Created the Robocopy job file $JobFile"
Write-Tee "[i] Beginning copy for $Username"
# Run the actual data copy and remove the job file afterwards
Start-Process robocopy -ArgumentList "$S $D $COPYALL /XJ /MIR /PURGE /ZB /R:0 /W:0 /NP /UNILOG+:$LogFile /JOB:$JobFile" -Wait
Write-Tee "[i] Removed the Robocopy job file"
$RemoveJobFile = Remove-Item $JobFile -Force
# Update the profile coutner
$X++
} #foreach($P in $ProfilePaths)
} #for ($i=0; $i -lt $Source.Count; $i++)
} #if ($PSCmdlet.ShouldProcess($Name))
Write-Tee @"
==============================================================================================
Onepath User Profile Migration End $(Get-Date)
-------------------------------------------------------------------------------------------
==============================================================================================
"@ -NoPrefix
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment