WinImageBuilder.psm1
# Copyright 2017 Cloudbase Solutions Srl # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. $ErrorActionPreference = "Stop" Set-StrictMode -Version 2 Import-Module Dism $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition $localResourcesDir = "$scriptPath\UnattendResources" $kmsProductKeysFile = "$scriptPath\kms_product_keys.json" Import-Module "$scriptPath\Config.psm1" Import-Module "$scriptPath\UnattendResources\ini.psm1" # Enforce Tls1.2, as GitHub and more websites require it. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $noHypervWarning = @" The Hyper-V role is missing from this machine. In order to be able to finish generating the image, you need to install the Hyper-V role. You can do so by running the following commands from an elevated powershell command prompt: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -All -NoRestart Don't forget to reboot after you install the Hyper-V role. "@ $VirtIODrivers = @("balloon", "netkvm", "pvpanic", "qemupciserial", "qxl", "qxldod", "vioinput", "viorng", "vioscsi", "vioserial", "viostor") $VirtIODriverMappings = @{ "2k8" = @(60, 1); "2k8r2" = @(61, 1); "w7" = @(61, 0); "2k12" = @(62, 1); "w8" = @(62, 0); "2k12r2" = @(63, 1); "w8.1" = @(63, 0); "2k16" = @(100, 1); "w10" = @(100, 0); } $AvailableCompressionFormats = @("tar","gz","zip") . "$scriptPath\Interop.ps1" class PathShouldExistAttribute : System.Management.Automation.ValidateArgumentsAttribute { [void] Validate([object]$arguments, [System.Management.Automation.EngineIntrinsics]$engineIntrinsics) { if (!(Test-Path -Path $arguments)) { throw "Path ``$arguments`` not found." } } } function Write-Log { Param($messageToOut) Write-Host ("{0} - {1}" -f @((Get-Date), $messageToOut)) } function Execute-Retry { Param( [parameter(Mandatory=$true)] $command, [int]$maxRetryCount=4, [int]$retryInterval=4 ) $currErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" $retryCount = 0 while ($true) { try { $res = Invoke-Command -ScriptBlock $command $ErrorActionPreference = $currErrorActionPreference return $res } catch [System.Exception] { $retryCount++ if ($retryCount -ge $maxRetryCount) { $ErrorActionPreference = $currErrorActionPreference throw } else { if($_) { Write-Warning $_ } Start-Sleep $retryInterval } } } } function Is-Administrator { $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent() $prp = New-Object System.Security.Principal.WindowsPrincipal($wid) $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator $isAdmin = $prp.IsInRole($adm) if (!$isAdmin) { throw "This cmdlet must be executed in an elevated administrative shell" } } function Get-WimInteropObject { Param( [parameter(Mandatory=$true)] [string]$WimFilePath ) return (New-Object WIMInterop.WimFile -ArgumentList $WimFilePath) } function Get-WimFileImagesInfo { <# .SYNOPSIS This function retrieves a list of the Windows Editions from an ISO file. .DESCRIPTION This function reads the Images content of the WIM file that can be found on a mounted ISO and it returns an object for each Windows Edition, each object containing a list of properties. .PARAMETER WimFilePath Location of the install.wim file found on the mounted ISO image. #> [CmdletBinding()] Param( [parameter(Mandatory=$true)] [string]$WimFilePath ) PROCESS { $w = Get-WimInteropObject $WimFilePath return $w.Images } } function Create-ImageVirtualDisk { [CmdletBinding()] Param( [parameter(Mandatory=$true)] [string]$VhdPath, [parameter(Mandatory=$true)] [long]$Size, [parameter(Mandatory=$true)] [string]$DiskLayout ) Write-Log "Creating Virtual Disk Image: $VhdPath..." $v = [WIMInterop.VirtualDisk]::CreateVirtualDisk($VhdPath, $Size) try { $v.AttachVirtualDisk() $path = $v.GetVirtualDiskPhysicalPath() # -match creates an env variable called $Matches $path -match "\\\\.\\PHYSICALDRIVE(?<num>\d+)" | Out-Null $diskNum = $Matches["num"] $volumeLabel = "OS" if ($DiskLayout -eq "UEFI") { Initialize-Disk -Number $diskNum -PartitionStyle GPT # EFI partition $systemPart = New-Partition -DiskNumber $diskNum -Size 200MB ` -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' ` -AssignDriveLetter & format.com "$($systemPart.DriveLetter):" /FS:FAT32 /Q /Y | Out-Null if ($LASTEXITCODE) { throw "Format failed" } # MSR partition New-Partition -DiskNumber $diskNum -Size 128MB ` -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}' | Out-Null # Windows partition $windowsPart = New-Partition -DiskNumber $diskNum -UseMaximumSize ` -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" ` -AssignDriveLetter } else { # BIOS Initialize-Disk -Number $diskNum -PartitionStyle MBR $windowsPart = New-Partition -DiskNumber $diskNum -UseMaximumSize ` -AssignDriveLetter -IsActive $systemPart = $windowsPart } Format-Volume -DriveLetter $windowsPart.DriveLetter ` -FileSystem NTFS -NewFileSystemLabel $volumeLabel ` -Force -Confirm:$false | Out-Null return @("$($systemPart.DriveLetter):", "$($windowsPart.DriveLetter):") } finally { Write-Log "Successfuly created disk: $VhdPath" $v.Close() } } function Apply-Image { Param( [parameter(Mandatory=$true)] [string]$winImagePath, [parameter(Mandatory=$true)] [string]$wimFilePath, [parameter(Mandatory=$true)] [int]$imageIndex ) Write-Log ('Applying Windows image "{0}" in "{1}"' -f $wimFilePath, $winImagePath) #Expand-WindowsImage -ImagePath $wimFilePath -Index $imageIndex -ApplyPath $winImagePath # Use Dism in place of the PowerShell equivalent for better progress update # and for ease of interruption with CTRL+C & Dism.exe /apply-image /imagefile:${wimFilePath} /index:${imageIndex} /ApplyDir:${winImagePath} if ($LASTEXITCODE) { throw "Dism apply-image failed" } } function Reset-BCDSearchOrder { Param( [parameter(Mandatory=$true)] [string]$systemDrive, [parameter(Mandatory=$true)] [string]$windowsDrive, [parameter(Mandatory=$true)] [string]$diskLayout ) if ($diskLayout -eq "BIOS") { Write-Log "Resetting BCD boot border" $ErrorActionPreference = "SilentlyContinue" $bcdeditPath = "${windowsDrive}\windows\system32\bcdedit.exe" if (!(Test-Path $bcdeditPath)) { Write-Warning ('"{0}" not found, using online version' -f $bcdeditPath) $bcdeditPath = "bcdedit.exe" } & $bcdeditPath /store ${systemDrive}\boot\BCD /set `{bootmgr`} device locate if ($LASTEXITCODE) { Write-Warning "BCDEdit failed: bootmgr device locate" } & $bcdeditPath /store ${systemDrive}\boot\BCD /set `{default`} device locate if ($LASTEXITCODE) { Write-Warning "BCDEdit failed: default device locate" } & $bcdeditPath /store ${systemDrive}\boot\BCD /set `{default`} osdevice locate if ($LASTEXITCODE) { Write-Warning "BCDEdit failed: default osdevice locate" } $ErrorActionPreference = "Stop" } } function Create-BCDBootConfig { Param( [parameter(Mandatory=$true)] [string]$systemDrive, [parameter(Mandatory=$true)] [string]$windowsDrive, [parameter(Mandatory=$true)] [string]$diskLayout, [parameter(Mandatory=$true)] [object]$image ) Write-Log ("Create BCDBoot Config for {0}" -f @($image.ImageName)) $bcdbootLocalPath = "bcdboot.exe" $bcdbootPath = "${windowsDrive}\windows\system32\bcdboot.exe" if (!(Test-Path $bcdbootPath)) { Write-Warning ('"{0}" not found, using online version' -f $bcdbootPath) $bcdbootPath = $bcdbootLocalPath } $ErrorActionPreference = "SilentlyContinue" # Note: older versions of bcdboot.exe don't have a /f argument if ($image.ImageVersion.Major -eq 6 -and $image.ImageVersion.Minor -lt 2) { $bcdbootOutput = & $bcdbootPath ${windowsDrive}\windows /s ${systemDrive} /v # Note(avladu): Retry using the local bcdboot path # when generating Win7 images on Win10 / Server 2k16 hosts if ($LASTEXITCODE) { Write-Log "Retrying with bcdboot.exe from host" $bcdbootOutput = & $bcdbootLocalPath ${windowsDrive}\windows /s ${systemDrive} /v /f $diskLayout } } else { $bcdbootOutput = & $bcdbootPath ${windowsDrive}\windows /s ${systemDrive} /v /f $diskLayout } if ($LASTEXITCODE) { $ErrorActionPreference = "Stop" throw "BCDBoot failed with error: $bcdbootOutput" } Reset-BCDSearchOrder -systemDrive $systemDrive -windowsDrive $windowsDrive ` -diskLayout $diskLayout $ErrorActionPreference = "Stop" Write-Log "BCDBoot config has been created." } function Transform-Xml { Param( [parameter(Mandatory=$true)] [string]$xsltPath, [parameter(Mandatory=$true)] [string]$inXmlPath, [parameter(Mandatory=$true)] [string]$outXmlPath, [parameter(Mandatory=$true)] $xsltArgs ) $xslt = New-Object System.Xml.Xsl.XslCompiledTransform($false) $xsltSettings = New-Object System.Xml.Xsl.XsltSettings($false, $true) $xslt.Load($xsltPath, $xsltSettings, (New-Object System.Xml.XmlUrlResolver)) $outXmlFile = New-Object System.IO.FileStream($outXmlPath, ` [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write) $argList = New-Object System.Xml.Xsl.XsltArgumentList foreach ($k in $xsltArgs.Keys) { $argList.AddParam($k, "", $xsltArgs[$k]) } $xslt.Transform($inXmlPath, $argList, $outXmlFile) $outXmlFile.Close() } function Generate-UnattendXml { Param( [parameter(Mandatory=$true)] [string]$inUnattendXmlPath, [parameter(Mandatory=$true)] [string]$outUnattendXmlPath, [parameter(Mandatory=$true)] [object]$image, [ValidatePattern("^$|^\S{5}-\S{5}-\S{5}-\S{5}-\S{5}")] [parameter(Mandatory=$false)] [string]$productKey, [parameter(Mandatory=$false)] $administratorPassword ) Write-Log "Generate Unattend Xml :$outUnattendXmlPath..." $xsltArgs = @{} $xsltArgs["processorArchitecture"] = ([string]$image.ImageArchitecture).ToLower() $xsltArgs["imageName"] = $image.ImageName $xsltArgs["versionMajor"] = $image.ImageVersion.Major $xsltArgs["versionMinor"] = $image.ImageVersion.Minor $xsltArgs["installationType"] = $image.ImageInstallationType $xsltArgs["administratorPassword"] = $administratorPassword if ($productKey) { $xsltArgs["productKey"] = $productKey } Transform-Xml -xsltPath "$scriptPath\Unattend.xslt" -inXmlPath $inUnattendXmlPath ` -outXmlPath $outUnattendXmlPath -xsltArgs $xsltArgs Write-Log "Xml was generated." } function Detach-VirtualDisk { Param( [Parameter(Mandatory=$true)] [string]$VhdPath ) try { $v = [WIMInterop.VirtualDisk]::OpenVirtualDisk($VhdPath) $v.DetachVirtualDisk() } finally { if ($v) { $v.Close() } } } function Check-DismVersionForImage { Param( [Parameter(Mandatory=$true)] [object]$image ) $dismVersion = New-Object System.Version ` (Get-Command dism.exe).FileVersionInfo.ProductVersion if ($image.ImageVersion.CompareTo($dismVersion) -gt 0) { Write-Warning "The installed version of DISM is older than the Windows image" } } function Convert-VirtualDisk { Param( [Parameter(Mandatory=$true)] [string]$vhdPath, [Parameter(Mandatory=$true)] [string]$outPath, [Parameter(Mandatory=$true)] [string]$format, [Parameter(Mandatory=$false)] [boolean]$CompressQcow2 ) Write-Log "Convert Virtual Disk: $vhdPath..." $format = $format.ToLower() $qemuParams = @("$scriptPath\bin\qemu-img.exe", "convert") if ($format -eq "qcow2" -and $CompressQcow2) { Write-Log "Qcow2 compression has been enabled." $qemuParams += @("-c", "-W", "-m16") } $qemuParams += @("-O", $format, $vhdPath, $outPath) Write-Log "Converting virtual disk image from $vhdPath to $outPath..." Execute-Retry { Start-Executable $qemuParams } Write-Log "Finish to convert virtual disk." } function Copy-CustomResources { Param( [Parameter(Mandatory=$true)] [string]$ResourcesDir, [string]$CustomResources, [string]$CustomScripts ) Write-Log "Copy Custom Resources: $CustomResources..." if (!(Test-Path "$resourcesDir")) { New-Item -Type Directory $resourcesDir | Out-Null } if ($CustomResources) { if (!(Test-Path "$resourcesDir\CustomResources")) { New-Item -Type Directory "$resourcesDir\CustomResources" | Out-Null } Write-Log "Copying: $CustomResources $resourcesDir" # Custom resources can be multiple directories, split by "," $customResourcesSplit = $CustomResources.split(",") foreach ($customResource in $customResourcesSplit) { Copy-Item -Recurse "$customResource\*" "$resourcesDir\CustomResources" } } if ($CustomScripts) { if (!(Test-Path "$resourcesDir\CustomScripts")) { New-Item -Type Directory "$resourcesDir\CustomScripts" | Out-Null } Write-Log "Copying: $CustomScripts $resourcesDir" # Custom scripts can be multiple directories, split by "," $customScriptsSplit = $CustomScripts.split(",") foreach ($customScript in $customScriptsSplit) { Copy-Item -Recurse "$customScript\*" "$resourcesDir\CustomScripts" } } Write-Log "Custom Resources at: $ResourcesDir." } function Copy-UnattendResources { Param( [Parameter(Mandatory=$true)] [string]$resourcesDir, [Parameter(Mandatory=$true)] [string]$imageInstallationType, [Parameter(Mandatory=$false)] [boolean]$InstallMaaSHooks, [Parameter(Mandatory=$false)] [string]$VMwareToolsPath ) Write-Log "Copy Unattend Resources: $imageInstallationType..." # Workaround to recognize the $resourcesDir drive. This seems a PowerShell bug Get-PSDrive | Out-Null if (!(Test-Path "$resourcesDir")) { New-Item -Type Directory $resourcesDir | Out-Null } Write-Log "Copying: $localResourcesDir $resourcesDir" Copy-Item -Recurse -Force "$localResourcesDir\*" $resourcesDir if ($InstallMaaSHooks) { $src = Join-Path $localResourcesDir "windows-curtin-hooks\curtin" if ((Test-Path $src)) { $dst = Split-Path $resourcesDir Copy-Item -Recurse $src $dst } else { throw "The Windows curtin hooks module is not present. Please run git submodule update --init " } } if ($VMwareToolsPath) { Write-Log "Copying VMwareTools..." $dst = Join-Path $resourcesDir "\VMware-tools.exe" Write-Log "VMware tools path is: $VMwareToolsPath" Copy-Item $VMwareToolsPath $dst } Write-Log "Resources have been copied." } function Validate-WindowsImageConfig { Param( [Parameter(Mandatory=$true)] [array]$ImageConfig ) switch ($windowsImageConfig.image_type) { "VMware" { if (!$windowsImageConfig.vmware_tools_path) { Write-Warning "VMware Tools path was not set. The image that you create might not be usable on VMware hypervisor type." } elseif (!(Test-Path $windowsImageConfig.vmware_tools_path)) { throw "VMware Tools path does not exist." } } } if ($windowsImageConfig.compression_format) { $compressionFormats = $windowsImageConfig.compression_format.split(".") $invalidCompressionFormat = $compressionFormats | Where-Object ` {$AvailableCompressionFormats -notcontains $_} if ($invalidCompressionFormat) { throw "Compression format $invalidCompressionFormat not available." } } } function Download-CloudbaseInit { Param( [Parameter(Mandatory=$true)] [string]$resourcesDir, [Parameter(Mandatory=$true)] [string]$osArch, [parameter(Mandatory=$false)] [switch]$BetaRelease, [parameter(Mandatory=$false)] [string]$MsiPath, [string]$CloudbaseInitConfigPath, [string]$CloudbaseInitUnattendedConfigPath ) $CloudbaseInitMsiPath = "$resourcesDir\CloudbaseInit.msi" if ($CloudbaseInitConfigPath) { Write-Log "Copying Cloudbase-Init custom configuration file..." Copy-Item -Force $CloudbaseInitConfigPath "$resourcesDir\cloudbase-init.conf" } if ($CloudbaseInitUnattendedConfigPath) { Write-Log "Copying Cloudbase-Init custom unattended configuration file..." Copy-Item -Force $CloudbaseInitUnattendedConfigPath ` "$resourcesDir\cloudbase-init-unattend.conf" } if ($MsiPath) { if (!(Test-Path $MsiPath)) { throw "Cloudbase-Init installer could not be copied. $MsiPath does not exist." } Write-Log "Copying Cloudbase-Init..." Copy-Item $MsiPath $CloudbaseInitMsiPath return } Write-Log "Downloading Cloudbase-Init..." $msiBuildArchMap = @{ "amd64" = "x64" "i386" = "x86" "x86" = "x86" } $msiBuildSuffix = "" if (-not $BetaRelease) { $msiBuildSuffix = "_Stable" } $CloudbaseInitMsi = "CloudbaseInitSetup{0}_{1}.msi" -f @($msiBuildSuffix, $msiBuildArchMap[$osArch]) $CloudbaseInitMsiUrl = "https://www.cloudbase.it/downloads/$CloudbaseInitMsi" Execute-Retry { (New-Object System.Net.WebClient).DownloadFile($CloudbaseInitMsiUrl, $CloudbaseInitMsiPath) } } function Download-ZapFree { Param( [Parameter(Mandatory=$true)] [string]$resourcesDir, [Parameter(Mandatory=$true)] [string]$osArch ) $ZapFreePath = "$resourcesDir\zapfree.exe" $ZapFree32Path = "$resourcesDir\zapfree32.exe" $ZapFreeZipPath = "$resourcesDir\ntfszapfree.zip" Write-Log "Downloading ntfszapfree..." $ZapFreeUrl = "https://github.com/felfert/ntfszapfree/releases/latest/download/ntfszapfree.zip" Execute-Retry { (New-Object System.Net.WebClient).DownloadFile($ZapFreeUrl, $ZapFreeZipPath) } Expand-Archive -LiteralPath $ZapFreeZipPath -DestinationPath $resourcesDir -Force Remove-Item -Force $ZapFreeZipPath if ($osArch.equals("amd64")) { Remove-Item -Force $ZapFree32Path } else { Move-Item -Force -Path $ZapFree32Path -Destination $ZapFreePath } } function Generate-ConfigFile { Param( [Parameter(Mandatory=$true)] [string]$resourcesDir, [Parameter(Mandatory=$true)] [hashtable]$values ) Write-Log "Generate config file: $resourcesDir..." $configIniPath = "$resourcesDir\config.ini" Import-Module "$localResourcesDir\ini.psm1" foreach ($i in $values.GetEnumerator()) { Set-IniFileValue -Path $configIniPath -Section "DEFAULT" -Key $i.Key -Value $i.Value } Write-Log "Config file was generated." } function Add-DriversToImage { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$true)] [string]$driversPath ) Write-Log ('Adding drivers from "{0}" to image "{1}"' -f $driversPath, $winImagePath) Execute-Retry { & Dism.exe /image:${winImagePath} /Add-Driver /driver:${driversPath} /ForceUnsigned /recurse if ($LASTEXITCODE) { throw "Dism failed to add drivers from: $driversPath" } } -retryInterval 1 } function Add-PackageToImage { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$true)] [string]$packagePath, [Parameter(Mandatory=$false)] [boolean]$ignoreErrors ) Write-Log ('Adding packages from "{0}" to image "{1}"' -f $packagePath, $winImagePath) & Dism.exe /image:${winImagePath} /Add-Package /Packagepath:${packagePath} if ($LASTEXITCODE -and !$ignoreErrors) { throw "Dism failed to add packages from: $packagePath" } elseif ($LASTEXITCODE) { Write-Log ("Dism failed to add packages from $packagePath. Skipping.") } } function Enable-FeaturesInImage { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$true)] [array]$featureNames ) if ($featureNames) { $cmd = @( "Dism.exe", ("/image:{0}" -f ${winImagePath}), "/Enable-Feature" ) foreach ($featureName in $featureNames) { $cmd += ("/FeatureName:{0}" -f $featureName) } Execute-Retry { & $cmd[0] $cmd[1..$cmd.Length] if ($LASTEXITCODE) { throw "Dism failed to enable features: $featureNames" } } } } function Add-CapabilitiesToImage { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$true)] [array]$capabilityNames ) if ($capabilityNames) { $cmd = @( "Dism.exe", ("/image:{0}" -f ${winImagePath}), "/Add-Capability" ) foreach ($capabilityName in $capabilityNames) { $cmd += ("/CapabilityName:{0}" -f $capabilityName) } Execute-Retry { & $cmd[0] $cmd[1..$cmd.Length] if ($LASTEXITCODE) { throw "Dism failed to add capabilities: $capabilityNames" } } } } function Check-EnablePowerShellInImage { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$true)] [object]$image ) # Windows 2008 R2 Server Core does not have powershell enabled by default $v62 = New-Object System.Version 6, 2, 0, 0 if ($image.ImageVersion.CompareTo($v62) -lt 0 ` -and $image.ImageInstallationType -eq "Server Core") { Write-Log "Enabling PowerShell in the Windows image" $psFeatures = @("NetFx2-ServerCore", "MicrosoftWindowsPowerShell", "NetFx2-ServerCore-WOW64", "MicrosoftWindowsPowerShell-WOW64" ) Enable-FeaturesInImage $winImagePath $psFeatures } } function Is-IsoFile { Param( [parameter(Mandatory=$true)] [string]$FilePath ) return ([System.IO.Path]::GetExtension($FilePath) -eq ".iso") } function Is-ServerInstallationType { Param( [parameter(Mandatory=$true)] [object]$image ) return ($image.ImageInstallationType -in @("Server", "Server Core")) } function Get-VirtIODrivers { Param( [parameter(Mandatory=$true)] [int]$MajorMinorVersion, [parameter(Mandatory=$true)] [int]$IsServer, [parameter(Mandatory=$true)] [string]$BasePath, [parameter(Mandatory=$true)] [string]$Architecture, [parameter(Mandatory=$false)] [int]$RecursionDepth = 0 ) Write-Log "Getting Virtual IO Drivers: $BasePath..." $driverPaths = @() foreach ($driver in $VirtioDrivers) { foreach ($osVersion in $VirtIODriverMappings.Keys) { $map = $VirtIODriverMappings[$osVersion] if (!(($map[0] -eq $MajorMinorVersion) -and ($map[1] -eq $isServer))) { continue } $driverPath = "{0}\{1}\{2}\{3}" -f @($basePath, $driver, $osVersion, $architecture) if (Test-Path $driverPath) { $driverPaths += $driverPath break } } } if (!$driverPaths -and $RecursionDepth -lt 1) { # Note(avladu): Fallback to 2012r2/w8.1 if no drivers are found $driverPaths = Get-VirtIODrivers -MajorMinorVersion 63 -IsServer $IsServer ` -BasePath $BasePath -Architecture $Architecture -RecursionDepth 1 } return $driverPaths Write-Log "Finished to get IO Drivers." } function Add-VirtIODrivers { Param( [parameter(Mandatory=$true)] [string]$vhdDriveLetter, [parameter(Mandatory=$true)] [object]$image, [parameter(Mandatory=$true)] [string]$driversBasePath ) Write-Log "Adding Virtual IO Drivers: $driversBasePath..." # For VirtIO ISO with drivers version lower than 1.8.x if ($image.ImageVersion.Major -eq 6 -and $image.ImageVersion.Minor -eq 0) { $virtioVer = "VISTA" } elseif ($image.ImageVersion.Major -eq 6 -and $image.ImageVersion.Minor -eq 1) { $virtioVer = "WIN7" } elseif ($image.ImageVersion.Major -eq 6 -and $image.ImageVersion.Minor -ge 2) { $virtioVer = "WIN8" } elseif (($image.ImageVersion.Major -eq 10 -and $image.ImageVersion.Minor -eq 0) ` -or $image.ImageVersion.Major -gt 10) { $virtioVer = "w10" } else { throw "Unsupported Windows version for VirtIO drivers: {0}" ` -f $image.ImageVersion } $virtioDir = "{0}\{1}\{2}" -f $driversBasePath, $virtioVer, $image.ImageArchitecture if (Test-Path $virtioDir) { Add-DriversToImage $vhdDriveLetter $virtioDir return } # For VirtIO ISO with drivers version higher than 1.8.x $majorMinorVersion = [string]$image.ImageVersion.Major + [string]$image.ImageVersion.Minor $virtioDriversPaths = Get-VirtIODrivers -MajorMinorVersion $majorMinorVersion ` -IsServer ([int](Is-ServerInstallationType $image)) -BasePath $driversBasePath ` -Architecture $image.ImageArchitecture foreach ($virtioDriversPath in $virtioDriversPaths) { if (Test-Path $virtioDriversPath) { Add-DriversToImage $vhdDriveLetter $virtioDriversPath } } Write-Log "Virtual IO Drivers was added." } function Add-VirtIODriversFromISO { <# .SYNOPSIS This function adds VirtIO drivers from a given ISO path to a mounted Windows VHD image. The VirtIO ISO contains all the synthetic drivers for the KVM hypervisor. .DESCRIPTION This function takes the VirtIO drivers from a specified ISO file and installs them into the given VHD, based on the characteristics given by the image parameter (which contains the image version, image architecture and installation type). More info can be found here: https://fedoraproject.org/wiki/Windows_Virtio_Drivers .PARAMETER VHDDriveLetter The drive letter of the mounted Windows VHD image. .PARAMETER Image The exact flavor of Windows installed on that image, so that the supported VirtIO drivers can be installed. .PARAMETER ISOPath The full path of the VirtIO ISO file containing the drivers. #> Param( [parameter(Mandatory=$true)] [string]$vhdDriveLetter, [parameter(Mandatory=$true)] [object]$image, [parameter(Mandatory=$true)] [string]$isoPath ) Write-Log "Adding Virtual IO Drivers from ISO: $isoPath..." $v = [WIMInterop.VirtualDisk]::OpenVirtualDisk($isoPath) try { if (Is-IsoFile $isoPath) { $v.AttachVirtualDisk() # We call Get-PSDrive to refresh the list of active drives. # Otherwise, "Test-Path $driversBasePath" will return $False # http://www.vistax64.com/powershell/2653-powershell-does-not-update-subst-mapped-drives.html Get-PSDrive | Out-Null $devicePath = $v.GetVirtualDiskPhysicalPath() $driversBasePath = Execute-Retry { $res = (Get-DiskImage -DevicePath $devicePath ` | Get-Volume).DriveLetter if (!$res) { throw "Failed to mount ISO ${isoPath}" } return $res } $driversBasePath += ":" Write-Log "Adding drivers from $driversBasePath" Add-VirtIODrivers -vhdDriveLetter $vhdDriveLetter -image $image ` -driversBasePath $driversBasePath } else { throw "The $isoPath is not a valid iso path." } } catch{ throw $_ } finally { if ($v) { $v.DetachVirtualDisk() $v.Close() } } Write-Log "ISO Virtual Drivers have been adeed." } function Set-DotNetCWD { # Make sure the PowerShell and .Net CWD match [Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath } function Get-PathWithoutExtension { Param( [Parameter(Mandatory=$true)] [string]$Path, [int]$Depth = 0 ) # NOTE(avladu): Cleanup all the extensions $fileName = [System.IO.Path]::GetFileNameWithoutExtension($Path) for($i = 0;$i -lt $Depth;$i++) { $fileName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) } return Join-Path ([System.IO.Path]::GetDirectoryName($Path)) $fileName } function Compress-Image { Param( [Parameter(Mandatory=$true)] [string]$VirtualDiskPath, [Parameter(Mandatory=$true)] [string]$ImagePath, [Parameter(Mandatory=$true)] [string]$compressionFormats, [parameter(Mandatory=$false)] [string]$ZipPassword ) Write-Log "Compressing image $VirtualDiskPath..." if (!(Test-Path $VirtualDiskPath)) { throw "$VirtualDiskPath not found" } $7zip = Get-7zipPath $pigz = Get-PigzPath $imageName = (Get-Item $VirtualDiskPath).Name $compressionFormatsArray = $compressionFormats.split(".") $virtualDiskPathRoot = [System.IO.Path]::GetDirectoryName((Resolve-Path $VirtualDiskPath).Path) $compressedImagePath = $VirtualDiskPath + "." + $compressionFormats if (Test-Path $compressedImagePath) { throw "Compressed $compressedImagePath already exists." } if (Test-Path $ImagePath) { throw "Target compression path $ImagePath already exists." } # Avoid storing the full path in the archive Push-Location $VirtualDiskPathRoot foreach ($compressionFormat in $compressionFormatsArray) { if ($compressionFormat -eq "tar") { $imageNameTar = "${imageName}.tar" Write-Log "Compressing ${imageName} to tar ${imageNameTar}" & $7zip a -aoa -ttar $imageNameTar $imageName if ($LASTEXITCODE) { throw "7za.exe failed to create tar ${imageNameTar}" } Remove-Item -Force $imageName $imageName = $imageNameTar } if ($compressionFormat -eq "gz") { $imageNameGz = "${imageName}.gz" Write-Log "Compressing ${imageName} to gzip ${imageNameGz}" & $pigz -3 -f -q $imageName if ($LASTEXITCODE) { throw "pigz.exe failed to create gzip ${imageNameGz}" } $imageName = $imageNameGz } if ($compressionFormat -eq "zip") { $imageNameZip = "${imageName}.zip" Write-Log "Compressing ${imageName} to zip ${imageNameZip}" $zipCommand = @($7zip, "a", "-aoa", "-tzip", $imageNameZip, ` $imageName, "-mx1") if ($ZipPassword) { Write-Log "The zip password is: $ZipPassword" $zipCommand += "-p$ZipPassword" } Start-Executable -Command $zipCommand Remove-Item -Force $imageName $imageName = $imageNameZip } } Pop-Location if (!(Test-Path $compressedImagePath)) { throw "Failed to compress image ${VirtualDiskPath} to ${compressedImagePath}" } if ($compressedImagePath -ne $imagePath) { Move-Item -Force $compressedImagePath $imagePath } } function Decompress-File { Param( [parameter(Mandatory=$true)] [string]$FilePath, [parameter(Mandatory=$true)] [string]$CompressionFormat, [string]$ZipPassword ) Write-Log "Decompressing image $FilePath..." if (!(Test-Path $FilePath)) { throw "$FilePath not found" } $7zip = Get-7zipPath $pigz = Get-PigzPath $imageName = (Get-Item $FilePath).Name $virtualDiskPathRoot = [System.IO.Path]::GetDirectoryName((Resolve-Path $FilePath).Path) # Avoid storing the full path in the archive Push-Location $VirtualDiskPathRoot try { if ($CompressionFormat -eq "tar") { $imageNameTar = $imageName -replace ".tar", "" Write-Log "Decompressing tar ${imageName} to ${imageNameTar}" $tarCommand = @($7zip, "e", $imageName, "-y") Start-Executable -Command $tarCommand | Out-Null $imageName = $imageNameTar } if ($CompressionFormat -eq "gz") { $imageNameGz = $imageName -replace ".gz", "" Write-Log "Decompressing gzip ${imageName} to ${imageNameGz}" & $pigz -k -d -f $imageName | Out-Null if ($LASTEXITCODE) { throw "pigz.exe failed to decompress gzip ${imageName}" } $imageName = $imageNameGz } if ($CompressionFormat -eq "zip") { $imageNameZip = $imageName -replace ".zip", "" Write-Log "Decompressing zip ${imageName} to ${imageNameZip}" $zipCommand = @($7zip, "e", $imageName, "-y") if ($ZipPassword) { $zipCommand += "-p$ZipPassword" } Start-Executable -Command $zipCommand | Out-Null $imageName = $imageNameZip } } finally { Pop-Location } return (Join-Path $virtualDiskPathRoot $imageName) } function Start-Executable { [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [Alias("7za.exe")] [array]$Command ) PROCESS { $cmdType = (Get-Command $Command[0]).CommandType if ($cmdType -eq "Application") { $ErrorActionPreference = "SilentlyContinue" $ret = & $Command[0] $Command[1..$Command.Length] 2>&1 $ErrorActionPreference = "Stop" } else { $ret = & $Command[0] $Command[1..$Command.Length] } if ($cmdType -eq "Application" -and $LASTEXITCODE) { Throw ("Failed to run: " + ($Command -Join " ")) } if ($ret -and $ret.Length -gt 0) { return $ret } return $false } } function Get-7zipPath { return Join-Path -Path "$localResourcesDir" -ChildPath "7za.exe" } function Get-PigzPath { return Join-Path -Path "$localResourcesDir" -ChildPath "pigz.exe" } function Resize-VHDImage { <# .SYNOPSIS This function resizes the VHD image to a minimum VHD size plus a FreeSpace parameter value buffer. .DESCRIPTION This function mounts the VHD given as parameter and retrieves the drive letter. After that it computes the actual size and the minimum supported size. .PARAMETER VirtualDiskPath The path to the VHD image to resize. .PARAMETER FreeSpace This is the extra buffer parameter. #> Param( [Parameter(Mandatory=$true)] [string]$VirtualDiskPath, [parameter(Mandatory=$false)] [Uint64]$FreeSpace=500MB ) Write-Log "Shrinking VHD to minimum size" $vhdSize = (Get-VHD -Path $VirtualDiskPath).Size $vhdSizeGB = $vhdSize/1GB Write-Log "Initial VHD size is: $vhdSizeGB GB" $mountedVHD = Mount-VHD -Path $VirtualDiskPath -Passthru Get-PSDrive | Out-Null $Drive = ($mountedVHD | Get-Disk | Get-Partition | Get-Volume | ` Sort-Object -Property Size -Descending | Select-Object -First 1).DriveLetter try { Optimize-Volume -DriveLetter $Drive -Defrag -ReTrim -SlabConsolidate $partitionInfo = Get-Partition -DriveLetter $Drive $partitionResizeInfo = Get-PartitionSupportedSize -DriveLetter $Drive $MinSize = $partitionResizeInfo.SizeMin $MaxSize = $partitionResizeInfo.SizeMax $CurrSize = $partitionInfo.Size/1GB Write-Log "Current partition size: $CurrSize GB" # Leave free space for making sure Sysprep finishes successfuly $newSizeGB = [int](($MinSize + $FreeSpace)/1GB) + 1 $NewSize = $newSizeGB*1GB Write-Log "New partition size: $newSizeGB GB" if (($NewSize - $FreeSpace) -gt $MinSize) { $global:i = 0 $global:sizeIncreased = 0 try { $step = 100MB # Adding 10 retries means increasing the size to a max of 1.5GB, # which should be enough for the Resize-Partition to succeed. Execute-Retry { $global:sizeIncreased = ($NewSize + ($step * $global:i)) Write-Log "Size increased: $sizeIncreased" $global:i = $global:i + 1 Resize-Partition -DriveLetter $Drive -Size $global:sizeIncreased -ErrorAction "Stop" } -maxRetryCount 10 } catch { Write-Log "Partition could not be resized using an incremental method" Write-Log "Trying to resize partition using a binary search method" $binaryTries = 0 # For example, with 10 binary tries and a max min difference of 1TB space, # we will get 1024 / 1024 = 1 GB difference $binaryMaxTries = 10 $MinSize = $global:sizeIncreased while (($MinSize -lt $MaxSize) -and ($binaryTries -lt $binaryMaxTries)) { $desiredSize = $MinSize + ($MaxSize - $MinSize) / 2 Write-Log "Trying to decrease the partition to $desiredSize" try { Resize-Partition -DriveLetter $Drive -Size $desiredSize -ErrorAction "Stop" Write-Log "Partition resized to $desiredSize. MaxSize becomes the desired size" $MaxSize = $desiredSize } catch { Write-Log "Partition could not be resized to $desiredSize. MinSize becomes the desired size" $MinSize = $desiredSize } $binaryTries ++ } } } } finally { Dismount-VHD -Path $VirtualDiskPath } $vhdMinSize = (Get-VHD -Path $VirtualDiskPath).MinimumSize if ($vhdSize -gt $vhdMinSize) { Resize-VHD $VirtualDiskPath -ToMinimumSize } $FinalDiskSize = ((Get-VHD -Path $VirtualDiskPath).Size/1GB) Write-Log "Final disk size: $FinalDiskSize GB" } function Check-Prerequisites { $needsHyperV = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V if ($needsHyperV.State -ne "Enabled") { throw $noHypervWarning } } function Wait-ForVMShutdown { Param( [Parameter(Mandatory=$true)] [string]$Name ) Write-Log "Waiting for $Name to finish sysprep." $isOff = (Get-VM -Name $Name).State -eq "Off" $vmMessages = @{} while ($isOff -eq $false) { Start-Sleep 1 $vmState = (Get-VM -Name $Name).State $isOff = $vmState -eq "Off" try { if ($vmState -ne "Running" -or ` !(Get-VMIntegrationService $Name -Name "Key-Value Pair Exchange").Enabled) { continue } $currentVMMessages = Get-KVPData -VMName $Name if (!$currentVMMessages) {continue} foreach ($stage in $currentVMMessages.keys) { if (!$vmMessages[$stage]) { Write-Log ("- - {0}: {1}" -f @($stage, $currentVMMessages[$stage])) } } $vmMessages = $currentVMMessages } catch { Write-Log "Could not retrieve VM runtime logs" } } } function Run-Sysprep { Param( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$VhdPath, [Parameter(Mandatory=$true)] [Uint64]$Memory, [Parameter(Mandatory=$true)] [int]$CpuCores, [Parameter(Mandatory=$true)] [string]$VMSwitch, [ValidateSet("1", "2")] [string]$Generation = "1", [switch]$DisableSecureBoot ) Write-Log "Creating VM $Name attached to $VMSwitch" New-VM -Name $Name -MemoryStartupBytes $Memory -SwitchName $VMSwitch ` -VhdPath $VhdPath -Generation $Generation | Out-Null Set-VMProcessor -VMname $Name -count $CpuCores | Out-Null Set-VMMemory -VMname $Name -DynamicMemoryEnabled:$false | Out-Null $vmAutomaticCheckpointsEnabledWrapper = (Get-VM -Name $Name) | Select-Object 'AutomaticCheckpointsEnabled' ` -ErrorAction SilentlyContinue $vmAutomaticCheckpointsEnabled = $false if ($vmAutomaticCheckpointsEnabledWrapper) { $vmAutomaticCheckpointsEnabled = $vmAutomaticCheckpointsEnabledWrapper.AutomaticCheckpointsEnabled } if ($vmAutomaticCheckpointsEnabled) { Set-VM -VMName $Name -AutomaticCheckpointsEnabled:$false } if ($DisableSecureBoot -and $Generation -eq "2") { Set-VMFirmware -VMName $Name -EnableSecureBoot Off } Write-Log "Starting $Name" Start-VM $Name | Out-Null Start-Sleep 5 Wait-ForVMShutdown $Name Remove-VM $Name -Confirm:$false -Force } function Convert-KvpData($xmlData) { $data = @{} foreach ($xmlItem in $xmlData) { $key = "" $value = "" $xmlData = [Xml]$xmlItem foreach ($i in $xmlData.INSTANCE.PROPERTY) { if ($i.Name -eq "Name") { $key = $i.Value } if ($i.Name -eq "Data") { $value = $i.Value } } if ($key -like "ImageGenerationLog-*") { $key = $key.replace("ImageGenerationLog-","") $data[$key] = $value } } return $data } function Get-KVPData { param($VMName) $wmiNamespace = "root\virtualization\v2" $vm = Get-WmiObject -Namespace $wmiNamespace ` -Query "Select * From Msvm_ComputerSystem Where ElementName=`'$VMName`'" if (!$vm) {return} $kvp = Get-WmiObject -Namespace $wmiNamespace ` -Query "Associators of {$vm} Where AssocClass=Msvm_SystemDevice ResultClass=Msvm_KvpExchangeComponent" if (!$kvp) {return} $kvpData = Convert-KvpData($kvp.GuestIntrinsicExchangeItems) return $kvpData } function Get-ImageInformation { Param( [Parameter(Mandatory=$true)] [string]$DriveLetter, [Parameter(Mandatory=$true)] [string]$ImageName ) $ntDll = "$driveLetter\Windows\system32\ntdll.dll" if (Test-Path $ntDll) { $versionString = (Get-Item $ntDll).VersionInfo.ProductVersion $osVersion = $versionString.split('.') $imageVersion = @{ "Major" = $osVersion[0]; "Minor" = $osVersion[1]; } } else { throw "Unable to determine OS Version" } if ((Get-Item $ntDll).Target -like "*amd64_microsoft-windows-ntdll*") { $imageArchitecture = "AMD64" } else { $imageArchitecture = "i386" } if ($imageName -notlike "*server*") { $imageInstallationType = "Client" } elseif ($imageName -like '*Core') { $imageInstallationType = "Server Core" } else { $imageInstallationType = "Server" } return @{ "imageVersion" = $imageVersion; "imageArchitecture" = $imageArchitecture; "imageInstallationType" = $imageInstallationType; } } function Set-WindowsWallpaper { Param( [Parameter(Mandatory=$true)][PathShouldExist()] [string]$WinDrive, [Parameter(Mandatory=$false)] [string]$WallpaperPath, [Parameter(Mandatory=$false)] [string]$WallpaperSolidColor ) Write-Log "Setting wallpaper..." $useWallpaperImage = $false $wallpaperGPOPath = Join-Path $localResourcesDir "GPO" if ($WallpaperPath -and $WallpaperSolidColor) { throw "WallpaperPath and WallpaperSolidColor cannot be set at the same time." } if ($WallpaperPath -or !($WallpaperSolidColor)) { if (!$WallpaperPath -or !(@('.jpg', '.jpeg') -contains ` (Get-Item $windowsImageConfig.wallpaper_path -ErrorAction SilentlyContinue).Extension)) { $WallpaperPath = Join-Path $localResourcesDir "Wallpaper.jpg" } if (!(Test-Path $WallpaperPath)) { throw "Walpaper path ``$WallpaperPath`` does not exist." } $wallpaperDestinationFolder = Join-Path $winDrive "\Windows\web\Wallpaper\Cloud" if (!(Test-Path $wallpaperDestinationFolder)) { New-Item -Type Directory $wallpaperDestinationFolder | Out-Null } Copy-Item -Force $WallpaperPath "$wallpaperDestinationFolder\Wallpaper.jpg" Write-Log "Wallpaper copied to the image." # Note(avladu) if the image already has been booted and has a wallpaper, the # GPO will not be applied for the users who have already logged in. # The wallpaper can still be changed by replacing the cached one. $cachedWallpaperPartPath = "\Users\Administrator\AppData\Roaming\Microsoft\Windows\Themes\TranscodedWallpaper*" $cachedWallpaperPath = Join-Path -ErrorAction SilentlyContinue $winDrive $cachedWallpaperPartPath if (Test-Path $cachedWallpaperPath) { $wallpaperPathFullName = (Get-Item $cachedWallpaperPath).FullName Remove-Item -Recurse -Force ((Get-Item $cachedWallpaperPath).DirectoryName + "\*") Copy-Item -Force $WallpaperPath $wallpaperPathFullName Write-Log "Cached wallpaper for user Administrator has been replaced." } $useWallpaperImage = $true } $windowsLocalGPOPath = Join-Path $winDrive "\Windows\System32\GroupPolicy" if (!(Test-Path $windowsLocalGPOPath)) { New-Item -Type Directory $windowsLocalGPOPath | Out-Null } Copy-Item -Recurse -Force "$wallpaperGPOPath\*" "$windowsLocalGPOPath\" $basePolicyRegistry = Join-Path $windowsLocalGPOPath "User/Registry.pol" $wallpaperPolicyRegistry = Join-Path $windowsLocalGPOPath "User/Registry-wallpaper.pol" $solidColorPolicyRegistry = Join-Path $windowsLocalGPOPath "User/Registry-solid-color.pol" if ($useWallpaperImage) { Move-Item -Force $wallpaperPolicyRegistry $basePolicyRegistry Remove-Item -Force $solidColorPolicyRegistry -ErrorAction SilentlyContinue } else { Move-Item -Force $solidColorPolicyRegistry $basePolicyRegistry Remove-Item -Force $wallpaperPolicyRegistry -ErrorAction SilentlyContinue } Write-Log "Wallpaper GPO copied to the image." Write-Log "Wallpaper was set." } function Reset-WindowsWallpaper { Param( [Parameter(Mandatory=$true)][PathShouldExist()] [string]$WinDrive ) $wallpaperDestination = Join-Path $winDrive "\Windows\web\Wallpaper\Cloud\Wallpaper.jpg" Remove-Item -Force -ErrorAction SilentlyContinue $wallpaperDestination $cachedWallpaperPartPath = "\Users\Administrator\AppData\Roaming\Microsoft\Windows\Themes\TranscodedWallpaper*" $cachedWallpaperPath = Join-Path -ErrorAction SilentlyContinue $winDrive $cachedWallpaperPartPath Remove-Item -Force -ErrorAction SilentlyContinue $cachedWallpaperPath $windowsLocalGPOPath = Join-Path $winDrive "\Windows\System32\GroupPolicy\User\Registry.pol" Remove-Item -Force -ErrorAction SilentlyContinue $windowsLocalGPOPath } function Get-TotalLogicalProcessors { $count = 0 $cpus = Get-WmiObject Win32_Processor foreach ($cpu in $cpus) { $count += $cpu.NumberOfLogicalProcessors } return $count } function Map-KMSProductKey { param($ImageName, $ImageVersion) $productKeysMap = Get-Content -Encoding ASCII $kmsProductKeysFile | ConvertFrom-Json try { $ImageVersionBuild = $ImageVersion.Build if ($ImageVersion.Major -eq "6") { $ImageVersionBuild = 0 } return ($productKeysMap | Select-Object -ExpandProperty "KMS" | ` Select-Object -ExpandProperty ([string]$ImageVersion.Major) | ` Select-Object -ExpandProperty ([string]$ImageVersion.Minor) | ` Select-Object -ExpandProperty ([string]$ImageVersionBuild) | ` Select-Object -ExpandProperty $ImageName) } catch { Write-Log "No valid KMS key found for image ${ImageName}" } } function Clean-WindowsUpdates { Param( [Parameter(Mandatory=$true)] [string]$winImagePath, [Parameter(Mandatory=$false)] [boolean]$PurgeUpdates ) Write-Log "Running offline dism Cleanup-Image..." if (([System.Environment]::OSVersion.Version.Major -gt 6) -or ([System.Environment]::OSVersion.Version.Minor -ge 2)) { if (!$PurgeUpdates) { Dism.exe /image:${winImagePath} /Cleanup-Image /StartComponentCleanup } else { Dism.exe /image:${winImagePath} /Cleanup-Image /StartComponentCleanup /ResetBase } if ($LASTEXITCODE) { throw "Offline dism Cleanup-Image failed." } else { Write-Log "Offline dism Cleanup-Image completed." } } } function New-WindowsOnlineImage { <# .SYNOPSIS This function generates a Windows image using Hyper-V to instantiate the image in order to apply the updates and install cloudbase-init. .DESCRIPTION This command requires Hyper-V to be enabled, a VMSwitch to be configured for external network connectivity if the updates are to be installed, which is highly recommended. This command uses internally the New-WindowsCloudImage to generate the base image and start a Hyper-V instance using the base image. After the Hyper-V instance shuts down, the resulting VHDX is shrunk to a minimum size and converted to the required format. The list of parameters can be found in the Config.psm1 file. #> Param( [parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$ConfigFilePath ) $windowsImageConfig = Get-WindowsImageConfig -ConfigFilePath $ConfigFilePath if ($windowsImageConfig.gold_image) { if (($windowsImageConfig.image_type -ne 'HYPER-V') -or ` (!$windowsImageConfig.virtual_disk_format -in @("VHD","VHDX")) -or ` (![System.IO.Path]::GetExtension($windowsImageConfig.image_path) -in @(".vhd",".vhdx"))) { throw "A golden image file should have a vhd(x) extension/disk` format and the image_type should be HYPER-V." } } Write-Log "Windows online image generation started." Is-Administrator if (!$windowsImageConfig.run_sysprep -and !$windowsImageConfig.force) { throw "You chose not to run sysprep. This will build an unusable Windows image. If you really want to continue use the ``force = true`` config option." } Check-Prerequisites if ($windowsImageConfig.external_switch) { $switch = Get-VMSwitch -Name $windowsImageConfig.external_switch -ErrorAction SilentlyContinue if (!$switch) { throw "Selected vmswitch {0} does not exist" -f $windowsImageConfig.external_switch } if ($switch.SwitchType -ne "External" -and !$windowsImageConfig.force) { throw ("Selected switch {0} is not an external switch. If you really want to continue use the ``force = true`` flag." -f $windowsImageConfig.external_switch) } } if ([int]$windowsImageConfig.cpu_count -gt [int](Get-TotalLogicalProcessors)) { throw "CpuCores larger then available (logical) CPU cores." } if (Test-Path $windowsImageConfig.image_path) { Write-Log "Found already existing image file. Removing it..." -ForegroundColor Yellow Remove-Item -Force $windowsImageConfig.image_path Write-Log "Already existent image file has been removed." -ForegroundColor Yellow } try { $barePath = Get-PathWithoutExtension $windowsImageConfig.image_path 3 $virtualDiskPath = $barePath + ".vhdx" $imagePath = $virtualDiskPath # We need different config files for New-WindowsCloudImage and New-WindowsOnlineImage $offlineConfigFilePath = $ConfigFilePath + ".offline" Copy-Item -Path $ConfigFilePath -Destination $offlineConfigFilePath Set-IniFileValue -Path $offlineConfigFilePath -Section 'DEFAULT' -Key 'image_path' ` -Value $virtualDiskPath Set-IniFileValue -Path $offlineConfigFilePath -Section 'DEFAULT' -Key 'virtual_disk_format' ` -Value 'VHDX' if ($windowsImageConfig.zip_password) { Remove-IniFileValue -Path $offlineConfigFilePath ` -Key 'zip_password' -Section 'DEFAULT' } if ($windowsImageConfig.compression_format) { Remove-IniFileValue -Path $offlineConfigFilePath ` -Key 'compression_format' -Section 'DEFAULT' } New-WindowsCloudImage -ConfigFilePath $offlineConfigFilePath if ($windowsImageConfig.run_sysprep) { if($windowsImageConfig.disk_layout -eq "UEFI") { $generation = "2" } else { $generation = "1" } $Name = "WindowsOnlineImage-Sysprep" + (Get-Random) Run-Sysprep -Name $Name -Memory $windowsImageConfig.ram_size -vhdPath $virtualDiskPath ` -VMSwitch $switch.Name -CpuCores $windowsImageConfig.cpu_count ` -Generation $generation -DisableSecureBoot:$windowsImageConfig.disable_secure_boot } if ($windowsImageConfig.shrink_image_to_minimum_size -eq $true) { Resize-VHDImage $virtualDiskPath } Optimize-VHD $VirtualDiskPath -Mode Full if ($windowsImageConfig.image_type -eq "MAAS") { $imagePath = $barePath + ".raw" Write-Log "Converting VHD to RAW" Convert-VirtualDisk -vhdPath $virtualDiskPath -outPath $imagePath -format "raw" Remove-Item -Force $virtualDiskPath } if ($windowsImageConfig.image_type -ceq "VMware") { $imagePath = $barePath + ".vmdk" Write-Log "Converting VHD to VMDK" Convert-VirtualDisk -vhdPath $virtualDiskPath -outPath $imagePath -format "vmdk" Remove-Item -Force $virtualDiskPath } if ($windowsImageConfig.image_type -eq "KVM") { $imagePath = $barePath + ".qcow2" Write-Log "Converting VHD to Qcow2" Convert-VirtualDisk -vhdPath $virtualDiskPath -outPath $imagePath -format "qcow2" ` -CompressQcow2 $windowsImageConfig.compress_qcow2 Remove-Item -Force $virtualDiskPath } if ($windowsImageConfig.compression_format) { Compress-Image -VirtualDiskPath $imagePath ` -ImagePath $windowsImageConfig.image_path ` -compressionFormats $windowsImageConfig.compression_format ` -ZipPassword $windowsImageConfig.zip_password | Out-Null } elseif ($imagePath -ne $windowsImageConfig['image_path']) { Move-Item -Force $imagePath $windowsImageConfig['image_path'] } } catch { Write-Log $_ if ($windowsImageConfig.image_path -and (Test-Path $windowsImageConfig.image_path)) { Remove-Item -Force $windowsImageConfig.image_path -ErrorAction SilentlyContinue } Throw } Write-Log "Windows online image generation finished. Image path: $($windowsImageConfig.image_path)" } function New-WindowsCloudImage { <# .SYNOPSIS This function creates a Windows Image, starting from an ISO file, without the need of Hyper-V to be enabled. The image, to become ready for cloud usage, needs to be started on a hypervisor and it will automatically shut down when it finishes all the operations needed to become cloud ready: cloudbase-init installation, updates and sysprep. .DESCRIPTION This script can generate a Windows Image in one of the following formats: VHD, VHDX, QCow2, VMDK or RAW. It takes the Windows flavor indicated by the ImageName from the WIM file and based on the parameters given, it will generate an image. This function does not require Hyper-V to be enabled, but the generated image is not ready to be deployed, as it needs to be started manually on another hypervisor. The image is ready to be used when it shuts down. The list of parameters can be found in the Config.psm1 file. #> param ( [parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$ConfigFilePath ) Write-Log "Cloud image generation started." try { $windowsImageConfig = Get-WindowsImageConfig -ConfigFilePath $ConfigFilePath $mountedWindowsIso = $null if ($windowsImageConfig.wim_file_path.EndsWith('.iso')) { $windowsImageConfig.wim_file_path = Get-Command $windowsImageConfig.wim_file_path ` -ErrorAction Ignore | Select-Object -ExpandProperty Source if($windowsImageConfig.wim_file_path -eq $null){ throw ("Unable to find source iso. Either specify the full path or add " + ` "the folder containing the iso to the path variable") } $mountedWindowsIso = [WIMInterop.VirtualDisk]::OpenVirtualDisk($windowsImageConfig.wim_file_path) $mountedWindowsIso.AttachVirtualDisk() Get-PSDrive | Out-Null $devicePath = $mountedWindowsIso.GetVirtualDiskPhysicalPath() $basePath = ((Get-DiskImage -DevicePath $devicePath ` | Get-Volume).DriveLetter) + ":" $windowsImageConfig.wim_file_path = "$($basePath)\Sources\install.wim" } Validate-WindowsImageConfig $windowsImageConfig Set-DotNetCWD Is-Administrator $image = Get-WimFileImagesInfo -WimFilePath $windowsImageConfig.wim_file_path | ` Where-Object { $_.ImageName -eq $windowsImageConfig.image_name } if (!$image) { throw ("Image {0} not found in WIM file {1}" -f @($windowsImageConfig.image_name, $windowsImageConfig.wim_file_path)) } Check-DismVersionForImage $image if (Test-Path $windowsImageConfig.image_path) { Write-Log "Found already existing image file. Removing it..." -ForegroundColor Yellow Remove-Item -Force $windowsImageConfig.image_path Write-Log "Already existent image file has been removed." -ForegroundColor Yellow } $vhdPath = "{0}.vhdx" -f (Get-PathWithoutExtension $windowsImageConfig.image_path) if (Test-Path $vhdPath) { Remove-Item -Force $vhdPath } try { $drives = Create-ImageVirtualDisk -VhdPath $vhdPath -Size $windowsImageConfig.disk_size ` -DiskLayout $windowsImageConfig.disk_layout $winImagePath = "$($drives[1])\" $resourcesDir = "${winImagePath}UnattendResources" $outUnattendXmlPath = "${winImagePath}Unattend.xml" $xmlunattendPath = Join-Path $scriptPath $windowsImageConfig['unattend_xml_path'] $xmlParams = @{'InUnattendXmlPath' = $xmlunattendPath; 'OutUnattendXmlPath' = $outUnattendXmlPath; 'Image' = $image; 'AdministratorPassword' = $windowsImageConfig.administrator_password; } if ($windowsImageConfig.product_key) { $productKey = $windowsImageConfig.product_key if ($productKey -eq "default_kms_key") { $productKey = Map-KMSProductKey $windowsImageConfig.image_name $image.ImageVersion } if ($productKey) { $xmlParams.Add('productKey', $productKey) } } Generate-UnattendXml @xmlParams Copy-UnattendResources -resourcesDir $resourcesDir -imageInstallationType $image.ImageInstallationType ` -InstallMaaSHooks $windowsImageConfig.install_maas_hooks ` -VMwareToolsPath $windowsImageConfig.vmware_tools_path Copy-CustomResources -ResourcesDir $resourcesDir -CustomResources $windowsImageConfig.custom_resources_path ` -CustomScripts $windowsImageConfig.custom_scripts_path Copy-Item $ConfigFilePath "$resourcesDir\config.ini" if ($windowsImageConfig.enable_custom_wallpaper) { Set-WindowsWallpaper -WinDrive $winImagePath -WallpaperPath $windowsImageConfig.wallpaper_path ` -WallpaperSolidColor $windowsImageConfig.wallpaper_solid_color } if ($windowsImageConfig.zero_unused_volume_sectors) { Download-ZapFree $resourcesDir ([string]$image.ImageArchitecture) } Download-CloudbaseInit -resourcesDir $resourcesDir -osArch ([string]$image.ImageArchitecture) ` -BetaRelease:$windowsImageConfig.beta_release -MsiPath $windowsImageConfig.msi_path ` -CloudbaseInitConfigPath $windowsImageConfig.cloudbase_init_config_path ` -CloudbaseInitUnattendedConfigPath $windowsImageConfig.cloudbase_init_unattended_config_path Apply-Image -winImagePath $winImagePath -wimFilePath $windowsImageConfig.wim_file_path ` -imageIndex $image.ImageIndex Create-BCDBootConfig -systemDrive $drives[0] -windowsDrive $drives[1] -diskLayout $windowsImageConfig.disk_layout ` -image $image Check-EnablePowerShellInImage $winImagePath $image if ($windowsImageConfig.drivers_path -and (Test-Path $windowsImageConfig.drivers_path)) { Add-DriversToImage $winImagePath $windowsImageConfig.drivers_path } if ($windowsImageConfig.virtio_iso_path) { Add-VirtIODriversFromISO -vhdDriveLetter $winImagePath -image $image ` -isoPath $windowsImageConfig.virtio_iso_path } if ($windowsImageConfig.virtio_base_path) { Add-VirtIODrivers -vhdDriveLetter $winImagePath -image $image ` -driversBasePath $windowsImageConfig.virtio_base_path } if ($windowsImageConfig.extra_features) { Enable-FeaturesInImage $winImagePath $windowsImageConfig.extra_features } if ($windowsImageConfig.extra_packages) { foreach ($package in $windowsImageConfig.extra_packages.split(",")) { Add-PackageToImage $winImagePath $package -ignoreErrors $windowsImageConfig.extra_packages_ignore_errors } } if ($windowsImageConfig.extra_capabilities) { Add-CapabilitiesToImage $winImagePath $windowsImageConfig.extra_capabilities } if ($windowsImageConfig.clean_updates_offline) { Clean-WindowsUpdates $winImagePath -PurgeUpdates $windowsImageConfig.purge_updates } Optimize-Volume -DriveLetter $drives[1].replace(":","") -Defrag -ReTrim -SlabConsolidate } finally { if (Test-Path $vhdPath) { Detach-VirtualDisk $vhdPath } } $barePath = Get-PathWithoutExtension $windowsImageConfig.image_path 3 $imagePath = $barePath + "." + $windowsImageConfig.virtual_disk_format if (!($windowsImageConfig.virtual_disk_format -in @("VHD", "VHDX"))) { Convert-VirtualDisk -vhdPath $vhdPath -outPath $imagePath ` -format $windowsImageConfig.virtual_disk_format Remove-Item -Force $vhdPath } elseif ($vhdPath -ne $imagePath) { Move-Item -Force $vhdPath $imagePath } if ($windowsImageConfig.compression_format) { Compress-Image -VirtualDiskPath $imagePath ` -ImagePath $windowsImageConfig['image_path'] ` -compressionFormats $windowsImageConfig.compression_format ` -ZipPassword $windowsImageConfig.zip_password | Out-Null } elseif ($imagePath -ne $windowsImageConfig['image_path']) { Move-Item -Force $imagePath $windowsImageConfig['image_path'] } Write-Log "Cloud image generation finished. Image path: $($windowsImageConfig.image_path)" } finally { if($mountedWindowsIso){ $mountedWindowsIso.DetachVirtualDisk() } } } function New-WindowsFromGoldenImage { <# .SYNOPSIS This function creates a functional Windows Image, starting from an already generated golden image. It will be started on Hyper-V and it will automatically shut down when it finishes all the operations needed to become cloud ready: cloudbase-init installation, updates and sysprep. .DESCRIPTION This function can generated a cloud ready Windows image starting from a golden image. The resulting image can have the following formats: VHD,VHDX, QCow2, VMDK or RAW. This command requires Hyper-V to be enabled, a VMSwitch to be configured for external network connectivity if the updates are to be installed, which is highly recommended. This command uses internally the New-WindowsOnlineImage to start a Hyper-V instance using the golden image provided as a parameter. After the Hyper-V instance shuts down, the resulting VHDX is shrunk to a minimum size and converted to the required format. #> [CmdletBinding()] Param( [parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$ConfigFilePath ) Write-Log "Cloud image from golden image generation started." $windowsImageConfig = Get-WindowsImageConfig -ConfigFilePath $ConfigFilePath Is-Administrator if (!$windowsImageConfig.run_sysprep -and !$windowsImageConfig.force) { throw "You chose not to run sysprep. This will build an unusable Windows image. If you really want to continue use the ``force = true`` config option." } Check-Prerequisites if ($windowsImageConfig.external_switch) { $switch = Get-VMSwitch -Name $windowsImageConfig.external_switch -ErrorAction SilentlyContinue if (!$switch) { throw "Selected vmswitch {0} does not exist" -f $windowsImageConfig.external_switch } if ($switch.SwitchType -ne "External" -and !$windowsImageConfig.force) { throw ("Selected switch {0} is not an external switch. If you really want to continue use the ``force = true`` flag." -f $windowsImageConfig.external_switch) } } if ([int]$windowsImageConfig.cpu_count -gt [int](Get-TotalLogicalProcessors)) { throw "CpuCores larger than available (logical) CPU cores." } try { Execute-Retry { Resize-VHD -Path $windowsImageConfig.gold_image_path -SizeBytes $windowsImageConfig.disk_size Set-VHD -Path $windowsImageConfig.gold_image_path -ResetDiskIdentifier -Force } | Out-Null Mount-VHD -Path $windowsImageConfig.gold_image_path -Passthru | Out-Null $driveNumber = Execute-Retry { Get-PSDrive | Out-Null $driveNumber = (Get-DiskImage -ImagePath $windowsImageConfig.gold_image_path | Get-Disk).Number if ($driveNumber -eq $null) { throw "Could not retrieve drive number for mounted vhd" } return $driveNumber } $partition = Execute-Retry { Get-PSDrive | Out-Null Set-Disk -Number $driveNumber -IsOffline $False $partition = Get-Partition -DiskNumber $driveNumber | Where-Object {@("Basic", "IFS") -contains $_.Type} if (!$partition -or !$partition.DriveLetter) { throw "Partition not found for mounted $($windowsImageConfig.gold_image_path)" } return $partition } $driveLetterGold = $partition.DriveLetter + ":" Write-Log "The mount point for the gold image is: ${driveLetterGold}" try { $maxPartitionSize = (Get-PartitionSupportedSize -DiskNumber $driveNumber -PartitionNumber ` $partition.PartitionNumber).SizeMax Resize-Partition -DiskNumber $driveNumber -PartitionNumber $partition.PartitionNumber ` -Size $maxPartitionSize -ErrorAction SilentlyContinue } catch { Write-Log "Partition has already the desired size" } $imageInfo = Get-ImageInformation $driveLetterGold -ImageName $windowsImageConfig.image_name if ($windowsImageConfig.virtio_iso_path) { Add-VirtIODriversFromISO -vhdDriveLetter $driveLetterGold -image $imageInfo ` -isoPath $windowsImageConfig.virtio_iso_path } if ($windowsImageConfig.drivers_path -and (Test-Path $windowsImageConfig.drivers_path)) { Add-DriversToImage $driveLetterGold $windowsImageConfig.drivers_path } $resourcesDir = Join-Path -Path $driveLetterGold -ChildPath "UnattendResources" Reset-BCDSearchOrder -systemDrive $driveLetterGold -windowsDrive $driveLetterGold ` -diskLayout $windowsImageConfig.disk_layout Copy-UnattendResources -resourcesDir $resourcesDir -imageInstallationType $windowsImageConfig.image_name ` -InstallMaaSHooks $windowsImageConfig.install_maas_hooks ` -VMwareToolsPath $windowsImageConfig.vmware_tools_path Copy-CustomResources -ResourcesDir $resourcesDir -CustomResources $windowsImageConfig.custom_resources_path ` -CustomScripts $windowsImageConfig.custom_scripts_path Copy-Item $ConfigFilePath "$resourcesDir\config.ini" if ($windowsImageConfig.enable_custom_wallpaper) { Set-WindowsWallpaper -WinDrive $driveLetterGold -WallpaperPath $windowsImageConfig.wallpaper_path ` -WallpaperSolidColor $windowsImageConfig.wallpaper_solid_color } else { Reset-WindowsWallpaper -WinDrive $driveLetterGold } if ($windowsImageConfig.zero_unused_volume_sectors) { Download-ZapFree $resourcesDir $imageInfo.imageArchitecture } Download-CloudbaseInit -resourcesDir $resourcesDir -osArch $imageInfo.imageArchitecture ` -BetaRelease:$windowsImageConfig.beta_release -MsiPath $windowsImageConfig.msi_path ` -CloudbaseInitConfigPath $windowsImageConfig.cloudbase_init_config_path ` -CloudbaseInitUnattendedConfigPath $windowsImageConfig.cloudbase_init_unattended_config_path Dismount-VHD -Path $windowsImageConfig.gold_image_path | Out-Null if ($windowsImageConfig.run_sysprep) { if($windowsImageConfig.disk_layout -eq "UEFI") { $generation = "2" } else { $generation = "1" } $Name = "WindowsGoldImage-Sysprep" + (Get-Random) Run-Sysprep -Name $Name -Memory $windowsImageConfig.ram_size -vhdPath $windowsImageConfig.gold_image_path ` -VMSwitch $switch.Name -CpuCores $windowsImageConfig.cpu_count ` -Generation $generation -DisableSecureBoot:$windowsImageConfig.disable_secure_boot } if ($windowsImageConfig.shrink_image_to_minimum_size -eq $true) { Resize-VHDImage $windowsImageConfig.gold_image_path } Optimize-VHD $windowsImageConfig.gold_image_path -Mode Full $barePath = Get-PathWithoutExtension $windowsImageConfig.image_path 3 $imagePath = $windowsImageConfig.gold_image_path if ($windowsImageConfig.image_type -eq "HYPER-V") { $imagePathVhdx = $barePath + ".vhdx" if ($imagePath -ne $imagePathVhdx) { Move-Item -Force $imagePath $imagePathVhdx $imagePath = $imagePathVhdx } } if ($windowsImageConfig.image_type -eq "MAAS") { $imagePathRaw = $barePath + ".raw" Write-Log "Converting VHD to RAW" Convert-VirtualDisk -vhdPath $imagePath -outPath $imagePathRaw ` -format "RAW" Remove-Item -Force $imagePath $imagePath = $imagePathRaw } if ($windowsImageConfig.image_type -eq "KVM") { $imagePathQcow2 = $barePath + ".qcow2" Write-Log "Converting VHD to QCow2" Convert-VirtualDisk -vhdPath $imagePath -outPath $imagePathQcow2 ` -format "qcow2" -CompressQcow2 $windowsImageConfig.compress_qcow2 Remove-Item -Force $imagePath $imagePath = $imagePathQcow2 } if ($windowsImageConfig.image_type -eq "VMware") { $imagePathVmdk = $barePath + ".vmdk" Write-Log "Converting VHD to VMDK" Convert-VirtualDisk -vhdPath $imagePath -outPath $imagePathVmdk ` -format "vmdk" Remove-Item -Force $imagePath $imagePath = $imagePathVmdk } if ($windowsImageConfig.compression_format) { Compress-Image -VirtualDiskPath $imagePath ` -ImagePath $windowsImageConfig['image_path'] ` -compressionFormats $windowsImageConfig.compression_format ` -ZipPassword $windowsImageConfig.zip_password | Out-Null } elseif ($imagePath -ne $windowsImageConfig['image_path']) { Move-Item -Force $imagePath $windowsImageConfig['image_path'] } Write-Log "Cloud image from golden image generation finished. Image path: $($windowsImageConfig.image_path)" } catch { try { Get-VHD $windowsImageConfig.gold_image_path | Dismount-VHD Remove-Item -Force $windowsImageConfig.gold_image_path } catch { Write-Log $_ } throw $_ } } function Test-OfflineWindowsImage { <# .SYNOPSIS This function verifies if a Windows image has been properly generated according to the configuration file. The verification is performed offline, without instantiating the image. .DESCRIPTION This function first tests if the config.image_path exists, then uses the extension and the config.compression_format to detect the compression and qemu-img binary to detect the image format. If any compression is detected, a decompression is performed for each compression. If the image format is other than vhdx, "qemu-img convert -O vhdx" is performed. The vhdx is mounted and the following checks are performed: 1. If Cloudbase-Init folder exists 2. If curtin (for MAAS) folder exists 3. If OEM drivers are installed Finally, the full chain of decompressed/converted files is removed. #> [CmdletBinding()] Param( [parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$ConfigFilePath ) Write-Log "Offline Windows image validation started." $windowsImageConfig = Get-WindowsImageConfig -ConfigFilePath $ConfigFilePath Is-Administrator if (!(Test-Path $windowsImageConfig.image_path)) { throw "Image validation failed: $($windowsImageConfig.image_path) does not exist." } $imageChain = @() $imagePath = $windowsImageConfig.image_path try { if ($windowsImageConfig.compression_format) { $compressionFormats = $windowsImageConfig.compression_format.split(".") [array]::Reverse($compressionFormats) $invalidCompressionFormat = $compressionFormats | Where-Object ` {$AvailableCompressionFormats -notcontains $_} if ($invalidCompressionFormat) { throw "Compression format $invalidCompressionFormat not available." } else { Write-Log "Compression format ${invalidCompressionFormat} is available." } foreach($compressionFormat in $compressionFormats) { $imageToDecompress = $imagePath $imagePath = Decompress-File -FilePath $imageToDecompress ` -CompressionFormat $compressionFormat ` -ZipPassword $windowsImageConfig.zip_password Write-Log "Image ${imageToDecompress} decompressed to ${imagePath}" $imageChain += $imagePath } } $imageFileExtension = [System.IO.Path]::GetExtension($imagePath) $fileExtension = '*' $diskFormat = '*' if ($windowsImageConfig.image_type -eq "HYPER-V") { $fileExtension = 'vhdx' $diskFormat = 'vhdx' if ($imageFileExtension -eq '.vhd') { $fileExtension = 'vhd' $diskFormat = 'vpc' } } if ($windowsImageConfig.image_type -eq "KVM") { $fileExtension = 'qcow2' $diskFormat = 'qcow2' } if ($windowsImageConfig.image_type -eq "MAAS") { $fileExtension = 'raw' $diskFormat = 'raw' } if ($windowsImageConfig.image_type -eq "VMware") { $fileExtension = 'vmdk' $diskFormat = 'vmdk' } if (!([System.IO.Path]::GetExtension($imagePath) -like ".${fileExtension}")) { throw "${imagePath} does not have ${fileExtension} extension." } else { Write-Log "${imagePath} has the correct ${fileExtension} extension." } $qemuInfoOutput = & "$scriptPath\bin\qemu-img.exe" info --output=json $imagePath $qemuInfoJson = ConvertFrom-Json ($qemuInfoOutput -join "") $qemuImgFormat = $qemuInfoJson | Select-Object "Format" if ($qemuImgFormat.Format -ne $diskFormat) { throw "${imagePath} does not have ${diskFormat} format." } else { Write-Log "${imagePath} has the correct ${diskFormat} format." } if (!(@("vhd", "vhdx").Contains($fileExtension))) { $barePath = Get-PathWithoutExtension $imagePath $tempImagePath = $barePath + ".vhdx" Convert-VirtualDisk -vhdPath $imagePath -outPath $tempImagePath ` -format "vhdx" $imagePath = $tempImagePath $imageChain += $imagePath } $childImagePath = $imagePath -ireplace ".vhd", "_bak.vhd" New-VHD -ParentPath $imagePath -Path $childImagePath | Out-Null $imagePath = $childImagePath $imageChain += $imagePath Mount-VHD -Path $imagePath -Passthru | Out-Null try { Get-PSDrive | Out-Null $driveNumber = (Get-DiskImage -ImagePath $imagePath | Get-Disk).Number Set-Disk -Number $driveNumber -IsOffline $False Get-PSDrive | Out-Null $mountPoint = (Get-Partition -DiskNumber $driveNumber | ` Where-Object {@("Basic", "IFS") -contains $_.Type}).DriveLetter + ":" # Test if Cloudbase-Init is installed $cloudbaseInitPath = "Program Files\Cloudbase Solutions\Cloudbase-Init" $cloudbaseInitPathX86 = "${cloudbaseInitPath} (x86)" if ((Test-Path (Join-Path $mountPoint $cloudbaseInitPath)) -or ` (Test-Path (Join-Path $mountPoint $cloudbaseInitPathX86))) { Write-Log "Cloudbase-Init is installed." } else { throw "Cloudbase-Init is not installed on the image." } # Test if curtin modules are installed if ($windowsImageConfig.install_maas_hooks) { if (Test-Path (Join-Path $mountPoint "curtin")) { Write-Log "Curtin hooks are installed." } else { throw "Curtin hooks are not installed on the image." } } # Test if extra drivers are installed if ($windowsImageConfig.virtio_iso_path -or $windowsImageConfig.virtio_base_path ` -or $windowsImageConfig.drivers_path) { $dismDriversOutput = (& Dism.exe /image:$mountPoint /Get-Drivers /Format:Table) $allDrivers = (Select-String "oem" -InputObject $dismDriversOutput -AllMatches).Matches.Count $virtDrivers = (Select-String "Red Hat, Inc." -InputObject $dismDriversOutput -AllMatches).Matches.Count $virtDrivers += (Select-String "QEMU" -InputObject $dismDriversOutput ` -AllMatches -CaseSensitive).Matches.Count Write-Log "Found ${allDrivers} drivers, from which ${virtDrivers} are VirtIO drivers." $minDriversCount = 1 if ($windowsImageConfig.virtio_iso_path -or $windowsImageConfig.virtio_base_path) { $minDriversCount = $VirtIODrivers.Count - 1 + ($allDrivers - $virtDrivers) } if ($allDrivers -lt $minDriversCount) { throw "Expected ${minDriversCount} ! >= ${allDrivers} drivers installed on the image." } } } finally { Dismount-VHD $imagePath } } finally { [array]::Reverse($imageChain) foreach ($chainItem in $imageChain) { if ($chainItem -ne $windowsImageConfig.image_path) { Write-Log "Removing chain file item ${chainItem}" Remove-Item -Force $chainItem -ErrorAction SilentlyContinue } } Write-Log "Offline Windows image validation finished." } } Export-ModuleMember New-WindowsCloudImage, Get-WimFileImagesInfo, New-MaaSImage, Resize-VHDImage, New-WindowsOnlineImage, Add-VirtIODriversFromISO, New-WindowsFromGoldenImage, Get-WindowsImageConfig, New-WindowsImageConfig, Test-OfflineWindowsImage |