sonic-screwdriver 1.4.0 → 2.2.1
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 +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
|