Skip to content

Instantly share code, notes, and snippets.

@hydrz
Last active November 19, 2022 06:02
Show Gist options
  • Save hydrz/0097affe6f5a93fd0fa6f06551c61204 to your computer and use it in GitHub Desktop.
Save hydrz/0097affe6f5a93fd0fa6f06551c61204 to your computer and use it in GitHub Desktop.
Create Linux Cloud Image on Hyper-v use Powershell
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[Alias('Path')]
[string]$WorkPath,
[ValidateScript({
$existingVm = Get-VM -Name $_ -ErrorAction SilentlyContinue
if (-not $existingVm) {
return $True
}
throw "There is already a VM named '$VMName' in this server."
})]
[Alias('Name')]
[string]$VMName,
[Alias('Password')]
[string]$RootPassword = 'root',
[Alias('Rsa')]
[string]$RootPublicKey,
[Alias('Switch')]
[string]$SwitchName,
[Alias('Cpu')]
[int]$ProcessorCount = 2,
[Alias('Memory')]
[int64]$MemoryStartupBytes = 4GB,
[Alias('Disk')]
[uint64]$DiskSizeBytes = 40GB,
# Extracted oscdimg From ADK https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install
[string]$OscdImgURL = 'https://github.com/hydrz/oscdimg/raw/main/oscdimg-amd64.zip',
# Download qemu-img from here: http://www.cloudbase.it/qemu-img-windows/
[string]$QemuImgURL = 'https://cloudbase.it/downloads/qemu-img-win-x64-2_3_0.zip',
[Alias('Image')]
[string]$DownloadImageURL = "https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/jammy/current/jammy-server-cloudimg-amd64.img",
# Custom cloud-init config file path
[string]$CloudInit
)
if (-not $WorkPath) {
$WorkPath = Read-Host -Prompt 'WorkPath (default: current directory)'
if (-not $WorkPath) {
$WorkPath = $PWD
}
}
if (-not $VMName) {
$VMName = Read-Host -Prompt 'VMName (required)'
if (-not $VMName) {
throw 'VMName is required.'
}
}
if (-not $RootPublicKey) {
if (Test-Path -Path "$($env:USERPROFILE)\.ssh\id_rsa.pub") {
$RootPublicKey = Get-Content -Path "$($env:USERPROFILE)\.ssh\id_rsa.pub"
}
}
if (-not $SwitchName) {
# Get all virtual switches with number
$switches = Get-VMSwitch | Select-Object -Property Name, SwitchType, NetAdapterInterfaceDescription
$switches | ForEach-Object {
$_ | Add-Member -MemberType NoteProperty -Name 'Number' -Value ($switches.IndexOf($_) + 1)
}
$switches | Format-Table -AutoSize
$SwitchName = Read-Host -Prompt 'SwitchName (default: 1)'
if (-not $SwitchName) {
$SwitchName = $switches[0].Name
}
else {
$SwitchName = $switches[$SwitchName - 1].Name
}
}
# Set working directories
$vmDir = "$WorkPath\vms"
$toolsDir = "$WorkPath\tools"
$cacheDir = "$WorkPath\cache"
# Check Paths
if (!(Test-Path $WorkPath)) { mkdir $WorkPath }
if (!(Test-Path $vmDir)) { mkdir $vmDir }
if (!(Test-Path $toolsDir)) { mkdir $toolsDir }
if (!(Test-Path $cacheDir)) { mkdir $cacheDir }
# Download oscdimg
$oscdImgPath = "$toolsDir\oscdimg\oscdimg.exe"
if (!(Test-Path $oscdImgPath)) {
Write-Host "Downloading oscdimg from $OscdImgURL"
$oscdImgZip = "$cacheDir\oscdimg.zip"
Invoke-WebRequest -Uri $OscdImgURL -OutFile $oscdImgZip
Expand-Archive -Path $oscdImgZip -DestinationPath $toolsDir\oscdimg
}
# Download qemu-img
$qemuImgPath = "$toolsDir\qemu\qemu-img.exe"
if (!(Test-Path $qemuImgPath)) {
Write-Host "Downloading qemu-img..."
$qemuImgZipPath = "$cacheDir\qemu.zip"
Invoke-WebRequest -Uri $QemuImgURL -OutFile $qemuImgZipPath
Expand-Archive -Path $qemuImgZipPath -DestinationPath $toolsDir\qemu
Remove-Item -Path $qemuImgZipPath
}
# Download the Ubuntu image
$fileName = $DownloadImageURL.Split("/")[-1].Split(".")[0]
$imagePath = "$cacheDir\$fileName.img"
if (!(Test-Path $cacheDir)) { mkdir $cacheDir }
if (!(Test-Path $imagePath)) {
Write-Host "Downloading image from $DownloadImageURL"
Invoke-WebRequest $DownloadImageURL -UseBasicParsing -OutFile $imagePath
}
# Delete the old VM
if ((Get-VM | Where-Object name -eq $VMName).Count -gt 0) { Stop-VM $VMName -TurnOff -Confirm:$false -Passthru | Remove-VM -Force }
$vmPath = "$($vmDir)\$($VMName)"
if (Test-Path $vmPath) { Remove-Item $vmPath -Recurse -Force }
mkdir $vmPath
# Create the seed ISO
$metaData = @"
instance-id: $($VMName)
local-hostname: $($VMName)
"@
$userData = @"
#cloud-config
{}
"@
# get the cloud-init config and replace unix line endings with windows line endings
if ($CloudInit) {
$userData = (Get-Content -Path $CloudInit -Raw) -replace "`r`n", "`r"
}
$vendorData = @"
#cloud-config
users:
- name: root
ssh_pwauth: False
ssh_authorized_keys:
- $($RootPublicKey)
chpasswd:
list: |
root:$($RootPassword)
expire: False
timezone: Asia/Shanghai
apt:
primary:
- arches: [default]
uri: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
"@
mkdir "$vmPath\seed"
Set-Content "$vmPath\seed\meta-data" ([byte[]][char[]] "$metaData") -Encoding Byte
Set-Content "$vmPath\seed\user-data" ([byte[]][char[]] "$userData") -Encoding Byte
Set-Content "$vmPath\seed\vendor-data" ([byte[]][char[]] "$vendorData") -Encoding Byte
$seedISO = "$vmPath\seed.iso"
& $oscdImgPath $vmPath\seed $seedISO -j2 -lcidata
Remove-Item "$vmPath\seed" -Recurse -Force
# Convert cloud image to VHDX
$vhdx = "$($vmPath)\$($fileName).vhdx"
& $qemuImgPath convert -f qcow2 $imagePath -O vhdx -o subformat=dynamic $vhdx
Resize-VHD -Path $vhdx -SizeBytes $DiskSizeBytes
# Create the VM
New-VM -Name $VMName -Generation 1 -MemoryStartupBytes $MemoryStartupBytes -SwitchName $SwitchName -VHDPath $vhdx
Set-VM -Name $VMName -ProcessorCount $ProcessorCount
Set-VMDvdDrive -VMName $VMName -Path $seedISO
Set-VM -Name $VMName -AutomaticStartAction StartIfRunning -AutomaticStopAction TurnOff
# Generate the MAC address
$vmMacAddress = "88:88:88:" + ((0..2 | ForEach-Object { (Get-Random -Minimum 1 -Maximum 255).ToString("X2") }) -join ':')
Set-VMNetworkAdapter -VMName $VMName -StaticMacAddress $vmMacAddress
Start-VM $VMName
@hydrz
Copy link
Author

hydrz commented Nov 17, 2022

Run from gist

iex "& { $(iwr https://gist.githubusercontent.com/hydrz/0097affe6f5a93fd0fa6f06551c61204/raw/vm-build.ps1) }"

Or download then run

./vm-build.ps1 -WorkPath e:/k8s -VMName k8s-master-1 -Cpu 2 -Memory 4GB -Disk 40GB -Switch External

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment