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!

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!

Inject Drivers to Windows 10 Install Media

So Windows 10 is here with its second build (9860) in its “Technical Preview”. Since I am an OSD guy, I like to make sure when a system is built all the drivers are there (even for my test machines and ad-hoc builds to “see if it works”). Usually this just entails adding some drivers and–for the most part–plug and play is nice enough to install everything for me. If you’re without MDT and/or SCCM or just like to have a USB installer ready for anything, you don’t have the luxury of a driver package and just want all the drivers to automatically be there when Windows installs.

This process is very common and really, this applies to Windows Vista (and up) but as I went to inject drivers to Windows 10 media from my Windows 8.1 machine I met an old-friend-of-an-error and figured this was worth sharing.

Get the Drivers

First thing’s first. Get the Drivers! For Windows 10, Microsoft has done a great job of letting Windows 7/8.1 drivers install (or attempt to) and maintain a large percentage of support. So, download all the drivers and put them into one directory. Make sure that any downloaded installers are extracted first to where you can see the inf, cat, and sys files. I know Dell and HP both have self extracting executable for most drivers they offer out.

If you’re like me, you’ll have a huge hierarchy of Manufacturer> Model showcasing all the different models that you want to test on and support.

drivers_u

 

Create Your Media

Connect a USB Drive (USB 3.0 is better :)). We’ll need to format the disk…This will remove all of the files so make sure to backup anything.

  1. Open an Administrative Command Prompt
  2. DiskPart <enter>
  3. List Disk <enter>
  4. Locate which disk is the usb drive. Easiest way is to tell by size.
  5. select disk # <enter>  (# is the disk number from previous step)
  6. clean <enter>
  7. create partition primary <enter>
  8. select partition 1 <enter>
  9. format fs=fat32 quick <enter>
  10. active <enter>
  11. assign <enter>
  12. exit <enter>

Now you have a clean and fresh USB drive to use.

Extract the Install Files

With Windows 8+ you can just right-click and select “Mount” on the iso. Copy all of the files to the USB drive.

With Windows 7, you’ll need a program like 7-zip. Open the ISO, and extract all files to the USB drive.

Mount the WIM

There are two WIM’s in your Windows Installer that we’re concerned about: <InstallRoot>\sources\boot.wim and <InstallRoot>\sources\install.wim.

  • The Boot.wim is what gets loaded when you boot to your install media. This is a WinPE image used to apply an OS–MDT/SCCM people will be very familiar with the boot media concept.
  • The install.wim file is the actual sysprepped OS Installation that will get applied to your hard drive. If you mounted this, it will look just like your C drive: Program Files, Windows, Users, etc…

So really we now dive to 2 scenarios:

  1. When you boot to your media, your hard drive (or other device) is not recognized.
  2. When Windows is installed, drivers are missing

Scenario 1 relates to your boot media, scenario 2 relates to the install.wim. A rule of thumb is that your boot.wim only needs essential drivers; eg: storage and network. In this scenario, we’re only concerned about storage, since we’re using USB as our source media. Really though, both processes are the same.

  1. Open an Administrative Command prompt.
  2. Create a directory to mount the WIM file (mkdir c:\mount)
  3. Mount the wim:
dism /mount-wim /wimfile:%PathToWimFile% /Index:1 /mountdir:%ImageMountDir%

Add in the Drivers:

To add the drivers we’ll need to point the command to the folder of drivers we created above.

REMEMBER: only add storage and network to boot.wim. Anything more is just silly.

If you have you install.wim mounted you can point to the top folder that contains all the drivers for your model. Example: c:\drivers\hp\EliteBook800SeriesG1

Add the drivers:

dism /image:%ImageMountDirFromAbove% /add-driver /driver:%PathToDriversFolder% /recurse /forceunsigned

The recurse option will check all sub folders. the forceunsigned will allow unsigned drivers to be added.

On Windows 8.1 while mounting my Windows 10 install.wim i was greeted with an error:

Deployment Image Servicing and Management tool
Version: 6.3.9600.17031
Error: 50

To service this Windows image requires the latest version of the DISM. See http://go.microsoft.com/fwlink/?LinkId=293395 to find the latest version of DISM, and http://go.microsoft.com/fwlink/?LinkId=293394 to learn how to install the latest version of DISM from the ADK on your computer.

The DISM log file can be found at C:\Windows\Logs\DISM\dism.log

We could look at the dism log if we wanted but to me this error is rather self-explanatory. Essentially this is saying that the OS i have mounted is newer than the dism tool i’m using. Dism version: 6.3.9600.17031…Windows 8.1 OS Version is 6.3.9600 (see what they did there?). Think of it this way: Each version supports the corresponding Windows release plus older (Backwards compatible). It cannot work with a newer version since older version doesn’t know what newer version is.

The fix for this is to use the DISM command included in the Windows setup disk. Rerun the dism command above but this time, we’ll need to use the full path to the Windows 10 media:

%PathToWindowsInstallMedia%\Sources\dism.exe  /image:%ImageMountDirFromAbove% /add-driver /driver:%PathToDriversFolder% /recurse /forceunsigned

And voila…now we have drivers added.

Save the Changes

Last thing is to commit the changes we made:

%PathToWindowsInstallMedia%\Sources\dism.exe  /unmount-wim /mountdir:%ImageMountedDir% /commit

Again, this process will really apply to any version of Windows where you’d like to inject drivers within your media!

 

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!

Using DiskPart with C#

I’m in the process of writing a little utility for our technicians to create a bootable USB device from an ISO. This will be primarily used with our SCCM OSD Media, DaRT media, and will probably find several other uses in time. In developing this I ran into an issue….we like to completely reformat the disk when media is created; probably not completely necessary but this seems to help minimize the issues we saw.

There are several articles out and about mentioning different COM and WMI interfaces you can use, but they seemed a bit messy to me. My love for diskpart has grown to an extent and therefore decided to leverage it…but how.

I didn’t like the idea of writing a file and then referencing a diskpart “script” using the “/s” command so I continued my experiment. For those of you who always use the interactive DiskPart console, you can list all your commands into a txt file and then specify this into the command line of a diskpart script.

For example:

<contents of diskpart.txt>

   select disk 1
   clean
   create partition primary
   select partition 1
   format fs=nfts quick
   active
   assign
   exit

I can then use this file on the command line as so:

> diskpart.exe /s diskpart.txt

and all my commands run as if i typed them individually in the diskpart console.

With Powershell, I can use a here-string for this and then pipe the string to diskpart:

$diskpartString = @"
     select disk 1
     clean
     create partition primary
     select partition 1
     format fs=nfts quick
     active
     assign
     exit
   "@

$diskpartString | diskpart

With C# I’m using a method to do this that mimics the same way using an instance of the Process Class:

 

using System.Diagnostics;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Process p = new Process();                                    // new instance of Process class
      p.StartInfo.UseShellExecute = false;                          // do not start a new shell
      p.StartInfo.RedirectStandardOutput = true;                    // Redirects the on screen results
      p.StartInfo.FileName = @"C:\Windows\System32\diskpart.exe";   // executable to run
      p.StartInfo.RedirectStandardInput = true;                     // Redirects the input commands
      p.Start();                                                    // Starts the process
      p.StandardInput.WriteLine("select disk 2");                   // Issues commands to diskpart
      p.StandardInput.WriteLine("clean");                           //   |
      p.StandardInput.WriteLine("create partition primary");        //   |
      p.StandardInput.WriteLine("select partition 1");              //   |
      p.StandardInput.WriteLine("format fs=fat32 quick");           //   |
      p.StandardInput.WriteLine("active");                          //   |
      p.StandardInput.WriteLine("assign");                          //   |
      p.StandardInput.WriteLine("exit");                            // _\|/_
      string output = p.StandardOutput.ReadToEnd();                 // Places the output to a variable
      p.WaitForExit();                                              // Waits for the exe to finish

      Console.WriteLine(output);                                    // prints the output
    }
  }
}

While this is merely one way of doing this, I like it because it’s self contained and doesn’t require any additional files or api calls. Having the diskpart executable is required. Luckily here, we bake this into our OS Images so we are safe! Perhaps the “dirtiest” part of this is the fact that if we want to pull information from this output, it gets messy. In my instance I just need the volume letter assigned which I’m pulling from WMI.

I’ve instituted a class library to handle most of DiskPart’s commands in this way. Once it has matured a bit i’ll post this back here.

Hopefully, this helps someone somewhere do this!

Enjoi!

Powershell: Update Distribution Priority for all Task Sequence Packages

Last week we had a most unfortunate incident. One of our domains we managed had 4 of the 5 drives fail in the SCCM Primary’s Disk Array. This domain fed off of our Packages from our SCCM 2007 environment. While most of the OSD media was created at the Central Site, the Task Sequence and Boot media were created and managed on this domain and on this SCCM Primary Server–Pretty much a total loss.

After the hardware was fixed, we updated the site to SCCM 2012 R2 and began rebuilding the OSD process. Since this new SCCM 2012 site needed to replicate content again, the multitude of packages began distributing. During the outage PC Deployments got behind so we needed a quick way to set the priority of all (50 or so) packages to “high” priority so they would distribute first. Rather than going through each package 1-by-1, I knew Powershell could do this quicker than I could 2 packages manually!

A few Cmdlets to familiarize ourselves with:

  • Get-CMTaskSequence – Gets a Config Manager Task Sequence
  • Get-CMPackage – Gets a Config Manager “Classic” Package
  • Get-CMDriverPackage – Gets a Config Manager Driver Package
  • Get-CMOperatingSystemImage – Gets a Config Manager Operating System Image (Not source media)
  • Get-CMBootImage – Gets a Config Manager Boot Image
  • Get-CMOperatingSystemInstaller – Gets a Config Manager Operating System Source Media Package

First thing is to get the Referenced Task Sequence Packages. This can be done with a quick one liner:

(Get-CMTaskSequence -TaskSequencePackageId AAA######).References.Package

From here we have packages to feed into other cmdlets and modify the priority. But there’s one catch…Depending on the type of package (OSImage, Package, Boot Media, Driver Package, etc…) a different cmdlet is required. To work around this, I went to SQL; while i realize this isn’t a *great* workaround, it does the job.

The following SQL Query can be used to easily get the appropriate Package Type:

[System.Data.SqlClient.SqlConnection]$sms = New-Object System.Data.SqlClient.SqlConnection("Data Source=$SqlServerName;Integrated Security=SSPI;Initial Catalog=$SqlTableName")
        [System.Data.SqlClient.SqlCommand]$qry = New-Object System.Data.SqlClient.SqlCommand("SELECT PackageID, Name, PackageType FROM v_package WHERE PackageID = '$packageId'", $sms)
try
        {
        $sms.Open()
        [System.Data.SqlClient.SqlDataReader]$res = $qry.ExecuteReader()

        while ($res.Read() -eq $true)
        {
            $tmp = @{}
            for ($i=0;$i -lt $res.FieldCount; $i++) {
                $tmp[$res.GetName($i)] = $res.GetValue($i)
            }
            $output += $tmp
            }
        }
        catch
        {
            Write-Host "SQL Connection / Query Failed:`n$_"
        }
        finally
        {
            $sms.Close()
        }

This will return the results into a very nice object to work with: “$output” in this case. Grabbing a column is easy as well: $output.<columnName> ($output.PackageType).

From here we do a switch statement on the “PackageType” value and update the priority. Free Samples:

Standard Package:

$pkg = Get-CMPackage -Id  -ErrorAction SilentlyContinue
$pkg.Priority = 1 #Set to High Priority
Set-CMPackage -InputObject $pkg -ErrorAction SilentlyContinue

Driver Package

$dp = Get-CMDriverPackage -ID $p -ErrorAction SilentlyContinue
$dp.Priority = 1  #Set to High Priority
Set-CMDriverPackage -InputObject $dp -ErrorAction SilentlyContinue

OS Image Package

$osp = Get-CMOperatingSystemImage -Id $p -ErrorAction SilentlyContinue
$osp.Priority = 1  #Set to High Priority
Set-CMOperatingSystemImage -InputObject $osp -ErrorAction SilentlyContinue

Boot Image Package

$bi = Get-CMBootImage -Id $p -ErrorAction SilentlyContinue
$bi.Priority = 1  #Set to High Priority
Set-CMBootImage -InputObject $bi -ErrorAction SilentlyContinue

Operating System Install Package

$osi = Get-CMOperatingSystemInstaller -Id $p -ErrorAction SilentlyContinue
$osi.Priority = 1  #Set to High Priority
Set-CMOperatingSystemInstaller -InputObject $osi -ErrorAction SilentlyContinue

…And that’s just about the basics of it. After using this script to set all of this up, I expanded to allow either a Task Sequence Package ID or an array of Package ID’s. There are some required items including Read Access to your Config Manager Sql Database, and the 2012R2 console has to be installed on the computer (to use the cmdlets).

See the complete script here: https://onedrive.live.com/redir?resid=3D90B836AD7F51CF%21984

Please let me know if you discover any bugs or any improvements as well! Hopefully this will help you out too!

Enjoy!
@ndswansong