In Part 1 we walked through the installation and configuration of Deployment Webservices.
In Part 2 we walked through securing the Webservice.
In this part I will demonstrate how to use the Webservice via a PowerShell script to securely move a computer object during the operating system deployment (OSD) task sequence using Microsoft Deployment Toolkit (MDT).
To achieve the end result we need to:
- Create some deployment share rules in MDT (CustomSettings.ini)
- Add two “Run PowerShell Script” tasks to the Task Sequence
- Download and place the PowerShell Script into the deployment share Scripts folder
Create some deployment share rules in MDT (CustomSettings.ini)
We add and set 3 new properties for WebServiceURL, StagingOU and FinalOU where…
- WebServiceURL is the URL to the Webservice
- StagingOU is typically an OU with blocked GPO inheritance that is a safe place for the computer object to be placed into during the build process.
- FinalOU is the OU you want the object to end up in towards the end of the build process.
[Settings] Priority=Default Properties=WebServiceURL,StagingOU,FinalOU [Default] WebServiceURL=http://mdt01.mydemothatrocks.com/MDTWS MachineObjectOU=OU=Staging,DC=mydemothatrocks,DC=com StagingOU=OU=Staging,DC=mydemothatrocks,DC=com FinalOU=OU=Gold Images,OU=Session Hosts,OU=Citrix,DC=mydemothatrocks,DC=com
Add two “Run PowerShell Script” tasks to the Task Sequence
The first one will move the computer object to the Staging OU:
- The task is added just before the OS is applied during the WinPE stage.
- It runs the script from the %SCRIPTROOT% folder with the following arguments:
- -MDT. This instructs the script to use the MDT build credentials.
- -TargetOU. This provides the OU the computer object will be moved to. In this case the Staging OU as specified as per the rules.
The second one will move the computer object to the Final OU:
- The task is added towards the end as one of the final tasks in your sequence.
- It runs the same script from the same location with the same arguments, with the only difference being the variable assigned to the TargetOU argument, as this is where we specify the the Final OU as per the rules.
Download the MoveOUwithWSandCredentials.ps1 (816 downloads) script and place it in the deployment share Scripts folder
<# This script will move a computer object from its current location in Active Directory to the supplied location using Maik Koster's Deployment Webservice and passing credentials. This is needed for two reasons: 1) WinPE does not support the use of ADSI 2) The MDT task sequence does not run as a Domain User with permissions to easily achieve this task. Whilst in MDT you can run a script as a different user, I wanted one that was more flexible in its approach so that I could pass it existing variables or derive them directly from the Task Sequence variables. The Active Directory Net Framework classes are NOT supported on WinPE. .Net uses ADSI to query AD. [adsi] and [adsisearcher] are built into PowerShell V2 and later - [adsisearcher] - is a builtin type accelerator for -> System.DirectoryServices.DirectorySearcher - [adsi] - is a builtin type accelerator for -> System.DirectoryServices.DirectoryEntry Note that whilst Johan Arwidmark has blogged about adding ADSI support to WinPE, it is not supported by Microsoft. As a Consultant I don't want to build an unsupported environment for my customers. So I choose to use Maik Koster's Deployment Webservice instead. Syntax Examples: - To move the computer object to the build (staging) OU MoveOUwithWSandCredentials.ps1 -MDT -TargetOU:"%MachineObjectOU%" - To move the computer object to the build (staging) OU and save the current OU location to the FinalOU Task Sequence Variable MoveOUwithWSandCredentials.ps1 -MDT -TargetOU:"%MachineObjectOU%" -SaveCurrentOU - To move the computer object to the final OU MoveOUwithWSandCredentials.ps1 -MDT -TargetOU:"OU=Citrix,OU=Servers" Where... -TargetOU = New OU Location relative to the Domain DN -SaveCurrentOU = Instructs the script to write the current OU location to the FinalOU Task Sequence Variable. -DomainAdminDomain = Domain in FQDN format preferrably -DomainAdmin = Username that has permissions to move computer objects in AD. You would typically use the Domain join account here. -DomainAdminPassword = Password -Decode = Decode (Optional). This is needed if you pass the DomainAdminDomain, DomainAdmin and DomainAdminPassword variables from MDT. -MDT = Get the DomainAdminDomain DomainAdmin DomainAdminPassword variables and automatically decrypt them. Note that the "Microsoft.SMS.TSEnvironment" object is only available during the Task Sequences. You cannot use this parameter to test this script outside of MDT. -WebServiceURL = The URL for Maik Koster's Deployment Webservice IMPORTANT: In large and/or multi-domain environments the DomainAdminDomain variable MUST be set in CustomSettings.ini in FQDN format and not as the NetBIOS domain name. The MoveComputerToOU function will return either true or false. So you may need to review the Deployment Webservices Debug and Trace logs to track down the real issue should it return false. It is almost always a permission issue. Script Name: MoveOUwithWSandCredentials.ps1 Release 1.5 Written by Jeremy Saunders (Jeremy@jhouseconsulting.com) 21st November 2016 Modified by Jeremy Saunders (Jeremy@jhouseconsulting.com) 9th October 2019 #> #------------------------------------------------------------- param( [String]$ComputerName=${env:computername}, [Switch]$MDT, [String]$DomainAdminDomain="", [String]$DomainAdmin="", [String]$DomainAdminPassword="", [Switch]$Decode, [String]$TargetOU="", [Switch]$SaveCurrentOU, [String]$WebServiceURL="" ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' #------------------------------------------------------------- Function IsTaskSequence([switch] $verbose) { # This code was taken from a discussion on the CodePlex PowerShell App Deployment Toolkit site. # It was posted by mmashwani. # The only issue with this function is that it writes terminating errors to the transcription log, which looks # rather ugly. So this function will eventually be updated so that it checks for the existenace of the relevant # snapins (Get-PSSnapin) and assemblies ([AppDomain]::CurrentDomain.GetAssemblies()) that have been loaded. # References: # - https://richardspowershellblog.wordpress.com/2007/09/30/assemblies-loaded-in-powershell/ # - http://learningpcs.blogspot.com.au/2012/06/powershell-v2-test-if-assembly-is.html Try { [__ComObject]$SMSTSEnvironment = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction 'SilentlyContinue' -ErrorVariable SMSTSEnvironmentErr } Catch { # The Microsoft.SMS.TSEnvironment assembly is not present. } If ($SMSTSEnvironmentErr) { Write-Verbose "Unable to load ComObject [Microsoft.SMS.TSEnvironment]. Therefore, script is not currently running from an MDT or SCCM Task Sequence." -verbose:$verbose Return $false } ElseIf ($null -ne $SMSTSEnvironment) { Write-Verbose "Successfully loaded ComObject [Microsoft.SMS.TSEnvironment]. Therefore, script is currently running from an MDT or SCCM Task Sequence." -verbose:$verbose Return $true } } #------------------------------------------------------------- $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptPath = $(&$ScriptPath) $ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString()) $Logfile = "$ScriptName-$($datestampforfilename).log" $logPath = "$($env:windir)\Temp" If (IsTaskSequence) { $tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment $logPath = $tsenv.Value("LogPath") } try { # The Microsoft.BDD.TaskSequencePSHost.exe (TSHOST) does not support # transcription, so we wrap this in a try catch to prevent errors. Start-Transcript "$logPath\$Logfile" } catch { Write-Verbose "This host does not support transcription" } #------------------------------------------------------------- If ($MDT) { If (IsTaskSequence) { Write-Verbose "Reading Task Sequence variables" -verbose $Decode = $True $DomainAdminDomain = $tsenv.Value("DomainAdminDomain") $DomainAdmin = $tsenv.Value("DomainAdmin") $DomainAdminPassword = $tsenv.Value("DomainAdminPassword") $ComputerName = $tsenv.Value("OSDComputerName") $WebServiceURL = $tsenv.Value("WebServiceURL") } Else { Write-Verbose "This script is not running from a task sequence" -verbose } } $ExitCode = 0 If ($DomainAdminDomain -ne "" -AND $DomainAdmin -ne "" -AND $DomainAdminPassword -ne "") { If ($Decode) { # Decode the base64 encoded blob using UTF-8 $DomainAdminDomain = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdminDomain)) $DomainAdmin = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdmin)) $DomainAdminPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdminPassword)) } $DomainAdminPasswordSecureString = ConvertTo-SecureString –String $DomainAdminPassword –AsPlainText -Force $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist "$DomainAdminDomain\$DomainAdmin",$DomainAdminPasswordSecureString If ($cred -is [system.management.automation.psCredential]) { $IsValidCredentials = $True write-verbose "Using credentials:" -verbose write-verbose "- DomainName: $DomainAdminDomain" -verbose write-verbose "- UserName: $DomainAdmin" -verbose # Move computer to required OU If ([String]::IsNullOrEmpty($TargetOU) -eq $false) { # Derive the Domain distinguishedName from the FQDN of the Domain we are joining $ValidDomainDN = $False If ($DomainAdminDomain -Like "*.*") { #$DomainDistinguishedName = "DC=" + (($DomainAdminDomain -split "\." | Select-Object ) -join ",DC=") $DomainDistinguishedName = 'DC=' + $DomainAdminDomain.Replace('.',',DC=') $ValidDomainDN = $True } # PowerShell will replace comma's with spaces on the command line, # so here we just replace the spaces with commas. $TargetOU = $TargetOU -Replace " OU=", ",OU=" $TargetOU = $TargetOU -Replace " DC=", ",DC=" If ($TargetOU -NotLike "*,DC=*") { If ($ValidDomainDN) { $TargetOU = "$TargetOU,$DomainDistinguishedName" } } $URI = $WebServiceURL + "/ad.asmx?WSDL" if ($URI.Contains("https")) { # If you’re running a self-signed certificate or don't have the full CA chain # available on the localhost where this script is run from, we need to tell # .NET to ignore this SSL indiscretion. [System.Net.ServicePointManager]::ServerCertificateValidationCallback={$true} } try { $wsCall = New-WebServiceProxy -Uri $URI -Credential $cred -ErrorAction:Stop } catch [System.Net.WebException] { $wsCall = $null Write-Host "ERROR: Unable to connect to SOAP server" Write-Host $_ } catch { $wsCall = $null Write-Host "ERROR: Unexpected error" Write-Host $_ Write-Host $_.Exception.Message Write-Host $_.Exception.GetType().FullName } If ($wsCall -ne $null) { #$wsCall | Get-Member $ForestInfo = $wsCall.GetForest() Write-verbose "The current Forest is: $($ForestInfo.Name)" -verbose Write-verbose "The Domains in the Forest are: $([string]::Join(",",($ForestInfo.Domains)))" -verbose Write-verbose "The AD Site for this computer based on its IP Address is: $($wsCall.GetADSite())" -verbose $isComputer = $False Try { $isComputer = $wsCall.DoesComputerExist($ComputerName) } Catch { $Host.UI.WriteErrorLine("ERROR: $($_)") } $MoveCompleted = $False If ($isComputer) { Write-verbose "There is a matching computer object in Active Directory" -verbose Write-verbose "- The distinguishedName is: $($wsCall.GetComputerAttribute($ComputerName,"distinguishedName"))" -verbose Write-verbose "- The description is: $($wsCall.GetComputerDescription($ComputerName))" -verbose $CurrentOU = ($wsCall.GetComputerParentPath($ComputerName) -split("//"))[1] Write-verbose "- Parent OU: $CurrentOU" -verbose If ($SaveCurrentOU) { If ($MDT) { If (IsTaskSequence) { Write-Verbose "Writing the current parent OU back to the FinalOU Task Sequence variable" -verbose $tsenv.Value("FinalOU") = $CurrentOU } Else { Write-Verbose "This script is not running from a task sequence" -verbose } } } If ($TargetOU -ne $CurrentOU) { write-verbose "Moving to target OU: $TargetOU" -verbose Try { $MoveCompleted = $wsCall.MoveComputerToOU($ComputerName,$TargetOU) } Catch { $Host.UI.WriteErrorLine("ERROR: $($_)") $ExitCode = 1 } If ($MoveCompleted) { Write-verbose "Successfully moved the computer object." -verbose } Else { $Host.UI.WriteErrorLine("ERROR: Unable to move the computer object. Verify that the account has the required permissions.") $ExitCode = 1 } } Else { write-verbose "No need to move the computer as it is already in the target OU" -verbose } } Else { Write-verbose "There is no matching computer object in Active Directory" -verbose } } Else { $Host.UI.WriteErrorLine("ERROR: Unable to connect to SOAP server.") $ExitCode = 1 } } Else { $Host.UI.WriteErrorLine("ERROR: TargetOU is a required parameter.") $ExitCode = 1 } } Else { $Host.UI.WriteErrorLine("ERROR: Invalid credentials.") $ExitCode = 1 } } Else { $Host.UI.WriteErrorLine("ERROR: Missing credentials.") $ExitCode = 1 } # We must set the $ExitCode variable when using Set-StrictMode in PowerShell # scripts used in MDT/SCCM Task Sequences. It's a known bug. If ($ExitCode -eq 0) { Write-Verbose "Completed with an exit code of $ExitCode" } Else { $Host.UI.WriteErrorLine("ERROR: Completed with an exit code of $ExitCode") } try { # The Microsoft.BDD.TaskSequencePSHost.exe (TSHOST) does not support # transcription, so we wrap this in a try catch to prevent errors. Stop-Transcript } catch { Write-Verbose "This host does not support transcription" } Exit $ExitCode
I hope this 3 part series has been helpful and shows the great value of this webservice.
Enjoy!