SCCM: In-Place Upgrade Restart Computer Issue

At first I wasn’t going to post anything about this…then I swallowed my pride and hope this would assist the “greater good”.

We went through TAP with Windows 10 and (of course) used the Upgrade Task Sequence Sample that Microsoft provided. It never really quiet worked right, but for our little pilot (~100 machines) it did it’s job. Recently, we went through our big jump to SCCM 1606 directly from 2012 R2 SP2 and now that we were “supported” with In-Place Upgrades I wanted to go through and update the Task Sequences we had been using to an official process that we could safely offer out the remaining 60k+ systems still running Windows 10 RTM and below.

I updated the Task Sequence, got all the custom steps in place where they needed to be but saw failure after failure. Digging in the logs I saw this:

Execution engine result code: Reboot(2)

Oddly enough this was coming from a “Restart Computer” step and as we all know Windows error 2 typically means “File not found”. Even more odd, if I’d reboot the computer, the Task Sequence would pick back up and resume right where it left off. I ran through various other workarounds including a “Run Command Line” doing a shutdown -r, as well as trying Variable dumps, procmon traces, etc…

When I brought up the issue to some of our SCCM contacts at Microsoft, they too were clueless except for Rob’s comment: “I know they changed some of the reboot handling with the new client in 1606…”. Knowing that these test systems probably didn’t have the new client, I reverted the OS, and what do you know… Still running the 2012 R2 SP2 client…I updated the client, re-ran the In Place Upgrade Task Sequence and was met with 100% success!

It’s one of those things you feel so stupid for missing, yet you know you’ll think twice as hard the next time! Since I wasn’t able to find this via any normal search engine, I’m hoping you all are much smarter than I am! Hopefully this saves someone some troubleshooting!

 

Enjoi!

Task Sequence Powershell Module

Awhile ago I submitted a request to Microsoft for a built-in Powershell Module during the Task Sequence. Mostly for basic things like getting and setting variables, but once I noticed what all the COM Objects exposed, I wanted to take this a step further.

Primarily you’ll have 2 COM objects you can create during a Task Sequence: Microsoft.SMS.TSEnvironment (getting/setting variables) and Microsoft.SMS.TSProgressUI (manipulating the Progress and messages). I’ve uploaded the module for you all to use at my github. I’ll run though some of the details below including visuals for the TSProgressUI items. Some, if not most, of these are-for lack of a more appropriate label-useless; but can still be fun to mess around with!

https://github.com/ndswanson/TaskSequenceModule

Microsoft.SMS.TSEnvironment

Confirm-TSEnvironment

This function is not exported, as it is used within the module to verify that the Microsoft.SMS.TSEnvironment COM Object is loaded before attempting any operations.

Get-TSVariables

Calls “GetVariables()” on the COM Object. This will return an array of all Task Sequence Variable NAMES only.

Get-TSValue

Calls “Value()” on the COM Object to return the value of a specific variable. the “Name” Parameter should be the Task Sequence Variable you want the value for.

Set-TSVariables

Assigns a value to a specific variable. A Boolean return indicates success. Note this is wrapped in a try/catch in the event you attempt to set a read-only variable.

Get-TSAllValues

Returns an object of all Task Sequence Variables and their assigned Values.

Microsoft.SMS.TSProgressUI

Confirm-TSProgressUISetup

This function is not exported, as it is used within the module to verify that the Microsoft.SMS.TSProgressUI COM Object is loaded before attempting any operations.

Show-TSActionProgress

Calls “ShowActionProgress()” on the COM object. This is the function I use 99.9% of the time for UI related actions. This will add the “second” progress bar and update the Text for that progress bar as well as fill the progress meter as you see fit. The neat thing here, is that the “Step” and “MaxStep” will automatically show a “100%” representation. For example, if i have 8 of 400 complete, I don’t have to do the extra scripting to pass in “2” and “100”. I can pass in 8, and 400, and the COM Object does the math for us.

There are other parameters here with “ShowActionProgress()”. With this function, those use the built-in variables “designed” to be used here. If you want to extend this function to allow those additional parameters, feel free. Personally, I don’t find them useful enough to include.

Step Param set to 5, MaxStep Param set to 10.
Show-TSActionProgress

Close-TSProgress

Calls “CloseProgressDialog()” on the COM object. Closes or “hides” the progress bar window. This will automatically reappear once the current step has completed, or if you call one of the “show” functions for the UI.

Show-TSProgress

Calls “ShowTSProgress” on the COM object. This function manipulates the “Top part” of the progress bar. If, for whatever reason, you wanted to manipulate the progress bar without adding the “second/bottom” bar, this is what you should use.

Step Param set to 5, MaxStep Param set to 10.
Show-TSProgress

Show-TSErrorDialog

Calls “ShowErrorDialog()” on the COM object. This wills how the typical “Error in the Task Sequence” Window. There are parameters for the error title, error message/details, exit code, a timeout, and a force reboot. The force reboot will trigger a reboot after the timeout (in seconds) has expired.

Show-TSErrorDialog

Show-TSMessage

Calls “ShowMessage()” on the COM Object. This essentially shows a MessageBox allowing you to specify the button configuration. The interesting (and somewhat useless) part of this, is that you do not receive a return value. If you want a simple “OK” message box, this should work, otherwise I’d recommend using a Normal MessageBox.

Button Options (Type Parameter):

  • 0 = OK
  • 1 = OK, Cancel
  • 2 = Abort, Retry, Ignore
  • 3 = Yes, No, Cancel
  • 4 = Yes, No
  • 5 = Retry, Cancel
  • 6 = Cancel, Try Again, Continue

Type Param set to 0.
Show-TSMessage

Show-TSRebootDialog

Calls “ShowRebootDialog()” on the COM Object. This will allow you to present a custom reboot window. I would recommend against this, as untimely reboots during a task sequence can cause some issues. This is essentially what causes the issue where Updates cause Double Reboots during OSD. If you do need a reboot after a step, either inset a restart computer step in your task sequence or just have your script exit “3010”.

Typically OrganzationName will use the TS Variable “_SMSTSOrgName”.
Show-TSRebootDialog

Show-TSSwapMediaDialog

Calls “ShowSwapMediaDialog()” on the COM Object. If you’ve ever used standalone media that was broken up into multiple CD/DVD sets (or played a Final Fantasy on Playstation), you’ve seen this one. This is a message indicating for you to swap media, letting you know what disk number to insert.

Show-TSSwapMediaDialog

 

Enjoi!

Configuration Manager: Content Location Requests

Not that long ago I was challenged with the task of writing a piece of automation for a ConfigMgr Client to check if a specific package was available in it’s boundary; or commonly referred to as a “Content Location Request”. All I was able to find was Robert Marshall’s [MVP] postings for Content Location Request (CLR) Generator (and the Console Edition). The hints at the ConfigMgr SDK we’re clear, so I went ahead and installed it.

To get a jump start I began looking for examples and yet again found a post by Robert Marshall.

The best part here is this works with SCCM 2012 AND the latest Technical Preview that Microsoft has released for Configuration Manager vNext!

So, here we go! Content Location Request’s using C# or Powershell!

With the ConfigMgr SDK installed we’ll be focusing on the Client Messaging SDK: C:\Program Files (x86)\Microsoft System Center 2012 R2 Configuration Manager SDK\Samples\Client Messaging SDK. Inside this folder you’ll see a Sample.cs file and help file (most important!). if you decide to go beyond what I do here, you’ll want to keep the help file close by! Navigating up and in the Redistributables folder you’ll find the Microsoft.ConfigurationManagement.Messaging.dll.

Loading Files

If using Visual Studio, add a reference to the Messaging dll and the appropriate using reference. While you’re at it add a using statement for System.Diagnostics if one does not already exist.

C#

using System.Diagnostics;
using Microsoft.ConfigurationManagement.Messaging.Framework;
using Microsoft.ConfigurationManagement.Messaging.Messages;
using Microsoft.ConfigurationManagement.Messaging.Sender.Http;

Powershell

[void] [Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
[void] [Reflection.Assembly]::LoadFile("c:\Microsoft.ConfigurationManagement.Messaging.dll")

When using the “LoadFile” method, you’ll need to put in the full path (no variables). In most cases this is an issue, especially if including the dll within the script path. Best way I’ve found is to us the Add-Type cmdlet (where a variable populated path is allowed) in a try block, then have a catch statement to copy the file and then call LoadFile. If using this in WinPE, you’ll NEED to do this due to the loadFromRemoteSouces default setting.

Powershell if anticipating WinPE:

$Script:CurrentDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition

try
{
   Add-Type -Path "$Script:CurrentDirectory\Utilities\Microsoft.ConfigurationManagement.Messaging.dll" | out-null
}
catch
{
   Copy-Item -Path "$Script:CurrentDirectory\Utilities\Microsoft.ConfigurationManagement.Messaging.dll" -Destination "x:\Microsoft.ConfigurationManagement.Messaging.dll" | Out-Null
   [void] [Reflection.Assembly]::LoadFile("x:\Microsoft.ConfigurationManagement.Messaging.dll") | out-null
}
[Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")

Setting up Objects

Now that we have the necessary references and using statements we’ll need to create the objects used through the request:

C#:

HttpSender hSender = new HttpSender();

ConfigMgrContentLocationRequest clr = new ConfigMgrContentLocationRequest();

ConfigMgrContentLocationReply cmReply = new ConfigMgrContentLocationReply();

Powershell:

$httpSender = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Sender.Http.HttpSender

$clr = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ConfigMgrContentLocationRequest

$cmReply = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationReply

Setting Up the Request

Now with our objects setup, we can begin formulating the request. The first thing we’ll need to do is run the “Discover” method of the ConfigMgrContentLocationRequest object. Essentially this grabs the systems IP Addresses, Current AD Site, AD Domain and AD Forest.

Nothing too difficult right? Well, not exactly! The AD Domain and AD Forest discovery uses the ADSI COM object. Still no problem though, right? Well, not if you intend to use this within WinPE. We’ve got 2 options then: (1) Add the ADSI Plugin to our WinPE image (Thanks to Johan Arwidmark), or (2) build a try catch.

Since the ADSI plugin is not included with WinPE and not supported by Microsoft, we’ll just use a simple try/catch. However, there’s another catch, we’ll need to specify the domain and forest strings.

C#:

private string myDomain = "mydomain.com";
private string myForest = "myForest.com";

try
{
   clr.Discover();
}
catch
{
clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses();
   clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite();
   clr.LocationRequest.Domain = new ContentLocationDomain() { Name = myDomain };
   clr.LocationRequest.Forest = new ContentLocationForest() { Name = myForest };

}

Powershell:

[string]$myDomain = "mydomain.com"
[string]$myForest = "myforest.com"

try
{
   $clr.Discover()
}
catch
{
   $clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses()           $clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite()
   $clr.LocationRequest.Domain = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationDomain
   $clr.LocationRequest.Domain.Name = $myDomain
   $clr.LocationRequest.Forest = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationForest
   $clr.LocationRequest.Forest.Name = $myForest
}

Next we’ll need to setup the parameters for the request. For this we’ll need a Management Point fqdn, Management Point Site Code, Package ID, and Package Source Version.

C#:

clr.SiteCode = mySiteCode;
clr.Settings.HostName = myManagementPoint;
clr.LocationRequest.Package.PackageId = packageID;
clr.LocationRequest.Package.Version = packageVersion;

Powershell:

$clr.SiteCode = $ManagementPointSiteCode
$clr.Settings.HostName = $ManagementPoint
$clr.LocationRequest.Package.PackageId = $PackageID
$clr.LocationRequest.Package.Version = $PackageVersion

Now, perhaps one of my favorite things here, is that we can manually set the AD Site code and get a location request as if we were at a different location. This is especially nice if we don’t have access to systems in those specific AD Sites and want to test!

C#:

if (myCustomAdSite != null)
{
   clr.LocationRequest.ContentLocationInfo.ADSite = myCustomAdSite;
}

Powershell:

if ($myCustomAdSite -ne $null)
{
   $clr.LocationRequest.ContentLocationInfo.ADSite = $myCustomAdSite
}

Next we’ll perform a validate on the message (ConfigMgrContentLocationRequest). This call validates that the message we have setup is good. If Validate fails, when the message is sent, it will undoubtedly fail as well. After validate we’ll send the request.

C#:

clr.Validate((IMessageSender)hSender);
cmReply = clr.SendMessage(hSender);

Powershell:

$clr.Validate([Microsoft.ConfigurationManagement.Messaging.Framework.IMessageSender]$httpSender)
$cmReply = $clr.SendMessage($httpSender)

Now we have our returned result! It will be returned as a string but in XML format. We can easily cast this into an XML object and do whatever we wish! Before returning the value, I’ve had some issues casting this due to a trailing  ” ” (Space) at the end of the string. Below is a quick loop that’ll take care of that, should it exist.

C#:

string response = cmReply.Body.Payload.ToString();

while (response[response.Length - 1] != '>')
{
   response = response.TrimEnd(response[response.Length - 1]);
}

Console.WriteLine(response);

Powershell:

$response = $cmReply.Body.Payload.ToString()

while($response[$response.Length -1] -ne '>')
{
   $response = $response.TrimEnd($response[$response.Length -1])
}

 Write-Output $response

Now we’ve got a nice neat Content Location Request! As you see below there’s quite a bit of information here. My primary accomplishment was checking that “SiteLocality” was “LOCAL” and there were LocationRecords.

Config Mgr 2012 R2 SP1:

<ContentLocationReply SchemaVersion="1.00">
   <ContentInfo PackageFlags="1073741825">
      <ContentHashValues/>
   </ContentInfo>
   <Sites>
      <Site>
         <MPSite IISSSLPreferedPort="443" IISPreferedPort="80" SiteLocality="LOCAL" MasterSiteCode="P01" SiteCode="P01"/>
         <LocationRecords>
            <LocationRecord>
               <ADSite Name="MyADSite"/>
               <IPSubnets>
                  <IPSubnet Address="xxx.xxx.xxx.xxx"/>
                  <IPSubnet Address=""/>
               </IPSubnets>
               <Metric Value=""/>
               <Version>8239</Version>
               <Capabilities SchemaVersion="1.0">
                  <Property Name="SSLState" Value="0"/>
               </Capabilities>
               <ServerRemoteName>myServer.Domain.com</ServerRemoteName>
               <DPType>SERVER</DPType>
               <Windows Trust="0"/>
               <Locality>LOCAL</Locality>
            </LocationRecord>
         </LocationRecords>
      </Site>
   </Sites>
   <RelatedContentIDs/>
</ContentLocationReply>

Config Mgr Technical Preview 4 (v1511):

<ContentLocationReply SchemaVersion="1.00">
   <BoundaryGroups BoundaryGroupListRetrieveTime="2015-12-06T22:19:36.400">
      <BoundaryGroup GroupID="16777217"/>
   <BoundaryGroups/>
   <ContentInfo PackageFlags="16777216">
      <ContentHashValues/>
   </ContentInfo>
   <Sites>
      <Site>
         <MPSite IISSSLPreferedPort="443" IISPreferedPort="80" SiteLocality="LOCAL" MasterSiteCode="P01" SiteCode="P01"/>
         <LocationRecords>
            <LocationRecord>
               <URL Signature="http://myServer.Domain.com/SMS_DP_SMSSIG$/XXX#####" Name="http://myServer.Domain.com/SMS_DP_SMSPKG$/XXX#####"/>
               <ADSite Name="MyADSite"/>
               <IPSubnets>
                  <IPSubnet Address="xxx.xxx.xxx.xxx"/>
                  <IPSubnet Address=""/>
               </IPSubnets>
               <Metric Value=""/>
               <Version>8325</Version>
               <Capabilities SchemaVersion="1.0">
                  <Property Name="SSLState" Value="0"/>
               </Capabilities>
               <ServerRemoteName>myServer.Domain.com</ServerRemoteName>
               <DPType>SERVER</DPType>
               <Windows Trust="0"/>
               <Locality>LOCAL</Locality>
            </LocationRecord>
         </LocationRecords>
      </Site>
   </Sites>
   <RelatedContentIDs/>
</ContentLocationReply>

These seem to differ ever so slightly from 2012 R2 (SP1) to v1511. There’s an added “BoundaryGroups” element and “URL Signature” Element as well. However, nothing that would (at least right now) prevent this code from running and working!

 

Full Code

C#:

// Set up the objects
HttpSender hSender = new HttpSender();

ConfigMgrContentLocationRequest clr = new ConfigMgrContentLocationRequest();

ConfigMgrContentLocationReply cmReply = new ConfigMgrContentLocationReply();

// Discover (the try/catch is in case the ADSI COM
// object cannot be loaded (WinPE).
try
{
 clr.Discover();
}
catch
{
   clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses();
   clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite();
   clr.LocationRequest.Domain = new ContentLocationDomain() { Name = myDomain };
   clr.LocationRequest.Forest = new ContentLocationForest() { Name = myForest };
}

// Define our SCCM Settings for the message
clr.SiteCode = mySiteCode;
clr.Settings.HostName = myManagementPoint;
clr.LocationRequest.Package.PackageId = packageID;
clr.LocationRequest.Package.Version = packageVersion;

// If we want to "spoof" a request from a different
// AD Site, we can just pass that AD Site in here.
if (myCustomAdSite != null)
{
   clr.LocationRequest.ContentLocationInfo.ADSite = myCustomAdSite;
}

// Validate clr.Validate((IMessageSender)hSender); // Send the message cmReply = clr.SendMessage(hSender);

// Get response
string response = cmReply.Body.Payload.ToString();

while (response[response.Length - 1] != '>')
{
   response = response.TrimEnd(response[response.Length - 1]);
}

Console.WriteLine(response);

Powershell:

# Set up the objects
$httpSender = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Sender.Http.HttpSender

$clr = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ConfigMgrContentLocationRequest

$cmReply = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationReply

# Discover (the try/catch is in case the ADSI COM
# object cannot be loaded (WinPE).
try
{
   $clr.Discover()
}
catch
{
   $clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses() $clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite()
   $clr.LocationRequest.Domain = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationDomain
   $clr.LocationRequest.Domain.Name = $myDomain
   $clr.LocationRequest.Forest = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationForest
   $clr.LocationRequest.Forest.Name = $myForest
}

# Define our SCCM Settings for the message
$clr.SiteCode = $ManagementPointSiteCode
$clr.Settings.HostName = $ManagementPoint
$clr.LocationRequest.Package.PackageId = $PackageID
$clr.LocationRequest.Package.Version = $PackageVersion

# If we want to "spoof" a request from a different
# AD Site, we can just pass that AD Site in here.
if ($myCustomAdSite -ne $null)
{
   $clr.LocationRequest.ContentLocationInfo.ADSite = $myCustomAdSite
}

# Validate
$clr.Validate([Microsoft.ConfigurationManagement.Messaging.Framework.IMessageSender]$httpSender)

# Send the message
$cmReply = $clr.SendMessage($httpSender)

# Get response
$response = $cmReply.Body.Payload.ToString()

while($response[$response.Length -1] -ne '>')
{
   $response = $response.TrimEnd($response[$response.Length -1])
}

 Write-Output $response

Enjoi!

Windows 10 with SCCM 2012 R2 (non SP1)

Need to deploy Windows 10 but don’t have the latest Service Pack installed with SCCM? No problem!

Before Windows 10 was released, we were one of many organizations testing Windows 10 within our production environment. With the preview of build 10049, the SCCM Client no longer worked! Looking at the ccmsetup logs an error was indicated when attempted to install the Windows Update Agent. I discovered a solution posted by Jorgen Nilsson (@ccmexec) who simply mentioned to skip this pre-requisite when launching ccmexec.

ccmexec.exe /mp:MyMP.Domain.com /skipprereq:windowsupdateagent30-x64.exe

And Voila! now you can get a client installed. However, as the OSD guy at my company, I was wanting to use OSD to deploy the OS with the full client and not have to worry about failures or any other issues with the Client during the OSD Process. Depending on you deployment scenario (In-Place Upgrade, Bare Metal, Build and Capture) you’ll need to do something slightly different!

Now, if you’ve upgraded to SCCM 2012 R2 SP1 (or SCCM 2012 SP2) this logic is already built-in. Crack open the ccmsetup.cab that’s with your SCCM Client Source Media. Inside is a xml file. Open it up.

SCCM 2012 R2 CU4:

<Item FileHash="D500A5B5945FAFC6A52FB54B7169B62C6C1137E1694184FF2EFF790AA1394ECE" FileName="x64/WindowsUpdateAgent30-x64.exe">
   <Applicability OS="ALL" Platform="X64">
      <Skip>Embedded</Skip>
      <Skip>OS=>=6.1</Skip>
   </Applicability>
   <Discovery Identifier="%windir%\system32\wuapi.dll" Type="File">
      <Property Operator=">=" Name="Version">7.4.7600.226</Property>
   </Discovery>
   <Installation OptionalParams="/quiet /norestart" InstallationType="EXE" Order="3"/>
</Item>

Now if we take a look at SCCM 2012 R2 SP1:

<Item FileHash="D500A5B5945FAFC6A52FB54B7169B62C6C1137E1694184FF2EFF790AA1394ECE" FileName="x64/WindowsUpdateAgent30-x64.exe">
   <Applicability OS="<10" Platform="X64">
      <Skip>Embedded</Skip>
      <Skip>OS=>=6.1</Skip>
   </Applicability>
   <Discovery Identifier="%windir%\system32\wuapi.dll" Type="File">
      <Property Operator=">=" Name="Version">7.4.7600.226</Property>
   </Discovery>
   <Installation OptionalParams="/quiet /norestart" InstallationType="EXE" Order="3"/>
</Item>

Difference being the Applicability tag which with the Service Pack installed (and Microsoft support for Windows 10) we see that the WUAgent is skipped all together, so, skipping it really doesn’t hurt you at all as far as the client goes.

Obviously though, you SHOULD be making plans to get this Service Pack installed, that way you can avoid these workarounds and have full support from Microsoft when deploying!

Build and Capture or Bare Metal

Armed with the information above, for a Task Sequence where you’re not running setup.exe (Build and Capture/Bare Metal/Hardlink) all you need is to add the “/skipprereq” line from above with your “Setup Windows and Config Manager”:

Installation properties: /skipprereq:windowsupdateagent30-x64.exe

SetupWinAndConfigMgr

In-Place Upgrade

This is a bit different since the In-Place Upgrade scenario doesn’t use the Setup Windows and Config Manager Step, however, the same fix applies.

Download the Upgrade Task Sequence found in this TechNet blog by Aaron Czechowski. When you import this into SCCM 2012, a few packages are created, on of which is called “Windows vNext Upgrade Scripts”. If you try to run the Upgrade Windows Task Sequence on a non-service pack SCCM install, you’ll see Windows Setup sit at 100% for a VERY long time, and then potentially rollback or fail out all together. If you look at the logs you’ll see errors pointing at the Windows Update Agent.

Make a copy of this directory and create yourself a revised “Windows vNext Upgrade Scripts”; this way when you do get the Service Pack installed (And you should soon!) you’re just flipping a package reference! The Upgrade Scripts are referenced by Windows Setup.exe. Look at the “Upgrade Windows” Step of this task sequence

Setup.exe /Auto:Upgrade /Quiet /NoReboot /DynamicUpdate Disable /PostOobe %SystemDrive%\_vNextUpgrade /PostRollback %SystemDrive%\_vNextUpgrade

What’s happening is when you specify the /PostOobe flag, its looking for a file “SetupComplete.cmd”; same logic applies for /PostRollback, which is looking for SetupRollback.cmd. (Feel free to have fun with that information!)

Now, cracking open SetupComplete.cmd, its essentially launching the powershell script SetupComplete.ps1. Edit SetupComplete.ps1 and look for this line:

$process = Start-Process $env:WinDir\ccmsetup\ccmsetup.exe -ArgumentList "/remediate:client" -Wait -NoNewWindow -PassThru

Modify it by injecting the skipprereq from above and you get this:

$process = Start-Process $env:WinDir\ccmsetup\ccmsetup.exe -ArgumentList "/remediate:client /skipprereq:windowsupdateagent30-x64.exe" -Wait -NoNewWindow -PassThru

Beautiful right? Well, almost. If you’ll be using this same package for 32-bit machines, you’ll need to exclude the x86 WUAgent, not the x64 one. Quick WMI call and you’re there:

if ((Get-WmiObject Win32_OperatingSystem).OsArchitecture -eq "64-bit")
{
   $process = Start-Process $env:WinDir\ccmsetup\ccmsetup.exe -ArgumentList "/remediate:client /skipprereq:windowsupdateagent30-x64.exe" -Wait -NoNewWindow -PassThru
}
else 
{
   $process = Start-Process $env:WinDir\ccmsetup\ccmsetup.exe -ArgumentList "/remediate:client /skipprereq:windowsupdateagent30-x86.exe" -Wait -NoNewWindow -PassThru
}

Quick fix to help you get Windows 10 deployed while waiting for that Service-Pack to finally be installed!

 

Enjoi!

Windows 10 Upgrade: Using USMT Hardlink

For those of us who have 32-bit Versions of Windows (whatever) floating around, you may have noticed that a lot of Hardware OEM’s are no longer providing 32-bit drivers for Windows 10. This creates an issue if/when you want to get those devices up to Windows 10. The In-Place Upgrade options built into the Windows 10 setup is amazing! However, it does not support 32-bit to 64-bit upgrade. Your best bet to getting this upgraded is to use whats called a USMT Hardlink and upgrade the OS.

What is USMT Hardlink?

USMT (User State Migration Tool) is a nice utility from Microsoft that is used to migrate a user profile from one operating system to the next. A USMT Hardlink deployment refers to a Windows OS Deployment (to the same hardware) where we’ll create a local offline copy of the user profile, apply the new OS (without cleaning the disk) and then injecting the user profile back to that machine. In the situation of say Windows XP to Windows 7, USMT Hardlink was our only option (As there was no In-Place Upgrade). The same goes for a 32-bit verions of Windows-whatever (7,8,8.1) to Windows 10 64-bit.

One thing to mention: Microsoft requires that for Windows 10 deployments, you have the appropriate Service Pack Installed. Now it is possible to do this without the Service Pack using a few small steps. I’ll include those steps with the process, look for “Without Service Pack“. If you do not have the Service Pack installed, you will need a system that has the Windows 10 ADK installed. We’ll need to pull some files and make some packages using that media.

Create the Task Sequence

  1. Once created, we’ll be deleting the whole thing, I’m running through this with you in case you need a Boot Image, MDT Package, USMT Package, or custom package created as this wizard will help you create those. If you already have these packages, feel free to create a custom Task Sequence and skip ahead.
  2. Make sure you have MDT 2013 installed with the ConfigMgr Integration setup, as well as the Windows ADK!
    Open the SCCM Console, Navigate to the Software Library Node, Expand Operating Systems.
  3. Right click “Task Sequence” and select “Create MDT Task Sequence”
    Create_MDTTaskSequence
  4. Select “Client Replace Task Sequence” and click Next.
    Create_Type
  5. Name your task sequence appropriately, “Windows 10 64-bit (Hardlink)” works for me!
    Create_Name
  6. Boot Image: Specify an existing or create a new.
    Create_BootImage

    1. Without Service Pack: You’ll need a WinPE Image from the Windows 10 ADK which cannot be done 100% without the Service Pack. Specify whatever boot image you have for now. We’ll fix this later!
  7. MDT Package: Specify an existing package or create a new.
    Create_MDT
  8. USMT Package: Specify an existing package or create a new one.
    Create_USMT
    Without Service Pack: On the machine with the Windows 10 ADK Installed, copy the following folder to the network and use this source to create your USMT Package: “C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\User State Migration Tool
  9. Settings Package: Specify and existing or create a new. I use the vanilla Custom Settings Package for this; I’ll assume you are too.
    Create_CustomSettings
  10. Review the Summary, and complete the wizard.

Packages

Boot Image:

If you’re running 2012 R2 SP1 (or 2012 SP2), then you should have the Windows 10 ADK installed on your server so new Boot Media should be a breeze. If, however, you do NOT have the Service Pack installed, a Win10 WinPE Boot media image CAN be imported and used, you just can’t modify it:

Boot Media with SCCM Service Pack:
Win10WinPE_WithSP

 

Boot Media without SCCM Service Pack. Notice we’re missing some key tabs:
Win10WinPE_NonSP

 

The solution here, is to create the Boot Media, then import the customized WIM file. Rather than provide instructions, I’ll direct you over to the best of the best: Johan Arwidmark. Follow his tutorial to create your custom WinPE.wim, then import directly into SCCM.

MDT Package:

Right now there isn’t a MDT version released with Windows 10 Support, However, I’ve been successful using the Toolkit Package from MDT 2013 as well as the Preview MDT that has been released.

USMT Package:

For your USMT Package, you’ll need to use what comes with the Windows 10 ADK. Install the Windows 10 ADK on any system then copy the following folder to the network and use this as the package source to create your USMT Package: “C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\User State Migration Tool“. Your package source must match the folders structure! You package source will have 3 folders: amd64, arm64, x86. If you have custom xml files to use with USMT, place them in the appropriate folder for the architecture of the OS.

Operating System Image

If you don’t already have SCCM 2012 R2 SP1 (2012 SP2) to create a proper Build and Capture, you can simply import the install.wim into SCCM under “Operating System Images”. Point directly to the install.wim inside the sources folder.

Reference the Boot Media

If a boot media is not referenced, a Restart Computer step cannot select the assigned Boot Media. It’s good to have this setup ahead of time, so all steps can be properly configured.

  1. Right-Click your Task Sequence and select Properties.
  2. Verify “Use a boot image” is checked.
  3. Click Browse and select the Windows 10 WinPE Image you have.
    1. If you want, you can add options to set only on specific Operating Systems. This will help prevent the wrong Client Operating System from running the deployment.
  4. Click OK.
    TS_Prop_BootMedia

Customize the Task Sequence

  1. Edit the Task Sequence.
  2. Now, If you ran through the wizard creating the Task Sequence, Remove all steps! Yes, All of them!
  3. Create a Folder called “Initialization”
    1. Within this group we’ll create 3 built in steps
      1. MDT> Use Toolkit Package
        1. Specify the packages you created for the Toolkit Package
      2. MDT> Gather
        1. Specify the packages your Custom Settings Package for Gather if you desire. I use “Gather only local data…”.
      3. General> Check Readiness
        1. Set “Ensure minimum memory (MB)” 2048 (you may adjust higher if you see fit)
        2. “Ensure minimum processor speed (MHz)” I left at the default (800)
        3. Verify “Ensure minimum free disk space (MB)” is at least 6000 (6GB).
        4. Verify “Ensure current OS to be refreshed is” is set to “Client”.
  4. Below those steps, add a folder called “Capture User State and Settings”. Within here we’ll have 6 steps in this order:
    1. General> Run Command Line
      1. Name: Capture Groups
      2. Command line: cscript.exe “%deployroot%\Scripts\ZTIGroups.wsf” /capture
    2. Settings> Capture Network Settings
      1. Check both options “Migrate domain…” and “Migrate network…”
    3. Settings> Capture Windows Settings
      1. Check all options “Migrate computer name”, “Migrate Registered user and organization names” and “Migrate time zone”
    4. General> Set Task Sequence Variable
      1. Name: “Set User State Store Path”
      2. Task Sequence Variable: OSDStateStorePath
      3. Value: %SystemDrive%\UserState
    5. General> Set Task Sequence Variable
      1. Name: Set USMT Additional Capture Options
      2. Task Sequence Variable: OSDMigrateAdditionalCaptureOptions
      3. The Value can be any mix of USMT capture arguments. Please note with the Windows 10 USMT Package, you do not need to specify the /hardlink parameter. I use the following:
        1. Value: /uel:90 /ui:DOMAIN\*
        2. This migrates all domain profiles that have been used in the last 90 days.
        3. Make sure to replace “DOMAIN” with your domain!
    6. User State> Capture User State
      1. Specify your Windows 10 USMT Package
      2. If you have custom capture files, be sure to specify those using the “Customize how user profiles are captured”
      3. In my example, I left “Enable verbose logging” and “Skip…EFS” unchecked
      4. Select “Copy by using file system access”
        1. “Continue if some files cannot be captures” is Checked
        2. “Capture locally by using links instead of by copying files” is Checked
  5. Create a new group called “Install Operating System”. We’ll have 8 steps in this order:
    1. General> Restart Computer
      1. Name: “Restart in Windows PE”
      2. Select “The boot image assigned to this task sequence”
      3. User notification is recommended in this case, so feel free to specify a friendly message for the user.
    2. MDT> Use Toolkit Package:
      1. Specify your MDT Package
    3. MDT> Gather
      1. Specify your Custom Settings package if you desire. I use “Gather only local data…”.
    4. Images> Apply Operating System
      1. Specify your Windows 10 Image
      2. If you have an answer file, specify that. I don’t use one here.
      3. Verify the Destination is set to “Next available formatted partition”
    5. Settings> Apply Windows Settings
      1. Specify your org information, local admin, and time zone settings as a good fallback if the captured settings cannot be used.
    6. Settings> Apply Network Settings
      1. If you’ll be upgrading a domain computer, make sure to specify the Domain and Account. Since we’ll be using the captured settings, we don’t need to specify a Domain OU.
    7. MDT> Gather
      1. Specify your Custom Settings package if you desire. I use “Gather only local data…”.
    8. General> Run Command Line
      1. Name: Configure
      2. Command line: cscript.exe “%deployroot%\Scripts\ZTIConfigure.wsf”
  6. Create a folder called “Apply Drivers”
    1. You can either use the Auto Apply or specify a Driver package. If you’re using driver packages make sure to filter any hardware specific information on each step.
  7. Create a folder called “Setup Operating System”. We’ll have 4 steps in this order:
    1. Images> Setup Windows and Configuration Manager.
      1. Specify your Package for the Configuration Manager Client
      2. Without Service Pack: add the following in the “Installation Properties”: /skipprereq:windowsupdateagent30-x64.exe
    2. General> Restart Computer
      1. “The currently installed default operating system”
      2. At this point its not necessary to provide notification.
    3. MDT> Use Toolkit Package
      1. Specify your MDT Package
    4. MDT> Gather
      1. Specify your Custom Settings package if you desire. I use “Gather only local data…”.
  8. Create a folder called “Software Installation”
    1. Here is where you’d put a list of software to install within your image. For us, this is where we place our standard Corporate Applications.
  9. Create a folder called “Install Software Updates”
    1. Typically we’ll have 3 repeated steps of “Install Software Updates”. If you are only deploying the OS, one should be enough, however, if you’re deploying software like “Office”, it may be wise to have 2 or 3 just to make sure all gets applied. At this time I only have one cycle:
      1. General> Install Software Updates
        1. All Software Updates
      2. General> Restart Computer
        1. “The Current Default Operating System”
        2. Notification is not necessary.
  10. Create a folder called “Restore User Files and Settings”. We’ll have the following 5 steps:
    1. MDT> User Toolkit Package
      1. Specify your MDT Package
    2. General> Run Command Line
      1. Name: Restore Groups
      2. Command line: cscript.exe “%deployroot%\Scripts\ZTIGroups.wsf” /restore
    3. General> Set Task Sequence Variable
      1. Name: Set USMT Additional Restore Options
      2. Task Sequence Variable: OSDMigrateAdditionalRestoreOptions
      3. Value: /uel:90 /ui:DOMAIN\*
    4. User State> Restore User Files and Settings
      1. Specify your USMT Package
      2. If you used custom xml files to capture user profiles, you’ll need to specify them again here for the restore.
      3. If you desire to restore local profiles, check the box
      4. Check “Continue if some files cannot be restored”.
    5. General> Restart Computer
      1. “The currently installed default operating system”
  11. And that’s it!
    HardLink_TaskSequence

Deploy

  1. Distribute your package references:
    1. Right Click the Task Sequence and select “Distribute Content”.
    2. Run through the wizard selecting any Distribution Points or Distribution Point Groups.
  2. If you don’t already, make sure you have a Collection setup to deploy to!
    Hardlink_Collection
  3. Right Click the Task Sequence and select “Deploy”,
    1. Select your Collection and complete the Wizard.
      hardlink_deployment
  4. Update Machine Policy on the client.
  5. Watch Software Center
    hardlink_SoftwareCenter
  6. Click Install and wait.
    Hardlink_run_startHardlink_applyOS
  7. Once all is done, We’re at Windows 10 64-bit!
    PostUpgradeLogin
    Hardlink_migratedDesktop

Enjoi!

WinPE and .NET (Part 3): WPF OSD Task Sequence Wizard

Introduction

In Part 1 we saw how to create the necessary DLL to prepare a Visual Studio Solution for use within a Task Sequence. In addition, we looked at some sample code about what it takes to get the value of a Task Sequence Variable. In Part 2 we looked at setting Task Sequence Variables and how we can leverage a .NET application to, not only get the value of a Task Sequence variable, but to set the value of one as well.

In part 3 we’ll be taking those concepts and applying those to a WPF application for a nice and simple OSD Boot Wizard!

WpfOSDWiz_Final

I’ll be going over my example in detail which should give you the basic concepts of how this works! Please feel free to style the application as you see fit for your needs and your company! My hope is that this gives you the information and inspiration you need to make your own WPF OSD Wizard! I’ll be using Visual Studio 2013 Professional but the express version should match up well for all that we are doing.

If you have not already, referencing Part 1, you need to: extract and create our DLL (If you did this in Part 1, you can reuse this DLL).

Basic Solution Setup

Open Visual Studio, go to File-> New Project…

On the left, expand Installed> Templates> Visual C# and Select Windows Desktop.

Select WPF Application, and set a name for your solution. I’ll be using the name “MyWpfOsdWizard”.

CreateSolution

Right click the MyWpfOsdWizard in the Solution Explorer and select “Properties”.

On the Application tab make sure you Target framework is set to “.NET Framework 4”.

We’ll get into the design in a bit, for now we’ll make sure that we have all the DLL’s referenced. This way, if you want to design all on your own, you’ve got the basic backbone functionality already in place.

In the Solution Explorer right-click References and select “Add Reference”. When the window pops up, click “Browse” and select the task sequence dll you created in Part 1 of my blog. Click OK.
AddReference

Now that this is referenced, we’ll add the helper class that I linked to in Part 2 of this series. I like to keep things organized, so right-click MyWpfOsdWizard in the Solution Explorer and select Add -> “New Folder”. Rename the folder “Classes”. Since this is a small project you don’t have to do this. I prefer to keep things organized and for other larger projects this has become a force of habit; and a good one at that! Navigate to my helper class and add the existing item. Right Click the Classes folder, select add> Existing Item, browse to the downloaded file and select OK.

Now we have all the necessary items added in and ready to go! Right now is the time when you should feel free to customize the look, feel and functionality of your Wizard; If you want! I’ll be continuing my example which (as seen in the above) will include a text box to set the Computer Name, set 5 custom Task Sequence Variables.

Setting up MainWindow

My design includes a Header, middle “frame”, and a footer. I style the wizard this way because if you’re using a highly customized MDT enabled Task Sequence, you may need more “pages” within your Wizard. We’ll only focus on one page in this example, but you can and should feel free to add in additional pages; for example, a summary page!

First we’ll need to set some settings for MainWindow. We’ll be setting a few Properties to create a front-most, full screen, non-sizable Window. Remove the Height and Width properties in you XAML code and add the necessary properties:

<Window x:Class="MyWpfOsdWizard.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="My WPF OSD Wizard" WindowStyle="None" WindowState="Maximized"
   ResizeMode="NoResize" WindowStartupLocation="CenterScreen" Topmost="True">

We set “Title” which is self-explanatory. The other properties, again, are my preference for the style of the Window.

  • WindowStyle = This sets if we have a window border and what that looks like. Setting this to “none” makes it a borderless window. See the link for an example of each.
  • WindowState = Minimized, Maximized, or “Normal”. We’ll use Maximized to ensure that the Wizard is all you see.
  • ResizeMode = This sets whether or not we want the user to be able to resize a Window. In this instance we don’t want them to, so we set NoResize.
  • WindowStartupLocation = This property tells the application where to put the Window. Since we set Maximized in the WindowState property, this perhaps isn’t all that necessary, but putting a window in a nice center screen location is usually a nice practice. If you choose not to use the WindowsState = Maximized property, setting the WindowStartupLocation to CenterScreen will ensure a nice centered window.
  • TopMost = Often times with Wizards, I’ve seen people battle having their Window in front of the Task Sequence Startup exe (the one that you click “Next” to right after you boot). By setting the TopMost property to True, we ensure that we’ll always have our Wizard in the foreground.

In the MainWindow, I’ll have a nice header, a Frame making up the “body” and a footer. I like this look and feel it gives a nice definition to the Wizard.

Setup the grid which will define into a 3×3 scheme:

<Grid>
   <Grid.RowDefinitions>
      <RowDefinition x:Name="Header" Height="150"/>
      <RowDefinition x:Name="Content"/>
      <RowDefinition x:Name="Footer" Height="50"/>
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition Width="150"/>
      <ColumnDefinition/>
      <ColumnDefinition Width="150"/>
   </Grid.ColumnDefinitions>
</Grid>

Your grid will look like this:

MWGrid

Setting up the Header Now, let’s style the header! Immediately after the closing add another Grid for your Header.

<!-- Grid for Header -->
<Grid Grid.ColumnSpan="3">
   <Grid.RowDefinitions>
      <RowDefinition Height="1.5*"/>
      <RowDefinition/>
   </Grid.RowDefinitions>
</Grid>

Inside of this grid we’ll create our styling elements for the header. There’s a bit of code here so I’ll summarize what we’re doing then give you the full output.

  • We’ll use the top half of our Header row as a Gray Rectangle with a text block “OPERATING SYSTEM DEPLOYMENT”. This will be aligned to the bottom right, have an opacity of 0.1, font size of 45 and font family of “Corbel”. We’ll use the Margin property to offset the right hand of this textblock.
  • The Bottom section will be a nice black to gray gradient background. We’ll add a textblock with a gradient brush, bold and italic. This will be aligned vertical center and to the right with a right-margin offset.

Code is here and image below of what this should look like now:

<!-- Grid for Header -->
<Grid Grid.ColumnSpan="3">
   <Grid.RowDefinitions>
      <RowDefinition Height="1.5*"/>
   <RowDefinition/>
</Grid.RowDefinitions>

<!-- Header -->
   <!-- Top Section -->
   <Rectangle Fill="LightGray"/>
   <TextBlock Text="OPERATING SYSTEM DEPLOYMENT" Margin="5"
      VerticalAlignment="Bottom" HorizontalAlignment="Right"
      FontSize="45" Opacity="0.1" FontFamily="Corbel"/>

   <!-- Bottom Section -->
   <Rectangle Grid.Row="1">
      <Rectangle.Fill>
         <LinearGradientBrush>
            <LinearGradientBrush.StartPoint>0.5,0</LinearGradientBrush.StartPoint>
            <LinearGradientBrush.EndPoint>0.5,1</LinearGradientBrush.EndPoint>
            <GradientStop Color="#363636" Offset="0"/>
            <GradientStop Color="#363636" Offset="0.02"/>
            <GradientStop Color="#616161" Offset="0.03"/>
            <GradientStop Color="#313131" Offset="1"/>
         </LinearGradientBrush>
      </Rectangle.Fill>
   </Rectangle>

   <TextBlock Grid.Row="1" Text="MY WPF OSD WIZARD" Margin="5"
      VerticalAlignment="Center" HorizontalAlignment="Right"
      FontFamily="Calibri" FontStyle="Italic" FontWeight="Bold"
      FontSize="40">
      <TextBlock.Foreground>
         <LinearGradientBrush>
            <LinearGradientBrush.StartPoint>0.5,0</LinearGradientBrush.StartPoint>
            <LinearGradientBrush.EndPoint>0.5,1</LinearGradientBrush.EndPoint>
            <GradientStop Color="#E8B100" Offset="0"/>
            <GradientStop Color="#E8B100" Offset="0.1"/>
            <GradientStop Color="#EABD1F" Offset="1"/>
         </LinearGradientBrush>
      </TextBlock.Foreground>
   </TextBlock>

</Grid>

FullGrid

Content Frame

The content section is incredibly simple…for now. We’ll use a frame to hold the XAML Page that we’ll create later. Not much to see here but our XAML code will look like this. Don’t worry…we’ll be back later to fill in the “Source” property. This code will go below your Header Grid (Not the Main grid from the window!–All your content stays inside of that Grid).

<!-- Content Frame -->
<Frame x:Name="contentFrame" Source=""
   Grid.Row="1" Grid.ColumnSpan="3"
   NavigationUIVisibility="Hidden"/>

Footer

The footer will be pretty simple. We’ll be using the same background gradient that we used in the bottom section of our header. We’ll have a center aligned TextBlock to contain whatever information you choose…copyright, contact support, hello world. Anything your heart desires. This code will go directly below your Content Frame section.

<!-- Footer -->
<Rectangle Grid.Row="2" Grid.ColumnSpan="3">
   <Rectangle.Fill>
      <LinearGradientBrush>
         <LinearGradientBrush.StartPoint>0.5,0</LinearGradientBrush.StartPoint>
         <LinearGradientBrush.EndPoint>0.5,1</LinearGradientBrush.EndPoint>
         <GradientStop Color="#363636" Offset="0"/>
         <GradientStop Color="#363636" Offset="0.02"/>
         <GradientStop Color="#616161" Offset="0.03"/>
         <GradientStop Color="#313131" Offset="1"/>
      </LinearGradientBrush>
   </Rectangle.Fill>
</Rectangle>

<TextBlock Grid.Row="3" Grid.ColumnSpan="3" Text="Created By Noah Swanson"
   VerticalAlignment="Center" HorizontalAlignment="Center"
   Foreground="#979797" FontSize="10"/>

Now we should have a well put together shell for our Wizard:

FullMainWindow

Now, If you’ve had experience with XAML, you’ve noticed that we use the same LinearGradientBrush in 2 places. So you may be asking “why not a resource dictionary?” The answer: Yes. This could very easily be put into a resource dictionary for easier use and integration. However, since we’d only have the one item, and to keep this example simple we’ll leave the code as is. If you want you can move this to a resource dictionary. If you’re adding more style and visual elements, you may make more use of one. MSDN Documentation on Resource Dictionaries can be found here.

Making the Page of our Wizard

The way we’re styling this, we have a Frame element in our MainWindow which will display our page. Not only does this help separate our Main Layout from our content, but also makes the app flexible in the event we want to add other pages, or reuse the same style for a completely different use.

In the Solution Explorer, right click MyWpfOsdWizard, Add-> New Folder. Name the folder “Pages”. Right-click the folder and select Add-> Page… When the Window pops up name your page “Landing.xaml” and click Add.
AddPage

The layout of the page will have a computer name field. We’ll add some validation logic to this field in case someone forgets about the 15 character limitation. Below that we’ll have 5 pairs of textblocks, one for a Variable Name and another for a Variable Value. These 5 pairs will represent 5 completely custom variables that can be set.

Open the Landing.xaml file. To give us a better workspace change the d:DesignHeight and d:DesignWidth to 600 and 800, respectively. If you’re wondering, setting d:DesignHeigh and d:DesignWidth does not affect the size of the page when the application is ran or compiled. “d” is declared with mc:Ignorable=”d”. Being that it is “Ignorable” the compiler will pay no attention to it. Since the properties are then DesignHeight and DesignWidth you really do take the “Design” part literally. Setting a “Height” or “Width” property in the Page element would set a size for the page and this would be bad. We want the page to automatically size itself to the frame we created in MainWindow. If you would set an actual Height and Width, you might get something ugly like this:
BadSize

However, it is perfectly safe and fair to use the default provided DesignHeight/Width properties without fear of a malformed looking application.

So first things first, setup the grid for the page.

<Grid>
   <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="10"/>
      <ColumnDefinition/>
   </Grid.ColumnDefinitions>
   <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
   </Grid.RowDefinitions>
</Grid>

We’ll add in a message in the first row indicating some instructions. In the second row we’ll have a nice black rectangle acting as a divider.

<!-- Instruction Header -->
<TextBlock Grid.ColumnSpan="3" HorizontalAlignment="Center" VerticalAlignment="Center"
   Text="Please enter a computer name and set any custom variables.
When complete, click Finish."
   Margin="0,10,0,10" FontFamily="Segoe UI" FontSize="15" TextAlignment="Center"/>
<!-- Rectagle Separator -->
   <Rectangle Fill="Black" Grid.Row="1" Grid.ColumnSpan="3" Height="3" Margin="0,10,0,10"/>

The third row is where we do something interesting. We’ll have a label in the left column. The TextBox will be contained in a horizontally oriented StackPanel. After the TextBox we’ll have a Dock Panel with some interesting code. The Dock Panel allows us to buddy up next to the TextBox. We’re stylizing it a bit making it color “OrangeRed” and then adding text inside of this from the “Segoe UI Symbol” font. The “Segoe UI Symbol” font is an amazing collection of symbols and icons that can be used easily! The character I chose will certainly bring attention to itself! Setting a tooltip is the text to display with you hover the mouse over it. XAML code of the Computer name section:

<!-- Computer Name Input and Validator -->
<StackPanel Orientation="Horizontal" Grid.Column="2" Grid.Row="2">
<TextBox x:Name="osdComputerName" Width="150" Height="20"
   VerticalAlignment="Center" HorizontalAlignment="Left"
   TextChanged="osdComputerName_TextChanged"/>
   <DockPanel LastChildFill="True" x:Name="computerNameErrIcon"
      ToolTip="Please enter a Computer Name!">
      <Border Background="OrangeRed" DockPanel.Dock="Right" Margin="5,0,0,0"
         Width="25" Height="25" CornerRadius="5">
         <TextBlock Foreground="White" Text="" FontFamily="Segoe UI Symbol"
            HorizontalAlignment="Center" VerticalAlignment="Center"/>
      </Border>
   </DockPanel>
</StackPanel>
<!-- Another separator -->
<Rectangle Fill="Black" Grid.Row="3" Grid.ColumnSpan="3"
Height="3" Margin="0,10,0,10" />

Over in your C# code it’s time to set the code for our “osdComputeName_TextChanged” event. Depending on the length of the osdComputerName TextBox, we’ll change the DockPanel’s visibility and tooltip.

private void osdComputerName_TextChanged(object sender, TextChangedEventArgs e)
{
   // Empty Computer Name
   if (osdComputerName.Text.Length < 1)
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Visible;
      computerNameErrIcon.ToolTip = "Please enter a Computer Name!";
   }
   // Computer Name is Too Long
   else if (osdComputerName.Text.Length > 15)
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Visible;
      computerNameErrIcon.ToolTip = "Computer Name cannot exceed 15 characters!";
   }
   // Computer Name is Good!
   else
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Hidden;
      computerNameErrIcon.ToolTip = "";
   }
}

Now we need to add functionality for some custom Task Sequence Variables. We’ll start with a heading/title of sorts:

<!-- Custom Variables -->
<TextBlock Text="Set Task Sequence Variable Manually:" Grid.Row="4"
   HorizontalAlignment="Right" VerticalAlignment="Center"
   Margin="0,10,10,10" FontFamily="Segoe UI" FontSize="15" />
<TextBlock Text="Variable Name" Grid.Row="5"
   HorizontalAlignment="Right" VerticalAlignment="Center"
   Margin="0,10,70,10" FontFamily="Segoe UI" FontSize="13" />
<TextBlock Text="Variable Value" Grid.Row="5" Grid.Column="2"
   HorizontalAlignment="Left" VerticalAlignment="Center"
   Margin="20,10,0,10" FontFamily="Segoe UI" FontSize="13" />

Below this we’ll add this 5 times incrementing the x:Name and Grid.Row as we go:

<TextBox x:Name="osdVarNameOne" Grid.Row="6" Width="150" Height="20"
   HorizontalAlignment="Right" VerticalAlignment="Center"
   Margin="10"/>
<TextBox x:Name="osdVarValOne" Grid.Row="6" Grid.Column="2"
   Width="150" Height="20"
   HorizontalAlignment="Left" VerticalAlignment="Center"
   Margin="10"/>

One last thing to do and that is add a button to complete our wizard. Below the last TextBox set for our fifth variable, add a button. Rather than specify “Content” we’ll add a TextBlock inside our Button so we can style the font a bit. Make sure to name the button and add a Click event. We’ll be disabling this button by default.

<!-- Finish Button -->
<Button x:Name="finishButton" Grid.Row="11" Grid.Column="2"
   IsEnabled="False" Height="40" Width="100" Margin="40"
   HorizontalAlignment="Right" VerticalAlignment="Bottom" Click="Button_Click">
   <TextBlock Text="Finish" Margin="0,10,0,10"
      HorizontalAlignment="Center" VerticalAlignment="Center"
      FontFamily="Segoe UI" FontSize="15"/>
</Button>

Back in your MainWindow.xaml update your frame element to include your newly created Page:

<Frame x:Name="contentFrame" Source="Pages\Landing.xaml"
   Grid.Row="1" Grid.ColumnSpan="3"
   NavigationUIVisibility="Hidden"/>

Now if you run your application you’ll have a pretty good looking start!
WpfOSDWiz_Final

Now, let’s complete our code. Open Landing.xaml.cs. We’ll need to setup our finish button click event along with adding one small thing to our computer name TextChange. Let’s knock out the TextChange event first. Remember how we had our finish button disabled by default? We don’t care if the variables aren’t complete, but the computer name property we need to tie some rules to. We’ve already got basic logic in there for name length, so let’s disable the button if our computer name is too short or too long. We’ll simply flip the button’s IsEnabled property.

private void osdComputerName_TextChanged(object sender, TextChangedEventArgs e)
{
   // Empty Computer Name
   if (osdComputerName.Text.Length < 1)
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Visible;
      computerNameErrIcon.ToolTip = "Please enter a Computer Name!";
      finishButton.IsEnabled = false;
   }
   // Computer Name is Too Long
   else if (osdComputerName.Text.Length > 15)
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Visible;
      computerNameErrIcon.ToolTip = "Computer Name cannot exceed 15 characters!";
      finishButton.IsEnabled = false;
   }
   // Computer Name is Good!
   else
   {
      computerNameErrIcon.Visibility = System.Windows.Visibility.Hidden;
      computerNameErrIcon.ToolTip = "";
      finishButton.IsEnabled = true;
   }
}

Now, let’s tackle the finish button’s Click logic. We’re going to use our TaskSequenceEnvironmentClass to set any of the variable we defined. We’ll first create our instance of the TaskSequenceEnvironment and then set variable if the textboxes aren’t empty:

private void Button_Click(object sender, RoutedEventArgs e)
{
   Classes.TaskSequenceEnvironment tsEnv = null;

   // Get instance of the Task Sequence environment.
   try
   {
      tsEnv = new Classes.TaskSequenceEnvironment();
   }
   catch
   {
      MessageBox.Show("Unable to locate Task Sequence Environment!", "WinPE .NET Wizard");
      Application.Current.Shutdown(1);
   }

   // Set the Computer Name
   tsEnv.SetTaskSequenceVariable("OSDComputerName", osdComputerName.Text);

   //Set Variable One, if present
   if (osdVarNameOne.Text != "" && osdVarValOne.Text != "")
   {
      tsEnv.SetTaskSequenceVariable(osdVarNameOne.Text, osdVarValOne.Text);
   }

   //Set Variable Two, if present
   if (osdVarNameTwo.Text != "" && osdVarValTwo.Text != "")
   {
      tsEnv.SetTaskSequenceVariable(osdVarNameTwo.Text, osdVarValTwo.Text);
   }

   //Set Variable Three, if present
   if (osdVarNameThree.Text != "" && osdVarValThree.Text != "")
   {
      tsEnv.SetTaskSequenceVariable(osdVarNameThree.Text, osdVarValThree.Text);
   }

   //Set Variable Four, if present
   if (osdVarNameFour.Text != "" && osdVarValFour.Text != "")
   {
      tsEnv.SetTaskSequenceVariable(osdVarNameFour.Text, osdVarValFour.Text);
   }

   //Set Variable Five, if present
   if (osdVarNameFive.Text != "" && osdVarValFive.Text != "")
   {
      tsEnv.SetTaskSequenceVariable(osdVarNameFive.Text, osdVarValFive.Text);
   }

   Application.Current.Shutdown(0);
}

Now, you can safely compile and run your solution!

Deploying and Using your Wizard

When you satisfied with your solution, compile it, and grab the EXE. Place this in a folder to be used as you SCCM Source for your package. In the Config Manager Console, create a new package from the Software Library Section under Application Management> Packages. Name the package “WPF OSD Wizard” set your source path and complete the rest of the information
CreatePackage

When prompted for a program select Do Not Create a Program. Complete the package creation wizard and replicate the content. Now we’ll need to create some boot media!

If you already have boot media skip this step, otherwise make sure you have the Windows ADK and MDT installed on your SCCM Server to create boot media. Right click Boot Images and select “Create Boot Media using MDT”. Specify an empty folder for the boot media to be created in and click Next. Name your boot media and click Next.
CreateBootMedia

We’ll create x86 Boot Media (but x64 will work too!). Click Next. On the components screen, make sure  you select .NET Framework or all of your hard work will have been in vain. I enjoy having Powershell available so I’ll add that too! Click Next
AddComponents

Here is where we get the option of adding a prestart command. DO NOT add the Wizard here! The reason being: This prestart command will trigger anytime your boot media is used. In certain task sequences you may reboot to WinPE using the assigned boot image. If you put your wizard in the prestart command here, anytime you rebooted to it, you’d have your wizard pop up. You may add additional files if you include extra tools in your boot image, or a custom background. I like to leave the F8 Command Prompt option available for troubleshooting sake.
BootMediaOptions

Click Next and Next once more to complete the wizard. Once this is complete, distribute your boot media to your Distribution Point(s).

Create the Bootable Media ISO

Right-click the Task Sequences item and select “Create Task Sequence Media”. Select Bootable Media and click Next. Select Dynamic Media and click Next. Select CD/DVD Set and pick an output location for the iso file. Click Next. If you wish set a password, otherwise leave the options default and click Next. Select your Boot Media, Distribution Point, and Management Point(s) and click Next.

Now for the prestart command! This prestart command differs from the previous in which your prestart command will only be seen when booting from this media you’re creating. Place a Checkbox next to “Enable Prestart Command”, put the name of your EXE in the box and select the package you created for your Wizard along with a distribution point. Click Next.
AddPrestart

Review the Summary and click Next to complete the Wizard. Place your ISO onto a USB, mount to a VM or setup with PXE and check out the magic!

WinPE Startup
WinPEStartup

Wizard Loaded
WizardLoaded

Once our task sequence takes off we can verify our variables using the GetTaskSequenceVariables.exe that was presented within Part 1 and voila!
VariableReport

If you have experience with WPF and want to pick apart my code, please don’t :). Yes, I realize accessing and object’s property directly (if (osdVarNameTwo.Text != “” && osdVarValTwo.Text != “”)) is bad practice, however for the demo’s sake and for simplicity, it works fine!

Hopefully this gives you the information needed to start creating your own .NET WinPE Wizard! Obviously we didn’t do anything too extravagant, but this should give you the starting point and inspiration needed!

My complete solution and code is here!

Enjoi!

TPM Not Found After OSD

Today I experienced something frustrating. After applying OSD on some new hardware I attempted to enable BitLocker (TPM+PIN Configuration). To my surprise I received an error that a valid TPM could not be found. Here is my experience and methodology for troubleshooting a missing TPM.

(1) Check WMI
Using Powershell:

Get-WmiObject Win32_TPM -Namespace root/cimv2/security/MicrosoftTPM | Select IsActivated_InitialValue, IsEnabled_InitialValue, IsOwned_InitialValue | Format-List

Usually you’d see something like this:

IsActivated_InitialValue : True
IsEnabled_InitialValue   : True
IsOwned_InitialValue     : True

(2) Check TPM MMC Console
If and when WMI is blank I move on to the TPM MMC console snap-in (tpm.msc).

(3) Check BIOS
At this point I’ve determined the TPM isn’t visible to the Operating System; It happens! On most BIOS you’ll have settings whether or not the Operating System can see and/or manage the TPM Device. Boot into the BIOS, look for a security section and check the TPM Status. In my instance this looked good! TPM was listed as Enabled and Activated. I rebooted back to the OS, repeated steps 1 and 2…both still with the same result.

(4) Vendor Software
When it comes to using vendor-provided installers/software/executables to install drivers, I typically have one rule: I DON’T! In my experience (Dell, Lenovo, HP, Samsung, MS Surface) Plug-n-Play will identify and capture 99% of the hardware without the necessity to install the vendor’s software. In this instance this was an Infineon TPM device so I grabbed the driver CD, extracted and ran. Unless absolutely necessary, I’ll usually use the software to install only the driver where possible. A lot of driver installations will also provide an application that tromps over the built in Windows functions with their own; Bloatware, Crapware…call it what you want, I find it extremely unnecessary. The most infuriating being the old “HP Wireless Assistant” which was a clunky “remix” of Windows’ normal Wireless Connection Menu and tray icon–I found this to be slow, clunky, and down right unnecessary!

Back to the TPM…I launched the “Infineon TPM Professional Package”. I select custom install hoping to see “Driver”. Instead I see a bunch of extra stuff I don’t need and when I went to tell the installer to “not install” a component, I saw this wasn’t an option…It would appear that in order to get an Infineon TPM device seen to the OS, I have to use their crappy application to “manage and control” it. Not gonna happen!

As I contemplated what to do next I eventually did what I probably should have done at step (1)…

Check Device Manager
Usually, device manger would show a TPM device under the “security” category as seen:

TPM_devmgmt_cropped

 

 

Unfortunately, not found! I then started digging in Device Manager and eventually stumbled across “Infineon Trusted Platform Module” under “System Devices”. That explains why the TPM MMC couldn’t find it!

TPM_inSYS

Right-Click -> Uninstall (making sure to check “Delete the driver software for this device”.

uninstallTPM

Richt-Click –> “Scan for Hardware Changes”

Just like magic, the TPM was detected and placed in the “Security Devices” category. TPM.msc detected it. BitLocker was enabled and there was much rejoicing. I then removed the “tpm driver” from the Driver Package, updated distribution points and all was well!

 

My hope is that this will save some of you from additional headaches if this comes up in your OSD world.

 

Enjoy!