train-awsssm 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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