Friday, February 8, 2013

PowerShell: Post-SysPrep scripts to Rename & Add Computer to Domain (using alternate credentials)


I tasked myself with the mission of using Microsoft's Windows 8 ADK to create a bootable Windows PE 4.0 UFD, and save a sysprepped workstation hard drive image (.wim) to the UFD. The UFD's WinPE boot sequence would have a PE-hooked batch file in which the user presses one button, walks away for 20 - 30 minutes and returns to a new PC which is on the Domain, in the correct OU, and has all its drivers loaded and GPO's applied.  The renaming and Domain adding portion of this process involved PowerShell's access to the win32_computersystem rename command and Add-Computer.

On my first attempt, I tried to rename the computer and add it to the domain during one boot. It just didn't work.  I had to reopen the pre-sysprep image and change the unattend.xml sysprep file's administrator login count from 1 to 2.  So the first boot checks then changes the computers name and places a RunOnce registry entry to kick off the second half of the process.  The second boot checks the computers name and if it is changed, grabs the IP address and starts a Add-Computer command associated with the OU of the corresponding IP subnet.

The PowerShell script during the first boot will create the computer name, add a regkey reboot hook, rename the computer, and restart it (it becomes passive during the second boot due to the computer name checks):

$ComputerName = ("Domain" + (gwmi win32_systemenclosure).SMBIOSAssetTag)

#A hook into the post-sysprep start script so it can be reran on next boot:

if ((gwmi win32_computersystem).name -ne $ComputerName){reg.exe add HKLM\Software\Microsoft\Windows\CurrentVersion\Runonce /v Restart /t REG_SZ /d "c:\scripts\start.bat" /f}

#Renaming the computer and restarting:

if ((gwmi win32_computersystem).name -ne $ComputerName){(gwmi win32_computersystem).rename($ComputerName);restart-computer}

#The second boot PowerShell script encrypts a domain user/pass, 
#determines the PC's IP subnet, then executes Add-Computer and 
#places the PC in the correct OU. Upon restart, kicks off GPUpdate at first Domain login.

#Saving the alternate Active Directory credentials:

$Username = "domain\delegateuser" 
$Password = convertto-securestring "secret" -asplaintext -force 
$Cred = new-object system.management.automation.pscredential($Username,$Password)

#Use ADSI to search for the computer object on the domain.  
#If one is found, add-computer with no OU is used.  If not then the script 
#finds the IP subnet and assigns the computers object to the appropriate OU


$searcher = [adsisearcher][adsi]""
$searcher.filter ="(cn=$ComputerName)"
$searchparm = $searcher.FindOne()
if (!($searchparm)){

#Checking the computer name then finding its IP subnet:

$ComputerName = ("Domain" + (gwmi win32_systemenclosure).SMBIOSAssetTag)
if ((gwmi win32_computersystem).name -ne $ComputerName){write "something went wrong with renaming the computer"|out-gr

idview;cmd /c pause}
$IP = [system.net.dns]::GetHostAddresses($env:computername)|?{$_.IPAddresstostring -like '192.168.*'}

#We've captured IP addresses to two octets and now find the 
#Active Directory Site via the third IP octet and perform the 
#add-computer command combined with the saved AD credentials and correct OU location:

if ($ip -like '192.168.0.*'){add-computer -domain MyDomain -Credential $cred -OUPath "OU=HQ,OU=WORKSTATIONS,DC=MyDomain,DC=COM"}
if ($ip -like '192.168.1.*'){add-computer -domain MyDomain -Credential $cred -OUPath "OU=Site1,OU=WORKSTATIONS,DC=MyDomain,DC=COM"}
if ($ip -like '192.168.2.*'){add-computer -domain MyDomain -Credential $cred -OUPath "OU=Site2,OU=WORKSTATIONS,DC=MyDomain,DC=COM"}

#closing the if statement concerning finding a computer object or not.
#The Else adds the computer to the domain and reattaching it to it's existing
#computer object

#Update 2/21/14
#I found it more efficient to take away the else statement and instead just create a catch-all:   add-computer -domain MyDomain -Credential $cred
#Another problem was re-imaging existing domain computers.  AD would try
#creating another GUID and cause GPO and other problems.  
#To fix this, I added:
start-process powershell.exe -credential $cred -argumentlist 'if (!(test-computersecurechannel)){test-computersecurechannel -repair}

#so if the computer is in the domain but not in one of the above mentioned subnets, it'll still be added to the domain.

}else{add-computer -domain MyDomain -Credential $cred}

#The computer is now a member of the Domain.  Next add a 
#Registry RunOnce key entry so the next boot will start a 
#forced application of GPUpdate relevant to it's OU:

reg.exe add HKLM\Software\Microsoft\Windows\CurrentVersion\Runonce /v GPUpdate /t REG_SZ /d "gpupdate /force" /f

Lastly, delete the scripts with passwords in them and restart the computer so it can change its network affiliation from WORKGROUP to the new Domain assignment:

reg.exe add HKLM\Software\Microsoft\Windows\CurrentVersion\Runonce /v DelPS1 /t REG_SZ /d "del c:\scripts*.ps1 /q" /f
restart-computer

This process works and has a positive side-effect: It stops the computer from joining the domain if the Computer Object already exists (which stops any sort of SID problems or unintended OU movements).

It's been suggested by a fellow Redditor that I should create a switch instead of the If statements breaking down the IP subnets.  I'll update this post after I've tested that portion.  Let me know if you have any questions, comments, or suggestions.

3 comments:

JDW said...

Greetings. Nice page!

I'll probably cross post this question to /r/powershell tonight.

We are deploying WES7 Thin Clients in our environment. Images are being pushed using HPDM. There is the capability to kick off a script after deployment via a command that is run by the local computer account (Administrator) on the deployed device.

I would prefer to keep this all in Powershell, as I'm attempting to learn it.

As I'm sure you know, using Add-Computer fails, if you specify an OU for the object to be created in. It works fine however if you do not specify the OU to use.

We could delete the object prior to imaging, or move it manually, but, that's not as slick!

I modified your script, and commented in my changes. It dies persistently at Line 19, with "The specified domain either does not exist or could not be contacted", meaning the credentials aren't valid to contact AD (I would presume it's the local machine account info).

Help?

Script as follows.

#PowerShell script encrypts a domain user/pass,
#determines the PC's IP subnet, then executes Add-Computer and
#places the PC in the correct OU. Upon restart, kicks off GPUpdate at first Domain login.

$ComputerName = ("$env:COMPUTERNAME")

#Saving the alternate Active Directory credentials:

$Username = "MyDomain\MyAuthUser"
$Password = convertto-securestring "MyAuthPassword" -asplaintext -force
$Cred = new-object system.management.automation.pscredential($Username,$Password)

#Use ADSI to search for the computer object on the domain.
#If one is found, add-computer with no OU is used. If not then the script
#finds the IP subnet and assigns the computers object to the appropriate OU

$searcher = [adsisearcher][adsi]""
$searcher.filter ="(cn=$ComputerName)"
$searchparm = $searcher.FindOne()

##This is a search of AD to find the current computer object, if found the script uses Add-Computer to join the computer the domain.
##Then the script uses uses the get-dn + PSBase.Move bits below to move the object to the "correct" OU.
##The else, joins the Add-Computer command to join the computer to the correct OU directly if it object didn't exist.

if (!($searchparm)){
add-computer -domain MyDomain -Credential $Cred
function get-dn ($SAMName)
{
$root = [ADSI]''
$searcher = new-object System.DirectoryServices.DirectorySearcher($root)
$searcher.filter = "(&(objectClass=Computer)(sAMAccountName= $env:ComputerName*))"
$user = $searcher.findall()

if ($user.count -gt 1)
{
$count = 0
foreach($i in $user)
{
write-host $count ": " $i.path
$count = $count + 1
}
$selection = Read-Host "Please select item: "
return $user[$selection].path
}
else
{
return $user[0].path
}

}

$Name = $args[0]
$path = get-dn $Name
"'" + $path + "'"
$from = [ADSI]"$path"
$to = [ADSI]"LDAP://OU=GoHere,OU=MyOU,DC=MyDomain,DC=DOT"
$from.PSBase.MoveTo($to,"cn="+$from.name)

}else{Add-Computer -DomainName "MyDomain" -Credential $Cred -OUPath ("OU=GoHere,OU=MyOU,DC=MyDomain,DC=DOT")}

#The computer is now a member of the Domain. Next add a
#Registry RunOnce key entry so the next boot will start a
#forced application of GPUpdate relevant to it's OU:

reg.exe add HKLM\Software\Microsoft\Windows\CurrentVersion\Runonce /v GPUpdate /t REG_SZ /d "gpupdate /force" /f

#Lastly, delete the scripts with passwords in them and restart the computer so it can change its network affiliation from WORKGROUP to the new Domain assignment

reg.exe add HKLM\Software\Microsoft\Windows\CurrentVersion\Runonce /v DelPS1 /t REG_SZ /d "del c:\Join.ps1 /q" /f

restart-computer

George said...

I tried that route of searching for the computer account using ADSI but found it doesn't work (it could be a trust thing since the user and computer are not both part of the domain so the DC won't give up the info). I ran out of testing time since we needed to push out all the replacements so I started with add-computer to an OU then if it fails I placed a generic add-computer without the OU. That way it works if the account exists or not. Now that I think about it, I could have tried the try{}catch{} method.

Sample:

if ($ip -like 'xxx.xx.xx.*'){add-computer -ea 0 -domain MyDomain -Credential $cred -OUPath "OU=HQ,OU=WORKSTATIONS,DC=MyDomain,DC=Company,DC=lcl"}

#then added afterwards
add-computer -ea 0 -domain DNR -Credential $cred

You could try:

try {if ($ip -like 'xxx.xx.xx.*'){add-computer -ea 0 -domain MyDomain -Credential $cred -OUPath "OU=HQ,OU=WORKSTATIONS,DC=MyDomain,DC=Company,DC=lcl"}}catch{add-computer -ea 0 -domain MyDomain -Credential $cred}

JDW said...

Hmmm, okay.

I'm not trying to search by name however, I'm trying to search AD for the computer object name being already in existence. Use add-computer to join it to the existing object, and then move it to the correct OU. Or if it doesn't exist, just join it to the correct spot.