Cloud.ps1


function Get-CloudImage {
  param (
    [string] $ImageVersion = "24.04" # $ImageName ="noble"
  )

  $cloud_path = Get-BoxPath -Path "cloud"

  Switch ($ImageVersion) {
    "22.04" {
      $_ = "jammy"
      $ImageVersion = "22.04"
    }
    "jammy" {
      $ImageOS = "ubuntu"
      $ImageVersionName = "jammy"
      $ImageVersion = "22.04"
      $ImageRelease = "release" # default option is get latest but could be fixed to some specific version for example "release-20210413"
      $ImageBaseUrl = "http://cloud-images.ubuntu.com/releases" # alternative https://mirror.scaleuptech.com/ubuntu-cloud-images/releases
      $ImageUrlRoot = "$ImageBaseUrl/$ImageVersionName/$ImageRelease/" # latest
      $ImageFileName = "$ImageOS-$ImageVersion-server-cloudimg-amd64"
      $ImageFileExtension = "ova"
      # Manifest file is used for version check based on last modified HTTP header
      $ImageHashFileName = "SHA256SUMS"
      $ImageManifestSuffix = "manifest"
    }
    "24.04" {
      $_ = "noble"
      $ImageVersion = "24.04"
    }
    "noble" {
      $ImageOS = "ubuntu"
      $ImageVersionName = "noble"
      $ImageVersion = "24.04"
      $ImageRelease = "release" # default option is get latest but could be fixed to some specific version for example "release-20210413"
      $ImageBaseUrl = "http://cloud-images.ubuntu.com/releases" # alternative https://mirror.scaleuptech.com/ubuntu-cloud-images/releases
      $ImageUrlRoot = "$ImageBaseUrl/$ImageVersionName/$ImageRelease/" # latest
      $ImageFileName = "$ImageOS-$ImageVersion-server-cloudimg-amd64"
      $ImageFileExtension = "ova"
      # Manifest file is used for version check based on last modified HTTP header
      $ImageHashFileName = "SHA256SUMS"
      $ImageManifestSuffix = "manifest"
    }
    default { throw "Image version $ImageVersion not supported." }
  }


  $ImagePath = "$($ImageUrlRoot)$($ImageFileName)"
  $ImageHashPath = "$($ImageUrlRoot)$($ImageHashFileName)"

  # storage location for base images
  $ImageCachePath = Join-Path $cloud_path $("$ImageOS-$ImageVersion")

  if (!(test-path $ImageCachePath)) { mkdir -Path $ImageCachePath | out-null }

  # Get the timestamp of the target build on the cloud-images site
  $BaseImageStampFile = join-path $ImageCachePath "baseimagetimestamp.txt"
  [string]$stamp = ''
  if (test-path $BaseImageStampFile) {
    $stamp = (Get-Content -Path $BaseImageStampFile | Out-String).Trim()
    Write-Verbose "Timestamp from cache: $stamp"
  }
  if ($BaseImageCheckForUpdate -or ($stamp -eq '')) {
    $stamp = (Invoke-WebRequest -UseBasicParsing "$($ImagePath).$($ImageManifestSuffix)").BaseResponse.LastModified.ToUniversalTime().ToString("yyyyMMddHHmmss")
    Set-Content -path $BaseImageStampFile -value $stamp -force
    Write-Verbose "Timestamp from web (new): $stamp"
  }

  # check if local cached cloud image is the target one per $stamp
  if (!(test-path "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)")) {
    try {
      # If we do not have a matching image - delete the old ones and download the new one
      Write-Verbose "Did not find: $($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)"
      Write-Host 'Removing old images from cache...' -NoNewline
      Remove-Item "$($ImageCachePath)" -Exclude 'baseimagetimestamp.txt', "$($ImageOS)-$($stamp).*" -Recurse -Force
      Write-Host -ForegroundColor Green " Done."

      # get headers for content length
      Write-Host 'Check new image size ...' -NoNewline
      $response = Invoke-WebRequest "$($ImagePath).$($ImageFileExtension)" -UseBasicParsing -Method Head
      $downloadSize = [int]$response.Headers["Content-Length"]
      Write-Host -ForegroundColor Green " Done."

      Write-Host "Downloading new Cloud image $ImageVersion ($([int]($downloadSize / 1024 / 1024)) MB)..." -NoNewline
      Write-Verbose $(Get-Date)

      Start-BitsTransfer "$($ImagePath).$($ImageFileExtension)" -Destination "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension).tmp"

      #$ProgressPreference = "SilentlyContinue" #Disable progress indicator because it is causing Invoke-WebRequest to be very slow
      # download new image
      #Invoke-WebRequest "$($ImagePath).$($ImageFileExtension)" -OutFile "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension).tmp" -UseBasicParsing
      #$ProgressPreference = "Continue" #Restore progress indicator.
      # rename from .tmp to $($ImageFileExtension)
      Remove-Item "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)" -Force -ErrorAction 'SilentlyContinue'
      Rename-Item -path "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension).tmp" `
        -newname "$($ImageOS)-$($stamp).$($ImageFileExtension)"
      Write-Host -ForegroundColor Green " Done."

      # check file hash
      Write-Host "Checking file hash for downloaded image..." -NoNewline
      Write-Verbose $(Get-Date)
      $hashSums = [System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest $ImageHashPath -UseBasicParsing).Content)
      Switch -Wildcard ($ImageHashPath) {
        '*SHA256*' {
          $fileHash = Get-FileHash "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)" -Algorithm SHA256
        }
        '*SHA512*' {
          $fileHash = Get-FileHash "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)" -Algorithm SHA512
        }
        default { throw "$ImageHashPath not supported." }
      }
      if (($hashSums | Select-String -pattern $fileHash.Hash -SimpleMatch).Count -eq 0) { throw "File hash check failed" }
      Write-Verbose $(Get-Date)
      Write-Host -ForegroundColor Green " Done."

    }
    catch {
      cleanupFile "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)"
      $ErrorMessage = $_.Exception.Message
      Write-Host "Error: $ErrorMessage"
      exit 1
    }
  }
  
  $ova = "$($ImageCachePath)\$($ImageOS)-$($stamp).$($ImageFileExtension)"
  return $ova
}


function New-CloudInit {
  param(
    [Parameter(mandatory = $true)]
    [string] $Path,
    [Parameter(mandatory = $true)]
    [string] $Hostname,
    [Parameter(mandatory = $true)]
    [string] $Address
  )

  if (-not(Test-Path -Path $path -PathType Container)) {
    New-Item -Path $path -ItemType Directory | Out-Null
  }

  $seedIso = Join-Path $Path "seed.iso"
  $metadata = Join-Path $Path "meta-data"
  $userdata = Join-Path $Path "user-data"

 "local-hostname: ${Hostname})" | Out-File -FilePath $metadata

 @"
#cloud-config
users:
  - name: box
    groups: sudo, docker
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    plain_text_passwd: password
    lock_passwd: false
    shell: /bin/bash
    #ssh_pwauth: true
    ssh_authorized_keys:
      - $(Get-Content (Get-SshKey -Public))
   
# package_update: false
   
bootcmd:
   
  #
  - echo "blacklist floppy" > /etc/modprobe.d/blacklist-floppy.conf
  - rmmod floppy
  - update-initramfs -u
   
  # Ubuntu cloud ova comes with its own netplan config for enp0s3
  - printf "network:\n ethernets:\n enp0s8:\n addresses:\n - ${Address}\n" > /etc/netplan/60-host-only.yaml
  - netplan apply
   
  # Disable snapd
  - systemctl disable snapd
  - systemctl mask snapd
   
runcmd:
  - touch /etc/cloud/cloud-init.disabled
"@
 | Out-File -FilePath $userdata

  # https://blog.idera.com/database-tools/powershell/powertips/creating-iso-files/
  # https://thedotsource.com/2021/03/16/building-iso-files-with-powershell-7/
  # https://gitlab.com/xtec/box/-/tree/207febefe8500f17d847ef44aff9c3d5bc145b3d/cloud-init

  $image = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
  $image.VolumeName = "cidata"
  # create CDROM, Joliet and UDF file systems
  #$image.FileSystemsToCreate = 7
  

  $MediaType = @('UNKNOWN','CDROM','CDR','CDRW','DVDROM','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','HDDVDROM','HDDVDR','HDDVDRAM','BDROM','BDR','BDRE') 
  $image.ChooseImageDefaultsForMediaType($MediaType.IndexOf(1))
  $image.Root.AddTree($metadata, $true)
  $image.Root.AddTree($userdata, $true)

  if (!('ISOFile' -as [type])) {  
    ($compiler = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = '/unsafe' 
    Add-Type -CompilerParameters $compiler -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();
    }
  }
}
'@
  
  }

  

  $image = $image.CreateResultImage()
  [ISOFile]::Create($seedIso, $image.ImageStream, $image.BlockSize, $image.TotalBlocks)

  Remove-Item $metadata, $userdata

  return $seedIso
}