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 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
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem "pry"
7
+ gem "bundler"
8
+ gem "rake"
9
+ gem "chefstyle"
10
+ end
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
+ ```
@@ -0,0 +1,7 @@
1
+ libdir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+
4
+ require "train-awsssm/version"
5
+
6
+ require "train-awsssm/transport"
7
+ require "train-awsssm/connection"
@@ -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,5 @@
1
+ module TrainPlugins
2
+ module AWSSSM
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ 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: []