param( [string]$ConfigPath = (Join-Path $PSScriptRoot 'config.txt'), [string]$Action = '', [string]$Ip = '', [string]$HashCode = '', [string]$RollbackIp = '', [switch]$RollbackStopFirst, [switch]$Help ) # PAM 部署主脚本(PowerShell 实现)。 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Show-DeployUsage { @' Usage: powershell -File .\deploy.ps1 [-ConfigPath .\config.txt] powershell -File .\deploy.ps1 -Action [-ConfigPath .\config.txt] [-Ip 192.168.1.10] [-HashCode xxxxx] [-RollbackStopFirst] powershell -File .\deploy.ps1 [-ConfigPath .\config.txt] -RollbackIp 192.168.1.10 [-RollbackStopFirst] Notes: - deploy.bat is only a wrapper for this script. - The wrapper avoids cmd.exe delayed-expansion issues with CLIENT_SECRET values containing exclamation marks. '@ | Write-Host } function Write-Info([string]$Message) { Write-Host "[INFO] $Message" } function Write-WarnLog([string]$Message) { Write-Host "[WARN] $Message" } function Write-ErrLog([string]$Message) { Write-Host "[ERROR] $Message" } $script:ActiveConfigPath = $ConfigPath $script:DownloadProgressState = [ordered]@{ Status = '' Success = '' Step = '' Msg = '' Message = '' RateOfProgress = '' RawResponse = '' } function Write-ResultLine([string]$Key, [AllowEmptyString()][string]$Value) { if ($null -eq $Value) { $Value = '' } $normalized = $Value.Replace("`r", ' ').Replace("`n", ' ') Write-Host ("{0}={1}" -f $Key, $normalized) } function Require-IpArgument([string]$TargetIp) { if (-not $TargetIp) { throw 'This action requires -Ip.' } } function Require-HashCodeArgument([string]$PublishHashCode) { if (-not $PublishHashCode) { throw 'This action requires -HashCode.' } } function Write-OnlineIpsResult([string[]]$Ips) { Write-ResultLine -Key 'ACTION' -Value 'get-online-ips' Write-ResultLine -Key 'COUNT' -Value ([string]$Ips.Count) foreach ($entry in $Ips) { Write-ResultLine -Key 'IP' -Value $entry } } function Write-DownloadProgressResult([string]$ActionName = 'poll-download-progress') { Write-ResultLine -Key 'ACTION' -Value $ActionName Write-ResultLine -Key 'STEP' -Value ([string]$script:DownloadProgressState.Step) Write-ResultLine -Key 'MSG' -Value ([string]$script:DownloadProgressState.Msg) Write-ResultLine -Key 'RATE_OF_PROGRESS' -Value ([string]$script:DownloadProgressState.RateOfProgress) Write-ResultLine -Key 'STATUS' -Value ([string]$script:DownloadProgressState.Status) Write-ResultLine -Key 'SUCCESS' -Value ([string]$script:DownloadProgressState.Success) Write-ResultLine -Key 'MESSAGE' -Value ([string]$script:DownloadProgressState.Message) } function Write-FlowStart([string]$Name, [string]$Detail = '') { if ($Detail) { Write-Info "[FLOW][START] $Name | $Detail" } else { Write-Info "[FLOW][START] $Name" } } function Write-FlowDone([string]$Name, [string]$Detail = '') { if ($Detail) { Write-Info "[FLOW][DONE] $Name | $Detail" } else { Write-Info "[FLOW][DONE] $Name" } } function Write-FlowFail([string]$Name, [string]$Detail = '') { if ($Detail) { Write-ErrLog "[FLOW][FAIL] $Name | $Detail" } else { Write-ErrLog "[FLOW][FAIL] $Name" } } function Invoke-FlowStep { param( [string]$Name, [scriptblock]$Action, [string]$Detail = '' ) Write-FlowStart -Name $Name -Detail $Detail try { $result = & $Action Write-FlowDone -Name $Name return $result } catch { Write-FlowFail -Name $Name -Detail $_.Exception.Message throw } } function Get-ManualRollbackCommand { param( [string]$Ip, [bool]$StopFirst ) $command = "powershell -File .\deploy.ps1 -ConfigPath `"$script:ActiveConfigPath`" -RollbackIp `"$Ip`"" if ($StopFirst) { $command += ' -RollbackStopFirst' } return $command } function Get-PendingRollbackStatus { param( [string]$Ip, [string]$Stage, [bool]$StopFirst, [string]$Reason ) $status = "PENDING_AGENT_CONFIRMATION(stopFirst=$($StopFirst.ToString().ToLowerInvariant()))" $command = Get-ManualRollbackCommand -Ip $Ip -StopFirst $StopFirst Write-WarnLog "检测到需要回滚: ip=$Ip stage=$Stage reason=$Reason stopFirst=$StopFirst" Write-WarnLog "当前脚本不会自动执行回滚。请由 Agent 与用户确认后,再执行: $command" return $status } function Convert-ResponseContent { param([AllowNull()][string]$Content) if ([string]::IsNullOrWhiteSpace($Content)) { return $null } try { if ($PSVersionTable.PSVersion.Major -ge 6) { return $Content | ConvertFrom-Json -Depth 100 } return $Content | ConvertFrom-Json } catch { return $Content } } function Get-ErrorBody { param([System.Management.Automation.ErrorRecord]$ErrorRecord) try { $response = $ErrorRecord.Exception.Response if ($null -eq $response) { return $ErrorRecord.Exception.Message } $stream = $response.GetResponseStream() if ($null -eq $stream) { return $ErrorRecord.Exception.Message } $reader = New-Object System.IO.StreamReader($stream) try { return $reader.ReadToEnd() } finally { $reader.Dispose() } } catch { return $ErrorRecord.Exception.Message } } function Get-ResponseValue { param( $Response, [string[]]$Candidates ) foreach ($candidate in $Candidates) { $current = $Response foreach ($segment in ($candidate -split '\.')) { if ($null -eq $current) { break } if ($current -is [string]) { $current = $null break } if ($current -is [System.Collections.IDictionary]) { if ($current.Contains($segment)) { $current = $current[$segment] } else { $current = $null break } } elseif ($current.PSObject.Properties.Name -contains $segment) { $current = $current.$segment } else { $current = $null break } } if ($null -ne $current -and -not [string]::IsNullOrWhiteSpace([string]$current)) { return [string]$current } } return $null } function Get-PamConfig { param([string]$Path) $config = [ordered]@{} if (Test-Path -LiteralPath $Path) { foreach ($rawLine in Get-Content -LiteralPath $Path -Encoding UTF8) { $line = $rawLine.TrimEnd("`r") if ($line -match '^\s*$' -or $line -match '^\s*[#;]') { continue } $index = $line.IndexOf('=') if ($index -lt 1) { continue } $key = $line.Substring(0, $index).Trim() $value = $line.Substring($index + 1).Trim() if ($value -match '^(.*?\S)\s+[;#].*$') { $value = $Matches[1] } switch ($key) { 'HOME_BASE_URL' { $config[$key] = $value } 'CLIENT_ID' { $config[$key] = $value } 'CLIENT_SECRET' { $config[$key] = $value } 'AIRPORT_CODE' { $config[$key] = $value } 'APP_NAME' { $config[$key] = $value } 'MODULE_NAME' { $config[$key] = $value } 'VERSION_NUMBER' { $config[$key] = $value } 'ZIP_FILE_PATH' { $config[$key] = $value } 'ACTION_TYPE' { $config[$key] = $value } 'TIMEOUT' { $config[$key] = $value } 'LOG_NAME' { $config[$key] = $value } } } } else { Write-WarnLog "Config file not found: $Path. Defaults will be used." } $defaults = [ordered]@{ HOME_BASE_URL = 'https://pam.home.com' CLIENT_ID = 'your_client_id' CLIENT_SECRET = 'your_client_secret' AIRPORT_CODE = 'HET' APP_NAME = 'PAM' MODULE_NAME = 'Node' VERSION_NUMBER = '2.0.5' ZIP_FILE_PATH = 'C:\path\to\pam-2.0.5.zip' ACTION_TYPE = 'FULL' TIMEOUT = '120' LOG_NAME = 'app.log' } foreach ($name in $defaults.Keys) { if (-not $config.Contains($name) -or [string]::IsNullOrWhiteSpace([string]$config[$name])) { $config[$name] = $defaults[$name] } } return [pscustomobject]$config } function Join-RequestPairs { param([System.Collections.IDictionary]$Values) $pairs = foreach ($key in $Values.Keys) { $encodedValue = [System.Uri]::EscapeDataString([string]$Values[$key]) '{0}={1}' -f $key, $encodedValue } return ($pairs -join '&') } function Test-ZipFile { param($Config) if (-not (Test-Path -LiteralPath $Config.ZIP_FILE_PATH)) { throw "Package file not found: $($Config.ZIP_FILE_PATH)" } } function Invoke-PamWebRequest { param( [ValidateSet('GET', 'POST', 'PUT')] [string]$Method, [string]$Url, [string]$Token, [hashtable]$Headers = @{}, [AllowNull()]$Body = $null, [string]$ContentType = '', [string]$OutFile = '' ) $allHeaders = @{} if ($Token) { $allHeaders['Authorization'] = "Bearer $Token" } foreach ($key in $Headers.Keys) { $allHeaders[$key] = $Headers[$key] } $params = @{ Uri = $Url Method = $Method Headers = $allHeaders ErrorAction = 'Stop' } if ($PSVersionTable.PSVersion.Major -lt 6) { $params['UseBasicParsing'] = $true } if ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body -and "$Body" -ne '') { $params['Body'] = $Body } if ($ContentType) { $params['ContentType'] = $ContentType } if ($OutFile) { $params['OutFile'] = $OutFile } try { $response = Invoke-WebRequest @params if ($OutFile) { return $OutFile } return Convert-ResponseContent $response.Content } catch { $body = Get-ErrorBody $_ throw "Request failed [$Method] $Url`n$body" } } function Invoke-PamMultipartUpload { param( [string]$Url, [string]$Token, [string]$FilePath, [hashtable]$Fields ) Add-Type -AssemblyName System.Net.Http $client = [System.Net.Http.HttpClient]::new() try { $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $Token) $content = [System.Net.Http.MultipartFormDataContent]::new() foreach ($entry in $Fields.GetEnumerator()) { $stringContent = [System.Net.Http.StringContent]::new([string]$entry.Value, [System.Text.Encoding]::UTF8) $content.Add($stringContent, $entry.Key) } $stream = [System.IO.File]::OpenRead($FilePath) try { $fileContent = [System.Net.Http.StreamContent]::new($stream) $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream') $content.Add($fileContent, 'file', [System.IO.Path]::GetFileName($FilePath)) $response = $client.PostAsync($Url, $content).GetAwaiter().GetResult() $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() if (-not $response.IsSuccessStatusCode) { throw "Upload failed [HTTP $([int]$response.StatusCode)]`n$body" } return Convert-ResponseContent $body } finally { $stream.Dispose() } } finally { $client.Dispose() } } function Get-Token { param($Config) Write-Info 'Getting token...' $body = Join-RequestPairs ([ordered]@{ grant_type = 'client_credentials' client_id = $Config.CLIENT_ID client_secret = $Config.CLIENT_SECRET }) $response = Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/oauth/token" -Token '' -Body $body -ContentType 'application/x-www-form-urlencoded' $token = Get-ResponseValue -Response $response -Candidates @('access_token') if (-not $token) { throw "Invalid token response: $response" } return $token } function New-VersionRecord { param($Config, [string]$Token) Write-Info 'Step 2.1: create version record' $body = Join-RequestPairs ([ordered]@{ versionNumber = $Config.VERSION_NUMBER applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME description = 'Auto Deploy' }) [void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/api/version/upgrade" -Token $Token -Body $body -ContentType 'application/x-www-form-urlencoded') } function Upload-Package { param($Config, [string]$Token) Write-Info 'Step 2.2: upload package' $response = Invoke-PamMultipartUpload -Url "$($Config.HOME_BASE_URL)/api/version/upgrade/upload" -Token $Token -FilePath $Config.ZIP_FILE_PATH -Fields @{ applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME versionNumber = $Config.VERSION_NUMBER } $hashCode = Get-ResponseValue -Response $response -Candidates @('hashCode', 'data.hashCode') if (-not $hashCode -and $response -is [string]) { $hashCode = $response.Trim() } if (-not $hashCode) { throw "Unable to parse hashCode from upload response: $response" } return $hashCode } function Publish-Version { param($Config, [string]$Token, [string]$HashCode) Write-Info 'Step 2.3: publish version' $payload = @{ airportCodesWhite = @($Config.AIRPORT_CODE) hashCode = $HashCode state = 'RELEASE' } | ConvertTo-Json -Depth 5 $query = Join-RequestPairs ([ordered]@{ versionNumber = $Config.VERSION_NUMBER applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME }) [void](Invoke-PamWebRequest -Method PUT -Url "$($Config.HOME_BASE_URL)/api/version/upgrade/profile?$query" -Token $Token -Body $payload -ContentType 'application/json') } function Get-NodeUrl { param($Config, [string]$Token) Write-Info 'Step 3.1: resolve node url' $response = Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/api/mcp/airport/target-node?airportCode=$($Config.AIRPORT_CODE)" -Token $Token if ($response -is [System.Collections.IDictionary]) { return [string]($response.Keys | Select-Object -First 1) } $propertyNames = @($response.PSObject.Properties.Name) if ($propertyNames.Count -gt 0) { return [string]$propertyNames[0] } throw "Unable to resolve node url: $response" } function Get-OnlineIps { param($Config, [string]$Token, [string]$NodeUrl) Write-Info 'Step 3.2: query online IP list' $query = Join-RequestPairs ([ordered]@{ applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME airportCode = $Config.AIRPORT_CODE }) $response = Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/ips?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } $ips = @() if ($response -is [System.Array]) { $ips = @($response | ForEach-Object { [string]$_ } | Where-Object { $_ }) } elseif ($response -is [System.Collections.IEnumerable] -and -not ($response -is [string])) { $ips = @($response | ForEach-Object { [string]$_ } | Where-Object { $_ }) } if ($ips.Count -eq 0) { throw "No online workstation matched the module. Raw response: $response" } return $ips } function Wait-DownloadProgress { param($Config, [string]$Token, [string]$NodeUrl) $query = Join-RequestPairs ([ordered]@{ applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME airportCode = $Config.AIRPORT_CODE versionNumber = $Config.VERSION_NUMBER }) $progressUrl = "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/download-cloud/progress?$query" $script:DownloadProgressState = [ordered]@{ Status = '' Success = '' Step = '' Msg = '' Message = '' RateOfProgress = '' RawResponse = '' } for ($attempt = 0; $attempt -lt 60; $attempt++) { $response = Invoke-PamWebRequest -Method GET -Url $progressUrl -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } $status = Get-ResponseValue -Response $response -Candidates @('status') $successFlag = Get-ResponseValue -Response $response -Candidates @('success') $step = Get-ResponseValue -Response $response -Candidates @('step') $msg = Get-ResponseValue -Response $response -Candidates @('msg') $progressValue = Get-ResponseValue -Response $response -Candidates @('rateOfProgress', 'progress', 'percent', 'data.rateOfProgress', 'data.progress', 'data.percent') $message = Get-ResponseValue -Response $response -Candidates @('message') if (-not $message) { $message = $msg } $script:DownloadProgressState = [ordered]@{ Status = [string]$status Success = [string]$successFlag Step = [string]$step Msg = [string]$msg Message = [string]$message RateOfProgress = [string]$progressValue RawResponse = [string]$response } $progressParts = [System.Collections.Generic.List[string]]::new() if ($msg) { $progressParts.Add("msg=$msg") } if ($step) { $progressParts.Add("step=$step") } if ($progressValue) { $progressParts.Add("rateOfProgress=$progressValue") } if ($status) { $progressParts.Add("status=$status") } if ($successFlag) { $progressParts.Add("success=$successFlag") } if ($message -and $message -ne $msg) { $progressParts.Add("message=$message") } if ($progressParts.Count -gt 0) { Write-Info ("Step 3.3b: async download progress -> {0}" -f ($progressParts -join ', ')) } else { Write-Info ("Step 3.3b: async download progress polling... ({0}/60)" -f ($attempt + 1)) } if ($step -eq 'DONE' -or $status -eq 'completed' -or $successFlag -eq 'true' -or (($msg -eq 'success') -and ($progressValue -eq '100'))) { return } if ((@($step, $message, $msg) -join ' ') -match '(?i)fail|error') { if (-not $message) { $message = $step } if (-not $message) { $message = $msg } throw "Node download failed: $message" } Start-Sleep -Seconds 2 } throw 'Node download timed out.' } function Create-DownloadTask { param($Config, [string]$Token, [string]$NodeUrl) Write-Info 'Step 3.3: download package to node' $query = Join-RequestPairs ([ordered]@{ versionNumber = $Config.VERSION_NUMBER applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME timeOut = '0' }) [void](Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/download-cloud?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl 'airport-code' = $Config.AIRPORT_CODE }) } function Download-CloudToNode { param($Config, [string]$Token, [string]$NodeUrl) Create-DownloadTask -Config $Config -Token $Token -NodeUrl $NodeUrl Wait-DownloadProgress -Config $Config -Token $Token -NodeUrl $NodeUrl } function Invoke-UpgradeRequest { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip) $query = Join-RequestPairs ([ordered]@{ airportCode = $Config.AIRPORT_CODE targetIp = $Ip applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME versionNumber = $Config.VERSION_NUMBER action = $Config.ACTION_TYPE autoStart = 'false' timeOut = $Config.TIMEOUT }) Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } } function Start-Application { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip) $query = Join-RequestPairs ([ordered]@{ airportCode = $Config.AIRPORT_CODE targetIp = $Ip applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME runStart = 'true' }) [void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/start-stop?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl }) } function Stop-Application { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip) $query = Join-RequestPairs ([ordered]@{ airportCode = $Config.AIRPORT_CODE targetIp = $Ip applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME runStart = 'false' }) [void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/start-stop?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl }) } function Verify-Ip { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip) $query = Join-RequestPairs ([ordered]@{ applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME airportCode = $Config.AIRPORT_CODE targetIp = $Ip }) Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/verify?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } } function Download-DeployLog { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip) $logsDir = Join-Path $PSScriptRoot 'logs' if (-not (Test-Path -LiteralPath $logsDir)) { $null = New-Item -ItemType Directory -Path $logsDir } $logFile = Join-Path $logsDir ("deploy_{0}.zip" -f $Ip) $errorFile = Join-Path $logsDir ("error_{0}.log" -f $Ip) $query = Join-RequestPairs ([ordered]@{ applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME airportCode = $Config.AIRPORT_CODE targetIp = $Ip logName = $Config.LOG_NAME }) try { [void](Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/log-download?$query" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } -OutFile $logFile) if ((Get-Item -LiteralPath $logFile).Length -gt 0) { @( 'Archive format: zip' ("Saved path: {0}" -f $logFile) ("Size: {0} bytes" -f (Get-Item -LiteralPath $logFile).Length) ) | Set-Content -LiteralPath "$logFile.summary" } else { 'Zip archive empty or no data' | Set-Content -LiteralPath "$logFile.summary" } } catch { Get-ErrorBody $_ | Set-Content -LiteralPath $errorFile "Log download failed. See $errorFile" | Set-Content -LiteralPath $logFile 'Log download failed' | Set-Content -LiteralPath "$logFile.summary" } return $logFile } function Invoke-Rollback { param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip, [bool]$StopFirst) if ($StopFirst) { try { Stop-Application -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } catch { } } try { $body = Join-RequestPairs ([ordered]@{ airportCode = $Config.AIRPORT_CODE targetIp = $Ip applicationName = $Config.APP_NAME moduleName = $Config.MODULE_NAME timeOut = $Config.TIMEOUT }) $response = Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node-proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/rollback" -Token $Token -Headers @{ 'Target-Node' = $NodeUrl } -Body $body -ContentType 'application/x-www-form-urlencoded' $rollbackSuccess = Get-ResponseValue -Response $response -Candidates @('success') if ($rollbackSuccess -and $rollbackSuccess -ne 'true') { return 'ROLLBACK_FAILED' } } catch { return 'ROLLBACK_REQUEST_FAILED' } try { $verify = Verify-Ip -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip if ((Get-ResponseValue -Response $verify -Candidates @('success')) -eq 'true') { return 'ROLLBACK_SUCCESS' } return 'ROLLBACK_VERIFY_FAILED' } catch { return 'ROLLBACK_VERIFY_FAILED' } } function Invoke-IpDeploy { param( $Config, [string]$Token, [string]$NodeUrl, [string]$Ip ) Write-Info "Processing IP: $Ip" try { $upgrade = Invoke-FlowStep -Name "Invoke-UpgradeRequest[$Ip]" -Action { Invoke-UpgradeRequest -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } } catch { $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'FAILED' Stage = 'UPGRADE' Message = 'Upgrade request failed' Rollback = 'ROLLBACK_NOT_RUN' LogFile = $logFile } } if ((Get-ResponseValue -Response $upgrade -Candidates @('success')) -ne 'true') { $message = Get-ResponseValue -Response $upgrade -Candidates @('message') if (-not $message) { $message = 'Upgrade failed' } $rollback = Get-PendingRollbackStatus -Ip $Ip -Stage 'UPGRADE' -StopFirst:$false -Reason $message $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'FAILED' Stage = 'UPGRADE' Message = $message Rollback = $rollback LogFile = $logFile } } try { Invoke-FlowStep -Name "Start-Application[$Ip]" -Action { Start-Application -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } | Out-Null } catch { $rollback = Get-PendingRollbackStatus -Ip $Ip -Stage 'START' -StopFirst:$true -Reason 'Application start failed' $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'FAILED' Stage = 'START' Message = 'Application start failed' Rollback = $rollback LogFile = $logFile } } try { $verify = Invoke-FlowStep -Name "Verify-Ip[$Ip]" -Action { Verify-Ip -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } } catch { $rollback = Get-PendingRollbackStatus -Ip $Ip -Stage 'VERIFY' -StopFirst:$true -Reason 'Health check request failed' $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'FAILED' Stage = 'VERIFY' Message = 'Health check request failed' Rollback = $rollback LogFile = $logFile } } if ((Get-ResponseValue -Response $verify -Candidates @('success')) -eq 'true') { $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'SUCCESS' Stage = '-' Message = '-' Rollback = '-' LogFile = $logFile } } $message = Get-ResponseValue -Response $verify -Candidates @('message') if (-not $message) { $message = 'Health check failed' } $rollback = Get-PendingRollbackStatus -Ip $Ip -Stage 'VERIFY' -StopFirst:$true -Reason $message $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip } return [pscustomobject]@{ Ip = $Ip Status = 'FAILED' Stage = 'VERIFY' Message = $message Rollback = $rollback LogFile = $logFile } } function Write-DeployReport { param( $Config, [System.Collections.Generic.List[object]]$Results, [int]$TotalCount ) $successCount = @($Results | Where-Object { $_.Status -eq 'SUCCESS' }).Count $failCount = @($Results | Where-Object { $_.Status -ne 'SUCCESS' }).Count Write-Host '' Write-Host '====================== DEPLOY REPORT ======================' Write-Host 'Mode: Batch/PowerShell' Write-Host "Airport: $($Config.AIRPORT_CODE)" Write-Host "Application: $($Config.APP_NAME)" Write-Host "Module: $($Config.MODULE_NAME)" Write-Host "Version: $($Config.VERSION_NUMBER)" Write-Host "Total: $TotalCount" Write-Host "Success: $successCount" Write-Host "Failed: $failCount" Write-Host '' Write-Host ('{0,-18} {1,-8} {2,-12} {3,-22} {4}' -f 'IP', 'STATUS', 'STAGE', 'ROLLBACK', 'LOG') foreach ($item in $Results) { Write-Host ('{0,-18} {1,-8} {2,-12} {3,-22} {4}' -f $item.Ip, $item.Status, $item.Stage, $item.Rollback, $item.LogFile) if ($item.Status -ne 'SUCCESS') { Write-Host (" Reason: {0}" -f $item.Message) } } } function Invoke-PamDeploy { param([string]$ConfigPath) $script:ActiveConfigPath = $ConfigPath $config = Invoke-FlowStep -Name 'Get-PamConfig' -Detail "path=$ConfigPath" -Action { Get-PamConfig -Path $ConfigPath } Invoke-FlowStep -Name 'Test-ZipFile' -Action { Test-ZipFile -Config $config } | Out-Null Write-Info "Deploy start: airport=$($config.AIRPORT_CODE), version=$($config.VERSION_NUMBER), module=$($config.APP_NAME)/$($config.MODULE_NAME)" $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } Invoke-FlowStep -Name 'New-VersionRecord' -Action { New-VersionRecord -Config $config -Token $token } | Out-Null $hashCode = Invoke-FlowStep -Name 'Upload-Package' -Action { Upload-Package -Config $config -Token $token } Invoke-FlowStep -Name 'Publish-Version' -Action { Publish-Version -Config $config -Token $token -HashCode $hashCode } | Out-Null $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $ips = Invoke-FlowStep -Name 'Get-OnlineIps' -Action { Get-OnlineIps -Config $config -Token $token -NodeUrl $nodeUrl } Invoke-FlowStep -Name 'Download-CloudToNode' -Action { Download-CloudToNode -Config $config -Token $token -NodeUrl $nodeUrl } | Out-Null $results = [System.Collections.Generic.List[object]]::new() foreach ($ip in $ips) { $results.Add((Invoke-IpDeploy -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip)) } Write-DeployReport -Config $config -Results $results -TotalCount $ips.Count } function Invoke-PamManualRollback { param( [string]$ConfigPath, [string]$Ip, [bool]$StopFirst ) $script:ActiveConfigPath = $ConfigPath $config = Invoke-FlowStep -Name 'Get-PamConfig' -Detail "path=$ConfigPath" -Action { Get-PamConfig -Path $ConfigPath } Write-Info "Manual rollback start: airport=$($config.AIRPORT_CODE), ip=$Ip, stopFirst=$StopFirst" $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $result = Invoke-FlowStep -Name "Invoke-Rollback[$Ip]" -Action { Invoke-Rollback -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip -StopFirst:$StopFirst } Write-Info "Manual rollback done: ip=$Ip result=$result" Write-Host "ROLLBACK RESULT: $result" } function Invoke-PamAction { param( [string]$ConfigPath, [string]$Action, [string]$Ip, [string]$HashCode, [bool]$StopFirst ) $script:ActiveConfigPath = $ConfigPath $config = Invoke-FlowStep -Name 'Get-PamConfig' -Detail "path=$ConfigPath" -Action { Get-PamConfig -Path $ConfigPath } switch ($Action.ToLowerInvariant()) { 'get-token' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } Write-ResultLine -Key 'ACTION' -Value 'get-token' Write-ResultLine -Key 'TOKEN' -Value $token } 'create-version' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } Invoke-FlowStep -Name 'New-VersionRecord' -Action { New-VersionRecord -Config $config -Token $token } | Out-Null Write-ResultLine -Key 'ACTION' -Value 'create-version' Write-ResultLine -Key 'VERSION_NUMBER' -Value $config.VERSION_NUMBER Write-ResultLine -Key 'RESULT' -Value 'OK' } 'upload-package' { Invoke-FlowStep -Name 'Test-ZipFile' -Action { Test-ZipFile -Config $config } | Out-Null $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $hash = Invoke-FlowStep -Name 'Upload-Package' -Action { Upload-Package -Config $config -Token $token } Write-ResultLine -Key 'ACTION' -Value 'upload-package' Write-ResultLine -Key 'HASH_CODE' -Value $hash } 'publish-version' { Require-HashCodeArgument -PublishHashCode $HashCode $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } Invoke-FlowStep -Name 'Publish-Version' -Action { Publish-Version -Config $config -Token $token -HashCode $HashCode } | Out-Null Write-ResultLine -Key 'ACTION' -Value 'publish-version' Write-ResultLine -Key 'HASH_CODE' -Value $HashCode Write-ResultLine -Key 'RESULT' -Value 'OK' } 'get-node-url' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Write-ResultLine -Key 'ACTION' -Value 'get-node-url' Write-ResultLine -Key 'NODE_URL' -Value $nodeUrl } 'get-online-ips' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $ips = Invoke-FlowStep -Name 'Get-OnlineIps' -Action { Get-OnlineIps -Config $config -Token $token -NodeUrl $nodeUrl } Write-OnlineIpsResult -Ips @($ips) } 'create-download-task' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Invoke-FlowStep -Name 'Create-DownloadTask' -Action { Create-DownloadTask -Config $config -Token $token -NodeUrl $nodeUrl } | Out-Null Write-ResultLine -Key 'ACTION' -Value 'create-download-task' Write-ResultLine -Key 'TIME_OUT' -Value '0' Write-ResultLine -Key 'RESULT' -Value 'TASK_CREATED' } 'poll-download-progress' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Invoke-FlowStep -Name 'Wait-DownloadProgress' -Action { Wait-DownloadProgress -Config $config -Token $token -NodeUrl $nodeUrl } | Out-Null Write-DownloadProgressResult } 'download-cloud-to-node' { $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Invoke-FlowStep -Name 'Download-CloudToNode' -Action { Download-CloudToNode -Config $config -Token $token -NodeUrl $nodeUrl } | Out-Null Write-DownloadProgressResult -ActionName 'download-cloud-to-node' Write-ResultLine -Key 'RESULT' -Value 'DONE' } 'upgrade-ip' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $response = Invoke-FlowStep -Name "Invoke-UpgradeRequest[$Ip]" -Action { Invoke-UpgradeRequest -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip } Write-ResultLine -Key 'ACTION' -Value 'upgrade-ip' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'SUCCESS' -Value (Get-ResponseValue -Response $response -Candidates @('success')) Write-ResultLine -Key 'MESSAGE' -Value (Get-ResponseValue -Response $response -Candidates @('message')) Write-ResultLine -Key 'RAW_RESPONSE' -Value ([string]$response) } 'start-ip' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Invoke-FlowStep -Name "Start-Application[$Ip]" -Action { Start-Application -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip } | Out-Null Write-ResultLine -Key 'ACTION' -Value 'start-ip' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'RUN_START' -Value 'true' Write-ResultLine -Key 'RESULT' -Value 'OK' } 'stop-ip' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } Invoke-FlowStep -Name "Stop-Application[$Ip]" -Action { Stop-Application -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip } | Out-Null Write-ResultLine -Key 'ACTION' -Value 'stop-ip' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'RUN_START' -Value 'false' Write-ResultLine -Key 'RESULT' -Value 'OK' } 'verify-ip' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $response = Invoke-FlowStep -Name "Verify-Ip[$Ip]" -Action { Verify-Ip -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip } Write-ResultLine -Key 'ACTION' -Value 'verify-ip' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'SUCCESS' -Value (Get-ResponseValue -Response $response -Candidates @('success')) Write-ResultLine -Key 'MESSAGE' -Value (Get-ResponseValue -Response $response -Candidates @('message')) Write-ResultLine -Key 'RAW_RESPONSE' -Value ([string]$response) } 'download-log' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $logFile = Invoke-FlowStep -Name "Download-DeployLog[$Ip]" -Action { Download-DeployLog -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip } Write-ResultLine -Key 'ACTION' -Value 'download-log' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'LOG_FILE' -Value $logFile } 'rollback-ip' { Require-IpArgument -TargetIp $Ip $token = Invoke-FlowStep -Name 'Get-Token' -Action { Get-Token -Config $config } $nodeUrl = Invoke-FlowStep -Name 'Get-NodeUrl' -Action { Get-NodeUrl -Config $config -Token $token } $rollbackResult = Invoke-FlowStep -Name "Invoke-Rollback[$Ip]" -Action { Invoke-Rollback -Config $config -Token $token -NodeUrl $nodeUrl -Ip $Ip -StopFirst:$StopFirst } Write-ResultLine -Key 'ACTION' -Value 'rollback-ip' Write-ResultLine -Key 'IP' -Value $Ip Write-ResultLine -Key 'STOP_FIRST' -Value ($StopFirst.ToString().ToLowerInvariant()) Write-ResultLine -Key 'ROLLBACK_RESULT' -Value $rollbackResult } default { throw "Unknown action: $Action" } } } if ($Help) { Show-DeployUsage exit 0 } if ($MyInvocation.InvocationName -ne '.') { try { if ($Action) { Invoke-PamAction -ConfigPath $ConfigPath -Action $Action -Ip $Ip -HashCode $HashCode -StopFirst:$RollbackStopFirst.IsPresent } elseif ($RollbackIp) { Invoke-PamManualRollback -ConfigPath $ConfigPath -Ip $RollbackIp -StopFirst:$RollbackStopFirst.IsPresent } else { Invoke-PamDeploy -ConfigPath $ConfigPath } } catch { Write-ErrLog $_ exit 1 } }