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!

Advertisements

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

Installing System Center Endpoint Protection on Windows 10

July 2015: Now that Windows 10 is RTM, is strongly recommend upgrading your SCCM infrastructure to the appropriate service pack. SCCM 2012 SP2 (or SCCM 2012 R2 SP1) provides native support for Windows 10. If you experience issues past the Service Pack upgrade, please consult your Microsoft Support contacts.

With Windows 10, you do not need to deploy SCEP as SCCM Can now manage Windows Defender out of the box: https://technet.microsoft.com/en-us/library/hh508770.aspx

Leaving this workaround up now that a supported process is in place, I fear that some may try to implement this non-support workaround in a production environment.

 

WinPE and .NET (Part 2): Creating and Editing Variables

In Part 1 we saw how to prepare a visual studio solution to be able to access the OSD Environment as well as obtain one specific or all of our task sequence variables. In there I linked a full solution that allows you to get all of those variables in a variety of file formats (txt, csv, xml, etc…).

In Part 2, I’ll be going over creating and setting Task Sequence variables via c# and .NET. If you have not read Part 1, please do. There are some important references and steps that need to be performed in order to properly access the environment.

As highlighted in Part 1, we need to:

  1. Extract and create our DLL (If you did this in Part 1, you can reuse this DLL)
  2. Create a solution setting the target framework to “.Net Framework 4”
  3. Reference the assembly in your solution

Now, just like we did in Par 1, we’ll need to create an instance of our Task Sequence environment:

ITSEnvClass TsEnvVar = new TSEnvClass();

If you recall the way we obtained variable names was using the GetVariables method of our Task Sequence Environment Instance. To get the value of a specific variable we accessed this by:

TsEnvVar["VariableName"];

Creating and Setting variables is just as easy! In fact, its the exact same! With our Task Sequence Environment Instance (TsEnvVar):

TsEnvVar["VariableName"] = "VariableValue";

Really…that’s all there is to it! I’d like to say there’s more magic behind this but really there isn’t!

To help with your own implementations of this, I’ve created a helper class that can be used for getting and setting variables. While is is very basic, it does give a good ‘skeleton’ to work with. I’ve used this class in countless projects for both my company and other experiments/tests I’ve done. You can view this class here.

Up Next: In Part 3 I’ll go through a very quick and basic Task Sequence Wizard using WPF and C#!

 

Enjoi!

WinPE and .NET (Part 1): Get Task Sequence Variables

When WinPE greeted me with the “.NET Framework” component I thought nothing of it…Then I decided to use it and couldn’t have seen more opportunity than when I first saw OSD.

I plan to make this a series of posts utilizing the .NET Framework with OSD ranging from the basics of Getting Task Sequence Variables, Setting Task Sequence Variables, and even adding these to make a visually rich WPF Task Sequence Wizard. I’ll focus on the C# language that Microsoft has made so easy to love. Although, keep in mind all these things can be done with C++ and .NET as well.

First things first we need the necessary dll in order to use the Task Sequence Environment.

Let me break and mention that there are 2 examples out there that explain this part a bit more than I will and may also provide more details for you:

Whether or not you’re following my blog or one of the two references there are a few prerequisites:

Creating the Necessary DLL

First we’ll need to crack open our boot media and get the tscore.dll file. We’ll use the handy DISM tool to do this:

  1. Get a SCCM Boot media ISO and mount it.
  2. Open the appropriate folder for your architecture and grab the tscore.dll file:
    1. <root>\SMS\bin\i386\tscore.dll
    2. <root>\SMS\bin\x64\tscore.dll
  3. Copy this locally.
  4. You’ll need to run the type library importer (tlbimp.exe) against this. As mentioned in John Socha-Leialoha’s blog above, this is specific to the .NET version you’ll be compiling in and installed to the boot media. A safe bet is .NET 4.0 (at least at this point in time) so we’ll go with that:
  5. “%WindowsSdkDir%\Bin\TlbImp” <PathFromStep3>\TSCore.dll”
  6. This will spit out a TsEnvironmentLib.dll file for us to import to Visual Studio.

Importing the DLL

For those of you familiar with adding references this should be fairly straight forward:

  1. Create your solution in Visual Studio and make sure to set the Target Framework to .NET Framework 4.
  2. In the Solution Explorer right-click References
  3. select Add Reference
  4. Click Browse and select the dll.
  5. Add the appropriate “using” to your code and you’re ready!

Getting Task Sequence Variables

There are a lot of variables in a Task Sequence and even more if you’re as customized as my solutions usually are. We’ll focus on the built-in variables for simplicity. Here are a few TechNet articles to become familiar with:

In the code we’ll need to get an instance of our Task Sequence Environment:

ITSEnvClass TsEnvVar = new TSEnvClass();

Now we could access any variable (if we know the name of it) which will return the value:

TsEnvVar["OSDComputerName"];

In order to get all of the variables we have set within our OSD environment, simply call the “GetVariables” method of our TSEnvClass instance which returns an array of our variable Names (not values).

object[] x = (object[])TsEnvVar.GetVariables();

You can then loop through the array from the snippet above to get all variables along with their values:

for (int i = 0; i < x.Length; i++)
{
    Console.WriteLine(x[i].ToString() + "\t:\t" + TsEnvVar[x[i].ToString()]);
}

And there we have it! For my own purposes I’ve created a nice little utility that will spit out all of the variables in a variety of formats. The solution is published to my GitHub here.

Up Next, Creating and Editing those variables!

 

Enjoi!

Get BitLocker Recovery From Active Directory with Powershell

At the company I currently work for we’ve been using BitLocker since Vista (although Vista install base was rather small). Having moved from another “Pretty Good” Disk encryption technology, one thing we struggled with was Recovery information. While the previous solution had its own server to store all recovery keys, BitLocker links these to the Computer Object. When that computer object is deleted, so is the key. This was a battle for us at first since we have automation in place to remove stale AD Accounts, but–at the time–did not have any method to backup that information. We immediately added a table to a database and as part of our cleanup would backup these Keys.

First: Why not MBAM?

I loosely touched on this in another post regarding TPM Backup information in AD and i’ll give the quick summary again. By the time MBAM cam around, we already had our backup system in place. This was added into a global utility that allowed our technicians to perform AD tasks without AD permissions (Create computer, reset, move ou, etc…). MBAM would have just been “another thing” for us to maintain and manage. With our custom tool and SCCM already in place, MBAM did not provide us any additional benefits. For those of you using MBAM, This may not be for you, however this still provides a great way to quickly grab the information for those ad-hoc requests.

As we move to upgrade our various automation from the tried-and-true VBScript to the ever-so-loved Powershell, Obtaining the BitLocker Recovery Information was an odd one. If you use the RSAT tools and pull up details of a computer object there is a “BitLocker Recovery tab”.

RecoveryB

Each BitLocker Recovery password is an object of the computer account object. The odd thing is that by doing a Get-ADComputer, Recovery information is not returned as a “property” like other items–and where I expected it. As usual, first step was to use a search engine and see what others had done. I found a few results that gave me what i wanted (notably this post) but didn’t like the way it was scripted. The snippet that does the work is here:

$objADObject = get-adobject -Filter * | Where-Object {$_.DistinguishedName -match $objComputer.Name -and $_.ObjectClass -eq "msFVE-RecoveryInformation"}

While this gets the job done it is slow and by slow i mean the combination of “-Filter *” and piping to “Where-Object” is looking at everything before seeing if its the item (droid) that I’m looking for. Recovery Key look-up for my system took approximately 15 seconds for the first result, and didnt finish processing until a few seconds after that.

Let’s step back and look at a BitLocker Recovery object (msFVE-RecoveryInformation); really lets justify the filter “$_.DistinguishedName -match $objComputer.Name”.

Each BitLocker Recovery object is of the Object Class: msFVE-RecoveryInformation. If you look at the DistinguishedName of an object we can see it stems from the Distinguished name of the computer object it is attached to:

DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swComputers,DC=swansonglab,DC=com

Unfortunately this is the best way (I’ve found) to tie a recovery object back to the computer account. For those curious, here is what’s contained in a msFVE-RecoveryInformation object:

CanonicalName : swansonglab.com/swComputers/WIN7X86PC01/2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
CN : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
Created : 10/20/2014 1:10:55 PM
createTimeStamp : 10/20/2014 1:10:55 PM
Deleted :
Description :
DisplayName :
DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swComputers,DC=swansonglab,DC=com
dSCorePropagationData : {12/31/1600 6:00:00 PM}
instanceType : 4
isDeleted :
LastKnownParent :
Modified : 10/20/2014 1:10:55 PM
modifyTimeStamp : 10/20/2014 1:10:55 PM
msFVE-KeyPackage : {136, 1, 0, 0...}
msFVE-RecoveryGuid : {255, 105, 157, 229...}
msFVE-RecoveryPassword : 465762-121880-049797-598411-533643-549890-128436-549736
msFVE-VolumeGuid : {146, 36, 120, 28...}
Name : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
nTSecurityDescriptor : System.DirectoryServices.ActiveDirectorySecurity
ObjectCategory : CN=ms-FVE-RecoveryInformation,CN=Schema,CN=Configuration,DC=swansonglab,DC=com
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : d0a15cc8-5f86-42ed-8942-633cec25b6b1
ProtectedFromAccidentalDeletion : False
sDRightsEffective : 15
showInAdvancedViewOnly : True
uSNChanged : 99936
uSNCreated : 99936
whenChanged : 10/20/2014 1:10:55 PM
whenCreated : 10/20/2014 1:10:55 PM

Remember in most cases getting recovery is searched by the GUID. This can be filtered on using Powershell with no problems or computer object linking. The example I’m looking at is getting all recovery objects attached to a computer account.

So my issues with the snippet above (using “-Filter *” and “Where-Object”), again, was time consuming. Since the Recovery Information objects are attached to a computer object we can simply use the computer account’s Distinguished Name as the search base for the command:

# Get Computer Object
> $computer = Get-ADComputer -Filter {Name -eq 'WIN7X86PC01'}

# Get all BitLocker Recovery Keys for that Computer. Note the 'SearchBase' parameter
> $BitLockerObjects = Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase $computer.DistinguishedName -Properties 'msFVE-RecoveryPassword'

# Output the results!
> $BitLockerObjects
DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 465762-121880-049797-598411-533643-549890-128436-549736
Name : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : d0a15cc8-5f86-42ed-8942-633cec25b6b1

DistinguishedName : CN=2014-10-20T13:11:29-06:00{450547C6-675C-4A61-B276-17CC620D3885},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 632126-201135-053504-485045-151657-139986-094820-137687
Name : 2014-10-20T13:11:29-06:00{450547C6-675C-4A61-B276-17CC620D3885}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : 8c3963ea-89ec-4b41-934b-ee6023d9d1e9

DistinguishedName : CN=2014-10-20T13:12:03-06:00{A29D2D47-89D6-4459-B106-40B1F62A04EF},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 497178-478654-023111-302291-606034-162855-504163-720698
Name : 2014-10-20T13:12:03-06:00{A29D2D47-89D6-4459-B106-40B1F62A04EF}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : 4a72004e-e76e-4cb3-a828-152011b8b541

Hopefully this has provided some additional information on BitLocker Recovery keys and how to obtain them should you ever need to!

Enjoi!