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.
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