Windows 10 Upgrade – Write Metrics from Registry to WMI

Like many SCCM Engineers in the End User Computing field I am working (bent on backwards, might I add) on creating a process to managing the bi-annual Windows 10 upgrades which can be repeated at least once a year. In my current environment we’re still in the early stages of testing our first upgrade where we’re looking to upgrade our estate from 1703 to 1803.

There are many ‘community champions’ (as I like to call them in my head) who have been very generous in sharing their knowledge and skills with the rest of us and thus taking out a lot of the guesswork and helping us along the way. Personally I am often inspired by their ideas which ignites a passion to do better and improve my own processes.

Sometimes I take a solution shared by a community champion and I use it as it is. Other times I have to make modifications or adapt the solution or script to suit my requirements. One such script is the SetOSDInfo.ps1 script (now renamed to Set-ComboInfo.ps1) from Gary Blok over at Garytown.com.

Recently Gary asked on Twitter if anybody’s modified or extended his script and I happened to mention my changes when he said he’d like to see it. Therefore this blog post is for Gary and anybody else who may find the script useful.

Before I post the script I’d like to take a moment to note down my reasons for modifying it. The original script from Gary is intended to be run in an OSD, Compatibility Scan and In-Place Upgrade task sequences which writes metrics to the registry and WMI. As Gary mentions in his blog post the functions which writes the data to the WMI is originally from Jason Sandy’s Set-OSDInfo script. I know the script works for many people as it is but I just couldn’t get it to work for myself, specifically the New-WMIClass function which kept throwing errors which I could not get past. Therefore I had to look elsewhere for inspiration to replace the New-WMIClass function and ended up also removing the New-WMIClassInstane function altogether. Apart from this there were a few other reasons why I had to modify the script to suit my requirements.

Firstly, I use the tattoo script from Jorgen Nilsson to write metrics to the registry during OSD, which I am very happy with, so I had no need for the OSD metrics from the script.

As for writing metrics during Compatibility Scan and IPU task sequences I prefer to write my metrics to the registry using Run Command Line steps within the Task Sequences as shown below:

thumbnail

I prefer this because I can visually see what metrics are being written in the task sequence editor without referring to the script. Also, when the time comes for me to hand over the process to my successor (whenever that time comes) I think this will help reduce some of the complexities in the task sequence. (Side note: I always endeavour to leave an environment in a better state than how I inherited it in the first place.) If I’m already writing the metrics to the registry in the task sequence then I did not need any portion of the script which writes the metrics to the registry again. Instead, what I needed was to collate the (already written) metrics from the registry and write these to WMI.

Lastly, the script requires you to create new variables with a prefix (New-Variable -Name “$($AttributePrefix)$Value” -Value $ID) which allows you to call the Get-Variable cmdlet (like $customAttributes = Get-Variable -Name “$AttributePrefix*”) that gives you an array object which you can loop through and write the data to registry and/or WMI. In my case my variables (or metrics) already start with a prefix – “CompatScan_” or “IPU_” hence I got rid of the need for the $AttributePrefix variable from the script.

I could probably expand a bit more on the script but I’m in a bit of a rush, especially since I promised Gary I’d get the script out there days ago.

Taking inspiration from Gary and Martin Bengtsson I plan on sharing my Compatibility Scan and In-Place Upgrade task sequence with the tech community as soon as I have refined my processes.

For now, here’s the scrip. You can also download it from this link.

[cmdletBinding()]

Param(

        [Parameter(Mandatory=$false)][String]$Namespace=emeneye,

        [Parameter(Mandatory=$false)][String]$Class=“IPU”,

        [Parameter(Mandatory=$false)][String]$Key=“Build”,

        [Parameter(Mandatory=$false)][String]$Build=“1803”,

        [Parameter(Mandatory=$false)][String]$RegPath=“HKLM:\Software\emeneye\Win10_Upgrade\1803″

)

 

# References:

# https://home.configmgrftw.com/configmgr-osd-information-script/

# https://garytown.com/collect-osd-ipu-info-with-hardware-inventory

# https://social.technet.microsoft.com/Forums/office/en-US/233a5cb3-ed1c-47f8-a495-a271f225a24e/writing-a-custom-wmi-class?forum=winserverpowershell

 

Clear-Host

 

######################################################################################################################################################

## Functions ##

######################################################################################################################################################

 

Function Get-WMINamespace {

[CmdletBinding()]

       Param(

        [Parameter(Mandatory=$false,valueFromPipeLine=$true)][string]$Namespace

       ) 

    Begin {

              Write-Host “Getting WMI namespace $Namespace

    }

    Process {

        If ($Namespace) {

            $filter = “Name = ‘$Namespace‘”

            $return = Get-WmiObject -Namespace “root” -Class “__namespace” -filter $filter

        }

        Else {

            $return = Get-WmiObject -Namespace root -Class __namespace

        }

    }

    End {

        Return $return

    }

}

 

Function New-WMINamespace {

[CmdletBinding()]

       Param(

        [Parameter(Mandatory=$true,valueFromPipeLine=$true)][string]$Namespace

       )

 

       If (!(Get-WMINamespace -Namespace $Namespace)) {

              $newNamespace = “”

              $rootNamespace = [wmiclass]root:__namespace

        $newNamespace = $rootNamespace.CreateInstance()

              $newNamespace.Name = $Namespace

              $newNamespace.Put(| out-null

            

              Write-Host “Namespace $($Namespace) created.”

       } Else {

              Write-Host “Namespace $($Namespace) is already present. Skipping..”

       }

}

 

Function Get-WMIClass {

[CmdletBinding()]

       Param(

              [Parameter(Mandatory=$false,valueFromPipeLine=$true)][string]$Class,

        [Parameter(Mandatory=$false)][string]$Namespace = “cimv2”

       ) 

    Begin {

              Write-Host “Getting WMI class $Class

    }

    Process {

              If (Get-WMINamespace -Namespace $Namespace) {

                     $namespaceFullName = “root\$Namespace

 

            Write-Host $namespaceFullName

            

                     If (!$Class) {

                           $return = Get-WmiObject -Namespace $namespaceFullName -Class * -list

                     } Else {

                           $return = Get-WmiObject -Namespace $namespaceFullName -Class $Class -list

                     }

              }

              Else {

                     Write-Host “WMI namespace $Namespace does not exist.”

                     $return = $null

              }

    }

    End {

        Return $return

    }

}

 

Function New-WMI-Class {

[CmdletBinding()]

       Param(

              [Parameter(Mandatory=$false,valueFromPipeLine=$true)][string]$Class,

        [Parameter(Mandatory=$false)][string]$Namespace,

        [Parameter(Mandatory=$false)][System.Management.Automation.PSVariable[]]$Attributes,

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

       )

  

    # Create the Class

    $newClass = New-Object System.Management.ManagementClass(“root\$Namespace, [String]::Empty, $null);

    $newClass[“__CLASS”] = $Class;

 

    # Add the Key property to the WMI Class

    $newClass.Properties.Add($($Key), [System.Management.CimType]::String, $false)

    $newClass.Properties[$($Key)].Qualifiers.Add(“key”, $true)

    $newClass.Properties[$($Key)].Qualifiers.Add(“read”, $true)

 

    ForEach ($Attr in $Attributes) {

 

        Set the property value to the attribute name

        $Property = $Attr.Name

 

        # Remove the attribute prefix and underscores from the property name

        $Property = $Property -replace “_”,“”

      

        If ($Property eq “(default)”) {

            # Do nothing

        } Else {

            # Add the property to the WMI class, but not the Key which is already added above

            If (-not (($Property eq $Key))) {

                # Add the property to the WMI Class

                $newClass.Properties.Add($Property, [System.Management.CimType]::String, $false)

 

                Set the Read qualifier to the property

                $newClass.Properties[$Property].Qualifiers.Add(“read”, $true)

            }

        }

    }

 

    $newClass.Put()

}

 

Function Get-PropertiesAndValuesFromRegistry {

[CmdletBinding()]

       Param

    (

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

       )

 

    Begin { 

        Push-Location

    }

 

    Process {

        Set-Location $RegPath

 

        $AllPropertiesInRegPath = Get-Item $RegPath | Select-Object ExpandProperty Property

 

        $Object = New-Object TypeName PSObject

 

        ForEach ($Property in $AllPropertiesInRegPath) {

            Add-Member InputObject $Object MemberType NoteProperty -Name $Property -Value (Get-ItemProperty -Path $RegPath -Name $Property).$Property

        }

 

        return $Object.PSObject.Properties

      

    }

    End {

        Pop-Location

    }

}

 

######################################################################################################################################################

## Write CompatScan Return Code and Error Message to Registry ##

######################################################################################################################################################

 

If ($Class eq CompatScan) {

    $TSEnv = New-Object ComObject Microsoft.SMS.TSEnvironment

 

    [int64]$decimalreturncode = $TSEnv.Value(“_SMSTSOSUpgradeActionReturnCode)

    $hexreturncode = “{0:X0}” -f [int64]$decimalreturncode

    #[int64] $hexreturncode = 0xC1900210

 

    $WinIPURet = @(

    @{ Err = “C1900210”Msg = ‘No compatibility issues.’}

    @{ Err = “C1900208”Msg = ‘Incompatible apps or drivers.’ }

    @{ Err = “C1900204”Msg = ‘Selected migration choice is not available.’ }

    @{ Err = “C1900200”Msg = ‘Not eligible for Windows 10.’ }

    @{ Err = “C190020E”Msg = ‘Not enough free disk space.’ }

    @{ Err = “C1900107”Msg = ‘Unsupported Operating System.’ }

    @{ Err = “80070652”Msg = ‘Previous Install Pending, Reboot.’ }

    @{ Err = “8024200D”Msg = ‘Update Needs to be Downloaded Again.’ }

    @{ Err = “0”Msg = ‘Windows Setup completed successfully.’ }

    )

 

    $ErrorMsg = $winipuret | ? err eq $hexreturncode  | % Msg

 

    # Write hexreturncode and errormsg to registry

    New-ItemProperty -Path $RegPath -Name CompatScan_HexReturnCode -Value $hexreturncode PropertyType String -Force

    Set-ItemProperty -Path $RegPath -Name CompatScan_ErrorMessage -Value $ErrorMsg

}

 

######################################################################################################################################################

## Write Username to Registry ## – This is the username of the person who kicks off the installation from Software Center

                              ## – See Gary Blok’s post at # https://garytown.com/gather-user-account-name-during-ipu

######################################################################################################################################################

 

If ($Class eq “IPU”) {

    If ($TSEnv.Value(“_SMSTSUserStartedeq $true) {

        # Get the username from the task sequence variable, which is set earlier in the task sequence

        # $UserAccount =$env:USERNAME

        $UserAccount = $TSEnv.Value(IPU_UserAccount)

 

        # Write the username to the registry

        New-ItemProperty -Path $RegPath -Name IPU_UserAccount -Value $UserAccount PropertyType String -Force

    }

}

 

######################################################################################################################################################

## Collate metrics from registry and set variables ##

######################################################################################################################################################

 

$PropertiesAndValuesFromRegistry = Get-PropertiesAndValuesFromRegistry RegPath $RegPath

 

$PropertiesAndValuesFromRegistry | Where-Object {$_.Name -like $Class*”| ForEach-Object {

    Set the variable name and value

    $Name = $_.Name

    $Value = $_.Value

 

    # Remove the variable if it already exists

    Remove-Variable -Name $Name ErrorAction SilentlyContinue

  

    # Create new variable

    New-Variable -Name $Name -Value $Value

}

 

######################################################################################################################################################

## Create the WMI Namespace and Class if they do not already exist ##

######################################################################################################################################################

 

# Create WMI Namespace

If (-not (Get-WMINamespace -Namespace $Namespace)) {

    # Namespace does not exist, proceed to create Namespace

    Try {

        New-WMINamespace -Namespace $Namespace

    } Catch {

        Write-Host “Error creating WMI namespace $Namespace

    }

}

 

# Create WMI Class

If (-not (Get-WMIClass -Class $Class -Namespace $Namespace)) {

    # Class does not exist

  

    # Collate all attributes

    $Attributes = Get-Variable -Name $Class*”

 

    Now create the WMI class

    New-WMI-Class -Class $Class -Namespace $Namespace -Attributes $Attributes -Key “Build”

Else {

    # Class does exist

  

    If an instance already exists with the same Build then delete the instance

    $Instance = Get-CimInstance ClassName $Class -Namespace root/$Namespace | Select Build ErrorAction SilentlyContinue

    If ($Instance.Build eq $Build) {

        Get-CimInstance ClassName $Class -Namespace root/$Namespace | Remove-CimInstance

    }

}

 

######################################################################################################################################################

## Collate all variables and create a WMI Class Instance

######################################################################################################################################################

 

# Get all attributes

$Attributes = Get-Variable -Name $Class*”

 

# Create empty hash table

$PropertyHash = @{}

 

# Loop through each attribute and populate the hash table

ForEach ($Attr in $Attributes) {

    Set the attribute name and clean it up – underscores and dashes causes the Set-WMIInstance cmdlet to fail

    $AttrName = $Attr.Name

    $AttrName = $AttrName -replace “_”,“”

    $AttrName = $AttrName -replace “-“,“”

 

  

    Set the attribute value

    $AttrValue = $Attr.Value

 

    # Add the attribute name and value to the hash table

    $PropertyHash.Add($($AttrName),$($AttrValue))

}

 

# Add the key value

$PropertyHash.Add($($Key),$($Build))

 

# Remove the (default) property – this errors out otherwise

$PropertyHash.Remove(“(default)”)

 

Create new WMI Class Instance, i.e. populate the class

Set-WmiInstance -Path \\.\root\$($Namespace):$Class -Arguments $PropertyHash

Leave a comment