Private/SSM/Invoke-SSMCommandScript.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
function Invoke-SSMCommandScript
{
<#
    .SYNOPSIS
        Run sripts on hosts using SSM AWS-RunPowerShellScript or AWS-RunShellScript.

    .DESCRIPTION
        Run sripts on hosts using SSM AWS-RunPowerShellScript or AWS-RunShellScript.

    .PARAMETER ScriptType
        Type of script. Instances will be checked for correct platform prior to submission.

    .PARAMETER InstanceIds
        List of instance IDs identifying instances to run the script on.

    .PARAMETER AsJson
        If set, attempt to parse command output as a JSON string and convert to an object.

    .PARAMETER AsText
        Print command output from each instance to the console

    .PARAMETER UseS3
        SSM truncates results to 2000 characters. If you expect results to exceed this, then this switch
        instructs SSM to send the results to S3. The cmdlet will retrieve these results and return them.

    .PARAMETER ScriptBlock
        ScriptBlock containing the script to run.

    .PARAMETER CommandText
        String containing commands to run

    .PARAMETER ExecutionTimeout
         The time in seconds for a command to be completed before it is considered to have failed. Default is 3600 (1 hour). Maximum is 172800 (48 hours).

    .PARAMETER Deliverytimeout
        The time in seconds for a command to be delivered to a target instance. Default is 600 (10 minutes).

    .OUTPUTS
        [PSObject], none
        If -AsText specified, then none
        Else
        List of PSObject, one per instance containing the following fields
        - InstanceId Instance for which this result pertains to
        - ResultObject If -AsJson and the result was successfully parsed, then an object else NULL
        - ResultText Standard Output returned by the script (Write-Host etc.)

    .NOTES
        aws-toolbox uses a working bucket for passing results through S3 which will be created if not found.
        Format of bucket name is aws-toolbox-workspace-REGIONNAME-AWSACCOUNTID

    .EXAMPLE
        Invoke-ATSSMPowerShellScript -InstanceIds ('i-00000001', 'i-00000002') -ScriptBlock { net user me mypassword /add ; net localgroup Administrators me /add }
        Creates a windows user and adds to local administrators group on given instances

    .EXAMPLE
        Invoke-ATSSMPowerShellScript -InstanceIds ('i-00000001', 'i-00000002') -AsJson -ScriptBlock { Invoke-RestMethod http://localhost/status | ConvertTo-Json }
        Calls a local rest service, returning a JSON string and parse the result back into an object.

    .EXAMPLE
        Invoke-ATSSMPowerShellScript -InstanceIds i-00000000 -AsText { dir c:\ }
        Returns directory listing from remote instance to the console.
#>

    [CmdletBinding(DefaultParameterSetName = 'AsText')]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateSet('WindowsPowerShell', 'Shell')]
        [string[]]$ScriptType,

        [Parameter(Mandatory=$true)]
        [string[]]$InstanceIds,

        [Parameter(Mandatory=$true, Position = 0)]
        [object]$CommandText,

        [Parameter(ParameterSetName = 'AsJson')]
        [switch]$AsJson,

        [Parameter(ParameterSetName = 'AsText')]
        [switch]$AsText,

        [switch]$UseS3,

        [int]$ExecutionTimeout = 3600,

        [int]$DeliveryTimeout = 600
    )

    if ($UseS3)
    {
        $s3Bucket = Get-WorkspaceBucket
        $s3KeyPrefix = 'ssm-run-command/'
    }

    $ssmCommands = $(
        if ($CommandText -is [scriptblock])
        {
            $CommandText.ToString() -split [Environment]::NewLine
        }
        elseif ($CommandText -is [string])
        {
            $CommandText -split [Environment]::NewLine
        }
        else
        {
            throw "SSM command must be string or scriptblock, not $($CommandText.GetType().Name)"
        }
    )

    $InstanceIds = $InstanceIds |
    Where-Object {
        $null -ne $_
    }

    if (($InstanceIds | Measure-Object).Count -eq 0)
    {
        Write-Warning "No instances specified!"
        return
    }

    $instanceTypes = Get-SSMEnabledInstances -InstanceId $InstanceIds
    $documentName = $null

    switch ($ScriptType)
    {
        'WindowsPowerShell'
        {
            if (-not $instanceTypes.Windows)
            {
                Write-Warning "None of specified instances are Windows, ready and SSM enabled."
                return
            }

            $InstanceIds = $instanceTypes.Windows
            $documentName = 'AWS-RunPowerShellScript'
        }

        'Shell'
        {
            if (-not $instanceTypes.NonWindows)
            {
                Write-Warning "None of specified instances are Linux/Unix, ready and SSM enabled."
                return
            }

            $InstanceIds = $instanceTypes.NonWindows
            $documentName = 'AWS-RunShellScript'
        }
    }


    if (($InstanceIds | Measure-Object).Count -gt 50)
    {
        $instanceGroups = Split-Array -Array $InstanceIds -Size 50
    }
    else
    {
        $instanceGroups = @()
        $instanceGroups += ,@($InstanceIds)
    }

    $InstanceGroups |
        ForEach-Object {

        $instanceGroup = $_

        # Build SSM command structure
        $runCommandParams = @{

            DocumentName   = $documentName
            InstanceId     = $instanceGroup
            TimeoutSeconds = $DeliveryTimeout
            Parameter      = @{

                workingDirectory = [string]::Empty
                executionTimeout = $ExecutionTimeout.ToString()
                commands         = $ssmCommands
            }
        }

        if ($UseS3)
        {
            $runCommandParams.Add('OutputS3BucketName', $s3Bucket.BucketName)
            $runCommandParams.Add('OutputS3KeyPrefix', $s3KeyPrefix)
        }

        if ($instanceGroup.Length -gt 4)
        {
            $sb = New-Object System.Text.StringBuilder
            $sb.AppendLine('Sending SSM command to:') | Out-Null
            $s = Split-Array -Array $instanceGroup -Size 4
            $s |
            Foreach-Object {
                $sb.AppendLine(" $($_ -join ', ')") | Out-Null
            }

            Write-Host $sb.ToString()
        }
        else
        {
            Write-Host "Sending SSM command to $($instanceGroup -join ', ')"
        }

        $cmd = Send-SSMCommand @runCommandParams

        Write-Host "Submitted command with ID $($cmd.CommandId) and waiting for status..."

        while (('Pending', 'InProgress') -icontains $cmd.Status)
        {
            Start-Sleep -Seconds 5
            $cmd = Get-SSMCommand -CommandId $cmd.CommandId
        }

        if ($cmd.Status -ine 'Success')
        {
            if ($cmd.StatusDetails)
            {
                Write-Warning "The command did not complete successfully. Status is $($cmd.StatusDetails)."
            }
            else
            {
                Write-Warning  "The command did not complete successfully."
            }
        }

        if ($UseS3)
        {
            Write-Host "Collecting results from S3..."
        }

        # Collect results
        $instanceGroup |
        Foreach-Object {

            $instanceId = $_

            if ($UseS3)
            {
                $invocation = Get-SSMCommandInvocation -CommandId $cmd.CommandId -InstanceId $instanceId

                $detail = New-Object PSObject -Property @{
                    StandardOutputContent = Get-ContentFromS3 -S3Url $invocation.StandardOutputUrl
                    StandardErrorContent = Get-ContentFromS3 -S3Url $invocation.StandardErrorUrl -ExpectContent (-not ($invocation.Status -ieq 'Success'))
                }
            }
            else
            {
                $detail = Get-SSMCommandInvocationDetail -CommandId $cmd.CommandId -InstanceId $instanceId
            }

            $obj = $null

            if ($AsJson)
            {
                try
                {
                    $obj = $detail.StandardOutputContent | ConvertFrom-Json
                }
                catch
                {
                    $obj = $null
                }
            }

            if ($AsText)
            {
                "----------- Instance $instanceId ----------- "

                if (-not ([string]::IsNullOrEmpty($detail.StandardOutputContent)))
                {
                    ""
                    $detail.StandardOutputContent
                    ""
                }

                if (-not ([string]::IsNullOrEmpty($detail.StandardErrorContent)))
                {
                    # Error output always to console
                    Write-Host
                    Write-Host "-------- Instance $instanceId Errors -------- "
                    Write-Host
                    Write-Host -ForegroundColor Red $detail.StandardErrorContent
                    Write-Host
                }
            }
            else
            {
                New-Object PSObject -Property @{
                    InstanceId = $instanceId
                    ResultObject = $obj
                    Stdout = $detail.StandardOutputContent
                    Stderr = $detail.StandardErrorContent
                }
            }
        }
    }
}