# 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.

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
}
}

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:rnCurrent Value: [$($CurrentSettings.CurrentValue)]rnUpdated 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:rnCurrent Value: [$($CurrentSettings.CurrentValue)]rnUpdated 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:

• 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: ## 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

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.

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.

# 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.

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.

• 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.

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' }


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;



$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



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)rn }  # 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}rn&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:

# 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: # 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: # 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  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

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!

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.