Public/New-ProvisioningPackage.ps1
# Copyright 2019 David Haymond. # # This Source Code Form is subject to the terms of the Mozilla Public License, # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. <# .SYNOPSIS Creates a new Windows desktop provisioning package. .DESCRIPTION The New-ProvisioningPackage command creates a new Windows provisioning package designed to automate setting up a new Windows 10 or Windows 11 device using a small subset of common settings. Packages can be copied to a USB drive and inserted during the Windows Out of Box Experience (OOBE) that appears when the device is first powered on. At a minimum, packages set the computer name and local administrator credentials. Packages can also join the computer to a domain, install applications, run scripts, add Wi-Fi profiles, and enable multi-app kiosk mode. The -ComputerName parameter accepts multiple computer names, and one provisioning package will be created for each computer name. You can also pipe a list of computer names to New-ProvisioningPackage. Each package contains a unique computer name but the packages are otherwise identical. .PARAMETER ComputerName Specifies one or more computer names. New-ProvisioningPackage will generate one package file for each computer name specified, using the computer name as the package file name with the .ppkg extension appended. For help generating a unique name, you can use %SERIAL%, which includes a hardware-specific serial number, or you can use %RAND:x%, which generates random characters of x length. .PARAMETER LocalAdminCredential Specifies the credentials of a local administrator account to create. .PARAMETER DomainName Specifies the name of a domain to join. If omitted, the provisioning package will set up the device as a workgroup computer. .PARAMETER DomainJoinCredential Specifies the credentials of a domain account with permission to join computers to a domain. .PARAMETER Application Specifies a list of applications or scripts to run during provisioning. This parameter accepts an array of values, each of which should be either a string or a hashtable. If a string is used, it should point to the path of the script or executable. Note that the file will be invoked without any command-line arguments. If a hashtable is used, it should contain one or more of the following keys: - Path (required): Specifies one or more scripts or executables. The first file will be executed (unless you specify otherwise using the Command key below). Any additional files will be copied to the same folder and can be referenced by the primary script or executable. - Name: Specifies the name of the application. Defaults to the first Path entry. - Command: Specifies the command executed during provisioning. Defaults to `cmd /c "<setup.exe>"`, where <setup.exe> is replaced with the name of the first entry in the Path key. Include this key when you need to pass command-line arguments to the executable (e.g. to cause an installer to run silently), or if your script isn't a batch file and needs to be run in a shell other than cmd.exe (e.g. powershell.exe). - ContinueInstall: Indicates whether subsequent installations should continue if the current install fails. Defaults to $true. - RestartRequired: Indicates whether or not to force a restart after running this application (and before proceeding with subsequent applications). Defaults to $false. - RestartExitCode: Specifies the exit code returned by the installer that indicates a restart is needed to complete installation. Defaults to 3010. - SuccessExitCode: Specifies the exit code returned by the installer that indicates the installation was successful. Defaults to 0. The application output is logged to a file on the target device located at %SystemDrive%\<app name>-install.log, where <app name> is the name of the app as specified in the Name key explained above. .PARAMETER Wifi Specifies a list of Wi-Fi profiles to configure during provisioning. This parameter accepts an array of hashtables, each containing one or more of the following keys: - Ssid (required): Specifies the Wi-Fi network name or SSID. - SecurityKey: If present, specifies the network security key for a WPA2-Personal Wi-Fi network. Omit this key if the network is open (unsecured). - AutoConnect: Indicates whether the target device should automatically connect to this network when in range. Defaults to $true. Note that if the Wifi parameter is specified, the resulting provisioning package will fail to install if the target device does not have a wireless network interface controller (NIC). .PARAMETER KioskXml Specifies the path to an XML file that contains the multi-app kiosk mode configuration settings. To learn how to create this file, see https://docs.microsoft.com/en-us/windows/configuration/lock-down-windows-10-to-specific-apps. .PARAMETER Path Specifies the output directory to save the provisioning packages to. .PARAMETER Force Overwrite existing files and surpress confirmation prompts. .EXAMPLE New-ProvisioningPackage -ComputerName PC01, PC02 Creates two provisioning packages (PC01.ppkg, PC02.ppkg), one for each computer specified in the ComputerName parameter. The user will be prompted for the local administrator account username and password. .EXAMPLE $params = @{ LocalAdminCredential = 'User' Application = '.\Office\setup.exe' Wifi = @{ Ssid = 'Internal'; SecurityKey = 'HouseSpeakerB#' } } Get-Content computer-names.txt | New-ProvisioningPackage @params Gets a list of computer names from computer-names.txt and pipes them to New-ProvisioningPackage, which generates a new package for each computer name. Each package will provision its respective device with a local administrator account named "User" and a Wi-Fi profile that connects to the Internal network with a security key of "HouseSpeakerB#". The package will also execute the ".\Office\setup.exe" installer during the provisioning process. .EXAMPLE $apps = @( 'setup.exe', @{ Path = 'C:\install.exe' Command = 'cmd /c "install.exe" /quiet' RestartRequired = $true } ) $wifiProfiles = @( @{ Ssid = 'AcmePrivate'; SecurityKey = 'CompanySecrets' } @{ Ssid = 'AcmePublic' } ) $params = @{ ComputerName = 'Bob-Laptop' LocalAdminCredential = 'admin' DomainName = 'ACME' DomainJoinCredential = 'ACME\Admin' Application = $apps Wifi = $wifiProfiles New-ProvisioningPackage @params Creates a provisioning package for a device to be named "Bob-Laptop". In addition to naming the computer, the provisioning package will join the device to the ACME domain using the ACME\Admin account and create a local admininistrator account named "admin". Two applications are specified for installation: setup.exe from the current directory, and C:\install.exe. The latter application will be run with the /quiet argument, and the device will restart after installation. The package will also configure two Wi-Fi profiles on the target device: the AcmePrivate WPA2-Personal network with a security key of CompanySecrets, and the open AcmePublic network. .EXAMPLE $params = @{ ComputerName = ('KIOSK1', 'KIOSK2') KioskXml = 'kiosk-settings.xml' Application = @{ Path = ('script.ps1', 'data.json') Command = 'powershell.exe -NoProfile -File script.ps1' } } New-ProvisioningPackage @params Creates provisioning packages for two kiosk computers using the settings in kiosk-settings.xml. Also executes a PowerShell script, which has access to a supporting datafile (data.json). .NOTES For more information about provisioning packages, visit https://docs.microsoft.com/en-us/windows/configuration/provisioning-packages/provisioning-packages For information about multi-app kiosk mode, see https://docs.microsoft.com/en-us/windows/configuration/lock-down-windows-10-to-specific-apps #> function New-ProvisioningPackage { [CmdletBinding( DefaultParameterSetName = 'Workgroup', SupportsShouldProcess = $true, ConfirmImpact = 'Low' )] param ( [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true )] [ValidatePattern('^[A-Za-z0-9\-:%]{1,63}$', ErrorMessage = 'The computer name "{0}" is invalid. ' + 'Supply a name composed of letters, numbers, and hyphens that is between 1 and 63 characters long. ' + 'Using %SERIAL% or %RAND:x% is also allowed to generate names (see Windows Configuration Designer documentation).' )] [string[]] $ComputerName, [Parameter( Mandatory = $true, Position = 1 )] [pscredential] $LocalAdminCredential, [Parameter( ParameterSetName = 'Domain', Mandatory = $true )] [string] $DomainName, [Parameter( ParameterSetName = 'Domain', Mandatory = $true )] [pscredential] $DomainJoinCredential, [object[]] $Application, [hashtable[]] $Wifi, [string] $KioskXml, [string] [PSDefaultValue(Help = 'Current directory')] $Path = (Get-Location).Path, [switch] $Force ) begin { if ($Force -and -not $Confirm) { $ConfirmPreference = 'None' } $script:TEMPDIR = New-TemporaryDirectory $icdRoot = Get-IcdRoot if (-not $icdRoot) { if (Install-Icd -Force:$Force) { $icdRoot = Get-IcdRoot } else { throw "Missing dependency: Windows Imaging and Configuration Designer" } } $icdExec = Join-Path -Path $icdRoot -ChildPath 'ICD.exe' $icdLogPath = Join-Path -Path $env:TEMP -ChildPath 'ProvisioningTools-ICD.log' } process { $ComputerName | ForEach-Object -Process { $currentComputerName = $_ $params = @{ ComputerName = $currentComputerName LocalAdminCredential = $LocalAdminCredential DomainName = $DomainName DomainJoinCredential = $DomainJoinCredential Application = $Application Wifi = $Wifi KioskXml = $KioskXml } $customizationsArgs = Get-CustomizationsArg @params $doc = New-CustomizationsXmlDocument @customizationsArgs $ppkgPath = Confirm-PackagePath -ComputerName $currentComputerName -Path $Path -Force:$Force if ($ppkgPath -and $PSCmdlet.ShouldProcess("Paths: $ppkgPath", "Build Provisioning Package")) { $xmlName = [System.IO.Path]::GetRandomFileName() $xmlPath = Join-Path -Path $script:TEMPDIR.FullName -ChildPath "$xmlName.xml" Set-XmlContent -XmlDocument $doc -Path $xmlPath -Confirm:$false try { # Run ICD.exe to generate the provisioning package $icdArgs = Get-IcdArg -IcdRoot $icdRoot -XmlPath $xmlPath -PackagePath $ppkgPath -Overwrite $Force $startProcessArgs = @{ FilePath = $icdExec ArgumentList = $icdArgs WindowStyle = 'Hidden' Wait = $true Confirm = $false RedirectStandardError = $icdLogPath } Start-Process @startProcessArgs } finally { # ICD.exe also generates a .cat file in addition to the .ppkg file. # We don't need it, so delete it. $catPath = Join-Path -Path $Path -ChildPath "$($currentComputerName).cat" Remove-Item -Path $catPath -ErrorAction SilentlyContinue -Confirm:$false -Force } if (-not (Test-Path $ppkgPath)) { Write-Error "Couldn't find the output package. Please see $icdLogPath for details." } } } } end { $script:TEMPDIR | Remove-Item -Recurse -Force } } |