sonic-screwdriver 1.4.0 → 2.0.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/bin/commit_docs.sh +26 -0
  3. data/.circleci/config.yml +70 -0
  4. data/.gitignore +1 -1
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +13 -2
  7. data/Gemfile +3 -3
  8. data/Gemfile.lock +43 -14
  9. data/Guardfile +17 -10
  10. data/LICENSE.txt +2 -2
  11. data/README.md +10 -10
  12. data/Rakefile +9 -2
  13. data/docs/_config.yml +3 -0
  14. data/docs/_docs/help.md +1 -1
  15. data/docs/_docs/install-bastion.md +5 -15
  16. data/docs/_docs/install.md +3 -3
  17. data/docs/_docs/settings.md +40 -56
  18. data/docs/_docs/tutorial-ecs-exec.md +16 -20
  19. data/docs/_docs/tutorial-ecs-sh.md +73 -0
  20. data/docs/_docs/tutorial-execute.md +93 -17
  21. data/docs/_docs/tutorial-ssh.md +13 -18
  22. data/docs/_docs/why-ec2-run-command.md +1 -1
  23. data/docs/_includes/commands.html +5 -5
  24. data/docs/_includes/content.html +5 -0
  25. data/docs/_includes/css/main.css +15 -9
  26. data/docs/_includes/css/sonic.css +7 -5
  27. data/docs/_includes/example.html +4 -4
  28. data/docs/_includes/reference.md +1 -0
  29. data/docs/_includes/subnav.html +2 -1
  30. data/docs/_reference/sonic-completion.md +44 -0
  31. data/docs/_reference/sonic-completion_script.md +25 -0
  32. data/docs/_reference/sonic-ecs-exec.md +30 -0
  33. data/docs/_reference/sonic-ecs-help.md +21 -0
  34. data/docs/_reference/sonic-ecs-sh.md +35 -0
  35. data/docs/_reference/sonic-ecs.md +25 -0
  36. data/docs/_reference/sonic-execute.md +84 -0
  37. data/docs/_reference/sonic-list.md +40 -0
  38. data/docs/_reference/sonic-ssh.md +86 -0
  39. data/docs/_reference/sonic-version.md +21 -0
  40. data/docs/img/tutorials/ec2-console-run-command.png +0 -0
  41. data/docs/quick-start.md +9 -10
  42. data/docs/reference.md +12 -0
  43. data/{bin → exe}/sonic +3 -3
  44. data/lib/bash_scripts/docker-exec.sh +1 -0
  45. data/lib/bash_scripts/docker-run.sh +8 -1
  46. data/lib/sonic.rb +10 -2
  47. data/lib/sonic/{aws_services.rb → aws_service.rb} +6 -1
  48. data/lib/sonic/base_command.rb +82 -0
  49. data/lib/sonic/cli.rb +37 -27
  50. data/lib/sonic/command.rb +8 -22
  51. data/lib/sonic/completer.rb +161 -0
  52. data/lib/sonic/completer/script.rb +6 -0
  53. data/lib/sonic/completer/script.sh +10 -0
  54. data/lib/sonic/core.rb +15 -0
  55. data/lib/sonic/default/settings.yml +6 -16
  56. data/lib/sonic/docker.rb +29 -1
  57. data/lib/sonic/ecs.rb +22 -0
  58. data/lib/sonic/execute.rb +153 -18
  59. data/lib/sonic/help.rb +9 -0
  60. data/lib/sonic/help/command/send.md +10 -0
  61. data/lib/sonic/help/completion.md +22 -0
  62. data/lib/sonic/help/completion_script.md +3 -0
  63. data/lib/sonic/help/ecs/exec.md +8 -0
  64. data/lib/sonic/help/ecs/sh.md +13 -0
  65. data/lib/sonic/help/execute.md +60 -0
  66. data/lib/sonic/help/list.md +17 -0
  67. data/lib/sonic/help/ssh.md +60 -0
  68. data/lib/sonic/list.rb +4 -1
  69. data/lib/sonic/setting.rb +47 -0
  70. data/lib/sonic/ssh.rb +41 -20
  71. data/lib/sonic/ssh/identifier_detector.rb +6 -2
  72. data/lib/sonic/version.rb +1 -1
  73. data/sonic.gemspec +14 -9
  74. data/spec/lib/cli_spec.rb +5 -10
  75. data/spec/lib/sonic/execute_spec.rb +0 -1
  76. data/spec/spec_helper.rb +18 -10
  77. metadata +115 -16
  78. data/docs/_docs/tutorial-ecs-run.md +0 -100
  79. data/lib/sonic/cli/help.rb +0 -152
  80. 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 < Command
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
- class_option :cluster, desc: "ECS Cluster to use. Default cluster is default"
9
- class_option :bastion, desc: "Bastion jump host to use. Defaults to no bastion server."
10
- class_option :project_root, desc: "Project root. Useful for testing.", hide: true
11
-
12
- desc "ssh [IDENTIFER]", "ssh into a instance using identifier. identifer can be several things: instance id, ec2 tag, ECS service name, etc"
13
- long_desc Help.ssh
14
- method_option :keys, :aliases => '-i', :desc => "comma separated list of ssh private key paths"
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 "ecs-exec [ECS_SERVICE]", "docker exec into running docker container associated with the service on a container instance"
21
- long_desc Help.ecs_exec
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]", "lists ec2 instances"
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 < Thor
5
- class << self
6
- def dispatch(m, args, options, config)
7
- # Allow calling for help via:
8
- # sonic command help
9
- # sonic command -h
10
- # sonic command --help
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
@@ -0,0 +1,6 @@
1
+ class Sonic::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _sonic() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(sonic completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _sonic sonic
data/lib/sonic/core.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Sonic
5
+ module Core
6
+ def root
7
+ path = ENV['SONIC_ROOT'] || '.'
8
+ Pathname.new(path)
9
+ end
10
+
11
+ def profile
12
+ ENV['SONIC_PROFILE'] || ENV['AWS_PROFILE']
13
+ end
14
+ end
15
+ end
@@ -1,16 +1,6 @@
1
- bastion: # cluster_host mapping
2
- default: # default is nil - which means a bastion host wont be used
3
- # Examples:
4
- # prod: bastion.mydomain.com
5
- # stag: ubuntu@bastion-stag.mydomain.com
6
- host_key_check: false
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 = @bastion ? bastion_host : ssh_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 AwsServices
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 = ssm.send_command(ssm_options)
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
- if success
37
- UI.say "Command sent to AWS SSM. To check the details of the command:"
38
- display_list_command(command_id)
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 = "#{@options[:project_root]}/#{path}" if @options[:project_root]
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
- Unable to find any instances with filter #{@filter.join(',')}.
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 display_list_command(command_id)
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
- copy_paste_clipboard(list_command)
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