Summary: Use a basic Windows PowerShell workflow to create multiple devices.
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
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 the final post in a five-part series. Before you read this, you should read:
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