Created
June 17, 2021 02:26
-
-
Save jborean93/b16f34069a614a2206093137e7da7b01 to your computer and use it in GitHub Desktop.
Creates a copy of a Windows ISO to use for unattended Hyper-V installations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright: (c) 2021, Jordan Borean (@jborean93) <[email protected]> | |
| # MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
| Function ConvertTo-UnattendedIso { | |
| <# | |
| .SYNOPSIS | |
| Converts a Windows ISO to one that doesn't require any keys to press on boot. | |
| .PARAMETER Path | |
| The Windows ISO to convert the UEFI loader to the no_prompt version. | |
| .PARAMETER Destination | |
| The path to create the new Windows ISO at. | |
| .NOTES | |
| This is used with Hyper-V to automatically boot to a Windows installer ISO without any manual intervention that | |
| is required on a default ISO. It has been tested on Windows Server 2012 and newer ISOs. | |
| Thanks to awakecoding for the initial script of creating an ISO using builtin tools to Windows. | |
| https://github.com/Devolutions/devolutions-labs/blob/bf9df3b89516885de29b41574ba04632caa24736/powershell/DevolutionsLabs.psm1#L67 | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory)] | |
| [String] | |
| $Path, | |
| [Parameter(Mandatory)] | |
| [String] | |
| $Destination | |
| ) | |
| $resolvedPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) | |
| if (-not (Test-Path -LiteralPath $resolvedPath)) { | |
| Write-Error -Message "Source ISO Path '$resolvedPath' does not exist" -Category ObjectNotFound | |
| return | |
| } | |
| $bootStream = $null | |
| Write-Verbose -Message "Mounting input ISO from '$resolvedPath'" | |
| $mount = Mount-DiskImage -ImagePath $resolvedPath -PassThru | |
| try { | |
| $volume = $mount | Get-Volume | |
| $mountPath = $volume.DriveLetter + ':' | |
| # The default is efisys.bin which prompts the user to press any key in a small time period. By using the | |
| # noprompt version Hyper-V will automatically boot the ISO. | |
| $efiNoPrompt = [IO.Path]::Combine($mountPath, 'efi', 'microsoft', 'boot', 'efisys_noprompt.bin') | |
| Write-Verbose -Message "Checking for efi noprompt at '$efiNoprompt'" | |
| if (-not (Test-Path -LiteralPath $efiNoPrompt)) { | |
| Write-Error -Message "Source ISO does not contain efisys_noprompt.bin at expected path" -Category ObjectNotFound | |
| return | |
| } | |
| $fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage | |
| $fsi.VolumeName = $volume.FileSystemLabel | |
| $fsi.FileSystemsToCreate = 4 # FsiFileSystemUDF | |
| $fsi.UDFRevision = 0x102 # 1.02 | |
| $fsi.FreeMediaBlocks = 0 | |
| # This is the magic that overrides the ISO boot command that UEFI will run. | |
| $bootStream = New-Object -ComObject ADODB.Stream | |
| $bootStream.Type = 1 # adTypeBinary | |
| $bootStream.Open() | |
| $bootStream.LoadFromFile($efiNoPrompt) | |
| $bootOptions = New-Object -ComObject IMAPI2FS.BootOptions | |
| $bootOptions.AssignBootImage($bootStream) | |
| $fsi.BootImageOptions = $bootOptions | |
| Write-Verbose -Message "Adding original ISO contents to new ISO" | |
| $fsi.Root.AddTree($mountPath, $false) | |
| Write-Verbose -Message "Test" | |
| $image = $fsi.CreateResultImage() | |
| } finally { | |
| if ($bootStream) { $bootStream.Close() } | |
| $mount | Dismount-DiskImage | Out-Null | |
| } | |
| $resolvedDest = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) | |
| Write-Verbose -Message "Opening new FileStream to destination path at '$resolvedDest'" | |
| $outFS = [System.IO.FileStream]::new($resolvedDest, 'Create', 'Write') | |
| try { | |
| # It's a lot slower to do this in pure PowerShell so we use C# | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.IO; | |
| using System.Runtime.InteropServices; | |
| using System.Runtime.InteropServices.ComTypes; | |
| namespace Com | |
| { | |
| public class IStreamExtension | |
| { | |
| public static void CopyTo(object src, Stream dest, int blockSize) | |
| { | |
| IStream inputStream = src as IStream; | |
| byte[] buffer = new byte[blockSize]; | |
| IntPtr readBuffer = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int))); | |
| try | |
| { | |
| int read = 0; | |
| do | |
| { | |
| Marshal.WriteInt32(readBuffer, buffer.Length); | |
| inputStream.Read(buffer, buffer.Length, readBuffer); | |
| read = Marshal.ReadInt32(readBuffer); | |
| dest.Write(buffer, 0, read); | |
| } | |
| while (read == buffer.Length); | |
| } | |
| finally | |
| { | |
| Marshal.FreeCoTaskMem(readBuffer); | |
| } | |
| } | |
| } | |
| } | |
| '@ | |
| Write-Verbose -Message "Writing ISO stream to destination" | |
| [Com.IStreamExtension]::CopyTo($image.ImageStream, $outFS, $image.BlockSize) | |
| } | |
| finally { | |
| $outFS.Dispose() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment