sonic-screwdriver 1.4.0 → 2.0.0
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 +70 -0
- data/.gitignore +1 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +13 -2
- data/Gemfile +3 -3
- data/Gemfile.lock +43 -14
- data/Guardfile +17 -10
- data/LICENSE.txt +2 -2
- data/README.md +10 -10
- 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 -3
- data/docs/_docs/settings.md +40 -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 +93 -17
- data/docs/_docs/tutorial-ssh.md +13 -18
- 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/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 +84 -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/img/tutorials/ec2-console-run-command.png +0 -0
- data/docs/quick-start.md +9 -10
- 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 +10 -2
- data/lib/sonic/{aws_services.rb → aws_service.rb} +6 -1
- data/lib/sonic/base_command.rb +82 -0
- data/lib/sonic/cli.rb +37 -27
- 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 +6 -16
- data/lib/sonic/docker.rb +29 -1
- data/lib/sonic/ecs.rb +22 -0
- data/lib/sonic/execute.rb +153 -18
- 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 +60 -0
- data/lib/sonic/help/list.md +17 -0
- data/lib/sonic/help/ssh.md +60 -0
- data/lib/sonic/list.rb +4 -1
- data/lib/sonic/setting.rb +47 -0
- data/lib/sonic/ssh.rb +41 -20
- data/lib/sonic/ssh/identifier_detector.rb +6 -2
- data/lib/sonic/version.rb +1 -1
- data/sonic.gemspec +14 -9
- data/spec/lib/cli_spec.rb +5 -10
- data/spec/lib/sonic/execute_spec.rb +0 -1
- data/spec/spec_helper.rb +18 -10
- metadata +115 -16
- 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/cli.rb
CHANGED
@@ -1,49 +1,59 @@
|
|
1
1
|
require 'thor'
|
2
|
-
require 'sonic/cli/help'
|
3
2
|
|
4
3
|
module Sonic
|
5
|
-
class CLI <
|
4
|
+
class CLI < BaseCommand
|
5
|
+
desc "ecs SUBCOMMAND", "ecs subcommands"
|
6
|
+
long_desc Help.text(:ecs)
|
7
|
+
subcommand "ecs", Ecs
|
8
|
+
|
9
|
+
# desc "command SUBCOMMAND", "command subcommands"
|
10
|
+
# long_desc Help.text(:command)
|
11
|
+
# subcommand "command", Command
|
12
|
+
|
6
13
|
class_option :verbose, type: :boolean
|
7
14
|
class_option :noop, type: :boolean
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
method_option :retry, :aliases => '-r', :type => :boolean, :desc => "keep retrying the server login until successful. Useful when on newly launched instances."
|
15
|
+
|
16
|
+
desc "ssh [IDENTIFER]", "Ssh into a instance using identifier. identifer can be several things: instance id, ec2 tag, ECS service name, etc."
|
17
|
+
long_desc Help.text(:ssh)
|
18
|
+
option :keys, :aliases => '-i', :desc => "comma separated list of ssh private key paths"
|
19
|
+
option :retry, :aliases => '-r', :type => :boolean, :desc => "keep retrying the server login until successful. Useful when on newly launched instances."
|
20
|
+
option :bastion, desc: "Bastion jump host to use. Defaults to no bastion server."
|
21
|
+
option :cluster, desc: "ECS Cluster to use. Default cluster is default"
|
16
22
|
def ssh(identifier, *command)
|
17
23
|
Ssh.new(identifier, options.merge(command: command)).run
|
18
24
|
end
|
19
25
|
|
20
|
-
desc "
|
21
|
-
long_desc Help.
|
22
|
-
def ecs_exec(service, *command)
|
23
|
-
Docker.new(service, options.merge(command: command)).exec
|
24
|
-
end
|
25
|
-
|
26
|
-
# Cannot name the command run because that is a reserved Thor keyword :(
|
27
|
-
desc "ecs-run [ECS_SERVICE]", "docker run with the service on a container instance"
|
28
|
-
long_desc Help.ecs_run
|
29
|
-
def ecs_run(service, *command)
|
30
|
-
Docker.new(service, options.merge(command: command)).run
|
31
|
-
end
|
32
|
-
|
33
|
-
desc "execute [FILTER] [COMMAND]", "runs command across fleet of servers via AWS Run Command"
|
34
|
-
long_desc Help.execute
|
26
|
+
desc "execute [FILTER] [COMMAND]", "Runs command across fleet of servers via AWS Run Command."
|
27
|
+
long_desc Help.text("execute")
|
35
28
|
option :zero_warn, type: :boolean, default: true, desc: "Warns user when no instances found"
|
36
29
|
# filter - Filter ec2 instances by tag name or instance_ids separated by commas
|
37
30
|
def execute(filter, *command)
|
38
31
|
Execute.new(command, options.merge(filter: filter)).execute
|
39
32
|
end
|
40
33
|
|
41
|
-
desc "list [FILTER]", "
|
42
|
-
long_desc Help.list
|
34
|
+
desc "list [FILTER]", "Lists ec2 instances."
|
35
|
+
long_desc Help.text(:list)
|
43
36
|
option :header, type: :boolean, desc: "Displays header"
|
44
37
|
# filter - Filter ec2 instances by tag name or instance_ids separated by commas
|
45
38
|
def list(filter)
|
46
39
|
List.new(options.merge(filter: filter)).run
|
47
40
|
end
|
41
|
+
|
42
|
+
desc "completion *PARAMS", "Prints words for auto-completion."
|
43
|
+
long_desc Help.text("completion")
|
44
|
+
def completion(*params)
|
45
|
+
Completer.new(CLI, *params).run
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "completion_script", "Generates a script that can be eval to setup auto-completion."
|
49
|
+
long_desc Help.text("completion_script")
|
50
|
+
def completion_script
|
51
|
+
Completer::Script.generate
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "version", "prints version"
|
55
|
+
def version
|
56
|
+
puts VERSION
|
57
|
+
end
|
48
58
|
end
|
49
59
|
end
|
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,6 @@
|
|
1
|
-
bastion:
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
service_cluster:
|
8
|
-
default: # defaults to nil
|
9
|
-
# Examples:
|
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
|
+
user: ec2-user
|
4
|
+
|
5
|
+
ecs_service_cluster_map:
|
6
|
+
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
|
|
@@ -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}".colorize(: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,6 +1,10 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require 'yaml'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
|
1
5
|
module Sonic
|
2
6
|
class Execute
|
3
|
-
include
|
7
|
+
include AwsService
|
4
8
|
|
5
9
|
def initialize(command, options)
|
6
10
|
@command = command
|
@@ -18,37 +22,163 @@ module Sonic
|
|
18
22
|
ssm_options = build_ssm_options
|
19
23
|
if @options[:noop]
|
20
24
|
UI.noop = true
|
21
|
-
command_id = "fake command id"
|
25
|
+
command_id = "fake command id for noop mode"
|
22
26
|
success = true # fake it for specs
|
23
27
|
else
|
24
28
|
instances_count = check_instances
|
25
29
|
return unless instances_count > 0
|
26
30
|
|
27
31
|
success = nil
|
32
|
+
puts "Sending command to SSM with options:"
|
33
|
+
puts YAML.dump(ssm_options.deep_stringify_keys)
|
34
|
+
puts
|
28
35
|
begin
|
29
|
-
resp =
|
36
|
+
resp = send_command(ssm_options)
|
30
37
|
command_id = resp.command.command_id
|
31
38
|
success = true
|
32
39
|
rescue Aws::SSM::Errors::InvalidInstanceId => e
|
33
40
|
ssm_invalid_instance_error_message(e)
|
34
41
|
end
|
35
42
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
43
|
+
|
44
|
+
return unless success
|
45
|
+
|
46
|
+
# IF COMMAND IS ONLY ON A SINGLE INSTANCE THEN WILL DISPLAY A BUNCH OF
|
47
|
+
# INFO ON THE INSTANCE. IF ITS A LOT OF INSTANCES, THEN SHOW A SUMMARY
|
48
|
+
# OF COMMANDS THAT WILL LEAD TO THE OUTPUT OF EACH INSTANCE.
|
49
|
+
UI.say "Command sent to AWS SSM. To check the details of the command:"
|
50
|
+
display_ssm_commands(command_id, ssm_options)
|
51
|
+
puts
|
52
|
+
return if @options[:noop]
|
53
|
+
wait(command_id)
|
54
|
+
display_ssm_output(command_id, ssm_options)
|
55
|
+
display_console_url(command_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait(command_id)
|
59
|
+
ongoing_states = ["Pending", "InProgress", "Delayed"]
|
60
|
+
|
61
|
+
print "Waiting for ssm command to finish..."
|
62
|
+
resp = ssm.list_commands(command_id: command_id)
|
63
|
+
status = resp["commands"].first["status"]
|
64
|
+
while ongoing_states.include?(status)
|
65
|
+
resp = ssm.list_commands(command_id: command_id)
|
66
|
+
status = resp["commands"].first["status"]
|
67
|
+
sleep 1
|
68
|
+
print '.'
|
69
|
+
end
|
70
|
+
puts "\nCommand finished."
|
71
|
+
puts
|
72
|
+
end
|
73
|
+
|
74
|
+
def display_ssm_output(command_id, ssm_options)
|
75
|
+
instance_ids = ssm_options[:instance_ids]
|
76
|
+
return unless instance_ids && instance_ids.size > 0
|
77
|
+
|
78
|
+
instance_id = instance_ids.first
|
79
|
+
if ssm_options[:instance_ids].size > 1
|
80
|
+
puts "Multiple instance targets. Only displaying output for #{instance_id}."
|
81
|
+
else
|
82
|
+
puts "Displaying output for #{instance_id}."
|
83
|
+
end
|
84
|
+
|
85
|
+
resp = ssm.get_command_invocation(
|
86
|
+
command_id: command_id, instance_id: instance_id
|
87
|
+
)
|
88
|
+
puts "Command status: #{colorized_status(resp["status"])}"
|
89
|
+
ssm_output(resp, "output")
|
90
|
+
ssm_output(resp, "error")
|
91
|
+
puts
|
92
|
+
end
|
93
|
+
|
94
|
+
def display_console_url(command_id)
|
95
|
+
region = `aws configure get region`.strip rescue 'us-east-1'
|
96
|
+
console_url = "https://#{region}.console.aws.amazon.com/systems-manager/run-command/#{command_id}"
|
97
|
+
puts "To see the more output details visit:"
|
98
|
+
puts " #{console_url}"
|
99
|
+
puts
|
100
|
+
copy_paste_clipboard(console_url)
|
101
|
+
UI.say "Pro tip: the console url is already in your copy/paste clipboard."
|
102
|
+
end
|
103
|
+
|
104
|
+
def colorized_status(status)
|
105
|
+
case status
|
106
|
+
when "Success"
|
107
|
+
status.colorize(:green)
|
108
|
+
when "Failed"
|
109
|
+
status.colorize(:red)
|
110
|
+
else
|
111
|
+
status
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# type: output or error
|
116
|
+
def ssm_output(resp, type)
|
117
|
+
content_key = "standard_#{type}_content"
|
118
|
+
s3_key = "standard_#{type}_url"
|
119
|
+
|
120
|
+
content = resp[content_key]
|
121
|
+
return if content.empty?
|
122
|
+
|
123
|
+
puts "Command standard #{type}:"
|
124
|
+
# "https://s3.amazonaws.com/lr-infrastructure-prod/ssm/commands/sonic/0a4f4bef-8f63-4235-8b30-ae296477261a/i-0b2e6e187a3f9ada9/awsrunPowerShellScript/0.awsrunPowerShellScript/stderr">
|
125
|
+
if content.include?("--output truncated--") && !resp[s3_key].empty?
|
126
|
+
s3_url = resp[s3_key]
|
127
|
+
info = s3_url.sub('https://s3.amazonaws.com/', '').split('/')
|
128
|
+
bucket = info[0]
|
129
|
+
key = info[1..-1].join('/')
|
130
|
+
resp = s3.get_object(bucket: bucket, key: key)
|
131
|
+
data = resp.body.read
|
132
|
+
puts data
|
133
|
+
|
134
|
+
path = "/tmp/sonic-output.txt"
|
135
|
+
puts "------"
|
136
|
+
puts "Output also written to #{path}"
|
137
|
+
IO.write(path, data)
|
138
|
+
else
|
139
|
+
puts content
|
39
140
|
end
|
141
|
+
|
142
|
+
# puts "#{s3_key}: #{resp[s3_key]}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_command(options)
|
146
|
+
retries = 0
|
147
|
+
|
148
|
+
begin
|
149
|
+
resp = ssm.send_command(options)
|
150
|
+
# puts "NOOP FOR NOW"
|
151
|
+
rescue Aws::SSM::Errors::UnsupportedPlatformType
|
152
|
+
retries += 1
|
153
|
+
# toggle AWS-RunShellScript / AWS-RunPowerShellScript
|
154
|
+
options[:document_name] =
|
155
|
+
options[:document_name] == "AWS-RunShellScript" ?
|
156
|
+
"AWS-RunPowerShellScript" : "AWS-RunShellScript"
|
157
|
+
|
158
|
+
puts "#{$!}"
|
159
|
+
puts "Retrying with document_name #{options[:document_name]}"
|
160
|
+
puts "Retries: #{retries}"
|
161
|
+
|
162
|
+
retries <= 1 ? retry : raise
|
163
|
+
end
|
164
|
+
|
165
|
+
resp
|
40
166
|
end
|
41
167
|
|
42
168
|
def build_ssm_options
|
43
169
|
criteria = transform_filter(@filter)
|
44
170
|
command = build_command(@command)
|
45
|
-
criteria.merge(
|
46
|
-
document_name: "AWS-RunShellScript",
|
47
|
-
comment: "sonic #{ARGV.join(' ')}",
|
48
|
-
parameters: {
|
49
|
-
"commands" => command
|
50
|
-
}
|
171
|
+
options = criteria.merge(
|
172
|
+
document_name: "AWS-RunShellScript", # default
|
173
|
+
comment: "sonic #{ARGV.join(' ')}"[0..99], # comment has a max of 100 chars
|
174
|
+
parameters: { "commands" => command }
|
51
175
|
)
|
176
|
+
settings_options = settings["send_command"] || {}
|
177
|
+
options.merge(settings_options.deep_symbolize_keys)
|
178
|
+
end
|
179
|
+
|
180
|
+
def settings
|
181
|
+
@settings ||= Setting.new.data
|
52
182
|
end
|
53
183
|
|
54
184
|
#
|
@@ -125,6 +255,7 @@ You can use the following command to check registered instances to SSM.
|
|
125
255
|
EOS
|
126
256
|
UI.warn(message)
|
127
257
|
copy_paste_clipboard(ssm_describe_command)
|
258
|
+
UI.say "Pro tip: ssm describe-instance-information already in your copy/paste clipboard."
|
128
259
|
end
|
129
260
|
|
130
261
|
def file_path?(command)
|
@@ -136,7 +267,7 @@ You can use the following command to check registered instances to SSM.
|
|
136
267
|
def file_path(command)
|
137
268
|
path = command.first
|
138
269
|
path = path.sub('file://', '')
|
139
|
-
path = "#{
|
270
|
+
path = "#{Sonic.root}/#{path}"
|
140
271
|
path
|
141
272
|
end
|
142
273
|
|
@@ -150,7 +281,7 @@ You can use the following command to check registered instances to SSM.
|
|
150
281
|
instances = List.new(@options).instances
|
151
282
|
if instances.count == 0
|
152
283
|
message = <<-EOL
|
153
|
-
|
284
|
+
Unable to find any instances with filter #{@filter.join(',')}.
|
154
285
|
Are you sure you specify the filter with either a EC2 tag or list instance ids?
|
155
286
|
If you are using ECS identifiers, they are not supported with this command.
|
156
287
|
EOL
|
@@ -170,16 +301,20 @@ EOL
|
|
170
301
|
text =~ /i-.{17}/ || text =~ /i-.{8}/
|
171
302
|
end
|
172
303
|
|
173
|
-
def
|
174
|
-
list_command = "aws ssm list-commands --command-id #{command_id}"
|
304
|
+
def display_ssm_commands(command_id, ssm_options)
|
305
|
+
list_command = " aws ssm list-commands --command-id #{command_id}"
|
175
306
|
UI.say list_command
|
176
|
-
|
307
|
+
|
308
|
+
return unless ssm_options[:instance_ids]
|
309
|
+
ssm_options[:instance_ids].each do |instance_id|
|
310
|
+
get_command = " aws ssm get-command-invocation --command-id #{command_id} --instance-id #{instance_id}"
|
311
|
+
UI.say get_command
|
312
|
+
end
|
177
313
|
end
|
178
314
|
|
179
315
|
def copy_paste_clipboard(command)
|
180
316
|
return unless RUBY_PLATFORM =~ /darwin/
|
181
317
|
system("echo '#{command}' | pbcopy")
|
182
|
-
UI.say "Pro tip: the aws ssm command is already in your copy/paste clipboard."
|
183
318
|
end
|
184
319
|
end
|
185
320
|
end
|