PowerShell

MDT Tutorial Part 12: Wrap Up

Living Table of Contents

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

What These Guides Are Not:
A guide to create a one-size fits all solution that fixes all the issues you’re ever going to encounter.

Why Bother with My Tutorials?

You totally don’t have to take my word on it!  I’m not world-reknowned like others listed on the Resources page – heck I’m not even county-reknowned so you totally should scrutinize the content and provide me with any constructive criticism you may have. 🙂

However I’ve learned from experience, as well as from others in the circles I travel in, that although turn-key solutions are great, once implemented those who implemented them are not fully equipped to maintain the environment.

Please do not misconstrue what I’m saying here: There is nothing wrong with the turn-key solutions out there!  It’s simply that we’re not all at the same level from a technical perspective and we all have knowledge gaps.  But it’s that combination that makes it challenging for some to support those turn-key solutions.

For me anyway I find that having a good base and some reference points better equips me for the road that lies ahead.  And when something breaks it’s an excellent opportunity to review my depth of knowledge on the subject to troubleshoot my way back into a functioning state.  But that’s me and it may not be you.  Maybe you’re just some savant and it comes naturally.

If you were brave enough to go through this process and successfully built, captured & deployed images, then you should have sufficient functional knowledge to efficiently use the turn-key solution below.

More Labs & Test Environments from Microsoft

Turn-Key Solution from Microsoft: Microsoft 365 Powered Device Lab Kit (Formerly Windows 10 Deployment and Management Lab Kit)

The Microsoft 365 powered device lab kit (formerly Windows 10 Deployment and Management Lab Kit) is a complete pre-configured virtual lab environment including evaluation versions of:

  • Windows 10 Enterprise, version 1803 (Windows 10 April 2018 Update)
  • System Center Configuration Manager, version 1802
  • Windows Assessment and Deployment Kit for Windows 10, version 1803
  • Microsoft Deployment Toolkit
  • Microsoft Application Virtualization (App-V) 5.1
  • Microsoft BitLocker Administration and Monitoring 2.5 SP1
  • Windows Server 2016
  • Microsoft SQL Server 2014
  • Connected trials of:
    • Office 365 Enterprise E5
    • Enterprise Mobility + Security

The best part is that it also includes illustrated step-by-step lab guides to take you through multiple deployment and management scenarios, including:

Servicing

  • Windows Analytics Update Compliance
  • Servicing Windows 10 with Configuration Manager
  • Servicing Office 365 ProPlus with Configuration Manager

Deployment and management

  • Modern Device Deployment
  • Modern Device Management with AutoPilot
  • Modern Device Co-Management
  • Office 365 ProPlus Deployment
  • BIOS to UEFI Conversion
  • Modern Application Management with Intune
  • Enterprise State Roaming
  • Remote Access (VPN)

Security

  • Windows Information Protection
  • Windows Defender Advanced Threat Protection
  • Windows Defender Application Guard
  • Windows Defender Application Control
  • Windows Defender Antivirus
  • Windows Defender Exploit Guard
  • Windows Hello for Business
  • Credential Guard
  • Device Encryption (BitLocker)
  • Remote Access (VPN)

Compatibility

  • Windows App Certification Kit
  • Windows Analytics Upgrade Readiness
  • Browser Compatibility
  • Windows App CertificationKit
  • Desktop Bridges

 

This is an amazing kit because of its holistic approach to helping IT Pros transition to the ‘Modern Desktop’.  As such it’s a hefty download and the hardware requirements are steep.

Turn-Key Solution from Johan Arwidmark: Hydration Kit

Johan has been churning out Hydration Kits since as far back as ConfigMgr 2007 SP2 and MDT 2010 so he’s one of the top 5 go-to’s for this sort of thing.

From the author:

This Kit builds a complete ConfigMgr CB 1702, and ConfigMgr TP 1703, with Windows Server 2016 and SQL Server 2016 SP1 infrastructure, and some (optional) supporting servers. This kit is tested on both Hyper-V and VMware virtual platforms, but should really work on any virtualization platform that can boot from an ISO. The kit offers a complete setup of both a primary site server running ConfigMgr Current Branch v1702 (server CM01, as well as a primary site server running ConfigMgr Technical Preview Branch v1703 (server CM02). You also find guidance on upgrading current branch platform to the latest build.

There’s plenty of documentation in the link:

https://deploymentresearch.com/Research/Post/580/Hydration-Kit-For-Windows-Server-2016-and-ConfigMgr-Current-Technical-Preview-Branch

 

Turn-Key Solution from Mikael Nystrom: Image Factory for Hyper-V

Mikael has been working on this for at least 2 years and is another in the top 5 go-to’s for this sort of thing.

From the author:

The image factory creates reference images using Microsoft Deployment Toolkit and PowerShell. You run the script on the MDT server. The script will connect to a hyper-v host specified in the XML file and build one virtual machine for each task sequences in the REF folder. It will then grab the BIOS serial number from each virtual machine, inject it into customsettings.ini, start the virtual machines, run the build and capture process, turn off the virtual machine and remove them all.

There’s some great documentation here but plenty to browse through in the link:

https://github.com/DeploymentBunny/ImageFactoryV3ForHyper-V

 

Turn-Key Solution from Mike Galvin: Image Factory for Microsoft Deployment Toolkit

I happened upon this by accident but I like how straight forward it appears to be.

From the author:

Recently I’ve been looking into automating my image build process further using PowerShell to create an Image Factory of sorts. There are other similar scripts that I’ve found online, but I like the relatively simple and straightforward method I’ve developed below. If like me you’re looking to automate your image build process so that you can have a fully up-to-date image library after patch Tuesday each month, then this may be for you.

This link is the original post where the script is introduced but I strongly urge you to start with the below link then work your way backwards from there.

https://gal.vin/2017/08/26/image-factory/

 

Turn-Key Solution from Coretech: Image Factory

This unfortunately is not something I can link without burning a bridge.  This solution is only obtainable from [select (?)] Coretech training classes or simply if Kent Agerlund likes you.  It’s conceptually similar to many others here, relying on Hyper-V to spin up VM’s, start them, boot, start a specific build & capture task sequence, run through the task sequence then destroy the VM.  Done.

 

In Closing

As you can see there are plenty of training, lab and turn key imaging solutions out there but to quote Browning:

Image the whole, then execute the parts
          Fancy the fabric
Quite, ere you build, ere steel strike fire from quartz,
          Ere mortar dab brick!

In other words, get the total picture of the process; and I argue there are two ways of doing that: with a telescope and a microscope.

Start with the telescope to look at all of it from thirty-thousand feet and you’ll see almost all these require a dab of elbow grease to get going.  In fact, most make assumptions about the environment as well as a certain amount of proficiency with the various technologies being used: PowerShell, MDT, Hyper-V, SCCM, Active Directory etc.

In the same vain that I’d rather learn to drive a manual transmission on an old beater car before I jump into an STI, GT3 RS, or <insert other absurdly expensive manual transmission vehicle here> , it makes more sense to have a slightly more-than-basic understanding of how the technology works before diving in head first.

Good Providence to you!

Advertisements

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&lt;span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			&gt;&amp;#65279;&lt;/span&gt;

 

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!

Facilitating KMS Activations

Occasionally we run into situations where Windows or the Office Suite fails to activate.

The solution: Use the built-in product-specific scripts to activate.

The problem: Many people in IT are not aware of these scripts, and thus how to use them, which results in a guaranteed call or email to the appropriate team.

Since I’m all about empowering people, I figured I’d put together a little script to help facilitate all this.

Enter: KMSActivate-MicrosoftProducts

I probably could have just left it as ‘Activate-MicrosoftProducts’ but I wanted to make sure potential users knew this was specifically for KMS scenarios, not MAK which has it’s own procedure.


[cmdletbinding()]
Param
    (
        [Parameter(Mandatory=$false)]
            [Switch]$ActivateOS = $true,

        [Parameter(Mandatory=$false)]
            [Switch]$ActivateOffice = $true
    )

Function Check-IfRunningWithAdministratorRights { If (!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]“Administrator”)) { Write-Warning “ERROR: You DO NOT have Administrator rights to run this script!`nPlease re-run this script as an Administrator!”; Break } }

Function Get-KMSHost { try { return (nslookup -type=srv _vlmcs._tcp | ? { $_ -like '*svr hostname*' }).Replace('svr hostname','').Replace('=','').Trim() } catch { throw $_ } }

Function Get-KMSClientSetupKey
    {
        # https://technet.microsoft.com/en-us/library/jj612867(v=ws.11).aspx
        [cmdletbinding()]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string]$OperatingSystemCaption = (Get-CimInstance -ClassName win32_operatingsystem).caption
            )

        Switch -Wildcard ($OperatingSystemCaption)
            {
                '*Windows Server 2016 Datacenter*' { return 'CB7KF-BWN84-R7R2Y-793K2-8XDDG' }

                '*Windows Server 2016 Standard*' { return 'WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY' }

                '*Windows Server 2016 Essentials*' { return 'JCKRF-N37P4-C2D82-9YXRT-4M63B' }

                '*Windows 10 Enterprise*' { return 'NPPR9-FWDCX-D2C8J-H872K-2YT43' }

                '*Windows 7 Enterprise*' { return '33PXH-7Y6KF-2VJC9-XBBR8-HVTHH' }

                default { write-host "ERROR INVALID OPERATING SYSTEM CAPTION: $_"; throw }
            }
    }

Function KMSActivate-OperatingSystem
    {
        [cmdletbinding()]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string]$KMSHost = $(Get-KMSHost),

                [Parameter(Mandatory=$false)]
                    [string]$KMSClientSetupKey = $(Get-KMSClientSetupKey)
            )

        Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$env:windir\System32\slmgr.vbs`" -dlv" -Wait

        Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$env:windir\System32\slmgr.vbs`" -skms $KMSHost" -Wait

        Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$env:windir\System32\slmgr.vbs`" -ipk $KMSClientSetupKey" -Wait

        Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$env:windir\System32\slmgr.vbs`" -ato" -Wait

        Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$env:windir\System32\slmgr.vbs`" -dlv" -Wait
    }

Function KMSActivate-OfficeSuite
    {
        [cmdletbinding()]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string]$KMSHost = $(Get-KMSHost)
            )

        #write-host "KMSHost [$KMSHost]"

        [System.Collections.ArrayList]$OfficeInstallationDirs = @()
        foreach($ProgFilePath in $env:ProgramFiles,${env:ProgramFiles(x86)})
            {
                if(!(Test-Path -Path $ProgFilePath -PathType Container)) { continue }
                foreach($OfficeVersion in (gci "$ProgFilePath\Microsoft Office" -Filter Office* -ErrorAction SilentlyContinue)) { $OfficeInstallationDirs += $OfficeVersion.Fullname }
            }

        foreach($OfficeInstallationDir in $OfficeInstallationDirs)
            {
                if(!(Test-Path -Path "$OfficeInstallationDir\ospp.vbs" -PathType Leaf)) { continue }

                Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$OfficeInstallationDir\ospp.vbs`" /dstatusall" -Wait

                Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$OfficeInstallationDir\ospp.vbs`" /sethst:$KMSHost" -Wait

                Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$OfficeInstallationDir\ospp.vbs`" /act" -Wait

                Start-Process -FilePath 'cscript' -ArgumentList "/nologo `"$OfficeInstallationDir\ospp.vbs`" /dstatusall" -Wait
            }
    }

Check-IfRunningWithAdministratorRights

if($ActivateOS -eq $true) { KMSActivate-OperatingSystem }

if($ActivateOffice -eq $true) { KMSActivate-OfficeSuite }

 

The script will attempt to:

  • Confirm it’s running elevated otherwise it’ll quit
  • Determine the KMS server (or use the one supplied)
  • Determine the OS to set the correct client setup key
  • Determine the version(s) of Office installed (if any)
  • Activate Windows 7, 10 and a few flavors of Server 2016
  • Activate any various versions of Office if present

Not saying this is the best way – just a way.  I merely wanted a turn-key solution for our IT staff and this was what made sense as it solves some of the more infrequent issues that occasionally arose.

Good Providence to you!

Lenovo BIOS Manipulation: Set-LenovoBIOSSetting

Changing a BIOS setting on a Lenovo, is a two step process:

  • Change the setting(s) in question
  • Commit (or Save) the all of your changes

Explanation

The script supports execution against remote systems to silently and easily toggle various BIOS settings.

By default the script relies on Get-LenovoBIOSSetting for validation purposes:

  1. It confirms the Setting you want to change is a valid setting.
  2. It confirms the count of the results returned from the previous command is equal to 1.
    This is done to ensure you specified an explicit setting like ‘Boot Up Num-Lock Status’ and not something generic like ‘Boot%’ which could return ‘Boot Agent’, ‘Boot Up Num-Lock Status’, ‘BootMode’, ‘BootOrder’ and so on.
  3. Finally, it will attempt to validate the Value you specified by evaluating the ‘OptionalValue’s returned by query.

Once it passes validation – or you bypass it – it then proceeds.

However there is a safety net and that is the requirement of the -Commit parameter to actually flip the bits.  If the -Commit parameter isn’t included, which is the default, it doesn’t make any changes.

Function Set-LenovoBIOSSetting


#region Function Set-LenovoBIOSSetting
Function Set-LenovoBIOSSetting
    {
        [cmdletbinding(SupportsShouldProcess=$True)]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string[]]$ComputerName = $env:computername,

                [Parameter(Mandatory=$true)]
                    [string]$Setting,

                [Parameter(Mandatory=$true)]
                [Alias('NewValue')]
                    [string]$Value,

                [Parameter(Mandatory=$false)]
                    [switch]$SkipCheck = $false,

                [Parameter(Mandatory=$false)]
                    [switch]$Commit
            )

        Begin { $NewValue = $Value }

        Process
            {
                foreach($Computer in $ComputerName)
                    {
                        if($SkipCheck -eq $false)
                            {
                                # Validate Setting - Part 1: Ensure this is a legitimate settng in the BIOS
                                $CurrentSettings = Get-LenovoBIOSSetting -ComputerName $Computer -Setting $Setting
                                if([string]::IsNullOrEmpty($CurrentSettings)) { Write-Output "ERROR INVALID SETTING SPECIFIED: [$Setting]"; throw }

                                # Validate Setting - Part 2: Ensure an 'exact' BIOS setting was specified like 'USB Port 1' not 'USB%' which could match several.
                                if(@($CurrentSettings).Count -gt 1) { Write-Output "ERROR SETTING [$Setting] RETURNED TOO MANY MATCHES: [$($CurrentSettings.Setting -join ', ')]"; throw }

                                # Validate New Value - If the OptionalValue isn't 'N/A' then we can verify the user supplied
                                if($CurrentSettings.OptionalValue -ne 'N/A')
                                    {
                                        if($($CurrentSettings.OptionalValue -split ',') -cnotcontains $Value) { Write-Output "ERROR: INVALID VALUE SPECIFIED [$Value] - MUST BE ONE OF: [$($CurrentSettings.OptionalValue)]"; throw }
                                    }

                                if($CurrentSettings.CurrentValue -eq $NewValue) { Write-Output "No change made for [$Setting] on [$Computer] because new and current values are identical:`r`nCurrent Value: [$($CurrentSettings.CurrentValue)]`r`nUpdated Value: [$NewValue]"; return }

                                $Action = "from [$($CurrentSettings.CurrentValue)] to [$NewValue]"
                            }
                        else { $Action = "to [$NewValue]" }

                        if($PSCmdlet.ShouldProcess("Setting [$Setting] on [$Computer] $Action"))
                            {
                                If($Commit -eq $true)
                                    {
                                        $OriginalEAP = $ErrorActionPreference
                                        $ErrorActionPreference = 'Stop'

                                        Try { $SetResult = (gwmi -class Lenovo_SetBiosSetting -namespace root\wmi -ComputerName $Computer -ErrorAction Stop).SetBiosSetting("$Setting,$NewValue") }
                                        Catch { Write-Output "ERROR: FAILED TO SET BIOS SETTING [$Setting] TO [$NewValue] ON [$Computer]: $_"; throw }

                                        $ErrorActionPreference = $OriginalEAP

                                        Switch($SetResult.return)
                                            {
                                                'Success' { Write-Output "$($SetResult.Return)fully set [$Setting] on [$Computer] $Action" }
                                                default
                                                    {
                                                        Write-Output "ERROR: FAILED TO SET [$Setting] TO [$NewValue] ON [$Computer]: [$($SetResult.Return)]"
                                                        if($Firmware -eq 'UEFI') { Write-Output "HINT: Firmware is UEFI [$Firmware] and you //may// first need to disable either 'OS Optimized Defaults' or 'SecureBoot' as these lock certain settings in the BIOS." }
                                                        throw
                                                    }
                                            }
                                    }
                                Else
                                    {
                                        Write-Output "WARNING: Commit is false [$Commit]; NOT Changing BIOS Setting [$Setting] on [$Computer] $Action!"
                                    }
                            }
                    }
            }

        End {  }
    }
#endregion Function Set-LenovoBIOSSetting

One Final Tidbit: Save-LenovoBIOSSettings

This one is important and I intentionally put this down here at the bottom.  Like I mentioned in the beginning, making BIOS change is a two step process, and so far we’ve only done one of the two.

The final step is to save the changes so that they’re committed.  Yes, I realize I’m using the parameter -Commit above which may be a little confusing but that’s just to make sure you don’t accidentally shoot yourself in the foot: write a change you didn’t mean to write and then forget what you had previously, so you struggle to get back to where you were before.

I intentionally didn’t include the ‘save’ code in the function above because I prefer to set all the changes first:

  • Switch to UEFI via ‘Boot Mode’ and ‘Boot Priority’ or ‘SecureBoot’
  • Set boot device order
  • Enable/Disable USB ports
  • Enable/Disable Bluetooth
  • Enable/Disable IPv4NetworkStack and/or IPv6NetworkStack
  • And so on…

Once all the changes are staged and no errors were encountered, I save the settings once via a call to a function which also requires the -Commit parameter to help prevent accidental foot shooting:


#region Function Set-LenovoBIOSSetting
Function Set-LenovoBIOSSetting
    {
        [cmdletbinding(SupportsShouldProcess=$True)]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string[]]$ComputerName = $env:computername,

                [Parameter(Mandatory=$true)]
                    [string]$Setting,

                [Parameter(Mandatory=$true)]
                [Alias('NewValue')]
                    [string]$Value,

                [Parameter(Mandatory=$false)]
                    [switch]$SkipCheck = $false,

                [Parameter(Mandatory=$false)]
                    [switch]$Commit
            )

        Begin { $NewValue = $Value }

        Process
            {
                foreach($Computer in $ComputerName)
                    {
                        if($SkipCheck -eq $false)
                            {
                                # Validate Setting - Part 1: Ensure this is a legitimate settng in the BIOS
                                $CurrentSettings = Get-LenovoBIOSSetting -ComputerName $Computer -Setting $Setting
                                if([string]::IsNullOrEmpty($CurrentSettings)) { Write-Output "ERROR INVALID SETTING SPECIFIED: [$Setting]"; throw }

                                # Validate Setting - Part 2: Ensure an 'exact' BIOS setting was specified like 'USB Port 1' not 'USB%' which could match several.
                                if(@($CurrentSettings).Count -gt 1) { Write-Output "ERROR SETTING [$Setting] RETURNED TOO MANY MATCHES: [$($CurrentSettings.Setting -join ', ')]"; throw }

                                # Validate New Value - If the OptionalValue isn't 'N/A' then we can verify the user supplied
                                if($CurrentSettings.OptionalValue -ne 'N/A')
                                    {
                                        if($($CurrentSettings.OptionalValue -split ',') -cnotcontains $Value) { Write-Output "ERROR: INVALID VALUE SPECIFIED [$Value] - MUST BE ONE OF: [$($CurrentSettings.OptionalValue)]"; throw }
                                    }

                                if($CurrentSettings.CurrentValue -eq $NewValue) { Write-Output "No change made for [$Setting] on [$Computer] because new and current values are identical:`r`nCurrent Value: [$($CurrentSettings.CurrentValue)]`r`nUpdated Value: [$NewValue]"; return }

                                $Action = "from [$($CurrentSettings.CurrentValue)] to [$NewValue]"
                            }
                        else { $Action = "to [$NewValue]" }

                        if($PSCmdlet.ShouldProcess("Setting [$Setting] on [$Computer] $Action"))
                            {
                                If($Commit -eq $true)
                                    {
                                        $OriginalEAP = $ErrorActionPreference
                                        $ErrorActionPreference = 'Stop'

                                        Try { $SetResult = (gwmi -class Lenovo_SetBiosSetting -namespace root\wmi -ComputerName $Computer -ErrorAction Stop).SetBiosSetting("$Setting,$NewValue") }
                                        Catch { Write-Output "ERROR: FAILED TO SET BIOS SETTING [$Setting] TO [$NewValue] ON [$Computer]: $_"; throw }

                                        $ErrorActionPreference = $OriginalEAP

                                        Switch($SetResult.return)
                                            {
                                                'Success' { Write-Output "$($SetResult.Return)fully set [$Setting] on [$Computer] $Action" }
                                                default
                                                    {
                                                        Write-Output "ERROR: FAILED TO SET [$Setting] TO [$NewValue] ON [$Computer]: [$($SetResult.Return)]"
                                                        if($Firmware -eq 'UEFI') { Write-Output "HINT: Firmware is UEFI [$Firmware] and you //may// first need to disable either 'OS Optimized Defaults' or 'SecureBoot' as these lock certain settings in the BIOS." }
                                                        throw
                                                    }
                                            }
                                    }
                                Else
                                    {
                                        Write-Output "WARNING: Commit is false [$Commit]; NOT Changing BIOS Setting [$Setting] on [$Computer] $Action!"
                                    }
                            }
                    }
            }

        End {  }
    }
#endregion Function Set-LenovoBIOSSetting

Obviously you’re welcome to do as you wish in your environment, even roll the execution of the WMI method to the Set-LenovoBIOSSettings function.

Just be safe and sure of what you’re doing 🙂

Good Providence to you as you reconfigure your BIOS’!

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!

Generate WindowsUpdate.Log Without Get-WindowsUpdateLog

Just like knowing that a shrimps heart is located in it’s head area (thorax) you can file this tidbit under useless facts.

If you find yourself in a situation where you need to convert some Windows Update .ETL files into human readable format and the Get-WindowsUpdateLog PowerShell cmdlet isn’t available for whatever reason, you can use TraceFmt.exe to do this for you.

The TraceFmt utility, available through both the Windows Software Development Kit (SDK) and Windows Driver Kit (WDK), takes the details in the trace logs and outputs a human-readable text file containing the formatted trace messages.

Usage:


tracefmt.exe -o "%UserProfile%\Desktop\TraceFmt-WindowsUpdate.log" %SystemRoot%\Logs\WindowsUpdate\WindowsUpdate.20171002.085155.537.1.etl -r srv*%SystemDrive%\Symbols*https://msdl.microsoft.com/download/symbols

Output:


Setting log file to: C:\windows\logs\WindowsUpdate\WindowsUpdate.20171002.085155.537.1.etl
Examining C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x64\default.tmf for message formats,  3 found.
Searching for TMF files on path: C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x64
Logfile C:\windows\logs\WindowsUpdate\WindowsUpdate.20171002.085155.537.1.etl:
        OS version              10.0.14393  (Currently running on 10.0.14393)
        Start Time              2017-10-02-08:51:55.537
        End Time                2017-10-02-09:01:57.790
        Timezone is             @tzres.dll,-112 (Bias is 300mins)
        BufferSize              4096 B
        Maximum File Size       128 MB
        Buffers  Written        3
        Logger Mode Settings    (11002009) ( sequential newfile paged)
        ProcessorCount          1

Processing completed   Buffers: 3, Events: 70, EventsLost: 0 :: Format Errors: 0, Unknowns: 7

Event traces dumped to C:\Users\perkinsjg\Desktop\TraceFmt-WindowsUpdate.log
Event Summary dumped to C:\Users\perkinsjg\Desktop\TraceFmt-WindowsUpdate.log.sum

 

Comparison

TraceFMT:

TraceFMTWindowsUpdateLog.png

Get-WindowsUpdateLog:

Get-WindowsUpdateLog

In Closing

The TraceFmt generated log file will not be identical to the one generated by the Get-WindowsUpdateLog PowerShell cmdlet; but it’ll help in a pinch!

For now, I bid you Good Providence!

Lenovo BIOS Manipulation: Get-LenovoBIOSSetting

First An Apology

I owe you an apology because I forgot to follow up: A year ago I posted about getting ‘pretty’ BIOS data via PowerShell+WMI but never followed up with a function/script; which I’ve been using for sometime now.  I lost sight of this so sorry about that.

Explanation

There really isn’t much to this: Execute Get-BIOSSetting without any parameters to pull ALL the BIOS settings from the current machine.

If you’re on a ThinkPad Laptop (T440 through T470, X240 through X270) it’ll output data like this


ComputerName    Setting                             CurrentValue                          OptionalValue
------------    -------                             ------------                          -------------
Leno-T470s AdaptiveThermalManagementAC         MaximizePerformance                   N/A
Leno-T470s AdaptiveThermalManagementBattery    Balanced                              N/A
Leno-T470s AlwaysOnUSB                         Enable                                N/A
Leno-T470s AMTControl                          Enable                                N/A
Leno-T470s BIOSPasswordAtBootDeviceList        Disable                               N/A
Leno-T470s BIOSPasswordAtReboot                Disable                               N/A
Leno-T470s BIOSPasswordAtUnattendedBoot        Enable                                N/A
Leno-T470s BIOSUpdateByEndUsers                Enable                                N/A
Leno-T470s BluetoothAccess                     Enable                                N/A
Leno-T470s BootDeviceListF12Option             Enable                                N/A
Leno-T470s BootDisplayDevice                   LCD                                   N/A
Leno-T470s BootMode                            Quick                                 N/A
Leno-T470s BootOrder                           USBCD:USBFDD:NVMe0:HDD0:USBHDD:PCILAN N/A
...
Leno-T470s USBPortAccess                       Enable                                N/A
Leno-T470s VirtualizationTechnology            Disable                               N/A
Leno-T470s VTdFeature                          Disable                               N/A
Leno-T470s WakeByThunderbolt                   Enable                                N/A
Leno-T470s WakeOnLAN                           ACOnly                                N/A
Leno-T470s WakeOnLANDock                       Enable                                N/A
Leno-T470s WiGig                               Enable                                N/A
Leno-T470s WiGigWake                           Disable                               N/A
Leno-T470s WindowsUEFIFirmwareUpdate           Enable                                N/A
Leno-T470s WirelessAutoDisconnection           Disable                               N/A
Leno-T470s WirelessLANAccess                   Enable                                N/A
Leno-T470s WirelessWANAccess                   Enable                                N/A

However if you’re on a ThinkCentre, like a M93, M900, M910 it’ll output something like this


ComputerName    Setting                                   CurrentValue                                                                                                              OptionalValue
------------    -------                                   ------------                                                                                                              -------------
Leno-M910 After Power Loss                          Last State                                                                                                                      Power On,Power Off,Last State
Leno-M910 Alarm Date(MM/DD/YYYY)                    [01/01/2016][Status:ShowOnly]                                                                                                   N/A
Leno-M910 Alarm Day of Week                         Sunday                                                                                                                          Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,SaturdayStatus:ShowOnly
Leno-M910 Alarm Time(HH:MM:SS)                      [00:00:00][Status:ShowOnly]                                                                                                     N/A
Leno-M910 Allow Flashing BIOS to a Previous Version Yes                                                                                                                             No,Yes
Leno-M910 Automatic Boot Sequence                   Network 1:M.2 Drive 1:PCIE4X_1 Drive:PCIE16X_1 Drive:SATA 1:SATA 2:SATA 3:SATA 4:Other Device                                   Network 2:Network 3:Network 4:USB FDD:USB HDD:USB CDROM:USB KEY
Leno-M910 Boot Agent                                PXE                                                                                                                             Disabled,PXE
Leno-M910 Boot Up Num-Lock Status                   On                                                                                                                              Off,On
...
Leno-M910 User Defined Alarm Time                   [00:00:00][Status:ShowOnly]                                                                                                     N/A
Leno-M910 VT-d                                      Enabled                                                                                                                         Disabled,Enabled
Leno-M910 Wake from Serial Port Ring                Primary                                                                                                                         Primary,Automatic,Disabled
Leno-M910 Wake on LAN                               Automatic                                                                                                                       Primary,Automatic,Disabled
Leno-M910 Wake Up on Alarm                          Disabled                                                                                                                        Single Event,Daily Event,Weekly Event,Disabled,User Defined
Leno-M910 Wednesday                                 Disabled                                                                                                                        Disabled,EnabledStatus:ShowOnly
Leno-M910 Windows UEFI Firmware Update              Enabled                                                                                                                         Disabled,Enabled

I like that the Desktop’s provide valid configuration settings and I hope this is something that will one day make it over to the laptops.

My two favorite parts about this script:

  • Supports querying remote systems
  • Supports querying for all, single or multiple specific settings

The script isn’t perfect but it works for our needs on our systems but I’m interested in hearing from you.

Function: Get-LenovoBIOSSetting

Usage

Get-LenovoBIOSSetting

Get-LenovoBIOSSetting -Setting Wi%

Get-LenovoBIOSSetting -ComputerName Leno-T470 -Setting 'Fingerprint%'

Get-LenovoBIOSSetting -ComputerName Leno-M900,Leno-M910 -Setting 'USB%','C State Support','Intel(R) Virtualization Technology'

Full code below


#region Function Get-LenovoBIOSSetting
Function Get-LenovoBIOSSetting
    {
        [cmdletbinding()]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string[]]$ComputerName = $env:computername,

                [Parameter(Mandatory=$false)]
                    [string[]]$Setting
            )

        Begin { [System.Collections.ArrayList]$BIOSSetting = @() }

        Process
            {
                foreach($Computer in $ComputerName)
                    {
                        if($Setting)
                            {
                                Foreach($Item in $Setting)
                                    {
                                        Try
                                            {
                                                $arrCurrentBIOSSetting = gwmi -class Lenovo_BiosSetting -namespace root\wmi -Filter "CurrentSetting like '$Item,%'" -ComputerName $Computer -ErrorAction Stop | ? { $_.CurrentSetting -ne "" } | Select -ExpandProperty CurrentSetting
                                                Foreach($CurrentBIOSSetting in $arrCurrentBIOSSetting)
                                                    {
                                                        [string]$CurrentItem = $CurrentBIOSSetting.SubString(0,$($CurrentBIOSSetting.IndexOf(',')))
                                                
                                                        if($CurrentBIOSSetting.IndexOf(';') -gt 0) { [string]$CurrentValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(',')+1),$CurrentBIOSSetting.IndexOf(';')-$($CurrentBIOSSetting.IndexOf(',')+1)) }
                                                        else { [string]$CurrentValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(',')+1)) }
                                                
                                                        if($CurrentBIOSSetting.IndexOf(';') -gt 0)
                                                            {
                                                                [string]$OptionalValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(';')+1))
                                                                [string]$OptionalValue = $OptionalValue.Replace('[','').Replace(']','').Replace('Optional:','').Replace('Excluded from boot order:','')
                                                            }
                                                        Else { [string]$OptionalValue = 'N/A' }
                                                                                
                                                        $BIOSSetting += [pscustomobject]@{ComputerName=$Computer;Setting=$CurrentItem;CurrentValue=$CurrentValue;OptionalValue=$OptionalValue;}
                                                
                                                        Remove-Variable CurrentItem,CurrentValue,OptionalValue -ErrorAction SilentlyContinue -WhatIf:$false
                                                    }
                                                Remove-Variable arrCurrentBIOSSetting -ErrorAction SilentlyContinue -WhatIf:$false
                                            }
                                        Catch { Write-Output "ERROR: UNABLE TO QUERY THE BIOS VIA CLASS [Lenovo_BiosSeting] ON [$Computer] - POSSIBLE INVALID SETTING SPECIFIED [$Item][$CurrentItem]: $_"; throw }
                                    }
                            }
                        Else
                            {
                                Try
                                    {
                                        $arrCurrentBIOSSetting = gwmi -class Lenovo_BiosSetting -namespace root\wmi -ComputerName $Computer -ErrorAction Stop | ? { $_.CurrentSetting -ne "" } | Select -ExpandProperty CurrentSetting
                                        Foreach($CurrentBIOSSetting in $arrCurrentBIOSSetting)
                                            {
                                                [string]$CurrentItem = $CurrentBIOSSetting.SubString(0,$($CurrentBIOSSetting.IndexOf(',')))
                                        
                                                if($CurrentBIOSSetting.IndexOf(';') -gt 0) { [string]$CurrentValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(',')+1),$CurrentBIOSSetting.IndexOf(';')-$($CurrentBIOSSetting.IndexOf(',')+1)) }
                                                else { [string]$CurrentValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(',')+1)) }
                                                                        
                                                if($CurrentBIOSSetting.IndexOf(';') -gt 0)
                                                    {
                                                        [string]$OptionalValue = $CurrentBIOSSetting.SubString($($CurrentBIOSSetting.IndexOf(';')+1))
                                                        [string]$OptionalValue = $OptionalValue.Replace('[','').Replace(']','').Replace('Optional:','').Replace('Excluded from boot order:','')
                                                    }
                                                Else { [string]$OptionalValue = 'N/A' }
                                                                                                                        
                                                $BIOSSetting += [pscustomobject]@{ComputerName=$Computer;Setting=$CurrentItem;CurrentValue=$CurrentValue;OptionalValue=$OptionalValue;}
                                        
                                                Remove-Variable CurrentItem,CurrentValue,OptionalValue -ErrorAction SilentlyContinue -WhatIf:$false
                                            }
                                        Remove-Variable arrCurrentBIOSSetting -ErrorAction SilentlyContinue -WhatIf:$false
                                    }
                                Catch { Write-Output "ERROR: UNABLE TO QUERY THE BIOS VIA CLASS [Lenovo_BiosSeting] ON [$Computer]: $_"; throw }
                            }
                    }
            }
        
        End { $BIOSSetting }
    }
#endregion Function Get-LenovoBIOSSetting

Wrap It Up!

If you decide to try this and run into problems or have questions, please let me know as I’m generally interested in use cases and constructive feedback.

Thanks and Good Providence to you!

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, &amp;amp;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.

 

Backing up Recovery Keys to MBAM and AD During OSD

Scenario

As we prepared for our Windows 10 roll out, we had MBAM all setup and ready to go when a wise man suggested we backup the keys to AD too.  I was a little perplexed: In my mind this is redundant since that’s what MBAM is supposed to do.  Can’t we just trust MBAM to do its thing?  But then the same wise man dropped a statement that I totally agreed with:

“I don’t want to be the one to have to explain to our CIO that we have no way of unlocking some VIP’s machine.”

Neither did I.

Here’s a high level overview of how we setup MBAM during OSD.
It’s not the best way and it’s not the only way.  It’s just a way.

Prerequisites:

  1. Export your BitLocker registry settings from a properly configured machine
  2. Edit the export, set the ‘ClientWakeupFrequency‘ to something low like 5 minutes
  3. Edit the export, set the ‘StatusReportingFrequency‘ to something low like 10 minutes
  4. Package up the .REG file as part of your MBAM client installation
    • This could either be a true Package, but I would recommend an Application that runs a wrapper to import the registry configuration; or create an MST; or add it to the original MSI.

Task Sequence Setup

  1. Wait until the machine is in real Windows, not WinPE
  2. Install the MBAM client (obviously!)
  3. Reboot
  4. Stop the MBAM service – We need to do this so that the settings we make below take effect
  5. Set the MBAM service to start automatically without delay – Want to make sure it fires as soon as possible.
  6. Import your BitLocker registry settings you exported & edited
    • This is the real meat: Since GPO’s are not applied during OSD, your GPO policies won’t reach the machine during the imaging process.  This will ensure your policies are in play as soon as possible.
    • Most places don’t set the  ‘ClientWakeupFrequency‘ and/or ‘StatusReportingFrequency‘ values to something insanely low via GPO which is why we manually edited the .REG file.  If you left them at the default values, the keys wouldn’t get escrowed for a few hours due to the way the MBAM client works.
  7. Optional but Recommended: Switch to AES-XTS-256 by setting ‘EncryptionMethodWithXtsOs‘ in ‘HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\FVE‘ to ‘7
  8. Start the MBAM service
  9. Enable BitLocker using the MBAM Deployment Scripts
  10. Reboot the machine
  11. Continue with your normal imaging process

The Good

  • We’ve not run into machines with improper configurations.
  • Every machine is encrypted using
    • full disk encryption versus used space
    • leverages AES-XTS-256
  • Keys are quickly escrowed to both AD and MBAM.
  • It just works: Deployed with 1511, we’re moving to 1607 and IT is testing 1703.

The Bad

  • I couldn’t figure out how to perform full disk AES-XTS-256 encryption in WinPE so this has to happen when we’re in a real OS.
    • I tried setting the keys via the registry but didn’t bother editing WSF files or trying to reverse engineer what goes on in that step to see if I could make it work.
  • Encryption does NOT begin until after someone logs on.
  • Encryption takes a while (but not too long) on SSDs.

In Closing

I would really like to hear from others on this one.  Because it was – and has been working for a over a year now – we really couldn’t justify dedicating bandwidth to exploring this further.  So we left it as-is.  My brain would like to see it work ‘properly’ one day, but that’ll have to wait.

Good Providence to you!

PSA: Locating Bad/Corrupt Registry.POL Files

For several months – a good chunk of 2017 in fact – we’ve encountered machines where Group Policy failed as evidenced by the following on affected machines:

  • Seeing errors like the following when runing gpupdate /force:

    Computer policy could not be updated successfully.   The following errors were encountered:
    The processing of Group Policy failed.  Windows could not apply the registry-based settings for the Group Policy object LocalGPO.  Group Policy settings will not be resolved until this event is resolved.  View the event details for more information on the file name and path that caused the failure.
    To diagnose the failure, review the event log or run GPRESULT /H GPReport.html from the command line to access information about Group Policy results.

  • Event ID 1096 which itself would show ErrorCode 13 with an ErrorDescription of ‘The data is invalid.’ along with the problematic Registry.POL file.

This is a well known and well documented problem:

The cause: Unknown but we have some suspects.

The fix is easy: Delete or rename the problematic Registry.pol file, which so far is always in %SystemRoot%\System32\GroupPolicy\Machine

But that’s reactionary and we want to be as proactive as possible.

Enter: Test-IsRegistryPOLGood

I spent a bunch of time trying to figure out an intelligent manner of doing this, and after a lot of trial & error I was seeing inconsistent results.  After really getting into the weeds I wondered whether or not the structure/format of the file was documented.  Turns out it is: https://msdn.microsoft.com/en-us/library/aa374407(v=vs.85).aspx)

The header contains the REGFILE_SIGNATURE expressed as 0x67655250 which is 80, 82, 101, 103 in bytes:


[System.BitConverter]::GetBytes(0x67655250)

The header is in the first 4 bytes of the file so we read that and evaluate the bytes


Get-Content -Encoding Byte -Path 'path\to\someRegistry.pol' -TotalCount 4

On a good file, this returns an array consisting of 80, 82, 101, 103 which is exactly what we want.

On a bad file – or at least all the ones I had access to – it returned all zero’s.

To examine the file via PowerShell:


Function Test-IsRegistryPOLGood
    {
        [cmdletbinding()]
        Param
            (
                [Parameter(Mandatory=$false)]
                    [string[]]$PathToRegistryPOLFile = $(Join-Path $env:windir 'System32\GroupPolicy\Machine\Registry.pol')
            )

        if(!(Test-Path -Path $PathToRegistryPOLFile -PathType Leaf)) { return $null }

        [Byte[]]$FileHeader = Get-Content -Encoding Byte -Path $PathToRegistryPOLFile -TotalCount 4

        if(($FileHeader -join '') -eq '8082101103') { return $true } else { return $false }
    }

 

However, I wasn’t sure how we were going to implement this, so I explored doing this via VBScript and found ADO to be the ideal (only?) way:


Option Explici
Dim arrRegistryPolFiles
if(WScript.Arguments.Count &amp;amp;gt; 0) then
    arrRegistryPolFiles = Array(WScript.Arguments(0))
else
    arrRegistryPolFiles&amp;amp;nbsp; = Array(CreateObject("WScript.Shell").ExpandEnvironmentStrings("%WINDIR%") &amp;amp;amp; "\System32\GroupPolicy\Machine\Registry.pol")
end if

Dim POLFile
For each POLFile in arrRegistryPolFiles
    if (CreateObject("Scripting.FileSystemObject").FileExists(POLFile)) then

        if(Join(ReadBinaryData(POLFile,3),"") = "8082101103") then
            wscript.echo True &amp;amp;amp; vbtab &amp;amp;amp; POLFile
        else
            wscript.echo False &amp;amp;amp; vbtab &amp;amp;amp; POLFile
        end if
    end if
next

wscript.quit

Function ReadBinaryData(Required_File,Int_Byte_Count)
' https://docs.microsoft.com/en-us/sql/ado/reference/ado-api/stream-object-ado
' https://docs.microsoft.com/en-us/sql/ado/reference/ado-api/stream-object-properties-methods-and-events

Const adTypeBinary = 1

' Requires Read/Write, otherwise it fails with Operation is not allowed in this context.
Const adModeReadWrite = 3

Dim arrByteArray : arrByteArray = Array(-1)
With CreateObject("ADODB.Stream")
    .Mode = adModeReadWrite
    .Type = adTypeBinary
    .Open
    .LoadFromFile Required_File

    Dim i
    For i=0 To Int_Byte_Count
        arrByteArray(i) = AscB(.Read(1))
        ReDim Preserve arrByteArray(UBound(arrByteArray)+1)
    Next
    .Close
End With

ReadBinaryData = arrByteArray
end function

 

After testing this out on our known good and known bad registry.pol files, we cast our rod (the code) into a special script all machines run to see if we’d catch any fish and sure enough we did!

From there it was just a matter of deciding how to handle the bad files: Alert IT for manual remediation or fix it during execution.

Here’s something ‘odd’ to me: When I convert the bits 80, 82, 101, 103 into HEX, I get 50, 52, 65, 67 which is the reverse of 67655250. Does anyone know why that is? I think it has something to do with Intel processors being ‘Little Endian’, which results in the bytes in memory not appearing in the same order as they do when fetched into a register but I don’t know; That’s a more than little beyond me!

In any event, hopefully someone will find this as useful as we did!

Good Providence to you!

Practical Use: Manipulating XML Files With PowerShell

In my previous post I talked about getting values from XML files.  Today we’re going to update a value in an XML file.

As before, I’m not a PowerShell or XML guru, but what I’m going to cover below works for me and I can’t think of a reason it wouldn’t work for you.

We’re going to continue using the sample XML file provided by Microsoft.  Copy the XML content & paste into a new file, books.xml, & save it some place convenient.

Getting the Current Value

We want to change an author’s name from Stefan to Stephane.  First, let’s find the entries that we need to change:

[xml]$XML = Get-Content &quot;C:\Users\Julius\Downloads\books.xml&quot;
$XML.catalog.book | ? { $_.author -like '*stefan' }

XML-SetValue-001

Great only one result so we don’t need to loop!

Setting the New Value

We know the property we want is author so we can simply  do something like this to set the new value:


($XML.catalog.book | ? { $_.author -like &quot;*stefan&quot; }).author = &quot;Knorr, Stephane&quot;

But I find it helpful to do something like this instead

$Node = $XML.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

$Node = $XML.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

&quot;Old Author: {0}&quot; -f $Node.author

$Node.author = $Node.author.Replace('Stefan','Stephane')

&quot;New Author: {0}&quot; -f $Node.author

XML-SetValue-002.PNG

If you had multiple entries, as is the case with author Eva Corets, you could do this:

foreach($Result in ($XML.catalog.book | ? { $_.author -like &quot;Corets*&quot; }))
    {
        write-host &quot;Book #$($Result.id) Written by $($Result.author)&quot;
        $Result.author = 'Mendez, Eva'
        write-host &quot;Book #$(Result.id) Updated to $($Result.author)`r`n
    }

XML-SetValue-003.PNG

Saving the Updated XML

The save operation is super easy:

$XML.Save(&quot;C:\Users\Julius\Downloads\book.xml&quot;)

Done!
But I’m a big fan of having backup copies, so I opt for something like:


[string]$XMLFile = &quot;C:\Users\Julius\Downloads\books.xml&quot;
[xml]$XML = Get-Content $XMLFile
foreach($Result in ($XML.catalog.book | ? { $_.author -like &quot;Corets*&quot; }))
    {
        &quot;Book #{0} Written by {1}&quot; -f $Result.id,$Result.author
        $Result.author = 'Mendez, Eva'
        &quot;Book #{0} Updated to {1}`r`n&quot; -f $Result.id,$Result.author
    }
Copy-Item -Path $XMLFile -Destination &quot;$XMLFile.ORIG.$(Get-date -Format 'yyyymmdd_hhmmss')&quot;
$XML.Save($XMLFile)

 

Function Set-XMLValue

Try to think of this function a more of a framework than a complete solution.  I recently had to update a few different XML files, some of which were on a few hundred machines and used this to do the heavy lifting.

Function Set-XMLValue
    {
        Param
            (
                [Parameter(Mandatory=$true)]
                    [string]$XMLFile,

                [Parameter(Mandatory=$true)]
                    [string]$XMLTreePath,

                [Parameter(Mandatory=$true)]
                    [string]$Property,

                [Parameter(Mandatory=$false)]
                    $OldValue,

                [Parameter(Mandatory=$true)]
                    $NewValue,

                [Parameter(Mandatory=$true)]
                    [string]$NewFile = $null
            )

        if(!(Test-Path -Path $XMLFile -PathType Leaf)) { return 2 }

        [bool]$DoUpdate = $false

        Try
            {
                [xml]$XML = Get-Content $XMLFile

                $XMLPath = '$XML.' + $XMLTreePath

                Foreach($Node in (Invoke-Expression $XMLPath | ? { $_.$Property -ieq &quot;$OldValue&quot; }))
                    {
                        # Check to confirm that particular property exists
                        if([string]::IsNullOrEmpty($Node)) { Write-host &quot;ERROR: NO PROPERTY [$Property] FOUND CONTAINING ORIGINAL VALUE [$OldValue]&quot;; [int]$Return = 2 }

                        # Get current value from XML
                        $CurrValue = $Node.$Property

                        # Phase 1: Analysis of parameters and values

                        # Check if the old value was specified
                        if($OldValue)
                            {
                                # When the old value is specified, the script check if the current value matches the old value.

                                # If the current value matches old value and will need to be updated
                                if($CurrValue -eq $OldValue) { $DoUpdate = $true }

                                # If the current value doesn't match the old value but matches the new, its already up to date
                                Elseif($CurrValue -eq $NewValue) { [int]$Return = 0 }

                                # If the current value doesn't match the old or new value we won't change anything but return the current value
                                Else { [string]$Return = &quot;WARNING: The current value [$CurrValue] did not match the specified [$OldValue] so NO changes were made.&quot; }

                            }
                        # If an old value was not specified, we'll update regardless of the current value
                        Else
                            {
                                # If the current value doesn't match the new value it will need to be updated
                                if($CurrValue -ne $NewValue) { $DoUpdate = $true }

                                # If the current value matches the new value its already up to date
                                elseif($CurrValue -eq $NewValue) { [int]$Return = 0 }
                            }

                        # Phase 2: Performing the update if deemed necessary
                        If($DoUpdate -eq $true)
                            {
                                # Update value
                                $Node.$Property = [string]$NewValue

                                # If we're using a new file, we don't need to backup the original file.
                                if([string]::IsNullOrEmpty($NewFile)) { $XML.Save($NewFile) }
                                Else
                                    {
                                        # Backup existing XML
                                        Copy-Item -Path $XMLFile -Destination &quot;$XMLFile.ORIG.$(Get-date -Format 'yyyymmdd_hhmmss')&quot; -Force -ErrorAction Stop

                                        # Save new/updated XML (overwrite existing)
                                        $XML.Save($XMLFile)
                                    }

                                # Success!
                                [int]$Return = 0
                            }
                    }
            }
        Catch { Write-Warning &quot;ERROR DOING XML UPDATE OPERATION for [$XMLFile]: $_&quot;; [int]$Return = 1 }
        return $Return
    }

$UpdateResult = Update-XMLValue -XMLFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -XMLTreePath catalog.book -Property author -OldValue 'Knorr, Stefan' -NewValue 'Knorr, Stephane' -NewFile &quot;C:\Users\Julius\Downloads\newbooks.xml&quot;

write-host &quot;UpdateResult [$UpdateResult]&quot;

This may not be fancy, and arguably complicated, but if you’re dealing with multiple XML files, PowerShell can be a huge timesaver.

 

Good Providence!

Practical Use: Getting Values from XML Files

A number of applications we use in the organization have XML-based configuration files and occasionally the need arises to validate the settings from the configuration.  For example, we might want to verify that a user is pointed to the right server, or that a particular setting is set to X.  We could treat XML files the same way we would a typical text file, as seen here, but where’s the fun in that?

I’m not a PowerShell or XML guru, but what I’m going to cover below works for me and I can’t think of a reason it wouldn’t work for you.

To keep things simple, we’ll be working with a sample XML file provided by Microsoft.  Copy the XML content & paste into a new file, books.xml, & save it some place convenient.

Ingest An XML File

In order to get started, you need to pull in the XML into a variable for further manipulation as such:

[xml]$XML = Get-Content &amp;amp;quot;C:\Users\Julius\Downloads\books.xml&amp;amp;quot;
# OR
$XML = [xml](Get-Content &amp;amp;quot;C:\Users\Julius\Downloads\books.xml&amp;amp;quot;)

Six of one, half a dozen of another; whatever works for you.

If you type $XML you should see something to the effect of:

XML-GetValues-001

Browsing the Structure

Once your XML variable is populated, it’s time to walk the XML tree structure to find the key that contains the data you’re looking for; And in order to do that you have to know the tree structure.

Without going too deep into this

  • The first line is the XML prolog containing the version information
  • The second is the <catalog> and that is our root element or node
  • All of the <book> nodes are children of the root
  • Each <book> node has a number of children: author, title, genre, price, publish_date & description.

Keep in mind this is a pretty simple & basic XML example, so the structure doesn’t go too deep.  You may find that your application XML’s are several layers deep.

If I wanted to see all the books, I would use

$XML.catalog.book

Which would then list the books:

XML-GetValues-002.PNG

 

Locating the Right Data

If I wanted to find all books by written by authors Stefan Knorr and Eva Corets, I would use

$XMl.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

$XMl.catalog.book | ? { $_.author -like &quot;Corets*&quot; }

That would quickly narrow the scope:

XML-GetValues-003.PNG

Getting Specific Values

To grab key bits of details from Stefan Knorr’s book I could do

$XML.catalog.book | ? { $_.author -like &quot;*stefan&quot;} | select id,price,publish_date

# OR

$Node = $XML.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

$Node.id

$Node.price

$Node.publish_date

XML-GetValues-004.PNG

I try to be as specific as possible when searching for data.

 

Function Get-XMLValue

This function is more of a framework as it depends squarely on the XML you’re working with and what you want to retrieve.  I recently had to verify a setting on a few hundred machines, so I used a modified version of this function to do the heavy lifting.

The function below is geared to return the book description when supplied the ID.

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

                [Parameter(Mandatory=$true)]
                    [string]$XMLTreePath,

                [Parameter(Mandatory=$true)]
                    [string]$AnchorProperty,

                [Parameter(Mandatory=$true)]
                    [string]$AnchorValue,

                [Parameter(Mandatory=$true)]
                    [string]$Property
            )

        if(!(Test-Path -Path $XMLFile -PathType Leaf)) { return 2 }

        Try
            {
                [xml]$XML = Get-Content $XMLFile

                $XMLPath = '$XML.' + $XMLTreePath
                $Return = @()
                Foreach($Node in (Invoke-Expression $XMLPath | ? { $_.$AnchorProperty -ieq &quot;$AnchorValue&quot; })) { $Return += $Node.$Property }

                if(-not $Return.count -gt 0) { Write-host &quot;ERROR: NO PROPERTY [$Property] FOUND BASED ON QUERY: [$AnchorProperty] = [$AnchorValue]&quot;; [int]$Return = 2 }
            }
        Catch { Write-Warning &quot;ERROR RETRIEVING VALUE OR PROPERTY [$Property] BASED ON QUERY: [$AnchorProperty] = [$AnchorValue] FROM [$XMLFile]: $_&quot;; [int]$Return = $_ }
        return $Return
    }

I couldn’t think of an elegant way to retrieve the values I wanted without specifying some qualifiers.  For instance, if I wanted the price of all Eva Corets books

  • The AnchorPrperty would be: author
  • The AnchorValue would be: Corets, Eva
  • The Property would be price

This way I’m certain to get the results I’m looking for.

Is there a better way?  Maybe.  This is what I came up with that met my need.

Usage:

# These two work
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk108
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk107

# This deoesn't
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk100

It may not be the most elegant solution but I’m hoping it’ll at least point you in the right direction should the need arise to get data from XML files.

 

Good Providence to you!

Practical Use: Find & Replace in a Text File

A number of applications rely on simple text-based configuration files versus some proprietary format.  This makes editing files – ones that can’t simply be replaced via GPO/GPP or Login Script – really easy to update.

Back in my VBScript days I would likely

  1. Ingest the file via ReadAll()
  2. Check if it contains the value via InStr
  3. Replace(old_value,new_value)
  4. Write out the file with the updated content

I figured Get-Content was going to behave similarly but I discovered each line is its own separate object which meant iterating through the array for the content.  No big deal but something new and good to know.

The method for locating your text is important depending on what you’re searching for.

Locating X File

If it’s simple text, you can get away with either operator: -match or -like.

  • Match will simply return true or false and is geared towards regular expression based searches.
  • Like will return the actual objects that match.

So if you just want to know whether or not the file contains X, you could go either way.

[string]$File = 'C:\windows\Temp\ASPNETSetup_00000.log'
$OriginalContent = Get-Content -Path $File
[string]$Find = 'Vista'

# Search via Match
($OriginalContent | % { $_ -match $Find }) -contains $true

# Search via Like (similar concept)
($OriginalContent | % { $_ -like &quot;*$Find*&quot; }) - contains $true

But if you want to locate X and do something with it, -like is your friend.

#Like
[string]$File = 'C:\windows\Temp\ASPNETSetup_00000.log'
$OriginalContent = Get-Content -Path $File
[string]$Find = 'Vista'
$Results = $OriginalContent | % { $_ -like &quot;*$Find*&quot; }

 

I don’t want to get too deep into this, because it’s well documented elsewhere, but I just want to mention that if you’re searching for something that contains special characters some additional care is necessary.

If you’re using -like, you should be fine:

# Like
[string]$File = &quot;C:\WINDOWS\temp\ASPNETSetup_00000.log&quot;
$OrigContent = Get-Content -Path $File
[string]$Find = '\/\/I/\/D0WZ'

# Return true/false
($OrigContent | % { $_ -like $Find }) -contains $true

# Get the lines that match
$Results = $OriginalContent | % { $_ -like &quot;*$Find*&quot; }

 

But if you’re using -match, you’ll need to either escape those characters manually:

# Match
[string]$File = &quot;C:\WINDOWS\temp\ASPNETSetup_00000.log&quot;
$OriginalContent = Get-Content -Path $File
[string]$Find = '\\\/\\\/I\/\\\/D0WZ'
($OriginalContent | % { $_ -match $Find }) -contains $true

Or rely on the Escape() method of the Regex class:

# Match
[string]$File = &quot;C:\WINDOWS\temp\ASPNETSetup_00000.log&quot;
$OriginalContent = Get-Content -Path $File
[string]$Find = '\/\/I/\/D0WZ'

($OriginalContent | % { $_ -match [regex]::Escape($Find) }) -contains $true

Replacing the Content

Now that you’ve confirmed file X contains Y, its time to replace it.  Since humans are prone to making mistakes, I always like to have a way of backing out of programmatic changes like, so the steps below include a backup process.

# Replace&amp;nbsp;X with Y and store it in a new variable
$NewContent = $OriginalContent | % { $_ -replace $Find,$Replace }

# Create a new file that will ultimately replace the existing file.
#     If you want a UTF-8 file with BOM use this
#$NewContent | Out-File -FilePath &quot;$File.NEW&quot; -Encoding utf8 -Force

#     If you just want a UTF-8 without BOM, this does the trick.
$NewContent | Out-File -FilePath &quot;$File.NEW&quot; -Encoding default -Force

# Backup the existing file
Copy-Item -Path $File -Destination &quot;$File.ORIG.$(Get-date -Format 'yyyymmdd_hhmmss')&quot; -Force

# Move the new file that we staged to overwrite the orignal
Move-Item -Path &quot;$File.NEW&quot; -Destination $File -Force

To the experts, this is really simple and basic stuff.  To those less seasoned, this is practical. 🙂

Good Providence!

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!