diff --git a/buildlab.ps1 b/buildlab.ps1 index 6ecd7a3..327d4cf 100644 --- a/buildlab.ps1 +++ b/buildlab.ps1 @@ -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 = @{ diff --git a/scripts/postinstall/wintriallab-postinstall.psm1 b/scripts/postinstall/wintriallab-postinstall.psm1 index bab5273..e97f7cf 100644 --- a/scripts/postinstall/wintriallab-postinstall.psm1 +++ b/scripts/postinstall/wintriallab-postinstall.psm1 @@ -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