Installing, Configuring, Securing and Using MDT Webservices – Part 3

by Jeremy Saunders on June 28, 2019

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.

Move OU with Webservice and Credentials - Staging OU

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.

Move OU with Webservice and Credentials - Final OU

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!

Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: