Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save chandra-goka/6c9a9f54d9cd7d9dbccc863f494cd458 to your computer and use it in GitHub Desktop.

Select an option

Save chandra-goka/6c9a9f54d9cd7d9dbccc863f494cd458 to your computer and use it in GitHub Desktop.
bootstrap.ps1
<#
.SYNOPSIS
Registers this host with ssm agent
.DESCRIPTION
Downloads the agent, installs it, must be run as Administrator
.VERSION
2.1 (d7bdf330dcc040e0406ddfd7911891aaf1c4b576)
.PARAMETER Command
Install: Install SSM agent and register this node
Uninstall: Uninstall SSM agent
Reregister: Clear existing agent registration and re-register this node
.PARAMETER Platform
Device platform such as vmware, dedicated (CORE physical), etc.
It's used by the script to distinguish where to fetch the JWT token from.
.PARAMETER HttpProxy
Proxy host to be used by the bootstrap script when making external calls.
It will also be set in the SSM agent's config after installation in order
to be used during the registering phase.
The input should be in the "<http|https>://<fqdn|ip>:<port>" format. For example: "http://10.22.33.44:4242"
.PARAMETER InstallerDownloadRegion
The AWS region to download the agent from. If not provided, the default
agent package URL will be used. For example: "eu-central-1"
.PARAMETER PlatformServicesBaseUrl
Platform Services Add-Ons API host for instance activation
.PARAMETER ResultFormat
text (default) | json
.EXAMPLE
.\bootstrap.ps1 -Command Install -Platform Azure
.EXAMPLE
.\bootstrap.ps1 -Command Install -Platform Vmware
.EXAMPLE
.\bootstrap.ps1 -Command Install -Platform Vmware -InstallerDownloadRegion eu-central-1 -HttpProxy http://10.22.33.44:4242
Ensure the SSM agent installer is downloaded from a specific region and that external calls from the bootstrap script and SSM agent registration use an Http proxy.
.EXAMPLE
.\bootstrap.ps1 -Command Uninstall
.EXAMPLE
.\bootstrap.ps1 -Command Reregister
.NOTES
Version: 2.0
Author: adam.tistler@rackspace.com
Creation Date: 09/21/22
#>
<#
.Description
Exit Codes : description
100: Failed to install Package 'AmazonSSMAgent
101: Failed to uninstall Package 'AmazonSSMAg
103: Unable to stop 'AmazonSsmAgent' service
104: Service 'AmazonSsmAgent' has not reached a 'running' status after multiple attempts.
105: GET request to activation job url failed.
106: SSM agent activation job was not successful
107: Agent activation job did not complete after multiple attempts
108: POST request to activation url failed.
109: POST request to activation url did not return a Location header.
110: Failed to execute agent registration command.
111: Failed to execute agent diagnostics command.
112: Management agent was not registered successfully.
113: ssm-cli.exe' command does not exist
114: Failed to execute clear agent registration command
115: HTTP request failed while installing Amazon SSM Agent
116: Package 'AmazonSsmAgent' is not currently installed
117: HTTP request failed while uninstalling Amazon SSM Agent
118: Could not retrieve token after multiple attempts. Rackspace vmware provisioning is responsible for setting the 'guestinfo.machine.id' custom property.
119: VMWare/OpenStack get token command failed.
120: GET request to metadata instance url failed.
121: GET request to metadata attest url failed.
122: Token file not found. Rackspace dedicated server provisioning is responsible for populating this file.
123: Token file is empty. Rackspace dedicated server provisioning is responsible for populating this file.
124: GET request to instance identity url failed
125: GET request to instance metadata identity url failed
126: Downloading agent package installer from package installer url failed
127: Command 'amazon-ssm-agent' not found
128: SSM package is not installed, cannot reregister agent
129: ssm-cli' command does not exist
133: The Rackspace management agent only supports Windows Server or Windows Domain Controllers, it does not support this product type.
134: The Rackspace management agent supports Windows versions ['2012', '2016', '2019', '2022'], it does not support this version.
135: The Rackspace management agent only supports 64-bit versions of Windows.
136: rpctool' command does not exist
137: Could not retrieve OpenStack auth token after multiple attempts
138: Got an error from OpenStack vendordata API
144: Something went wrong. Uncaught exception occurred.
#>
param(
[Parameter(Mandatory = $true, ParameterSetName = "UnitTesting")]
[switch]$UnitTesting,
[Parameter(Mandatory = $true, ParameterSetName = "Execution")][ValidateSet("Install", "Reregister", "Uninstall")]
[string]$Command,
[Parameter(Mandatory = $true, ParameterSetName = "Execution")][ValidateSet("Dedicated", "VMWare", "GCP", "Azure", "OpenStackFlex", "OpenStack")]
[string]$Platform,
[Parameter(ParameterSetName = "Execution")]
[ValidatePattern('^(http|https):\/\/([-a-zA-Z0-9._~!$&\"*+,;=:(PCTENCODED)]*\.[a-z]{2,}|[-a-zA-Z0-9._~!$&\"*+,;=:(PCTENCODED)]*|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):\d{2,5}')]
[string]$HttpProxy, # valid "<http|https>://<fqdn|ip>:<port>" format
[Parameter(Mandatory = $false, ParameterSetName = "Execution")][ValidatePattern('([a-z]{2})\-([a-z]+)-(\d+)')]
[string]$InstallerDownloadRegion,
[Parameter(Mandatory = $false, ParameterSetName = "Execution")][ValidateSet("text", "json")]
[string]$ResultFormat = "text",
[Parameter(Mandatory = $false, ParameterSetName = "Execution")]
[string]$PlatformServicesBaseUrl = "https://add-ons.api.manage.rackspace.com"
)
$SupportedWindowsVersions = @("2012", "2016", "2019", "2022", "2025")
$DefaultAgentPackageUrl = "https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/windows_amd64/AmazonSSMAgentSetup.exe"
$RegionalAgentPackageUrl = "https://amazon-ssm-{0}.s3.{0}.amazonaws.com/latest/windows_amd64/AmazonSSMAgentSetup.exe"
$IgnoreTagKey = "rackspace-addon-ignore"
$SsmService = "AmazonSsmAgent"
$SsmProcess = "amazon-ssm-agent"
$AgentPackageName = "Amazon SSM Agent"
$RegistrationWait = 5
$LogPath = [Environment]::CurrentDirectory
$LogName = "agent_bootstrap.log"
$AgentProgramDir = "C:\Program Files\Amazon\SSM"
$AgentDataDir = "$Env:ProgramData\Amazon\SSM"
$DebugPreference = "Continue"
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$ResultDelimiterFormat = "$( '-'*25 ){0}$( '-'*25 )"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Add-Type -AssemblyName System.Net.Http
function WriteLog {
param(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Message,
[Parameter()][ValidateNotNullOrEmpty()][ValidateSet('DEBUG', 'WARN')][string]$Level = 'DEBUG'
)
$FullPath = Join-Path -Path $LogPath -ChildPath $LogName
If (!(Test-Path -Path $LogPath)) {
New-Item -Path $LogPath -ItemType Directory
}
If (!(Test-Path -Path $FullPath -PathType Leaf)) {
New-Item -Path $FullPath -ItemType File
}
$Line = AsText -Message $Message -Level $Level
Add-Content -Path $FullPath -Value $Line
return $Line
}
function AsText {
param(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Message,
[Parameter()][ValidateNotNullOrEmpty()][ValidateSet('DEBUG', 'WARN')][string]$Level = 'DEBUG'
)
$Format = "{0} {1} - {2}"
$Line = ($Format -f ((Get-Date).ToUniversalTime().ToString("o")), $Level, $Message)
return $Line
}
function AsJSON {
param(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Message,
[Parameter()][ValidateNotNullOrEmpty()][ValidateSet('DEBUG', 'WARN')][string]$Level = 'DEBUG',
[Parameter()][string]$Details = ''
)
return @{
timestamp = ((Get-Date).ToUniversalTime().ToString("o"))
level = $Level
message = $Message
details = $Details
} | ConvertTo-Json -Compress
}
function Die {
param(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Message,
[Parameter(Mandatory = $true)][int]$ExitCode,
[Parameter(Mandatory = $false)][string]$Details = ''
)
if ($Details -ne '') {
$DetailedMessage = "${Message} `r`n ${Details}"
}
else {
$DetailedMessage = $Message
}
$Line = WriteLog -Level 'WARN' -Message $DetailedMessage # Ignoring output to prevent writing to console twice
Write-Warning ($ResultDelimiterFormat -f "Failed")
if ($ResultFormat -eq 'json') {
Write-Warning ( AsJSON -Level 'WARN' -Message $Message -Details $Details)
}
else {
Write-Warning ( AsText -Level 'WARN' -Message $DetailedMessage)
}
exit $ExitCode
}
function Success {
param(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Message,
[Parameter(Mandatory = $false)][string]$Details = ''
)
if ($Details -ne '') {
$DetailedMessage = "${Message} `r`n ${Details}"
}
else {
$DetailedMessage = $Message
}
$Line = WriteLog -Level 'DEBUG' -Message $DetailedMessage # Ignoring output to prevent writing to console twice
Write-Debug ($ResultDelimiterFormat -f "Success")
if ($ResultFormat -eq 'json') {
Write-Debug ( AsJSON -Level 'DEBUG' -Message $Message -Details $Details)
}
else {
Write-Debug ( AsText -Level 'DEBUG' -Message $DetailedMessage)
}
exit 0
}
function IsTruthy([AllowNull()]$Value) {
return (($Value -is [string]) -and ('true', 't', 'yes', 'y', '1').Contains($Value.toLower()))
}
function Decode-JwtPayload {
param (
[Parameter(Mandatory = $true)]
[string]$Token
)
$parts = $Token -split '\.'
if ($parts.Count -ne 3) {
return @{ }
}
$payload = $parts[1]
# Convert Base64URL → Base64
$payload = $payload.Replace('-', '+').Replace('_', '/')
# Restore padding
switch ($payload.Length % 4) {
2 { $payload += '==' }
3 { $payload += '=' }
0 { }
default { return @{ } }
}
try {
$bytes = [Convert]::FromBase64String($payload)
$json = [Text.Encoding]::UTF8.GetString($bytes)
return $json | ConvertFrom-Json
} catch {
return @{ }
}
}
function VmwarePlatform {
function GetToken {
$GetTokenAttempts = 20
$GetTokenDelay = 60
$RpcToolPath = "C:\Program Files\VMware\VMware Tools\rpctool.exe"
if (!(Test-Path -Path $RpcToolPath)) {
Die "${RpcToolPath} command does not exist" -ExitCode 136
}
try {
foreach ($Attempt in 1..$GetTokenAttempts) {
WriteLog "Running vmware get token command (attempt ${Attempt}): ${RpcToolPath} 'info-get guestinfo.machine.id'" | Write-Debug
$Token = & $RpcToolPath 'info-get guestinfo.machine.id' | Out-String
if ($Token) {
break
}
Start-Sleep -Seconds $GetTokenDelay
}
if ($Token) {
WriteLog "Found vmware auth token: ${Token}" | Write-Debug
return $Token
}
Die "Could not retrieve token after ${GetTokenAttempt} attempts. Rackspace vmware provisioning is responsible for setting the 'guestinfo.machine.id' custom property." -ExitCode 118
}
catch {
Die "Vmware get token command failed: $( $_.Exception.Message )" -ExitCode 119
}
}
function IsAgentDisabled {
WriteLog "Platform 'vmware' does not support bypassing agent bootstrap using tags" | Write-Debug
return $false
}
return @{
ActivationUrl = "${PlatformServicesBaseUrl}/v2/instance/activate"
FallbackActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/activate"
Name = "vmware"
GetToken = $Function:GetToken
IsAgentDisabled = $Function:IsAgentDisabled
}
}
function Azure-GetMetadata {
$InstanceMetadataUrl = "http://169.254.169.254/metadata/instance/compute?api-version=2021-01-01&format=json"
WriteLog "Making GET request to azure instance metadata url: ${InstanceMetadataUrl}" | Write-Debug
$Params = @{
Uri = $InstanceMetadataUrl
Headers = @{ Metadata = "true" }
UseBasicParsing = $true
}
try {
$Response = Invoke-WebRequest @Params
}
catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "GET request to metadata instance url ${InstanceMetadataUrl} failed: $( $_.Exception.Message )" -ExitCode 120
}
return ConvertFrom-Json $Response.Content
}
function Azure-GetAttestDocument {
$AttestUrl = "http://169.254.169.254/metadata/attested/document?api-version=2021-01-01"
WriteLog "Making GET request to azure instance attest document url: ${InstanceMetadataUrl}" | Write-Debug
$Params = @{
Uri = $AttestUrl
Headers = @{ Metadata = "true" }
UseBasicParsing = $true
}
try {
$Response = Invoke-WebRequest @Params
}
catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "GET request to metadata attest url ${AttestUrl} failed: $( $_.Exception.Message )" -ExitCode 121
}
return ConvertFrom-Json $Response.Content
}
function Azure-GetInstanceTags {
$Tags = @{ }
$Metadata = Azure-GetMetadata
WriteLog "Got Azure metadata: $Metadata" | Write-Debug
foreach ($item in $Metadata.tagsList) {
$Tags[$item.name] = $item.value
}
return $Tags
}
function AzurePlatform {
function GetToken {
$InstanceMetadata = Azure-GetMetadata
$AttestDoc = Azure-GetAttestDocument
$TokenData = @{
instance = @{
subscriptionId = $InstanceMetadata.subscriptionId
location = $InstanceMetadata.location
name = $InstanceMetadata.name
vmId = $InstanceMetadata.vmId
vmScaleSetName = $InstanceMetadata.vmScaleSetName
}
signature = $AttestDoc.signature
encoding = $AttestDoc.encoding
}
return ( ConvertTo-Json -InputObject $TokenData -Compress)
}
function IsAgentDisabled {
WriteLog "Checking Azure metadata for '${IgnoreTagKey}' tag" | Write-Debug
$Tags = Azure-GetInstanceTags
return IsTruthy($Tags[$IgnoreTagKey])
}
return @{
ActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/azure/activate"
FallbackActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/azure/activate"
Name = "azure"
GetToken = $Function:GetToken
IsAgentDisabled = $Function:IsAgentDisabled
}
}
function DedicatedPlatform {
function GetToken {
$TokenFile = $Env:ProgramData + "\rackspace\rackspace_agent\token"
if (!(Test-Path -Path $TokenFile -PathType Leaf)) {
Die "Token file not found: ${TokenFile}. Rackspace dedicated server provisioning is responsible for populating this file." -ExitCode 122
}
$Token = (Get-Content $TokenFile -Raw).Trim()
if (!$Token) {
Die "Token file is empty: ${TokenFile}. Rackspace dedicated server provisioning is responsible for populating this file." -ExitCode 123
}
return $Token
}
function IsAgentDisabled {
WriteLog "Platform 'dedicated' does not support bypassing agent bootstrap using tags" | Write-Debug
return $false
}
return @{
ActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/activate"
FallbackActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/activate"
Name = "dedicated"
GetToken = $Function:GetToken
IsAgentDisabled = $Function:IsAgentDisabled
}
}
function Gcp-GetMetadataAttribute([Parameter(Mandatory = $true)][string]$Key) {
$InstanceMetadataBaseUrl = "http://metadata.google.internal/computeMetadata/v1/instance"
$Url = "${InstanceMetadataBaseUrl}/attributes/${Key}"
$Params = @{
Uri = $Url
Headers = @{ "Metadata-Flavor" = "Google" }
UseBasicParsing = $true
}
Try {
$Response = Invoke-WebRequest @Params
return $Response.Content
}
catch [System.Net.WebException] {
if ($_.Exception.Response.StatusCode -eq 404) {
return $null
}
Die "GET request to instance identity url ${Url} failed: $( $_.Exception.Message )" -ExitCode 125
}
catch [System.Net.Http.HttpRequestException] {
Die "GET request to instance identity url ${Url} failed: $( $_.Exception.Message )" -ExitCode 125
}
}
function GcpPlatform {
function GetToken {
$InstanceMetadataBaseUrl = "http://metadata.google.internal/computeMetadata/v1/instance"
$Url = "${InstanceMetadataBaseUrl}/service-accounts/default/identity?audience=platform.manage.rackspace.com&format=full"
$Params = @{
Uri = $Url
Headers = @{ "Metadata-Flavor" = "Google" }
UseBasicParsing = $true
}
Try {
$Response = Invoke-WebRequest @Params
}
catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "GET request to instance identity url ${Url} failed: $( $_.Exception.Message )" -ExitCode 124
}
return $Response.Content
}
function IsAgentDisabled {
WriteLog "Checking GCP metadata for ${IgnoreTagKey}" | Write-Debug
return IsTruthy(Gcp-GetMetadataAttribute($IgnoreTagKey))
}
return @{
ActivationUrl = "${PlatformServicesBaseUrl}/v1.0/instance/gcp/activate"
Name = "gcp"
GetToken = $Function:GetToken
IsAgentDisabled = $Function:IsAgentDisabled
}
}
function OpenStack-GetTokenFromVendordata {
$GetTokenAttempts = 20
$GetTokenDelay = 60
$VendordataUrl = "http://169.254.169.254/openstack/latest/vendor_data2.json"
try {
foreach ($Attempt in 1..$GetTokenAttempts) {
WriteLog "Getting OpenStack vendordata (attempt ${Attempt}): ${VendordataUrl}" | Write-Debug
try {
$Response = Invoke-WebRequest -Uri $VendordataUrl -UseBasicParsing
$ParsedResponse = ConvertFrom-Json $Response.Content
$Token = $ParsedResponse.platform_services.token
if ($Token) {
break
}
$ErrorMessage = $ParsedResponse.platform_services.error.message
if ($ErrorMessage) {
break
}
} catch {
WriteLog "Could not determine JWT token: $( $_.Exception.Message )" | Write-Debug
}
Start-Sleep -Seconds $GetTokenDelay
}
if ($Token) {
WriteLog "Found OpenStack auth token: ${Token}" | Write-Debug
return $Token
}
if ($ErrorMessage) {
Die "OpenStack vendordata API error: ${ErrorMessage}" -ExitCode 138
}
Die "Could not retrieve token after ${GetTokenAttempt} attempts. The Platform Services API is responsible to provide it as part of vendordata." -ExitCode 137
}
catch {
Die "Failed to get JWT token for OpenStack: $( $_.Exception.Message )" -ExitCode 119
}
}
function OpenStackPlatform {
function GetToken {
return OpenStack-GetTokenFromVendordata
}
function IsAgentDisabled {
# For now we shortcut this, but we can likely support it in a later iteration
WriteLog "Platform 'openstack' does not support bypassing agent bootstrap using tags" | Write-Debug
return $false
}
return @{
ActivationUrl = "${PlatformServicesBaseUrl}/v2/instance/activate"
FallbackActivationUrl = "${PlatformServicesBaseUrl}/v2/instance/activate"
Name = "openstack"
GetToken = $Function:GetToken
IsAgentDisabled = $Function:IsAgentDisabled
}
}
function GetPlatform([Parameter(Mandatory = $true)][string][ValidateSet("Dedicated", "VMWare", "GCP", "Azure", "OpenStackFlex", "OpenStack")]$PlatformName) {
if ($PlatformName -eq 'GCP') {
return GcpPlatform
}
elseif ($PlatformName -eq 'Azure') {
return AzurePlatform
}
elseif ($PlatformName -eq 'Vmware') {
return VmwarePlatform
}
elseif ($PlatformName -eq 'Dedicated') {
return DedicatedPlatform
}
elseif ($PlatformName -eq 'OpenStack') {
return OpenStackPlatform
}
elseif ($PlatformName -eq 'OpenStackFlex') {
WriteLog -Level 'WARN' "OpenStackFlex platform has been deprecated, please use OpenStack instead." |
Write-Warning
return OpenStackPlatform
}
}
function IsSsmPackageInstalled {
if ($PSVersionTable.PSVersion.Major -ge 5) {
try {
WriteLog "Checking if package ${AgentPackageName} is installed using Get-Package" | Write-Debug
Get-Package -Name $AgentPackageName -ProviderName msi
return $true
} catch {
return $false
}
} else {
WriteLog "Checking if package ${AgentPackageName} is installed using Get-WmiObject" | Write-Debug
$result = Get-WmiObject Win32_Product | where { $_.Name -eq $AgentPackageName }
return $result -ne $null
}
}
function DownloadInstaller(
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy,
[Parameter(Mandatory = $false)][AllowNull()][string]$InstallerDownloadRegion,
[Parameter(Mandatory = $false)]$UseCached = $false) {
$TmpDir = $env:TEMP + "\ssm"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
$OutFile = "${TmpDir}\AmazonSSMAgentSetup.exe"
if ($UseCached -and (Test-Path -Path $OutFile -PathType Leaf)) {
WriteLog "Found existing ssm package installer ${OutFile}" | Write-Debug
return $OutFile
}
$AgentPackageUrl = $DefaultAgentPackageUrl
if ($InstallerDownloadRegion) {
$AgentPackageUrl = $RegionalAgentPackageUrl -f $InstallerDownloadRegion
}
$Params = @{
Uri = $AgentPackageUrl
OutFile = $OutFile
UseBasicParsing = $true
}
if ($HttpProxy) {
WriteLog "Using proxy for connection: ${HttpProxy}" | Write-Debug
$Params['Proxy'] = $HttpProxy
}
WriteLog "Downloading agent package installer from ${AgentPackageUrl} to ${OutFile}" | Write-Debug
Try {
Invoke-WebRequest @Params
}
Catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "Downloading agent package installer from ${AgentPackageUrl} failed: $( $_.Exception.Message )" -ExitCode 126
}
return $OutFile
}
function InstallSsmPackage([Parameter(Mandatory = $true)][string]$PackageFile) {
$InstallOptions = @("/norestart", "/q", "/log", "install.log")
WriteLog "Installing agent package: ${PackageFile} $( $InstallOptions -Join " " )" | Write-Debug
Start-Process $PackageFile -ArgumentList $InstallOptions -Wait
if (!(IsSsmPackageInstalled)) {
Die "Package installer failed, check install.log for details" -ExitCode 100
}
}
function UninstallSsmPackage(
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy,
[Parameter(Mandatory = $false)][AllowNull()][string]$InstallerDownloadRegion
) {
# Note: uninstalling via Uninstall-Package still leaves the ProviderName=Programs
# Get-Package -Name $AgentPackageName | Uninstall-Package -AllVersions
$UninstallOptions = @("/norestart", "/uninstall", "/q", "/log", "uninstall.log")
$PackageFile = DownloadInstaller -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion -UseCached $true
WriteLog "Uninstalling agent package: ${PackageFile} $( $UninstallOptions -Join " " )" | Write-Debug
Start-Process $PackageFile -ArgumentList $UninstallOptions -Wait
if (IsSsmPackageInstalled) {
Die "Package uninstaller failed, check uninstall.log for details" -ExitCode 101
}
}
function StartSsmService {
Try {
$Service = Get-Service -Name $SsmService
if ($Service.Status -eq 'Running') {
return $Service
}
WriteLog "Starting agent service: ${SsmService}" | Write-Debug
Start-Service -Name $SsmService
return CheckSsmServiceStatus('Running')
}
Catch {
Die "Start agent service command failed: $( $_.Exception.Message )" -ExitCode 102
}
}
function StopSsmService {
Try {
$Service = Get-Service -Name $SsmService
if ($Service.Status -eq 'Stopped') {
return $Service
}
if ($Service.Status -eq 'StartPending') {
WriteLog "Agent service is in 'StartPending' status, stopping process: $SsmService" | Write-Debug
Stop-Process -Name $SsmProcess -Force
Start-Sleep -Seconds 2
}
WriteLog "Stopping agent service: ${SsmService}" | Write-Debug
Stop-Service -Name $SsmService
return CheckSsmServiceStatus('Stopped')
}
Catch {
Die "Stop agent service command failed: $( $_.Exception.Message )" -ExitCode 103
}
}
function CheckSsmServiceStatus([Parameter(Mandatory = $true)][string]$Status) {
$Attempts = 10
$Delay = 2
foreach ($Attempt in 1..$Attempts) {
WriteLog "Checking that agent service is in ${Status} status (attempt: ${Attempt})" | Write-Debug
$Service = Get-Service -Name $SsmService
if ($Service.Status -eq $Status) {
break
}
WriteLog "Agent service status is: ${Service.Status}, checking again in ${Delay} seconds" | Write-Debug
Start-Sleep -Seconds $Delay
}
if ($Service.Status -eq $Status) {
WriteLog "Agent service has reached ${Status} status" | Write-Debug
return $Service
}
Die "Agent service has not reached a ${Status} status after ${Attempts} attempts. Last status: ${Service.Status}" -ExitCode 104
}
function GetJob([Parameter(Mandatory = $true)][string]$JobUrl,
[Parameter(Mandatory = $true)][string]$Token,
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy) {
function FormatJob([Parameter(Mandatory = $true)][PSObject]$Job) {
return "Job Details:`r`n$( ConvertTo-Json -InputObject $Job -Compress )"
}
$GetJobDelay = 10
$GetJobAttempts = 18
$Params = @{
Uri = $JobUrl
Method = 'GET'
Headers = @{ 'X-Auth-Token' = $Token; 'Accept' = 'application/json' }
UseBasicParsing = $true
}
if ($HttpProxy) {
WriteLog "Using proxy for connection: ${HttpProxy}" | Write-Debug
$Params['Proxy'] = $HttpProxy
}
foreach ($Attempt in 1..$GetJobAttempts) {
WriteLog "Requesting agent activation job (attempt: ${Attempt}): ${JobUrl}" | Write-Debug
Try {
$Response = Invoke-WebRequest @Params
}
Catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
$Message = "GET request to job url ${JobUrl} failed: $( $_.Exception.Message )."
if ($_.Exception.Response) {
$Message = "${Message}`r`n$( DumpResponse $_.Exception.Response )"
}
Die $Message -ExitCode 105
}
$ResponseContent = ConvertFrom-Json $Response.Content
$JobData = $ResponseContent.data
$Job = $JobData.items[0]
if ($Job.status -eq "RUNNING") {
WriteLog "Agent activation job ${JobUrl} is still running, checking again in ${GetJobDelay} seconds" | Write-Debug
Start-Sleep -Seconds $GetJobDelay
}
elseif ($Job.status -eq "SUCCEEDED") {
WriteLog "Agent activation job ${JobUrl} completed successfully.`r`n$( FormatJob $Job )" | Write-Debug
break
}
elseif ($Job.status -ne "SUCCEEDED") {
Die "Agent activation job ${JobUrl} completed unsuccessfully (status: $( Job.status )).`r`n$( FormatJob $Job )" -ExitCode 106
}
}
if ($Job.status -eq "SUCCEEDED") {
return $Job
}
Die "Agent activation job ${JobUrl} did not complete after ${GetJobAttempts}.`r`n`Last $( FormatJob $Job )" -ExitCode 107
}
function DumpResponse([Parameter(Mandatory = $true)][System.Net.HttpWebResponse]$Response) {
$Data = @{
StatusCode = $Response.StatusCode
Headers = $Response.Headers
Content = $Response.Content
}
return "Response:`r`n$( ConvertTo-Json -InputObject $Data -Compress )"
}
function GetActivation([Parameter(Mandatory = $true)][hashtable]$Platform,
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy) {
WriteLog "Getting auth token for agent activation on platform: $( $Platform.Name )" | Write-Debug
$Token = &$Platform.GetToken
$ActivationUrl = $Platform.ActivationUrl
$DecodedToken = Decode-JwtPayload -Token $Token
if ($DecodedToken.urn -eq $null) {
$ActivationUrl = $Platform.FallbackActivationUrl
}
WriteLog "Requesting agent activation: $( $ActivationUrl )" | Write-Debug
$Params = @{
Uri = $ActivationUrl
Method = 'POST'
Headers = @{ 'X-Auth-Token' = $Token; 'Content-Type' = 'application/json'; 'Accept' = 'application/json'; 'User-Agent' = 'Rackspace-SSM-Bootstrap/2.1' }
UseBasicParsing = $true
}
if ($HttpProxy) {
WriteLog "Using proxy for connection: ${HttpProxy}" | Write-Debug
$Params['Proxy'] = $HttpProxy
}
Try {
$Response = Invoke-WebRequest @Params
}
Catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
$Message = "POST request to activation url $( $ActivationUrl ) failed: $( $_.Exception.Message )."
if ($_.Exception.Response) {
$Message = "${Message}`r`n$( DumpResponse $_.Exception.Response )"
}
Die $Message -ExitCode 108
}
if (!($Response.Headers["Location"])) {
Die "POST request to activation url $( $ActivationUrl ) did not return a Location header.`r`n$( DumpResponse $Response )" -ExitCode 109
}
$JobUrl = $Response.Headers["Location"]
WriteLog "Agent activation request responded with job: ${JobUrl}" | Write-Debug
$Job = GetJob -JobUrl $JobUrl -Token $Token -HttpProxy $HttpProxy
return @{
Code = $Job.message.activation_code
Id = $Job.message.activation_id
Region = $Job.message.region
SystemAccount = $Job.message.system_account
}
}
<#
.DESCRIPTION
RegisterSsm - Registers the SSM agent with the activation code and id provided.
If the HttpProxy parameter is provided, it will set the HTTP_PROXY and HTTPS_PROXY environment variables
to the provided value before registering the agent. It will then remove the environment variables after
the agent has been registered. For some reason, the amazon-ssm-agent.exe does not respect the
HKLM:\SYSTEM\CurrentControlSet\Services\AmazonSSMAgent registry settings for proxy config.
#>
function RegisterSsm([Parameter(Mandatory = $true)][hashtable]$Activation, [Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy) {
$AgentExePath = "$AgentProgramDir\amazon-ssm-agent.exe"
try {
if ($HttpProxy) {
$env:HTTP_PROXY = $HttpProxy
$env:HTTPS_PROXY = $HttpProxy
}
Start-process $AgentExePath -ArgumentList @("-register", "-code", $Activation.Code, "-id", $Activation.Id, "-region", $Activation.Region) -Wait
if ($HttpProxy) {
Remove-Item -Path "env:HTTP_PROXY"
Remove-Item -Path "env:HTTPS_PROXY"
}
}
catch {
Die "Agent registration failed: $( $_.Exception.Message )" -ExitCode 110
}
}
function ActivateSsm([Parameter(Mandatory = $true)][hashtable]$Activation, [Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy) {
StopSsmService | Out-Null
RegisterSsm -Activation $Activation -HttpProxy $HttpProxy
StartSsmService | Out-Null
}
function GetSsmDiagnostics {
$SsmCliPath = "$AgentProgramDir\ssm-cli.exe"
if (!(Test-Path -Path $SsmCliPath)) {
Die "${SsmCliPath} command does not exist" -ExitCode 129
}
WriteLog "Running agent diagnostics command: ${SsmCliPath} get-diagnostics" | Write-Debug
$Result = & $SsmCliPath get-diagnostics --output json | Out-String
if ($?) {
return ($Result | ConvertFrom-JSON).DiagnosticsOutput
} else {
Die "Agent diagnostics command failed: ${Result}" -ExitCode 111
}
}
function CheckSsmActivation {
$SsmDiagnostics = GetSsmDiagnostics
$FailedChecks = $SsmDiagnostics | Where-Object { $_.Status -eq "Failed" }
$AgentInfo = GetAgentInformation
if ($FailedChecks) {
WriteLog -Level 'WARN' "Some management agent diagnostics failed, please review:`r`n$( ConvertTo-Json -InputObject $FailedChecks -Compress )" |
Write-Warning
}
if ($AgentInfo) {
Success "Management agent was registered successfully." -Details $( ConvertTo-Json -InputObject $AgentInfo -Compress )
}
else {
$ErrorLogsPath = "${AgentDataDir}/Logs/errors.log"
$ErrorLogs = Get-Content $ErrorLogsPath -Tail 10
Die "Management agent was not registered successfully, display last 10 lines of ${ErrorLogsPath}:`r`n${ErrorLogs}" -ExitCode 112
}
}
function GetAgentInformation {
WriteLog "GetAgentInformation" | Write-Debug
$SsmCliPath = "$AgentProgramDir\ssm-cli.exe"
if (!(Test-Path -Path $SsmCliPath)) {
Die "${SsmCliPath} command does not exist" -ExitCode 113
}
WriteLog "Running agent instance information command: ${SsmCliPath} get-instance-information" | Write-Debug
try {
$Result = & $SsmCliPath get-instance-information | Out-String
if ($?) {
return $Result | ConvertFrom-JSON
} else {
$ErrorLine = $Result.Split([Environment]::NewLine) | ForEach-Object {
if ($_ -match 'error:') {
return $_
}
} | Select-Object -First 1
WriteLog "Agent is not registered: '${ErrorLine}'" | Write-Debug
return $false
}
} catch {
Die "Failed to get agent information: $( $_.Exception )"
}
}
function ClearAgentRegistration {
$SsmAgentPath = "${AgentProgramDir}\amazon-ssm-agent.exe"
if (!(Test-Path -Path $SsmAgentPath)) {
Die "${SsmAgentPath} command does not exist" -ExitCode 127
}
WriteLog "Running agent registration clear command: ${SsmAgentPath} -register -clear" | Write-Debug
$Result = & $SsmAgentPath -register -clear
if ($?) {
WriteLog "Successfully cleared agent registration" | Write-Debug
return $true
}
Die "Agent registration clear command failed: ${Result}" -ExitCode 114
}
function GetOsProductType {
return Get-CimInstance Win32_OperatingSystem | Select-Object -expand ProductType
}
function GetOsName {
return Get-CimInstance Win32_OperatingSystem | Select-Object -expand Caption
}
function CheckOsSupported {
$ProductType = GetOsProductType
$IsServer = $ProductType -eq 3
$IsDomainController = $ProductType -eq 2
$OsName = GetOsName
$IsVersionSupported = $null -ne ($SupportedWindowsVersions | Where-Object { $OsName -match $_ })
if (!($IsServer -or $IsDomainController)) {
Die "The Rackspace management agent only supports Windows Server or Windows Domain Controllers, it does not support product type: ${ProductType}." -ExitCode 133
}
if (!($IsVersionSupported)) {
Die "The Rackspace management agent supports Windows versions [$( $SupportedWindowsVersions -join "," )], it does not support ${OsName}." -ExitCode 134
}
if (!([Environment]::Is64BitOperatingSystem)) {
Die "The Rackspace management agent only supports 64-bit versions of Windows." -ExitCode 135
}
}
function CheckPwshVersion {
if ($PSVersionTable.PSVersion.Major -lt 3) {
Die "This script requires powershell version > = 3" -ExitCode 143
}
}
function ConfigureProxy([parameter(Mandatory = $true)][string]$HttpProxy) {
$ServiceKey = "HKLM:\SYSTEM\CurrentControlSet\Services\AmazonSSMAgent"
$KeyInfo = (Get-Item -Path $ServiceKey).GetValue("Environment")
$ProxyVariables = @("http_proxy=${HttpProxy}", "https_proxy=${HttpProxy}", "no_proxy=169.254.169.254")
if ($null -eq $KeyInfo) {
New-ItemProperty -Path $ServiceKey -Name Environment -Value $ProxyVariables -PropertyType MultiString -Force
}
else {
Set-ItemProperty -Path $ServiceKey -Name Environment -Value $ProxyVariables
}
}
function ConfigureSsmProfile {
$TemplateFile = "$AgentProgramDir\amazon-ssm-agent.json.template"
$ConfigFile = "$AgentProgramDir\amazon-ssm-agent.json"
$IsProfileUpdated = $false
try {
# If config file doesn't exist, create it from template
if (!(Test-Path -Path $ConfigFile)) {
if (Test-Path -Path $TemplateFile) {
WriteLog "Creating SSM agent configuration file from template" | Write-Debug
Copy-Item -Path $TemplateFile -Destination $ConfigFile
}
else {
WriteLog "SSM agent template configuration file not found: ${TemplateFile}" | Write-Debug
return $IsProfileUpdated
}
}
# Read and update the configuration file
$Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
# Ensure Profile section exists
if (-not $Config.PSObject.Properties.Name.Contains('Profile')) {
$Config | Add-Member -Type NoteProperty -Name 'Profile' -Value @{}
}
# Ensure ShareProfile property exists in Profile section
if (-not $Config.Profile.PSObject.Properties.Name.Contains('ShareProfile')) {
$Config.Profile | Add-Member -Type NoteProperty -Name 'ShareProfile' -Value ''
}
# Update the ShareProfile value only if it's different
if ($Config.Profile.ShareProfile -ne "rackspace") {
$Config.Profile.ShareProfile = "rackspace"
$IsProfileUpdated = $true
}
# Write the updated configuration only if changes were made
if ($IsProfileUpdated) {
$Config | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
WriteLog "Updated SSM agent configuration to use 'rackspace' profile" | Write-Debug
}
else {
WriteLog "SSM agent configuration already has 'rackspace' profile" | Write-Debug
}
}
catch {
Write-Error "Failed to update SSM agent configuration: $( $_.Exception.Message )"
}
return $IsProfileUpdated
}
function Install(
[Parameter(Mandatory = $true)][string][ValidateSet("Dedicated", "VMWare", "GCP", "Azure", "OpenStackFlex", "OpenStack")]$PlatformName,
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy,
[Parameter(Mandatory = $false)][AllowNull()][string]$InstallerDownloadRegion
) {
$Platform = GetPlatform -PlatformName $PlatformName
WriteLog "Executing install command on platform: $( $Platform.Name )" | Write-Debug
Try {
if (&$Platform.IsAgentDisabled) {
WriteLog "Found a truthy value for '${IgnoreTagKey}' in $( $Platform.Name ) metadata, skipping agent installation" | Write-Debug
Exit 0
}
else {
WriteLog "Agent install is enabled, '${IgnoreTagKey}' not found in metadata" | Write-Debug
}
if (!(IsSsmPackageInstalled)) {
$PackageFile = DownloadInstaller -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion
InstallSsmPackage -PackageFile $PackageFile
}
# Configure the SSM profile
$IsProfileUpdated = ConfigureSsmProfile
if ($HttpProxy) {
WriteLog "Using proxy for connection: ${HttpProxy}" | Write-Debug
ConfigureProxy -HttpProxy $HttpProxy
}
$Registration = GetAgentInformation
if ($Registration) {
WriteLog "Agent is already registered $( ConvertTo-Json -InputObject $Registration -Compress )" | Write-Debug
if ($IsProfileUpdated) {
StopSsmService | Out-Null
}
StartSsmService | Out-Null
CheckSsmActivation
Exit 0
}
$Activation = GetActivation -Platform $Platform -HttpProxy $HttpProxy
ActivateSsm -Activation $Activation -HttpProxy $HttpProxy
WriteLog "Waiting ${RegistrationWait} seconds before checking agent activation status" | Write-Debug
Start-Sleep -Seconds $RegistrationWait
CheckSsmActivation
}
Catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "HTTP request failed: $( $_.Exception.Message )" -ExitCode 115
}
}
function Reregister([Parameter(Mandatory = $true)][string][ValidateSet("Dedicated", "VMWare", "GCP", "Azure", "OpenStack")]$PlatformName,
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy) {
WriteLog "Executing reregister command on platform ${PlatformName}" | Write-Debug
$Platform = GetPlatform -PlatformName $PlatformName
Try {
if (&$Platform.IsAgentDisabled) {
WriteLog "Found a truthy value for '${IgnoreTagKey}' in $( $Platform.Name ) metadata, skipping agent installation" | Write-Debug
Exit 0
}
else {
WriteLog "Agent install is enabled, '${IgnoreTagKey}' not found in metadata" | Write-Debug
}
if (!(IsSsmPackageInstalled)) {
Die "SSM package is not installed, cannot reregister agent" -ExitCode 128
}
StopSsmService | Out-Null
ClearAgentRegistration | Out-Null
$Activation = GetActivation -Platform $Platform -HttpProxy $HttpProxy
ActivateSsm -Activation $Activation -HttpProxy $HttpProxy
WriteLog "Waiting ${RegistrationWait} seconds before checking agent activation status" | Write-Debug
Start-Sleep -Seconds $RegistrationWait
CheckSsmActivation
}
Catch [System.Net.Http.HttpRequestException], [System.Net.WebException] {
Die "HTTP request failed: $( $_.Exception.Message )" -ExitCode 117
}
}
function Uninstall(
[Parameter(Mandatory = $false)][AllowNull()][string]$HttpProxy,
[Parameter(Mandatory = $false)][AllowNull()][string]$InstallerDownloadRegion
) {
WriteLog "Executing uninstall command" | Write-Debug
if (!(IsSsmPackageInstalled)) {
Die "Package $SsmService is not currently installed" -ExitCode 116
}
StopSsmService
ClearAgentRegistration | Out-Null
UninstallSsmPackage -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion
Success "Agent was successfully uninstalled"
}
function Main {
Param (
[Parameter(Mandatory = $true)][string]$Platform,
[Parameter(Mandatory = $true)][string]$Command,
[Parameter(Mandatory = $false)][string]$HttpProxy,
[Parameter(Mandatory = $false)][string]$InstallerDownloadRegion
)
process {
Try {
CheckOsSupported
CheckPwshVersion
if ($Command -eq "Install") {
Install -PlatformName $Platform -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion
}
elseif ($Command -eq "Uninstall") {
Uninstall -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion
}
elseif ($Command -eq "Reregister") {
Reregister -PlatformName $Platform
}
}
Catch {
$err = $_
Write-Error ($err.Exception | Format-List -Force | Out-String) -ErrorAction Continue
Die "Uncaught exception: [$( $err.Exception.getType().fullname )] $( $err.Exception.Message ), exiting..." -ExitCode 144
}
}
}
if ($MyInvocation.InvocationName -ne ".") {
Write-Output($MyInvocation.InvocationName)
Main -Command $Command -Platform $Platform -HttpProxy $HttpProxy -InstallerDownloadRegion $InstallerDownloadRegion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment