UnattendXML.ps1
<#PSScriptInfo
.VERSION 0.9.1 .GUID 6340fcb3-0b5b-4605-9f0d-a436e610e01e .AUTHOR Darren R. Starr .COMPANYNAME Nocturnal Holdings AS .COPYRIGHT 2016 Nocturnal Holdings AS .TAGS unattend .LICENSEURI https://opensource.org/licenses/MIT .PROJECTURI https://github.com/darrenstarr/PowershellAutomation .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 0.9.1 - Made liberal use of error suppression to allow clear text passwords where applicable - Moved to working with Visual Studio Code and Git to keep things clean and in sync - Fixed Where to Where-Object alias warnings #> <# .DESCRIPTION Powershell class to generate unattend.xml files #> Param() <# .SYNOPSIS Values of WillReboot for the section RunSynchronousCommand within unattend.xml .LINK https://technet.microsoft.com/en-us/library/cc722061(v=ws.10).aspx #> enum EnumWillReboot { Always OnRequest Never } <# .SYNOPSIS Valid values for authentication types for remote desktop login .LINK https://technet.microsoft.com/en-us/library/cc722192(v=ws.10).aspx #> enum EnumRdpAuthentication { NetworkLevel = 0 UserLevel = 1 } <# .SYNOPSIS An API for generating Unattend.xml files for Windows Server 2016 .DESCRIPTION UnattendXML is a class designed for generating "properly formatted" XML that meets the schema requirements of Microsoft's Windows Server 2016 unattend.xml format. The code is written as a flat class instead of a serialized data structure as the excessive additional complexity one would expect from serialization would be overwhelming to implement. Given the current state of the class, it is only implemented as much as necessary to perform the operations the author of the class needed. As comments, needs and suggestions as well as patches increase, the functionality of the class will increase. The current design risks a namespace clutter and possibily even constraints due to its flat but easy to use nature. .EXAMPLE using module UnattendXML.ps1 $unattend = [UnattendXml]::new() $unattend.SetComputerName('BobsPC') $unattend.SetRegisteredOwner('Bob Minion') $unattend.SetRegisteredOrganization('Minions Evil Empire') $unattend.SetTimeZone('W. Europe Standard Time') $unattend.SetAdministratorPassword('C1sco12345') $unattend.SetInterfaceIPAddress('Ethernet', '10.1.1.5', 24, '10.1.1.1') $unattend.SetDHCPEnabled('Ethernet', $false) $unattend.SetRouterDiscoveryEnabled('Ethernet', $false) $unattend.SetInterfaceIPv4Metric('Ethernet', 10) $outputXML = $unattend.ToXml() #> class UnattendXml { hidden [Xml]$document = (New-Object -TypeName Xml) hidden [System.Xml.XmlElement]$XmlUnattended hidden static [string] $XmlNs = 'urn:schemas-microsoft-com:unattend' hidden static [string] $ProcessorArchitecture='amd64' hidden static [string] $VersionScope='nonSxS' hidden static [string] $LanguageNeutral='neutral' hidden static [string] $WCM = 'http://schemas.microsoft.com/WMIConfig/2002/State' hidden static [string] $XmlSchemaInstance = 'http://www.w3.org/2001/XMLSchema-instance' static hidden [string] WillRebootToString([EnumWillReboot]$Value) { if($Value -eq [EnumWillReboot]::Always) { return 'Always' } if($Value -eq [EnumWillReboot]::Never) { return 'Never' } if($Value -eq [EnumWillReboot]::OnRequest) { return 'OnRequest' } throw 'Invalid value for WillReboot' } static hidden [string] RdpAuthenticationModeToString([EnumRdpAuthentication]$Value) { if($Value -eq [EnumRdpAuthentication]::NetworkLevel) { return 0 } if($Value -eq [EnumRdpAuthentication]::UserLevel) { return 1 } throw 'Invalid value for RDP authentication mode' } hidden [System.Xml.XmlElement] GetSettingsNode([string]$Pass) { # TODO : Should this be -eq $Pass? $result = $this.XmlUnattended.ChildNodes | Where-Object { $_.Name -eq 'Settings' -and $_.Attributes['pass'].'#text' -like $Pass } If ($null -eq $result) { $result = $this.document.CreateElement('settings', $this.document.DocumentElement.NamespaceURI) $result.SetAttribute('pass', $Pass) $this.XmlUnattended.AppendChild($result) | Out-Null } return $result } hidden [System.Xml.XmlElement] GetOfflineServicingSettings() { return $this.GetSettingsNode('offlineServicing') } hidden [System.Xml.XmlElement] GetSpecializeSettings() { return $this.GetSettingsNode('specialize') } hidden [System.Xml.XmlElement] GetOobeSystemSettings() { return $this.GetSettingsNode('oobe') } hidden [System.Xml.XmlElement] GetSectionFromSettings([System.Xml.XmlElement]$XmlSettings, [string]$Name) { $result = $XmlSettings.ChildNodes | Where-Object { $_.LocalName -eq 'component' -and $_.Attributes['name'].'#text' -eq $Name } if ($null -eq $result) { $result = $this.document.CreateElement('component', $this.document.DocumentElement.NamespaceURI) $result.SetAttribute('name', $Name) $result.SetAttribute('processorArchitecture', [UnattendXml]::ProcessorArchitecture) $result.SetAttribute('publicKeyToken', '31bf3856ad364e35') $result.SetAttribute('language', [UnattendXml]::LanguageNeutral) $result.SetAttribute('versionScope', [UnattendXml]::VersionScope) $result.SetAttribute('xmlns:wcm', [UnattendXml]::WCM) $result.SetAttribute('xmlns:xsi', [UnattendXml]::XmlSchemaInstance) $XmlSettings.AppendChild($result) | Out-Null } return $result } hidden [System.Xml.XmlElement] GetWindowsShellSetupSection([System.Xml.XmlElement]$XmlSettings) { return $this.GetSectionFromSettings($XmlSettings, 'Microsoft-Windows-Shell-Setup') } hidden [System.Xml.XmlElement] GetTerminalServicesLocalSessionManager([System.Xml.XmlElement]$XmlSettings) { return $this.GetSectionFromSettings($XmlSettings, 'Microsoft-Windows-TerminalServices-LocalSessionManager') } hidden [System.Xml.XmlElement] GetTerminalServicesRdpWinStationExtensions([System.Xml.XmlElement]$XmlSettings) { return $this.GetSectionFromSettings($XmlSettings, 'Microsoft-Windows-TerminalServices-RDP-WinStationExtensions') } hidden [System.Xml.XmlElement] GetWindowsTCPIPSection([System.Xml.XmlElement]$XmlSettings) { return $this.GetSectionFromSettings($XmlSettings, 'Microsoft-Windows-TCPIP') } hidden [System.Xml.XmlElement] GetTCPIPInterfaces([System.Xml.XmlElement]$XmlSettings) { $XmlComponent = $this.GetWindowsTCPIPSection($XmlSettings) $result = $XmlComponent.ChildNodes | Where-Object { $_.Name -eq 'Interfaces' } if ($null -eq $result) { $result = $this.document.CreateElement('Interfaces', $this.document.DocumentElement.NamespaceURI) $XmlComponent.AppendChild($result) | Out-Null } return $result } hidden [System.Xml.XmlElement] GetTCPIPInterfaceFromInterfaces([System.Xml.XmlElement]$Interfaces, [string]$Identifier) { $interfaceNodes = $Interfaces.ChildNodes | Where-Object { $_.LocalName -eq 'Interface' } foreach($interfaceNode in $interfaceNodes) { $identifierNode = $interfaceNode.ChildNodes | Where-Object { $_.LocalName -eq $Identifier } if ($identifierNode.InnerText -eq $IdentifierNode) { return $interfaceNode } } $interfaceNode = $this.document.CreateElement('Interface', $this.document.DocumentElement.NamespaceURI) $interfaceNode.SetAttribute('action', [UnattendXML]::WCM, 'add') $Interfaces.AppendChild($interfaceNode) $identifierNode = $this.document.CreateElement('Identifier', $this.document.DocumentElement.NamespaceURI) $identifierNodeText = $this.document.CreateTextNode($Identifier) $identifierNode.AppendChild($identifierNodeText) $interfaceNode.AppendChild($identifierNode) return $interfaceNode } hidden [System.Xml.XmlElement] GetTCPIPInterface([System.Xml.XmlElement]$XmlSettings, [string]$Identifier) { $interfaces =$this.GetTCPIPInterfaces($XmlSettings) return $this.GetTCPIPInterfaceFromInterfaces($interfaces, $Identifier) } hidden [System.Xml.XmlElement] GetOrCreateChildNode([System.Xml.XmlElement]$ParentNode, [string]$LocalName) { $result = $ParentNode.ChildNodes | Where-Object { $_.LocalName -eq $LocalName } if ($null -eq $result) { $result = $this.document.CreateElement($LocalName, $this.document.DocumentElement.NamespaceURI) $ParentNode.AppendChild($result) } return $result } hidden [System.Xml.XmlElement] GetTCPIPv4Settings([System.Xml.XmlElement]$Interface) { return $this.GetOrCreateChildNode($Interface, 'IPv4Settings') } hidden [System.Xml.XmlElement] GetTCPIPv4Setting([System.Xml.XmlElement]$Interface, [string]$SettingName) { $settings = $this.GetTCPIPv4Settings($Interface) return $this.GetOrCreateChildNode($settings, $SettingName) } hidden [System.Xml.XmlElement] GetTCPIPUnicastIPAddresses([System.Xml.XmlElement]$Interface) { return $this.GetOrCreateChildNode($Interface, 'UnicastIPAddresses') } hidden [System.Xml.XmlElement] GetTCPIPUnicastIPAddress([System.Xml.XmlElement]$Interface, [string]$KeyValue) { $unicastIPAddresses = $this.GetTCPIPUnicastIPAddresses($Interface) $result = $unicastIPAddresses.ChildNodes | Where-Object { $_.LocalName -eq 'IpAddress' -and $_.Attributes['keyValue'].'#text' -eq $KeyValue } if ($null -eq $result) { $result = $this.document.CreateElement('IpAddress', $this.document.DocumentElement.NamespaceURI) $result.SetAttribute('action', [UnattendXML]::WCM, 'add') $result.SetAttribute('keyValue',[UnattendXML]::WCM, $KeyValue) $unicastIPAddresses.AppendChild($result) } return $result } hidden [System.Xml.XmlElement] GetTCPIPRoutes([System.Xml.XmlElement]$Interface) { return $this.GetOrCreateChildNode($Interface, 'Routes') } hidden [System.Xml.XmlElement] GetTCPIPRoute([System.Xml.XmlElement]$Interface, [string]$Prefix) { $routes = $this.GetTCPIPRoutes($Interface) $routeNodes = ($routes.ChildNodes | Where-Object { $_.LocalName -eq 'Route' }) $routeIdentifier = '0' # TODO : Better handling of when there's a missing identifier or prefix node foreach($routeNode in $routeNodes) { $prefixNode = ($routeNode.ChildNodes | Where-Object { $_.LocalName -eq 'Prefix' }) if ($prefixNode.InnerText -eq $Prefix) { return $routeNode } $identifierNode = $routeNode.ChildNodes | Where-Object { $_.LocalName -eq 'Identifier' } if(([Convert]::ToInt32($identifierNode.InnerText)) -gt ([Convert]::ToInt32($routeIdentifier))) { $routeIdentifier = $identifierNode.InnerText } } $routeIdentifier = ([Convert]::ToInt32($routeIdentifier)) + 1 $routeNode = $this.document.CreateElement('Route', $this.document.DocumentElement.NamespaceURI) $routeNode.SetAttribute('action', [UnattendXML]::WCM, 'add') $routes.AppendChild($routeNode) $identifierNode = $this.document.CreateElement('Identifier', $this.document.DocumentElement.NamespaceURI) $identifierNodeText = $this.document.CreateTextNode($routeIdentifier.ToString()) $identifierNode.AppendChild($identifierNodeText) $routeNode.AppendChild($identifierNode) $prefixNode = $this.document.CreateElement('Prefix', $this.document.DocumentElement.NamespaceURI) $prefixNodeText = $this.document.CreateTextNode($Prefix) $prefixNode.AppendChild($prefixNodeText) $routeNode.AppendChild($prefixNode) return $routeNode } hidden [System.Xml.XmlElement]GetFirstLogonCommandSection() { $xmlSettings = $this.GetOobeSystemSettings() $xmlComponent = $this.GetWindowsShellSetupSection($xmlSettings) $firstLogonCommands = $this.GetOrCreateChildNode($xmlComponent, 'FirstLogonCommands') return $firstLogonCommands } hidden [System.Xml.XmlElement]GetRunSynchronousSection() { $xmlSettings = $this.GetSpecializeSettings() $xmlComponent = $this.GetWindowsShellSetupSection($xmlSettings) return $this.GetOrCreateChildNode($xmlComponent, 'RunSynchronous') } hidden [string]ConvertToString([SecureString]$SecureString) { if (-not $SecureString) { return $null } $ManagedPasswordString = $null $PointerToPasswordString = $null try { $PointerToPasswordString = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($SecureString) $ManagedPasswordString = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($PointerToPasswordString) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($PointerToPasswordString) } return $ManagedPasswordString } hidden [void]SetAdministratorPassword([SecureString]$AdministratorPassword) { $xmlSettings = $this.GetOfflineServicingSettings() $XmlComponent = $this.GetWindowsShellSetupSection($xmlSettings) $XmlUserAccounts = $this.document.CreateElement('OfflineUserAccounts', $this.document.DocumentElement.NamespaceURI) $XmlComponent.AppendChild($XmlUserAccounts) $XmlAdministratorPassword = $this.document.CreateElement('OfflineAdministratorPassword', $this.document.DocumentElement.NamespaceURI) $XmlUserAccounts.AppendChild($XmlAdministratorPassword) $XmlValue = $this.document.CreateElement('Value', $this.document.DocumentElement.NamespaceURI) $XmlText = $this.document.CreateTextNode([Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes(($this.ConvertToString($AdministratorPassword)) + 'OfflineAdministratorPassword'))) $XmlValue.AppendChild($XmlText) $XmlAdministratorPassword.AppendChild($XmlValue) $XmlPlainText = $this.document.CreateElement('PlainText', $this.document.DocumentElement.NamespaceURI) $XmlPassword = $this.document.CreateTextNode('false') $XmlPlainText.AppendChild($XmlPassword) $XmlAdministratorPassword.AppendChild($XmlPlainText) } hidden [void]SetTextNodeValue([System.Xml.XmlElement]$Parent, [string]$NodeName, [string]$Value) { $namedNode = $this.GetOrCreateChildNode($Parent, $NodeName) $textValueNode = $this.document.CreateTextNode($Value) $namedNode.AppendChild($textValueNode) } hidden [void]SetBoolNodeValue([System.Xml.XmlElement]$Parent, [string]$NodeName, [bool]$Value) { $this.SetTextNodeValue($Parent, $NodeName, ($Value.ToString().ToLower())) } hidden [void]SetInt32NodeValue([System.Xml.XmlElement]$Parent, [string]$NodeName, [Int32]$Value) { $this.SetTextNodeValue($Parent, $NodeName, ($Value.ToString())) } UnattendXml() { $XmlDecl = $this.document.CreateXmlDeclaration('1.0', 'utf-8', $Null) $XmlRoot = $this.document.DocumentElement $this.document.InsertBefore($XmlDecl, $XmlRoot) $this.XmlUnattended = $this.document.CreateElement('unattend', [UnattendXml]::XmlNs) $this.XmlUnattended.SetAttribute('xmlns:wcm', [UnattendXML]::WCM) $this.XmlUnattended.SetAttribute('xmlns:xsi', [UnattendXML]::XmlSchemaInstance) $this.document.AppendChild($this.XmlUnattended) } <# .SYNOPSIS Configures the registered owner of the Windows installation #> [void]SetRegisteredOwner([string]$RegisteredOwner) { $offlineServiceSettings = $this.GetOfflineServicingSettings() $windowsShellSetupNode = $this.GetWindowsShellSetupSection($offlineServiceSettings) $this.SetTextNodeValue($windowsShellSetupNode, 'RegisteredOwner', $RegisteredOwner) } <# .SYNOPSIS Configures the registered organization of the Windows installation #> [void]SetRegisteredOrganization([string]$RegisteredOrganization) { $offlineServiceSettings = $this.GetOfflineServicingSettings() $windowsShellSetupNode = $this.GetWindowsShellSetupSection($offlineServiceSettings) $this.SetTextNodeValue($windowsShellSetupNode, 'RegisteredOrganization', $RegisteredOrganization) } <# .SYNOPSIS Configures the name of the computer #> [void]SetComputerName([string]$ComputerName) { $offlineServiceSettings = $this.GetOfflineServicingSettings() $windowsShellSetupNode = $this.GetWindowsShellSetupSection($offlineServiceSettings) $this.SetTextNodeValue($windowsShellSetupNode, 'ComputerName', $ComputerName) } <# .SYNOPSIS Configures the time zone for the computer .NOTES The configured time zone must be a valid value as defined by Microsoft .LINK https://technet.microsoft.com/en-us/library/cc749073(v=ws.10).aspx #> [void]SetTimeZone([string]$TimeZone) { $offlineServiceSettings = $this.GetOfflineServicingSettings() $windowsShellSetupNode = $this.GetWindowsShellSetupSection($offlineServiceSettings) $this.SetTextNodeValue($windowsShellSetupNode, 'TimeZone', $TimeZone) } <# .SYNOPSIS Sets the state of whether DHCPv4 is enabled for a given interface .LINK https://technet.microsoft.com/en-us/library/cc748924(v=ws.10).aspx #> [void]SetDHCPEnabled([string]$InterfaceIdentifier, [bool]$Enabled) { $XmlSettings = $this.GetSpecializeSettings() $interfaceNode = $this.GetTCPIPInterface($XmlSettings, $InterfaceIdentifier) $interfaceTCPIPSettings = $this.GetTCPIPv4Settings($interfaceNode) $this.SetBoolNodeValue($interfaceTCPIPSettings, 'DhcpEnabled', $Enabled) } <# .SYNOPSIS Sets the state of whether IPv4 Router Discovery is enabled for a given interface .LINK https://technet.microsoft.com/en-us/library/cc749578(v=ws.10).aspx https://www.ietf.org/rfc/rfc1256.txt https://en.wikipedia.org/wiki/ICMP_Router_Discovery_Protocol #> [void]SetRouterDiscoveryEnabled([string]$InterfaceIdentifier, [bool]$Enabled) { $XmlSettings = $this.GetSpecializeSettings() $interfaceNode = $this.GetTCPIPInterface($XmlSettings, $InterfaceIdentifier) $interfaceTCPIPSettings = $this.GetTCPIPv4Settings($interfaceNode) $this.SetBoolNodeValue($interfaceTCPIPSettings, 'RouterDiscoveryEnabled', $Enabled) } <# .SYNOPSIS Sets the IPv4 routing metric value for the interface itself. .NOTES If you don't understand this value, set it to 10. .LINK https://technet.microsoft.com/en-us/library/cc766415(v=ws.10).aspx #> [void]SetInterfaceIPv4Metric([string]$InterfaceIdentifier, [Int32]$Metric) { $XmlSettings = $this.GetSpecializeSettings() $interfaceNode = $this.GetTCPIPInterface($XmlSettings, $InterfaceIdentifier) $interfaceTCPIPSettings = $this.GetTCPIPv4Settings($interfaceNode) $this.SetInt32NodeValue($interfaceTCPIPSettings, 'Metric', $Metric) } <# .SYNOPSIS Sets the IPv4 address, subnet mask, ad default gateway for the given interface. .NOTES While multiple addresses are allowed on an interface, this function assumes you'll have only one. It is recommended that when configuring a static IP address, you : * Disable DHCPv4 for the interface * Disable IPv4 ICMP Router Discovery for the interface * Configure a proper routing metric for the interface .LINK https://technet.microsoft.com/en-us/library/cc749412(v=ws.10).aspx https://technet.microsoft.com/en-us/library/cc749535(v=ws.10).aspx #> [void]SetInterfaceIPAddress([string]$InterfaceIdentifier, [string]$IPAddress, [Int32]$PrefixLength, [string]$DefaultGateway) { $XmlSettings = $this.GetSpecializeSettings() $interfaceNode = $this.GetTCPIPInterface($XmlSettings, $InterfaceIdentifier) $ipAddressNode = $this.GetTCPIPUnicastIPAddress($interfaceNode, '1') # TODO : Handle pre-existing inner text node. $ipAddressTextNode = $this.document.CreateTextNode(("{0}/{1}" -f $IPAddress,$PrefixLength)) $ipAddressNode.AppendChild($ipAddressTextNode) # TODO : Create 'SetRoute' member function which modifies the value if it's already set $routeNode = $this.GetTCPIPRoute($interfaceNode, '0.0.0.0/0') $metricNode = $this.document.CreateElement('Metric', $this.document.DocumentElement.NamespaceURI) $metricNodeText = $this.document.CreateTextNode('10') $metricNode.AppendChild($metricNodeText) $routeNode.AppendChild($metricNode) $nextHopNode = $this.document.CreateElement('NextHopAddress', $this.document.DocumentElement.NamespaceURI) $nextHopNodeText = $this.document.CreateTextNode($DefaultGateway) $nextHopNode.AppendChild($nextHopNodeText) $routeNode.AppendChild($nextHopNode) } <# .SYNOPSIS Configures the administrator password for the new System .NOTES This command uses a plain text password. .LINK https://msdn.microsoft.com/en-us/library/windows/hardware/dn986490(v=vs.85).aspx #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText')] [void] SetAdministratorPassword([string]$AdministratorPassword) { $this.SetAdministratorPassword((ConvertTo-SecureString $AdministratorPassword -AsPlainText -Force)) } <# .SYNOPSIS Add's a command to the FirstLogonCommand list .PARAMETER Description A description of what the command is to do .PARAMETER command The command to run .LINK https://technet.microsoft.com/en-us/library/cc722150(v=ws.10).aspx #> [void] AddFirstLogonCommand([string]$Description, [string]$Command) { $firstLogonCommands = $this.GetFirstLogonCommandSection() $highestOrderNumber = 0 $asyncCommands = $firstLogonCommands.ChildNodes | Where-Object { $_.LocalName -eq 'AsynchronousCommand' } foreach($asyncCommand in $asyncCommands) { $orderNumber = $asyncCommand.ChildNodes | Where-Object { $_.LocalName -eq 'order' } $highestOrderNumber = [Math]::Max($highestOrderNumber, [Convert]::ToInt32($orderNumber.InnerText)) } $orderValueNode = $this.document.CreateTextNode(($highestOrderNumber + 1).ToString()) $orderNode = $this.document.CreateElement('Order', $this.document.DocumentElement.NamespaceURI) $orderNode.AppendChild($orderValueNode) $descriptionTextNode = $this.document.CreateTextNode($Description) $descriptionNode = $this.document.CreateElement('Description', $this.document.DocumentElement.NamespaceURI) $descriptionNode.AppendChild($descriptionTextNode) $commandTextNode = $this.document.CreateTextNode($Command) $commandNode = $this.document.CreateElement('Command', $this.document.DocumentElement.NamespaceURI) $commandNode.AppendChild($commandTextNode) $asyncCommandNode = $this.document.CreateElement('AsynchronousCommand', $this.document.DocumentElement.NamespaceURI) $asyncCommandNode.SetAttribute('action', [UnattendXML]::WCM, 'add') $asyncCommandNode.AppendChild($orderNode) $asyncCommandNode.AppendChild($descriptionNode) $asyncCommandNode.AppendChild($commandNode) $firstLogonCommands.AppendChild($asyncCommandNode) } <# .SYNOPSIS Adds a run synchronous command to the specialize section .DESCRIPTION Adds a command to the ordered list of synchronous commands to be executed as part of the startup process for post installation steps through sysprep.exe. .PARAMETER Description A description of the command to run on startup .PARAMETER Command The command to execute including Path and arguments .PARAMETER WillReboot Whether the command will reboot the system after running .LINK https://technet.microsoft.com/en-us/library/cc722359(v=ws.10).aspx #> [System.Xml.XmlElement] AddRunSynchronousCommand([string]$Description, [string]$Command, [EnumWillReboot]$WillReboot=[EnumWillReboot]::Never) { $runSynchronousSection = $this.GetRunSynchronousSection() $highestOrderNumber = 0 $synchronousCommands = $runSynchronousSection.ChildNodes | Where-Object { $_.LocalName -eq 'RunSynchronousCommand' } foreach($synchronousCommand in $synchronousCommands) { $orderNumber = $synchronousCommand.ChildNodes | Where-Object { $_.LocalName -eq 'order' } $highestOrderNumber = [Math]::Max($highestOrderNumber, [Convert]::ToInt32($orderNumber.InnerText)) } $orderValueNode = $this.document.CreateTextNode(($highestOrderNumber + 1).ToString()) $orderNode = $this.document.CreateElement('Order', $this.document.DocumentElement.NamespaceURI) $orderNode.AppendChild($orderValueNode) $descriptionTextNode = $this.document.CreateTextNode($Description) $descriptionNode = $this.document.CreateElement('Description', $this.document.DocumentElement.NamespaceURI) $descriptionNode.AppendChild($descriptionTextNode) $pathTextNode = $this.document.CreateTextNode($Command) $pathNode = $this.document.CreateElement('Path', $this.document.DocumentElement.NamespaceURI) $pathNode.AppendChild($pathTextNode) $willRebootTextNode = $this.document.CreateTextNode([UnattendXml]::WillRebootToString($WillReboot)) $willRebootNode = $this.document.CreateElement('WillReboot', $this.document.DocumentElement.NamespaceURI) $willRebootNode.AppendChild($willRebootTextNode) $synchronousCommandNode = $this.document.CreateElement('RunSynchronousCommand', $this.document.DocumentElement.NamespaceURI) $synchronousCommandNode.SetAttribute('action', [UnattendXml]::WCM, 'add') $synchronousCommandNode.AppendChild($orderNode) $synchronousCommandNode.AppendChild($descriptionNode) $synchronousCommandNode.AppendChild($pathNode) $synchronousCommandNode.AppendChild($willRebootNode) $runSynchronousSection.AppendChild($synchronousCommandNode) return $synchronousCommandNode } <# .SYNOPSIS Adds a run synchronous command to the specialize section .DESCRIPTION Adds a command to the ordered list of synchronous commands to be executed as part of the startup process for post installation steps through sysprep.exe. .PARAMETER Description A description of the command to run on startup .PARAMETER Command The command to execute including Path and arguments .NOTES This function is an overload which defaults the 'WillReboot' value to never .LINK https://technet.microsoft.com/en-us/library/cc722359(v=ws.10).aspx #> [System.Xml.XmlElement] AddRunSynchronousCommand([string]$Description, [string]$Command) { return $this.AddRunSynchronousCommand($Description, $Command, [EnumWillReboot]::Never) } <# .SYNOPSIS Adds a run synchronous command to the specialize section .DESCRIPTION Adds a command to the ordered list of synchronous commands to be executed as part of the startup process for post installation steps through sysprep.exe. .PARAMETER Description A description of the command to run on startup .PARAMETER Domain The login domain to use for "Run As" for the command .PARAMETER Username The login username to use for "Run As" for the command .PARAMETER Password The login password to use for the "Run As" for the command .PARAMETER Command The command to execute including Path and arguments .PARAMETER WillReboot Whether the command will reboot the system after running .NOTES Warning, when providing login information in the unattend.xml, a copy of the unattend file may end up stored within the \Windows\Panther directory with the passwords in tact. The file should be explicitly removed upon completion .LINK https://technet.microsoft.com/en-us/library/cc722359(v=ws.10).aspx #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams')] [System.Xml.XmlElement] AddRunSynchronousCommand([string]$Description, [string]$Domain, [string]$Username, [string]$Password, [string]$Command, [EnumWillReboot]$WillReboot=[EnumWillReboot]::Never) { $synchronousCommandNode = $this.AddRunSynchronousCommand($Description, $Command, $WillReboot) $domainTextNode = $this.document.CreateTextNode($domain) $domainNode = $this.document.CreateElement('Domain', $this.document.DocumentElement.NamespaceURI) $domainNode.AppendChild($domainTextNode) $usernameTextNode = $this.document.CreateTextNode($Username) $usernameNode = $this.document.CreateElement('Username', $this.document.DocumentElement.NamespaceURI) $usernameNode.AppendChild($usernameTextNode) $passwordTextNode = $this.document.CreateTextNode($Password) $passwordNode = $this.document.CreateElement('Password', $this.document.DocumentElement.NamespaceURI) $passwordNode.AppendChild($passwordTextNode) $credentialsNode = $this.document.CreateElement('Credentials', $this.document.DocumentElement.NamespaceURI) $credentialsNode.AppendChild($domainNode) $credentialsNode.AppendChild($usernameNode) $credentialsNode.AppendChild($passwordNode) $synchronousCommandNode.AppendChild($credentialsNode) return $synchronousCommandNode } <# .SYNOPSIS Adds a run synchronous command to the specialize section .DESCRIPTION Adds a command to the ordered list of synchronous commands to be executed as part of the startup process for post installation steps through sysprep.exe. .PARAMETER Description A description of the command to run on startup .PARAMETER Domain The login domain to use for "Run As" for the command .PARAMETER Username The login username to use for "Run As" for the command .PARAMETER Password The login password to use for the "Run As" for the command .PARAMETER Command The command to execute including Path and arguments .NOTES This is an overloaded function which sets the default value of WillReboot to never Warning, when providing login information in the unattend.xml, a copy of the unattend file may end up stored within the \Windows\Panther directory with the passwords in tact. The file should be explicitly removed upon completion .LINK https://technet.microsoft.com/en-us/library/cc722359(v=ws.10).aspx #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams')] [System.Xml.XmlElement] AddRunSynchronousCommand([string]$Description, [string]$Domain, [string]$Username, [string]$Password, [string]$Command) { return $this.AddRunSynchronousCommand($Description, $Domain, $Username, $Password, $Command, [EnumWillReboot]::Never) } <# .SYNOPSIS Enables Windows Terminal Services to connect .LINK https://technet.microsoft.com/en-us/library/cc722017%28v=ws.10%29.aspx?f=255&MSPPError=-2147217396 #> [void]SetRemoteDesktopEnabled() { $xmlSettings = $this.GetSpecializeSettings() $terminalServicesLocalSessionManager = $this.GetTerminalServicesLocalSessionManager($xmlSettings) $this.SetBoolNodeValue($terminalServicesLocalSessionManager, 'fDenyTSConnections', $false) } <# .SYNOPSIS Configures whether to support user or network level authentication for RDP sessions .LINK https://technet.microsoft.com/en-us/library/cc722192(v=ws.10).aspx #> [void]SetRemoteDesktopAuthenticationMode([EnumRdpAuthentication]$AuthenticationMode) { $xmlSettings = $this.GetSpecializeSettings() $terminalServicesRdpWinStationExtensions = $this.GetTerminalServicesRdpWinStationExtensions($xmlSettings) $this.SetTextNodeValue($terminalServicesRdpWinStationExtensions, 'UserAuthentication', [UnattendXml]::RdpAuthenticationModeToString($AuthenticationMode)) } <# .SYNOPSIS Generates XML text that can be saved to a file #> [string]ToXml() { $xmlWriterSettings = New-Object System.Xml.XmlWriterSettings $xmlWriterSettings.Indent = $true; $xmlWriterSettings.Encoding = [System.Text.Encoding]::Utf8 $stringWriter = New-Object System.IO.StringWriter $xmlWriter = [System.Xml.XmlWriter]::Create($stringWriter, $xmlWriterSettings) $this.document.WriteContentTo($xmlWriter) $xmlWriter.Flush() $stringWriter.Flush() return $stringWriter.ToString() } } |