Howto: Prep an Image for Scale HCOS - Driver Add

You can add Virtio drivers to your image before you deploy it, and save yourself some time. Here’s the script!

# Function to get an available drive letter
function Get-AvailableDriveLetter {
    $used = (Get-Volume).DriveLetter | Where-Object { $_ }
    for ($letter = [char]'D'; $letter -le [char]'Z'; $letter = [char]([byte]$letter + 1)) {
        if ($used -notcontains $letter) {
            return $letter
        }
    }
    throw "No available drive letters found."
}

# Function to ensure the WimMount driver is installed
function Ensure-WimMountDriver {
    $servicePath = 'HKLM:\SYSTEM\CurrentControlSet\Services\WIMMount'
    if (-not (Test-Path $servicePath)) {
        Write-Host "WIMMount service is missing. Attempting to install Windows ADK Deployment Tools..." -ForegroundColor Yellow

        $adkUrl = "https://go.microsoft.com/fwlink/?linkid=2266288"
        $adkSetupPath = "$env:TEMP\adksetup.exe"
        
        try {
            Invoke-WebRequest -Uri $adkUrl -OutFile $adkSetupPath -ErrorAction Stop
            Start-Process -FilePath $adkSetupPath -ArgumentList "/quiet /features OptionIds.DeploymentTools" -Wait -NoNewWindow
            
            if (Test-Path $servicePath) {
                Write-Host "WIMMount service successfully installed." -ForegroundColor Green
            } else {
                throw "Failed to install WIMMount service."
            }
        } catch {
            throw "Failed to download or install Windows ADK: $_"
        } finally {
            if (Test-Path $adkSetupPath) {
                Remove-Item $adkSetupPath -Force -ErrorAction SilentlyContinue
            }
        }
    } else {
        Write-Host "WIMMount service is already installed." -ForegroundColor Green
    }

    # Clean up stale mounted images registry entries
    $mountedImagesPath = 'HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images'
    if (Test-Path $mountedImagesPath) {
        Remove-Item -Path $mountedImagesPath -Recurse -Force -ErrorAction SilentlyContinue
        Write-Host "Cleared stale mounted images registry entries." -ForegroundColor Green
    }
}

# Function to verify and mount the image from $SourceDisk
function Mount-ImageFromSource {
    param (
        [Parameter(Mandatory=$true)]
        [string]$SourceDisk,
        [string]$MountPath = "$env:TEMP\WimMount"
    )
    
    if (-not (Test-Path $SourceDisk)) {
        throw "Source file $SourceDisk does not exist."
    }
    
    $extension = [System.IO.Path]::GetExtension($SourceDisk).ToLower()
    $supportedExtensions = @('.wim', '.vhd', '.vhdx')
    if ($extension -notin $supportedExtensions) {
        throw "Unsupported file type: $extension. Supported types are .wim, .vhd, .vhdx."
    }
    
    if ($extension -eq '.wim') {
        Ensure-WimMountDriver
        
        if (-not (Test-Path $MountPath)) {
            New-Item -Path $MountPath -ItemType Directory -Force | Out-Null
        }
        
        try {
            Write-Host "Mounting WIM image..." -ForegroundColor Yellow
            Mount-WindowsImage -ImagePath $SourceDisk -Index 1 -Path $MountPath -ErrorAction Stop
            Write-Host "Successfully mounted WIM $SourceDisk to $MountPath." -ForegroundColor Green
            
            return @{
                Type = 'wim'
                Path = $MountPath
                TempDisk = $null
                Original = $SourceDisk
                DiskNumber = $null
            }
        } catch {
            throw "Failed to mount WIM image: $_"
        }
        
    } elseif ($extension -in @('.vhd', '.vhdx')) {
        $tempDisk = "$env:TEMP\temp_image_$(Get-Date -Format 'yyyyMMdd_HHmmss')$extension"
        
        try {
            Write-Host "Creating temporary copy for VHD/VHDX modifications..." -ForegroundColor Yellow
            Copy-Item -Path $SourceDisk -Destination $tempDisk -ErrorAction Stop
            Write-Host "Created temporary copy at $tempDisk." -ForegroundColor Green
            
            Write-Host "Mounting VHD/VHDX..." -ForegroundColor Yellow
            $mounted = Mount-DiskImage -ImagePath $tempDisk -PassThru -ErrorAction Stop
            $diskNumber = $mounted.Number
            
            Start-Sleep -Seconds 3  # Allow time for disk to be recognized
            
            $partitions = Get-Partition -DiskNumber $diskNumber
            $assignedLetters = @()
            
            foreach ($part in $partitions) {
                if (-not $part.DriveLetter -and $part.Type -ne 'Reserved' -and $part.Size -gt 100MB) {
                    $letter = Get-AvailableDriveLetter
                    Set-Partition -DiskNumber $diskNumber -PartitionNumber $part.PartitionNumber -NewDriveLetter $letter
                    $assignedLetters += $letter
                    Write-Host "Assigned drive letter $letter to partition $($part.PartitionNumber)" -ForegroundColor Cyan
                } elseif ($part.DriveLetter) {
                    $assignedLetters += $part.DriveLetter
                }
            }
            
            # Find Windows installation
            $imagePath = $null
            foreach ($letter in $assignedLetters) {
                $potentialPath = "$letter`:"
                if (Test-Path "$potentialPath\Windows\System32\config\SYSTEM") {
                    $imagePath = $potentialPath
                    break
                }
            }
            
            if (-not $imagePath) {
                throw "No Windows installation found in the mounted VHD/VHDX partitions."
            }
            
            Write-Host "Successfully mounted VHD/VHDX to $imagePath." -ForegroundColor Green
            
            return @{
                Type = 'vhd'
                Path = $imagePath
                TempDisk = $tempDisk
                Original = $SourceDisk
                DiskNumber = $diskNumber
            }
        } catch {
            if (Test-Path $tempDisk) {
                Dismount-DiskImage -ImagePath $tempDisk -ErrorAction SilentlyContinue
                Remove-Item $tempDisk -Force -ErrorAction SilentlyContinue
            }
            throw "Failed to mount VHD/VHDX: $_"
        }
    }
}

# Function to check for virtio-win.iso
function Get-VirtioISO {
    param (
        [string]$ScriptRoot = '',
        [string]$LocalIsoPath = ''
    )
    
    # Handle empty or null ScriptRoot
    if ([string]::IsNullOrWhiteSpace($ScriptRoot)) {
        $ScriptRoot = if ($PSScriptRoot) { 
            $PSScriptRoot 
        } else { 
            $env:TEMP
        }
    }
    
    Write-Host "Using ScriptRoot: $ScriptRoot" -ForegroundColor Green
    
    $isoName = "virtio-win.iso"
    $defaultIsoPath = Join-Path $ScriptRoot $isoName
    
    # If a local ISO path is specified, use it
    if (-not [string]::IsNullOrWhiteSpace($LocalIsoPath)) {
        Write-Host "Local ISO path specified: $LocalIsoPath" -ForegroundColor Yellow
        if (-not (Test-Path $LocalIsoPath)) {
            throw "Provided local ISO path $LocalIsoPath does not exist."
        }
        if ([System.IO.Path]::GetExtension($LocalIsoPath) -ne '.iso') {
            throw "Provided local path $LocalIsoPath is not an ISO file."
        }
        try {
            Copy-Item -Path $LocalIsoPath -Destination $defaultIsoPath -Force -ErrorAction Stop
            Write-Host "Copied provided local ISO to $defaultIsoPath." -ForegroundColor Green
            return $defaultIsoPath
        } catch {
            throw "Failed to copy local ISO: $_"
        }
    }
    
    # No local ISO specified - check if default exists, otherwise download
    if (Test-Path $defaultIsoPath) {
        Write-Host "$isoName already exists at $defaultIsoPath." -ForegroundColor Green
        return $defaultIsoPath
    } else {
        Write-Host "$isoName not found at $defaultIsoPath. Downloading..." -ForegroundColor Yellow
        try {
            $url = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.271-1/virtio-win.iso"
            
            Write-Host "Downloading from: $url" -ForegroundColor Cyan
            Write-Host "Saving to: $defaultIsoPath" -ForegroundColor Cyan
            
            Invoke-WebRequest -Uri $url -OutFile $defaultIsoPath -ErrorAction Stop
            
            # Verify the file was created and has content
            if (Test-Path $defaultIsoPath) {
                $fileSize = (Get-Item $defaultIsoPath).Length
                Write-Host "Download complete. File size: $($fileSize / 1MB) MB" -ForegroundColor Green
                return $defaultIsoPath
            } else {
                throw "Download appeared to succeed but file was not created at $defaultIsoPath"
            }
        } catch {
            throw "Failed to download virtio-win.iso: $_"
        }
    }
}

# Function to mount the ISO
function Mount-VirtioISO {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$IsoPath
    )
    
    Write-Host "Attempting to mount ISO: $IsoPath" -ForegroundColor Cyan
    
    # Validate the ISO path exists and is a file
    if (-not (Test-Path $IsoPath)) {
        throw "ISO file does not exist at path: $IsoPath"
    }
    
    $item = Get-Item $IsoPath
    if ($item.PSIsContainer) {
        throw "Path is a directory, not a file: $IsoPath"
    }
    
    if ($item.Extension -ne '.iso') {
        throw "File is not an ISO: $IsoPath"
    }
    
    try {
        Write-Host "Mounting ISO file..." -ForegroundColor Yellow
        $mountedIso = Mount-DiskImage -ImagePath $IsoPath -PassThru -ErrorAction Stop
        Start-Sleep -Seconds 3  # Allow more time for mount
        
        $volume = $mountedIso | Get-Volume
        if (-not $volume -or -not $volume.DriveLetter) {
            throw "Failed to get drive letter for mounted ISO"
        }
        
        $driveLetter = $volume.DriveLetter + ":"
        Write-Host "ISO successfully mounted at $driveLetter." -ForegroundColor Green
        return $driveLetter
    } catch {
        throw "Failed to mount ISO: $_"
    }
}

# Function to add drivers to the image - handles both WIM and VHD/VHDX
function Add-DriversToImage {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$MountPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$DriverSource,
        [bool]$ForceUnsigned = $false,
        [Parameter(Mandatory=$true)]
        [ValidateSet('wim', 'vhd')]
        [string]$ImageType
    )
    
    Write-Host "Adding drivers from $DriverSource to $ImageType image at $MountPath..." -ForegroundColor Yellow
    Write-Host "Force unsigned drivers: $ForceUnsigned" -ForegroundColor Cyan
    
    try {
        if ($ImageType -eq 'wim') {
            # For WIM images, use Add-WindowsDriver PowerShell cmdlet
            Write-Host "Using Add-WindowsDriver for WIM image..." -ForegroundColor Cyan
            Add-WindowsDriver -Path $MountPath -Driver $DriverSource -Recurse -ForceUnsigned:$ForceUnsigned -ErrorAction Stop
            Write-Host "All drivers successfully added to WIM image." -ForegroundColor Green
            
        } elseif ($ImageType -eq 'vhd') {
            # For VHD/VHDX, use DISM.exe directly with /Image parameter
            Write-Host "Using DISM.exe for VHD/VHDX image..." -ForegroundColor Cyan
            
            # Verify Windows directory exists
            $windowsPath = Join-Path $MountPath "Windows"
            if (-not (Test-Path $windowsPath)) {
                throw "Windows directory not found at $windowsPath. This may not be a valid Windows installation."
            }
            
            # Build DISM arguments
            $dismArgs = @(
                "/Image:$MountPath",
                "/Add-Driver",
                "/Driver:$DriverSource",
                "/Recurse"
            )
            
            if ($ForceUnsigned) {
                $dismArgs += "/ForceUnsigned"
            }
            
            Write-Host "Running: dism.exe $($dismArgs -join ' ')" -ForegroundColor Cyan
            
            # Create temp files for output capture
            $outputFile = "$env:TEMP\dism_output_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
            $errorFile = "$env:TEMP\dism_error_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
            
            try {
                $process = Start-Process -FilePath "dism.exe" -ArgumentList $dismArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput $outputFile -RedirectStandardError $errorFile
                
                # Read and display output
                if (Test-Path $outputFile) {
                    $output = Get-Content $outputFile -Raw
                    if ($output) {
                        Write-Host "DISM Output:" -ForegroundColor Cyan
                        Write-Host $output -ForegroundColor White
                        
                        # Parse output for success/failure counts
                        if ($output -match "Installing (\d+) of (\d+)") {
                            $totalDrivers = $matches[2]
                            Write-Host "Total drivers processed: $totalDrivers" -ForegroundColor Cyan
                        }
                    }
                }
                
                # Handle different exit codes
                switch ($process.ExitCode) {
                    0 { 
                        Write-Host "✅ DISM driver injection completed successfully - all drivers installed." -ForegroundColor Green 
                    }
                    50 { 
                        Write-Host "⚠️  DISM completed with partial success - some unsigned drivers were skipped." -ForegroundColor Yellow
                        Write-Host "   This is normal when ForceUnsigned=false. Critical drivers were likely installed." -ForegroundColor Yellow
                        if (-not $ForceUnsigned) {
                            Write-Host "   💡 Tip: Use -ForceUnsigned `$true to install unsigned drivers if needed." -ForegroundColor Cyan
                        }
                    }
                    default {
                        $errorContent = ""
                        if (Test-Path $errorFile) {
                            $errorContent = Get-Content $errorFile -Raw
                        }
                        throw "DISM failed with exit code: $($process.ExitCode). Error: $errorContent"
                    }
                }
            } finally {
                # Clean up temp files
                Remove-Item $outputFile -ErrorAction SilentlyContinue
                Remove-Item $errorFile -ErrorAction SilentlyContinue
            }
        }
    } catch {
        throw "Failed to add drivers: $_"
    }
}


# Function to handle cleanup and commit
function Complete-ImageProcessing {
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$MountInfo,
        [string]$IsoPath,
        [bool]$Commit = $null  # $null = prompt user, $true = auto-commit, $false = auto-discard
    )
    
    $transcriptFile = "DISM_Driver_Add_Transcript_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
    Start-Transcript -Path $transcriptFile
    
    Write-Host "`n=== Driver Addition Process Completed ===" -ForegroundColor Green
    Write-Host "Image Type: $($MountInfo.Type)" -ForegroundColor Cyan
    Write-Host "Mount Path: $($MountInfo.Path)" -ForegroundColor Cyan
    
    # Dismount ISO first
    if ($IsoPath -and (Get-DiskImage -ImagePath $IsoPath -ErrorAction SilentlyContinue).Attached) {
        Dismount-DiskImage -ImagePath $IsoPath -ErrorAction SilentlyContinue
        Write-Host "ISO dismounted." -ForegroundColor Green
    }
    
    # Determine commit action
    $commitAction = $false
    if ($Commit -eq $null) {
        # Prompt user for decision
        do {
            Write-Host "`nWould you like to commit the changes? (Y/N): " -ForegroundColor Yellow -NoNewline
            $response = Read-Host
        } while ($response -notin @('Y', 'y', 'N', 'n'))
        
        $commitAction = ($response -eq 'Y' -or $response -eq 'y')
    } else {
        # Use the provided parameter value
        $commitAction = $Commit
        if ($commitAction) {
            Write-Host "`nAuto-committing changes (Commit parameter = True)..." -ForegroundColor Green
        } else {
            Write-Host "`nAuto-discarding changes (Commit parameter = False)..." -ForegroundColor Yellow
        }
    }
    
    try {
        if ($MountInfo.Type -eq 'wim') {
            if ($commitAction) {
                Write-Host "Committing changes to WIM image..." -ForegroundColor Yellow
                Dismount-WindowsImage -Path $MountInfo.Path -Save
                Write-Host "✅ Changes committed and WIM image unmounted." -ForegroundColor Green
            } else {
                Write-Host "Discarding changes to WIM image..." -ForegroundColor Yellow
                Dismount-WindowsImage -Path $MountInfo.Path -Discard
                Write-Host "❌ Changes discarded and WIM image unmounted." -ForegroundColor Yellow
            }
        } elseif ($MountInfo.Type -eq 'vhd') {
            Write-Host "Dismounting VHD/VHDX..." -ForegroundColor Yellow
            Dismount-DiskImage -ImagePath $MountInfo.TempDisk
            
            if ($commitAction) {
                Write-Host "Committing changes by replacing original image..." -ForegroundColor Yellow
                Copy-Item -Path $MountInfo.TempDisk -Destination $MountInfo.Original -Force
                Write-Host "✅ Changes committed by replacing original with modified image." -ForegroundColor Green
            } else {
                Write-Host "❌ Changes discarded." -ForegroundColor Yellow
            }
            
            # Clean up temp file
            Remove-Item $MountInfo.TempDisk -Force
            Write-Host "Temporary file cleaned up." -ForegroundColor Green
        }
    } catch {
        Write-Error "Error during cleanup: $_"
    } finally {
        Stop-Transcript
        Write-Host "Transcript saved to $transcriptFile." -ForegroundColor Cyan
    }
}


# Main orchestration function
function Start-DismDriverAddition {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceDisk,
        [bool]$ForceUnsigned = $false,
        [string]$LocalIsoPath = '',
        [bool]$Commit = $null  # $null = prompt, $true = auto-commit, $false = auto-discard
    )
    
    $mountInfo = $null
    $isoPath = $null
    $driverSource = $null
    
    try {
        Write-Host "=== Starting DISM Driver Addition Process ===" -ForegroundColor Green
        Write-Host "Source disk: $SourceDisk" -ForegroundColor Cyan
        Write-Host "Force unsigned: $ForceUnsigned" -ForegroundColor Cyan
        Write-Host "Local ISO path: '$LocalIsoPath'" -ForegroundColor Cyan
        Write-Host "Commit mode: $(if ($Commit -eq $null) { 'Prompt User' } elseif ($Commit) { 'Auto-Commit' } else { 'Auto-Discard' })" -ForegroundColor Cyan
        
        # Mount the source image
        Write-Host "`n--- Mounting Source Image ---" -ForegroundColor Magenta
        $mountInfo = Mount-ImageFromSource -SourceDisk $SourceDisk
        
        # Get the virtio ISO
        Write-Host "`n--- Getting VirtIO ISO ---" -ForegroundColor Magenta
        $isoPath = Get-VirtioISO -LocalIsoPath $LocalIsoPath
        
        # Validate isoPath is a string
        if (-not $isoPath -or $isoPath -isnot [string]) {
            throw "Get-VirtioISO did not return a valid string path. Returned: $($isoPath.GetType().Name)"
        }
        
        # Mount the ISO
        Write-Host "`n--- Mounting VirtIO ISO ---" -ForegroundColor Magenta
        $driverSource = Mount-VirtioISO -IsoPath $isoPath
        
        # Add drivers
        Write-Host "`n--- Adding Drivers ---" -ForegroundColor Magenta
        Add-DriversToImage -MountPath $mountInfo.Path -DriverSource $driverSource -ForceUnsigned $ForceUnsigned -ImageType $mountInfo.Type
        
        # Complete processing with commit parameter
        Write-Host "`n--- Completing Process ---" -ForegroundColor Magenta
        Complete-ImageProcessing -MountInfo $mountInfo -IsoPath $isoPath -Commit $Commit
        
    } catch {
        Write-Error "Process failed: $_"
        Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Error at line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
        
        # Emergency cleanup (same as before)
        if ($driverSource -and $isoPath) {
            try {
                if ((Get-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue).Attached) {
                    Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue
                    Write-Host "Emergency: ISO dismounted" -ForegroundColor Yellow
                }
            } catch {
                Write-Host "Emergency cleanup warning: Could not dismount ISO" -ForegroundColor Red
            }
        }
        
        if ($mountInfo) {
            try {
                if ($mountInfo.Type -eq 'wim' -and (Test-Path $mountInfo.Path)) {
                    Dismount-WindowsImage -Path $mountInfo.Path -Discard -ErrorAction SilentlyContinue
                    Write-Host "Emergency: WIM dismounted" -ForegroundColor Yellow
                } elseif ($mountInfo.Type -eq 'vhd' -and $mountInfo.TempDisk) {
                    Dismount-DiskImage -ImagePath $mountInfo.TempDisk -ErrorAction SilentlyContinue
                    Remove-Item $mountInfo.TempDisk -Force -ErrorAction SilentlyContinue
                    Write-Host "Emergency: VHD dismounted and temp file removed" -ForegroundColor Yellow
                }
            } catch {
                Write-Host "Emergency cleanup warning: Could not clean up mounted image" -ForegroundColor Red
            }
        }
        
        throw
    }
}


# Usage examples and parameter setup
$SourceDisk = "Z:\Image.vhd"
$ForceUnsigned = $false
$LocalIsoPath = ""
$Commit = $true  # Add this line for auto-commit

# Run the main function
Start-DismDriverAddition -SourceDisk $SourceDisk -ForceUnsigned $ForceUnsigned -LocalIsoPath $LocalIsoPath -Commit $Commit