Add code for building updated Windows ISOs

- Improve PS code, though the lab scripts are kind of a mess still
- Move some functions over to the postinstall module (should rename)
- Rip out calls to dism.exe in favor of the Powershell dism module
- Use an exported $ArchitectureId dictionary variable like a global
  constant for talking about architecture names
- Programmatically get the path of ADK/WAIK files & folders
- Call out to oscdimg.exe to make a new ISO
jowjDev
Micah R Ledbetter 9 years ago
parent e1865e44b6
commit c1430c0bdb

@ -8,6 +8,7 @@ Which build actions do you want to perform?
.parameter tag
A tag for the temporary directory, the output directory, and the resulting Vagrant box
#>
function buildlab {
[cmdletbinding()]
param(
[parameter(mandatory=$true,ParameterSetName="BuildPacker")]
@ -30,8 +31,20 @@ param(
[switch] $whatIf
)
import-module dism -verbose:$false
# Module useful for Download-URL at least. TODO: this mixes concerns and may not be ideal?
ipmo $PSScriptRoot\scripts\postinstall\wintriallab-postinstall.psm1
get-module wintriallab-postinstall | remove-module
import-module $PSScriptRoot\scripts\postinstall\wintriallab-postinstall.psm1 -verbose:$false
Set-StrictMode -Version 2.0
# This seems to be required with strict mode?
$verbose = $false
# This correctly covers -verbose -verbose:$false and -verbose:$true
if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) {
$verbose = $true
}
$dateStamp = get-date -UFormat "%Y-%m-%d-%H-%M-%S"
$packerOutDir = "$baseOutDir\PackerOut"
@ -42,13 +55,16 @@ $labTempDir = "$baseOutDir\temp-$dateStamp"
if ($tempDirOverride) { $labTempDir = $tempDirOverride }
$wimMountDir = "${labTempDir}\MountInstallWim"
$installMediaTemp = "${labTempDir}\InstallMedia"
$newMediaIsoPath = "${labTempDir}\windows.iso"
$errorActionPreference = "Stop"
#$fullConfigName = "wintriallab-${baseConfigName}-${dateStamp}"
$fullConfigName = "wintriallab-${baseConfigName}"
set-alias packer (gcm packer | select -expand path)
set-alias vagrant (gcm vagrant | select -expand path)
$outDir = "${packerOutDir}\${fullConfigName}"
if ($tag) { $outDir += "-${tag}"}
@ -65,7 +81,7 @@ function Download-WSUSOfflineUpdater {
$url = "http://download.wsusoffline.net/$filename"
$dlPath = "$labTempDir\$filename"
Get-WebUrl -url $url -downloadPath $dlPath
$exDir = resolve-path "$wsusOfflineDir\.." # why the .. ? because the zipfile puts everything in a 'wsusoffline' folder
$exDir = resolve-path "$wsusOfflineDir\.." # why the ".." ? because the zipfile puts everything in a 'wsusoffline' folder
sevenzip x "$dlPath" "-o$exDir"
}
function Download-WindowsUpdates {
@ -75,122 +91,81 @@ function Download-WindowsUpdates {
}
}
function Test-AdminPrivileges {
$me = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
return $me.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
}
function Get-DismWimInfo {
[cmdletbinding()] param (
[parameter(mandatory=$true)] [string] $wimfile
)
if (-not (Test-AdminPrivileges)) { throw "Admin privileges are required" }
write-verbose "Getting WIM information for file '$wimFile'"
$wimFile = resolve-path $wimFile | select -expand Path
$wimInfoText = Invoke-ExpressionAndCheck "dism /Get-WimInfo /WimFile:`"$wimfile`""
$wimIndexes = @()
$currentIndex = $false
foreach ($line in $wimInfoText) {
write-verbose $line
if ($line.startsWith("Index : ")) {
$currentIndex = New-Object PSObject
[int]$index = $line -replace "Index : ",""
Add-Member -inputObject $currentIndex -NotePropertyName "Index" -NotePropertyValue $index
Add-Member -inputObject $currentIndex -NotePropertyName "WimFile" -NotePropertyValue $wimFile
Add-Member -inputObject $currentIndex -MemberType ScriptProperty -Name DebugDesc -Value {
"$($this.WimFile) / $($this.Index) / $($this.Name)"
}
}
elseif ([String]::IsNullOrEmpty($line)) {
if ($currentIndex) { $wimIndexes += @($currentIndex) }
$currentIndex = $null
}
elseif ($currentIndex) {
$splitLine = [Regex]::Split($line, " : ")
Add-Member -inputObject $currentIndex -NotePropertyName $splitLine[0] -NotePropertyValue $splitLine[1]
}
}
return $wimIndexes
}
function Apply-WindowsUpdatesToWim {
[cmdletbinding()] param (
[parameter(mandatory=$true)] [string] $wimFile,
[parameter(mandatory=$true)] [string] $wimMountDir,
[parameter(mandatory=$true)] [string] $winUpdateDir
<#
.notes
The install.wim file doesn't (ever? sometimes?) denote architecture in its image names, but boot.wim (always? usually?) does
#>
function Get-BootWimArchitecture {
[cmdletbinding()] param(
[parameter(mandatory=$true)] $wimFile
)
write-verbose "Applying Windows Updates to '$wimFile' from '$winUpdateDir'"
#Unblock-File $installWim
Set-ItemProperty -path $wimFile -name IsReadOnly -value $false -force
foreach ($wimInfo in (Get-DismWimInfo -wimFile $wimFile)) {
write-verbose "Attempging to apply WSUS Offline Updates to $($wimInfo.DebugDesc)"
$wimMountSubdir = mkdir "${wimMountDir}\$($wimInfo.Index)" -force | select -expand fullname
Invoke-ExpressionAndCheck "dism /mount-wim /wimfile:`"$installWim`" /mountdir:`"$wimMountSubdir`" /index:`"$($wimInfo.index)`""
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { throw "External command failed with exit code '$LASTEXITCODE'" }
$bootWimInfo = Get-WindowsImage -imagePath $wimFile -verbose:$verbose
ls $updatePath\* -include *.cab |% {
write-verbose "Applying update at $_ to directory at $wimMountSubdir"
Invoke-ExpressionAndCheck "dism /image:`"$wimMountSubdir`" /add-package /packagepath:`"$_`""
}
Invoke-ExpressionAndCheck "dism /unmount-wim /mountdir:`"$wimMountSubdir`""
}
}
function Get-BootWimBitness($wimFile) {
$bootWimInfo = Get-DismWimInfo -wimFile $wimFile
$arch = $null
if (-not $bootWimInfo) { throw "Got no information for wimfile at '$wimFile'"}
elseif ($bootWimInfo[0].Name -match "x86") { return "i386" }
elseif ($bootWimInfo[0].Name -match "x64") { return "amd64" }
else { throw "Could not determine WimBitness"}
}
function Get-WOShortCode($OSName, $OSBitness) {
$shortCodeTable = @{
"8.1" = "w63"
}
$shortCodeTable.keys |% { if ($OSName -match $_) { $shortCode = $shortCodeTable[$_] } }
if (-not $shortCode) { throw "Could not determine shortcode for an OS named '$OSName'" }
write-verbose "Found shortcode '$shortcode' for OS named '$OSName' of bitness '$bitness'"
if ($OSBitness -match "i386") { $shortCode += "" }
elseif ($OSBitness -match "amd64") { $shortCode += "-x64" }
else { throw "Could not determine shortcode for an OS of bitness '$OSBitness'" }
elseif ($bootWimInfo[0].ImageName -match "x86") { $arch = $ArchitectureId.i386 }
elseif ($bootWimInfo[0].ImageName -match "x64") { $arch = $ArchitectureId.amd64 }
else { throw "Could not determine architecture for '$wimFile'"}
return $shortCode
write-verbose "Found an architecture of '$arch' for '$wimFile'"
return $arch
}
function Apply-WindowsUpdatesToIso {
[cmdletbinding()] param (
[parameter(mandatory=$true)] [string] $iso,
[parameter(mandatory=$true)] [string] $inputIso,
[parameter(mandatory=$true)] [string] $outputIso,
[parameter(mandatory=$true)] [string] $wsusOfflineDir,
[parameter(mandatory=$true)] [string] $wimMountDir
)
$myWimMounts = @()
mount-diskimage -imagepath $iso
$mountedDrive = get-diskimage -imagepath $iso | get-volume | select -expand DriveLetter
$mountedDrive = get-diskimage -imagepath $iso | get-volume | select -expand DriveLetter
mount-diskimage -imagepath $inputIso
$mountedDrive = get-diskimage -imagepath $inputIso | get-volume | select -expand DriveLetter
$installWim = "$labTempDir\install.wim"
if (-not (test-path $installWim)) {
cp "${mountedDrive}:\Sources\install.wim" $labTempDir -verbose:$verbose
}
else {
write-verbose "Using EXISTING install.wim at '$installWim'"
}
Set-ItemProperty -path $wimFile -name IsReadOnly -value $false -force
$installWim = cp "${mountedDrive}:\Sources\install.wim" $labTempDir -passthru | select -expand fullname
$bitness = Get-BootWimBitness -wimFile "${mountedDrive}:\sources\boot.wim"
dismount-diskimage -imagepath $iso
$arch = Get-BootWimArchitecture -wimFile "${mountedDrive}:\sources\boot.wim" -verbose:$verbose
dismount-diskimage -imagepath $inputIso
$wimInfo = Get-DismWimInfo -wimFile $installWim
$shortCode = Get-WOShortCode -OSName $wimInfo[0].Name -bitness $bitness
$wimInfo = Get-WindowsImage -imagePath $installWim
$shortCode = Get-WOShortCode -OSName $wimInfo[0].ImageName -OSArchitecture $arch
$updatePath = resolve-path "${wsusOfflineDir}\client\$shortCode\glb" | select -expand Path
Apply-WindowsUpdatesToWim -wimFile $installWim -wimMountDir $wimMountDir -winUpdateDir $updatePath
foreach ($wimInfo in (Get-WindowsImage -imagePath $installWim)) {
write-verbose "Attempging to apply WSUS Offline Updates to $wimInfo)"
$wimMountSubdir = mkdir "${wimMountDir}\$($wimInfo.ImageIndex)" -force | select -expand fullname
Mount-WindowsImage -imagePath $installWim -index $wimInfo.ImageIndex -path $wimMountSubdir
dismount-diskimage -imagepath $iso
try {
Add-WindowsPackage -PackagePath $updatePath -path $wimMountSubdir
}
catch {
write-verbose "Caught error(s) when installing packages:`n`n$_`n"
}
# foreach ($update in (ls $updatePath\* -include *.cab,*.msu)) {
# write-verbose "Applying update at $update to directory at $wimMountSubdir"
# try {
# Add-WindowsPackage -PackagePath $update -path $wimMountSubdir | out-null
# }
# catch {
# write-verbose "Failed to add package '$update' to mounted WIM at '$wimMountSubdir' with error '$_'; continuing..."
# }
# }
Dismount-WindowsImage -Path $wimMountSubdir -Save
}
New-WindowsInstallMedia -sourceIsoPath $inputIso -installMediaTemp $installMediaTemp -installWimPath $installWim -outputIsoPath $outputIso
}
function Build-PackerFile {
@ -314,18 +289,7 @@ if ($DownloadWSUS) {
Download-WindowsUpdates
}
if ($ApplyWSUS) {
# Doing this different while in development
Apply-WindowsUpdatesToIso -iso $isoPath -wsusOfflineDir $wsusOfflineDir -wimMountDir $wimMountDir
<#
$installWim = resolve-path $labTempDir\install_81_x86.wim | select -expand path
$wimInfo = Get-DismWimInfo -wimFile $installWim
$bitness = "i386"
$shortCode = Get-WOShortCode -OSName $wimInfo[0].Name -OSBitness $bitness
$updatePath = resolve-path "${wsusOfflineDir}\client\$shortCode\glb" | select -expand Path
Apply-WindowsUpdatesToWim -Verbose -wimFile $installWim -wimMountDir $wimMountDir -winUpdateDir $updatePath
#>
throw "TODO: what about the cmdlets in 'gcm -module dism' ??"
throw "TODO: to convert this back to an iso, I have to have the WAIK and oscdimg.exe it looks like. Ugh."
Apply-WindowsUpdatesToIso -inputIso $isoPath -outputIso $newMediaIsoPath -wsusOfflineDir $wsusOfflineDir -wimMountDir $wimMountDir -verbose:$verbose
}
if ($BuildPacker) {
$bpfParam = @{

@ -4,11 +4,14 @@ fucking Packer
#>
<#
.notes
This is intended for use in the postinstall phase.
Only functions that were intended to run in that phase should have the concept of a "LabTempDir".
TODO: make sure this is always a 100% normalized path
#>
function Get-LabTempDir {
write-verbose "Function: $($MyInvocation.MyCommand)..."
if ("${script:WinTrialLabTemp}") {}
if ("${script:WinTrialLabTemp}") {} # noop
elseif ("${env:WinTrialLabTemp}") {
$script:WinTrialLabTemp = $env:WinTrialLabTemp
}
@ -31,12 +34,35 @@ function Invoke-ExpressionAndCheck {
[parameter(mandatory=$true)] [string] $command
)
$global:LASTEXITCODE = 0
write-verbose "Invoking expression '$command'"
invoke-expression -command $command
write-verbose "Expression '$command' had a last exit code of '$LastExitCode'"
if ($global:LASTEXITCODE -ne 0) {
throw "LASTEXITCODE: ${global:LASTEXITCODE} for command: '${command}'"
}
}
# function Copy-ItemAndExclude {
# [cmdletbinding()] param(
# [parameter(mandatory=$true)] [string] $path,
# [parameter(mandatory=$true)] [string] $destination,
# [parameter(mandatory=$true)] [string[]] $exclude,
# [switch] $force
# )
# $path = resolve-path $path | select -expand path
# $sourceItems = Get-ChildItem -Path $path -Recurse -Exclude $exclude
# write-verbose "Found $($sourceItems.count) items to copy from '$path'"
# #$sourceItems | copy-item -force:$force -destination {Join-Path $destination $_.FullName.Substring($path.length)}
# $sourceItems | copy-item -force:$force -destination {
# if ($_.GetType() -eq [System.IO.FileInfo]) {
# Join-Path $destination $_.FullName.Substring($path.length)
# }
# else {
# Join-Path $destination $_.Parent.FullName.Substring($path.length)
# }
# }
# }
function Get-WebUrl {
param(
[parameter(mandatory=$true)] [string] $url,
@ -68,6 +94,11 @@ function Get-WebUrl {
(New-Object System.Net.WebClient).DownloadFile($url, $downloadPath)
}
$ArchitectureId = @{
i386 = "i386"
amd64 = "amd64"
}
<#
.description
Return the OS Architecture, as determined by WMI
@ -81,15 +112,10 @@ function Get-OSArchitecture {
write-verbose "Function: $($MyInvocation.MyCommand)..."
#reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OSARCHITECTURE=32BIT || set OSARCHITECTURE=64BIT
$OSArch = Get-WmiObject -class win32_operatingsystem -property osarchitecture | select -expand OSArchitecture
if ($OSArch -match "64") {
return "amd64"
}
elseif ($OSArch -match "32") {
return "i386"
}
else {
throw "Could not determine OS Architecture from string '$OSArch'"
}
if ($OSArch -match "64") { return $ArchitectureId.amd64 }
elseif ($OSArch -match "32") { return $ArchitectureId.i386 }
else { throw "Could not determine OS Architecture from string '$OSArch'" }
}
function Test-AdminPrivileges {
@ -194,8 +220,6 @@ function Show-ErrorReport {
exit 1
}
}
set-alias err Show-ErrorReport
$script:szInstallDir = "$env:ProgramFiles\7-Zip"
set-alias sevenzip "${script:szInstallDir}\7z.exe"
@ -218,13 +242,6 @@ function Install-SevenZip {
write-verbose "Downloaded '$szUrl' to '$szDlPath', now running msiexec..."
#msiexec /qn /i "$szDlPath"
#[Diagnostics.Process]::Start("msiexec",@("/quiet","/qn","/i",$szDlPath)).WaitForExit()
#[Diagnostics.Process]::Start("msiexec", "/i","`"$szDlPath`","/q","/INSTALLDIR=`"$szInstallDir`"")).WaitForExit()
#msiexec /i "`"$sqlDlPath`"" /q "/INSTALLDIR=`"$script:szInstallDir`""
#$msiArgs = '/i "${0}" /q /INSTALLDIR="{1}"' -f $szDlPath, $szInstallDir
#$msiArgs = '/i "${0}" /qn /INSTALLDIR="{1}"' -f $szDlPath, $szInstallDir
#([Diagnostics.Process]::Start("msiexec", $msiArgs)).WaitForExit()
msiexec /qn /i "$szDlPath"
sleep 30 # Windows is bad, written by bad people who write bad software. More like softWHEREdidyougetthisideaitSUCKS amirite??
if ($LASTEXITCODE -and ($LASTEXITCODE -ne 0)) { throw "External command failed with code '$LASTEXITCODE'" }
@ -495,11 +512,156 @@ function Set-PasswordExpiry {
cmd.exe /c wmic useraccount where "name='$accountName'" set "PasswordExpires=$pe"
}
$exAlias = @("sevenzip")
$exFunction = @(
"Get-OSArchitecture"
"Get-LabTempDir"
"Install-SevenZip"
"Install-VBoxAdditions"
)
export-modulemember -alias * -function *
<#
.description
Get the path of the Windows ADK or AIK or whatever the fuck they're calling it from a format string
- {0} is always the WAIK directory
- e.g. "C:\Program Files (x86)\Windows Kits\8.1\"
- e.g. "X:\Program Files\Windows Kits\8.0"
- {1} is always the host architecture (x86 or amd64)
- i THINK this is right, but I don't understand WHY. why do you need an amd64 version of oscdimg.exe?
- however, there are arm executables lying around, and i definitely can't execute those. wtf?
So we expect a string like "{0}\bin\{1}\wsutil.exe"
#>
function Get-AdkPath {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $pathFormatString
)
$adkPath = ""
$possibleAdkPaths = @("${env:ProgramFiles(x86)}\Windows Kits\8.1","${env:ProgramFiles}\Windows Kits\8.1")
$possibleAdkPaths |% { if (test-path $_) { $adkPath = $_ } }
if (-not $adkPath) { throw "Could not find the Windows Automated Installation Kit" }
write-verbose "Found the WAIK at '$adkPath'"
$arch = Get-OSArchitecture
switch ($arch) {
$ArchitectureId.i386 {
$formatted = $pathFormatString -f $adkPath,$waikArch
if (test-path $formatted) { return $formatted }
}
$ArchitectureId.amd64 {
foreach ($waikArch in @("amd64","x64")) {
$formatted = $pathFormatString -f $adkPath,$waikArch
if (test-path $formatted) { return $formatted }
}
}
default {
throw "Could not determine architecture of '$arch'"
}
}
throw "Could not resolve format string '$pathFormatString' to an existing path"
}
function New-WindowsInstallMedia {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $sourceIsoPath,
[parameter(mandatory=$true)] [string] $installMediaTemp, # WILL BE DELETED
[parameter(mandatory=$true)] [string] $installWimPath, # your new install.wim file
[parameter(mandatory=$true)] [string] $outputIsoPath
)
$oscdImgPath = Get-AdkPath "{0}\Assessment and Deployment Kit\Deployment Tools\{1}\Oscdimg\oscdimg.exe"
$installWimPath = resolve-path $installWimPath | select -expand path
$installMediaTemp = mkdir -force $installMediaTemp | select -expand fullname
$outputIsoParentPath = split-path $outputIsoPath -parent
$outputIsoFilename = split-path $outputIsoPath -leaf
$outputIsoParentPath = mkdir -force $outputIsoParentPath | select -expand fullname
if (test-path $installMediaTemp) { rm -recurse -force $installMediaTemp }
mkdir -force $installMediaTemp | out-null
$diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume
if (-not $diskVol) {
mount-diskimage -imagepath $sourceIsoPath
$diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume
}
$driveLetter = $diskVol | select -expand DriveLetter
$existingInstallMediaDir = "${driveLetter}:"
# TODO: the first copy here copies the original install.wim, and the second copies the new one over it
# this is really fucking dumb right? but then, THIS is way fucking dumber:
# http://stackoverflow.com/questions/731752/exclude-list-in-powershell-copy-item-does-not-appear-to-be-working
# PS none of those solutions are generic enough to get included so fuck it
copy-item -recurse -path "$existingInstallMediaDir\*" -destination "$installMediaTemp" -verbose:$verbose
remove-item -force -path "$installMediaTemp\sources\install.wim"
copy-item -path $installWimPath -destination "$installMediaTemp\sources\install.wim" -force -verbose:$verbose
$etfsBoot = resolve-path "$existingInstallMediaDir\boot\etfsboot.com" | select -expand Path
$oscdimgCall = '& "{0}" -m -n -b"{1}" "{2}" "{3}"' -f @($oscdImgPath, $etfsBoot, $installMediaTemp, $outputIsoPath)
write-verbose "Calling OSCDIMG: '$oscdimgCall"
Invoke-ExpressionAndCheck $oscdimgCall -verbose:$verbose
dismount-diskimage -imagepath $sourceIsoPath
}
<#
.notes
For use with WSUS Offline Updater
#>
function Get-WOShortCode {
param(
[parameter(mandatory=$true)] [string] $OSName,
[parameter(mandatory=$true)] [string] $OSArchitecture
)
# I'm adding to this list slowly, only as I encounter the actual names from install.wim
# on the trial CDs when I actually try to install them
$shortCodeTable = @{
"8.1" = "w63"
}
$shortCodeTable.keys |% { if ($OSName -match $_) { $shortCode = $shortCodeTable[$_] } }
if (-not $shortCode) { throw "Could not determine shortcode for an OS named '$OSName'" }
if ($OSArchitecture -match $ArchitectureId.i386) { $shortCode += "" }
elseif ($OSArchitecture -match $ArchitectureId.amd64) { $shortCode += "-x64" }
else { throw "Could not determine shortcode for an OS of architecture '$OSArchitecture'" }
write-verbose "Found shortcode '$shortcode' for OS named '$OSName' of architecture '$OSArchitecture'"
return $shortCode
}
### TEMP SECTION
# This section contains stuff that's probably only useful in development
function Get-DownloadedUpdates {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $wsusOfflineClientDir
)
$updateFiles = ls $wsusOfflineClientDir\w*\*\*kb*
#$updateFiles = ls $wsusOfflineClientDir\w63\glb\*kb* | select -first 20
$updates = @()
foreach ($u in $updateFiles) {
$update = New-Object PSObject -Property @{ Item = $u }
if ($u.name -match ".*(kb[0-9]+(\-v[0-9]+)?).*") {
$kb = $matches[1]
Add-Member -inputObject $update -NotePropertyMembers @{KB=$kb}
}
$updates += @($update)
}
return $updates
}
function SearchUpdatesList {
[cmdletbinding()] param(
[parameter(mandatory=$true)] $updateList,
[parameter(mandatory=$true)] [string] $kbFragment
)
return $updateList |? { $_.kb -match $kbFragment }
}
# Exports:
$emmParams = @{
Alias = @("sevenzip")
Variable = @("ArchitectureId")
Function = "*"
# Function = @(
# "Get-OSArchitecture"
# "Get-LabTempDir"
# "Install-SevenZip"
# "Install-VBoxAdditions"
# )
}
export-modulemember @emmParams

Loading…
Cancel
Save