From 8a999fdd58c96fe823aaba9bcd5a9650b3f538a4 Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Mon, 2 Nov 2015 17:27:46 -0600 Subject: [PATCH] Reliably start WinRM, support Win10x86, fix other Big features: - Reliably start WinRM now!! - Support Windows 10 x86! Smaller features - Improve README - Don't power off the (virtual) monitor to save power - Add Invoke-ScriptblockAndCatch and use it in postinstall scripts Fixes and reorgs: - Move all the packer stuff inside the packer/ directory - Break out possibly-nonfunctional slipstream stuff to its own module - Clean up broken bits in buildlab - Clean up vestigial bits in wintriallab-postinstall - Fix lots of broken pieces after encountering them one by one - Fix RestartAction stuff in autounattend-postinstall --- buildlab.ps1 | 121 +---- packer/windows_10_x86/Autounattend.xml | 154 +++++++ .../vagrantfile-windows_10_x86.template | 27 ++ .../windows_10_x86.packerfile.json | 54 +++ .../windows_81_x86}/Autounattend.xml | 10 +- .../vagrantfile-windows_81_x86.template | 0 .../windows_81_x86.packerfile.json | 19 +- readme.markdown | 60 ++- scripts/autounattend-postinstall.ps1 | 30 +- scripts/enable-winrm.ps1 | 31 +- scripts/provisioner-postinstall.ps1 | 33 +- scripts/slipstream.psm1 | 179 ++++++++ scripts/win-updates.ps1 | 12 +- scripts/wintriallab-postinstall.psm1 | 427 ++++++------------ slipstream.ps1 | 35 -- todo.markdown | 17 - 16 files changed, 667 insertions(+), 542 deletions(-) create mode 100644 packer/windows_10_x86/Autounattend.xml create mode 100644 packer/windows_10_x86/vagrantfile-windows_10_x86.template create mode 100644 packer/windows_10_x86/windows_10_x86.packerfile.json rename {windows_81_x86 => packer/windows_81_x86}/Autounattend.xml (96%) rename {windows_81_x86 => packer/windows_81_x86}/vagrantfile-windows_81_x86.template (100%) rename {windows_81_x86 => packer/windows_81_x86}/windows_81_x86.packerfile.json (68%) create mode 100644 scripts/slipstream.psm1 delete mode 100644 slipstream.ps1 delete mode 100644 todo.markdown diff --git a/buildlab.ps1 b/buildlab.ps1 index b2a552e..14aed64 100644 --- a/buildlab.ps1 +++ b/buildlab.ps1 @@ -16,14 +16,10 @@ param( [parameter(mandatory=$true,ParameterSetName="VagrantUp")] [string] $baseConfigName, - [parameter(mandatory=$true,ParameterSetName="DownloadWSUS")] [switch] $DownloadWSUS, - [parameter(mandatory=$true,ParameterSetName="ApplyWSUS")] [switch] $ApplyWSUS, [parameter(mandatory=$true,ParameterSetName="BuildPacker")] [switch] $BuildPacker, [parameter(mandatory=$true,ParameterSetName="AddToVagrant")] [switch] $AddToVagrant, [parameter(mandatory=$true,ParameterSetName="VagrantUp")] [switch] $VagrantUp, - [parameter(mandatory=$true,ParameterSetName="ApplyWSUS")] [string] $isoPath, - [string] $baseOutDir = "D:\iso\wintriallab", [string] $tempDirOverride, [string] $tag, @@ -31,21 +27,9 @@ param( [switch] $whatIf ) -import-module dism -verbose:$false - -# Module useful for Download-URL at least. TODO: this mixes concerns and may not be ideal? -get-module wintriallab-postinstall | remove-module -import-module $PSScriptRoot\scripts\wintriallab-postinstall.psm1 -verbose:$false - +$errorActionPreference = "Stop" Set-StrictMode -Version 2.0 -# This seems to be required with strict mode? -$verbose = $true -# 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" $packerCacheDir = "$baseOutDir\packer_cache" @@ -59,7 +43,6 @@ $wimMountDir = "${labTempDir}\MountInstallWim" $installMediaTemp = "${labTempDir}\InstallMedia" $newMediaIsoPath = "${labTempDir}\windows.iso" -$errorActionPreference = "Stop" $fullConfigName = "wintriallab-${baseConfigName}" set-alias packer (gcm packer | select -expand path) @@ -69,98 +52,11 @@ set-alias vagrant (gcm vagrant | select -expand path) $outDir = "${packerOutDir}\${fullConfigName}" if ($tag) { $outDir += "-${tag}"} -$packerConfigRoot = "${PSScriptRoot}\${baseConfigName}" +$packerConfigRoot = "${PSScriptRoot}\packer\${baseConfigName}" $packerFile = "${packerConfigRoot}\${baseConfigName}.packerfile.json" $packedBoxPath = "${outDir}\${baseConfigName}_virtualbox.box" $vagrantTemplate = "${packerConfigRoot}\vagrantfile-${baseConfigName}.template" -function Download-WSUSOfflineUpdater { - if (test-path $wsusOfflineDir) { - throw "WSUSOffline is already extracted to '$wsusOfflineDir'" - } - $filename = "wsusoffline101.zip" - $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 - sevenzip x "$dlPath" "-o$exDir" -} -function Download-WindowsUpdates { - set-alias DownloadUpdates "$wsusOfflineDir\cmd\DownloadUpdates.cmd" - foreach ($product in @('w63','w63-x64','w100','w100-x64')) { - DownloadUpdates $product glb /includedotnet /verify - } -} - -<# -.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 - ) - $bootWimInfo = Get-WindowsImage -imagePath $wimFile -verbose:$verbose - - $arch = $null - if (-not $bootWimInfo) { throw "Got no information for wimfile at '$wimFile'"} - 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'"} - - write-verbose "Found an architecture of '$arch' for '$wimFile'" - return $arch -} - - -function Apply-WindowsUpdatesToIso { - [cmdletbinding()] param ( - [parameter(mandatory=$true)] [string] $inputIso, - [parameter(mandatory=$true)] [string] $outputIso, - [parameter(mandatory=$true)] [string] $wsusOfflineDir, - [parameter(mandatory=$true)] [string] $wimMountDir - ) - - $myWimMounts = @() - - 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 $installWim -name IsReadOnly -value $false -force - - $arch = Get-BootWimArchitecture -wimFile "${mountedDrive}:\sources\boot.wim" -verbose:$verbose - dismount-diskimage -imagepath $inputIso - - $wimInfo = Get-WindowsImage -imagePath $installWim - $shortCode = Get-WOShortCode -OSName $wimInfo[0].ImageName -OSArchitecture $arch - #$updatePath = resolve-path "${wsusOfflineDir}\client\$shortCode\glb" | select -expand Path - $updatePath = "D:\iso\wintriallab\temp-slipstream\WSUSCache\w63-i386-glb" - - foreach ($wimInfo in (Get-WindowsImage -imagePath $installWim)) { - $wimMountSubdir = mkdir "${wimMountDir}\$($wimInfo.ImageIndex)" -force | select -expand fullname - Mount-WindowsImage -imagePath $installWim -index $wimInfo.ImageIndex -path $wimMountSubdir - - write-verbose "Applying '$((ls $updatePath).count)' updates to '$wimInfo'''" - try { - Add-WindowsPackage -PackagePath $updatePath -path $wimMountSubdir - } - catch { - write-verbose "Caught error(s) when installing packages:`n`n$_`n" - } - - Dismount-WindowsImage -Path $wimMountSubdir -Save - } - - New-WindowsInstallMedia -sourceIsoPath $inputIso -installMediaTemp $installMediaTemp -installWimPath $installWim -outputIsoPath $outputIso -} - function Build-PackerFile { [cmdletbinding()] param( @@ -199,10 +95,10 @@ function Build-PackerFile { popd } $outBox = get-item $outDir\*.box - if ($outBox.count -gt 1) { + if ($outBox.PSObject.Properties['count'] -and $outBox.count -gt 1) { throw "Somehow you came up with more than one box here: '$outBox'" } - if ($outBox -notmatch [Regex]::Escape($packedBoxPath)) { + if ($outBox.fullname -notmatch [Regex]::Escape($packedBoxPath)) { throw "Found an output box '$outBox', but it doesn't match the expected packed box path of '$packedBoxPath'" } cp "$vagrantTemplate" "$outDir\Vagrantfile" @@ -279,15 +175,6 @@ if ($baseConfigName) { write-output "" } -if ($DownloadWSUS) { - if (-not (test-path $wsusOfflineDir)) { - Download-WSUSOfflineUpdater - } - Download-WindowsUpdates -} -if ($ApplyWSUS) { - Apply-WindowsUpdatesToIso -inputIso $isoPath -outputIso $newMediaIsoPath -wsusOfflineDir $wsusOfflineDir -wimMountDir $wimMountDir -verbose:$verbose -} if ($BuildPacker) { $bpfParam = @{ packerFile = $packerFile diff --git a/packer/windows_10_x86/Autounattend.xml b/packer/windows_10_x86/Autounattend.xml new file mode 100644 index 0000000..0dac836 --- /dev/null +++ b/packer/windows_10_x86/Autounattend.xml @@ -0,0 +1,154 @@ + + + + + + + + + + 1 + Primary + true + + + + + false + NTFS + C + 1 + 1 + + + + 0 + true + + OnError + + + true + + + + + NPPR9-FWDCX-D2C8J-H872K-2YT43 + Never + + + + + + 0 + 1 + + OnError + false + + + /IMAGE/NAME + Windows 10 Enterprise Evaluation + + + + + + + + en-US + + en-US + en-US + en-US + en-US + en-US + + + + + false + + + + + + + V@grant123 + true</PlainText> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>V@grant123</Value> + <PlainText>true</PlainText> + </Password> + <Description>Vagrant User</Description> + <DisplayName>vagrant</DisplayName> + <Group>administrators</Group> + <Name>vagrant</Name> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Home</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + </OOBE> + <AutoLogon> + <Password> + <Value>V@grant123</Value> + <PlainText>true</PlainText> + </Password> + <Username>vagrant</Username> + <Enabled>true</Enabled> + </AutoLogon> + <FirstLogonCommands> + + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c powershell -ExecutionPolicy Unrestricted -Command "Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force"</CommandLine> + <Description>Set Execution Policy</Description> + <Order>1</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + + <SynchronousCommand wcm:action="add"> + <!-- This task MUST include enabling winrm for Packer to be able to continue --> + <CommandLine>powershell.exe -File A:\autounattend-postinstall.ps1</CommandLine> + <Description>Run Postinstall Script</Description> + <Order>99</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + + </FirstLogonCommands> + <ShowWindowsLive>false</ShowWindowsLive> + </component> + </settings> + <settings pass="specialize"> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <OEMInformation> + <HelpCustomized>false</HelpCustomized> + </OEMInformation> + <!-- Rename computer here. --> + <ComputerName>vagrant-10-x86</ComputerName> + <TimeZone>Central Standard Time</TimeZone> + <RegisteredOwner/> + </component> + <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <SkipAutoActivation>true</SkipAutoActivation> + </component> + </settings> + <cpi:offlineImage xmlns:cpi="urn:schemas-microsoft-com:cpi" cpi:source="catalog:d:/sources/install_windows 7 ENTERPRISE.clg"/> +</unattend> diff --git a/packer/windows_10_x86/vagrantfile-windows_10_x86.template b/packer/windows_10_x86/vagrantfile-windows_10_x86.template new file mode 100644 index 0000000..6631343 --- /dev/null +++ b/packer/windows_10_x86/vagrantfile-windows_10_x86.template @@ -0,0 +1,27 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.require_version ">= 1.6.2" + +Vagrant.configure("2") do |config| + config.vm.define "vagrant-windows-10" + config.vm.box = "windows_10" + config.vm.communicator = "winrm" + + # Admin user name and password + config.winrm.username = "vagrant" + config.winrm.password = "V@grant123" + + config.vm.guest = :windows + config.windows.halt_timeout = 15 + + config.vm.network :forwarded_port, guest: 3389, host: 3389, id: "rdp", auto_correct: true + + config.vm.provider :virtualbox do |v, override| + #v.gui = true + v.customize ["modifyvm", :id, "--memory", 1024] + v.customize ["modifyvm", :id, "--cpus", 1] + v.customize ["setextradata", "global", "GUI/SuppressMessages", "all" ] + end + +end diff --git a/packer/windows_10_x86/windows_10_x86.packerfile.json b/packer/windows_10_x86/windows_10_x86.packerfile.json new file mode 100644 index 0000000..2d0df3c --- /dev/null +++ b/packer/windows_10_x86/windows_10_x86.packerfile.json @@ -0,0 +1,54 @@ +{ + "variables": { + "output_directory": "packer-output" + }, + "builders": [ + { + "type": "virtualbox-iso", + "iso_url": "http://care.dlservice.microsoft.com/dl/download/C/3/9/C399EEA8-135D-4207-92C9-6AAB3259F6EF/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO", + "iso_checksum_type": "sha1", + "iso_checksum": "875b450d67e7176b8b3c72a80c60a0628bf1afac", + "headless": true, + "boot_wait": "2m", + "communicator": "winrm", + "winrm_username": "vagrant", + "winrm_password": "V@grant123", + "winrm_timeout": "72h", + "shutdown_command": "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\"", + "guest_os_type": "Windows10", + "disk_size": 61440, + "guest_additions_mode": "attach", + "floppy_files": [ + "./Autounattend.xml", + "../../scripts/win-updates.ps1", + "../../scripts/enable-winrm.ps1", + "../../scripts/wintriallab-postinstall.psm1", + "../../scripts/autounattend-postinstall.ps1", + "../../scripts/provisioner-postinstall.ps1" + ], + "vboxmanage": [ + ["setextradata", "global", "GUI/SuppressMessages", "all" ], + ["modifyvm", "{{.Name}}", "--memory", "2048" ], + ["modifyvm", "{{.Name}}", "--cpus", "2" ], + ["modifyvm", "{{.Name}}", "--accelerate2dvideo", "on"], + ["modifyvm", "{{.Name}}", "--vram", 128] + ] + } + ], + "provisioners": [ + + { + "type": "powershell", + "inline": ["A:\\provisioner-postinstall.ps1 -Verbose"] + } + + ], + "post-processors": [ + { + "type": "vagrant", + "keep_input_artifact": false, + "output": "{{user `output_directory`}}/windows_10_x86_{{.Provider}}.box", + "vagrantfile_template": "vagrantfile-windows_10_x86.template" + } + ] +} diff --git a/windows_81_x86/Autounattend.xml b/packer/windows_81_x86/Autounattend.xml similarity index 96% rename from windows_81_x86/Autounattend.xml rename to packer/windows_81_x86/Autounattend.xml index 344d39c..5e453e7 100644 --- a/windows_81_x86/Autounattend.xml +++ b/packer/windows_81_x86/Autounattend.xml @@ -29,14 +29,8 @@ </DiskConfiguration> <UserData> <AcceptEula>true</AcceptEula> - - <!-- do I need this for anything at all? - <FullName>Vagrant Administrator</FullName> - <Organization>Vagrant Inc.</Organization> - --> - <!-- - NOTE: If you are re-configuring this for use of a retail key + NOTE: If you are re-configuring this for use of a retail key and using a retail ISO, you need to adjust the <ProductKey> block below to look like this: @@ -137,7 +131,7 @@ <Order>99</Order> <RequiresUserInput>true</RequiresUserInput> </SynchronousCommand> - + </FirstLogonCommands> <ShowWindowsLive>false</ShowWindowsLive> </component> diff --git a/windows_81_x86/vagrantfile-windows_81_x86.template b/packer/windows_81_x86/vagrantfile-windows_81_x86.template similarity index 100% rename from windows_81_x86/vagrantfile-windows_81_x86.template rename to packer/windows_81_x86/vagrantfile-windows_81_x86.template diff --git a/windows_81_x86/windows_81_x86.packerfile.json b/packer/windows_81_x86/windows_81_x86.packerfile.json similarity index 68% rename from windows_81_x86/windows_81_x86.packerfile.json rename to packer/windows_81_x86/windows_81_x86.packerfile.json index d1cc996..6460f2f 100644 --- a/windows_81_x86/windows_81_x86.packerfile.json +++ b/packer/windows_81_x86/windows_81_x86.packerfile.json @@ -13,22 +13,25 @@ "communicator": "winrm", "winrm_username": "vagrant", "winrm_password": "V@grant123", - "winrm_timeout": "8h", + "winrm_timeout": "72h", "shutdown_command": "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\"", "guest_os_type": "Windows81", "disk_size": 61440, "guest_additions_mode": "attach", "floppy_files": [ "./Autounattend.xml", - "../scripts/win-updates.ps1", - "../scripts/enable-winrm.ps1", - "../scripts/wintriallab-postinstall.psm1", - "../scripts/autounattend-postinstall.ps1", - "../scripts/provisioner-postinstall.ps1" + "../../scripts/win-updates.ps1", + "../../scripts/enable-winrm.ps1", + "../../scripts/wintriallab-postinstall.psm1", + "../../scripts/autounattend-postinstall.ps1", + "../../scripts/provisioner-postinstall.ps1" ], "vboxmanage": [ - [ "modifyvm", "{{.Name}}", "--memory", "2048" ], - [ "modifyvm", "{{.Name}}", "--cpus", "2" ] + ["setextradata", "global", "GUI/SuppressMessages", "all" ], + ["modifyvm", "{{.Name}}", "--memory", "2048" ], + ["modifyvm", "{{.Name}}", "--cpus", "2" ], + ["modifyvm", "{{.Name}}", "--accelerate2dvideo", "on"], + ["modifyvm", "{{.Name}}", "--vram", 128] ] } ], diff --git a/readme.markdown b/readme.markdown index 0def2aa..f7b8b47 100644 --- a/readme.markdown +++ b/readme.markdown @@ -1,17 +1,65 @@ windows-trial-lab: scripts for building one or more machines from Windows trial ISOs +## Credits + +This started as some customizations for [joefitzgerald/packer-windows](https://github.com/joefitzgerald/packer-windows) that got a liiiiiiiittle out of hand. + +These were the *types* of changes I'm trying to make: + +- I rewrote their Windows Update script to be much more readable (imo). Now it has clearly defined functions with parameter blocks, you can set the postinstall step when calling it (rather than hardcoding calling `A:\openssh.ps1`) and you only have to set it once, and all functions MAY read global variables set at the top level but DO NOT write to them. +- I want to use WinRM rather than OpenSSH + - As a result of this, I don't copy anything to the host for provisioning, because this is buggy with WinRM +- I rewrote lots of their scripts as functions in my Powershell module +- I log to Windows Event Log + +And these are some specific changes that may impact you + +- The original project has [a way to install KB2842230](https://github.com/joefitzgerald/packer-windows/blob/master/scripts/hotfix-KB2842230.bat). I haven't run into this problem, but if I did, I'd have to figure this one out too. I'm not sure but it appears that they have an installer script but not a downloader script - it's unclear whether people are actually using this or not. +- The original project has [a script that forces all network locations to be private](https://github.com/joefitzgerald/packer-windows/blob/master/scripts/fixnetwork.ps1), which is necessary to enable PS Remoting. I haven't hit a problem that this solved yet, so I don't include it. + - The Windows 10 Autounattend.xml also sets the NewNetworkWindowOff registry key, per <https://technet.microsoft.com/en-us/library/gg252535%28v=ws.10%29.aspx>, by doing `cmd.exe /c reg add "HKLM\System\CurrentControlSet\Control\Network\NewNetworkWindowOff"`, before running the fixnetwork.ps1 script. +- I don't have VMware, only VirtualBox, so all the VMware support was removed (since I was rewriting it and couldn't test it, this seemed like the right thing to do) +- I use WinRM rather than OpenSSH +- I don't include installers for puppet/salt/etc +- I had to change the vagrant user's password to something more complex so you could remote in to it + ## Layout and script purpose - marionettist/windows-trial-lab/ + - buildlab.ps1 # controls the whole flow of everything - scripts/ - windeploy-marionettist/ - windeploy-marionettist.psm1 - (etc) - autounattend-postinstall.ps1 # run from Autounattend.xml, contains hardcoded values - - packer-postinstall.ps1 # run by a packer provisioner, contains hardcoded values - - download-windowsupdates.ps1 # run on schedule, should use a config file somewhere - - build-updatedwindowsisos.ps1 # run on schedule, should use a config file somewhere - - build-vagrantboxes.ps1 # run on schedule, should use a config file somewhere - - buildlab.ps1 # ?? maybe? controls the whole flow of everything? I at least need something that talks to Packer - - (any other scripts) + - provisioner-postinstall.ps1 # run by a packer provisioner, contains hardcoded values + - win-updates.ps1 # run from autounattend-postinstall if desired, reboots system repeatedly + - enable-winrm.ps1 # run from autounattend-postinstall + - packer/ + - (folders for each version of Windows) + +## To do + +buildlab.ps1 improvements: + +- would like to use the -tag in the name for the vagrant box too, but that requires parameterizing both the packerfile and the vagrantfile template :/ not sure what to do about this +- I have a concept of "packer basename" and "tag" in buildlab. Extend this to also have "architecture" and "flavor" (or something - to capture Server Standard vs Core vs Datacenter etc) + +packer/vagrant/postinstall improvements: + +- store passwords securely for shit and/or generate them on the fly +- use client certs for WinRM: https://msdn.microsoft.com/en-us/library/aa384295%28v=vs.85%29.aspx +- enable clipboard and drag&drop in my Vagrantfile - though NOT for throwaway VMs that might be insecure! +- would be great if I didn't have duplicated Autounattend.xml files everywhere - can I templatize this? +- disable monitor blanking (useful for seeing where your VM is at from VBox's Manager app without actually opening the window) + +other improvements + +- I really wish I had a way to slipstream updates into ISOs so the first Windows Update run is just getting recent stuff. There are 150+ updates for Win 8.1 at first boot, and these take a few hours to install. Ughhhh. + +upstream improvements + +- It's possible that the original project might be interested in some of the stuff I've done, particularly the Windows Update work, and maybe even my postinstall module. Clean up the code and submit it to them and see what they think. + +## Whines +- The shell, windows-shell, and powershell provisioners are VERY finicky. I canNOT make them work reliably. The easiest thing I can figure out how to do is to use a Powershell provisioner to call a file with no arguments over WinRM. lmfao diff --git a/scripts/autounattend-postinstall.ps1 b/scripts/autounattend-postinstall.ps1 index bae6121..f98c701 100644 --- a/scripts/autounattend-postinstall.ps1 +++ b/scripts/autounattend-postinstall.ps1 @@ -1,29 +1,25 @@ -[cmdletbinding()] -param() +[cmdletbinding()] param() import-module $PSScriptRoot\wintriallab-postinstall.psm1 -try { +Invoke-ScriptblockAndCatch -scriptBlock { Write-EventLogWrapper "Starting the autounattend postinstall script" + Set-IdleDisplayPoweroffTime -seconds 0 Set-PasswordExpiry -accountName "vagrant" -expirePassword $false Disable-HibernationFile Enable-MicrosoftUpdate + Set-AllNetworksToPrivate # Required for Windows 10, not required for 81, not sure about other OSes Install-VBoxAdditions -fromDisc # Need to reboot for some of these drivers to take # To reboot, then run Windows updates, then enable WinRM: - $restartCommand = "$PSHOME\powershell.exe -File A:\win-updates.ps1 -CalledFromRegistry -RestartAction RunAtLogon -PostUpdateExpression '$PSHOME\powershell.exe -File A:\enable-winrm.ps1'" - - # To reboot, then run winrm immediately without Windows Update - #$restartCommand = "$PSHOME\powershell.exe -File A:\enable-winrm.ps1" - - Set-RestartRegistryEntry -restartAction RunAtLogon -restartCommand $restartCommand + $winRmCommand = "$PSHome\powershell.exe -File A:\enable-winrm.ps1" + $winUpdateCpmmand = "$PSHOME\powershell.exe -File A:\win-updates.ps1 -CalledFromRegistry -RestartAction RunAtLogon -PostUpdateExpression `"$winRmCommand`"" + + # To install Windows Updates then enable WinRM after reboot: + Set-RestartRegistryEntry -restartAction RunAtLogon -restartCommand $winUpdateCpmmand + + # To just enable WinRM without installing updates after reboot: + #Set-RestartRegistryEntry -restartAction RunAtLogon -restartCommand $winRmCommand + Restart-Computer -force } -catch { - $message = "======== CAUGHT EXCEPTION ========`r`n$_`r`n" - $message += "======== ERROR STACK ========" - $error |% { $message += "$_`r`n----`r`n" } - $message += "======== ========" - Write-EventLogWrapper $message - exit 666 -} diff --git a/scripts/enable-winrm.ps1 b/scripts/enable-winrm.ps1 index 6f96c39..c5c77aa 100644 --- a/scripts/enable-winrm.ps1 +++ b/scripts/enable-winrm.ps1 @@ -1,18 +1,13 @@ -# I've had the best luck doing it this way - NOT doing it in a single batch script -# Sometimes one of these commands will stop further execution in a batch script, but when I -# call cmd.exe over and over like this, that problem goes away. -cmd.exe /c 'winrm quickconfig -q' -cmd.exe /c 'winrm quickconfig -transport:http' -cmd.exe /c 'winrm set winrm/config @{MaxTimeoutms="1800000"}' -cmd.exe /c 'winrm set winrm/config/winrs @{MaxMemoryPerShellMB="2048"}' -cmd.exe /c 'winrm set winrm/config/service @{AllowUnencrypted="true"}' -cmd.exe /c 'winrm set winrm/config/client @{AllowUnencrypted="true"}' -cmd.exe /c 'winrm set winrm/config/service/auth @{Basic="true"}' -cmd.exe /c 'winrm set winrm/config/client/auth @{Basic="true"}' -cmd.exe /c 'winrm set winrm/config/service/auth @{CredSSP="true"}' -cmd.exe /c 'winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"}' -cmd.exe /c 'netsh advfirewall firewall set rule group="remote administration" new enable=yes' -cmd.exe /c 'netsh firewall add portopening TCP 5985 "Port 5985"' -cmd.exe /c 'net stop winrm' -cmd.exe /c 'sc.exe config winrm start= auto' -cmd.exe /c 'net start winrm' +import-module $PSScriptRoot\wintriallab-postinstall.psm1 + +try { + Enable-WinRM +} +catch { + $message = "======== CAUGHT EXCEPTION ========`r`n$_`r`n" + $message += "======== ERROR STACK ========`r`n" + $error |% { $message += "$_`r`n----`r`n" } + $message += "======== ========" + Write-EventLogWrapper $message + exit 666 +} diff --git a/scripts/provisioner-postinstall.ps1 b/scripts/provisioner-postinstall.ps1 index 563af79..1c9fca7 100644 --- a/scripts/provisioner-postinstall.ps1 +++ b/scripts/provisioner-postinstall.ps1 @@ -2,26 +2,14 @@ Fucking Packer is giving me problems with its shell, windows-shell, and powershell provisioners, so fuck it Don't require parameters - it won't run with parameters during post install. This is just for clarity & ease of debugging #> -[cmdletbinding()] -param( +[cmdletbinding()] param( $packerBuildName = ${env:PACKER_BUILD_NAME}, - $packerBuilderType = ${env:PACKER_BUILDER_TYPE}, - $tempDir # calculated later on if this is empty + $packerBuilderType = ${env:PACKER_BUILDER_TYPE} ) - $errorActionPreference = "stop" - -write-verbose "PostInstall for packer build '$packerBuildName' of type '$packerBuilderType'" - -if ($packerBuilderType -notmatch "virtualbox") { - $warning = "@@@WARNING@@@ I have no way to install tools for your selected Packer build type of '$packerBuilderType'" - write-host -foreground red -object $warning -} - -$LASTEXITCODE = 0 # just in case - import-module $PSScriptRoot\wintriallab-postinstall.psm1 -try { +Invoke-ScriptblockAndCatch -scriptBlock { + Write-EventLogWrapper "PostInstall for packer build '$packerBuildName' of type '$packerBuilderType'" Install-SevenZip Disable-AutoAdminLogon Enable-RDP @@ -41,16 +29,3 @@ try { Install-CompiledDotNetAssemblies # Takes about 15 minutes for me Compress-WindowsInstall # Takes maybe another 15 minutes } -catch { - write-host "======== CAUGHT EXCEPTION ========" - write-host "$_" - write-host "======== CALL STACK ========" - Get-PSCallStack | format-list - write-host "======== ERROR STACK ========" - for ($i=0; $i -lt $error.count; $i+=1) { - write-host "`$error[$i]" - write-host $error[$i] - } - write-host "======== ========" - exit 666 -} diff --git a/scripts/slipstream.psm1 b/scripts/slipstream.psm1 new file mode 100644 index 0000000..2d7234f --- /dev/null +++ b/scripts/slipstream.psm1 @@ -0,0 +1,179 @@ +import-module dism -verbose:$false + +# TODO: Copy-ItemAndExclude +# 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-EventLogWrapper "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) +# } +# } +# } + + +<# +.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-EventLogWrapper "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 { # TODO fixme not sure I wanna handle temp dirs this way?? + [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-EventLogWrapper "Calling OSCDIMG: '$oscdimgCall" + Invoke-ExpressionAndCheck $oscdimgCall -verbose:$verbose + + dismount-diskimage -imagepath $sourceIsoPath +} + + +<# +.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 + ) + $bootWimInfo = Get-WindowsImage -imagePath $wimFile -verbose:$verbose + + $arch = $null + if (-not $bootWimInfo) { throw "Got no information for wimfile at '$wimFile'"} + 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'"} + + write-verbose "Found an architecture of '$arch' for '$wimFile'" + return $arch +} + + +function Apply-WindowsUpdatesToIso { + [cmdletbinding()] param ( + [parameter(mandatory=$true)] [string] $inputIso, + [parameter(mandatory=$true)] [string] $outputIso, + [parameter(mandatory=$true)] [string] $wsusOfflineDir, + [parameter(mandatory=$true)] [string] $wimMountDir + ) + + $myWimMounts = @() + + 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 $installWim -name IsReadOnly -value $false -force + + $arch = Get-BootWimArchitecture -wimFile "${mountedDrive}:\sources\boot.wim" -verbose:$verbose + dismount-diskimage -imagepath $inputIso + + $wimInfo = Get-WindowsImage -imagePath $installWim + $shortCode = Get-WOShortCode -OSName $wimInfo[0].ImageName -OSArchitecture $arch + #$updatePath = resolve-path "${wsusOfflineDir}\client\$shortCode\glb" | select -expand Path + $updatePath = "D:\iso\wintriallab\temp-slipstream\WSUSCache\w63-i386-glb" + + foreach ($wimInfo in (Get-WindowsImage -imagePath $installWim)) { + $wimMountSubdir = mkdir "${wimMountDir}\$($wimInfo.ImageIndex)" -force | select -expand fullname + Mount-WindowsImage -imagePath $installWim -index $wimInfo.ImageIndex -path $wimMountSubdir + + write-verbose "Applying '$((ls $updatePath).count)' updates to '$wimInfo'''" + try { + Add-WindowsPackage -PackagePath $updatePath -path $wimMountSubdir + } + catch { + write-verbose "Caught error(s) when installing packages:`n`n$_`n" + } + + Dismount-WindowsImage -Path $wimMountSubdir -Save + } + + New-WindowsInstallMedia -sourceIsoPath $inputIso -installMediaTemp $installMediaTemp -installWimPath $installWim -outputIsoPath $outputIso +} \ No newline at end of file diff --git a/scripts/win-updates.ps1 b/scripts/win-updates.ps1 index e27b133..5ef1b60 100644 --- a/scripts/win-updates.ps1 +++ b/scripts/win-updates.ps1 @@ -370,7 +370,17 @@ function Run-WindowsUpdate { } if ($MyInvocation.InvocationName -ne '.') { - Run-WindowsUpdate + try { + Run-WindowsUpdate + } + catch { + $message = "======== CAUGHT EXCEPTION ========`r`n$_`r`n" + $message += "======== ERROR STACK ========`r`n" + $error |% { $message += "$_`r`n----`r`n" } + $message += "======== ========" + Write-WinUpEventLog $message + exit 666 + } } # Original version was 234 lines \ No newline at end of file diff --git a/scripts/wintriallab-postinstall.psm1 b/scripts/wintriallab-postinstall.psm1 index ece16f6..88277f8 100644 --- a/scripts/wintriallab-postinstall.psm1 +++ b/scripts/wintriallab-postinstall.psm1 @@ -4,16 +4,6 @@ param( [String] $ScriptName = $MyInvocation.MyCommand.Name ) -<# -jesus fucking christ -fucking Packer -TODO: -- make every function 100% reliant on itself only. -- get rid of calls to Get-LabTempDir -- decided whether I'm using $URLs or not lol -- get better logging - use an Event Log -#> - ### Global Constants that I use elsewhere $ArchitectureId = @{ @@ -21,7 +11,7 @@ $ArchitectureId = @{ i386 = "i386" } $WindowsVersionId = @{ - w63 = "w63" # TODO: rename to w81 probably + w81 = "w81" w10 = "w10" w10ltsb = "w10ltsb" server2012r2 = "server2012r2" @@ -30,7 +20,7 @@ $OfficeVersionId = @{ o2013 = "o2013" } $IsoUrls = @{ - $WindowsVersionId.w63 = @{ + $WindowsVersionId.w81 = @{ $ArchitectureId.i386 = "http://care.dlservice.microsoft.com/dl/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X86FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X86FREE_EN-US_DV9.ISO" $ArchitectureId.amd64 = "http://care.dlservice.microsoft.com/dl/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X64FREE_EN-US_DV9.ISO" } @@ -55,7 +45,7 @@ $WSUSOfflineRepoBaseUrl = "https://svn.wsusoffline.net/svn/wsusoffline/trunk" $szUrl = "http://7-zip.org/a/$szFilename" $URLs = @{ ISOs = @{ - $WindowsVersionId.w63 = @{ + $WindowsVersionId.w81 = @{ $ArchitectureId.i386 = "http://care.dlservice.microsoft.com/dl/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X86FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X86FREE_EN-US_DV9.ISO" } } @@ -95,161 +85,35 @@ function Get-WebUrl { return (get-item $outFile) } -function Invoke-ExpressionAndCheck { +<# +.synopsis +Invoke an expression; log the expression, any output, and the last exit code +#> +function Invoke-ExpressionAndLog { [cmdletbinding()] param( [parameter(mandatory=$true)] [string] $command, + [switch] $invokeWithCmdExe, + [switch] $checkExitCode, [int] $sleepSeconds ) $global:LASTEXITCODE = 0 - Write-EventLogWrapper "Invoking expression '$command'" - invoke-expression -command $command - Write-EventLogWrapper "Expression '$command' had a last exit code of '$LastExitCode'" - if ($global:LASTEXITCODE -ne 0) { - throw "LASTEXITCODE: ${global:LASTEXITCODE} for command: '${command}'" + if ($invokeWithCmdExe) { + Write-EventLogWrapper "Invoking CMD: '$command'" + $output = cmd /c "$command" } - if ($sleepSeconds) { start-sleep $sleepSeconds } -} - -# TODO: Copy-ItemAndExclude -# 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-EventLogWrapper "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 Apply-XmlTransform { - [cmdletbinding()] param( - [parameter(mandatory=$true)] [string] $xmlFile, - [parameter(mandatory=$true)] [string] $xsltFile, - [parameter(mandatory=$true)] [string] $outFile - ) - $xmlFile = resolve-path $xmlFile | select -expand path - $xsltFile = resolve-path $xsltFile | select -expand path - - if (test-path $outFile) { throw "outFile exists at '$outFile'" } - $outParent = split-path -parent $outFile | resolve-path | select -expand Path - $outName = split-path -leaf $outFile - $outFile = "$outParent\$outName" - - $xslt = New-Object System.Xml.Xsl.XslCompiledTransform - $xslt.Load($xsltFile) - $xslt.Transform($xmlFile, $outFile) - return (get-item $outFile) -} - -<# -.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-EventLogWrapper "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'" - } + else { + Write-EventLogWrapper "Invoking Powershell expression: '$command'" + $output = invoke-expression -command $command } - throw "Could not resolve format string '$pathFormatString' to an existing path" -} - -<# -.notes -For use with WSUS Offline Updater -#> -function Get-WOShortCode { # TODO fixme I think I don't need this anymore because I'm not using WSUS Offline anymore - 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" + Write-EventLogWrapper "Expression '$command' had a last exit code of '$LastExitCode' and output the following to the console:`r`n`r`n$output" + if (if ($checkExitCode -and $global:LASTEXITCODE -ne 0) { + throw "LASTEXITCODE: ${global:LASTEXITCODE} for command: '${command}'" } - - $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-EventLogWrapper "Found shortcode '$shortcode' for OS named '$OSName' of architecture '$OSArchitecture'" - return $shortCode + if ($sleepSeconds) { start-sleep $sleepSeconds } } - ### Publicly exported functions called directly from slipstreaming scripts -<# -.notes -This is intended for use in the postinstall phase, on the target machine -We expect calling scripts to get a lab temp dir with this function, but we do NOT permit functions in the module to call it -TODO: does that even make sense to do?? -Only functions that were intended to run in that phase should have the concept of a "LabTempDir". -NOTE: this will return the same directory every time it's called until the module is reimported -#> -function Get-LabTempDir { - if ("${script:WinTrialLabTemp}") {} # noop - elseif ("${env:WinTrialLabTemp}") { - $script:WinTrialLabTemp = $env:WinTrialLabTemp - } - else { - $dateStamp = get-date -UFormat "%Y-%m-%d-%H-%M-%S" - $script:WinTrialLabTemp = "${env:Temp}\WinTrialLab-$dateStamp" - } - $script:WinTrialLabTemp = [System.IO.Path]::GetFullPath($script:WinTrialLabTemp) - Write-EventLogWrapper "Using WinTrialLabTemp directory at '${script:WinTrialLabTemp}'" - if (-not (test-path $script:WinTrialLabTemp)) { - Write-EventLogWrapper "Temporary directory does not exist, creating it..." - mkdir -force $script:WinTrialLabTemp | out-null - } - return $script:WinTrialLabTemp -} - <# .synopsis Wrapper that writes to the event log but also to the screen @@ -271,6 +135,31 @@ function Write-EventLogWrapper { Write-EventLog -LogName $eventLogName -Source $EventLogSource -EventID $eventId -EntryType $entryType -Message $MessagePlus } +<# +.synopsis +Invoke a scriptblock. If it throws, write the errors out to the event log and exist with an error code +.notes +This is intended to be a handy wrapper for calling functions in this module that takes care of logging an exception for you. +See the autounattend-postinstall.ps1 and provisioner-postinstall.ps1 scripts for examples. +#> +function Invoke-ScriptblockAndCatch { + [cmdletbinding()] param( + [parameter(mandatory=$true)] [ScriptBlock] $scriptBlock, + [int] $failureExitCode = 666 + ) + try { + $scriptBlock.invoke() + } + catch { + $message = "======== CAUGHT EXCEPTION ========`r`n$_`r`n" + $message += "======== ERROR STACK ========`r`n" + $_ |% { $message += "$_`r`n----`r`n" } + $message += "======== ========" + Write-EventLogWrapper $message + exit 666 + } +} + <# .synopsis Create and set the registry property which will run this script on reboot @@ -341,7 +230,7 @@ function Install-SevenZip { Write-EventLogWrapper "Downloaded '$szUrl' to '$szDlPath', now running msiexec..." $msiCall = '& msiexec /qn /i "{0}"' -f $szDlPath # Windows suxxx so msiexec sometimes returns right away? or something idk. fuck - Invoke-ExpressionAndCheck -command $msiCall -sleepSeconds 30 + Invoke-ExpressionAndLog -checkExitCode -command $msiCall -sleepSeconds 30 } finally { rm -force $szDlPath @@ -362,9 +251,9 @@ function Install-VBoxAdditions { Write-EventLogWrapper "Installing the Oracle certificate..." $oracleCert = resolve-path "$baseDir\cert\oracle-vbox.cer" | select -expand path # NOTE: Checking for exit code, but this command will fail with an error if the cert is already installed - Invoke-ExpressionAndCheck -command ('& "{0}" add-trusted-publisher "{1}" --root "{1}"' -f "$baseDir\cert\VBoxCertUtil.exe",$oracleCert) + Invoke-ExpressionAndLog -checkExitCode -command ('& "{0}" add-trusted-publisher "{1}" --root "{1}"' -f "$baseDir\cert\VBoxCertUtil.exe",$oracleCert) Write-EventLogWrapper "Installing the virtualbox additions" - Invoke-ExpressionAndCheck -command ('& "{0}" /with_wddm /S' -f "$baseDir\VBoxWindowsAdditions.exe") # returns IMMEDIATELY, goddamn fuckers + Invoke-ExpressionAndLog -checkExitCode -command ('& "{0}" /with_wddm /S' -f "$baseDir\VBoxWindowsAdditions.exe") # returns IMMEDIATELY, goddamn fuckers while (get-process -Name VBoxWindowsAdditions*) { write-host 'Waiting for VBox install to finish...'; sleep 1; } Write-EventLogWrapper "virtualbox additions have now been installed" } @@ -375,7 +264,7 @@ function Install-VBoxAdditions { $vbgaPath = mkdir -force "${env:Temp}\InstallVbox" | select -expand fullname try { Write-EventLogWrapper "Extracting iso at '$isoPath' to directory at '$vbgaPath'..." - Invoke-ExpressionAndCheck -command ('sevenzip x "{0}" -o"{1}"' -f $isoPath, $vbgaPath) + Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" -o"{1}"' -f $isoPath, $vbgaPath) InstallVBoxAdditionsFromDir $vbgaPath } finally { @@ -402,7 +291,8 @@ function Disable-AutoAdminLogon { set-itemproperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon -Value 0 } -function Enable-RDP { # TODO fixme +function Enable-RDP { + Write-EventLogWrapper "Enabling RDP" netsh advfirewall firewall add rule name="Open Port 3389" dir=in action=allow protocol=TCP localport=3389 reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f } @@ -410,15 +300,16 @@ function Enable-RDP { # TODO fixme function Install-CompiledDotNetAssemblies { # http://support.microsoft.com/kb/2570538 # http://robrelyea.wordpress.com/2007/07/13/may-be-helpful-ngen-exe-executequeueditems/ + # Don't check the return value - sometimes it fails and that's fine - $ngen = "${env:WinDir}\microsoft.net\framework\v4.0.30319\ngen.exe" - Invoke-ExpressionAndCheck -command "& $ngen update /force /queue" - Invoke-ExpressionAndCheck -command "& $ngen executequeueditems" + set-alias ngen32 "${env:WinDir}\microsoft.net\framework\v4.0.30319\ngen.exe" + ngen32 update /force /queue + ngen32 executequeueditems if ((Get-OSArchitecture) -match $ArchitectureId.amd64) { - $ngen64 = "${env:WinDir}\microsoft.net\framework64\v4.0.30319\ngen.exe" - Invoke-ExpressionAndCheck -command "& $ngen64 update /force /queue" - Invoke-ExpressionAndCheck -command "& $ngen64 executequeueditems" + set-alias ngen64 "${env:WinDir}\microsoft.net\framework64\v4.0.30319\ngen.exe" + ngen64 update /force /queue + ngen64 executequeueditems } } @@ -428,23 +319,23 @@ function Compress-WindowsInstall { $udfZipPath = Get-WebUrl -url $URLs.UltraDefragDownload.$OSArch -outDir $env:temp $udfExPath = "${env:temp}\ultradefrag-portable-6.1.0.$OSArch" # This archive contains a folder - extract it directly to the temp dir - Invoke-ExpressionAndCheck -command ('sevenzip x "{0}" "-o{1}"' -f $udfZipPath,$env:temp) + Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $udfZipPath,$env:temp) $sdZipPath = Get-WebUrl -url $URLs.SdeleteDownload -outDir $env:temp $sdExPath = "${env:temp}\SDelete" # This archive does NOT contain a folder - extract it to a subfolder (will create if necessary) - Invoke-ExpressionAndCheck -command ('sevenzip x "{0}" "-o{1}"' -f $sdZipPath,$sdExPath) + Invoke-ExpressionAndLog -checkExitCode -command ('sevenzip x "{0}" "-o{1}"' -f $sdZipPath,$sdExPath) stop-service wuauserv rm -recurse -force ${env:WinDir}\SoftwareDistribution\Download start-service wuauserv - Invoke-ExpressionAndCheck -command ('& {0} --optimize --repeat "{1}"' -f "$udfExPath\udefrag.exe","$env:SystemDrive") + Invoke-ExpressionAndLog -checkExitCode -command ('& {0} --optimize --repeat "{1}"' -f "$udfExPath\udefrag.exe","$env:SystemDrive") $sdKey = "HKCU:\Software\Sysinternals\SDelete" if (-not (test-path $sdKey)) { New-Item $sdKey -Force } Set-ItemProperty -path $sdKey -name EulaAccepted -value 1 - Invoke-ExpressionAndCheck -command ('& {0} -q -z "{1}"' -f "$sdExPath\SDelete.exe",$env:SystemDrive) + Invoke-ExpressionAndLog -checkExitCode -command ('& {0} -q -z "{1}"' -f "$sdExPath\SDelete.exe",$env:SystemDrive) } finally { rm -recurse -force $udfZipPath,$udfExPath,$sdZipPath,$sdExPath -ErrorAction Continue @@ -530,63 +421,36 @@ function Disable-HibernationFile { Set-ItemProperty -path $powerKey -name HibernateEnabled -value 0 # disable hibernation altogether } -function Enable-WinRM { # TODO fixme, would prefer this to be in Powershell if possible. also it's totally insecure +<# +.synopsis +Forcibly enable WinRM +.notes +TODO: Rewrite in pure Powershell +#> +function Enable-WinRM { [cmdletbinding()] param() Write-EventLogWrapper "Enabling WinRM..." - cmd /c winrm quickconfig -q - cmd /c winrm quickconfig -transport:http - cmd /c winrm set winrm/config @{MaxTimeoutms="1800000"} - cmd /c winrm set winrm/config/winrs @{MaxMemoryPerShellMB="1800"} - cmd /c winrm set winrm/config/service @{AllowUnencrypted="true"} - cmd /c winrm set winrm/config/service/auth @{Basic="true"} - cmd /c winrm set winrm/config/client/auth @{Basic="true"} - cmd /c winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"} - cmd /c netsh advfirewall firewall set rule group="remote administration" new enable=yes - cmd /c netsh firewall add portopening TCP 5985 "Port 5985" - cmd /c net stop winrm - cmd /c sc config winrm start= auto - cmd /c net start winrm - -<# - # cmd /c winrm quickconfig -q - # cmd /c winrm quickconfig -transport:http - Enable-PSRemoting –force -SkipNetworkProfileCheck - - set-item wsman:\localhost\MaxTimeoutms -value 1800000 -force - set-item wsman:\localhost\Shell\MaxMemoryPerShellMB -value 1800 -force - set-item wsman:\localhost\Service\AllowUnencrypted -value $true -force - set-item wsman:\localhost\Service\Auth\Basic -value $true -force - set-item wsman:\localhost\Client\Auth\Basic -value $true -force - - # cmd /c winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"} - $httpListener = $null - foreach ($listener in (ls WSMan:\localhost\Listener)) { - foreach ($key in $listener.keys) { - $subKey = $key.split("=")[0] - $subVal = $key.split("=")[1] - if (($subKey -match "Transport") -and ($subVal = "HTTP")) { - $httpListener = $listener - break - } - } - if ($httpListener) { break } - } - set-item $httpListener -value 5894 - - Set-Item wsman:\localhost\client\auth\CredSSP -value true -force - Enable-WSManCredSSP -force -role server -force - #set-item wsman:localhost\client\trustedhosts -value * -force - #set-item wsman:localhost\client\trustedhosts -value 1.1.2.242 - set-item wsman:\localhost\listener\listener*\port -value 81 -force - - stop-service winrm - set-service -StartupType "automatic" - start-service winrm - - winrm get winrm/config - winrm enumerate winrm/config/listener - #> + # I've had the best luck doing it this way - NOT doing it in a single batch script + # Sometimes one of these commands will stop further execution in a batch script, but when I + # call cmd.exe over and over like this, that problem goes away. + # Note: order is important. This order makes sure that any time packer can successfully + # connect to WinRm, it won't later turn winrm back off or make it unavailable. + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'net stop winrm' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'sc.exe config winrm start= auto' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm quickconfig -q' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm quickconfig -transport:http' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config @{MaxTimeoutms="1800000"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/winrs @{MaxMemoryPerShellMB="2048"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service @{AllowUnencrypted="true"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/client @{AllowUnencrypted="true"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{Basic="true"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/client/auth @{Basic="true"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/service/auth @{CredSSP="true"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"}' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'netsh advfirewall firewall set rule group="remote administration" new enable=yes' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'netsh firewall add portopening TCP 5985 "Port 5985"' + Invoke-ExpressionAndLog -invokeWithCmdExe -command 'net start winrm' } function Set-PasswordExpiry { # TODO fixme use pure Powershell @@ -594,78 +458,69 @@ function Set-PasswordExpiry { # TODO fixme use pure Powershell [parameter(mandatory=$true)] [string] $accountName, [parameter(mandatory=$true)] [bool] $expirePassword ) - $pe = "TRUE" - if (-not $expirePassword) { $pe = "FALSE" } - cmd.exe /c wmic useraccount where "name='$accountName'" set "PasswordExpires=$pe" + $passwordExpires = if ($expirePassword) {"TRUE"} else {"FALSE"} + $command = @" +wmic useraccount where "name='{0}'" set "PasswordExpires={1}" +"@ + $command = $command -f $accountName, $passwordExpiress + Invoke-ExpressionAndLog -invokeWithCmdExe -command $command } -function New-WindowsInstallMedia { # TODO fixme not sure I wanna handle temp dirs this way?? - [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 +<# +.synopsis +Set all attached networks to Private +.description +(On some OSes) you cannot enable Windows PowerShell Remoting on network connections that are set to Public +Spin through all the network locations and if they are set to Public, set them to Private +using the INetwork interface: +http://msdn.microsoft.com/en-us/library/windows/desktop/aa370750(v=vs.85).aspx +For more info, see: +http://blogs.msdn.com/b/powershell/archive/2009/04/03/setting-network-location-to-private.aspx +#> +function Set-AllNetworksToPrivate { + [cmdletbinding()] param() - $diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume - if (-not $diskVol) { - mount-diskimage -imagepath $sourceIsoPath - $diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume + # Network location feature was only introduced in Windows Vista - no need to bother with this + # if the operating system is older than Vista + if([environment]::OSVersion.version.Major -lt 6) { return } + + if(1,3,4,5 -contains (Get-WmiObject win32_computersystem).DomainRole) { throw "Cannot change network location on a domain-joined computer" } + + # Disable the GUI which will modally pop up (at least on Win10) lol + New-Item "HKLM:\System\CurrentControlSet\Control\Network\NewNetworkWindowOff" -force | out-null + + # Get network connections + $networkListManager = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}")) + foreach ($connection in $networkListManager.GetNetworkConnections()) { + $connName = $connection.GetNetwork().GetName() + $oldCategory = $connection.GetNetwork().GetCategory() + $connection.getNetwork().SetCategory(1) + $newCategory = $connection.GetNetwork().GetCategory() + Write-EventLogWrapper "Changed connection category for '$connName' from '$oldCategory' to '$newCategory'" } - $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-EventLogWrapper "Calling OSCDIMG: '$oscdimgCall" - Invoke-ExpressionAndCheck $oscdimgCall -verbose:$verbose - - dismount-diskimage -imagepath $sourceIsoPath } -function Get-WindowsUpdateUrls { # TODO: is this how we wanna do temps tho? +<# +.synopsis +Set the idle time that must elapse before Windows will power off a display +.parameter seconds +The number of seconds before poweroff. A value of 0 means never power off. +.notes +AFAIK, this cannot be done without shelling out to powercfg +#> +function Set-IdleDisplayPoweroffTime { [cmdletbinding()] param( - [parameter(mandatory=$true)] $windowsVersion, - [parameter(mandatory=$true)] $osArchitecture, - [parameter(mandatory=$true)] $packageXml, - [parameter(mandatory=$true)] $outFile, - [switch] $debugSaveXslt + [parameter(mandatory=$true)] [int] $seconds ) - $xsltPath = [IO.Path]::GetTempFileName() - Write-EventLogWrapper "Downloading XSLT to '$xsltPath'" - - if ($osArchitecture -match $ArchitectureId.i386) { $arch = "x86" } - elseif ($osArchitecture -match $ArchitectureId.amd64) { $arch = "x64" } - else { throw "Dunno bout architecture '$osArchitecture'" } - - $xsltUrl = "$WSUSOfflineRepoBaseUrl/xslt/ExtractDownloadLinks-$windowsVersion-$arch-glb.xsl" - Get-WebFile -url $xsltUrl -outFile $xsltPath - - Apply-XmlTransform -xmlFile $packageXml -xsltFile $xsltPath -outFile $outFile - if (-not $debugSaveXslt) { rm -force $xsltPath } - - return $outFile + $currentScheme = (powercfg /getactivescheme).split()[3] + $DisplaySubgroupGUID = "7516b95f-f776-4464-8c53-06167f40cc99" + $TurnOffAfterGUID = "3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e" + set-alias powercfg "${env:SystemRoot}\System32\powercfg.exe" + powercfg /setacvalueindex $currentScheme $DisplaySubgroupGUID $TurnOffAfterGUID 0 } + # Exports: #TODO $emmParams = @{ Alias = @("sevenzip") diff --git a/slipstream.ps1 b/slipstream.ps1 deleted file mode 100644 index 947e35c..0000000 --- a/slipstream.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -[cmdletbinding()] param( - [parameter(mandatory=$true)] [string] $osArchitecture, - [parameter(mandatory=$true)] [string] $WindowsVersion, - [parameter(mandatory=$true)] [string] $isoPath, - [parameter(mandatory=$true)] [string] $osArchitecture, - [parameter(mandatory=$true)] [string] $osArchitecture, -) - -# 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 -} - -Set-StrictMode -Version 2.0 -$ErrorActionPreference = "Stop" - -get-module slipstream | remove-module -ipmo $PSScriptRoot\slipstream.psm1 - -$arch = $ArchitectureId.i386 -$winver = $WindowsVersionId.w63 -$ssTempDir = 'D:\iso\wintriallab\temp-slipstream' -mkdir -force $ssTempDir | out-null -$packageXmlFile = 'D:\iso\wintriallab\wsusscn2\wsusscn2\cabs-extracted\package.xml' -$wuUrlFile = "$ssTempDir\wuUrls.txt" -$wuDownloadCache = "${ssTempDir}\WSUSCache\${winver}-${arch}-glb" -mkdir -force $wuDownloadCache | out-null - -Get-WindowsUpdateUrls -windowsVersion $winver -osArchitecture $arch -packageXml $packageXmlFile -outFile $wuUrlFile -verbose:$verbose -foreach ($url in (gc $wuUrlFile)) { - Get-WebFile -url $url -outDir $wuDownloadCache -verbose:$verbose -} - diff --git a/todo.markdown b/todo.markdown deleted file mode 100644 index e75bacd..0000000 --- a/todo.markdown +++ /dev/null @@ -1,17 +0,0 @@ -- what the FUCK is going on in vagrant-ssh.bat -- better windows update mechanism imo -- would like to use the -tag in the name for the vagrant box too, but that requires parameterizing both the packerfile and the vagrantfile template :/ not sure what to do about this -- store passwords securely for shit and/or generate them on the fly -- test `lab2 -action VagrantUp -baseConfigName windows_81_x86 -tag PreLunchTest` tomorrow afternoon - will it have 89 days remaining? or 90? basically, was it activated at boot or nah? -- need to audit **all** of the scripts ../scripts actually -- it seems like the vbox tools aren't getting installed? why not? -- enable clipboard and drag&drop in my Vagrantfile - though NOT for throwaway VMs that might be insecure! -- Seems like I'm not actually defragging it in my compact.bat? -- Fucking vbox guest tools aren't installing, god I hate batch scripts - -Dumb shit I figured out about how fucking bad Packer is while I got to do this - -- The shell, windows-shell, and powershell provisioners are VERY finicky. I canNOT make them work reliably -- Best thing to do is upload a script with a "file" provisioner and run it with a "windows-shell" provisioner that has one inline command -- ps you can't upload directories for some reason. just individual files. one. by. fucking. one. -