train-awsssm 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +9 -10
- data/lib/train-awsssm/connection.rb +158 -42
- data/lib/train-awsssm/transport.rb +1 -0
- data/lib/train-awsssm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef5a20eca471f5a767da71380b22425fd4676d3a435b0da83077cd3d14fc2c2f
|
4
|
+
data.tar.gz: 97f8b4e0d98b1a20f23b58df148696dceda51843da9fdacd51b274fbf3c94af1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19cb91506eb8d0b5db6eeb49f6e94ab071616dca46ebeeac187065958a954d8ef9c2e7a80c2e3b72de3d914f252132e25f75e6b2b43e08467fe9eddb2c230b54
|
7
|
+
data.tar.gz: c81a2c12459fc95e44c451cf7be89f1c1723ce9820c2790e7963b2676384a8623234f84657d4ddbce8316a03926ee73402bc5640bb5b01cd2d33dfcd3c15060e
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## Version 0.2.0
|
6
|
+
|
7
|
+
- Add more parameter and status checks
|
8
|
+
- Add InSpec support by properly handling failing commands
|
9
|
+
- Add Windows support
|
10
|
+
- Refactor code
|
11
|
+
|
5
12
|
## Version 0.1.1
|
6
13
|
|
7
14
|
- Add support for passing in an instance-id instead of IP/DNS
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# train-awsssm - Train Plugin for using AWS Systems Manager Agent
|
2
2
|
|
3
|
-
This plugin allows applications that rely on Train to communicate via AWS SSM with Linux instances.
|
4
|
-
|
5
|
-
Windows is currently not yet supported
|
3
|
+
This plugin allows applications that rely on Train to communicate via AWS SSM with Linux/Windows instances.
|
6
4
|
|
7
5
|
## Requirements
|
8
6
|
|
@@ -10,7 +8,7 @@ The instance in question must run on AWS and you need to have all AWS credential
|
|
10
8
|
|
11
9
|
You need the [SSM agent to be installed](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) on the machine (most current AMIs already have this integrated) and the machine needs to have the managed policy `AmazonSSMManagedInstanceCore` or a least privilege equivalent attached as IAM profile.
|
12
10
|
|
13
|
-
Commands will be executed under the `root`
|
11
|
+
Commands will be executed under the `root`/`Administrator` users.
|
14
12
|
|
15
13
|
## Installation
|
16
14
|
|
@@ -20,12 +18,13 @@ You can build and install this gem on your local system as well via a Rake task:
|
|
20
18
|
|
21
19
|
## Transport parameters
|
22
20
|
|
23
|
-
| Option | Explanation
|
24
|
-
| -------------------- |
|
25
|
-
| `host` | IP, DNS name or EC2 ID of instance
|
26
|
-
| `
|
27
|
-
| `
|
28
|
-
| `
|
21
|
+
| Option | Explanation | Default |
|
22
|
+
| -------------------- | ------------------------------------------------- | ---------------- |
|
23
|
+
| `host` | IP, DNS name or EC2 ID of instance | (required) |
|
24
|
+
| `mode` | Mode for connection, only 'run-command' currently | run-command |
|
25
|
+
| `execution_timeout` | Maximum time until timeout | 60 |
|
26
|
+
| `recheck_invocation` | Interval of rechecking AWS command invocation | 1.0 |
|
27
|
+
| `recheck_execution` | Interval of rechecking completion of command | 1.0 |
|
29
28
|
|
30
29
|
## Limitations
|
31
30
|
|
@@ -6,24 +6,25 @@ require "train"
|
|
6
6
|
module TrainPlugins
|
7
7
|
module AWSSSM
|
8
8
|
class Connection < Train::Plugins::Transport::BaseConnection
|
9
|
+
attr_reader :instance_id, :options
|
10
|
+
attr_writer :ssm, :ec2
|
11
|
+
|
9
12
|
def initialize(options)
|
10
13
|
super(options)
|
11
14
|
|
12
15
|
check_options
|
13
|
-
|
14
|
-
@ssm = Aws::SSM::Client.new
|
15
16
|
end
|
16
17
|
|
17
18
|
def close
|
18
|
-
logger.info format("[AWS-SSM] Closed connection to %s",
|
19
|
+
logger.info format("[AWS-SSM] Closed connection to %s", options[:host])
|
19
20
|
end
|
20
21
|
|
21
22
|
def uri
|
22
|
-
"aws-ssm://#{
|
23
|
+
"aws-ssm://#{options[:host]}/"
|
23
24
|
end
|
24
25
|
|
25
26
|
def run_command_via_connection(cmd, &data_handler)
|
26
|
-
logger.info format("[AWS-SSM] Sending command to %s",
|
27
|
+
logger.info format("[AWS-SSM] Sending command to %s", options[:host])
|
27
28
|
exit_status, stdout, stderr = execute_on_channel(cmd, &data_handler)
|
28
29
|
|
29
30
|
CommandResult.new(stdout, stderr, exit_status)
|
@@ -32,7 +33,7 @@ module TrainPlugins
|
|
32
33
|
def execute_on_channel(cmd, &data_handler)
|
33
34
|
logger.debug format("[AWS-SSM] Command: '%s'", cmd)
|
34
35
|
|
35
|
-
result = execute_command(
|
36
|
+
result = execute_command(options[:host], cmd)
|
36
37
|
|
37
38
|
stdout = result.standard_output_content || ""
|
38
39
|
stderr = result.standard_error_content || ""
|
@@ -43,26 +44,47 @@ module TrainPlugins
|
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
47
|
+
# Return Systems Manager API client
|
48
|
+
#
|
49
|
+
# @return Aws::SSM::Client
|
50
|
+
def ssm
|
51
|
+
@ssm ||= ::Aws::SSM::Client.new
|
49
52
|
end
|
50
53
|
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
+
# Return EC2 API client
|
55
|
+
#
|
56
|
+
# @return Aws::EC2::Client
|
57
|
+
def ec2
|
58
|
+
@ec2 ||= ::Aws::EC2::Client.new
|
54
59
|
end
|
55
60
|
|
56
|
-
# Check if
|
57
|
-
|
58
|
-
|
61
|
+
# Check if options are as needed
|
62
|
+
#
|
63
|
+
# @raise [ArgumentError] if any options were incorrectly configured
|
64
|
+
def check_options
|
65
|
+
unless options[:host]
|
66
|
+
raise ArgumentError, format("Missing required option :host for train-awsssm")
|
67
|
+
end
|
68
|
+
|
69
|
+
unless supported_modes.include? options[:mode]
|
70
|
+
raise ArgumentError, format("Wrong mode `%s`, supported: %s", options[:mode], supported_modes.join(", "))
|
71
|
+
end
|
72
|
+
|
73
|
+
address = options[:host]
|
74
|
+
@instance_id = address.start_with?("i-") ? address : resolve_instance_id(address)
|
75
|
+
|
76
|
+
raise ArgumentError, format("Instance %s is not running", instance_id) unless instance_running?
|
77
|
+
raise ArgumentError, format("Instance %s is not managed by SSM or agent unreachable", instance_id) unless managed_instance?
|
59
78
|
end
|
60
79
|
|
61
80
|
# Resolve EC2 instance ID associated with a primary IP or a DNS entry
|
62
|
-
|
81
|
+
#
|
82
|
+
# @param [String] address Host or IP address
|
83
|
+
# @return [String] Instance ID, if any
|
84
|
+
# @raise [ArgumentError] if instance could not be resolved from address
|
85
|
+
def resolve_instance_id(address)
|
63
86
|
logger.debug format("[AWS-SSM] Trying to resolve address %s", address)
|
64
87
|
|
65
|
-
ec2 = Aws::EC2::Client.new
|
66
88
|
instances = ec2.describe_instances.reservations.collect { |r| r.instances.first }
|
67
89
|
|
68
90
|
# Resolve, if DNS name and not Amazon default
|
@@ -81,75 +103,169 @@ module TrainPlugins
|
|
81
103
|
].include?(address)
|
82
104
|
end&.instance_id
|
83
105
|
|
84
|
-
raise format("Could not resolve instance ID for address %s", address) if id.nil?
|
106
|
+
raise ArgumentError, format("Could not resolve instance ID for address %s", address) if id.nil?
|
85
107
|
|
86
108
|
logger.debug format("[AWS-SSM] Resolved address %s to instance ID %s", address, id)
|
109
|
+
|
87
110
|
id
|
111
|
+
rescue ::Aws::Errors::ServiceError => e
|
112
|
+
raise ArgumentError, format("Error looking up Instance ID for %s: %s", address, e.message)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if this is an IP address
|
116
|
+
#
|
117
|
+
# @param [String] address Host, IP address or other input
|
118
|
+
# @return [Boolean] If it is an IPv4 address
|
119
|
+
def ip_address?(address)
|
120
|
+
!!(address =~ Resolv::IPv4::Regex)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Check if this is a DNS name
|
124
|
+
#
|
125
|
+
# @param [String] address Host, IP address or other input
|
126
|
+
# @return [Boolean] If it is a DNS name
|
127
|
+
def dns_name?(address)
|
128
|
+
!ip_address?(address)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check if this is an internal/external AWS DNS entry
|
132
|
+
#
|
133
|
+
# @param [String] address Host, IP address or other input
|
134
|
+
# @return [Boolean] If it is an Amazon-provided DNS name
|
135
|
+
def amazon_dns?(dns)
|
136
|
+
dns_name?(dns) && (dns.end_with?(".compute.amazonaws.com") || dns.end_with?(".compute.internal"))
|
88
137
|
end
|
89
138
|
|
90
139
|
# Request a command invocation and wait until it is registered with an ID
|
91
|
-
|
92
|
-
|
140
|
+
#
|
141
|
+
# @param [String] command_id Command ID from SSM
|
142
|
+
def wait_for_invocation(command_id)
|
143
|
+
invocation_result(command_id)
|
93
144
|
|
94
145
|
# Retry until the invocation was created on AWS
|
95
|
-
rescue Aws::SSM::Errors::InvocationDoesNotExist
|
96
|
-
sleep
|
146
|
+
rescue ::Aws::SSM::Errors::InvocationDoesNotExist
|
147
|
+
sleep options[:recheck_invocation]
|
97
148
|
retry
|
98
149
|
end
|
99
150
|
|
100
151
|
# Return the result of a given command invocation
|
101
|
-
|
102
|
-
|
152
|
+
#
|
153
|
+
# @param [String] command_id Command ID from SSM
|
154
|
+
# @return [Aws::SSM::Types::GetCommandInvocationResult] Invocation result
|
155
|
+
def invocation_result(command_id)
|
156
|
+
ssm.get_command_invocation(instance_id: instance_id, command_id: command_id)
|
103
157
|
end
|
104
158
|
|
105
159
|
# Return if a non-terminal command status was given
|
160
|
+
#
|
161
|
+
# @param [String] name status from invocation
|
162
|
+
# @return [Boolean] If execution is still in progress
|
106
163
|
# @see https://docs.aws.amazon.com/systems-manager/latest/userguide/monitor-commands.html
|
107
164
|
def in_progress?(name)
|
108
165
|
%w{Pending InProgress Delayed}.include? name
|
109
166
|
end
|
110
167
|
|
111
168
|
# Return if a terminal command status was given
|
169
|
+
#
|
170
|
+
# @param [String] name status from invocation
|
171
|
+
# @return [Boolean] If execution is finished, aborted or timed out
|
112
172
|
# @see https://docs.aws.amazon.com/systems-manager/latest/userguide/monitor-commands.html
|
113
173
|
def terminal_state?(name)
|
114
174
|
!in_progress?(name)
|
115
175
|
end
|
116
176
|
|
117
177
|
# Execute a command via SSM
|
178
|
+
#
|
179
|
+
# @param [String] address IP, Host or Instance ID
|
180
|
+
# @param [String] command Command to execute
|
181
|
+
# @return [Aws::SSM::Types::GetCommandInvocationResult] Invocation result
|
182
|
+
# @raise [ArgumentError] if instance is not reachable
|
183
|
+
# @raise [RuntimeError] if execution failed or timed out
|
118
184
|
def execute_command(address, command)
|
119
|
-
|
120
|
-
address
|
121
|
-
else
|
122
|
-
instance_id(address)
|
123
|
-
end
|
185
|
+
ssm_document = windows_instance? ? "AWS-RunPowerShellScript" : "AWS-RunShellScript"
|
124
186
|
|
125
|
-
cmd =
|
187
|
+
cmd = ssm.send_command(instance_ids: [instance_id], document_name: ssm_document, parameters: { "commands": [command] })
|
126
188
|
cmd_id = cmd.command.command_id
|
127
189
|
|
128
|
-
wait_for_invocation(
|
190
|
+
wait_for_invocation(cmd_id)
|
129
191
|
logger.debug format("[AWS-SSM] Execution ID %s", cmd_id)
|
130
192
|
|
131
193
|
start_time = Time.now
|
132
|
-
result = invocation_result(
|
194
|
+
result = invocation_result(cmd.command.command_id)
|
133
195
|
|
134
|
-
until terminal_state?(result.status) || Time.now - start_time >
|
135
|
-
result = invocation_result(
|
136
|
-
sleep
|
196
|
+
until terminal_state?(result.status) || Time.now - start_time > options[:execution_timeout]
|
197
|
+
result = invocation_result(cmd.command.command_id)
|
198
|
+
sleep options[:recheck_execution]
|
137
199
|
end
|
138
200
|
|
139
|
-
if Time.now - start_time >
|
201
|
+
if Time.now - start_time > options[:execution_timeout]
|
140
202
|
raise format("Timeout waiting for execution")
|
141
|
-
elsif result.status
|
203
|
+
elsif !%w{Success Failed}.include? result.status
|
204
|
+
# Failing commands is normal for InSpec
|
142
205
|
raise format('Execution failed with state "%s": %s', result.status, result.standard_error_content || "unknown")
|
143
206
|
end
|
144
207
|
|
145
208
|
result
|
146
209
|
end
|
147
210
|
|
148
|
-
# Check if
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
211
|
+
# Check if instance is Windows based.
|
212
|
+
# Could also use the `train.connection.platform` mechanics, but they are very slow.
|
213
|
+
#
|
214
|
+
# @return [Boolean] If this is a Windows instance
|
215
|
+
def windows_instance?
|
216
|
+
ec2_instance_data.platform == "windows"
|
217
|
+
end
|
218
|
+
|
219
|
+
# Check if instance is running.
|
220
|
+
#
|
221
|
+
# @param [String] instance_id EC2 instance ID
|
222
|
+
# @return [Boolean] If the instance is currently running
|
223
|
+
def instance_running?
|
224
|
+
ec2_instance_data.state.name == "running"
|
225
|
+
end
|
226
|
+
|
227
|
+
# Check if instance is reachable via SSM.
|
228
|
+
#
|
229
|
+
# @param [String] instance_id EC2 instance ID
|
230
|
+
# @return [Boolean] If the instance is reachable
|
231
|
+
def managed_instance?
|
232
|
+
instance = ssm_instance_data
|
233
|
+
return false unless instance
|
234
|
+
|
235
|
+
instance.ping_status == "Online"
|
236
|
+
end
|
237
|
+
|
238
|
+
# Get instance data from SSM
|
239
|
+
#
|
240
|
+
# @param [String] instance_id EC2 instance ID
|
241
|
+
# @return [Aws::SSM::Types::InstanceInformation] Available SSM instance data
|
242
|
+
# @raise [ArgumentError] if instance ID could not be found
|
243
|
+
def ssm_instance_data
|
244
|
+
response = ssm.describe_instance_information(filters: [{ key: "InstanceIds", values: [instance_id] }])
|
245
|
+
|
246
|
+
response.instance_information_list&.first
|
247
|
+
rescue ::Aws::Errors::ServiceError => e
|
248
|
+
raise ArgumentError, format("Error looking up SSM-managed instance %s: %s", instance_id, e.message)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Get instance data from EC2
|
252
|
+
#
|
253
|
+
# @param [String] instance_id EC2 instance ID
|
254
|
+
# @return [Aws::EC2::Types::Instance] Available instance data
|
255
|
+
# @raise [ArgumentError] if instance ID could not be found
|
256
|
+
def ec2_instance_data
|
257
|
+
instances = ec2.describe_instances(instance_ids: [instance_id])
|
258
|
+
|
259
|
+
instances.reservations.first.instances.first
|
260
|
+
rescue ::Aws::Errors::ServiceError => e
|
261
|
+
raise ArgumentError, format("Error looking up Instance %s: %s", instance_id, e.message)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Supported run modes.
|
265
|
+
#
|
266
|
+
# @return [Array<String>] Supported modes
|
267
|
+
def supported_modes
|
268
|
+
%w{run-command}
|
153
269
|
end
|
154
270
|
end
|
155
271
|
end
|
data/lib/train-awsssm/version.rb
CHANGED