Use PowerShell to Create Windows To Go Keys—Part 5


Summary: Use a basic Windows PowerShell workflow to create multiple devices.

Hey, Scripting Guy! Question Hey, Scripting Guy! I was wondering if there is an efficient way to use Windows PowerShell to create multiple Windows To Go keys at the same time.

—TM

Hey, Scripting Guy! Answer Hello TM,

Honorary Scripting Guy, Sean Kearney, is here today. I am going to wind up my week of posts by playing with that very idea.

   Note  This is a five-part series that includes the following posts:

Today I’m going to implement a basic Windows PowerShell workflow to image multiple Windows To Go keys from the same image. The process is actually very close to creating one key, but I need to trap for a few situations:

  • Obtain a complete list of all potential Windows To Go keys
  • Create code for multiple and independent Unattend.xml files
  • Designate a unique pair of drive letters for the operating system and system drive for each workflow

First off, I’ll give that workflow a name. I’ll name this one simply CreateWTG:

workflow CreateWTG

{

Now I get all available Windows To Go devices attached to the computer:

$WTG= Get-Disk | Where-Object { (‘Imation IronKey Wkspace’,’Kingston DT Ultimate’) -match $_.Friendlyname }

I’ll begin to process all of the keys attached by using Foreach –parallel. This keyword operates in a similar manner to the standard Foreach-Object, but it launches the processes in parallel.

Foreach -parallel ($WTGDisk in $WTG)

 {

I’m going to add Start-Sleep with a random sequence to try to ensure that the individually spawned processes don’t try to grab the same drive Letter. I’ll pick a three-minute random window:

Start-Sleep (Get-Random 180)

My next task is to go through the list of keys to use to identify all available drive letters. I will do this each time to try to ensure that I do not clash with drive letters that are in use by any other process. For this, I am going to use two separate tricks in PowerShell.

I first use the CIM class, cim_LogicalDisk, which will provide all letters assigned to physical devices and active network drive letters. I can target the DeviceID property, which contains the drive letter:

[string[]]$InUse=$NULL

$InUse+=Get-CimInstance cim_logicaldisk | select-object -ExpandProperty DeviceID

Now I have a second issue: drive letters that are assigned to a network drive but are offline. I have not been able to find a CIM class that has this information. But fortunately, it’s all stored in the Registry under HKEY_CURRENT_USER\Network. I can run Get-ChildItem against this key to get the list. This will show all network drive letters whether or not they are offline.

$InUse+=Get-ChildItem Registry::HKey_Current_User\Network –Name

I now have all the drive letters that are in use by Windows. However, if you examine the list, you will see that the results are “Dirty.” The data from CIM_LogicalDisk and the data from Get-ChildItem don’t align. Some have colons, and some are lowercase.

I only need to use this list for a simple match comparison. So I am going to build a list of drive letters from A – Z, then put together an array that contains only those that are not in use.

First I define the array and start the loop:

[string[]]$Available=$NULL

# Step through all letters from A to Z

# Yes weI could have just said 65 to 90 but I thought

# you might find it neat to see how to get the ASCII number

# for a Character

#

for ($x = ([byte][char]’A’); $x -le ([byte][char]’Z’); $x++)

{

Here I have a simple comparison. If the character in question does not match anything in the array of drive letters that are in use, I will add it to the array.

   Note  For those of you (like myself) who are IT pros, the explanation point character ( ! ) indicates a Boolean NOT.

 If (![boolean]($InUse -match [char][byte]$x)) { $Available+=[char][byte]$x }

}

I could have easily written it like as follows to perform the same thing. (Yes, sometimes Boolean can make your head spin if you’re not a developer.)

 If ([boolean]($InUse -match [char][byte]$x) –eq $False) { $Available+=[char][byte]$x }

Now that I have an available list, I’ll grab two drive letters. I’ll use a little Get-Random to avoid having things clash.

$Position=[int](Get-Random $Available.count)

 $DriveSystem=$Available[$Position]

 $DriveOS=$Available[$Position+1]

I can now clear the disk, and partition and format the key in question. Note how I have updated the variable from $WTG to $WTGDisk. (Remember that I am now in a Foreach process.)

$DiskNumber=$WTGDisk.DiskNumber

Clear-Disk –Number $DiskNumber –RemoveData –RemoveOEM –Confirm:$False

Get-Disk –number $DiskNumber | Get-Partition | Remove-Partition –confirm:$False

Initialize-Disk –Number $DiskNumber –PartitionStyle MBR

$System=New-Partition -DiskNumber $DiskNumber -size (350MB) –IsActive

$OS= New-Partition –DiskNumber $DiskNumber –UseMaximumSize

Format-Volume -NewFileSystemLabel “System” -FileSystem FAT32 -Partition $System -confirm:$False

Format-Volume -NewFileSystemLabel “Windows” -FileSystem NTFS -Partition $OS -confirm:$False

Set-Partition -InputObject $System -NewDriveLetter $DriveSystem

Set-Partition -InputObject $OS -NewDriveLetter $DriveOS

Set-Partition -InputObject $OS –NoDefaultDriveLetter

After the disk is partitioned, I apply the image. However, I need to make sure the log file has a unique name because by default, it’s simply called DISM.LOG. So I’ll add the disk number as part of its temporary log name.

$Wimfile=’.\install.wim’

Expand-WindowsImage –imagepath “$wimfile” –index 1 –ApplyPath “$DriveOS`:\” -LogPath “.\Dism$($DiskNumber).log”

Now here is where I hit a snag. The BCDBoot command I need to execute is not a recognized command in the PowerShell workflow engine. But I can alleviate this issue by wrapping it as an inline script, which launches a separate PowerShell process for it to execute out of the workflow.

Because this is a new PowerShell process I need tell it what variables it should use from the existing workflow and then assign it a name. This can be a little confusing for the IT pro at first because the new name can be exactly the same as the old name.

inlinescript

  {

  $OSDrive=Using:OSDrive

  $SystemDrive=UsingSystemDrive 

  & “$($env:windir)\system32\bcdboot” “$OSDrive`:\Windows” /f ALL /s “$Systemdrive`:”

  }

I prepare the SAN-Policy.xml as in my previous post, but with one minor change. I will make the file name unique for this process so that I don’t have multiple PowerShell processes accessing the same file with the same cmdlet and getting some type of File in Use error message. I will use $OSDrive as the unique characteristic to modify the file name.

$Policy=@”

<?xml version=’1.0′ encoding=’utf-8′ standalone=’yes’?>

<unattend xmlns=”urn:schemas-microsoft-com:unattend”>

 <settings pass=”offlineServicing”>

 <component

  xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State”

  xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”

  language=”neutral”

  name=”Microsoft-Windows-PartitionManager”

  processorArchitecture=”x86″

  publicKeyToken=”31bf3856ad364e35″

  versionScope=”nonSxS”

  >

  <SanPolicy>4</SanPolicy>

 </component>

 <component

  xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State”

  xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”

  language=”neutral”

  name=”Microsoft-Windows-PartitionManager”

  processorArchitecture=”amd64″

  publicKeyToken=”31bf3856ad364e35″

  versionScope=”nonSxS”

  >

  <SanPolicy>4</SanPolicy>

 </component>

 </settings>

</unattend>

“@

$SanPolicyFile=”.\$OSDrive-san-policy.xml”

Remove-item $SanPolicyFile -erroraction SilentlyContinue

Add-content -path $SanPolicyFile -Value $Policy

Use-WindowsUnattend –unattendpath $SanPolicyFile –path “$OSdrive`:\”

I now inject the drivers as previously from our source folder:

$Drivers=’.\Drivers’

Add-WindowsDriver –Path “$DriveOS`:” –driver $Drivers –recurse

For the final touch, I will add the Unattend.xml files. I use the same technique with the SAN-Policy.xml file to make the source file unique. What I must do, however, is ensure the file name is still called unattend.xml when it transfers to the destination Windows To Go key.

$Computername=”WTG-$(Get-Random)”

$Organization=’Contoso Inc.’

$Owner=’Contoso Inc. IT Dept.’

$Timezone=’Eastern Standard Time’

$AdminPassword=’P@ssw0rd’

$Unattend=@”

<?xml version=”1.0″ encoding=”utf-8″?>

<unattend xmlns=”urn:schemas-microsoft-com:unattend”>

 <settings pass=”specialize”>

  <component name=”Microsoft-Windows-Shell-Setup” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”>

   <ComputerName>$Computername</ComputerName>

   <RegisteredOrganization>$Organization</RegisteredOrganization>

   <RegisteredOwner>$Owner</RegisteredOwner>

   <TimeZone>$Timezone</TimeZone>

  </component>

 </settings>

 <settings pass=”oobeSystem”>

  <component name=”Microsoft-Windows-Shell-Setup” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”>

   <UserAccounts>

    <AdministratorPassword>

     <Value>$Adminpassword</Value>

     <PlainText>true</PlainText>

    </AdministratorPassword>

   </UserAccounts>

              <AutoLogon>

               <Password>

                <Value>$Adminpassword</Value>

                <PlainText>true</PlainText>

               </Password>

              <Username>administrator</Username>

              <LogonCount>1</Log\onCount>

              <Enabled>true</Enabled>

              </AutoLogon>

   <RegisteredOrganization>$Organization</RegisteredOrganization>

   <RegisteredOwner>$Owner</RegisteredOwner>

   <OOBE>

    <HideEULAPage>true</HideEULAPage>

    <SkipMachineOOBE>true</SkipMachineOOBE>

   </OOBE>

  </component>

 </settings>

 <cpi:offlineImage cpi:source=”” xmlns:cpi=”urn:schemas-microsoft-com:cpi” />

</unattend>

“@

$UnattendFile=”.\$OSDrive-unattend.xml”

Remove-item $UnattendFile -erroraction SilentlyContinue

Add-content -path $Unattendfile -Value $Unattend

Copy-Item -path $Unattendfile -destination “$DriveOS`:\Windows\System32\Sysprep\unattend.xml”

Finally, I need to remove the drive letters from the partitions to place them back into the available pool:

 Get-Volume -DriveLetter $OSDrive | Get-Partition | Remove-PartitionAccessPath -accesspath “$OSDrive`:\”

 Get-Volume -DriveLetter $SystemDrive | Get-Partition | Remove-PartitionAccessPath -accesspath “$SystemDrive`:\”

At this point, we should have a complete workflow for creating Windows To Go keys. There are, of course, many ways to improve on this example—such as adding some logging, bringing in online or offline domain joining, or adding some error trapping.

My hope is that you can use this as a small example for how you could leverage a workflow to make your job easier in the Windows To Go world.

If you would like a copy of this workflow, you can download it from the Script Center Repository. Play with it directly and see what you can do to improve on its design: Sample Workflow to Deploy Multiple Windows To Go Keys.

I invite you to follow The Scripting Guys on Twitter and Facebook. If you have any questions, send an email to The Scripting Guys at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, remember eat your cmdlets every day with a dash of creativity.

Sean Kearney, Windows PowerShell MVP and Honorary Scripting Guy

Comments (7)

  1. MockMyBeret says:

    I’ve been reading through these posts the past few days and I absolutely love your process. Very nice and clean.

    I see a small problem, though relying on the random drive letter picking… A thought experiment (If my thought experiment is correct…) takes me to the following…

    if

    $Position=[int](Get-Random $Available.count)

    happens to pick the highest value, both of the following will populate with no values.

    $DriveSystem=$Available[$Position]

    $DriveOS=$Available[$Position+1]

  2. MockMyBeret says:

    Ok, I ran the get-random several hundred times and the values returned 0 through {{upper}} … but {{upper}} will populate $DriveSystem with the highest available drive letter and leave $DriveOS with no value.

  3. MockMyBeret says:

    Proposed solution:

    $Position=[int](Get-Random -Minimum 0 -Maximum ($Available.count -1))

  4. MockMyBeret says:

    Bonus: I figured out how to post on here without being anonymous.

  5. @mockmyberet

    I love it ! The cool part here is when somebody takes an idea (because really this IS more of a Proof of Concept) and sees something I didn’t and improves upon it. VERY cool!

    One of the problems I couldn’t solve was how to identify the drive letters in use to an outside source. Perhaps dumping the output to a common CSV or XML file that each workflow could track?

    Either way…. well done !

    Sean
    Windows PowerShell MVP
    Honorary Scripting Guy

  6. iXlinQ says:

    Hi Sean,

    after a long time of absence I’m trying to get back (and keep up) with the blog entries as PS V5 is out now!
    A very interesting five part series … though I can’t try it out in practice 🙁

    @MockMyBeret: The get-random thing is really a little bit tricky and I’ve nearly always forgotten that the Maximum number will not be returned by it 🙁
    As I have sometimes used the drive letter testing myself, I just wanted to add another possible solution to picking a free drive letter:

    Short Version:
    $avail=”; [int][char]’A’..[int][char]’Z’ | %{$i=[char]$_; try{ sl ($i+’:’) -ea 1} catch {$avail+=$i}}
    $freeDriveLetter=$avail[(get-random $avail.Length)]

    Here everything is stored in a string: $avail and I use "Set-Location" (sl) with a drive letter to see if I can do it or not.
    If there is an error I assume, the drive letter is available and append it to the string $avail.

    One thing to pay attention to: If you are running PS in admin mode the results are most likely not the same as in normal mode ( not elevated )!

    Greetings, Klaus

    P.S: Long Version:

    $avail=”;
    [int][char]’A’..[int][char]’Z’ |
    ForEach-Object {$i=[char]$_; try{ Set-Location ($i+’:’) -ErrorAction Stop} catch {$avail+=$i}}

  7. @iXlinQ

    Love it! Way cool additional approach to a tricky problem 🙂

    Sean
    Honorary Scripting Guy

Skip to main content