diff --git a/images.CI/macos/anka/Anka.Helpers.psm1 b/images.CI/macos/anka/Anka.Helpers.psm1 new file mode 100644 index 00000000..56fc54c0 --- /dev/null +++ b/images.CI/macos/anka/Anka.Helpers.psm1 @@ -0,0 +1,218 @@ +function Push-AnkaTemplateToRegistry { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $RegistryUrl, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TagVersion, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TemplateName + ) + + $command = "anka registry -a $RegistryUrl push -t $TagVersion $TemplateName" + Invoke-AnkaCommand -Command $command +} + +function Get-AnkaVM { + param( + [string] $VMName + ) + + $command = "anka --machine-readable list" + if (-not [string]::IsNullOrEmpty($VMName)) { + $command = "anka --machine-readable show $VMName" + } + Invoke-AnkaCommand -Command $command | ConvertFrom-Json | Foreach-Object body +} + +function Get-AnkaVMStatus { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName + ) + + $command = "anka --machine-readable list $VMName" + Invoke-AnkaCommand -Command $command | ConvertFrom-Json | Foreach-Object { $_.body.status } +} + +function Get-AnkaVMIPAddress { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName + ) + + Get-AnkaVM -VMName $VMName | Foreach-Object ip +} + +function Invoke-AnkaCommand { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Command + ) + + $result = bash -c "$Command 2>&1" | Out-String + if ($LASTEXITCODE -ne 0) { + Write-Error "There is an error during command execution:`n$result" + exit 1 + } + $result +} + +function New-AnkaVMTemplate { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $InstallerPath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TemplateName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TemplateUsername, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TemplatePassword, + + [Parameter(Mandatory)] + [int] $CPUCount, + + [Parameter(Mandatory)] + [int] $RamSizeGb, + + [Parameter(Mandatory)] + [int] $DiskSizeGb + ) + + $env:ANKA_DEFAULT_USER = $TemplateUsername + $env:ANKA_DEFAULT_PASSWD = $TemplatePassword + $env:ANKA_CREATE_SUSPEND = 0 + $command = "anka create --cpu-count '$CPUCount' --ram-size '${RamSizeGb}G' --disk-size '${DiskSizeGb}G' --app '$InstallerPath' $TemplateName" + Invoke-AnkaCommand -Command $command +} + +function Remove-AnkaVM { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName + ) + + $command = "anka delete $VMName --yes" + $isTemplateExists = Get-AnkaVM | Where-Object name -eq $VMName + if ($isTemplateExists) { + $null = Invoke-AnkaCommand -Command $command + } +} + +function Set-AnkaVMVideoController { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ShortMacOSVersion, + + [ValidateSet("fbuf", "pg")] + [string] $Controller = "pg" + ) + + $command = "anka modify $VMName set display -c $Controller" + # Apple Metal is available starting from Big Sur + if (-not $ShortMacOSVersion.StartsWith("10.")) { + $null = Invoke-AnkaCommand -Command $command + } +} + +function Set-AnkaVMDisplayResolution { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $DisplayResolution + ) + + $command = "anka modify $VMName set display -r $DisplayResolution" + $null = Invoke-AnkaCommand -Command $command +} + +function Start-AnkaVM { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName + ) + + $command = "anka start $VMName" + $vmStatus = Get-AnkaVMStatus -VMName $VMName + if ($vmStatus -eq "stopped") { + $null = Invoke-AnkaCommand -Command $command + } +} + +function Stop-AnkaVM { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName + ) + + $command = "anka stop $VMName" + $vmStatus = Get-AnkaVMStatus -VMName $VMName + if ($vmStatus -eq "running") { + $null = Invoke-AnkaCommand -Command $command + } +} + +function Wait-AnkaVMIPAddress { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName, + + [int] $RetryCount = 20, + [int] $Seconds = 60 + ) + + $condition = { Get-AnkaVMIPAddress -VMName $VMName } + $null = Invoke-WithRetry -BreakCondition $condition -RetryCount $RetryCount -Seconds $Seconds +} + +function Wait-AnkaVMSSHService { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName, + + [int] $RetryCount = 20, + [int] $Seconds = 60 + ) + + Start-Sleep -Seconds $Seconds + Write-Host "`t[*] Waiting for '$VMName' VM to get an IP address" + Wait-AnkaVMIPAddress -VMName $VMName -RetryCount $RetryCount -Seconds $Seconds + + $ipAddress = Get-AnkaVMIPAddress -VMName $VMName + Write-Host "`t[*] The '$ipAddress' IP address for '$VMName' VM" + + Write-Host "`t[*] Checking if SSH on a port is open" + $isSSHPortOpen = Test-SSHPort -IPAddress $ipAddress + if (-not $isSSHPortOpen) { + Write-Host "`t[x] SSH port is closed" + exit 1 + } +} diff --git a/images.CI/macos/anka/CreateCleanAnkaTemplate.ps1 b/images.CI/macos/anka/CreateCleanAnkaTemplate.ps1 new file mode 100644 index 00000000..2be1d965 --- /dev/null +++ b/images.CI/macos/anka/CreateCleanAnkaTemplate.ps1 @@ -0,0 +1,179 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [version] $MacOSVersion, + + [ValidateNotNullOrEmpty()] + [string] $TemplateUsername, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $TemplatePassword, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $RegistryUrl, + + [ValidateNotNullOrEmpty()] + [string] $TemplateName, + + [bool] $DownloadLatestVersion = $true, + [bool] $BetaSearch = $false, + [bool] $InstallSoftwareUpdate = $true, + [bool] $EnableAutoLogon = $true, + [int] $CPUCount = 6, + [int] $RamSizeGb = 7, + [int] $DiskSizeGb = 300, + [string] $DisplayResolution = "1920x1080" +) + +$ErrorActionPreference = "Stop" +$WarningPreference = "SilentlyContinue" + +# Import helper modules +Import-Module "$PSScriptRoot/Anka.Helpers.psm1" +Import-Module "$PSScriptRoot/Service.Helpers.psm1" + +# Helper functions +function Invoke-EnableAutoLogon { + if (-not $EnableAutoLogon) { + Write-Host "`t[*] Skip configuring AutoLogon" + return + } + + $ipAddress = Get-AnkaVMIPAddress -VMName $TemplateName + + Write-Host "`t[*] Enable AutoLogon" + Enable-AutoLogon -HostName $ipAddress -UserName $TemplateUsername -Password $TemplatePassword + + Write-Host "`t[*] Reboot '$TemplateName' VM to enable AutoLogon" + Restart-VMSSH -HostName $ipAddress | Show-StringWithFormat + + Wait-AnkaVMSSHService -VMName $TemplateName -Seconds 30 + + Write-Host "`t[*] Checking if AutoLogon is enabled" + Test-AutoLogon -VMName $TemplateName -UserName $TemplateUsername +} + +function Invoke-SoftwareUpdate { + if (-not $InstallSoftwareUpdate) { + Write-Host "`t[*] Skip installing software updates" + return + } + + $ipAddress = Get-AnkaVMIPAddress -VMName $TemplateName + + # Install Software Updates + # Security updates may not be able to install(hang, freeze) when AutoLogon is turned off + Write-Host "`t[*] Finding available software" + $newUpdates = Get-SoftwareUpdate -HostName $ipAddress + + if (-not $newUpdates) { + Write-Host "`t[*] No Updates Available" + return + } + + Write-Host "`t[*] Fetching Software Updates ready to install on '$TemplateName' VM:" + Show-StringWithFormat $newUpdates + Write-Host "`t[*] Installing Software Updates on 'test_vm' VM:" + Install-SoftwareUpdate -HostName $ipAddress | Show-StringWithFormat + + # Check if Action: restart + if ($newUpdates.Contains("Action: restart")) { + Write-Host "`t[*] Sleep 60 seconds before the software updates have been installed" + Start-Sleep -Seconds 60 + + Write-Host "`t[*] Waiting for loginwindow process" + Wait-LoginWindow -HostName $ipAddress | Show-StringWithFormat + + # Re-enable AutoLogon after installing a new security software update + Invoke-EnableAutoLogon + + # Check software updates have been installed + $updates = Get-SoftwareUpdate -HostName $ipAddress + if ($updates.Contains("Action: restart")) { + Write-Host "`t[x] Software updates failed to install: $updates" + Show-StringWithFormat $updates + exit 1 + } + } + + Write-Host "`t[*] Show the install history:" + $hUpdates = Get-SoftwareUpdateHistory -HostName $ipAddress + Show-StringWithFormat $hUpdates +} + +function Invoke-UpdateSettings { + $isConfRequired = $InstallSoftwareUpdate -or $EnableAutoLogon + if (-not $isConfRequired) { + Write-Host "`t[*] Skip additional configuration" + return + } + + Write-Host "`t[*] Starting '$TemplateName' VM" + Start-AnkaVM -VMName $TemplateName + + Write-Host "`t[*] Waiting for SSH service on '$TemplateName' VM" + Wait-AnkaVMSSHService -VMName $TemplateName -Seconds 30 + + # Configure AutoLogon + Invoke-EnableAutoLogon + + # Install software updates + Invoke-SoftwareUpdate + + Write-Host "`t[*] Stopping '$TemplateName' VM" + Stop-AnkaVM -VMName $TemplateName +} + +function Test-VMStopped { + $vmStatus = Get-AnkaVMStatus -VMName $TemplateName + if ($vmStatus -ne "stopped") { + Write-Host "`t[x] VM '$TemplateName' state is not stopped. The current state is '$vmStatus'" + exit 1 + } +} + +# Password is passed as env-var "SSHPASS" +$env:SSHUSER = $TemplateUsername +$env:SSHPASS = $TemplatePassword + +Write-Host "`n[#1] Download macOS application installer:" +$macOSInstaller = Get-MacOSInstaller -MacOSVersion $MacOSVersion -DownloadLatestVersion $DownloadLatestVersion -BetaSearch $BetaSearch +$shortMacOSVersion = Get-ShortMacOSVersion -MacOSVersion $MacOSVersion +if ([string]::IsNullOrEmpty($TemplateName)) { + $TemplateName = "clean_macos_${shortMacOSVersion}_${DiskSizeGb}gb" +} + +Write-Host "`n[#2] Create a VM template:" +Write-Host "`t[*] Deleting existed template with name '$TemplateName' before creating a new one" +Remove-AnkaVM -VMName $TemplateName + +Write-Host "`t[*] Creating Anka VM template with name '$TemplateName' and '$TemplateUsername' user" +Write-Host "`t[*] CPU Count: $CPUCount, RamSize: ${RamSizeGb}G, DiskSizeGb: ${DiskSizeGb}G, InstallerPath: $macOSInstaller, TemplateName: $TemplateName" +New-AnkaVMTemplate -InstallerPath $macOSInstaller ` + -TemplateName $TemplateName ` + -TemplateUsername $TemplateUsername ` + -TemplatePassword $TemplatePassword ` + -CPUCount $CPUCount ` + -RamSizeGb $RamSizeGb ` + -DiskSizeGb $DiskSizeGb | Show-StringWithFormat + +Write-Host "`n[#3] Configure AutoLogon and/or install software updates:" +Invoke-UpdateSettings + +Write-Host "`n[#4] Finalization '$TemplateName' configuration and push to the registry:" +Write-Host "`t[*] The '$TemplateName' VM status is stopped" +Test-VMStopped + +# Configure graphics settings +Write-Host "`t[*] Enabling Graphics Acceleration with Apple Metal for '$TemplateName' VM" +Set-AnkaVMVideoController -VMName $TemplateName -ShortMacOSVersion $ShortMacOSVersion + +Write-Host "`t[*] Setting screen resolution to $DisplayResolution for $TemplateName" +Set-AnkaVMDisplayResolution -VMName $TemplateName -DisplayResolution $DisplayResolution + +# Push a VM template (and tag) to the Cloud +Write-Host "`t[*] Pushing '$TemplateName' image with '$ShortMacOSVersion' tag to the '$RegistryUrl' registry..." +Push-AnkaTemplateToRegistry -RegistryUrl $registryUrl -TagVersion $shortMacOSVersion -TemplateName $TemplateName diff --git a/images.CI/macos/anka/Service.Helpers.psm1 b/images.CI/macos/anka/Service.Helpers.psm1 new file mode 100644 index 00000000..fce822d6 --- /dev/null +++ b/images.CI/macos/anka/Service.Helpers.psm1 @@ -0,0 +1,315 @@ +function Enable-AutoLogon { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $UserName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Password + ) + + $url = "https://raw.githubusercontent.com/actions/virtual-environments/main/images/macos/provision/bootstrap-provisioner/kcpassword.py" + $script = Invoke-RestMethod -Uri $url + $base64 = [Convert]::ToBase64String($script.ToCharArray()) + $kcpassword = "echo $base64 | base64 --decode > ~/kcpassword;sudo python ./kcpassword '${Password}';rm ./kcpassword" + $loginwindow = "sudo /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser '${UserName}';sudo /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUserScreenLocked -bool false" + $command = "${kcpassword};$loginwindow" + Invoke-SSHPassCommand -HostName $HostName -Command $command +} + +function Get-AvailableVersions { + param ( + [bool] $IsBeta = $false + ) + + if ($IsBeta) { + $searchPostfix = " beta" + } + + $command = { /usr/sbin/softwareupdate --list-full-installers | grep "macOS" } + $condition = { $LASTEXITCODE -eq 0 } + $softwareUpdates = Invoke-WithRetry -Command $command -BreakCondition $condition | Where-Object { $_.Contains("Title: macOS") -and $_ -match $searchPostfix } + $allVersions = $softwareUpdates -replace "(\* )?(Title|Version|Size):" | ConvertFrom-Csv -Header OSName, OSVersion | Select-Object OSName, OSVersion -Unique + + $allVersions +} + +function Get-MacOSInstaller { + param ( + [Parameter(Mandatory)] + [version] $MacOSVersion, + + [bool] $DownloadLatestVersion = $false, + [bool] $BetaSearch = $false + ) + + # Enroll machine to DeveloperSeed if we need beta and unenroll otherwise + $seedutil = "/System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/Resources/seedutil" + if ($BetaSearch) { + Write-Host "`t[*] Beta Version requested. Enrolling machine to DeveloperSeed" + sudo $seedutil enroll DeveloperSeed | Out-Null + } else { + Write-Host "`t[*] Reseting the seed before requesting stable versions" + sudo $seedutil unenroll | Out-Null + } + + # Validate there is no softwareupdate at the moment + Test-SoftwareUpdate + + # Validate availability OSVersion + Write-Host "`t[*] Finding available full installers" + $availableVersions = Get-AvailableVersions -IsBeta $BetaSearch + if ($DownloadLatestVersion) { + $shortMacOSVersion = Get-ShortMacOSVersion -MacOSVersion $MacOSVersion + $filterSearch = "${shortMacOSVersion}." + $filteredVersions = $availableVersions.Where{ $_.OSVersion.StartsWith($filterSearch) } + if (-not $filteredVersions) { + Write-Host "`t[x] Failed to find any macOS versions using '$filterSearch' search condition" + Show-StringWithFormat $availableVersions + exit 1 + } + Show-StringWithFormat $filteredVersions + $osVersions = $filteredVersions.OSVersion | Sort-Object {[version]$_} + $MacOSVersion = $osVersions | Select-Object -Last 1 + Write-Host "`t[*] The 'DownloadLatestVersion' flag is set. Latest macOS version is '$MacOSVersion' now" + } + + $macOSName = $availableVersions.Where{ $MacOSVersion -eq $_.OSVersion }.OSName + if (-not $macOSName) { + Write-Host "`t[x] Requested macOS '$MacOSVersion' version not found in the list of available installers. Available versions are:`n$($availableVersions.OSVersion)" + Write-Host "`t[x] Make sure to pass '-BetaSearch `$true' if you need a beta version installer" + exit 1 + } + + $installerPathPattern = "/Applications/Install*${macOSName}.app" + if (Test-Path $installerPathPattern) { + $previousInstallerPath = Get-Item -Path $installerPathPattern + Write-Host "`t[*] Removing '$previousInstallerPath' installation app before downloading the new one" + sudo rm -rf "$previousInstallerPath" + } + + # Download macOS installer + Write-Host "`t[*] Requested macOS '$MacOSVersion' version installer found, fetching it from Apple Software Update" + $result = Invoke-WithRetry { /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version $MacOSVersion } {$LASTEXITCODE -eq 0} | Out-String + if (-not $result.Contains("Install finished successfully")) { + Write-Host "`t[x] Failed to fetch $MacOSVersion macOS `n$result" + exit 1 + } + + $installerPath = (Get-Item -Path $installerPathPattern).FullName + Write-Host "`t[*] Installer successfully downloaded to '$installerPath'" + + $installerPath +} + +function Get-ShortMacOSVersion { + param ( + [Parameter(Mandatory)] + [version] $MacOSVersion + ) + + # Take Major.Minor version for macOS 10 (10.14 or 10.15) and Major for all further versions + $MacOSVersion.Major -eq 10 ? $MacOSVersion.ToString(2) : $MacOSVersion.ToString(1) +} + +function Get-SoftwareUpdate { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName + ) + + $command = "/usr/sbin/softwareupdate --list" + $result = Invoke-SSHPassCommand -HostName $HostName -Command $command + $result | Where-Object { $_ -match "(Label|Title):" } | Out-String +} + +function Get-SoftwareUpdateHistory { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName + ) + + $command = "/usr/sbin/softwareupdate --history" + Invoke-SSHPassCommand -HostName $HostName -Command $command | Out-String +} + +function Install-SoftwareUpdate { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName + ) + + $command = "sudo /usr/sbin/softwareupdate --all --install --restart --verbose" + Invoke-SSHPassCommand -HostName $HostName -Command $command +} + +function Invoke-SSHPassCommand { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Command, + + [int] $ConnectTimeout = 10, + [int] $ConnectionAttempts = 10, + [int] $ServerAliveInterval = 30 + ) + + $sshArg = @( + "sshpass" + "-e" + "ssh" + "-o UserKnownHostsFile=/dev/null" + "-o StrictHostKeyChecking=no" + "-o ConnectTimeout=$ConnectTimeout" + "-o ConnectionAttempts=$ConnectionAttempts" + "-o LogLevel=ERROR" + "-o ServerAliveInterval=$ServerAliveInterval" + "${env:SSHUSER}@${HostName}" + ) + $sshPassOptions = $sshArg -join " " + bash -c "$sshPassOptions \""$Command\"" 2>&1" +} + +function Invoke-WithRetry { + param( + [scriptblock] $Command, + [scriptblock] $BreakCondition, + [int] $RetryCount = 20, + [int] $Seconds = 60 + ) + + while ($RetryCount -gt 0) { + if ($Command) { + $result = & $Command + } + + if (& $BreakCondition) { + return $result + } + + $RetryCount-- + if ($RetryCount -eq 0) { + Write-Error "No more attempts left: $BreakCondition" + } + Write-Host "`t [/] Waiting $Seconds seconds before retrying. Retries left: $RetryCount" + Start-Sleep -Seconds $Seconds + } + + $result +} + +function Restart-VMSSH { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName + ) + + $command = "sudo reboot" + Invoke-SSHPassCommand -HostName $HostName -Command $command +} + +function Show-StringWithFormat { + param( + [Parameter(ValuefromPipeline)] + [object] $string + ) + + process { + ($string | Out-String).Trim().split("`n") | ForEach-Object { Write-Host "`t $_" } + } +} + +function Test-AutoLogon { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $VMName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $UserName + ) + + Invoke-WithRetry -BreakCondition { + # pwsh crashes if it invokes directly + # https://github.com/dotnet/runtime/issues/59059 + $ankaUser = "" | bash -c "anka run $VMName /usr/bin/id -nu" + $UserName -eq $ankaUser + } +} + +function Test-SoftwareUpdate { + param ( + [string] $UpdateProcessName = "softwareupdate" + ) + + $command = { + $updateProcess = (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue).id + if ($updateProcess) { + # Workaround to get commandline param as it doesn't work for macOS atm https://github.com/PowerShell/PowerShell/issues/13943 + $processName = /bin/ps -o command= $updateProcess + Write-Host "`t[*] Another software update process with '$updateProcess' id is in place with the following arguments '$processName'" + } + } + $condition = { + $null -eq (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue) + } + + Invoke-WithRetry -Command $command -BreakCondition $condition +} + +function Test-SSHPort { + param( + [Parameter(Mandatory)] + [ipaddress] $IPAddress, + + [int] $Port = 22, + [int] $Timeout = 2000 + ) + + Invoke-WithRetry -Command {$true} -BreakCondition { + try { + $client = [System.Net.Sockets.TcpClient]::new() + $client.ConnectAsync($IPAddress, $Port).Wait($Timeout) + } + catch { + $false + } + finally { + $client.Close() + } + } +} + +function Wait-LoginWindow { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $HostName, + + [int] $RetryCount = 60, + [int] $Seconds = 60 + ) + + $condition = { + $psCommand = "/bin/ps auxww" + $lw = "/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow" + $ctk = "/System/Library/Frameworks/CryptoTokenKit.framework/ctkahp.bundle/Contents/MacOS/ctkahp" + $proc = Invoke-SSHPassCommand -HostName $HostName -Command $psCommand | Out-String + $proc.Contains($lw) -and $proc.Contains($ctk) + } + Invoke-WithRetry -RetryCount $RetryCount -Seconds $Seconds -BreakCondition $condition +} diff --git a/images/macos/provision/configuration/CreateCleanAnkaTemplate.ps1 b/images/macos/provision/configuration/CreateCleanAnkaTemplate.ps1 deleted file mode 100644 index c4e6689c..00000000 --- a/images/macos/provision/configuration/CreateCleanAnkaTemplate.ps1 +++ /dev/null @@ -1,228 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [Version] $MacOSVersion, - - [ValidateNotNullOrEmpty()] - [String] $TemplateUsername, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [String] $TemplatePassword, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [String] $RegistryUrl, - - [Bool] $BetaSearch = $false, - [Int] $CpuCount = 6, - [Int] $RamSizeGb = 7, - [Int] $DiskSizeGb = 300, - [String] $DisplayResolution = "1920x1080" -) - -$ErrorActionPreference = "Stop" - -function Get-MacOSInstallers { - param ( - [version] $MacOSVersion, - [bool] $BetaSearch = $false - ) - - # Enroll machine to DeveloperSeed if we need beta and unenroll otherwise - $seedutil = "/System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/Resources/seedutil" - if ($BetaSearch) { - Write-Host "Beta Version requested. Enrolling machine to DeveloperSeed" - sudo $seedutil enroll DeveloperSeed | Out-Null - } else { - Write-Host "Reseting the seed before requesting stable versions" - sudo $seedutil unenroll | Out-Null - } - - # Validate there is no softwareupdate at the moment - Test-SoftwareUpdate - - # Validate availability OSVersion - $availableVersions = Get-AvailableVersions -IsBeta $BetaSearch - $macOSName = $availableVersions.Where{ $_.OSVersion -eq $MacOSVersion }.OSName - if (-not $macOSName) { - Write-Host "Requested macOS '$MacOSVersion' version not found in the list of available installers. Available versions are:`n$($availableVersions.OSVersion)" - Write-Host 'Make sure to pass "-BetaSearch $true" if you need a beta version installer' - exit 1 - } - - $installerPathPattern = "/Applications/Install*${macOSName}.app" - if (Test-Path $installerPathPattern) { - $previousInstallerPath = Get-Item -Path $installerPathPattern - Write-Host "Removing '$previousInstallerPath' installation app before downloading the new one" - sudo rm -rf "$previousInstallerPath" - } - - # Download macOS installer - Write-Host "Requested macOS '$MacOSVersion' version installer found, fetching it from Apple Software Update" - $result = Invoke-WithRetry { softwareupdate --fetch-full-installer --full-installer-version $MacOSVersion } {$LASTEXITCODE -eq 0} | Out-String - if (-not $result.Contains("Install finished successfully")) { - Write-Host "[Error]: failed to fetch $MacOSVersion macOS '$MacOSVersion' `n$result" - exit 1 - } - - $installerPath = Get-Item -Path $installerPathPattern - Write-Host "Installer successfully downloaded to '$installerPath'" - - return $installerPath.FullName -} - -function Invoke-WithRetry { - param( - [scriptblock] $Command, - [scriptblock] $BreakCondition, - [int] $RetryCount = 20, - [int] $Seconds = 60 - ) - - while ($RetryCount -gt 0) { - $result = & $Command - - if (& $BreakCondition) { - break - } - - $RetryCount-- - Write-Host "Waiting $Seconds seconds before retrying. Retries left: $RetryCount" - Start-Sleep -Seconds $Seconds - } - - $result -} - -function Get-AvailableVersions { - param ( - [Int] $RetryCount = 20, - [Int] $RetryInterval = 60, - [Bool] $IsBeta = $false - ) - - if ($IsBeta) { - $searchPostfix = " beta" - } - - $softwareUpdates = Invoke-WithRetry { softwareupdate --list-full-installers | Where-Object { $_.Contains("Title: macOS") -and $_ -match $searchPostfix } } { {$LASTEXITCODE -eq 0}} - $allVersions = $softwareUpdates -replace "(\* )?(Title|Version|Size):" | ConvertFrom-Csv -Header OsName, OsVersion - - return $allVersions -} - -function Test-SoftwareUpdate { - param ( - [String] $UpdateProcessName = "softwareupdate", - [Int] $RetryCount = 20, - [Int] $RetryInterval = 60 - ) - - $command = { - $updateProcess = (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue).id - if ($updateProcess) { - # Workaround to get commandline param as it doesn't work for macOS atm https://github.com/PowerShell/PowerShell/issues/13943 - $processName = ps -o command= $updateProcess - Write-Host "Another software update process is in place with the following arguments '$processName', wait $RetryInterval seconds, $RetryCount attempts left" - } - } - $condition = { - $null -eq (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue) - } - - Invoke-WithRetry -Command $command -BreakCondition $condition -} - -function New-AnkaVMTemplate { - param ( - [String] $InstallerPath, - [String] $ShortMacOSVersion, - [String] $TemplateName, - [String] $TemplateUsername, - [String] $TemplatePassword, - [Int] $CpuCount, - [Int] $RamSizeGb, - [Int] $DiskSizeGb, - [String] $DisplayResolution - ) - - $isTemplateExists = (Invoke-Anka { anka --machine-readable list } | ConvertFrom-Json).body.name -eq $templateName - if ($isTemplateExists) { - Write-Host "Deleting existed template with name '$templateName' before creating a new one" - Invoke-Anka { anka delete $templateName --yes } - } - - Write-Host "Creating Anka VM template with name '$TemplateName' and user $TemplateUsername" - $env:ANKA_DEFAULT_USER = $TemplateUsername - $env:ANKA_DEFAULT_PASSWD = $TemplatePassword - $env:ANKA_CREATE_SUSPEND = 0 - Write-Host "Cpu Count: $CpuCount, RamSize: ${RamSizeGb}G, DiskSizeGb: ${DiskSizeGb}G, InstallerPath: $InstallerPath, TemplateName: $templateName" - Invoke-Anka { anka create --cpu-count $CpuCount --ram-size "${RamSizeGb}G" --disk-size "${DiskSizeGb}G" --app $InstallerPath $templateName } - - # Apple Metal is available starting from Big Sur - if (-not $ShortMacOSVersion.StartsWith("10.")) { - Write-Host "Enabling Graphics Acceleration with Apple Metal for $templateName" - Invoke-Anka { anka modify $templateName set display -c pg } - } - - Write-Host "Setting screen resolution to $DisplayResolution for $templateName" - Invoke-Anka { anka modify $templateName set display -r $DisplayResolution } - - return $templateName -} - -function Add-AnkaImageToRegistry { - param ( - [String] $RegistryUrl, - [String] $ShortMacOSVersion, - [String] $TemplateName - ) - - Write-Host "Pushing image to the registry..." - Invoke-Anka { anka registry -a $RegistryUrl push -t $ShortMacOSVersion $TemplateName } -} - -function Invoke-Anka { - param ( - [scriptblock] $Cmd - ) - - & $Cmd - if ($LASTEXITCODE -ne 0) { - Write-Error "There is an error during command execution" - exit 1 - } -} - -function Get-ShortMacOSVersion { - param ( - [Version] $MacOSVersion - ) - - # Take Major.Minor version for macOS 10 (10.14 or 10.15) and Major for all further versions - if ($MacOSVersion.Major -eq 10) { - $shortMacOSVersion = $MacOSVersion.ToString(2) - } - else { - $shortMacOSVersion = $MacOSVersion.Major - } - - return $shortMacOSVersion -} - -$macOSInstaller = Get-MacOSInstallers -MacOSVersion $MacOSVersion -BetaSearch $BetaSearch -$shortMacOSVersion = Get-ShortMacOSVersion -MacOSVersion $MacOSVersion -$templateName = "clean_macos_${shortMacOSVersion}_${DiskSizeGb}gb" -New-AnkaVMTemplate -InstallerPath $macOSInstaller ` - -ShortMacOSVersion $shortMacOSVersion ` - -TemplateName $templateName ` - -TemplateUsername $TemplateUsername ` - -TemplatePassword $TemplatePassword ` - -CpuCount $CpuCount ` - -RamSizeGb $RamSizeGb ` - -DiskSizeGb $DiskSizeGb ` - -DisplayResolution $DisplayResolution - -Add-AnkaImageToRegistry -RegistryUrl $registryUrl -ShortMacOSVersion $shortMacOSVersion -TemplateName $templateName \ No newline at end of file