train-awsssm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []