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!

Advertisements

Active Directory Migrations: Setting user attributes

In the ongoing series of AD Migrations….

These next couple are very specific to our environment but I’m putting them out here for posterity. Both use the Quest AD Powershell tools, which are very powerful tools when it comes to object manipulation in AD. I suggest you go download and install them immediately – Quest

This first one sets an extended attribute on the user in the source domain so that we know they’ve been migrated. It’s more for a key for reports and stuff, but can be good info to have.

The second combines a couple of things. The first thing it does is set the displayname and UPN for the migrated user to go with our new standards (oh, did I mention we’re changing the UPN but leaving the SAM alone and changing the displayname?)

It also turns on ActiveSync for the users if they need it. Made the most sense for our scripts to put it here.

AttribMig.ps1:


##filename attribmig.ps1
## Set transcript output
$Tranoutput="d:\migration\Outputs\" + $date + "SourceSIP.txt"
start-transcript -path $Tranoutput -append

##call include file
. .\params.ps1

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

##Set migrated attribute in source
## We're just setting an extended attribute using QAD and piping that out to the screen.

ForEach ($item in $import){
 write-host "Attribute being set for " $item.sourcename -foregroundcolor yellow
 set-qaduser -service $SourceDC -identity $item.sourcename -objectAttributes @{"extensionattribute4"="MigratedToCorp"}
}
stop-transcript

DisplayName.ps1


## Set our transcript and output file
$Tranoutput="d:\migration\Outputs\" + $date + "UPN.txt"
start-transcript -path $Tranoutput -append

##call include file
. .\params.ps1

##Create Sessions to Exchange 2010 in Target
$ExchSession=New-PSSession -ConfigurationName Microsoft.Exchange -connectionuri $ExchURI -credential $LocalCredentials

## Import our file and session

$import=import-csv $importfile
import-pssession $ExchSession|out-null

##Perform set user to fix displayname and upn
## Set the displayname and UPN based off of input file

foreach ($item in $Import){
 $UPN=$item.newupn+"@csgicorp.com"
 $AS=$item.ActiveSync.ToUpper()
 write-host "Setting UPN and AS for " $item.smtp -foregroundcolor yellow
 set-user -identity $item.smtp -displayname $item.displayname -userprincipalname $UPN

## Check if user is supposed to have ActiveSync Enabled and turn it on if they do

if ($AS -eq "YES"){set-casmailbox -identity $item.smtp -activesyncenabled $true}

}

## Clean up after ourselves and stop transcript
remove-pssession $ExchSession
stop-transcript

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

Active Directory Migrations: DNS Suffixes & Reboot

Part 2 of the continuing series of how to migrate users and computers in a domain migration.

This next script shouldn’t be necessary, if you’re managing your DNS suffixes correctly, using WINS, or don’t have a ton of DNS suffixes out there.

In this migration we’re turning off WINS as part of the migration. There literally dozens of possible DNS Suffixes users can have in their search order, and none of it is centrally managed. The preferred way to do this is to manager your suffix search order thru either GPO or DHCP options, but that wasn’t possible here. We also wanted to make sure that all of our user workstations were online prior to the migration and didn’t have any lingering issues that would prevent them from migrating, so wanted to be able to reboot the workstations first, before we did anything.

This script is actually doing a lot of stuff, and calls another script (pasted in below). Comments inline

# FileName: AppendToRegistry.ps1
# Purpose: To append a string value to a registry entry locally or remotely
# Hive valid values: "ClassesRoot","CurrentConfig","CurrentUser","DynData","LocalMachine","PerformanceData","Users"

## Set the date format and output files for our transcript
## Start transcript to log everything that happens on the screen.

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

## DNS Suffixes we want to ensure get added to workstations
## This doesn't overwrite existing suffixes, but checks to see if these exist. If they do, cool.
## If they don't, add them

$domains = @("DOM1.COM","DOM2.com","DOM3.com")

## Include file generated previously that all the computer names we need to hit.
## Set array variables for our ping script so that we don't have timeouts connecting to machines that aren't up
$RegistryFile = "D:\Migration\admtincludes\CompIncludes.csv"
$NoPing=@()
$YesPing=@()

## Import our CSV file and for each item in the list do a test-connection (i.e. ping)
## If a machine can't be pinged (pung?), add it to the array
## Output the array to the screen in bright colors so that we know we can't connect to them.
## We need to know this so that we don't try to migrate that user or desktop

$NoPing=import-csv -path $Registryfile|where-object {-not (Test-Connection -count 1 -computername $_.computer -erroraction silentlycontinue)}
clear-host
if ($noping){foreach ($item in $NoPing){write-host "The following computers are not responding:" $item.computer -backgroundcolor "red"}}

## Set a new variable and try to ping them all again.
## This sets the array that we use later for actually setting registry and reboot

$YesPing=import-csv -path $Registryfile|where-object {(Test-Connection -count 1 -computername $_.computer -erroraction silentlycontinue)}

##Using the array of up machines, set variables for computers and reg keys

$YesPing| ForEach-Object {
 $computer = $_.Computer
 $hive = "LocalMachine"
 $key = "System\CurrentcontrolSet\services\tcpip\parameters"
 $property = "SearchList"

##Call the other powershell file Get-RemoteRegistry.ps1 and pass variables to it.

[string]$currentValue=./Get-RemoteRegistry -hive $hive -key $key -name $property -computername $computer
 $modified = $false
 $newvalue = [string]::Empty

## If there is no value, set the value to the three default domains
## I.e. if there was no DNS Suffixes set

if ([string]::IsNullOrEmpty($currentValue))  {
$newvalue = $domains -join ','
$modified = $true}
 else {
## if there suffixes set
$domains | ForEach-Object {
## convert everything to lowercase
## set new values comma delimited, set modified variable to true
            if ($currentValue.ToLower().IndexOf($_) -lt 0) {
$newvalue = $newvalue + "," + $_
$modified = $true}
}

## Add current value and new value together, comma delimited
if ($modified){$newvalue = $currentValue + $newvalue}
 }
 # Check to see if the value was modified
# Call set-remoteregistry.ps1 and pass variables
 if ($modified)
 {
 ./Set-RemoteRegistry -key $key -name $property -value $newvalue -type string -computername $computer –force
 write-host "DNS Registry set on: " $computer
 }

## Reboot workstation
 restart-computer $computer -force
 write-host $Computer " is being rebooted"

}
stop-transcript

Get-RemoteRegistry.ps1:


[CmdletBinding(SupportsShouldProcess=$true)]
param
(
 [Parameter(Position=0, Mandatory=$false)]
 [System.String]
 $ComputerName = $Env:COMPUTERNAME,
 [Parameter(Position=1, Mandatory=$false)]
 [ValidateSet("ClassesRoot","CurrentConfig","CurrentUser","DynData","LocalMachine","PerformanceData","Users")]
 [System.String]
 $Hive = "LocalMachine",
 [Parameter(Position=2, Mandatory=$true, HelpMessage="Enter Registry key in format System\CurrentControlSet\Services")]
 [ValidateNotNullOrEmpty()]
 [System.String]
 $Key,
 [Parameter(Position=3, Mandatory=$true)]
 [ValidateNotNullOrEmpty()]
 [System.String]
 $Name
)

#Open remote registry
try
{
 $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $ComputerName)

}
catch
{

Write-Error "The computer $ComputerName is inaccessible. Please check computer name. Please ensure remote registry service is running and you have administrative access to $ComputerName."
 Return
}

$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $ComputerName)
$regkey = $reg.OpenSubkey($Key)
return $regkey.GetValue($Name)

Set-RemoteRegistry.ps1:


<#
 .SYNOPSIS
 Set-RemoteRegistry allows user to set any given registry key/value pair.

.DESCRIPTION
 Set-RemoteRegistry allows user to change registry on remote computer using remote registry access.

.PARAMETER ComputerName
 Computer name where registry change is desired. If not specified, defaults to computer where script is run.

.PARAMETER Hive
 Registry hive where the desired key exists. If no value is specified, LocalMachine is used as default value. Valid values are: ClassesRoot,CurrentConfig,CurrentUser,DynData,LocalMachine,PerformanceData and Users.

.PARAMETER Key
 Key where item value needs to be created/changed. Specify Key in the following format: System\CurrentControlSet\Services.

.PARAMETER Name
 Name of the item that needs to be created/changed.

 .PARAMETER Value
 Value of item that needs to be created/changed. Value must be of correct type (as specified by -Type).

 .PARAMETER Type
 Type of item being created/changed. Valid values for type are: String,ExpandString,Binary,DWord,MultiString and QWord.

 .PARAMETER Force
 Allows user to bypass confirmation prompts.

 .EXAMPLE
 PS C:\> .\Set-RemoteRegistry.ps1 -Key SYSTEM\CurrentControlSet\services\AudioSrv\Parameters -Name ServiceDllUnloadOnStop -Value 1 -Type DWord

.EXAMPLE
 PS C:\> .\Set-RemoteRegistry.ps1 -ComputerName ServerA -Key SYSTEM\CurrentControlSet\services\AudioSrv\Parameters -Name ServiceDllUnloadOnStop -Value 0 -Type DWord -Force

.INPUTS
 System.String

.OUTPUTS
 System.String

.NOTES
 Created and maintainted by Bhargav Shukla (MSFT). Please report errors through contact form at http://blogs.technet.com/b/bshukla/contact.aspx. Do not remove original author credits or reference.

.LINK
 http://blogs.technet.com/bshukla
#>
 [CmdletBinding(SupportsShouldProcess=$true)]
 param
 (
 [Parameter(Position=0, Mandatory=$false)]
 [System.String]
 $ComputerName = $Env:COMPUTERNAME,
 [Parameter(Position=1, Mandatory=$false)]
 [ValidateSet("ClassesRoot","CurrentConfig","CurrentUser","DynData","LocalMachine","PerformanceData","Users")]
 [System.String]
 $Hive = "LocalMachine",
 [Parameter(Position=2, Mandatory=$true, HelpMessage="Enter Registry key in format System\CurrentControlSet\Services")]
 [ValidateNotNullOrEmpty()]
 [System.String]
 $Key,
 [Parameter(Position=3, Mandatory=$true)]
 [ValidateNotNullOrEmpty()]
 [System.String]
 $Name,
 [Parameter(Position=4, Mandatory=$true)]
 [ValidateNotNullOrEmpty()]
 [System.String]
 $Value,
 [Parameter(Position=5, Mandatory=$true)]
 [ValidateSet("String","ExpandString","Binary","DWord","MultiString","QWord")]
 [System.String]
 $Type,
 [Parameter(Position=6, Mandatory=$false)]
 [Switch]
 $Force
 )

 If ($pscmdlet.ShouldProcess($ComputerName, "Open registry $Hive"))
 {
 #Open remote registry
 try
 {
 $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $ComputerName)

 }
 catch
 {

Write-Error "The computer $ComputerName is inaccessible. Please check computer name. Please ensure remote registry service is running and you have administrative access to $ComputerName."
 Return
 }
 }

If ($pscmdlet.ShouldProcess($ComputerName, "Check existense of $Key"))
 {
 #Open the targeted remote registry key/subkey as read/write
 $regKey = $reg.OpenSubKey($Key,$true)

 #Since trying to open a regkey doesn't error for non-existent key, let's sanity check
 #Create subkey if parent exists. If not, exit.
 If ($regkey -eq $null)
 {
 Write-Warning "Specified key $Key does not exist in $Hive."
 $Key -match ".*\x5C" | Out-Null
 $parentKey = $matches[0]
 $Key -match ".*\x5C(\w*\z)" | Out-Null
 $childKey = $matches[1]

try
 {
 $regtemp = $reg.OpenSubKey($parentKey,$true)
 }
 catch
 {
 Write-Error "$parentKey doesn't exist in $Hive or you don't have access to it. Exiting."
 Return
 }
 If ($regtemp -ne $null)
 {
 Write-Output "$parentKey exists. Creating $childKey in $parentKey."
 try
 {
 $regtemp.CreateSubKey($childKey) | Out-Null
 }
 catch
 {
 Write-Error "Could not create $childKey in $parentKey. You may not have permission. Exiting."
 Return
 }

$regKey = $reg.OpenSubKey($Key,$true)
 }
 else
 {
 Write-Error "$parentKey doesn't exist. Exiting."
 Return
 }
 }

 #Cleanup temp operations
 try
 {
 $regtemp.close()
 Remove-Variable $regtemp,$parentKey,$childKey
 }
 catch
 {
 #Nothing to do here. Just suppressing the error if $regtemp was null
 }
 }

 #If we got this far, we have the key, create or update values
 If ($Force)
 {
 If ($pscmdlet.ShouldProcess($ComputerName, "Create or change $Name's value to $Value in $Key. Since -Force is in use, no confirmation needed from user"))
 {
 $regKey.Setvalue("$Name", "$Value", "$Type")
 }
 }
 else
 {
 If ($pscmdlet.ShouldProcess($ComputerName, "Create or change $Name's value to $Value in $Key. No -Force specified, user will be asked for confirmation"))
 {
 $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes",""
 $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No",""
 $choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
 $caption = "Warning!"
 $message = "Value of $Name will be set to $Value. Current value `(If any`) will be replaced. Do you want to proceed?"
 Switch ($result = $Host.UI.PromptForChoice($caption,$message,$choices,0))
 {
 1
 {
 Return
 }
 0
 {
 $regKey.Setvalue("$Name", "$Value", "$Type")
 }
 }
 }
 }

 #Cleanup all variables
 try
 {
 $regKey.close()
 Remove-Variable $ComputerName,$Hive,$Key,$Name,$Value,$Force,$reg,$regKey,$yes,$no,$caption,$message,$result
 }
 catch
 {
 #Nothing to do here. Just suppressing the error if any variable is null
 }

Active Directory Migrations: Generating Files

So as you can probably tell from my last few posts I’ve been embroiled in an AD forest to forest migration for the past little while (and a little while longer). I have approximately 3000 users to migrate and am responsible for the mailbox move, user move, computer move, groups moves, Lync move, etc, etc. Basically I’ve got everything that makes a user a user.

I’ve scripted all of it and it took a ton of hours to get the scripts working right and to get all the little tweaks in there. It’s working pretty smoothly right now and I’m going to impart my wisdom in the next series of posts. My first bit of wisdom is to stay away from these types of migrations if you can help it 🙂 They’re a pain and there are so many moving parts that it’s difficult to get everything working just quite right.

But now that you’ve been told you just gotta do this, let me help where I can.

This first script is what I call my master script. I have the PM’s generating what I call the gold sheet for every migration that we do. In it is every field I need for every single script that I run. What this script here does is take the CSV file I get from them and generate all the various include files I need for the other scripts to run (some can’t take the massive initial file).

Modify this for your needs:

## Get the date in a format we want
## Set our transcript file and start the transcript so we have a log

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

## Set output variables. Declare them as arrays
$output=@()

$compOutput=@()
$ADMTCompOutput=@()
$BLCompOutput=@()

## Import our initial include file for all the users we're migrating today
## For each item in the include file, pull some of the variables and set them in the multi-column array
## Write them to an output array
## Set our various arrays to the output format

import-csv "d:\migration\admtincludes\Userincludes.csv"| ForEach-Object {
 $obj = New-Object PSObject | Select-Object SourceName, TargetRDN, TargetUPN
 $obj.SourceName = $_.sourceName
 $obj.TargetRDN = $_.TargetRDN
 $obj.TargetUPN = $_.TargetUPN
 $output += $obj

$CompObj=new-object PSObject|select-object Computer
 $CompObj.Computer=$_.computer
 $CompOutput+=$compobj

$ADMTCompObj="DOMAINNAME\"+$_.computer
 $ADMTCompOutput+=$ADMTcompobj

$ADMTCompObj2="DOMAINNAME\"+$_.computer
 $ADMTCompOutput2+=$ADMTcompobj2+";"
 $BLCompObj=new-object PSObject|select-object Computer
 $BLCompObj.Computer=$_.computer+".DOMAIN.COM"
 $BLCompOutput+=$BLcompobj

}

## Take all of our arrays we just populated and export them to a standard output filename
## all the other scripts look for these files in this format
## A couple are exported as CSV's, some are outputted as text files. Note the difference

write-host "...Generating Files..." -foregroundcolor red
$output|export-csv d:\migration\admtincludes\$date"ADMTMigration.csv" -notypeinformation
$CompOutput|export-csv d:\migration\admtincludes\CompIncludes.csv -notypeinformation
$ADMTCompOutput|out-file d:\migration\admtincludes\ADMTCompIncludes.txt
$ADMTCompOutput2|out-file d:\migration\admtincludes\ADMTCompMigIncludes.txt
$BLCompOutput|export-csv d:\migration\admtincludes\BitLockerCompIncludes.csv -notypeinformation

## Stop our screen transcript and read the log if necessary

stop-transcript

Windows 2008 Fine Grained Password Policies

If you’re like me you’ve always heard how 2008 introduced fine grained password policies (i.e the ability to have more than one password policy on the domain), but never actually had a need to do it. Typical needs for it would be if your admin accounts need more restrictive policies than your non-admin accounts. Which we should all really be doing!

Well now you’ve started looking into them and you’ve realized there’s no easy/great way to implement them in the GUI. You either need to do it directly in ADSIEdit or via Powershell. Both have their benefits, although Powershell is easier. Powershell also has some weird restrictions on what you can set from the command, so you have to go back and edit it after.

PSO’s (password policies) are also a little weird in that they don’t apply directly to OU’s, which is probably how you want to apply them. They only apply to user objects or to Global Groups. I’ll cover in another article how to auto-populate a group based on OU.

    1. Create a global group called whatever you want. For my example I used G_FineGrained_Elev.
    2. Because I want this to apply to the builtin Domain Admins accounts plus any other OU’s I happen to specify I also create other Global Groups specific to each OU I want (i.e. G_FG_Admins or whatever). Then I populate my 1st group with Domain Admins and this new group.
    3. Open Powershell
import-module ActiveDirectory
New-ADFineGrainedPasswordPolicy -name "Elev_PSO" -Precedence 200 -Description "The Elevated Accounts Password Policy" -DisplayName "Elevated Accounts PSO" -ComplexityEnabled $True -lockoutduration "0.00:30:00" -lockoutobservationwindow "0.00:30:00" -lockoutthreshold 3 -minpasswordage "7.00:00:00" -passwordhistorycount 4 -reversibleencryptionenabled $false -maxpasswordage "60.00:00:00" -minpasswordlength 15
Add-ADFineGrainedPasswordPolicySubject Elev_PSO -subjects 'G_FineGrained_Elev'

And that’s pretty much it.

  • -Precedence = you can define an order of application for PSO’s if a user gets more than 1. Lowest precedence wins
  • -ComplexityEnabled $True or $False = You want it complex, don’t you?
  • -lockoutduration “0.00:30:00” = How long to stay locked out. In the format of Day/Hour/Minute
  • -lockoutthreshold = How many attempts before locking it out
  • -minpasswordlength = the whole reason you’re doing this
  • -maxpasswordage = I’m calling this out on the odd chance you want to set the passwords to never expire. Not a good idea but there may be cause to do this. You need to set it to a value of (Never) and it can be edited in ADSIEdit or in the attribute editor on the PSO object.

To view your PSO after creation, open ADUC and click on View –> Advanced Features. Browse the System container and open the Password Settings Container. You can then open and edit the attributes on your objects.