I need to customize a Linux (Ubuntu) image by cloud-init on Hyper-V. So it determines the environment - PowerShell on Windows and there are no fancy features for cloud-init, so you need a custom ISO with the YAML files. As you can expect, I started googling it. You will find an excellent script created by Chris Wu link. You will find the original script at the end of the article for reference.
Issues
After I tried to execute it - I run immediately into this issue:
Add-Type:
Line |
36 | Add-Type -CompilerParameters $cp -TypeDefinition @'
| ~~~~~~~~~~~~~~~~~~~
| A parameter cannot be found that matches parameter name 'CompilerParameters'.
New-IsoFile: Adding 'arcsdk.dll' would result in a result image having a size larger than the current configured limit. Try a different media type.
I had to find that the solution is in PowerShell version - in the latest and the greatest PS you need to change the section, where are you telling the compiler, that you need an unsafe type (aka DLL interop call from .NET).
See:
...
Begin {
#### Comment / delete next line
#($cp = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = '/unsafe'
if (!('ISOFile' -as [type])) {
# Change from
# Add-Type -CompilerParameters $cp -TypeDefinition @'
# to
Add-Type -CompilerParameters "/unsafe" -TypeDefinition @'
public class ISOFile
...
ISO file format
The second issue is more uncanny. So now you can create an ISO file, with YAML files, you can mount it by double-clicking on the .iso file so everything seems to be OK. But cloud-init is not working. On the other hand, everybody is using oscdimg.exe from Windows ADK for making such an iso. You discover that -j2 means Joilet extension for ISO9660 (Original ISO specs from before millennia :-) So let’s try to specify format for ISO file in PowerShell. You will discover interop interface specifies the property FileSystemsToCreate where you need to use enum FsiFileSystems.
FsiFileSystems enum
| FsiFileSystemNone = 0
| FsiFileSystemISO9660 = 1
| FsiFileSystemJoliet = 2
| FsiFileSystemUDF = 4
The usage is a little bit weird - Each next is an extension of the previous so values are 1, 3, 7 aka ISO9660, ISO9660 + Joilet, ISO9660 + Joilet + UDF. The default value is 7, so we need to change it to 3, to emulate the switch for oscdimg.exe.
Almost at the end of the script you need to add:
...
End {
if ($Boot) { $Image.BootImageOptions=$Boot }
# Next Line is added
$Image.FileSystemsToCreate = 3 #Joilet
$Result = $Image.CreateResultImage()
[ISOFile]::Create($Target.FullName,$Result.ImageStream,$Result.BlockSize,$Result.TotalBlocks)
...
Definitely, it will be better to extend the script to have the fsi format as a parameter. I will do that next time. So that is it - we have Powershell function to create iso for cloud-init on Hyper-V.
Original script
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: New-IsoFile
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($item in $Source) {
if($item -isnot [System.IO.FileInfo] -and $item -isnot [System.IO.DirectoryInfo]) {
$item = Get-Item -LiteralPath $item
}
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 }
$Result = $Image.CreateResultImage()
[ISOFile]::Create($Target.FullName,$Result.ImageStream,$Result.BlockSize,$Result.TotalBlocks)
Write-Verbose -Message "Target image ($($Target.FullName)) has been created"
$Target
}
}