SCCM

Configure Network Settings Error: Enter the DNS server IP address.

Problem:

You’re imaging a machine on a network segment not serviced by a DHCP server.

On the first screen of the Task Sequence Wizard – the one that says ‘Welcome to the Task Sequence Wizard‘ – you click the ‘Configure Network Settings‘ button and enter the ‘Configure Static IP Network Settings‘ screen.

After entering everything in correctly and clicking ‘OK‘, you receive the following error:

EnterTheDNSServerIPAddress

Configure Network Settings Error: Enter the DNS server IP address.

Troubleshooting:

  • The network configuration you entered is correct.
  • You reboot, try again, same error.
  • Maybe you try another machine and it works
  • Maybe you try another machine and it doesn’t work.

Potential Cause 1:

In my limited experience, this seems happens when you’re trying to set IP details on an adapter in a VMware VM that’s not connected.  We saw this on a few VMware VM’s that were recently stood up and had adapters but not connected to any networks.  Oops.

Potential Cause 2:

This is more of a delay/timing issue: While configuring a handful of VMware VM’s, I hit my stride with entering the IP information and found that when I entered the DNS IP address, tabbed down to the domain suffix and pressed [Enter] I’d get this error.  If I tried again another 1-2 seconds after getting the error it would accept.

Because I was running into this so often I spent a little time trying to test this and anecdotally it seems that when you make modifications to the domain suffix field, something happens behind the scenes that causes this delay.  I found that if I excluded the domain suffix field I didn’t have this problem at all.  I also learned that if I entered the domain suffix first then entered the DNS IP address I didn’t have this problem either.

It’s not clear what’s going on but I just need to ‘Trust But Verify’ the VM configuration and slow down when entering the details.  (^_^)

Good Providence to you!

Advertisements

MDT Tutorial Part 11: Troubleshooting Part 6: Unable to mount the WIM, so the update process cannot continue

Living Table of Contents

 

What These Guides Are:
A guide to help give you some insight into the troubleshooting process in general.

What These Guides Are Not:
A guide to fix all issues you’re going to encounter.

We’re going to role-play a bunch of scenarios and try to work through them.  Remember in math where you had to show your work?  Well, what follows is like that which is why this post is [more than] a [little] lengthy.

Unable to mount the WIM, so the update process cannot continue

When updating your deployment share it fails almost immediately with error Unable to mount the WIM, so the update process cannot continue.

This slideshow requires JavaScript.

Error Text:


=== Making sure the deployment share has the latest x86 tools ===

=== Processing LiteTouchPE (x86) boot image ===

Building requested boot image profile.
Determining if any changes have been made in the boot image configuration.
No existing boot image profile found for platform x86 so a new image will be created.
Calculating hashes for requested content.
Changes have been made, boot image will be updated.
Windows PE WIM C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\x86\en-us\winpe.wim will be used.
Unable to mount the WIM, so the update process cannot continue.

=== Completed processing platform x86 ===

=== Making sure the deployment share has the latest x64 tools ===

=== Processing LiteTouchPE (x64) boot image ===

Building requested boot image profile.
Determining if any changes have been made in the boot image configuration.
No existing boot image profile found for platform x64 so a new image will be created.
Calculating hashes for requested content.
Changes have been made, boot image will be updated.
Windows PE WIM C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\en-us\winpe.wim will be used.
Unable to mount the WIM, so the update process cannot continue.

=== Completed processing platform x64 ===

=== Processing complete ===

You might try a variety of things like:

  • Use DISM to get-wiminfo to confirm the WIMs are OK
  • Bounce the MDT machine
  • Perform a repair installation of the ADK
  • Use DISM to mount the WIM manually and you get an error 577

We talked about this in Part 2 : Did you remember to patch the ADK?  🙂

Copypasta Closing

Hopefully these examples will help give you an idea of the overall troubleshooting process.  Most of the time the problems you’ll encounter will be caused by a typso, order of operations or a ‘known issue’ that requires a specific process to be followed.

As you make changes to your environment, here’s what I recommend:

  • Be diligent about keeping a change log so you can easily backtrack
  • Backup your CS.INI or Bootstrap.ini before you make any changes
  • Backup your ts.xml or unattend.xml (in DeploymentShare\Control\TaskSequenceID) before you make any changes
  • Introduce small changes at time with set checkpoints in between and set milestones markers where you backup core files (e.g cs.ini bootstrap.ini ts.xml unattend.xml etc) to help minimize frustration troubleshooting.

And if when you do run into some turbulence, upload relevant logs (at least smsts.log but be prepared to submit others depending on the issue) to a file sharing service like OneDrive, post on TechNet then give a shout to your resources on Twitter.

Good Providence to you!

Deployment Error Invalid DeploymentType value "" specified.  The deployment will not proceed.

MDT Tutorial Part 11: Troubleshooting Part 5: Invalid DeploymentType value “” specified. The deployment will not proceed.

Living Table of Contents

 

What These Guides Are:
A guide to help give you some insight into the troubleshooting process in general.

What These Guides Are Not:
A guide to fix all issues you’re going to encounter.

We’re going to role-play a bunch of scenarios and try to work through them.  Remember in math where you had to show your work?  Well, what follows is like that which is why this post is [more than] a [little] lengthy.

Invalid DeploymentType value “” specified. The deployment will not proceed.

You enabled the Windows Updates steps in the Build & Capture Task Sequence so you can have a fully patched Windows 10 v1511 WIM.

Troubleshoot-014.PNG

And you also updated your CustomSettings.ini so that your Build & Capture VM would set all the properties/variables but not start immediately.


[Settings]
Priority=MACAddress,GetAbbrModel,Build,Default
Properties=OfficeCode,AbbrModel

;vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
; BEGIN MACADDRESS SECTION
; This is my Windows 10 v1511 build & capture VM
[00:15:5D:13:79:01]
SkipTaskSequence=NO
TaskSequenceID=BC151164ENT
SkipComputerName=NO
OSDComputerName=BnC-#UCase(Right(Replace(Replace("0000000%SERIALNUMBER%"," ","",1,-1,1),"-","",1,-1,1),8))#
SkipDomainMembership=NO
JoinWorkgroup=BnC-WrkGrp
SkipUserData=NO
SkipComputerBackup=NO
ComputerBackupLocation=NETWORK
BackupDir=Captures\%OSDComputerName%
BackupFile=%OSDComputerName%_%TaskSequenceID%_#year(date) & "-" & month(date) & "-" & day(date) & "_" & Hour(Now()) & Minute(Now())#.wim
SkipProductKey=NO
SkipLocaleSelection=NO
SkipTimeZone=NO
SkipAdminPassword=NO
SkipCapture=NO
DoCapture=YES
SkipBitLocker=NO
SkipSummary=NO

; END MACADDRESS SECTION
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

;vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
; BEGIN GETABBRMODEL SECTION
; Lets get the abbreviated model
[GetAbbrModel]
UserExit=jgp_GetAbbrModel.vbs
AbbrModel=#GetAbbrModel#
; END GETABBRMODEL SECTION
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

;vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
; BEGIN BUILD SECTION
; Set things here for use below
[Build]
OSDComputerName=%OfficeCode%-%AbbrModel%-#UCase(Right(Replace(Replace("0000000%SERIALNUMBER%"," ","",1,-1,1),"-","",1,-1,1),8))#
; END GETABBRMODEL SECTION
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

;vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
; BEGIN DEFAULT SECTION
[Default]
OSInstall=Y
; Skip Screen: Task Sequence
SkipTaskSequence=NO
; Skip Screen: Computer Details
SkipComputerName=NO
; Skip Screen: Computer Details
SkipDomainMembership=NO
; Skip Screen: Move Data and Settings & User Data (Restore)
SkipUserData=NO
; Skip Screen: Computer Backup
SkipComputerBackup=NO
; Skip Screen: Product Key
SkipProductKey=NO
; Skip Screen: Locale & Time
SkipLocaleSelection=NO
KeyboardLocale=en-US
UserLocale=en-US
UILanguage=en-US
; Skip Screen: Locale & Time
SkipTimeZone=NO
; https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx
TimeZoneName=Eastern Standard Time
; Skip Screen: Administrator Password
SkipAdminPassword=NO
; Skip Screen: Capture Image
SkipCapture=NO
; Skip Screen: BitLocker
SkipBitLocker=NO
; Skip Screen: Ready to begin
SkipSummary=NO
; Skip Screen: R
SkipFinalSummary=NO
SLShare=%DeployRoot%\TSLogs
SLShareDynamicLogging=%DeployRoot%\TSLogs\%OSDComputerName%
EventService=http://ITF1MDT01:9800
; END DEFAULTSECTION
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Having learned your lesson from last time, you test the updated CustomSettings.ini & spent a few minutes verifying the output in the console.  Feeling confident, you boot into WinPE and when the Wizard I displays, verify all the defaults are set correctly.

This slideshow requires JavaScript.

You click begin and almost immediately the process halts with an obscure error:

Deployment Error Invalid DeploymentType value

Deployment Error Invalid DeploymentType value “” specified.  The deployment will not proceed.

You’d be forgiven for thinking you’ve made a mistake and I’d be willing to bet you’d figure this out but that road wouldn’t be fun.

Fortunately Michael Niehaus himself confirmed this is a bug (SRC1, SRC2) and there are two options to fixing/working around the bug:

  1. Easiest: Update your CS.INI with SkipProductKey=YES.
  2. Hardest (and arguably unsupported?):
    1. Open DeployWiz_ProductKeyVista.vbs in the Scripts directory
    2. Find: ​if oProperties("DeploymentType") = "UPGRADE" then
    3. Replace: If Property("DeploymentType") = "UPGRADE" then
    4. Save the file and try again
Note: There appears to be a plausible explanation of what’s happening here if you’re interested.

Since you rarely need to prompt for a Product Key – especially considering you can stick it in the unattend.xml or you go down the easy path and now the image kicks off.

Troubleshoot-028.PNG

Copypasta Closing

Hopefully these examples will help give you an idea of the overall troubleshooting process.  Most of the time the problems you’ll encounter will be caused by a typso, order of operations or a ‘known issue’ that requires a specific process to be followed.

As you make changes to your environment, here’s what I recommend:

  • Be diligent about keeping a change log so you can easily backtrack
  • Backup your CS.INI or Bootstrap.ini before you make any changes
  • Backup your ts.xml or unattend.xml (in DeploymentShare\Control\TaskSequenceID) before you make any changes
  • Introduce small changes at time with set checkpoints in between and set milestones markers where you backup core files (e.g cs.ini bootstrap.ini ts.xml unattend.xml etc) to help minimize frustration troubleshooting.

And if when you do run into some turbulence, upload relevant logs (at least smsts.log but be prepared to submit others depending on the issue) to a file sharing service like OneDrive, post on TechNet then give a shout to your resources on Twitter.

Good Providence to you!

MDT Tutorial Part 11: Troubleshooting Part 4: Task Sequence Variable is Being Overwritten

Living Table of Contents

 

What These Guides Are:
A guide to help give you some insight into the troubleshooting process in general.

What These Guides Are Not:
A guide to fix all issues you’re going to encounter.

We’re going to role-play a bunch of scenarios and try to work through them.  Remember in math where you had to show your work?  Well, what follows is like that which is why this post is [more than] a [little] lengthy.

Task Sequence Variable is Being Overwritten – The Case of the Incorrectly Named Log Folder

After [finally] fixing your computer naming issue, you decide to take a look at the live BDD.log just to keep an eye on things.  You hop into the TSLogs directory & find 2 directories that have the same time stamp:

Troubleshoot-008

According to your CS.INI the “real” log directory should be named after the machine so how did that other one get created?  You open the BDD.log in BLD-HPV-22404895 and it’s practically empty, but you do see that the property is being set to the other directory.

Troubleshoot-009.PNG

There are some clues in the BDD that lead you to above that point to a smoking gun.

You open the BDD.log in the other directory & scroll to the top & see a familiar set of lines: The same ones you see when testing the CustomSettings.ini manually:

Troubleshoot-011

While the machine is imaging you press F8 to launch a console, fire up CMTrace, open the smsts.log (in %temp%\smstslog), scroll to the top & search downwards for ‘dyn’:

Troubleshoot-012.PNG

Ok, you’re piecing it together:

  1. Initially it’s good, points to the proper location
  2. Then at some point it changes
  3. In the first, short, BDD.log you see ‘Task Sequence’ mentioned
  4. In the second, long, BDD.log you see lines that look like it’s processing the CustomSettings.ini, something that also happens during the Task Sequence,
  5. The smsts.log shows a step that sets a variable
  6. SLShareDynamicLogging isn’t a property (or variable) that is ‘last write wins’, so it can’t be a case of a rogue CustomSettings.ini

So maybe it’s the Task Sequence?

Troubleshoot-013

Bingo was his name.

 

Copypasta Closing

Hopefully these examples will help give you an idea of the overall troubleshooting process.  Most of the time the problems you’ll encounter will be caused by a typso, order of operations or a ‘known issue’ that requires a specific process to be followed.

As you make changes to your environment, here’s what I recommend:

  • Be diligent about keeping a change log so you can easily backtrack
  • Backup your CS.INI or Bootstrap.ini before you make any changes
  • Backup your ts.xml or unattend.xml (in DeploymentShare\Control\TaskSequenceID) before you make any changes
  • Introduce small changes at time with set checkpoints in between and set milestones markers where you backup core files (e.g cs.ini bootstrap.ini ts.xml unattend.xml etc) to help minimize frustration troubleshooting.

And if when you do run into some turbulence, upload relevant logs (at least smsts.log but be prepared to submit others depending on the issue) to a file sharing service like OneDrive, post on TechNet then give a shout to your resources on Twitter.

Good Providence to you!

Troubleshooting the Lenovo ThinInstaller Installation

During OSD, install the Lenovo ThinInstaller, and have been doing this for quite some time.  Whenever a new build is released, we duplicate the source files and swapping out the older installer for the new.  This process worked well for several versions:

  • v1.2.0014
  • v1.2.0015
  • v1.2.0017
  • v1.2.0018

In early 2017 we updated our Lenovo ThinInstaller to v1.2.0020 and noticed that the ThinInstaller installation executable would run seemingly forever, but during OSD and in real Windows when installing manually.  We reverted back to 1.2.0018 until we could do more testing to narrow the scope of the problem.  Shortly afterwards, 1.2.0022 was released so we tried again thinking maybe it was a build-specific bug, but it too failed.

We spent a decent amount of time trying to hunt this down and confirmed it wasn’t:

  • unique to the Task Sequence
  • a model specific issue
  • an upgrade scenario (e.g.: only 1.2.0014 to 1.2.0022)
  • software that was already installed and causing a conflict
  • security software on the machine.

Again, we could reproduce at will by removing the new installer and restoring the old.

A while later we learned that Lenovo had made some changes to the ThinInstaller package, and you can read about that here:
https://thinkdeploy.blogspot.com/2017/03/changes-required-to-use-thin-installer.html

To ensure the highest rate of installation success, we took the shotgun approach:


# Unregister the DLL if it's found - update the path accordingly
if(Test-Path -Path "$envProgramFilesX86\ThinInstaller\Lenovo.PnPSignedDriverEx.dll" -PathType Leaf)
    {
        If(Test-Path -Path "$env:windir\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe" -PathType Leaf)
            {
                Start-Process -FilePath "$env:windir\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe" -ArgumentList "/u `"$envProgramFilesX86\ThinInstaller\Lenovo.PnPSignedDriverEx.dll`"" -PassThru -Wait
            }
        elseif(Test-Path -Path "$env:windir\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe" -PathType Leaf)
            {
                Start-Process -FilePath "$env:windir\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe" -ArgumentList "/u `"$envProgramFilesX86\ThinInstaller\Lenovo.PnPSignedDriverEx.dll`"" -PassThru -Wait
            }
    }

# Remove the existing 'installation'
Rename-Item -Path "${env:ProgramFiles(x86)}\ThinInstaller" -NewName "ThinInstaller_$(Get-Date -Format 'yyyy-MM-dd_hhmmss')"; Start-Sleep -Seconds 3

# Perform the 'install'
Start-Process -FilePath 'thin_installer_VERSION.exe' -ArgumentList "/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG=Path\To\Install.log" -Wait

# Drop in the ThinInstaller.exe.configuration
Copy-Item -Path $(Join-Path $PSScriptRoot 'ThinInstaller.exe.configuration') -Destination "${env:ProgramFiles(x86)}\ThinInstaller\ThinInstaller.exe.configuration" -Force<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

 

Some time around ThinInstaller v1.2.0029, the Lenovo.PnPSignedDriverEx.dll as well as the .cmd files used to register/unregister said DLL, quietly disappeared suggesting they either found a better way to do what they were doing or baked that process into their installer or … ?  That said, the above code is probably no longer needed.  However, since ThinInstaller is an application that’s generally available to all Lenovo machines in Software Center, we left the code in there on the off chance someone upgrades or uninstalls/reinstalls ThinInstaller on a machine with an older build.

If you find the ThinInstaller is hanging during upgrade scenarios, try the above to see if that helps.

If you find the ThinInstaller is hanging during a fresh installation, try generating a log and review the log file to see where it’s getting hung up then troubleshoot from there.

Good Providence To You!

Title: Windows Setup Body: Windows could not parse or process unattend answer file [C:windowsPantherunattend.xml] for pass [specialize]. The answer file is invalid.

MDT Tutorial Part 11: Troubleshooting Part 3: Windows could not parse or process unattend answer file [C:\windows\Panther\unattend.xml] for pass [specialize].  The answer file is invalid.

Living Table of Contents

 

What These Guides Are:
A guide to help give you some insight into the troubleshooting process in general.

What These Guides Are Not:
A guide to fix all issues you’re going to encounter.

We’re going to role-play a bunch of scenarios and try to work through them.  Remember in math where you had to show your work?  Well, what follows is like that which is why this post is [more than] a [little] lengthy.

Windows could not parse or process unattend answer file [C:\windows\Panther\unattend.xml] for pass [specialize].  The answer file is invalid.

Your last victory is short lived as the same error message appears and this time unattend.xml looks fine:

Troubleshoot-010.PNG

Stumped, you might search for ‘Microsoft-Windows-Shell-Setup’ which might lead you here:
https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup

As you review each section carefully the issue becomes clear: The computer name is more than 15 characters.

Copypasta Closing

Hopefully these examples will help give you an idea of the overall troubleshooting process.  Most of the time the problems you’ll encounter will be caused by a typso, order of operations or a ‘known issue’ that requires a specific process to be followed.

As you make changes to your environment, here’s what I recommend:

  • Be diligent about keeping a change log so you can easily backtrack
  • Backup your CS.INI or Bootstrap.ini before you make any changes
  • Backup your ts.xml or unattend.xml (in DeploymentShare\Control\TaskSequenceID) before you make any changes
  • Introduce small changes at time with set checkpoints in between and set milestones markers where you backup core files (e.g cs.ini bootstrap.ini ts.xml unattend.xml etc) to help minimize frustration troubleshooting.

And if when you do run into some turbulence, upload relevant logs (at least smsts.log but be prepared to submit others depending on the issue) to a file sharing service like OneDrive, post on TechNet then give a shout to your resources on Twitter.

Good Providence to you!

Getting Real Lenovo Model Details

Our computer naming convention includes a portion of the model number to make it easier to identify trends and those with old, or new, assets.  Coming from a Dell shop where we did something similar, I was disappointed to learn that Lenovo machines didn’t populate the Model details the same way.  So instead of seeing something like ThinkPad W541, we were seeing something very cryptic: 20EFCTO1WW

Get your Decoder Ring

Thinking something was up with the built-in scripts or logic I ran the below on a Lenovo machine which confirmed it was what it was:


wmic path win32_computersystem get model

Model
20EFCTO1WW

For a while we kept a matrix of sorts that we’d feed into our CustomSettings.ini to ensure machines were named correctly.  We expected this pain as models were phased out and new models came in, but it was also very frustrating as the details would change mid-stream for the same model.  This led us to studying the Lenovo Product Specifications Reference or PSREF.

Not being keen on this, I learned somewhere (unsure of the actual source) that Lenovo stashes the bits we were after in Win32_ComputerSystemProduct under Version


wmic path win32_computersystemproduct get version

Version
ThinkPad W541

Once confirmed across a few machines, I went right to work.

UserExit Script: GetAbbrModel.vbs

This is a modified version of the script we use in production but the result is the same: It gets the human-readable format of the model, trims the parts we don’t want and returns an abbreviated version of the model.  So a ThinkPad W541 ends up being returned to MDT/SCCM as W54.  You can modify to suit, like creating a new property/variable called RealModel and assigning the script output to that or overwrite the existing Model property via the script itself.

The script works on 99% of the machines in our environment but it does occasionally fail:

  • some unexpected data is in there: sometimes it’s really bizzare or mirrors Model in Win32_ComputerSystem or Name in Win32_ComputerSystemProduct.
  • most of the time it’s because the field is blank/null/empty and we typically see this on machines that were serviced, specifically a board replacement, and tech didn’t run the utility to set the bits in the BIOS.  Accidents happen.
  • it’s running on very hardware that should have been retired 🙂

Good Providence to you as you adjust it to suit your needs!


' //***************************************************************************
' //
' // Solution:Get Model Abbreviation for Microsoft Deployment
' // File: jgp_GetAbbrModel.vbs
' //
' // Purpose: Gets & sets the correct model abbreviation for use in computer name and other custom configurations
' //
' // ***** End Header *****
' //***************************************************************************

'//----------------------------------------------------------------------------
'//
'// Global constant and variable declarations
'//
'//----------------------------------------------------------------------------
Option Explicit

'//----------------------------------------------------------------------------
'// End declarations
'//----------------------------------------------------------------------------

'//----------------------------------------------------------------------------
'// Main routine
'//----------------------------------------------------------------------------
Function UserExit(sType, sWhen, sDetail, bSkip)
	UserExit = Success
End Function

Function GetAbbrModel()
	on error goto 0
	Dim sMake : sMake = oEnvironment.Item("Make")
	Dim sModel : sModel = oEnvironment.Item("Model")
	Dim sAbbrModel : sAbbrModel = "UNK"

	Select Case UCase(sMake)

		Case UCase("Dell")

			If InStr(1,sModel,"OptiPlex ",1) > 0 Then
				sAbbrModel = Left(Replace(sModel,"ptiPlex ","",1,-1,1),3)
			elseif InStr(1,sModel,"Latitude ",1) > 0 Then
				sAbbrModel = Left(Replace(sModel,"Latitude ","",1,-1,1),3)
			elseif InStr(1,sModel,"XPS",1) > 0 Then
				sAbbrModel = Left(Replace(sModel,"PS","",1,-1,1),3)
			end if

		Case UCase("Lenovo")
			Dim oCSP
			For Each oCSP in GetObject("winmgmts:").ExecQuery("SELECT Version,Name FROM Win32_ComputerSystemProduct")
				Dim sLenovoModel : sLenovoModel = oCSP.Version
				Dim sLenovoProductType : sLenovoProductType = oCSP.Name
				exit for
			Next

			If InStr(1,sLenovoModel,"ThinkCentre ",1) > 0 Then
				sAbbrModel = Left(Replace(sLenovoModel,"ThinkCentre ","",1,-1,1),3)
			elseif InStr(1,sLenovoModel,"ThinkStation ",1) > 0 Then
					sAbbrModel = Left(Replace(sLenovoModel,"ThinkStation ","",1,-1,1),3)
			elseif InStr(1,sLenovoModel,"ThinkPad ",1) > 0 Then
				if Instr(1,sLenovoModel,"Carbon",1) > 0 Then
					If InStr(1,sLenovoModel,"Carbon 4th",1) > 0 Then
						sAbbrModel = Left(Replace(Replace(Replace(sLenovoModel,"ThinkPad ","",1,-1,1),"arbon 4th","")," ",""),3)
					elseif InStr(1,sLenovoModel,"Carbon 3rd",1) > 0 Then
						sAbbrModel = Left(Replace(Replace(Replace(sLenovoModel,"ThinkPad ","",1,-1,1),"arbon 3rd","")," ",""),3)
					elseif InStr(1,sLenovoModel,"Carbon 2nd",1) > 0 Then
						sAbbrModel = Left(Replace(Replace(Replace(sLenovoModel,"ThinkPad ","",1,-1,1),"arbon 2nd","")," ",""),3)
					elseif InStr(1,sLenovoModel,"Carbon",1) > 0 Then
						sAbbrModel = Left(Replace(Replace(Replace(sLenovoModel,"ThinkPad ","",1,-1,1),"arbon","")," ",""),3)
					end if
				else
					sAbbrModel = Left(Replace(sLenovoModel,"ThinkPad ","",1,-1,1),3)
				end if
			else
				' Alternatively you could build & maintain (yuck) a table of product types
				Select Case UCase(Left(sLenovoProductType,4))
					Case UCase("5032")
						sAbbrModel = "M81"

					case UCase("20EF")
						sAbbrModel = "W54"
				End Select
			end if

		Case UCase("innotek GmbH")
			sAbbrModel = UCase(Left(sMake,1) & Mid(sMake,8,1) & Right(sMake,1))

		Case UCase("VMware, Inc.")
			sAbbrModel = UCase(Left(sMake,3))

		Case UCase("Microsoft Corporation")
			sAbbrModel = "HPV"

	End Select
	GetAbbrModel = sAbbrModel
End Function

Preparing for Windows 10: Move Computers into to Win 10 OUs

One thing that has annoyed me about MDT and SCCM was that there wasn’t a built-in method to move machines into the proper OU.  As such it required creating something from scratch and I opted for something that didn’t require dependencies, like the ActiveDirectory module.

This isn’t the best way and this isn’t the only way – it’s just a way; one of many in fact.

Please note that this is NOT the ideal way to handle any operations that require credentials!  Keeping credentials in a script is bad practice as anyone snooping around could happen upon them and create some problems.  Instead, you should rely on web services to do this and Maik Koster has put together an excellent little care package to help get you started.

Move-ComputerToOU Prerequisites

My script has a few prerequisites:

  • The current AD site
  • A [local] Domain Controller to use (recommended)
  • The current OU of the machine to be moved
  • The new OU to move the machine into

It’s important to know that this script does not rely on the ActiveDirectory module.
One of my [many] quirks is to try to keep everything self-contained where it makes sense to do so, and I liked the idea of not having to rely on some installed component, an EXE and so on.  But to be honest, web services is the way to go for this.

Getting the Current AD Site

Better see this post for that.

Getting a Local Domain Controller

Better see this post for that.

Getting the Current OU

Better see this post for that.

Getting / Setting the New OU

If this is being executed as part of an OSD, yank those details from the MachineObjectOU Task Sequence variable via something like:

Function Get-TaskSequenceEnvironmentVariable
    {
        Param([Parameter(Mandatory=$true,Position=0)]$VarName)
        Try { return (New-Object -COMObject Microsoft.SMS.TSEnvironment).Value($VarName) }
        Catch { throw $_ }
    }
$MachineObjectOU = Get-TaskSequenceEnvironmentVariable MachineObjectOU

Otherwise just feed it the new OU via the parameter

Process Explanation

We first have to find existing object in AD


# This is the machine we want to move
$ADComputer = $env:COMPUTERNAME

# Create Directory Entry with authentication
$de = New-Object System.DirectoryServices.DirectoryEntry($LDAPPath,"$domain\$user",$pass)

# Create Directory Searcher
$ds = New-Object System.DirectoryServices.DirectorySearcher($de)

# Fiter for the machine in question
$ds.Filter = "(&(ObjectCategory=computer)(samaccountname=$ADComputerName$))"

# Optionally, set other search parameters
#$ds.SearchRoot = $de
#$ds.PageSize = 1000
#$ds.Filter = $strFilter
#$ds.SearchScope = "Subtree"
#$colPropList = "distinguishedName", "Name", "samaccountname"
#foreach ($Property in $colPropList) { $ds.PropertiesToLoad.Add($Property) | Out-Null }

# Execute the find operation
$res = $ds.FindAll()

 

Then we bind to the existing computer object


# If there's an existing asset in AD with that sam account name, it should be the first - and only - item in the array.
# So we bind to the existing computer object in AD
$CurrentComputerObject = New-Object System.DirectoryServices.DirectoryEntry($res[0].path,"$domain\$user",$pass)

# Extract the current OU
$CurrentOU = $CurrentComputerObject.Path.Substring(8+$SiteDC.Length+3+$ADComputerName.Length+1)

 

From there setup the destination OU


# This Here we set the new OU location
$DestinationOU = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$SiteDC/$NewOU","$domain\$user",$pass)

 

And finally move the machine from the old/current OU to the new/destination OU


# And now we move the asset to that new OU
$Result = $CurrentComputerObject.PSBase.MoveTo($DestinationOU)

Move-ComputerToProperOU.ps1

So this is a shortened version of the script I’m using in production.  All you need to do is fill in the blanks and test it in your environment.


# There's a separate function to get the local domain controller
$SiteDC = Get-SiteDomainController
# Or you can hard code the local domain controller
$SiteDC = 'localdc.domain.fqdn'

# Build LDAP String
# I //think// there were maybe two cases whre this din't work
$LocalRootDomainNamingContext = ([ADSI]"LDAP://$SiteDC/RootDSE").rootDomainNamingContext
# So I added logic to trap that and pull what I needed from SiteDC
#$LocalRootDomainNamingContext = $SiteDC.Substring($SiteDC.IndexOf('.'))
#$LocalRootDomainNamingContext = ($LocalRootDomainNamingContext.Replace('.',',DC=')).Substring(1)

# Building the LDAP string
$LDAPPath = 'LDAP://' + $SiteDC + "/" + $LocalRootDomainNamingContext

# Set my domian for authentication
$domain = 'mydomain'

# Set the user for authentication
$user = 'myjoindomainaccount'

# Set the password for authentication
$pass = 'my sekret leet-o-1337 creds'

# This is the machine I want to find & move into the proper OU.
$ADComputerName = $env:COMPUTERNAME

# This is the new OU to move the above machine into.
$NewOU = 'OU=Laptops,OU=Win10,OU=Office,OU=CorpWorkstations,DC=domain,DC=fqdn'

# Create Directory Entry with authentication
$de = New-Object System.DirectoryServices.DirectoryEntry($LDAPPath,"$domain\$user",$pass)

# Create Directory Searcher
$ds = New-Object System.DirectoryServices.DirectorySearcher($de)

# Fiter for the machine in question
$ds.Filter = "(&(ObjectCategory=computer)(samaccountname=$ADComputerName$))"

# Optionally, set other search parameters
#$ds.SearchRoot = $de
#$ds.PageSize = 1000
#$ds.Filter = $strFilter
#$ds.SearchScope = "Subtree"
#$colPropList = "distinguishedName", "Name", "samaccountname"
#foreach ($Property in $colPropList) { $ds.PropertiesToLoad.Add($Property) | Out-Null }

# Execute the find operation
$res = $ds.FindAll()

# If there's an existing asset in AD with that sam account name, it should be the first - and only - item in the array.
# So we bind to the existing computer object in AD
$CurrentComputerObject = New-Object System.DirectoryServices.DirectoryEntry($res[0].path,"$domain\$user",$pass)

# Extract the current OU
$CurrentOU = $CurrentComputerObject.Path.Substring(8+$SiteDC.Length+3+$ADComputerName.Length+1)

# This Here we set the new OU location
$DestinationOU = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$SiteDC/$NewOU","$domain\$user",$pass)

# And now we move the asset to that new OU
$Result = $CurrentComputerObject.PSBase.MoveTo($DestinationOU)

 

Happy Upgrading & Good Providence!

Task Sequence Fails to Start 0x80041032

Recommended reading: https://blogs.msdn.microsoft.com/steverac/2014/08/25/policy-flow-the-details/

After preparing a 1730 upgrade Task Sequence for our 1607 machines, I kicked it off on a box via Software Center and walked away once the status changed to ‘Installing thinking I’d come back to an upgraded machine an hour later.  To my surprise, I was still logged on and the TS was still ‘running’.  A few hours later the button was back to ‘Install’ with a status of ‘Available’.  Thinking I goofed I tried again and the same thing happened.

I assumed it was unique to this one machine so I tried it on another and it behaved the same way.  I then ran the upgrade sequence on 5 other machines and they all exhibited the same behavior.  I knew the Task Sequence was sound because others were using it, so it was definitely something unique to my machines but what?

Since I was doing this on 1607 machines I tried upgrading form 1511 to 1703 and 1511 to 1607 but they too failed the same way confirming it was not Task Sequence specific but again unique to my machines.  After spending a quite a few cycles on this, my original machine started failing differently: I was now seeing a ‘Retry’ button with a status of ‘Failed’.  I checked the smsts.log but it didn’t have a recent date/time stamp so it never got that far.  Hmm…

Check the TSAgent.log

Opening the TSAgent.log file I could see some 80070002 errors about not being able to delete HKLM\Software\Microsoft\SMS\Task Sequence\Active Request Handle but the real cause was a bit further up.

TSAgentLog

The lines of interest:


Getting assignments from local WMI. TSAgent 9/1/2017 12:58:27 PM 1748 (0x06D4)
pIWBEMServices->;;ExecQuery (BString(L"WQL"), BString (L"select * from XXX_Policy_Policy4"), WBEM_FLAG_FORWARD_ONLY, NULL, &pWBEMInstanceEnum), HRESULT=80041032 (e:\nts_sccm_release\sms\framework\osdmessaging\libsmsmessaging.cpp,3205) TSAgent 9/1/2017 1:03:55 PM 1748 (0x06D4)
Query for assigned policies failed. 80041032 TSAgent 9/1/2017 1:03:55 PM 1748 (0x06D4)oPolicyAssignments.RequestAssignmentsLocally(), HRESULT=80041032 (e:\cm1702_rtm\sms\framework\tscore\tspolicy.cpp,990) TSAgent 9/1/2017 1:03:55 PM 1748 (0x06D4)
Failed to get assignments from local WMI (Code 0x80041032) TSAgent 9/1/2017 1:03:55 PM 1748 (0x06D4)

The source of error code 80041032 is Windows Management (WMI) and translates to ‘Call cancelled’ which presumably happened while running the query select * from XXX_Policy_Policy4, where XXX is the site code.

I ran a similar query on my machine to get a feel for the number of items in there:


(gwmi -Class xxx_policy_policy4 -Namespace root\xxx\Policy\machine\RequestedConfig).Count

Which ended up failing with a Quota violation error suggesting I’ve reached the WMI memory quota.

Increase WMI MemoryPerHost & MemoryAllHosts

Fortunately, there’s a super helpful TechNet Blog post about this.  Since all of my test machines were running into this, I decided to make life easier for myself and use PowerShell to accomplish the task on a few of them thinking I’d have to raise the limit once.


$PHQC = gwmi -Class __providerhostquotaconfiguration -Namespace root
$PHQC.MemoryPerHost = 805306368
# Below is optional but mentioned in the article
#$PHQC.MemoryAllHosts = 2147483648
$PHQC.Put()
Restart-Computer

After the machine came up I ran the same query again, and after 2 minutes and 38 seconds it returned over 1800 items.  Great!  I ran it again and after 5 minutes it failed with the same quota violation error.  Boo urns.  I kept raising MemoryPerHost and MemoryAllHosts to insane levels to get the query to run back to back successfully.

The good news is that I made progress suggesting I’m definitely hitting some sort of memory ceiling that has now been raised.

The bad news is why me and not others?  Hmm…

Root Cause Analysis

I checked the deployments available to that machine and wowzers it was super long.  I won’t embarrass myself by posting an image of that but it was very long.  This helped to identify areas of improvement in the way we’re managing collections & deploying things, be it Applications, Packages, Software Updates and so on.

On my patient zero machine I ran the following to clear out the policies stored in WMI:


gwmi -namespace root\xxx\softmgmtagent -query "select * from ccm_tsexecutionrequest" | remove-wmiobject

gwmi -namespace root\xxx -query "select * from sms_maintenancetaskrequests" | remove-wmiobject

restart-service -name ccmexec

I then downloaded the policies and tried to image – it worked!  I decided to let that machine go and focus on some administrative cleanup in SCCM.  After tidying things up a bit, the rest of my 1607 machines started the 1703 upgrade Task Sequence without issue and the 1511 machines ran the 1607 upgrade Task Sequence as well.

As we continue to phase out Windows 7, we’ll hopefully update our methods to help avoid problems like this and perform that routine maintenance a little more frequently.

 

Upgrading Windows 10 1511 to 1607

In 2016 we began the process of moving from Windows 7 to Windows 10 1511 learning a ton along the way.  After 1607 went Current Branch for Business (CBB) we began planning for that upgrade phase, and what lies below is a overview of that process.

Initial Smoke Test

Once 1607 went CBB we very quickly threw an upgrade Task Sequence together to see what the upgrade process looked like and what, if anything, broke.  The upgrade process went smoothly, the vast majority of applications worked but there were a handful of things that needed to be worked out

  • Remote Server Administration Tools (RSAT) was no longer present
  • Citrix Single Sign-On was broken
  • A bunch of Universal Windows Platform (UWP) or Modern Applications we didn’t want were back
  • Default File Associations had reverted
  • The Windows default wall paper had returned
  • User Experience Virtualization (UE-V/UX-V) wasn’t working.
  • Taskbar pinnings were incorrect; Specifically Edge and the Windows Store had returned

Still, everything seemed very doable so we had high hopes for getting it out there quickly.

Approach: Servicing vs Task Sequence

Wanting to get our feet wet with Windows as as Service (WaaS), we explored leveraging Servicing Plans to get everyone on to 1607 but quickly ran into a show stopper: How were we going to fix all of the 1607 upgrade ‘issues’ if we went down this path?

We couldn’t find an appealing solution for this, so we went ahead with the Task Sequence based upgrade approach.  This gave us greater flexibility because not only could we fix all the upgrade issues but also do a little housekeeping, like install new applications, upgrade existing applications, remove retired applications and more.  This was far simpler and more elegant than setting up a bunch of deployments for the various tasks we wanted to accomplish either before the upgrade or after.

Avoiding Resume Updating/Generating Events

One concern with Servicing was ensuring the upgrade wasn’t heavy handed, forcing a machine to either upgrade mid-day because they were beyond the deadline or during the night because they left their machine on.  This was because the upgrade would bounce their machine which could potentially result in lost work, something most people find undesirable.  With Servicing, we couldn’t come up with a sure-fire way to check for and block the upgrade if, say instances of work applications were detected, such as the Office suite, Acrobat and so on.

Sure, we could increase the auto-save frequency – perhaps setting it to 1 minute – and craft a technical solution to programmatically save files in the Office Suite, safe Drafts and try to do some magic to save PDF’s and so on.  But at the end of the day, we couldn’t account for every situation: we would never know if the person wanted to create a new file vs a new version or simply overwrite the existing one.  And most importantly, we didn’t want to have to answer why a bunch of Partners lost work product as a result of the upgrade.

So, we decided to go the Task Sequence route.

Task Sequence Based Upgrade

Now that we knew which way we need to go, it was just a matter of building the fixes to remediate the upgrade issues then setup the Task Sequence.

Upgrade Remediation

  • Remote Server Administration Tools (RSAT) – Prior to performing the OS upgrade, a script is executed to detect RSAT, and if present, a Boolean variable which is referenced after the upgrade is complete to triggers re-installation of RSAT.
    .
  • Citrix Single Sign-On – This is a known issue – see CTX216312 for more details.
    .
  • Universal Windows Platform (UWP) applications – Re-run our in-house script to remove the applications.
    .
  • Default File Associations
    • Option 1: Prior to performing the OS upgrade, export HKCR and HKCU\Software\Classes then import post upgrade.
    • Option 2: Re-apply the defaults via dism from our ‘master’ file.
      .
  • Wallpaper – Re-apply in the Task Sequence by taking advantage of the img0 trick.
    .
  • UE-V/UX-V – The upgrade process broke the individual components of the UE-V Scheduled Tasks requiring a rebuild.  Once fixed on a machine we copied the good/fixed files from C:\Windows\System32\Tasks\Microsoft\UE-V and setup the Task Sequence:
    1. Enable UE-V during the upgrade via PowerShell
    2. Copied the fixed files into C:\Windows\System32\Tasks\Microsoft\UE-V
    3. Updated the command line the Scheduled Task ran
    4. Disabled the ‘Synchronize Settings at Logoff‘ Scheduled Task since that was still buggy, causing clients to hang on log off.
      .
  • Taskbar Pinnings – Prior to performing the OS upgrade, export HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Taskband then import post upgrade.
    .
  • Critical Process Detection – CustomSettings.ini calls a user exit script that loops through a series of key executables (outlook.exe, winword.exe etc.) running tasklist to see if a process is detected and if so sets a Task Sequence variable that’s later evaluated.

Since we were going the Task Sequence route, and it would be generally available in Software Center, it was decided a password prompt might help prevent accidental foot shooting.  So shortly after the Task Sequence launches an HTA driven password prompt is displayed that only IT should be able to successfully navigate.  This added yet another line of defense for anyone who ‘accidentally’ launched the Task Sequence;
Even though one has to click through two prompts to confirm the installation but whatever. 🙂

Determine Current OU of Machine

I created this function ages ago as part of a larger script that needed to be executed completely unattended.

Please note that this is NOT the ideal way to handle any operations that require credentials.  Keeping credentials in a script is bad practice as anyone snooping around could happen upon them and create some problems.  Instead, you should rely on webservices to do this and Maik Koster has put together an excellent little care package to help you get started.

Get-CurrentOU Prerequisites

My script has a few prerequisites:

  • The name of the computer you’re working with
  • The current AD site
  • A [local] Domain Controller to connect to

This script does not rely on the ActiveDirectory module as I needed this to execute in environments where it wouldn’t be present.
I personally try to keep everything self contained where it makes sense to do so.  It’s one of my [many] quirks.

The Computer Name

Just feed it as a parameter or let it default to the current machine name.

Finding the Current AD Site

Better see this post for that.

Finding a Local Domain Controller

Better see this post for that.

Get-CurrentOU

Function Get-CurrentOU
    {
        Param
            (
                [Parameter(Mandatory=$true)]
                    [string]$ADComputerName = $env:COMPUTERNAME,

                [Parameter(Mandatory=$false)]
                    [string]$Site = $Global:Site,

                [Parameter(Mandatory=$false)]
                    [string]$SiteDC = $Global:SiteDC
            )

        Try
            {
                $Domain = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
                $_computerType = 'CN=Computer,CN=Schema,CN=Configuration,' + $Domain
                $path = 'LDAP://' + $SiteDC + "/" + $Domain
                $12 = 'YwBvAG4AdABvAHMAbwAuAGMAbwBtAFwAcwBlAHIAdgBpAGMAZQBfAGEAYwBjAG8AdQBuAHQAXwBqAG8AaQBuAF8AZABvAG0AYQBpAG4A'
                $3 = 'bQB5ACAAdQBiAGUAcgAgAHMAZQBrAHIAZQB0ACAAUAA0ADUANQB3ADAAcgBkACAAZgBvAHIAIABhAG4AIAAzADEAMwAzADcAIABoAGEAeAAwAHIAIQAhACEAMQAhAA=='
                $DirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($path,[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($12)),[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($3)))
                $DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher($DirectoryEntry)
                $DirectorySearcher.Filter = "(&(ObjectCategory=computer)(samaccountname=$ADComputerName$))"
                $SearchResults = $DirectorySearcher.FindAll()

                if($SearchResults.count -gt 0) { return (New-Object System.DirectoryServices.DirectoryEntry($SearchResults[0].Path,[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($12)),[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($3)))).Path.Substring(8+$siteDC.Length+3+$ADComputerName.length+1) }
                Else { Write-Host "ERROR: Computer object not found in AD: [$ADComputerName]"; return $false }
            }
        Catch { return $_ }
    }

 

This has worked well for me, but you’re welcome to use a different filter instead, such as

$DirectorySearcher.Filter = "(Name=$ADComputerName)"

 

As I mentioned above, you should really explore webservices instead of hardcoding passwords in scripts but this will work in a pinch until you can get that setup.

 

Good Providence!

Finding a Local Domain Controller

I created this function ages ago as part of a larger script that needed to be executed completely unattended.

Since I started down the System Engineer / Desktop Engineer / Application Packager path a handful of years ago, I’ve seen some interesting behaviors.  For example, when joining a domain, a machine might reach out to a domain controller overseas versus one at the local site or even in the same continent.  And because things were working, those types of anomalies were never really looked into.

This only ever really came up when there were OU structure changes.  For instance, when going from XP to Windows 7, or after completing an AD remediation project.  As machines were being moved around, we noticed the Domain Controller used varied and the time it took to replicate those changes varied from:

  • instantaneously – suggesting the change was made on a local DC OR a DC with change notification enabled
  • upwards of an hour – suggesting a remote DC (and usually verified by logs or checking manually) or on a DC where change notification was not enabled.

So I decided to write a small function for my scripts that would ensure they were using a local Domain Controller every time.

There are a handful of ways to get a domain controller

  • LOGONSERVER
  • dsquery
  • Get-ADDomainController
  • Other third-party utilities like adfind, admod

But either they were not always reliable (as was the case with LOGONSERVER which would frequently point to a remote DC) or they required supporting files (as is the case with dsquery and Get-ADDomainController).  Nothing wrong with creating packages, adding these utilities to %ScriptRoot% or even calling files on a network share.
I simply preferred something self-contained.

Find Domain Controllers via NSLOOKUP

I started exploring by using nslookup to locate for service record (SRV) resource records for domain controllers and I landed on this:

nslookup -type=srv _kerberos._tcp.dc._msdcs.$env:USERDNSDOMAIN

Problem was it returned every domain controller in the organization.  Fortunately  you can specify an AD site name to narrow it down to that specific AD Site:

nslookup -type=srv _kerberos._tcp.$ADSiteName._sites.$env:USERDNSDOMAIN

Bingo!  This gave me exactly what I was hoping to find and the DC’s it returns are in a different order every time which helps to round-robin things a bit.

From there it was all downhill:

  1. Put AD Sites in the script
  2. Figure out which Site the machine is in by it’s gateway IP
  3. Use nslookup to query for SRV records in that site
  4. Pull the data accordingly.

Yes I know – hard coding AD Sites and gateway IP’s is probably frowned upon because

What if it changes?

I don’t know about your organisation but in my [limited] experience the rationale was that AD Sites and gateway IP’s typically don’t change often enough to warrant that level of concern, so it didn’t deter me from using this method.  But I do acknowledge that it is something to remember especially during expansion.

Also, I already had all of our gateway IP’s accessible in a bootstrap.ini due to our existing MDT infrastructure making this approach much simpler workwise.

Get-SiteDomainController

And here’s where I ended up.  The most useful part – to me anyway – is that it works in Windows (real Windows) at any stage of the imaging process and doesn’t require anything extra to work.  The only gotcha is that it does not work in WinPE because nslookup isn’t built-in and I don’t know why after all these years it still isn’t built-in.

Function Get-SiteDomainController
    {
        Param
            (
                [Parameter(Mandatory=$true)]
                    [string]$Site,

                [Parameter(Mandatory=$true)]
                    [string]$Domain
            )
        Try { return (@(nslookup -type=srv _kerberos._tcp.$Site._sites.$Domain | where {$_ -like "*hostname*"}))[0].SubString(20) }
        Catch { return $_ }
    }

Hindsight

I’ve been using the above method for determining the local DC for years and its been super reliable for me.  It wasn’t until recently that I learned that the query I was using would not always return a domain controller since the query simply locates servers that are running the Kerberos KDC service for that domain.  Whoops.

Instead, I should have used this query which will always only return domain controllers:

nslookup -type=srv _kerberos._tcp.$ADSiteName._sites.dc._msdcs.$env:USERDNSDOMAIN

 

Honorable Mention: NLTEST

I would have been able to accomplish the same task with nltest via

nltest /dclist:$env:USERDNSDOMAIN

And then continuing with the gateway IP to AD Site translation to pull out just the entries I need.

nltest /dclist:$env:USERDNSDOMAIN | find /i ": $ADSiteName"

One interesting thing to note about nltest is that it seems to always return the servers in the same order.  This could be good for consistency’s sake, but it could also be a single point of failure if that first server has a problem.

The nslookup query on the other hand returns results in a different order each time it’s executed.  Again, this could also be bad, making it difficult to spot an issue with a Domain Controller, but it could be good in that it doesn’t halt whatever process is using the output of that function.

¯\_(ツ)_/¯

Anyway, since the nslookup script has been in production for some time now and works just fine, I’m not going to change it.  However, any future scripts I create that need similar functionality will likely use the updated nslookup query method.

 

Good Providence!

Determine AD Site

I created this function ages ago as part of a larger script that needed to be executed completely unattended.

For a variety of reasons, I needed a reliable way to determine which AD Site a particular machine was in.  Over the years I’ve seen some really odd behaviors that – technically – shouldn’t happen.  But as with many organizations (one hopes anyway…) things don’t always go as planned so you need that backup plan.

Get-Site Prerequisites

My script has a few prerequisites:

I personally try to keep everything self contained where it makes sense to do so.  It’s one of my [many(?)] quirks.

Finding the Current Default Gateway IP

Better see this post for that.

Get-Site Explained

In our environment, the machine names contain the office location which makes it easy to figure out where the machine is physically.  However, not every machine is named ‘correctly’ (read: follows this naming convention) as they were either renamed by their owners or are running an image prior to this new naming convention.  This is why I’m leveraging a hash table array of IP’s as a backup.

It seemed tedious at first, but since I already had a list of IP’s in the bootstrap.ini so it made this a little easier.  Also, Site IP’s don’t change often which means this should be a reliable method for years to come with only the occasional update.

Function Get-Site
    {
        Param([Parameter(Mandatory=$true)][string]$DefaultGatewayIP)

        Switch($env:COMPUTERNAME.Substring(0,2))
            {
                { 'HQ', 'TX', 'FL' -contains $_ } { $Site = $_ }
                Default
                    {
                        $htOfficeGateways = @{
                            "Headquarters"  = "192.168.0.1","192.168.10.1";
                            "Office1"       = "192.168.1.1","192.168.11.1";
                            "Office2"       = "192.168.2.1","192.168.12.1";
                        }

                        $DefaultGatewayIP = '192.168.0.1'
                        #$DefaultGatewayIP = '192.168.1.1'
                        #$DefaultGatewayIP = '192.168.2.1'

                        Foreach($Office in ($htOfficeGateways.GetEnumerator() | Where-Object { $_.Value -eq $DefaultGatewayIP } ))
                            {
                                Switch($($Office.Name))
                                    {
                                        "Headquarters" { $Site = 'HQ' }
                                        "Office1"      { $Site = 'TX' }
                                        "Office2"      { $Site = 'FL' }
                                    }
                            }
                    }
            }
        return $Site
    }

 

I didn’t add a check prior to returning $Site to confirm it was actually set but that can be handled outside of the function as well.  Otherwise you could do $Site = 'DDOJSIOC' at the top and then check for that later since that should never be a valid site … unless you’re Billy Rosewood.

 

Get Gateway IP Address

I created this function ages ago as part of a larger script that needed to be executed completely unattended.

I needed a method to determine which office a particular asset was in at the time.  I settled on using the gateway IP address since I already had a list of them from our BootStrap.ini and CustomSettings.ini.

Originally I was using this query:

[array]$arrDefaultGatewayIP = gwmi Win32_NetworkAdapterConfiguration -ErrorAction Stop | select -ExpandProperty DefaultIPGateway

But this proved problematic on machines running older versions of PowerShell so I ended up using this instead.

[array]$arrDefaultGatewayIP = gwmi Win32_NetworkAdapterConfiguration -ErrorAction Stop | Where { $_.DefaultIPGateway -ne $null } | select -ExpandProperty DefaultIPGateway

Alternatively I suppose I could have Try’ied the first then executed the latter if PoSH Caught any exceptions.

And converted it to a function

Function Get-GatewayIP

Function Get-GatewayIP
{
try { return [string](gwmi Win32_networkAdapterConfiguration -Filter 'IPEnabled = True' -ErrorAction Stop | ? { $_.DefaultIPGateway -ne $null } | select -ExpandProperty DefaultIPGateway | Select -First 1) }
catch { return $_ }
}

From there it was just a matter of validating that the return result looked like an IP before feeding them through a hash table array to determine the office location.

So Why is this Relevant?

My original plan was to rely on the computer name since, in our environment, machine names contain an abbreviated version of the office name – the office code for lack of a better term.  For example,

  • a machine based out of the New York office might be called SOME-NY-THING
  • another in our Nation’s Capital might be OTHER-DC-THING etc.

However some machines don’t follow this format because:

  1. they’re running an older build imaged prior to the new naming convention
  2. some overzealous/tenacious individuals have renamed their machines

I had also considered querying AD to get the machine’s current OU but since this script didn’t always run under the context of an AD account (e.g.: in WinPE) it meant baking the credentials into the script, and I really don’t like doing that any more than I have to.
(Hint: Webservices is the answer to that problem)

I’ve been using this approach for well over a year now and its come in handy countless times.  Hopefully you’ll find some value for this as well.

Good Providence!

Preparing for Windows 10: OU Structure

Our team has been playing around with Windows 10 in our corporate environment for months now and one thing that was apparent from day one was that some something was breaking the Task Bar rendering the Start Menu & Cortana completely useless.  (i.e.: You click on it and it doesn’t work.)  We were certain it was a GPO, but we didn’t know which GPO or more specifically which setting within was creating the problem.  We started digging into it a little bit but it wasn’t long before we realized it was almost a moot point because:

  1. Some GPOs were specific to Windows 7 & wouldn’t apply to Windows 10
  2. Some GPOs were specific to Office 2010 & wouldn’t apply to Office 2016
  3. Others were simply no longer needed like our IEM policy for IE9.
  4. Finally, and perhaps most importantly: we wanted to take this opportunity to start from scratch: Re-validate all the GPO’s, the settings within & consolidating where possible.

And in order to do all that, we needed to re-evaluate our OU structure.

When it comes to OU structures, depending on the audience, it makes for very “exciting” conversation as some OU structures have a fairly simple layout:

  • Corp Computers
    • Hardware Type

While others look like a rabbit warren:

  • Continent
    • Country
      • Region (or equivalent)
        • State  (or equivalent)
          • City
            • Role/Practice Group/Functional Group
              • Hardware Type
                • Special Configurations
                  • Other
Note: I’m not knocking the latter or suggesting the former is better.  Really just depends on the specific needs of the organization.
No one size fits all; but some sizes may fit many.

When we did an image refresh some time ago – prior to our SCCM implementation – we took a ‘middle of the road’ approach with the main branches being location and hardware type. Later during during our SCCM implementation last summer, knowing that Windows 10 was on the horizon we revised the layout adding the OS branch in order to properly support both environments.

Bulk Creating OU’s

Once you know your layout, for example:

  • CorpSystems
    • Location
      • OS
        • Hardware Type

It’s time to create them all.  I wasn’t about to do this by hand so I hunted around for a way to recursively create OUs.  The solution comes courtesy of a serverfault question, and I just tweaked it to work for us.

Function Create-NewOU
    {
        # http://serverfault.com/questions/624279/how-can-i-create-organizational-units-recursively-on-powershell
        Param
            (
                [parameter(Position = 0,Mandatory = $true,HelpMessage = "Name of the new OU (e.g.: Win10)")]
                [alias('OUName')]
                    [string]$NewOU,

                [parameter(Position = 1,Mandatory = $false,HelpMessage = "Location of the new OU (e.g.: OU=IT,DC=Contoso,DC=com)")]
                [alias("OUPath")]
                    [string]$Path
            )

        # For testing purposes
        #$NewOU = 'Win10'
        #$NewOU = 'OU=SpecOps,OU=IT,OU=Laptops,OU=Win10,OU=Miami,OU=Florida,OU=Eastern,OU=NorthAmerica'
        #$Path = 'DC=it,DC=contoso,DC=com'
        #$Path = (Get-ADRootDSE).defaultNamingContext

        Try
            {
                if($Path) { if($NewOU.Substring(0,3) -eq 'OU=') { $NewOU = $NewOU + ',' + $Path } else { $NewOU = 'OU=' + $NewOU + ',' + $Path } }
                else { if($NewOU.Substring(0,3) -eq 'OU=') { $NewOU = $NewOU + ',' + (Get-ADRootDSE).defaultNamingContext } else { $NewOU = 'OU=' + $NewOU + ',' + (Get-ADRootDSE).defaultNamingContext } }

                # A regex to split the distinguishedname (DN), taking escaped commas into account
                $DNRegex = '(?<![\\]),'

                # We'll need to traverse the path, level by level, let's figure out the number of possible levels
                $Depth = ($NewOU -split $DNRegex).Count

                # Step through each possible parent OU
                for($i = 1;$i -le $Depth;$i++)
                    {
                        $NextOU = ($NewOU -split $DNRegex,$i)[-1]
                        if(($NextOU.Substring(0,3) -eq "OU=") -and ([ADSI]::Exists("LDAP://$NextOU") -eq $false)) { [String[]]$MissingOUs += $NextOU }
                    }

                # Reverse the order of missing OUs, we want to create the top-most needed level first
                [array]::Reverse($MissingOUs)

                # Now create the missing part of the tree, including the desired OU
                foreach($OU in $MissingOUs)
                    {
                        $NewOUName = (($OU -split $DNRegex,2)[0] -split "=")[1]
                        $NewOUPath = ($OU -split $DNRegex,2)[1]

                        write-host "Creating [$NewOUName] in [$NewOUPath]"
                        New-ADOrganizationalUnit -Name $newOUName -Path $newOUPath -Verbose
                    }
            }
        Catch { return $_ }
        return $true
    }

 

With that done, it was on to the next bit which was creating and linking GPOs.

 

Good Providence!