Active Directory Migrations: New Hire Process

In our scenario we had to keep creating new hires in the legacy domain so that we could get sidHistory, until we can say that we’re done and all things have been migrated.

I did it all in one script, but it’s a little big. I like it cause I did some new (to me) stuff like menus and consolidation. It’s what I wanted to do with the rest of the migration scripts, but just didn’t quite work right. If you’re migrating 100 people at once, you want to verify that everything in step 1 has worked correctly before going on to step 2.

Below is the script. I’ll try to explain as we go, but most of it is just re-doing of things we’ve done before.

This script also assumes that you still have your daily sync scripts going from source to target domain and that you’re waiting at least a day between new hire creation in legacy and migration to target. If not, you’ll need to run the prepare-mailboxmove script manually to create the MEU

$warningpreference='silentlycontinue'

## Function to look in the source directory for all CSV files. Sort by most recent date and return the last 10.
## This way we can manipulate that file and use it for the rest of the script

function SourceFileMenu()

{
$sourceFiles = Get-ChildItem $sourcefolder\*.csv | Sort LastWriteTime -Descending | select name, lastwritetime -first 10
Write-Host ("=" * 80)
Write-Host "Available migration source files"
Write-Host ("-" * 80)

[int]$optionPrefix = 1

# Create menu list
foreach ($option in $sourceFiles)
{
if ($displayProperty -eq $null)
{
Write-Host ("{0,3}: {1,-40} {2}" -f $optionPrefix,$option.Name,$option.lastWriteTime)
}
else
{
Write-Host ("{0,3}: {1}" -f $optionPrefix,$option.$displayProperty)
}
$optionPrefix++
}

$maxOptions = $optionPrefix - 1

Write-Host ("-" * 80)
$response = 0
while($response -lt 1 -or $response -gt $sourcefiles.count)
{
[int]$response = Read-Host "Select a source file [1 - $maxOptions]"
}

$val = $null

if ($response -gt 0 -and $response -le $sourceFiles.Count)
{
$val = $sourceFiles[$response-1]
}

$pattern = [regex] "(.*)\.csv"

##save our selection into a variable and return it to the caller
$sourcename = $pattern.matches($val.Name) | foreach {$_.groups[1].value}

return $sourcename
}

##Pause function. Mainly so that the script will still work if you're using the ISE
Function Pause ($Message = "Press any key to continue . . . ") {
If ($psISE) {
# The "ReadKey" functionality is not supported in Windows PowerShell ISE.

$Shell = New-Object -ComObject "WScript.Shell"
$Button = $Shell.Popup("Click OK to continue.", 0, "Script Paused", 0)

Return
}
##ISE related stuff
Write-Host -NoNewline $Message

$Ignore =
16, # Shift (left or right)
17, # Ctrl (left or right)
18, # Alt (left or right)
20, # Caps lock
91, # Windows key (left)
92, # Windows key (right)
93, # Menu key
144, # Num lock
145, # Scroll lock
166, # Back
167, # Forward
168, # Refresh
169, # Stop
170, # Search
171, # Favorites
172, # Start/Home
173, # Mute
174, # Volume Down
175, # Volume Up
176, # Next Track
177, # Previous Track
178, # Stop Media
179, # Play
180, # Mail
181, # Select Media
182, # Application 1
183 # Application 2

While ($KeyInfo.VirtualKeyCode -Eq $Null -Or $Ignore -Contains $KeyInfo.VirtualKeyCode) {
$KeyInfo = $Host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown")
}

Write-Host
}

##Function for moving the objects to the correct OU
function CheckSite ($Site){
## Base OU where you have your users. This assumes you separate everything by region.
$BaseUserOU="domain.com/Users OU/"
switch ($Site){
"Site1 {
$OU="/CO"
$UserOU=$BaseUserOU + $OU
}
"Site2" {
$OU="/NC"
$UserOU=$BaseUserOU + $OU
}
"Site3 {
$ou="/MA"
$UserOU=$BaseUserOU + $OU
}

}

## Return the OU to the caller
return $userOU
}

## MainMenu builder
function mainMenu() {

Clear-Host;
#… Present the Menu Options
Write-Host “`n`New Hire Migration Process `n” -ForegroundColor Magenta
Write-Host “`t`tThese steps are in the order they need to be run.” -Fore Cyan
Write-Host “`t`tPlease run them sequentially and ensure each step finishes successfully” -Fore Cyan
Write-Host “`t`tbefore continuing on.`n” -Fore Cyan
Write-Host “`t`t`t1. Generate Include Files” -Fore Cyan
Write-Host “`t`t`t2. Enter Passwords” -Fore Cyan
write-host "`t`t`t3. Check for MEU" -fore cyan
Write-Host “`t`t`t4. ADMT” -Fore Cyan
Write-Host “`t`t`t5. Mailbox Move” -Fore Cyan
Write-Host "`t`t`t6. Set migrated attribute" -ForegroundColor Cyan
write-host "`t`t`t7. Set UPN and Displayname" -ForegroundColor Cyan
Write-Host "`t`t`t8. Disable Lync in source" -ForegroundColor Cyan
Write-Host "`t`t`t9. Enable Lync in target" -ForegroundColor Cyan
Write-Host "`t`t`t10. Set SIP in source -ForegroundColor Cyan
Write-Host "`t`t`t11. Move User to correct OU" -ForegroundColor Cyan
write-host "`t`t`t12. Clean up Files" -fore cyan
Write-Host “`t`t`tQ. for Quit`n” -Fore Cyan

}

##Variables. Add our snapin, decare paths, etc.
Add-PSSnapin Quest.ActiveRoles.ADManagement -erroraction SilentlyContinue
$Path="D:\newhire\includes"
$currfolder = Split-Path -parent $MyInvocation.MyCommand.Definition
$sourceFolder = $currfolder + "/Includes"

##Present the menu as a do/while, thus ensuring we only exit when we want
do {
## Call menu and store the keystroke into a var
mainMenu;
write-host "You last chose number " $input

$input = Read-Host "Enter a number for an option"

##Perform necessary option based on input
switch ($input) {
"1" { ## GEnerate include files for various scripts. renames the include file chosen to userincludes.csv and prepares the admt include
$IncludeFile=SourceFileMenu
$IFile=$includeFile + ".csv"
copy-item $sourcefolder\$IFile -destination $sourcefolder"\UserIncludes.csv"
$output=@()
import-csv $path\Userincludes.csv| ForEach-Object {
$obj = New-Object PSObject | Select-Object SourceName, TargetRDN, TargetUPN
$obj.SourceName = $_.sourceName
$obj.TargetRDN = $_.TargetRDN
$obj.TargetUPN = $_.TargetUPN
$output += $obj
}
write-host "...Generating Files..." -foregroundcolor red

$output|export-csv $path\"ADMTUserInclude.csv" -notypeinformation
Pause
}
"2" { ## Loads the parameter input. Prompts for credentials
write-host "Input target Creds" -backgroundcolor red -foregroundcolor white
$LocalCredentials=get-credential

write-host "Input source creds" -backgroundcolor red -foregroundcolor white
$RemoteCredentials=get-credential

$ImportFile="D:\newhire\includes\UserIncludes.csv"
$SourceDc="sourcedc1.sourcedomain.com"
$TargetDC="targetdc1.targetdomain.com"
$TargetDeliveryDomain="targetdomain.com"

$ExchURI="http://targetex1.targetdomain.com/powershell"
$LyncURI="https://registrarpool.targetdomain.com/ocspowershell"
$LyncFEURI="https://targetfe.targetdomain.com/ocspowershell"
$LyncFESourceURI="https://sourcefe.sourcedomain.com/ocspowershell"
$LyncURISource="https://sourcelyncdir.sourcedomain.com/ocspowershell"
$SourceDomain="sourcedomain.com"
$TargetDomain="targetdomain.com"
$RegistrarPool="registrarpool.targetdomain.com"
$TargetOU="Target/OU"
$import=import-csv $ImportFile

##Create Lync sessions for source and target
$LyncSessionSource=New-PSSession -connectionuri $LyncURISource -credential $RemoteCredentials
$LyncSession=New-PSSession -connectionuri $LyncURI -credential $LocalCredentials

write-host "Variables Loaded"
Pause
}
"3" { ##Check for existence of MEU. We can't do a migration if meu doesn't exist
foreach ($item in $Import){
write-host "Checking if user is enabled for UM in source " $item.smtp -foregroundcolor yellow

##check user attributes to see if they're enabled for UM. They shouldn't be, but just in case
$CheckUM=get-qaduser -service $SourceDC -identity $item.sourcename -includedProperties msExchUMRecipientDialPlanLink
$CheckDial=$checkUM.msExchUMRecipientDialPlanLink
if ($CheckDial){
write-host $item.smtp "is enabled for Unified Messaging. Please go disable and come back and rerun script" -fore yellow
$input="Q"
}
}
##Create Sessions
## Exchange sessions throw an error if it takes too long to get back to them, so we have to create and delete them as we go
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials

import-pssession $ExchSession|out-null

## Check if there's an MEU in target domain. If not, exit
foreach ($item in $import){
$aUser=get-mailuser $item.smtp
if (!$aUser){write-host "No MEU exists for " $item.smtp "please exit and run the Prepare script manually" -fore yellow
$input="Q"
}
if ($aUser){
##We've had some weird issues where the email address policy isn't applying immediately, so just in case let's go ahead and turn it off and back on for the users.
## We wait 30 seconds because otherwise the script goes too fast.
write-host "MEU Exists for " $item.smtp
write-host "Disabling EaP"
start-sleep -s 30
set-mailuser -identity $item.smtp -emailAddressPolicyenabled $false
write-host "enabling EAP"
start-sleep -s 30
set-mailuser -identity $item.smtp -emailaddresspolicyenabled $true
}
}
remove-pssession $Exchsession
pause
}
"4" { ## ADMT
## Can't be done via script because ADMT won't migrate the sid unless we have it installed on a DC or launch the GUI. Since that's the whole point we launch the GUI to do the ADMT
& C:\Windows\ADMT\migrator.msc
Pause
}
"5" { ## Mailbox Move
##Create Sessions
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials

import-pssession $ExchSession|out-null

foreach ($User in $import){
New-MoveRequest -Identity $user.smtp -domaincontroller $TargetDC -RemoteLegacy -RemoteGlobalCatalog $SourceDC -RemoteCredential $RemoteCredentials -TargetDeliveryDomain $TargetDeliveryDomain -baditemlimit 50 -warningaction silentlycontinue
}

## Rather than go check manually in exchange for move status, let's just do it all here. Wait 30 seconds between tries.
## Even with no data in the mailbox it still takes about 5 minutes to do the process
foreach ($User in $import){

do {
start-sleep -s 30
$a=get-moverequeststatistics -identity $user.smtp
clear-host
write-host "Getting move request statistics for " $user.smtp -fore yellow
write-host "`t`t`t " $a.status -fore cyan
}
until ($a.status -eq "Completed")
}
remove-pssession $ExchSession

Pause
}
"6" { ## Set attribute for reporting
foreach ($item in $Import){
write-host "Attribute being set for " $item.sourcename -foregroundcolor yellow
set-qaduser -service $SourceDC -identity $item.sourcename -objectAttributes @{"extensionattribute4"="MigratedToCorp"}
}
Pause
}
"7" { ## Set UPN and displayname, enable AS
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials

import-pssession $ExchSession|out-null
foreach ($item in $Import){
$UPN=$item.newupn+"@csgicorp.com"
$AS=$item.ActiveSync.ToUpper()
write-host "Setting UPN and AS for " $item.displayname -foregroundcolor yellow
set-user -identity $item.displayname -displayname $item.displayname -userprincipalname $UPN
if ($AS -eq "YES"){set-casmailbox -identity $item.smtp -activesyncenabled $true}
}
remove-pssession $ExchSession
Pause
}
"8" { ## Disable Lync in source
import-pssession $LyncSessionSource
foreach ($item in $import){
write-host "Disabling Lync for " $item.olddisplayname -foregroundcolor yellow
disable-csuser -identity $item.olddisplayname
}

remove-pssession $LyncSessionSource
## We add a delay otherwise it goes too fast
start-sleep 60
Pause
}
"9" { ## enable lync in target
import-pssession $LyncSession

foreach ($item in $import){
enable-csuser -identity $item.displayname -registrarpool $registrarpool -sipaddresstype EmailAddress
write-host "Enabling Lync for: " $item.displayname -foregroundcolor yellow
}
remove-pssession $LyncSession
pause
}
"10" { ##Set sip attribute in source
ForEach ($item in $import){
write-host "Adding source SIP for " $item.sourcename -foregroundcolor yellow
$NewSIP="sip:" + $item.smtp
set-qaduser -service $SourceDC -identity $item.sourcename -objectAttributes @{"msRTCSIP-PrimaryUserAddress"=$newsip}
}
pause
}
"11" { ##Move to OU
$date=get-date -format "yyyyMMdd"
foreach ($item in $import){
$return=CheckSite $item.sitecode
get-qaduser -service $targetDC -identity $item.displayname|move-qadobject -service $targetDC -identity {$_} -newparentcontainer $return
}
pause
}
"12" { ##Cleanup created files and move them to a completed dir
$date=get-date -format "yyyyMMdd"
move-item "$sourcefolder\ADMTUserInclude.csv" "$sourcefolder\Completed" -force
move-item "$sourcefolder\UserIncludes.csv" "$sourcefolder\Completed" -force
$ANewName=$date+"ADMTUserInclude.csv"
$UNewName=$date+"UserIncludes.csv"

if (!($(test-path "$sourcefolder\completed\$anewname"))){
rename-item "$sourcefolder\Completed\ADMTUserInclude.csv" $ANewName -force
rename-item "$sourcefolder\Completed\UserIncludes.csv" $UNewName -force
}
else {
remove-item "$sourcefolder\completed\$anewname"
remove-item "$sourcefolder\completed\$Unewname"
rename-item "$sourceFolder\Completed\ADMTUserInclude.csv" $ANewName -force
rename-item "$sourcefolder\Completed\UserIncludes.csv" $UNewName -force

}

pause
}

"Q" { ## quit and clean up our sessions if we missed any
get-pssession|remove-pssession
}
default {
Clear-Host;
Write-Host "Invalid input. Please enter a valid option. Press any key to continue.";
Pause
}
}

} until ($input.ToUpper() -eq "Q");
get-pssession|remove-pssession
Clear-Host;

Active Directory Migrations: Final Thoughts

I’ve now given you all my scripts I used for my AD migrations. They represent a huge amount of work on my part for writing, testing, compiling and refining them. Please use them wisely and if they help you out feel free to write a comment and let me know. I like to know I’m not just writing for myself.

Also keep in mind that you are not done. Not remotely. Now you have to go back and fix everything else you didn’t touch: DNS zones, DHCP, file share permissions, file servers, applications servers, applications, GPO’s, contacts, Sharepoint, etc, etc. Try to think of everything a user does on a daily basis and figure out if and how it needs to be migrated. You probably band-aided everything to get it working in the interim, but you still need to go back and FIX it.

To that end, what about your new hires? Hopefully you enfolded them in the migration process or at the end you’ll find out you have another 20-100 people who still need to be migrated. All because you didn’t define that process in the beginning. I know, because we ran out of time to do it on ours and had to do them all over again.

What about your remote users? Are you making them come into the office, mail their equipment in and be offline for days, or what? We did some hodge-podge process of creating a new local user account on remote PC’s and handholding them thru logging in with that, VPN in, and then migrate their computer and have them do it all over again so we could get the IP and finish the process. By the end it worked great, but 1 person could really only handle a couple of these remote users at a time.

And what are you going to do until all of the above is done? Do new hires need to be onboarded into their legacy domain or can they go directly into the new domain? Likely you’ve still got applications tied to the old domain that require sidHistory, group access or whatever, so your new users will need to come into the old and then be migrated into the new before they even start. Hopefully your onboarding process has that flexibility. (I’ll cover that powershell script in another post.)

Hopefully I’ve been of some help to you.

Active Directory Migrations: Mailbox Moves 2.0

If you’ve been following along with the process you’ll know I already covered mailbox moves. So you’re probably wondering what this post is about.

“Well, Jason,” you’re probably saying, “your process is nice enough and really saved me from having to write a lot of code, but what do I do for this users with huge mailboxes? Like, I have a few execs with 10GB mailboxes. I know I was never supposed to allow that, but, you know, they’re execs. And I have a tiny migration window.”

I’m glad you asked.

I found a nifty little switch on the mailbox move called “suspendwhenreadytocomplete”. Essentially what it does is kick off a mailbox move of a user into a temporary staging area (i.e you still have mailflow” and then when it gets to 95% it just stops. You could conceivably leave it there forever. Or days. Either one. The idea here is that that last 5% is the remaining mail items (and the changes) and then it flips the switch from MEU to mailbox and puts the mail attributes on the account in the target domain. Since I saw an average of about 20 minutes per 1GB of a mailbox during moves, I could work backward to see how much time it would take to move that mailbox and was able to kick it off early.

A couple huge caveats with this:

1. Only works from Exchange 2007 or 2010 to Exchange 2010.
2. You canNOT have Unified Messaging turned on for the user in the source domain when you start. Same as for a mailbox move. What this means is that the users won’t have voicemail until they’re actually migrated. There are ways around this using alternate names on your UM policy, but that didn’t work for us based on how we were changing things up.

Here’s the script. This helped me tremendously for my migrations:


##since this is a oneoff, specify the email address we're doing this to.
Param(
 [string]$smtp
)
##call include file

. .\params.ps1

##Create Sessions
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials
##import the session
import-pssession $ExchSession|out-null
## Do a move request for each mailbox
##note the last option -suspendwhenreadytocomplete

New-MoveRequest -Identity $smtp -domaincontroller $TargetDC -RemoteLegacy -RemoteGlobalCatalog $SourceDC -RemoteCredential $RemoteCredentials -TargetDeliveryDomain $TargetDeliveryDomain -baditemlimit 50 -suspendwhenreadytocomplete

##clean uup
remove-pssession $ExchSession

That’s it. Then when you’re ready to go on with your migration just go into the Exchange console to the move requests section and right-click and select “Complete Move Request”

Active Directory Migrations: A few little odds and ends

We had to do a few random clean up items to get machines to work correctly post-migration. Set a few registry keys, copy some files, enable Bitlocker keys, etc.

This first one copies a couple BAT files over to the client machine and then sets an auto run registry key. Since we were completely changing out the Lync environment and using a whole new SIP we had to force the client to re-do autodiscovery


##call include file
. .\params.ps1

##this was our list of relevant computer names.
$import=$computerlist

ForEach ($item in $import){
 $computer=$item.computer
 write-host "Setting Reg Key on " $Computer
##let's copy the bat files over. We use 2 just in case the first one misses it. Lync likes to start up as soon as you login, but must be closed for this setting to take effect
 copy-item d:\migration\scripts\nightof\lyncreset.bat -destination \\$computer\c$
 copy-item d:\migration\scripts\nightof\lyncreset2.bat -destination \\$computer\c$
##registry magic
 $HKLM = 2147483650
 $key = "Software\Microsoft\Windows\CurrentVersion\Runonce"
 $reg = [wmiclass]"\\$computer\root\default:StdRegprov"
 $value="c:\Lyncreset.bat"
 $reg.SetStringValue($HKLM, $key, $name, $value)
}

Lyncreset.bat


@echo off

rem Kill Lync
taskkill /IM communicator.exe /f

rem Delete the autodiscovery settings
reg delete HKCU\Software\Microsoft\Shared\UcClient /va /f

rem Delete the OAB's and the nickname cache for older clients
rmdir "%userprofile%\appdata\local\microsoft\outlook\offline address books" /s /q

rmdir "%userprofile%\local settings\application data\microsoft\outlook\offline address books" /s /q
ren "%userprofile%\AppData\Roaming\Microsoft\Outlook\*.nk2" *.nk2old

rem add another runonce so that we can do this all over again the next time we boot.
reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Runonce /ve /d "C:\LyncReset2.bat" /f

LyncReset2.bat


rem Check if the empty file exists. we don't want to turn this into a loop.
if not exist %temp%\EmptyFile.txt (
 taskkill /IM communicator.exe /f
 reg delete HKCU\Software\Microsoft\Shared\UcClient /va /f
 rmdir "%userprofile%\local settings\application data\microsoft\outlook\offline address books" /s /q
 rmdir "%userprofile%\appdata\local\microsoft\outlook\offline address books" /s /q
)

rem create the empty file
echo. 2>%temp%\EmptyFile.txt

Bitlocker was a fun one. No easy Powershell way to do this, so had to run some commands, scrape the output, then run some other commands.


#setting a variable so we don't get prompted for creds in our params file. None of these need credentials.
$Creds="NO"

#setting the variables for bitlocker. I tried to build a script to poll the client for actual HDD's but it didn't work consistently without enabling WinRM on all the machines, so had to hardcode these in there. Made the script a mess, but ran out of time for a clean way to do it.
$app="manage-bde.exe"
$DriveLetters = @("C","D","E")
##call include file
. .\params.ps1

##set up an array
$bad=@()

$import=import-csv "d:\migration\admtincludes\bitlockercompincludes1.csv"

## go thru each item in the import file, then go thru each drive in the drive array

foreach ($item in $import){

$computer=$item.computer
 foreach ($drive in $driveletters){
#set up our variables. Re-null out some and configure drives
$a=$null
 $b=$null
 $c=$null
 $key=$null
 $getparams=$null
 $putparams=$null
 $share=$drive+"$"
 $Bdrive=$drive+":"

## test the drive to see if it's good. most computers only have C$. If they have D$ or E$ this will run the commands against those drives
if ($(Test-path "\\$computer\$share")){
## Set up our param list for the manage-bde command
## i.e. manage-bde.exe -cn MYcomp -protectors -delete C: -type recoverypassword
## deletes the protectors on mycomp's c:
$Parameters =@("-cn","$computer","-protectors","-delete","$Bdrive","-type","recoverypassword")

## Run the command. Since we'red doing an executable we must do it this way
 & $App $Parameters

##basically the same as above, but we now want to add the recovery key back into the new AD
## i.e. manage-bde.exe -cn mycomp -protectors -add C: -recoverypassword
 $Parameters = @("-cn","$computer","-protectors","-add","$Bdrive","-recoverypassword")
 & $App $Parameters

##sometimes the above command doesn't work, so as a backup we want to tell it backup the recovery key to AD
##this requires getting the current key first
##i.e. manage-bde.exe -cn mycomp -protectors -get C:

$GetParams=@("-cn",$Computer,"-protectors","-get",$Bdrive)
##store the results into a variable
 $Result=& $app $GetParams

##browse the results for the line we're looking for
##We'll get a couple lines back one is the DRA and one is the actual ID for the drive that we need
 $a=$result|foreach-object {if($_ -match "ID") {$_}}
 if ($a){
##let's manipulate the data
##get the last line in the data that has ID in it
 $b=$a[-1].trim()
##convert it to a string and then split each space, creating an array of the results. I.e. "ID:" is [0] and the GUID is the [1]. "Password" is [2] and the password is [3]
 $c=$b.ToString().split(' ')

##set the key to the last item (i.e. the password)
 $Key=$c[-1]

##rewrite the params so that we can backup the key
##ie. manage-bde.exe -cn mycomp -protectors -adbackup C: -ID 11111-1111-....
 $PutParams=@("-cn",$Computer,"-protectors","-adbackup",$Bdrive,"-ID",$key)
 & $app $putparams
}
 }
##write out computers we couldn't connect to
ELSE {$BAD+=$Computer}
 }
}

$bad|sort|unique|out-file "d:\migration\admtincludes\BLOutput.txt"

Active Directory Migrations: Assorted Lync Stuff

Wow, so it’s been a whole 3 months since I’ve posted. Can you tell I got deep into actually doing migrations rather than talking about it? It took a little longer than I’d hoped – mainly due to scheduling – but we managed to finish the migrations. I personally migrated a little over 2,000 people. Due to our migration windows I never did more than 150 people at a time, but I was surely itching to.

These next couple of scripts are Lync related. Not too much involved in them, mainly creating the remote session and then running a quick command. Note that we installed the Lync module on our ADMT server, but for some reason some of the commands don’t work natively so we had to do some thru remote pssessions and some thru the module


##call include file.
## this is just in case we forgot to call it before
. .\params.ps1

##Create Sessions
##The usual pssession, just pointing to the Lync server in the source domain and using our source credentials
$LyncSessionSource=New-PSSession -connectionuri $LyncURISource -credential $RemoteCredentials

## Import our include file
$import=import-csv $importfile

##Source side disable-csuser
##Import the session, and then for each item in the include file, disable them for Lync in the source domain.
##Lync uses display name so we have to make sure that's in our include file
import-pssession $LyncSessionSource
foreach ($item in $import){
 write-host "Disabling Lync for " $item.olddisplayname -foregroundcolor yellow
 disable-csuser -identity $item.olddisplayname
}

## Let's clean up our session
remove-pssession $LyncSessionSource

Our next script is basically the same, but opposite. Let’s enable them for Lync in the target domain


##call include file
. .\params.ps1

##Create Sessions
$LyncSession=New-PSSession -connectionuri $LyncURI -credential $LocalCredentials

##Import
$import=import-csv $importfile

##Target side enable cs-user
##Import the session, then go thru the input file
##Since we changed the displayname of users as part of the process we had to account for that in the include file
## The rest of the commands are Lync specific. When you enable you have to specify registrar pool and address type
import-pssession $LyncSession

foreach ($item in $import){
 if ($item.smtp){
 enable-csuser -identity $item.displayname -registrarpool $registrarpool -sipaddresstype EmailAddress
 write-host "Enabling Lync for: " $item.displayname -foregroundcolor yellow
 }
}

##Let's clean up after ourselves
remove-pssession $LyncSession

This one is basically the same as the previous, but we have to set options for the users that can’t be set in the enable command. We’re setting the LineURI (i.e. telephone number) and turning on Enterprise Voice.


##call include file
. .\params.ps1

##Create Sessions
$LyncSession=New-PSSession -connectionuri $LyncURI -credential $LocalCredentials
$import=import-csv $importfile

##Target side enable cs-user
import-pssession $LyncSession

foreach ($item in $import){
 if ($item.LineURI){
 set-csuser -identity $item.displayname -EnterpriseVoiceEnabled $true -LineURI $item.LineURI
 write-host "Setting EV and LineURI for: " $item.displayname -foregroundcolor yellow
 }
}
## let's clean up, shall we?
remove-pssession $LyncSession

This last one grants the voice policy and the mobility policy, if applicable. This is using the Lync powershell modules on the ADMT server


$Importfile="\\omacsgiadmt01\d$\Migration\admtincludes\UserIncludes.csv"
$import=import-csv $importfile

import-module Lync

foreach ($item in $import){
 grant-csvoicepolicy -identity $item.displayname -policyname $item.CSVoicePolicy
 write-host "Granting CSVoice: " $item.displayname -foregroundcolor yellow
 $LM=$item.LyncMobility.ToUpper()
 if ($LM -eq "YES"){
 Grant-CsMobilityPolicy -identity $item.DisplayName -Policyname "CSGI Mobility"
 write-host "Granting CSMobility: " $item.displayname -foregroundcolor yellow
 }
}

That’s it for Lync!

Active Directory Migrations: User ADMT and Mailbox Move

I’m not going to go thru the screenshots of how to ADMT a user over. That’s documented well enough elsewhere (i.e. HERE).

All I’ll really say about that is that you NEVER want to migrate Exchange attributes of a user. Never, ever, ever. The process for migrating a user and a mailbox is simple enough:

  • Run prepare-moverequest.ps1 for each mailbox that needs to be created. It creates an MEU (mail enabled user) on the target side with all the exchange attributes you need. Essentially a fancy contact
  • ADMT the user (see above). Exclude: homeMDB, homeMTA, mailnickname, all the msExch*, all the msRTCSIP, proxyaddresses, targetaddress
  • Perform a move mailbox (see below). The move mailbox converts the source account into a MEU on the source domain. This is needed for mailflow.

## Get date, set our transcript file, etc.

$date=get-date -format "yyyyMMdd"
$Tranoutput="d:\migration\Outputs\" + $date + "Mailboxmove.txt"
start-transcript -path $Tranoutput -append

##call include file
## This ensures that the variables are loaded, altho if you followed the previous article they already should be
. .\params.ps1

## Create Remote Powershell session to the Exchange 2010 server
## Greate for making sure all your command can be run from one place
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials
import-pssession $ExchSession|out-null
## Import the include file
$import=import-csv $ImportFile
## Do a move request for each mailbox using all of our variables
## Targetdeliverydomain is important to ensure mailflow. It should be set to a 3rd SMTP domain that is only being used by the 2010 environment
## This then gets set on the MEU in the source side

foreach ($item in $import){
 New-MoveRequest -Identity $item.smtp -domaincontroller $TargetDC -RemoteLegacy -RemoteGlobalCatalog $SourceDC -RemoteCredential $RemoteCredentials -TargetDeliveryDomain $TargetDeliveryDomain -baditemlimit 50
}
## clean up after yourself and close your remote powershell session
remove-pssession $ExchSession

stop-transcript

The move mailbox ends up being the easiest part of this whole process. You can run powershell commands to check the status of the move request or just go into the Exchange console and check it there.

Active Directory Migrations: Parameters file & Ping

So at this point in our continuing series we’ve generated our include files for the various steps we’re going to run today and we’ve also added DNS suffixes to our user workstations and rebooted them. Now we want to make sure that they’ve actually come back from the reboots.

You could do a simple command/batch file and ping them all and that might be the easier way to go, but I prefer powershell and since we’re already in powershell, why wouldn’t want to do it that way?

This is also the point where we want to make sure that our Powershell session contains all the variables we need for the rest of our scripts. There’s a ton of stuff that’s going on depending on who you’re moving, from what source/target environment you’re going to, etc. So rather than re-init those variables for each script it makes more sense to load them once and save them everywhere.

The first thing we want to do is open Powershell.

We need to launch this script as . .\params.p1  The extra dot space in front of it allows us to use these variables elsewhere. It’s actually pretty powerful. I liken it to old days of building web pages where we’d use an include file for using the same footer everywhere.


## filename params.ps1

## Set variables
## In this script I want to check to see if the variable is already loaded in memory (hence the if statements)
## If they are, ignore. If they're not, load them in

if (!$ImportFile){$ImportFile="D:\Migration\admtincludes\UserIncludes.csv"}
if (!$SourceDC){$SourceDc="SourceDC1.domain1.com"}
if (!$TargetDC){$TargetDC="TargetDC1.domain2.com"}

## Get credentials for both sides for the scripts
if (!$LocalCredentials){
 write-host "Input Target Creds" -backgroundcolor red -foregroundcolor white
 $LocalCredentials=get-credential}
if (!$RemoteCredentials){
 write-host "Input Source creds" -backgroundcolor red -foregroundcolor white
 $RemoteCredentials=get-credential}

if (!$TargetDeliveryDomain){$TargetDeliveryDomain="domain2.com"}
if (!$ExchURI){$ExchURI="http://TargetExchange.domain2.com/powershell"}
if (!$LyncURI){$LyncURI="https://TargetLyncPool.domain2.com/ocspowershell"}
if (!$LyncFEURI){$LyncFEURI="https://TargetLyncFe.domain2.com/ocspowershell"}
if (!$LyncFEUSourceRI){$LyncFESourceURI="https://SourceDomainLyncFE.domain.com/ocspowershell"}
if (!$LyncURISource){$LyncURISource="https://SourcedomainLyncDir.domain.com/ocspowershell"}
if (!$SourceDomain){$SourceDomain="domain1.com"}
if (!$TargetDomain){$TargetDomain="domain2.com"}
if (!$RegistrarPool){$RegistrarPool="lyncregistrar.domain2.com"}
if (!$TargetOU){$TargetOU="OU1/OU2"}
if (!$MasterGroup){$MasterGroup="d:\migration\mastergrouplist.txt"}
if (!$date){$date=get-date -format "yyyyMMdd"}
if (!$ComputerList){$ComputerList = Import-Csv "D:\Migration\admtincludes\BitLockerCompIncludes.csv"}
if (!$PrecheckCompList){$PrecheckCompList = import-csv "d:\migration\admtincludes\compincludes.csv"}

## Make sure you have the Quest AD tools installed

if ( (Get-PSSnapin -Name Quest.ActiveRoles.ADManagement -ErrorAction SilentlyContinue) -eq $null )
{Add-PSSnapin Quest.ActiveRoles.ADManagement}

DNSPing.ps1


## File name dnsping.ps1
## Set date and transcript

$date=get-date -format "yyyyMMdd"
$Tranoutput="d:\migration\Outputs\" + $date + "Ping.txt"
start-transcript -path $Tranoutput

clear-host

## Similar to previous script, set array for machines you can't connect to
## Add them to the array

$NoPing=@()
$Computer=import-csv "d:\migration\admtincludes\CompIncludes.csv"
foreach ($comp in $computer){
 $ok=test-connection $comp.computer -count 1 -quiet
 write-host "Testing..." $comp.computer -foregroundcolor yellow
 if (-not($ok)){
 $noping+=$comp.computer
 }
}

## For each machine you can't ping, output to the host in a nice bright color that you couldn't connect to it
## In my case, of running migrations in the middle of the night, if I can't connect I remove them from the migration list and continue on.

foreach ($item in $NoPing){write-host "The following computers are not responding:" $item -backgroundcolor "red"}
if (!$NoPing){write-host "All computers responding" -foregroundcolor yellow}

stop-transcript