schtritoff / hyperv-vm-provisioning

Quick provisioning of Linux VM using Hyper-V on Windows
77 stars 29 forks source link

Switch to full PowerShell for ISO creation #5

Closed AvrumFeldman closed 1 week ago

AvrumFeldman commented 1 year ago

I'm sorry that I'm doing this as an issue instead of a PR I'm just too lazy for a full PR but I still want to give this (hopefully) helpful code.

The below code creates an ISO completely in PowerShell, without relying on external software dependencies. I found this a while back floating around on the internet, and crudely modified it to make it work for cloud-init, you might appreciate to use this instead of relying on oscdimg.

function New-IsoFile  {  
<#  
    .Synopsis  
    Creates a new .iso file  
    .Description  
    The New-IsoFile cmdlet creates a new .iso file containing content from chosen folders  
    .Example  
    New-IsoFile "c:\tools","c:Downloads\utils"  
    This command creates a .iso file in $env:temp folder (default location) that contains c:\tools and c:\downloads\utils folders. The folders themselves are included at the root of the .iso image.  
    .Example 
    New-IsoFile -FromClipboard -Verbose 
    Before running this command, select and copy (Ctrl-C) files/folders in Explorer first.  
    .Example  
    dir c:\WinPE | New-IsoFile -Path c:\temp\WinPE.iso -BootFile "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\efisys.bin" -Media DVDPLUSR -Title "WinPE" 
    This command creates a bootable .iso file containing the content from c:\WinPE folder, but the folder itself isn't included. Boot file etfsboot.com can be found in Windows ADK. Refer to IMAPI_MEDIA_PHYSICAL_TYPE enumeration for possible media types: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366217(v=vs.85).aspx  
    .Notes 
    NAME:  `
    AUTHOR: Chris Wu 
    LASTEDIT: 03/23/2016 14:46:50  
#>  

    [CmdletBinding(DefaultParameterSetName='Source')]Param( 
        [parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true, ParameterSetName='Source')]$Source,  
        [parameter(Position=2)][string]$Path = "$env:temp\$((Get-Date).ToString('yyyyMMdd-HHmmss.ffff')).iso",  
        [ValidateScript({Test-Path -LiteralPath $_ -PathType Leaf})][string]$BootFile = $null, 
        [ValidateSet('CDR','CDRW','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','BDR','BDRE')][string] $Media = 'DVDPLUSRW_DUALLAYER', 
        [string]$Title = (Get-Date).ToString("yyyyMMdd-HHmmss.ffff"),  
        [switch]$Force, 
        [parameter(ParameterSetName='Clipboard')][switch]$FromClipboard 
    )

    Begin {  
        ($cp = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = '/unsafe' 
        if (!('ISOFile' -as [type])) {  
        Add-Type -CompilerParameters $cp -TypeDefinition @' 
public class ISOFile  
{ 
public unsafe static void Create(string Path, object Stream, int BlockSize, int TotalBlocks)  
{  
    int bytes = 0;  
    byte[] buf = new byte[BlockSize];  
    var ptr = (System.IntPtr)(&bytes);  
    var o = System.IO.File.OpenWrite(Path);  
    var i = Stream as System.Runtime.InteropServices.ComTypes.IStream;  

    if (o != null) { 
    while (TotalBlocks-- > 0) {  
        i.Read(buf, BlockSize, ptr); o.Write(buf, 0, bytes);  
    }  
    o.Flush(); o.Close();  
    } 
} 
}  
'@  
        } 

        if ($BootFile) { 
        if('BDR','BDRE' -contains $Media) { Write-Warning "Bootable image doesn't seem to work with media type $Media" } 
        ($Stream = New-Object -ComObject ADODB.Stream -Property @{Type=1}).Open()  # adFileTypeBinary 
        $Stream.LoadFromFile((Get-Item -LiteralPath $BootFile).Fullname) 
        ($Boot = New-Object -ComObject IMAPI2FS.BootOptions).AssignBootImage($Stream) 
        } 

        $MediaType = @('UNKNOWN','CDROM','CDR','CDRW','DVDROM','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','HDDVDROM','HDDVDR','HDDVDRAM','BDROM','BDR','BDRE') 

        Write-Verbose -Message "Selected media type is $Media with value $($MediaType.IndexOf($Media))" 
        ($Image = New-Object -com IMAPI2FS.MsftFileSystemImage -Property @{VolumeName=$Title}).ChooseImageDefaultsForMediaType($MediaType.IndexOf($Media)) 

        if (!($Target = New-Item -Path $Path -ItemType File -Force:$Force -ErrorAction SilentlyContinue)) { Write-Error -Message "Cannot create file $Path. Use -Force parameter to overwrite if the target file already exists."; break } 
    }  

    Process { 
        if($FromClipboard) { 
            if($PSVersionTable.PSVersion.Major -lt 5) { Write-Error -Message 'The -FromClipboard parameter is only supported on PowerShell v5 or higher'; break } 
            $Source = Get-Clipboard -Format FileDropList 
        } 

        foreach($sitem in $Source) { 
            if($sitem -isnot [System.IO.FileInfo] -and $item -isnot [System.IO.DirectoryInfo]) { 
                $sitems = Get-childItem -LiteralPath $sitem 
            } 
            foreach ($item in $sitems) {
                if($item) { 
                    Write-Verbose -Message "Adding item to the target image: $($item.FullName)" 
                    try { $Image.Root.AddTree($item.FullName, $true) } catch { Write-Error -Message ($_.Exception.Message.Trim() + ' Try a different media type.') } 
                } 
            }

        } 
    } 

    End {  
        if ($Boot) { $Image.BootImageOptions=$Boot }
            $image.FileSystemsToCreate = 3
            $Image.ISO9660InterchangeLevel = 2
            $Image.GetDefaultFileSystemForImport(1)
            $Image.UseRestrictedCharacterSet = $true
            $Result = $Image.CreateResultImage()
            [ISOFile]::Create($Target.FullName,$Result.ImageStream,$Result.BlockSize,$Result.TotalBlocks)
            Write-Verbose -Message "Target image ($($Target.FullName)) has been created"
            $Target
    } 
}

I did some changes to common version floating around, namely I hardcoded in that the ISO should specifically use the ISO9660 filesystem and not UDF as per the documentation this is required. The actual changes are $image.FileSystemsToCreate = 3 (link) and $Image.GetDefaultFileSystemForImport(1) (link) I did some other minor changes but those most probably aren't really needed.

schtritoff commented 1 year ago

Why would we do that, is it better than using oscdimg?

BTW oscdimg is established official Microsoft tool so maybe still better choice since it is battle tested. We would still rely on other external binary dependancies (bsdtar, qemu-img) so this would not help us remove externals dependencies.

If you would like to merge this please make PR with following

AvrumFeldman commented 1 year ago

Hi @schtritoff

Your repo helped me out so I was just trying to reciprocate in kind.

I know oscdimg is an established and official tool to create images, I personally like to avoid shipping binaries with Powershell scripts since they add an external (avoidable) dependency, I figured you might also like to reduce an external dependency, I agree this doesn't remove all external dependencies, but every dependency less imho is better.

This above code relies on the build in windows imapi2 API which is a native windows API to create ISOs so it's not some complete homebrew code.

I specifically didn't create a PR since I don't have the time for this, but I still wanted to be helpful so I offered the code as is for you or anyone else to be able to make a PR.

Feel free to close this issue as is 😀

Thanks again for your amazing tool.

samstagern commented 1 week ago

It makes sense from a license perspective to switch. There is a simpler powershell solution based on the former script here: https://github.com/TheDotSource/New-ISOFile. I am testing it and will likely make a PR soon.

samstagern commented 1 week ago

Hi @schtritoff

Your repo helped me out so I was just trying to reciprocate in kind.

I know oscdimg is an established and official tool to create images, I personally like to avoid shipping binaries with Powershell scripts since they add an external (avoidable) dependency, I figured you might also like to reduce an external dependency, I agree this doesn't remove all external dependencies, but every dependency less imho is better.

This above code relies on the build in windows imapi2 API which is a native windows API to create ISOs so it's not some complete homebrew code.

I specifically didn't create a PR since I don't have the time for this, but I still wanted to be helpful so I offered the code as is for you or anyone else to be able to make a PR.

Feel free to close this issue as is 😀

Thanks again for your amazing tool.

Please try out the draft of the pull request

samstagern commented 1 week ago

Using isoinfo is a good way to check if the iso matches the specification of cloud-image:

This is the isoinfo from the Microsoft generated one:

10:36 $ isoinfo -d -i ubuntu-demo-metadata.iso CD-ROM is in ISO 9660 format System id: Volume id: CIDATA Volume set id: CIDATA Publisher id: MICROSOFT CORPORATION Data preparer id: MICROSOFT CORPORATION, ONE MICROSOFT WAY, REDMOND WA 98052, (425) 882-8080 Application id: OSCDIMG 2.56 (01/01/2005 TM) Copyright File id: Abstract File id: Bibliographic File id: Volume set size is: 1 Volume set sequence number is: 1 Logical block size is: 2048 Volume size is: 25 NO Joliet present NO Rock Ridge present

The above code does not create an image which matches this for now.

samstagern commented 1 week ago

I add an alternative iso creation with mkisofs.

schtritoff commented 1 week ago

thanks @AvrumFeldman for the hints, implemented in 01bae57

For reference - if for some reason we would need better support for ISO generation - for example to make it cross platform - DiscUtils library would be a good candidate https://www.nuget.org/packages/LTRData.DiscUtils.Containers/

samstagern commented 5 days ago

can confirm it works. Had a network issue on my side before. Thanks.