2021-10-15 14:32:29 +03:00
function Enable-AutoLogon {
param (
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ string ] $HostName ,
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ string ] $UserName ,
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ string ] $Password
)
2023-11-15 12:12:28 +01:00
$url = "https://raw.githubusercontent.com/actions/runner-images/main/images/macos/assets/bootstrap-provisioner/setAutoLogin.sh"
2021-10-15 14:32:29 +03:00
$script = Invoke-RestMethod -Uri $url
$base64 = [ Convert ]:: ToBase64String ( $script . ToCharArray ())
2022-03-01 15:13:02 +03:00
$command = "echo $base64 | base64 --decode > ./setAutoLogin.sh;sudo bash ./setAutoLogin.sh ' ${UserName} ' ' ${Password} ';rm ./setAutoLogin.sh"
2021-10-15 14:32:29 +03:00
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
2023-05-17 11:39:37 +02:00
function Invoke-SoftwareUpdateArm64 {
param (
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ string ] $HostName ,
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
2023-11-09 15:45:50 +01:00
[ string ] $Password ,
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ array ] $ListOfUpdates
2023-05-17 11:39:37 +02:00
)
2023-11-09 15:45:50 +01:00
# Define the next macOS version
$command = "sw_vers"
$guestMacosVersion = Invoke-SSHPassCommand -HostName $HostName -Command $command
switch -regex ( $guestMacosVersion [ 1 ]) {
2024-09-20 16:06:10 +02:00
'12.\d' { $nextOSVersion = 'macOS Ventura|macOS Sonoma|macOS Sequoia' }
'13.\d' { $nextOSVersion = 'macOS Sonoma|macOS Sequoia' }
'14.\d' { $nextOSVersion = 'macOS Sequoia' }
2023-11-09 15:45:50 +01:00
}
2023-11-15 12:12:28 +01:00
$url = "https://raw.githubusercontent.com/actions/runner-images/main/images/macos/assets/auto-software-update-arm64.exp"
2023-05-17 11:39:37 +02:00
$script = Invoke-RestMethod -Uri $url
2024-09-20 16:06:10 +02:00
foreach ( $update in $ListOfUpdates ) {
if ( $update -notmatch $nextOSVersion ) {
2023-11-09 15:45:50 +01:00
$updatedScript = $script . Replace ( "MACOSUPDATE" , $ ( $ ( $update . trim ()). Replace ( " " , "\ " )))
$base64 = [ Convert ]:: ToBase64String ( $updatedScript . ToCharArray ())
$command = "echo $base64 | base64 --decode > ./auto-software-update-arm64.exp;chmod +x ./auto-software-update-arm64.exp; ./auto-software-update-arm64.exp ${Password} ;rm ./auto-software-update-arm64.exp"
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
}
2023-05-17 11:39:37 +02:00
}
2021-10-15 14:32:29 +03:00
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
}
2023-04-17 16:42:19 +02:00
function Get-AvailableIPSWVersions {
param (
[ bool ] $IsBeta = $false ,
[ bool ] $IsLatest = $true ,
[ string ] $MacOSCodeNameOrVersion
)
if ( $IsBeta ) {
2023-11-07 16:05:11 +01:00
$command = { mist list firmware " $MacOSCodeNameOrVersion " - -compatible - -include-betas - -latest - -export "/Applications/export.json" }
2023-04-17 16:42:19 +02:00
} elseif ( $IsLatest ) {
2023-11-07 16:05:11 +01:00
$command = { mist list firmware " $MacOSCodeNameOrVersion " - -compatible - -latest - -export "/Applications/export.json" }
2023-04-17 16:42:19 +02:00
} else {
2023-11-07 16:05:11 +01:00
$command = { mist list firmware " $MacOSCodeNameOrVersion " - -compatible - -export "/Applications/export.json" }
2023-04-17 16:42:19 +02:00
}
$condition = { $LASTEXITCODE -eq 0 }
Invoke-WithRetry -Command $command -BreakCondition $condition | Out-Null
$softwareList = get-content -Path "/Applications/export.json"
2023-11-07 16:05:11 +01:00
$availableBuilds = ( $softwareList | ConvertFrom-Json ). build
if ( $null -eq $availableBuilds ) {
2023-04-17 16:42:19 +02:00
Write-Host "Requested macOS ' $MacOSCodeNameOrVersion ' version not found in the list of available installers."
2023-09-15 14:32:34 +02:00
$command = { mist list firmware " $( $MacOSCodeNameOrVersion . split ( '.' )[ 0 ]) " }
2023-04-17 16:42:19 +02:00
Invoke-WithRetry -Command $command -BreakCondition $condition
exit 1
}
2023-11-07 16:05:11 +01:00
return $availableBuilds
2023-04-17 16:42:19 +02:00
}
function Get-MacOSIPSWInstaller {
param (
[ Parameter ( Mandatory )]
[ version ] $MacOSVersion ,
[ bool ] $DownloadLatestVersion = $false ,
[ bool ] $BetaSearch = $false
)
if ( $MacOSVersion -eq [ version ] "12.0" ) {
$MacOSName = "macOS Monterey"
} elseif ( $MacOSVersion -eq [ version ] "13.0" ) {
$MacOSName = "macOS Ventura"
2023-11-07 17:27:55 +01:00
} elseif ( $MacOSVersion -eq [ version ] "14.0" ) {
$MacOSName = "macOS Sonoma"
2023-04-17 16:42:19 +02:00
} else {
$MacOSName = $MacOSVersion . ToString ()
}
Write-Host " `t [*] Finding available full installers"
if ( $DownloadLatestVersion -eq $true ) {
2023-11-07 16:05:11 +01:00
$targetBuild = Get-AvailableIPSWVersions -IsLatest $true -MacOSCodeNameOrVersion $MacOSName
Write-Host " `t [*] The 'DownloadLatestVersion' flag is set to true. Latest compatible macOS build of ' $MacOSName ' is ' $targetBuild '"
2023-04-17 16:42:19 +02:00
} elseif ( $BetaSearch -eq $true ) {
2023-11-07 16:05:11 +01:00
$targetBuild = Get-AvailableIPSWVersions -IsBeta $true -MacOSCodeNameOrVersion $MacOSName
Write-Host " `t [*] The 'BetaSearch' flag is set to true. Latest compatible beta macOS build of ' $MacOSName ' is ' $targetBuild '"
2023-04-17 16:42:19 +02:00
} else {
2023-11-07 16:05:11 +01:00
$targetBuild = Get-AvailableIPSWVersions -MacOSCodeNameOrVersion $MacOSName -IsLatest $false
Write-Host " `t [*] Available compatible macOS builds of ' $MacOSName ' are: $( $targetBuild -join ', ' ) "
if ( $targetBuild . Count -gt 1 ) {
Write-Error " `t [*] Please specify the exact build number of macOS you want to install"
exit 1
}
2023-04-17 16:42:19 +02:00
}
$installerPathPattern = "/Applications/Install ${macOSName} *.ipsw"
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
$installerDir = "/Applications/"
$installerName = "Install ${macOSName} .ipsw"
2023-11-07 16:05:11 +01:00
Write-Host " `t [*] Requested macOS ' $targetBuild ' version installer found, fetching it from mist database"
Invoke-WithRetry { mist download firmware " $targetBuild " - -output-directory $installerDir - -firmware-name " $installerName " } { $LASTEXITCODE -eq 0 } | Out-Null
2023-04-17 16:42:19 +02:00
if ( Test-Path " $installerDir$installerName " ) {
$result = " $installerDir$installerName "
} else {
2023-11-07 16:05:11 +01:00
Write-Error " `t [*] Requested macOS ' $targetBuild ' version installer failed to download"
2023-04-17 16:42:19 +02:00
exit 1
}
return $result
}
2023-09-15 14:32:34 +02:00
2021-10-15 14:32:29 +03:00
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 {
2024-03-14 10:53:11 +01:00
Write-Host " `t [*] Resetting the seed before requesting stable versions"
2021-10-15 14:32:29 +03:00
sudo $seedutil unenroll | Out-Null
}
2024-03-14 10:53:11 +01:00
# Validate there is no software update at the moment
2021-10-15 14:32:29 +03:00
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
}
2023-04-17 16:42:19 +02:00
Show-StringWithFormat $filteredVersions
2023-11-07 16:05:11 +01:00
$osVersions = $filteredVersions . OSVersion | Sort-Object { [ version ] $_ }
2021-10-15 14:32:29 +03:00
$MacOSVersion = $osVersions | Select-Object -Last 1
Write-Host " `t [*] The 'DownloadLatestVersion' flag is set. Latest macOS version is ' $MacOSVersion ' now"
}
2022-08-03 12:12:07 +02:00
$macOSName = $availableVersions . Where { $MacOSVersion -eq $_ . OSVersion }. OSName . Split ( " " )[ 1 ]
2021-10-15 14:32:29 +03:00
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
}
2021-12-07 14:30:22 +03:00
# Clear LastRecommendedMajorOSBundleIdentifier to prevent error during fetching updates
# Install failed with error: Update not found
Update-SoftwareBundle
2021-10-15 14:32:29 +03:00
# Download macOS installer
Write-Host " `t [*] Requested macOS ' $MacOSVersion ' version installer found, fetching it from Apple Software Update"
2023-08-24 15:15:31 +02:00
Invoke-WithRetry -Command { sudo / usr / local / bin / mist download installer $MacOSVersion application - -force - -export installer . json - -output-directory / Applications } -BreakCondition { $LASTEXITCODE -eq 0 } | Out-Null
if ( -not ( Test-Path installer . json -PathType leaf )) {
Write-Host " `t [x] Failed to fetch $MacOSVersion macOS"
2021-10-15 14:32:29 +03:00
exit 1
}
2023-08-24 15:15:31 +02:00
$installerPath = ( Get-Content installer . json | Out-String | ConvertFrom-Json ). options . applicationPath
2022-08-03 12:12:07 +02:00
if ( -not $installerPath ) {
Write-Host " `t [x] Path not found using ' $installerPathPattern '"
exit 1
}
2021-10-15 14:32:29 +03:00
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 ()]
2023-04-17 16:42:19 +02:00
[ string ] $HostName ,
2023-05-17 11:39:37 +02:00
[ array ] $listOfUpdates ,
[ string ] $Password
2021-10-15 14:32:29 +03:00
)
2023-12-15 12:16:58 +01:00
# If an update is happening on macOS arm64 we will use the additional tool to install updates.
$osArch = $ ( arch )
if ( $osArch -eq "arm64" ) {
Invoke-SoftwareUpdateArm64 -HostName $HostName -Password $Password -ListOfUpdates $listOfUpdates
2023-04-17 16:42:19 +02:00
} else {
2023-12-15 12:16:58 +01:00
foreach ( $update in $listOfUpdates ) {
$command = "sudo /usr/sbin/softwareupdate --restart --verbose --install ' $( $update . trim ()) '"
2023-05-17 11:39:37 +02:00
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
2023-04-17 16:42:19 +02:00
}
2021-10-15 14:32:29 +03:00
}
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 " "
2023-11-07 16:05:11 +01:00
if ( $PSVersionTable . PSVersion . Major -eq 7 -and $PSVersionTable . PSVersion . Minor -le 2 ) {
$result = bash -c " $sshPassOptions \"" $Command \"" 2>&1"
} else {
$result = bash -c " $sshPassOptions `" $Command `" 2>&1"
}
2023-08-25 11:29:01 +02:00
if ( $LASTEXITCODE -ne 0 ) {
Write-Error "There is an error during command execution: `n $result "
exit 1
}
$result
2021-10-15 14:32:29 +03:00
}
function Invoke-WithRetry {
param (
[ scriptblock ] $Command ,
[ scriptblock ] $BreakCondition ,
[ int ] $RetryCount = 20 ,
[ int ] $Seconds = 60
)
while ( $RetryCount -gt 0 ) {
2023-12-15 12:16:58 +01:00
try {
if ( $Command ) {
$result = & $Command
}
2021-10-15 14:32:29 +03:00
2023-12-15 12:16:58 +01:00
if (& $BreakCondition ) {
return $result
}
} catch {
Write-Host " `t [!] Error during command execution: $_ "
2021-10-15 14:32:29 +03:00
}
$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
)
2023-08-25 11:29:01 +02:00
#
# https://unix.stackexchange.com/questions/58271/closing-connection-after-executing-reboot-using-ssh-command
#
$command = '(sleep 1 && sudo reboot &) && exit'
2021-10-15 14:32:29 +03:00
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
function Show-StringWithFormat {
param (
[ Parameter ( ValuefromPipeline )]
[ object ] $string
)
2023-04-17 16:42:19 +02:00
2021-10-15 14:32:29 +03:00
process {
( $string | Out-String ). Trim (). split ( " `n " ) | ForEach-Object { Write-Host " `t $_ " }
}
}
2021-11-22 15:18:46 +03:00
function Remove-CurrentBetaSeed {
param (
[ Parameter ( Mandatory )]
[ ValidateNotNullOrEmpty ()]
[ string ] $HostName
)
$command = "sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/Resources/seedutil unenroll"
Invoke-SSHPassCommand -HostName $HostName -Command $command | Out-String
}
2021-10-15 14:32:29 +03:00
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
}
2021-12-07 14:30:22 +03:00
function Update-SoftwareBundle {
$productVersion = sw_vers -productVersion
if ( $productVersion . StartsWith ( '11.' ) ) {
sudo rm -rf / Library / Preferences / com . apple . commerce . plist
sudo / usr / bin / defaults delete / Library / Preferences / com . apple . SoftwareUpdate . plist LastRecommendedMajorOSBundleIdentifier | Out-Null
}
}