train-awsssm 0.1.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 +7 -0
- data/Gemfile +10 -0
- data/README.md +47 -0
- data/lib/train-awsssm.rb +7 -0
- data/lib/train-awsssm/connection.rb +143 -0
- data/lib/train-awsssm/transport.rb +19 -0
- data/lib/train-awsssm/version.rb +5 -0
- data/train-awsssm.gemspec +27 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 87fb6b7111f611cc31aea97504e8617c055a696bd5f60b64a4a684454436d83a
|
4
|
+
data.tar.gz: d9756ffccc53b4e438b7f5e8acc5b11d2c477d20360531bf2829e7a3e472b3e4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d080f63b75929c5e8b66874d0eb0320a3eb8e455866823de24f613712ec22e50337631fd104ae15530c506df8b12bbec4b58fa4b20aafed728fb20da67ed4fa9
|
7
|
+
data.tar.gz: '028d2b8807d11ec343186344ee781736d17a34353fac07cc07be36fda94b6384f7a11f0de6bcd771daec7c676e6cf52b98564039e655ef9090fca889d1f8da33'
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# train-awsssm - Train Plugin for using AWS Systems Manager Agent
|
2
|
+
|
3
|
+
This plugin allows applications that rely on Train to communicate via AWS SSM.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
The instance in question must run on AWS and you need to have all AWS credentials
|
8
|
+
set up for the shell which executes the command. Please check the [AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)
|
9
|
+
for appropriate configuration files and environment variables.
|
10
|
+
|
11
|
+
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
|
12
|
+
have this integrated) and the machine needs to have the managed policy
|
13
|
+
`AmazonSSMManagedInstanceCore` or a least privilege equivalent attached as
|
14
|
+
IAM profile.
|
15
|
+
|
16
|
+
Commands will be executed under the `ssm-user` user.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
You will have to build this gem yourself to install it as it is not yet on
|
21
|
+
Rubygems.Org. For this there is a rake task which makes this a one-liner:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
rake install:local
|
25
|
+
```
|
26
|
+
|
27
|
+
## Transport parameters
|
28
|
+
|
29
|
+
| Option | Explanation | Default |
|
30
|
+
| -------------------- | --------------------------------------------- | ---------------- |
|
31
|
+
| `host` | IP or DNS name of instance | (required) |
|
32
|
+
| `execution_timeout` | Maximum time until timeout | 60 |
|
33
|
+
| `recheck_invocation` | Interval of rechecking AWS command invocation | 1.0 |
|
34
|
+
| `recheck_execution` | Interval of rechecking completion of command | 1.0 |
|
35
|
+
|
36
|
+
## Example use
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "train-awsssm"
|
40
|
+
train = Train.create("awsssm", {
|
41
|
+
host: '172.16.3.12',
|
42
|
+
logger: Logger.new($stdout, level: :info)
|
43
|
+
})
|
44
|
+
conn = train.connection
|
45
|
+
result = conn.run_command("apt upgrade -y")
|
46
|
+
conn.close
|
47
|
+
```
|
data/lib/train-awsssm.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require "aws-sdk-ec2"
|
2
|
+
require "aws-sdk-ssm"
|
3
|
+
require "resolv"
|
4
|
+
require "train"
|
5
|
+
|
6
|
+
module TrainPlugins
|
7
|
+
module AWSSSM
|
8
|
+
class Connection < Train::Plugins::Transport::BaseConnection
|
9
|
+
def initialize(options)
|
10
|
+
super(options)
|
11
|
+
|
12
|
+
@ssm = Aws::SSM::Client.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
logger.info format("[AWS-SSM] Closed connection to %s", @options[:host])
|
17
|
+
end
|
18
|
+
|
19
|
+
def uri
|
20
|
+
"aws-ssm://#{@options[:host]}/"
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_command_via_connection(cmd, &data_handler)
|
24
|
+
logger.info format("[AWS-SSM] Sending command to %s", @options[:host])
|
25
|
+
exit_status, stdout, stderr = execute_on_channel(cmd, &data_handler)
|
26
|
+
|
27
|
+
CommandResult.new(stdout, stderr, exit_status)
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute_on_channel(cmd, &data_handler)
|
31
|
+
logger.debug format("[AWS-SSM] Command: '%s'", cmd)
|
32
|
+
|
33
|
+
result = execute_command(@options[:host], cmd)
|
34
|
+
|
35
|
+
stdout = result.standard_output_content || ""
|
36
|
+
stderr = result.standard_error_content || ""
|
37
|
+
exit_status = result.response_code
|
38
|
+
|
39
|
+
[exit_status, stdout, stderr]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Check if this is an IP address
|
45
|
+
def ip_address?(address)
|
46
|
+
!!(address =~ Resolv::IPv4::Regex)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if this is a DNS name
|
50
|
+
def dns_name?(address)
|
51
|
+
!ip_address?(address)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Check if this is an internal/external AWS DNS entry
|
55
|
+
def amazon_dns?(dns)
|
56
|
+
dns.end_with?(".compute.amazonaws.com") || dns.end_with?(".compute.internal")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Resolve EC2 instance ID associated with a primary IP or a DNS entry
|
60
|
+
def instance_id(address)
|
61
|
+
logger.debug format("[AWS-SSM] Trying to resolve address %s", address)
|
62
|
+
|
63
|
+
ec2 = Aws::EC2::Client.new
|
64
|
+
instances = ec2.describe_instances.reservations.collect { |r| r.instances.first }
|
65
|
+
|
66
|
+
# Resolve, if DNS name and not Amazon default
|
67
|
+
if dns_name?(address) && !amazon_dns?(address)
|
68
|
+
address = Resolv.getaddress(address)
|
69
|
+
logger.debug format("[AWS-SSM] Resolved non-internal AWS address to %s", address)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check the primary IPs and hostnames for a match
|
73
|
+
id = instances.detect do |i|
|
74
|
+
[
|
75
|
+
i.private_ip_address,
|
76
|
+
i.public_ip_address,
|
77
|
+
i.private_dns_name,
|
78
|
+
i.public_dns_name,
|
79
|
+
].include?(address)
|
80
|
+
end&.instance_id
|
81
|
+
|
82
|
+
raise format("Could not resolve instance ID for address %s", address) if id.nil?
|
83
|
+
|
84
|
+
logger.debug format("[AWS-SSM] Resolved address %s to instance ID %s", address, id)
|
85
|
+
id
|
86
|
+
end
|
87
|
+
|
88
|
+
# Request a command invocation and wait until it is registered with an ID
|
89
|
+
def wait_for_invocation(instance_id, command_id)
|
90
|
+
invocation_result(instance_id, command_id)
|
91
|
+
|
92
|
+
# Retry until the invocation was created on AWS
|
93
|
+
rescue Aws::SSM::Errors::InvocationDoesNotExist
|
94
|
+
sleep @options[:recheck_invocation]
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return the result of a given command invocation
|
99
|
+
def invocation_result(instance_id, command_id)
|
100
|
+
@ssm.get_command_invocation(instance_id: instance_id, command_id: command_id)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return if a non-terminal command status was given
|
104
|
+
# @see https://docs.aws.amazon.com/systems-manager/latest/userguide/monitor-commands.html
|
105
|
+
def in_progress?(name)
|
106
|
+
%w{Pending InProgress Delayed}.include? name
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return if a terminal command status was given
|
110
|
+
# @see https://docs.aws.amazon.com/systems-manager/latest/userguide/monitor-commands.html
|
111
|
+
def terminal_state?(name)
|
112
|
+
!in_progress?(name)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Execute a command via SSM
|
116
|
+
def execute_command(address, command)
|
117
|
+
instance_id = instance_id(address)
|
118
|
+
|
119
|
+
cmd = @ssm.send_command(instance_ids: [instance_id], document_name: "AWS-RunShellScript", parameters: { "commands": [command] })
|
120
|
+
cmd_id = cmd.command.command_id
|
121
|
+
|
122
|
+
wait_for_invocation(instance_id, cmd_id)
|
123
|
+
logger.debug format("[AWS-SSM] Execution ID %s", cmd_id)
|
124
|
+
|
125
|
+
start_time = Time.now
|
126
|
+
result = invocation_result(instance_id, cmd.command.command_id)
|
127
|
+
|
128
|
+
until terminal_state?(result.status) || Time.now - start_time > @options[:execution_timeout]
|
129
|
+
result = invocation_result(instance_id, cmd.command.command_id)
|
130
|
+
sleep @options[:recheck_execution]
|
131
|
+
end
|
132
|
+
|
133
|
+
if Time.now - start_time > @options[:execution_timeout]
|
134
|
+
raise format("Timeout waiting for execution")
|
135
|
+
elsif result.status != "Success"
|
136
|
+
raise format('Execution failed with state "%s": %s', result.status, result.standard_error_content || "unknown")
|
137
|
+
end
|
138
|
+
|
139
|
+
result
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "train-awsssm/connection"
|
2
|
+
|
3
|
+
module TrainPlugins
|
4
|
+
module AWSSSM
|
5
|
+
class Transport < Train.plugin(1)
|
6
|
+
name "awsssm"
|
7
|
+
|
8
|
+
option :host, required: true
|
9
|
+
|
10
|
+
option :execution_timeout, default: 60.0
|
11
|
+
option :recheck_invocation, default: 1.0
|
12
|
+
option :recheck_execution, default: 1.0
|
13
|
+
|
14
|
+
def connection(_instance_opts = nil)
|
15
|
+
@connection ||= TrainPlugins::AWSSSM::Connection.new(@options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "train-awsssm/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "train-awsssm"
|
7
|
+
spec.version = TrainPlugins::AWSSSM::VERSION
|
8
|
+
spec.authors = ["Thomas Heinen"]
|
9
|
+
spec.email = ["theinen@tecracer.de"]
|
10
|
+
spec.summary = "Train Transport for AWS Systems Manager Agents"
|
11
|
+
spec.description = "Train plugin to use the AWS Systems Manager Agent to execute commands on machines without SSH/WinRM "
|
12
|
+
spec.homepage = "https://github.com/tecracer_theinen/train-awsssm"
|
13
|
+
spec.license = "Apache-2.0"
|
14
|
+
|
15
|
+
spec.files = %w{
|
16
|
+
README.md train-awsssm.gemspec Gemfile
|
17
|
+
} + Dir.glob(
|
18
|
+
"lib/**/*", File::FNM_DOTMATCH
|
19
|
+
).reject { |f| File.directory?(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "train", "~> 2.0"
|
23
|
+
spec.add_dependency "aws-sdk-ec2", "~> 1.129"
|
24
|
+
spec.add_dependency "aws-sdk-ssm", "~> 1.69"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bump", "~> 0.8"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: train-awsssm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Heinen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-01-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: train
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk-ec2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.129'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.129'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: aws-sdk-ssm
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.69'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.69'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bump
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.8'
|
69
|
+
description: 'Train plugin to use the AWS Systems Manager Agent to execute commands
|
70
|
+
on machines without SSH/WinRM '
|
71
|
+
email:
|
72
|
+
- theinen@tecracer.de
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- Gemfile
|
78
|
+
- README.md
|
79
|
+
- lib/train-awsssm.rb
|
80
|
+
- lib/train-awsssm/connection.rb
|
81
|
+
- lib/train-awsssm/transport.rb
|
82
|
+
- lib/train-awsssm/version.rb
|
83
|
+
- train-awsssm.gemspec
|
84
|
+
homepage: https://github.com/tecracer_theinen/train-awsssm
|
85
|
+
licenses:
|
86
|
+
- Apache-2.0
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.0.3
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Train Transport for AWS Systems Manager Agents
|
107
|
+
test_files: []
|