diff --git a/scripts/Server2019/Register-ScheduledTask.ps1 b/scripts/Server2019/Register-ScheduledTask.ps1 new file mode 100644 index 0000000..57b7986 --- /dev/null +++ b/scripts/Server2019/Register-ScheduledTask.ps1 @@ -0,0 +1,7 @@ +[CmdletBinding()] +Param( + # No parameters +) + +# Create scheduled task +& schtasks.exe /Create /TN 'OVF-Properties' /SC ONSTART /RU SYSTEM /TR "powershell.exe -file C:\Payload\Apply-OVFProperties.ps1" \ No newline at end of file diff --git a/scripts/Server2019/payload/Apply-OVFProperties.ps1 b/scripts/Server2019/payload/Apply-OVFProperties.ps1 new file mode 100644 index 0000000..b4086b4 --- /dev/null +++ b/scripts/Server2019/payload/Apply-OVFProperties.ps1 @@ -0,0 +1,230 @@ +#Requires -Modules 'ADDSDeployment' +[CmdletBinding()] +Param( + # No parameters +) + +$NewEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + ErrorAction = 'SilentlyContinue' +} +New-EventLog @NewEventLogSplat +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 1 + Message = 'OVF-Properties sequence initiated' +} +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 = 'OVF-Properties' + EntryType = 'Error' + EventID = 66 + Message = "Unexpected or no value set for property 'deployment.type', cannot provision." + } + Write-EventLog @WriteEventLogSplat + & schtasks.exe /Change /TN 'OVF-Properties' /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 = 'OVF-Properties' + 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 'OVF-Properties' /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'] + } + 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 = 'OVF-Properties' + 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 = @{ + InterfaceAlias = (Get-NetAdapter).Name + 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" +} +Get-Item @GetItemSplat | ForEach-Object { + Try { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 4 + Message = "Running script: '$($_.FullName)'" + } + Write-EventLog @WriteEventLogSplat + & $_.FullName -Parameter $ovfPropertyValues + } + Catch { + $WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Error' + EventID = 66 + Message = $_.Exception.Message + } + Write-EventLog @WriteEventLogSplat + } +} + +$WriteEventLogSplat = @{ + LogName = 'Application' + Source = 'OVF-Properties' + EntryType = 'Information' + EventID = 42 + Message = 'OVF-Properties sequence applied and finished' +} +Write-EventLog @WriteEventLogSplat +& schtasks.exe /Change /TN 'OVF-Properties' /DISABLE diff --git a/scripts/Update-OvfConfiguration.yml b/scripts/Update-OvfConfiguration.yml index 44372c3..5614077 100644 --- a/scripts/Update-OvfConfiguration.yml +++ b/scripts/Update-OvfConfiguration.yml @@ -1,5 +1,19 @@ -DeploymentConfigurations: [] +DeploymentConfigurations: +- Id: domainmember + Label: Domain member + Description: Windows Server joined to an Active Directory domain +- Id: standalone + Label: Stand-alone + Description: Stand-alone Windows Server 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,24 +46,54 @@ 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