sonic-screwdriver 1.4.0 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/bin/commit_docs.sh +26 -0
- data/.circleci/config.yml +72 -0
- data/.gitignore +2 -1
- data/CHANGELOG.md +29 -3
- data/Gemfile +3 -3
- data/Guardfile +17 -10
- data/LICENSE.txt +2 -2
- data/README.md +25 -28
- data/Rakefile +9 -2
- data/docs/_config.yml +3 -0
- data/docs/_docs/help.md +1 -1
- data/docs/_docs/install-bastion.md +5 -15
- data/docs/_docs/install.md +3 -13
- data/docs/_docs/next-steps.md +1 -1
- data/docs/_docs/settings.md +42 -56
- data/docs/_docs/tutorial-ecs-exec.md +16 -20
- data/docs/_docs/tutorial-ecs-sh.md +73 -0
- data/docs/_docs/tutorial-execute.md +106 -38
- data/docs/_docs/tutorial-ssh.md +15 -19
- data/docs/_docs/why-ec2-run-command.md +1 -1
- data/docs/_includes/commands.html +5 -5
- data/docs/_includes/content.html +5 -0
- data/docs/_includes/css/main.css +15 -9
- data/docs/_includes/css/sonic.css +7 -5
- data/docs/_includes/example.html +4 -4
- data/docs/_includes/footer.html +6 -4
- data/docs/_includes/reference.md +1 -0
- data/docs/_includes/subnav.html +2 -1
- data/docs/_reference/sonic-completion.md +44 -0
- data/docs/_reference/sonic-completion_script.md +25 -0
- data/docs/_reference/sonic-ecs-exec.md +30 -0
- data/docs/_reference/sonic-ecs-help.md +21 -0
- data/docs/_reference/sonic-ecs-sh.md +35 -0
- data/docs/_reference/sonic-ecs.md +25 -0
- data/docs/_reference/sonic-execute.md +85 -0
- data/docs/_reference/sonic-list.md +40 -0
- data/docs/_reference/sonic-ssh.md +86 -0
- data/docs/_reference/sonic-version.md +21 -0
- data/docs/bin/web +1 -1
- data/docs/img/tutorials/ec2-console-run-command.png +0 -0
- data/docs/quick-start.md +17 -22
- data/docs/reference.md +12 -0
- data/{bin → exe}/sonic +3 -3
- data/lib/bash_scripts/docker-exec.sh +1 -0
- data/lib/bash_scripts/docker-run.sh +8 -1
- data/lib/sonic.rb +11 -3
- data/lib/sonic/{aws_services.rb → aws_service.rb} +6 -1
- data/lib/sonic/base_command.rb +82 -0
- data/lib/sonic/checks.rb +2 -2
- data/lib/sonic/cli.rb +41 -29
- data/lib/sonic/command.rb +8 -22
- data/lib/sonic/completer.rb +161 -0
- data/lib/sonic/completer/script.rb +6 -0
- data/lib/sonic/completer/script.sh +10 -0
- data/lib/sonic/core.rb +15 -0
- data/lib/sonic/default/settings.yml +9 -16
- data/lib/sonic/docker.rb +30 -2
- data/lib/sonic/ecs.rb +22 -0
- data/lib/sonic/execute.rb +203 -51
- data/lib/sonic/help.rb +9 -0
- data/lib/sonic/help/command/send.md +10 -0
- data/lib/sonic/help/completion.md +22 -0
- data/lib/sonic/help/completion_script.md +3 -0
- data/lib/sonic/help/ecs/exec.md +8 -0
- data/lib/sonic/help/ecs/sh.md +13 -0
- data/lib/sonic/help/execute.md +59 -0
- data/lib/sonic/help/list.md +17 -0
- data/lib/sonic/help/ssh.md +60 -0
- data/lib/sonic/list.rb +5 -2
- data/lib/sonic/setting.rb +47 -0
- data/lib/sonic/ssh.rb +42 -23
- data/lib/sonic/ssh/identifier_detector.rb +7 -3
- data/lib/sonic/ui.rb +2 -2
- data/lib/sonic/version.rb +1 -1
- data/sonic.gemspec +14 -9
- data/spec/lib/cli_spec.rb +11 -11
- data/spec/lib/sonic/execute_spec.rb +1 -2
- data/spec/spec_helper.rb +18 -10
- metadata +115 -19
- data/Gemfile.lock +0 -134
- data/docs/_docs/tutorial-ecs-run.md +0 -100
- data/lib/sonic/cli/help.rb +0 -152
- data/lib/sonic/settings.rb +0 -115
data/lib/sonic/command.rb
CHANGED
@@ -1,25 +1,11 @@
|
|
1
|
-
require 'thor'
|
2
|
-
|
3
1
|
module Sonic
|
4
|
-
class Command <
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# sonic command -D
|
12
|
-
#
|
13
|
-
# as well thor's normal way:
|
14
|
-
#
|
15
|
-
# sonic help command
|
16
|
-
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
17
|
-
if args.length > 1 && !(args & help_flags).empty?
|
18
|
-
args -= help_flags
|
19
|
-
args.insert(-2, "help")
|
20
|
-
end
|
21
|
-
super
|
22
|
-
end
|
2
|
+
class Command < BaseCommand
|
3
|
+
desc "send [FILTER] [COMMAND]", "runs command across fleet of servers via AWS Run Command"
|
4
|
+
long_desc Help.text("command/send")
|
5
|
+
option :zero_warn, type: :boolean, default: true, desc: "Warns user when no instances found"
|
6
|
+
# filter - Filter ec2 instances by tag name or instance_ids separated by commas
|
7
|
+
def send(filter, *command)
|
8
|
+
Commander.new(command, options.merge(filter: filter)).execute
|
23
9
|
end
|
24
10
|
end
|
25
|
-
end
|
11
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
=begin
|
2
|
+
Code Explanation:
|
3
|
+
|
4
|
+
There are 3 types of things to auto-complete:
|
5
|
+
|
6
|
+
1. command: the command itself
|
7
|
+
2. parameters: command parameters.
|
8
|
+
3. options: command options
|
9
|
+
|
10
|
+
Here's an example:
|
11
|
+
|
12
|
+
mycli hello name --from me
|
13
|
+
|
14
|
+
* command: hello
|
15
|
+
* parameters: name
|
16
|
+
* option: --from
|
17
|
+
|
18
|
+
When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
|
19
|
+
|
20
|
+
## Arity
|
21
|
+
|
22
|
+
For example, say you had a method for a CLI command with the following form:
|
23
|
+
|
24
|
+
ufo scale service count --cluster development
|
25
|
+
|
26
|
+
It's equivalent ruby method:
|
27
|
+
|
28
|
+
scale(service, count) = has an arity of 2
|
29
|
+
|
30
|
+
So typing:
|
31
|
+
|
32
|
+
ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
|
33
|
+
|
34
|
+
So the completion should only show options, something like this:
|
35
|
+
|
36
|
+
--noop --verbose --cluster
|
37
|
+
|
38
|
+
## Splat Arguments
|
39
|
+
|
40
|
+
When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
|
41
|
+
|
42
|
+
ship(service) = 1
|
43
|
+
scale(service, count) = 2
|
44
|
+
ships(*services) = -1
|
45
|
+
foo(example, *rest) = -2
|
46
|
+
|
47
|
+
Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
|
48
|
+
|
49
|
+
Here are some test cases, hit TAB after typing the command:
|
50
|
+
|
51
|
+
sonic completion
|
52
|
+
sonic completion hello
|
53
|
+
sonic completion hello name
|
54
|
+
sonic completion hello name --
|
55
|
+
sonic completion hello name --noop
|
56
|
+
|
57
|
+
sonic completion
|
58
|
+
sonic completion sub:goodbye
|
59
|
+
sonic completion sub:goodbye name
|
60
|
+
|
61
|
+
## Subcommands and Thor::Group Registered Commands
|
62
|
+
|
63
|
+
Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
|
64
|
+
|
65
|
+
* regular command: ufo ship
|
66
|
+
* subcommand: ufo docker
|
67
|
+
* Thor::Group command: ufo init
|
68
|
+
|
69
|
+
Auto-completion accounts for each of these type of commands.
|
70
|
+
=end
|
71
|
+
module Sonic
|
72
|
+
class Completer
|
73
|
+
autoload :Script, 'sonic/completer/script'
|
74
|
+
|
75
|
+
def initialize(command_class, *params)
|
76
|
+
@params = params
|
77
|
+
@current_command = @params[0]
|
78
|
+
@command_class = command_class # CLI initiall
|
79
|
+
end
|
80
|
+
|
81
|
+
def run
|
82
|
+
if subcommand?(@current_command)
|
83
|
+
subcommand_class = @command_class.subcommand_classes[@current_command]
|
84
|
+
@params.shift # destructive
|
85
|
+
Completer.new(subcommand_class, *@params).run # recursively use subcommand
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
# full command has been found!
|
90
|
+
unless found?(@current_command)
|
91
|
+
puts all_commands
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
# will only get to here if command aws found (above)
|
96
|
+
arity = @command_class.instance_method(@current_command).arity.abs
|
97
|
+
if @params.size > arity or thor_group_command?
|
98
|
+
puts options_completion
|
99
|
+
else
|
100
|
+
puts params_completion
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def subcommand?(command)
|
105
|
+
@command_class.subcommands.include?(command)
|
106
|
+
end
|
107
|
+
|
108
|
+
# hacky way to detect that command is a registered Thor::Group command
|
109
|
+
def thor_group_command?
|
110
|
+
command_params(raw=true) == [[:rest, :args]]
|
111
|
+
end
|
112
|
+
|
113
|
+
def found?(command)
|
114
|
+
public_methods = @command_class.public_instance_methods(false)
|
115
|
+
command && public_methods.include?(command.to_sym)
|
116
|
+
end
|
117
|
+
|
118
|
+
# all top-level commands
|
119
|
+
def all_commands
|
120
|
+
commands = @command_class.all_commands.reject do |k,v|
|
121
|
+
v.is_a?(Thor::HiddenCommand)
|
122
|
+
end
|
123
|
+
commands.keys
|
124
|
+
end
|
125
|
+
|
126
|
+
def command_params(raw=false)
|
127
|
+
params = @command_class.instance_method(@current_command).parameters
|
128
|
+
# Example:
|
129
|
+
# >> Sub.instance_method(:goodbye).parameters
|
130
|
+
# => [[:req, :name]]
|
131
|
+
# >>
|
132
|
+
raw ? params : params.map!(&:last)
|
133
|
+
end
|
134
|
+
|
135
|
+
def params_completion
|
136
|
+
offset = @params.size - 1
|
137
|
+
offset_params = command_params[offset..-1]
|
138
|
+
command_params[offset..-1].first
|
139
|
+
end
|
140
|
+
|
141
|
+
def options_completion
|
142
|
+
used = ARGV.select { |a| a.include?('--') } # so we can remove used options
|
143
|
+
|
144
|
+
method_options = @command_class.all_commands[@current_command].options.keys
|
145
|
+
class_options = @command_class.class_options.keys
|
146
|
+
|
147
|
+
all_options = method_options + class_options + ['help']
|
148
|
+
|
149
|
+
all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
|
150
|
+
filtered_options = all_options - used
|
151
|
+
filtered_options.uniq
|
152
|
+
end
|
153
|
+
|
154
|
+
# Useful for debugging. Using puts messes up completion.
|
155
|
+
def log(msg)
|
156
|
+
File.open("/tmp/complete.log", "a") do |file|
|
157
|
+
file.puts(msg)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/lib/sonic/core.rb
ADDED
@@ -1,16 +1,9 @@
|
|
1
|
-
bastion:
|
2
|
-
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# hi-web-prod: prod
|
11
|
-
# hi-clock-prod: prod
|
12
|
-
# hi-worker-prod: prod
|
13
|
-
# hi-web-stag: stag
|
14
|
-
# hi-clock-stag: stag
|
15
|
-
# hi-worker-stag: stag
|
16
|
-
user: ec2-user
|
1
|
+
bastion:
|
2
|
+
host_key_check: false
|
3
|
+
# host:
|
4
|
+
|
5
|
+
ssh:
|
6
|
+
user: ec2-user
|
7
|
+
|
8
|
+
ecs_service_cluster_map:
|
9
|
+
default: default
|
data/lib/sonic/docker.rb
CHANGED
@@ -19,6 +19,7 @@ module Sonic
|
|
19
19
|
|
20
20
|
def setup
|
21
21
|
validate!
|
22
|
+
confirm_ssh_access
|
22
23
|
copy_over_container_data
|
23
24
|
end
|
24
25
|
|
@@ -29,7 +30,7 @@ module Sonic
|
|
29
30
|
|
30
31
|
# command is an Array
|
31
32
|
def execute(*command)
|
32
|
-
UI.say "=> #{command.join(' ')}".
|
33
|
+
UI.say "=> #{command.join(' ')}".color(:green)
|
33
34
|
success = system(*command)
|
34
35
|
unless success
|
35
36
|
UI.error(command.join(' '))
|
@@ -46,10 +47,37 @@ module Sonic
|
|
46
47
|
kernel_exec(*args)
|
47
48
|
end
|
48
49
|
|
50
|
+
def build_host
|
51
|
+
host = @bastion ? bastion_host : ssh_host
|
52
|
+
host = "#{@user}@#{host}" unless host.include?('@')
|
53
|
+
host
|
54
|
+
end
|
55
|
+
|
56
|
+
def confirm_ssh_access
|
57
|
+
host = build_host
|
58
|
+
puts "Checking access to instance #{detector.instance_id}"
|
59
|
+
|
60
|
+
ssh = ["ssh", ssh_options, "-At", host, "uptime", "2>&1"]
|
61
|
+
command = ssh.join(' ')
|
62
|
+
puts "=> #{command}".color(:green)
|
63
|
+
output = `#{command}`
|
64
|
+
if output.include?("Permission denied")
|
65
|
+
puts output
|
66
|
+
UI.error("Access to the instance denied. Maybe check your ssh keys.")
|
67
|
+
exit 1
|
68
|
+
elsif output.include?(" up ")
|
69
|
+
puts "Access OK!"
|
70
|
+
else
|
71
|
+
puts output
|
72
|
+
UI.error("There was an error trying to access the instnace.")
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
49
77
|
def copy_over_container_data
|
50
78
|
create_container_data
|
51
79
|
|
52
|
-
host =
|
80
|
+
host = build_host
|
53
81
|
|
54
82
|
# LEVEL 1
|
55
83
|
# Always clean up remote /tmp/sonic in case of previous interrupted run.
|
data/lib/sonic/ecs.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sonic
|
2
|
+
autoload :Docker, 'sonic/docker'
|
3
|
+
|
4
|
+
class Ecs < BaseCommand
|
5
|
+
|
6
|
+
class_option :bastion, desc: "Bastion jump host to use. Defaults to no bastion server."
|
7
|
+
class_option :cluster, desc: "ECS Cluster to use. Default cluster is default"
|
8
|
+
|
9
|
+
desc "exec [ECS_SERVICE]", "docker exec into running docker container associated with the service on a container instance"
|
10
|
+
long_desc Help.text("ecs/exec")
|
11
|
+
def exec(service, *command)
|
12
|
+
Docker.new(service, options.merge(command: command)).exec
|
13
|
+
end
|
14
|
+
|
15
|
+
# Cannot name the command run because that is a reserved Thor keyword :(
|
16
|
+
desc "sh [ECS_SERVICE]", "docker run with the service on a container instance"
|
17
|
+
long_desc Help.text("ecs/sh")
|
18
|
+
def sh(service, *command)
|
19
|
+
Docker.new(service, options.merge(command: command)).run
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/sonic/execute.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
|
1
4
|
module Sonic
|
2
5
|
class Execute
|
3
|
-
include
|
6
|
+
include AwsService
|
4
7
|
|
5
8
|
def initialize(command, options)
|
6
9
|
@command = command
|
7
10
|
@options = options
|
8
|
-
@
|
11
|
+
@tags = @options[:tags]
|
12
|
+
@instance_ids = @options[:instance_ids]
|
9
13
|
end
|
10
14
|
|
11
15
|
# aws ssm send-command \
|
@@ -15,40 +19,193 @@ module Sonic
|
|
15
19
|
# --parameters '{"commands":["#!/usr/bin/python","print \"Hello world from python\""]}' \
|
16
20
|
# --query "Command.CommandId"
|
17
21
|
def execute
|
22
|
+
check_filter_options!
|
18
23
|
ssm_options = build_ssm_options
|
19
24
|
if @options[:noop]
|
20
25
|
UI.noop = true
|
21
|
-
command_id = "fake command id"
|
26
|
+
command_id = "fake command id for noop mode"
|
22
27
|
success = true # fake it for specs
|
23
28
|
else
|
24
29
|
instances_count = check_instances
|
25
30
|
return unless instances_count > 0
|
26
31
|
|
27
32
|
success = nil
|
33
|
+
puts "Sending command to SSM with options:"
|
34
|
+
puts YAML.dump(ssm_options.deep_stringify_keys)
|
35
|
+
puts
|
28
36
|
begin
|
29
|
-
resp =
|
37
|
+
resp = send_command(ssm_options)
|
38
|
+
|
30
39
|
command_id = resp.command.command_id
|
31
40
|
success = true
|
32
41
|
rescue Aws::SSM::Errors::InvalidInstanceId => e
|
33
42
|
ssm_invalid_instance_error_message(e)
|
34
43
|
end
|
35
44
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
45
|
+
|
46
|
+
return unless success
|
47
|
+
|
48
|
+
# IF COMMAND IS ONLY ON A SINGLE INSTANCE THEN WILL DISPLAY A BUNCH OF
|
49
|
+
# INFO ON THE INSTANCE. IF ITS A LOT OF INSTANCES, THEN SHOW A SUMMARY
|
50
|
+
# OF COMMANDS THAT WILL LEAD TO THE OUTPUT OF EACH INSTANCE.
|
51
|
+
UI.say "Command sent to AWS SSM. To check the details of the command:"
|
52
|
+
display_ssm_commands(command_id, ssm_options)
|
53
|
+
puts
|
54
|
+
return if @options[:noop]
|
55
|
+
status = wait(command_id)
|
56
|
+
instances_found = display_ssm_output(command_id)
|
57
|
+
display_console_url(command_id)
|
58
|
+
|
59
|
+
if status == "Success"
|
60
|
+
puts "Command successful: #{status}".color(:green)
|
61
|
+
exit(0)
|
62
|
+
else
|
63
|
+
puts "Command unsuccessful: #{status}".color(:red)
|
64
|
+
exit(1)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def wait(command_id)
|
69
|
+
ongoing_states = ["Pending", "InProgress", "Delayed"]
|
70
|
+
|
71
|
+
print "Waiting for ssm command to finish..."
|
72
|
+
resp = ssm.list_commands(command_id: command_id)
|
73
|
+
status = resp["commands"].first["status"]
|
74
|
+
while ongoing_states.include?(status)
|
75
|
+
resp = ssm.list_commands(command_id: command_id)
|
76
|
+
status = resp["commands"].first["status"]
|
77
|
+
sleep 1
|
78
|
+
print '.'
|
79
|
+
end
|
80
|
+
puts "\nCommand finished."
|
81
|
+
puts
|
82
|
+
status
|
83
|
+
end
|
84
|
+
|
85
|
+
def display_ssm_output(command_id)
|
86
|
+
resp = ssm.list_command_invocations(command_id: command_id)
|
87
|
+
command_invocations = resp.command_invocations
|
88
|
+
command_invocation = command_invocations.first
|
89
|
+
unless command_invocation
|
90
|
+
puts "WARN: No instances found that matches the --tags or --instance-ids option".color(:yellow)
|
91
|
+
return false # instances_found
|
92
|
+
end
|
93
|
+
instance_id = command_invocation.instance_id
|
94
|
+
|
95
|
+
if command_invocations.size > 1
|
96
|
+
puts "Multiple instance targets. Total targets: #{command_invocations.size}. Only displaying output for #{instance_id}."
|
97
|
+
else
|
98
|
+
puts "Displaying output for #{instance_id}."
|
99
|
+
end
|
100
|
+
|
101
|
+
resp = ssm.get_command_invocation(
|
102
|
+
command_id: command_id, instance_id: instance_id
|
103
|
+
)
|
104
|
+
puts "Command status: #{colorized_status(resp["status"])}"
|
105
|
+
ssm_output(resp, "output")
|
106
|
+
ssm_output(resp, "error")
|
107
|
+
puts
|
108
|
+
true # instances_found
|
109
|
+
end
|
110
|
+
|
111
|
+
def display_console_url(command_id)
|
112
|
+
region = `aws configure get region`.strip rescue 'us-east-1'
|
113
|
+
console_url = "https://#{region}.console.aws.amazon.com/systems-manager/run-command/#{command_id}"
|
114
|
+
puts "To see the more output details visit:"
|
115
|
+
puts " #{console_url}"
|
116
|
+
puts
|
117
|
+
copy_paste_clipboard(console_url, "Pro tip: the console url is already in your copy/paste clipboard.")
|
118
|
+
end
|
119
|
+
|
120
|
+
def colorized_status(status)
|
121
|
+
case status
|
122
|
+
when "Success"
|
123
|
+
status.color(:green)
|
124
|
+
when "Failed"
|
125
|
+
status.color(:red)
|
126
|
+
else
|
127
|
+
status
|
39
128
|
end
|
40
129
|
end
|
41
130
|
|
131
|
+
# type: output or error
|
132
|
+
def ssm_output(resp, type)
|
133
|
+
content_key = "standard_#{type}_content"
|
134
|
+
s3_key = "standard_#{type}_url"
|
135
|
+
|
136
|
+
content = resp[content_key]
|
137
|
+
return if content.empty?
|
138
|
+
|
139
|
+
puts "Command standard #{type}:"
|
140
|
+
# "https://s3.amazonaws.com/infra-prod/ssm/commands/sonic/0a4f4bef-8f63-4235-8b30-ae296477261a/i-0b2e6e187a3f9ada9/awsrunPowerShellScript/0.awsrunPowerShellScript/stderr">
|
141
|
+
if content.include?("--output truncated--") && !resp[s3_key].empty?
|
142
|
+
s3_url = resp[s3_key]
|
143
|
+
info = s3_url.sub('https://s3.amazonaws.com/', '').split('/')
|
144
|
+
bucket = info[0]
|
145
|
+
key = info[1..-1].join('/')
|
146
|
+
resp = s3.get_object(bucket: bucket, key: key)
|
147
|
+
data = resp.body.read
|
148
|
+
puts data
|
149
|
+
|
150
|
+
path = "/tmp/sonic-output.txt"
|
151
|
+
puts "------"
|
152
|
+
puts "Output also written to #{path}"
|
153
|
+
IO.write(path, data)
|
154
|
+
else
|
155
|
+
puts content
|
156
|
+
end
|
157
|
+
|
158
|
+
# puts "#{s3_key}: #{resp[s3_key]}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def send_command(options)
|
162
|
+
retries = 0
|
163
|
+
|
164
|
+
begin
|
165
|
+
resp = ssm.send_command(options)
|
166
|
+
rescue Aws::SSM::Errors::UnsupportedPlatformType
|
167
|
+
retries += 1
|
168
|
+
# toggle AWS-RunShellScript / AWS-RunPowerShellScript
|
169
|
+
options[:document_name] =
|
170
|
+
options[:document_name] == "AWS-RunShellScript" ?
|
171
|
+
"AWS-RunPowerShellScript" : "AWS-RunShellScript"
|
172
|
+
|
173
|
+
puts "#{$!}"
|
174
|
+
puts "Retrying with document_name #{options[:document_name]}"
|
175
|
+
puts "Retries: #{retries}"
|
176
|
+
|
177
|
+
retries <= 1 ? retry : raise
|
178
|
+
end
|
179
|
+
|
180
|
+
resp
|
181
|
+
end
|
182
|
+
|
42
183
|
def build_ssm_options
|
43
|
-
criteria =
|
184
|
+
criteria = transform_filter_option
|
44
185
|
command = build_command(@command)
|
45
|
-
criteria.merge(
|
46
|
-
document_name: "AWS-RunShellScript",
|
47
|
-
comment: "sonic #{ARGV.join(' ')}",
|
48
|
-
parameters: {
|
49
|
-
|
50
|
-
|
186
|
+
options = criteria.merge(
|
187
|
+
document_name: "AWS-RunShellScript", # default
|
188
|
+
comment: "sonic #{ARGV.join(' ')}"[0..99], # comment has a max of 100 chars
|
189
|
+
parameters: { "commands" => command },
|
190
|
+
# Default CloudWatchLog settings. Can be overwritten with settings.yml send_command
|
191
|
+
# IMPORTANT: make sure the EC2 instance the command runs on has access to write to CloudWatch Logs.
|
192
|
+
cloud_watch_output_config: {
|
193
|
+
# cloud_watch_log_group_name: "ssm", # Defaults to /aws/ssm/AWS-RunShellScript (aws/ssm/SystemsManagerDocumentName https://amzn.to/38TKVse)
|
194
|
+
cloud_watch_output_enabled: true,
|
195
|
+
},
|
51
196
|
)
|
197
|
+
settings_options = settings["send_command"] || {}
|
198
|
+
options.merge(settings_options.deep_symbolize_keys)
|
199
|
+
end
|
200
|
+
|
201
|
+
def settings
|
202
|
+
@settings ||= Setting.new.data
|
203
|
+
end
|
204
|
+
|
205
|
+
def check_filter_options!
|
206
|
+
return if @tags || @instance_ids
|
207
|
+
puts "ERROR: Please provide --tags or --instance-ids option".color(:red)
|
208
|
+
exit 1
|
52
209
|
end
|
53
210
|
|
54
211
|
#
|
@@ -58,41 +215,31 @@ module Sonic
|
|
58
215
|
#
|
59
216
|
# Examples
|
60
217
|
#
|
61
|
-
#
|
218
|
+
# transform_filter_option
|
62
219
|
# # => {
|
63
220
|
# instance_ids: ["i-006a097bb10643e20"],
|
64
221
|
# targets: [{key: "Name", values: "hi-web-prod,hi-worker-prod"}]
|
65
222
|
# }
|
66
223
|
#
|
67
224
|
# Returns the duplicated String.
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
key: "tag:#{tag_name}",
|
83
|
-
values: tags
|
84
|
-
}]
|
225
|
+
def transform_filter_option
|
226
|
+
if @tags
|
227
|
+
list = @tags.split(';')
|
228
|
+
targets = list.inject([]) do |final,item|
|
229
|
+
tag_name,value_list = item.split('=')
|
230
|
+
values = value_list.split(',').map(&:strip)
|
231
|
+
# structure expected by ssm send_command
|
232
|
+
option = {
|
233
|
+
key: "tag:#{tag_name}",
|
234
|
+
values: values
|
235
|
+
}
|
236
|
+
final << option
|
237
|
+
final
|
238
|
+
end
|
85
239
|
{targets: targets}
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
# Either all instance ids are no instance ids is a valid filter
|
90
|
-
def validate_filter(filter)
|
91
|
-
if filter.detect { |i| instance_id?(i) }
|
92
|
-
instance_ids = filter.select { |i| instance_id?(i) }
|
93
|
-
instance_ids.size == filter.size
|
94
|
-
else
|
95
|
-
true
|
240
|
+
else # @instance_ids
|
241
|
+
instance_ids = @instance_ids.split(',')
|
242
|
+
{instance_ids: instance_ids}
|
96
243
|
end
|
97
244
|
end
|
98
245
|
|
@@ -109,7 +256,7 @@ module Sonic
|
|
109
256
|
# The script is being feed inline so just join the command together into one script.
|
110
257
|
# Still keep in an array form because that's how ssn.send_command works with AWS-RunShellScript
|
111
258
|
# usually reads the command.
|
112
|
-
|
259
|
+
command.is_a?(Array) ? command : [command]
|
113
260
|
end
|
114
261
|
end
|
115
262
|
|
@@ -124,7 +271,7 @@ You can use the following command to check registered instances to SSM.
|
|
124
271
|
#{ssm_describe_command}
|
125
272
|
EOS
|
126
273
|
UI.warn(message)
|
127
|
-
copy_paste_clipboard(ssm_describe_command)
|
274
|
+
copy_paste_clipboard(ssm_describe_command, "Pro tip: ssm describe-instance-information already in your copy/paste clipboard.")
|
128
275
|
end
|
129
276
|
|
130
277
|
def file_path?(command)
|
@@ -136,7 +283,7 @@ You can use the following command to check registered instances to SSM.
|
|
136
283
|
def file_path(command)
|
137
284
|
path = command.first
|
138
285
|
path = path.sub('file://', '')
|
139
|
-
path = "#{
|
286
|
+
path = "#{Sonic.root}/#{path}"
|
140
287
|
path
|
141
288
|
end
|
142
289
|
|
@@ -150,7 +297,7 @@ You can use the following command to check registered instances to SSM.
|
|
150
297
|
instances = List.new(@options).instances
|
151
298
|
if instances.count == 0
|
152
299
|
message = <<-EOL
|
153
|
-
|
300
|
+
Unable to find any instances with filter #{@filter.join(',')}.
|
154
301
|
Are you sure you specify the filter with either a EC2 tag or list instance ids?
|
155
302
|
If you are using ECS identifiers, they are not supported with this command.
|
156
303
|
EOL
|
@@ -170,16 +317,21 @@ EOL
|
|
170
317
|
text =~ /i-.{17}/ || text =~ /i-.{8}/
|
171
318
|
end
|
172
319
|
|
173
|
-
def
|
174
|
-
list_command = "aws ssm list-commands --command-id #{command_id}"
|
320
|
+
def display_ssm_commands(command_id, ssm_options)
|
321
|
+
list_command = " aws ssm list-commands --command-id #{command_id}"
|
175
322
|
UI.say list_command
|
176
|
-
|
323
|
+
|
324
|
+
return unless ssm_options[:instance_ids]
|
325
|
+
ssm_options[:instance_ids].each do |instance_id|
|
326
|
+
get_command = " aws ssm get-command-invocation --command-id #{command_id} --instance-id #{instance_id}"
|
327
|
+
UI.say get_command
|
328
|
+
end
|
177
329
|
end
|
178
330
|
|
179
|
-
def copy_paste_clipboard(command)
|
331
|
+
def copy_paste_clipboard(command, text)
|
180
332
|
return unless RUBY_PLATFORM =~ /darwin/
|
181
333
|
system("echo '#{command}' | pbcopy")
|
182
|
-
UI.say
|
334
|
+
UI.say text
|
183
335
|
end
|
184
336
|
end
|
185
337
|
end
|