From 00557e50f95d826a8a8964dceab90ce59b432474 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Sat, 10 Apr 2021 15:32:31 +0200 Subject: [PATCH] Replay sidestream changes --- .drone.yml | 38 ++- packer/preseed/Windows10/Sysprep_Unattend.xml | 12 +- packer/windows10.json | 53 ++-- packer/windows10.virt+phys.json | 156 ------------ scripts/Copy-DatastoreItem.ps1 | 52 ---- scripts/New-WindowsImageJob.ps1 | 51 ---- scripts/Remove-Resources.ps1 | 28 ++- scripts/Uninstall-VMwareTools.Sysprep.cmd | 7 - scripts/Update-OvfConfiguration.ps1 | 56 ++++- scripts/Update-OvfConfiguration.yml | 79 +++++- scripts/Windows10/01.Disabled services.ps1 | 22 -- scripts/Windows10/02.Disable IPv6.ps1 | 3 - .../Windows10/03.Power settings timeout.ps1 | 15 -- scripts/Windows10/Register-ScheduledTask.ps1 | 7 + .../payload/Apply-FirstBootConfig.ps1 | 238 ++++++++++++++++++ 15 files changed, 448 insertions(+), 369 deletions(-) delete mode 100644 packer/windows10.virt+phys.json delete mode 100644 scripts/Copy-DatastoreItem.ps1 delete mode 100644 scripts/New-WindowsImageJob.ps1 delete mode 100644 scripts/Uninstall-VMwareTools.Sysprep.cmd delete mode 100644 scripts/Windows10/01.Disabled services.ps1 delete mode 100644 scripts/Windows10/02.Disable IPv6.ps1 delete mode 100644 scripts/Windows10/03.Power settings timeout.ps1 create mode 100644 scripts/Windows10/Register-ScheduledTask.ps1 create mode 100644 scripts/Windows10/payload/Apply-FirstBootConfig.ps1 diff --git a/.drone.yml b/.drone.yml index 4067018..7ba3a0e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,14 @@ kind: pipeline type: kubernetes name: 'Packer Build' + +volumes: +- name: output + claim: + name: flexvolsmb-drone-output +- name: scratch + claim: + name: flexvolsmb-drone-scratch steps: - name: Windows 10 @@ -11,10 +19,12 @@ steps: sed -i -e "s/<>/$${WINRM_PASSWORD}/g" \ packer/preseed/Windows10/Autounattend.xml \ packer/preseed/Windows10/Sysprep_Unattend.xml + - | + yamllint -d "{extends: relaxed, rules: {line-length: disable}}" scripts - | packer validate \ -var-file=packer/variables.vsphere.json \ - -var vm_name=${DRONE_COMMIT_SHA:0:10}-$DRONE_BUILD_NUMBER \ + -var vm_name=$DRONE_BUILD_NUMBER-${DRONE_COMMIT_SHA:0:10} \ -var vm_guestos=win10 \ -var repo_username=$${REPO_USERNAME} \ -var repo_password=$${REPO_PASSWORD} \ @@ -25,7 +35,7 @@ steps: packer build \ -on-error=cleanup \ -var-file=packer/variables.vsphere.json \ - -var vm_name=${DRONE_COMMIT_SHA:0:10}-$DRONE_BUILD_NUMBER \ + -var vm_name=$DRONE_BUILD_NUMBER-${DRONE_COMMIT_SHA:0:10} \ -var vm_guestos=win10 \ -var repo_username=$${REPO_USERNAME} \ -var repo_password=$${REPO_PASSWORD} \ @@ -47,8 +57,22 @@ steps: volumes: - name: output path: /output - -volumes: -- name: output - claim: - name: flexvolsmb-drone-output +- name: Remove temporary resources + image: bv11-cr01.bessems.eu/library/packer-extended + commands: + - | + pwsh -file scripts/Remove-Resources.ps1 \ + -VMName $DRONE_BUILD_NUMBER-${DRONE_COMMIT_SHA:0:10} \ + -VSphereFQDN 'bv11-vc.bessems.lan' \ + -VSphereUsername 'administrator@vsphere.local' \ + -VSpherePassword $${VSPHERE_PASSWORD} + environment: + VSPHERE_PASSWORD: + from_secret: vsphere_password + volumes: + - name: scratch + path: /scratch + when: + status: + - success + - failure diff --git a/packer/preseed/Windows10/Sysprep_Unattend.xml b/packer/preseed/Windows10/Sysprep_Unattend.xml index dc2e387..fc79fe6 100644 --- a/packer/preseed/Windows10/Sysprep_Unattend.xml +++ b/packer/preseed/Windows10/Sysprep_Unattend.xml @@ -31,21 +31,11 @@ UTC - secret + <> true</PlainText> </AdministratorPassword> </UserAccounts> </component> - <!-- <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <RunSynchronous> - <RunSynchronousCommand wcm:action="add"> - <Order>1</Order> - <Path>c:\windows\system32\net.exe user administrator /active:yes</Path> - <Description>Enable Built-in Administrator</Description> - <CommandLine></CommandLine> - </RunSynchronousCommand> - </RunSynchronous> - </component> --> </settings> <settings pass="specialize"> </settings> diff --git a/packer/windows10.json b/packer/windows10.json index 84ae62f..8711bad 100644 --- a/packer/windows10.json +++ b/packer/windows10.json @@ -69,7 +69,11 @@ ], "provisioners": [ { - "type": "windows-update" + "type": "windows-update", + "filters": [ + "exclude:$_.Title -like '*Preview*'", + "include:$true" + ] }, { "type": "powershell", @@ -83,49 +87,48 @@ "inline": [ "choco config set --name=limit-output --value=LimitOutput", "choco install -y 7zip.install", - "choco install -y putty" + "choco install -y sysinternals", + "choco install -y firefox" + ] + }, + { + "type": "windows-update", + "filters": [ + "exclude:$_.Title -like '*Preview*'", + "include:$true" ] }, { "type": "powershell", - "scripts": [ - "scripts/Windows10/01.Disabled services.ps1", - "scripts/Windows10/02.Disable IPv6.ps1", - "scripts/Windows10/03.Power settings timeout.ps1" + "inline": [ + "New-Item -Path 'C:\\Payload\\Scripts' -ItemType 'Directory' -Force:$True -Confirm:$False" ] }, { - "type": "windows-update" + "type": "file", + "source": "scripts/Windows10/payload/", + "destination": "C:\\Payload\\" }, { - "type": "windows-restart", - "restart_check_command":"powershell -command \"& {Write-Output 'Probing restart status'}\"" + "type": "powershell", + "scripts": [ + "scripts/Windows10/Register-ScheduledTask.ps1" + ] } ], "post-processors": [[ { "type": "shell-local", "inline": [ - "pwsh -file scripts/Update-OvfConfiguration.ps1 \\", - " -OVFFile './output-win10/{{user `vm_guestos`}}-{{user `vm_name`}}.ovf'", + "pwsh -command \"& scripts/Update-OvfConfiguration.ps1 \\", + " -OVFFile '/scratch/win10/{{user `vm_guestos`}}-{{user `vm_name`}}.ovf' \\", + " -Parameter @{'appliance.name'='{{user `vm_guestos`}}';'appliance.version'='{{user `vm_name`}}'}\"", "pwsh -file scripts/Update-Manifest.ps1 \\", - " -ManifestFileName './output-win10/{{user `vm_guestos`}}-{{user `vm_name`}}.mf'", + " -ManifestFileName '/scratch/win10/{{user `vm_guestos`}}-{{user `vm_name`}}.mf'", "ovftool --acceptAllEulas --allowExtraConfig --overwrite \\", - " './output-win10/{{user `vm_guestos`}}-{{user `vm_name`}}.ovf' \\", + " '/scratch/win10/{{user `vm_guestos`}}-{{user `vm_name`}}.ovf' \\", " /output/Windows10.ova" ] } - ], - [ - { - "type": "shell-local", - "inline": [ - "pwsh -file scripts/Remove-Resources.ps1 \\", - " -VMName '{{user `vm_guestos`}}-{{user `vm_name`}}' \\", - " -VSphereFQDN '{{user `vcenter_server`}}' \\", - " -VSphereUsername '{{user `vsphere_username`}}' \\", - " -VSpherePassword '{{user `vsphere_password`}}'" - ] - } ]] } diff --git a/packer/windows10.virt+phys.json b/packer/windows10.virt+phys.json deleted file mode 100644 index 1270bfe..0000000 --- a/packer/windows10.virt+phys.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "builders": [ - { - "type": "vsphere-iso", - "name": "win10-virtual", - - "vcenter_server": "{{user `vcenter_server`}}", - "username": "{{user `vsphere_username`}}", - "password": "{{user `vsphere_password`}}", - "insecure_connection": "true", - - "vm_name": "{{user `vm_guestos`}}-{{user `vm_name`}}-virtual", - "datastore": "{{user `vsphere_datastore`}}", - "folder": "{{user `vsphere_folder`}}", - "datacenter": "{{user `vsphere_datacenter`}}", - "host": "{{user `vsphere_host`}}", - "boot_order": "disk,cdrom", - - "guest_os_type": "windows9_64Guest", - - "communicator": "winrm", - "winrm_username": "administrator", - "winrm_password": "{{user `winrm_password`}}", - "winrm_timeout": "10m", - - "cpus": 2, - "RAM": 8192, - - "network_adapters": [ - { - "network": "{{user `vsphere_network`}}", - "network_card": "vmxnet3" - } - ], - "storage": [ - { - "disk_size": 20480, - "disk_thin_provisioned": true - } - ], - "disk_controller_type": "lsilogic-sas", - "usb_controller": "xhci", - - "iso_url": "https://sn.itch.fyi/Repository/iso/Microsoft/Windows%2010/20H2/Win10_20H2_v2_English_x64.iso", - "iso_checksum": "sha256:6C6856405DBC7674EDA21BC5F7094F5A18AF5C9BACC67ED111E8F53F02E7D13D", - "iso_paths": [ - "[Datastore01.NAS] contentlib-5c2187fa-55c5-4285-b06b-3f5f1ff9428d/e9342f62-6132-4044-bd42-48cab8c77034/VMware-tools-windows-11.2.1-17243207_4f88be10-b163-446b-ad7d-992e63b0e3ac.iso" - ], - - "floppy_files": [ - "packer/preseed/Windows10/Autounattend.xml", - "packer/preseed/Windows10/Sysprep_Unattend.xml", - "scripts/Set-NetworkProfile.ps1", - "scripts/Disable-WinRM.ps1", - "scripts/Enable-WinRM.ps1", - "scripts/Install-VMwareTools.cmd" - ], - - "boot_command": "", - "boot_wait": "5m", - - "shutdown_command": "C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /oobe /unattend:A:\\Sysprep_Unattend.xml", - "shutdown_timeout": "1h" - }, - { - "type": "vsphere-iso", - "name": "win10-physical", - - "vcenter_server": "{{user `vcenter_server`}}", - "username": "{{user `vsphere_username`}}", - "password": "{{user `vsphere_password`}}", - "insecure_connection": "true", - - "vm_name": "{{user `vm_guestos`}}-{{user `vm_name`}}-physical", - "datastore": "{{user `vsphere_datastore`}}", - "folder": "{{user `vsphere_folder`}}", - "datacenter": "{{user `vsphere_datacenter`}}", - "host": "{{user `vsphere_host`}}", - "boot_order": "disk,cdrom", - - "guest_os_type": "windows9_64Guest", - - "communicator": "winrm", - "winrm_username": "administrator", - "winrm_password": "{{user `winrm_password`}}", - "winrm_timeout": "10m", - - "cpus": 2, - "RAM": 8192, - - "network_adapters": [ - { - "network": "{{user `vsphere_network`}}", - "network_card": "vmxnet3" - } - ], - "storage": [ - { - "disk_size": 20480, - "disk_thin_provisioned": true - } - ], - "disk_controller_type": "lsilogic-sas", - "usb_controller": "xhci", - - "iso_url": "https://sn.itch.fyi/Repository/iso/Microsoft/Windows%2010/20H2/Win10_20H2_v2_English_x64.iso", - "iso_checksum": "sha256:6C6856405DBC7674EDA21BC5F7094F5A18AF5C9BACC67ED111E8F53F02E7D13D", - "iso_paths": [ - "[Datastore01.NAS] contentlib-5c2187fa-55c5-4285-b06b-3f5f1ff9428d/e9342f62-6132-4044-bd42-48cab8c77034/VMware-tools-windows-11.2.1-17243207_4f88be10-b163-446b-ad7d-992e63b0e3ac.iso" - ], - - "floppy_files": [ - "packer/preseed/Windows10/Autounattend.xml", - "packer/preseed/Windows10/Sysprep_Unattend.xml", - "scripts/Set-NetworkProfile.ps1", - "scripts/Disable-WinRM.ps1", - "scripts/Enable-WinRM.ps1", - "scripts/Install-VMwareTools.cmd", - "scripts/Uninstall-VMwareTools.Sysprep.cmd" - ], - - "boot_command": "", - "boot_wait": "5m", - - "shutdown_command": "A:\\Uninstall-VMwareTools.Sysprep.cmd", - "shutdown_timeout": "1h" - } - ], - "provisioners": [ - { - "type": "windows-update" - }, - { - "type": "powershell", - "inline": [ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", - "Invoke-Expression ((New-Object Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" - ] - }, - { - "type": "powershell", - "inline": [ - "choco config set --name=limit-output --value=LimitOutput", - "choco install -y 7zip.install", - "choco install -y putty" - ] - }, - { - "type": "windows-update" - }, - { - "type": "windows-restart", - "restart_check_command":"powershell -command \"& {Write-Output 'Probing restart status'}\"" - } - ] - } diff --git a/scripts/Copy-DatastoreItem.ps1 b/scripts/Copy-DatastoreItem.ps1 deleted file mode 100644 index aa24a17..0000000 --- a/scripts/Copy-DatastoreItem.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -[CmdletBinding()] -Param( - [Parameter(Mandatory)] - [string]$VMName, - [Parameter(Mandatory)] - [string]$VSphereFQDN, - [Parameter(Mandatory)] - [string]$VSphereUsername, - [Parameter(Mandatory)] - [string]$VSpherePassword -) - -$PowerCliConfigurationSplat = @{ - Scope = 'User' - ParticipateInCEIP = $False - Confirm = $False - InvalidCertificateAction = 'Ignore' -} -Set-PowerCLIConfiguration @PowerCliConfigurationSplat | Out-Null - -$ConnectVIServerSplat = @{ - Server = $VSphereFQDN - User = "$VSphereUsername" - Password = "$VSpherePassword" - WarningAction = 'SilentlyContinue' -} -Connect-VIServer @ConnectVIServerSplat | Out-Null - -$GetVMSplat = @{ - Name = $VMName -} -$VM = Get-VM @GetVMSplat - -$GetHarddiskSplat = @{ - VM = $VM -} -$Harddisk = Get-Harddisk @GetHarddiskSplat -$VMFolder = ($Harddisk.Filename.Substring(0, $Harddisk.Filename.LastIndexOf('/')) -split ' ')[1] - -$NewDatastoreDriveSplat = @{ - Name = 'ds' - Datastore = ($VM | Get-Datastore) -} -New-DatastoreDrive @NewDatastoreDriveSplat - -$CopyDatastoreItemSplat = @{ - Item = "ds:\$($VMFolder)\*.vmdk" - Destination = (Get-Item $PWD) -} -Copy-DatastoreItem @CopyDatastoreItemSplat - -Disconnect-VIServer * -Confirm:$False \ No newline at end of file diff --git a/scripts/New-WindowsImageJob.ps1 b/scripts/New-WindowsImageJob.ps1 deleted file mode 100644 index 634d41e..0000000 --- a/scripts/New-WindowsImageJob.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -#Requires -Modules 'dism' -Param( - [Parameter(Mandatory)] - [string]$ImageName, - [Parameter(Mandatory)] - [string]$SourceFolder, - [Parameter(Mandatory)] - [string]$DestinationFile -) - -$StartJobSplat = @{ - ArgumentList = $ImageName, $SourceFolder, $DestinationFile - ScriptBlock = { - Param( - $ImageName, - $SourceFolder, - $DestinationFile - ) - - $NewWindowsImageSplat = @{ - Name = $ImageName - CapturePath = $SourceFolder - ImagePath = $DestinationFile - Verify = $True - } - New-WindowsImage @NewWindowsImageSplat - } -} -$Job = Start-Job @StartJobSplat - -While ($Job.State -eq 'Running') { - $GetItemSplat = @{ - Path = $DestinationFile - ErrorAction = 'SilentlyContinue' - } - $OutputFile = Get-Item @GetItemSplat - If ($OutputFile) { - Write-Host "Export in progress ... $($OutputFile.FullName); Size: $('{0:n2}' -f ($OutputFile.Length / 1MB))MB" - } - Else { - Write-Host "Export initiating ... " - } - - $StartSleepSplat = @{ - Seconds = 30 - } - Start-Sleep @StartSleepSplat -} - -Receive-Job $Job -Remove-Job $Job \ No newline at end of file diff --git a/scripts/Remove-Resources.ps1 b/scripts/Remove-Resources.ps1 index 31c964d..4d27bd6 100644 --- a/scripts/Remove-Resources.ps1 +++ b/scripts/Remove-Resources.ps1 @@ -26,14 +26,26 @@ $ConnectVIServerSplat = @{ } Connect-VIServer @ConnectVIServerSplat | Out-Null -$RemoveVMSplat = @{ - VM = "$($VMName)*" - DeletePermanently = $True - Confirm = $False - ErrorAction = 'SilentlyContinue' +$GetVMSplat = @{ + Name = "*$($VMName)*" + ErrorAction = 'SilentlyContinue' +} +If ([boolean](Get-VM @GetVMSplat)) { + $RemoveVMSplat = @{ + VM = Get-VM @GetVMSplat + DeletePermanently = $True + Confirm = $False + ErrorAction = 'SilentlyContinue' + } + Remove-VM @RemoveVMSplat } -Remove-VM @RemoveVMSplat -# Also delete ISO/floppy? +Disconnect-VIServer * -Confirm:$False -Disconnect-VIServer * -Confirm:$False \ No newline at end of file +$RemoveItemSplat = @{ + Path = "/scratch/*" + Recurse = $True + Force = $True + Confirm = $False +} +Remove-Item @RemoveItemSplat \ No newline at end of file diff --git a/scripts/Uninstall-VMwareTools.Sysprep.cmd b/scripts/Uninstall-VMwareTools.Sysprep.cmd deleted file mode 100644 index f75b7a5..0000000 --- a/scripts/Uninstall-VMwareTools.Sysprep.cmd +++ /dev/null @@ -1,7 +0,0 @@ -@rem Uninstall VMware Tools -@rem (wait for orphaned child process to finish) -@rem Silent mode, basic UI, no reboot -start "Uninstall VMware Tools" /b /w e:\setup64 /s /v "/qb REBOOT=R REMOVE=ALL" - -@rem Initiate Sysprep -C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /unattend:A:\Sysprep_Unattend.xml /quiet /shutdown \ No newline at end of file diff --git a/scripts/Update-OvfConfiguration.ps1 b/scripts/Update-OvfConfiguration.ps1 index fd731de..c0fc01f 100644 --- a/scripts/Update-OvfConfiguration.ps1 +++ b/scripts/Update-OvfConfiguration.ps1 @@ -9,7 +9,8 @@ Param( Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting." } })] - [string]$OVFFile + [string]$OVFFile, + [hashtable]$Parameter ) $GetContentSplat = @{ @@ -21,7 +22,24 @@ $ConvertFromYamlSplat = @{ Yaml = $RawContent AllDocuments = $True } -$OVFConfig = ConvertFrom-Yaml @ConvertFromYamlSplat +$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + +# Check if the respective .yml file declared substitutions which need to be parsed +If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $OVFConfig = $YamlDocuments[0..($YamlDocuments.Count - 2)] +} +Else { + $OVFConfig = $YamlDocuments +} $SourceFile = Get-Item -Path $OVFFile $GetContentSplat = @{ @@ -62,6 +80,22 @@ If ($OVFConfig.DeploymentConfigurations.Count -gt 0) { $XMLAttrTransport = $XML.CreateAttribute('transport', $XML.DocumentElement.ovf) $XMLAttrTransport.Value = 'com.vmware.guestInfo' [void]$XML.SelectSingleNode('//Any:VirtualHardwareSection', $NS).Attributes.Append($XMLAttrTransport) +ForEach ($ExtraConfig in $OVFConfig.AdvancedOptions) { + $XMLExtraConfig = $XML.CreateElement('vmw:ExtraConfig', $XML.DocumentElement.vmw) + + $XMLExtraConfigAttrRequired = $XML.CreateAttribute('required', $XML.DocumentElement.ovf) + $XMLExtraConfigAttrRequired.Value = "$([boolean]$ExtraConfig.Required)".ToLower() + $XMLExtraConfigAttrKey = $XML.CreateAttribute('key', $XML.DocumentElement.vmw) + $XMLExtraConfigAttrKey.Value = $ExtraConfig.Key + $XMLExtraConfigAttrValue = $XML.CreateAttribute('value', $XML.DocumentElement.vmw) + $XMLExtraConfigAttrValue.Value = $ExtraConfig.Value + + [void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrRequired) + [void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrKey) + [void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrValue) + [void]$XML.SelectSingleNode('//Any:VirtualHardwareSection', $NS).AppendChild($XMLExtraConfig) +} +Write-Host "Added $($OVFConfig.AdvancedOptions.Count) 'vmw:ExtraConfig' nodes" $XMLProductSection = $XML.SelectSingleNode('//Any:ProductSection', $NS) If ($XMLProductSection -eq $Null) { @@ -94,13 +128,13 @@ ForEach ($Category in $OVFConfig.PropertyCategories) { $XMLPropertyAttrKey.Value = $Property.Key $XMLPropertyAttrType = $XML.CreateAttribute('type', $XML.DocumentElement.ovf) Switch -regex ($Property.Type) { - 'boolean' { + '^boolean' { $XMLPropertyAttrType.Value = 'boolean' } - 'int' { + '^int' { $XMLPropertyAttrType.Value = 'uint8' $Qualifiers = @() - If ($Property.Type -match 'int\((\d*)\.\.(\d*)\)') { + If ($Property.Type -match '^int\((\d*)\.\.(\d*)\)') { If ($Matches[1]) { $Qualifiers += "MinValue($($Matches[1]))" } @@ -112,20 +146,20 @@ ForEach ($Category in $OVFConfig.PropertyCategories) { [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) } } - 'ip' { + '^ip' { $XMLPropertyAttrType.Value = 'string' $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.vmw) $XMLPropertyAttrQualifiers.Value = 'Ip' [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) } - 'password' { + '^password' { $XMLPropertyAttrType.Value = 'string' $XMLPropertyAttrPassword = $XML.CreateAttribute('password', $XML.DocumentElement.ovf) $XMLPropertyAttrPassword.Value = 'true' [void]$XMLProperty.Attributes.Append($XMLPropertyAttrPassword) $Qualifiers = @() - If ($Property.Type -match 'password\((\d*)\.\.(\d*)\)') { + If ($Property.Type -match '^password\((\d*)\.\.(\d*)\)') { If ($Matches[1]) { $Qualifiers += "MinLen($($Matches[1]))" } @@ -137,10 +171,10 @@ ForEach ($Category in $OVFConfig.PropertyCategories) { [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) } } - 'string' { + '^string' { $XMLPropertyAttrType.Value = 'string' $Qualifiers = @() - If ($Property.Type -match 'string\((\d*)\.\.(\d*)\)') { + If ($Property.Type -match '^string\((\d*)\.\.(\d*)\)') { If ($Matches[1]) { $Qualifiers += "MinLen($($Matches[1]))" } @@ -150,7 +184,7 @@ ForEach ($Category in $OVFConfig.PropertyCategories) { $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) $XMLPropertyAttrQualifiers.Value = $Qualifiers -join ' ' [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) - } ElseIf ($Property.Type -match 'string\[(.*)\]') { + } ElseIf ($Property.Type -match '^string\[(.*)\]') { $XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf) $XMLPropertyAttrQualifiers.Value = "ValueMap{$($Matches[1] -replace '","', '", "')}" [void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers) diff --git a/scripts/Update-OvfConfiguration.yml b/scripts/Update-OvfConfiguration.yml index 1791c47..9eb931e 100644 --- a/scripts/Update-OvfConfiguration.yml +++ b/scripts/Update-OvfConfiguration.yml @@ -1,5 +1,19 @@ -DeploymentConfigurations: [] +DeploymentConfigurations: +- Id: domainmember + Label: Domain member + Description: Windows 10 client joined to an Active Directory domain +- Id: standalone + Label: Stand-alone + Description: Stand-alone Windows 10 client PropertyCategories: +- Name: 0) Deployment information + ProductProperties: + - Key: deployment.type + Type: string + Value: + - domainmember + - standalone + UserConfigurable: false - Name: 1) Operating System ProductProperties: - Key: guestinfo.hostname @@ -7,6 +21,23 @@ PropertyCategories: Label: Hostname* Description: '(max length: 15 characters)' DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.administratorpw + Type: password(7..) + Label: Local administrator password* + Description: Must meet password complexity rules + DefaultValue: password + Configurations: + - standalone + UserConfigurable: true + - Key: guestinfo.ntpserver + Type: string(1..) + Label: Time server* + Description: A comma-separated list of timeservers + DefaultValue: 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org + Configurations: + - standalone UserConfigurable: true - Name: 2) Networking ProductProperties: @@ -15,22 +46,68 @@ PropertyCategories: Label: IP Address* Description: '' DefaultValue: '' + Configurations: '*' UserConfigurable: true - Key: guestinfo.prefixlength Type: int(8..32) Label: Subnet prefix length* Description: '' DefaultValue: '24' + Configurations: '*' UserConfigurable: true - Key: guestinfo.dnsserver Type: ip Label: DNS server* Description: '' DefaultValue: '' + Configurations: '*' UserConfigurable: true - Key: guestinfo.gateway Type: ip Label: Gateway* Description: '' DefaultValue: '' + Configurations: '*' UserConfigurable: true +- Name: 3) Active Directory membership + ProductProperties: + - Key: addsconfig.domainname + Type: string(1..) + Label: Domain name* + Description: Must be able to be resolved through provided DNS server + DefaultValue: example.org + Configurations: + - domainmember + UserConfigurable: true + - Key: addsconfig.username + Type: string(1..) + Label: Domain account username* + Description: '' + DefaultValue: username + Configurations: + - domainmember + UserConfigurable: true + - Key: addsconfig.password + Type: password(1..) + Label: Domain account password* + Description: '' + DefaultValue: password + Configurations: + - domainmember + UserConfigurable: true +AdvancedOptions: +- Key: appliance.name + Value: "{{ appliance.name }}" + Required: false +- Key: appliance.version + Value: "{{ appliance.version }}" + Required: false + +--- +Variables: +- Name: appliance.name + Expression: | + $Parameter['appliance.name'] +- Name: appliance.version + Expression: | + $Parameter['appliance.version'] diff --git a/scripts/Windows10/01.Disabled services.ps1 b/scripts/Windows10/01.Disabled services.ps1 deleted file mode 100644 index aa9c833..0000000 --- a/scripts/Windows10/01.Disabled services.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -# Retrieve all respective services (by ID) -$GetServiceSplat = @{ - Name = @( - 'wuauserv' - 'W3SVC', - 'XboxGipSvc', - 'XblGameSave' - ) - ErrorAction = 'SilentlyContinue' -} -$Services = Get-Service @GetServiceSplat - -# Stop and disable all respective services -ForEach ($Service in $Services) { - $SetServiceSplat = @{ - Name = $Service.Name - Status = 'Stopped' - StartupType = 'Disabled' - ErrorAction = 'SilentlyContinue' - } - Set-Service @SetServiceSplat -} \ No newline at end of file diff --git a/scripts/Windows10/02.Disable IPv6.ps1 b/scripts/Windows10/02.Disable IPv6.ps1 deleted file mode 100644 index bbdfcf2..0000000 --- a/scripts/Windows10/02.Disable IPv6.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -$nic = get-netadapter - -Disable-NetAdapterBinding -InterfaceAlias $nic.name -ComponentID ms_tcpip6 \ No newline at end of file diff --git a/scripts/Windows10/03.Power settings timeout.ps1 b/scripts/Windows10/03.Power settings timeout.ps1 deleted file mode 100644 index c8fb80c..0000000 --- a/scripts/Windows10/03.Power settings timeout.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -# Disable monitor timeout (plugged in/battery) -#& powercfg /change monitor-timeout-ac 0 -#& powercfg /change monitor-timeout-dc 0 - -# Disable disk timeout (plugged in/battery) -#& powercfg /change disk-timeout-ac 0 -#& powercfg /change disk-timeout-dc 0 - -# Disable standby timeout (plugged in/battery) -& powercfg /change standby-timeout-ac 0 -& powercfg /change standby-timeout-dc 0 - -# Disable hibernate timeout (plugged in/battery) -& powercfg /change hibernate-timeout-ac 0 -& powercfg /change hibernate-timeout-dc 0 \ No newline at end of file diff --git a/scripts/Windows10/Register-ScheduledTask.ps1 b/scripts/Windows10/Register-ScheduledTask.ps1 new file mode 100644 index 0000000..21973d6 --- /dev/null +++ b/scripts/Windows10/Register-ScheduledTask.ps1 @@ -0,0 +1,7 @@ +[CmdletBinding()] +Param( + # No parameters +) + +# Create scheduled task +& schtasks.exe /Create /TN 'FirstBoot' /SC ONSTART /RU SYSTEM /TR "powershell.exe -file C:\Payload\Apply-FirstBootConfig.ps1" \ No newline at end of file diff --git a/scripts/Windows10/payload/Apply-FirstBootConfig.ps1 b/scripts/Windows10/payload/Apply-FirstBootConfig.ps1 new file mode 100644 index 0000000..a997806 --- /dev/null +++ b/scripts/Windows10/payload/Apply-FirstBootConfig.ps1 @@ -0,0 +1,238 @@ +[CmdletBinding()] +Param( + # No parameters +) + +$SetLocationSplat = @{ + Path = $PSScriptRoot +} +Set-Location @SetLocationSplat + +$NewEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + ErrorAction = 'SilentlyContinue' +} +New-EventLog @NewEventLogSplat +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Information' + EventID = 1 + Message = "FirstBoot sequence initiated [working directory: '$PWD']" +} +Write-EventLog @WriteEventLogSplat + +$VMwareToolsExecutable = "C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" + +[xml]$ovfEnv = & $VMwareToolsExecutable --cmd "info-get guestinfo.ovfEnv" | Out-String +$ovfProperties = $ovfEnv.ChildNodes.NextSibling.PropertySection.Property + +$ovfPropertyValues = @{} +foreach ($ovfProperty in $ovfProperties) { + $ovfPropertyValues[$ovfProperty.key] = $ovfProperty.Value +} + +# Check for mandatory values +Switch ($ovfPropertyValues['deployment.type']) { + 'domainmember' { + $MandatoryProperties, $MissingProperties = @('guestinfo.hostname', 'guestinfo.ipaddress', 'guestinfo.prefixlength', 'guestinfo.gateway', 'addsconfig.domainname', 'addsconfig.username', 'addsconfig.password'), @() + } + 'standalone' { + $MandatoryProperties, $MissingProperties = @('guestinfo.hostname', 'guestinfo.ipaddress', 'guestinfo.prefixlength', 'guestinfo.gateway', 'guestinfo.administratorpw', 'guestinfo.ntpserver'), @() + } + default { + # Mandatory values missing, cannot provision. + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Error' + EventID = 66 + Message = "Unexpected or no value set for property 'deployment.type', cannot provision." + } + Write-EventLog @WriteEventLogSplat + & schtasks.exe /Change /TN 'FirstBoot' /DISABLE + Stop-Computer -Force + Exit + } +} +ForEach ($Property in $MandatoryProperties) { + If (!$ovfPropertyValues[$Property]) { + $MissingProperties += $Property + } +} +If ($MissingProperties.Length -gt 0) { + # Mandatory values missing, cannot provision. + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Error' + EventID = 66 + Message = "Missing values for mandatory properties $(($MissingProperties | ForEach-Object {"'{0}'" -f $_}) -join ', '), cannot provision." + } + Write-EventLog @WriteEventLogSplat + & schtasks.exe /Change /TN 'FirstBoot' /DISABLE + Stop-Computer -Force + Exit +} + +# Set hostname and description +If ($Env:ComputerName -ne $ovfPropertyValues['guestinfo.hostname']) { + $RenameComputerSplat = @{ + NewName = $ovfPropertyValues['guestinfo.hostname'] + Force = $True + Confirm = $False + } + Rename-Computer @RenameComputerSplat + $SetCimInstanceSplat = @{ + InputObject = (Get-CimInstance -ClassName 'Win32_OperatingSystem') + Property = @{ + Description = $ovfPropertyValues['guestinfo.hostname'] + } + } + Set-CimInstance @SetCimInstanceSplat + + # Restart the computer to apply changes + Restart-Computer -Force + Exit +} + +# Configure network interface +If ((Get-WmiObject -Class 'Win32_NetworkAdapterConfiguration').IPAddress -NotContains $ovfPropertyValues['guestinfo.ipaddress']) { + $NewNetIPAddressSplat = @{ + InterfaceAlias = (Get-NetAdapter).Name + AddressFamily = 'IPv4' + IPAddress = $ovfPropertyValues['guestinfo.ipaddress'] + PrefixLength = $ovfPropertyValues['guestinfo.prefixlength'] + DefaultGateway = $ovfPropertyValues['guestinfo.gateway'] + } + $IPAddress = New-NetIPAddress @NewNetIPAddressSplat + + # Wait for network connection to become available + $Timestamp, $TimeoutMinutes = (Get-Date), 5 + Do { + If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Warning' + EventID = 13 + Message = "Timeout after $($TimeoutMinutes) minutes waiting for network connection to become available." + } + Write-EventLog @WriteEventLogSplat + Break + } + + Start-Sleep -Milliseconds 250 + + $GetNetIPAddressSplat = @{ + IPAddress = $ovfPropertyValues['guestinfo.ipaddress'] + InterfaceIndex = $IPAddress.InterfaceIndex + AddressFamily = 'IPv4' + ErrorAction = 'SilentlyContinue' + } + } Until ((Get-NetIPAddress @GetNetIPAddressSplat).AddressState -eq 'Preferred') + + $OldErrorActionPreference, $ErrorActionPreference = $ErrorActionPreference, 'SilentlyContinue' + $TestNetConnectionSplat = @{ + ComputerName = ([IPAddress]$ovfPropertyValues['guestinfo.dnsserver']).IPAddressToString + InformationLevel = 'Quiet' + } + $SetDnsClientServerAddressSplat = @{ + InterfaceAlias = (Get-NetAdapter).Name + ServerAddresses = If ( + [boolean]($ovfPropertyValues['guestinfo.dnsserver'] -as [IPaddress]) -and (Test-NetConnection @TestNetConnectionSplat)) { + ($ovfPropertyValues['guestinfo.dnsserver']) + } else { + ('127.0.0.1') + } + Validate = $False + } + Set-DnsClientServerAddress @SetDnsClientServerAddressSplat + $ErrorActionPreference, $OldErrorActionPreference = $OldErrorActionPreference, $NULL +} + +Switch ($ovfPropertyValues['deployment.type']) { + 'domainmember' { + # Join Active Directory domain as member + If (!(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + $AddComputerSplat = @{ + DomainName = $ovfPropertyValues['addsconfig.domainname'] + Credential = New-Object System.Management.Automation.PSCredential( + $ovfPropertyValues['addsconfig.username'], + (ConvertTo-SecureString $ovfPropertyValues['addsconfig.password'] -AsPlainText -Force) + ) + # OUPath = $ovfPropertyValues['addsconfig.organizationalunit'] + Restart = $True + Force = $True + Confirm = $False + } + Add-Computer @AddComputerSplat + + # Previous cmdlet performs a reboot on completion; so these are commented out + # Restart-Computer -Force + # Exit + } + } + 'standalone' { + # Change password of built-in Administrator + $BuiltinAdministrator = (Get-LocalUser | Where-Object {$_.SID -match '-500'}) + $ConvertToSecureStringSplat = @{ + String = $ovfPropertyValues['guestinfo.administratorpw'] + AsPlainText = $True + Force = $True + } + $SetLocalUserSplat = @{ + InputObject = $BuiltinAdministrator + Password = ConvertTo-SecureString @ConvertToSecureStringSplat + PasswordNeverExpires = $True + AccountNeverExpires = $True + ### This setting is not allowed on the last administrator + # UserMayChangePassword = $False + Confirm = $False + } + Set-LocalUser @SetLocalUserSplat + } +} + +# Iterate through and invoke all payload scripts +#! TODO: add registry values to determine which scripts have already been invoked (in case of intermediate reboots) +$GetItemSplat = @{ + Path = "$($PSScriptRoot)\Scripts\*.ps1" +} +ForEach ($Script in (Get-Item @GetItemSplat)) { + Try { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Information' + EventID = 4 + Message = "Running script: '$($Script.FullName)'" + } + Write-EventLog @WriteEventLogSplat + & $Script.FullName -Parameter $ovfPropertyValues + } + Catch { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Error' + EventID = 66 + Message = @" +Error occurred while executing script '$($Script.Name)': +$($_.Exception.Message) +"@ + } + Write-EventLog @WriteEventLogSplat + } +} + +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'FirstBoot' + EntryType = 'Information' + EventID = 42 + Message = 'FirstBoot sequence applied and finished' +} +Write-EventLog @WriteEventLogSplat +& schtasks.exe /Change /TN 'FirstBoot' /DISABLE