class Sonic::Execute
Public Class Methods
new(command, options)
click to toggle source
# File lib/sonic/execute.rb, line 8 def initialize(command, options) @command = command @options = options @tags = @options[:tags] @instance_ids = @options[:instance_ids] end
Public Instance Methods
build_command(command)
click to toggle source
# File lib/sonic/execute.rb, line 271 def build_command(command) if file_path?(command) path = file_path(command) if File.exist?(path) IO.readlines(path).map {|s| s.strip} else UI.error("File #{path} could not be found. Are you sure it exist?") exit 1 end else # The script is being feed inline so just join the command together into one script. # Still keep in an array form because that's how ssn.send_command works with AWS-RunShellScript # usually reads the command. command.is_a?(Array) ? command : [command] end end
build_ssm_options()
click to toggle source
# File lib/sonic/execute.rb, line 197 def build_ssm_options criteria = transform_filter_option command = build_command(@command) comment = @options[:comment] || "sonic #{ARGV.join(' ')}" comment = comment[0..99] # comment has a max of 100 chars parameters = { "commands" => command } t = @options[:execution_timeout] || @options[:timeout] parameters[:executionTimeout] = [t.to_s] if t # weird but executionTimeout expects an Array with String element options = criteria.merge( document_name: "AWS-RunShellScript", # default comment: comment, parameters: parameters, # Default CloudWatchLog settings. Can be overwritten with settings.yml send_command # IMPORTANT: make sure the EC2 instance the command runs on has access to write to CloudWatch Logs. cloud_watch_output_config: { # cloud_watch_log_group_name: "ssm", # Defaults to /aws/ssm/AWS-RunShellScript (aws/ssm/SystemsManagerDocumentName https://amzn.to/38TKVse) cloud_watch_output_enabled: true, }, ) t = @options[:execution_timeout] || @options[:timeout] options[:timeout_seconds] = t.to_s if t settings_options = settings["send_command"] || {} options.merge(settings_options.deep_symbolize_keys) end
check_filter_options!()
click to toggle source
# File lib/sonic/execute.rb, line 230 def check_filter_options! return if @tags || @instance_ids puts "ERROR: Please provide --tags or --instance-ids option".color(:red) exit 1 end
check_instances()
click to toggle source
Counts the number of instances found using the filter and displays a helpful message to the user if 0 found.
# File lib/sonic/execute.rb, line 317 def check_instances return if @options[:zero_warn] == false # The list options is a superset of the execute options so we can pass # it right through instances = List.new(@options).instances if instances.count == 0 message = <<-EOL Unable to find any instances with filter #{@filter.join(',')}. Are you sure you specify the filter with either a EC2 tag or list instance ids? If you are using ECS identifiers, they are not supported with this command. EOL UI.warn(message) end instances.count end
cli?()
click to toggle source
# File lib/sonic/execute.rb, line 78 def cli? $0.include?('sonic') end
colorized_status(status)
click to toggle source
# File lib/sonic/execute.rb, line 134 def colorized_status(status) case status when "Success" status.color(:green) when "Failed" status.color(:red) else status end end
copy_paste_clipboard(command, text)
click to toggle source
# File lib/sonic/execute.rb, line 356 def copy_paste_clipboard(command, text) return unless RUBY_PLATFORM =~ /darwin/ system("echo '#{command}' | pbcopy") UI.say text end
display_console_url(command_id)
click to toggle source
# File lib/sonic/execute.rb, line 125 def display_console_url(command_id) region = `aws configure get region`.strip rescue 'us-east-1' console_url = "https://#{region}.console.aws.amazon.com/systems-manager/run-command/#{command_id}" puts "To see the more output details visit:" puts " #{console_url}" puts copy_paste_clipboard(console_url, "Pro tip: the console url is already in your copy/paste clipboard.") end
display_ssm_commands(command_id, ssm_options)
click to toggle source
# File lib/sonic/execute.rb, line 345 def display_ssm_commands(command_id, ssm_options) list_command = " aws ssm list-commands --command-id #{command_id}" UI.say list_command return unless ssm_options[:instance_ids] ssm_options[:instance_ids].each do |instance_id| get_command = " aws ssm get-command-invocation --command-id #{command_id} --instance-id #{instance_id}" UI.say get_command end end
display_ssm_output(command_id)
click to toggle source
# File lib/sonic/execute.rb, line 99 def display_ssm_output(command_id) resp = ssm.list_command_invocations(command_id: command_id) command_invocations = resp.command_invocations command_invocation = command_invocations.first unless command_invocation puts "WARN: No instances found that matches the --tags or --instance-ids option".color(:yellow) return false # instances_found end instance_id = command_invocation.instance_id if command_invocations.size > 1 puts "Multiple instance targets. Total targets: #{command_invocations.size}. Only displaying output for #{instance_id}." else puts "Displaying output for #{instance_id}." end resp = ssm.get_command_invocation( command_id: command_id, instance_id: instance_id ) puts "Command status: #{colorized_status(resp["status"])}" ssm_output(resp, "output") ssm_output(resp, "error") puts true # instances_found end
execute()
click to toggle source
aws ssm send-command \
--instance-ids i-030033c20c54bf149 \ --document-name "AWS-RunShellScript" \ --comment "Demo run shell script on Linux Instances" \ --parameters '{"commands":["#!/usr/bin/python","print \"Hello world from python\""]}' \ --query "Command.CommandId"
# File lib/sonic/execute.rb, line 21 def execute check_filter_options! ssm_options = build_ssm_options if @options[:noop] UI.noop = true command_id = "fake command id for noop mode" success = true # fake it for specs else instances_count = check_instances return unless instances_count > 0 success = nil puts "Sending command to SSM with options:" puts YAML.dump(ssm_options.deep_stringify_keys) puts begin resp = send_command(ssm_options) command_id = resp.command.command_id success = true rescue Aws::SSM::Errors::InvalidInstanceId => e ssm_invalid_instance_error_message(e) end end return unless success # IF COMMAND IS ONLY ON A SINGLE INSTANCE THEN WILL DISPLAY A BUNCH OF # INFO ON THE INSTANCE. IF ITS A LOT OF INSTANCES, THEN SHOW A SUMMARY # OF COMMANDS THAT WILL LEAD TO THE OUTPUT OF EACH INSTANCE. UI.say "Command sent to AWS SSM. To check the details of the command:" display_ssm_commands(command_id, ssm_options) puts return if @options[:noop] status = wait(command_id) display_ssm_output(command_id) display_console_url(command_id) if status == "Success" puts "Command successful: #{status}".color(:green) exit_status(0) else puts "Command unsuccessful: #{status}".color(:red) exit_status(1) end end
exit_status(code)
click to toggle source
# File lib/sonic/execute.rb, line 68 def exit_status(code) exit(code) if cli? if code == 0 true else raise "Error running command" end end
file_path(command)
click to toggle source
# File lib/sonic/execute.rb, line 308 def file_path(command) path = command.first path = path.sub('file://', '') path = "#{Sonic.root}/#{path}" path end
file_path?(command)
click to toggle source
# File lib/sonic/execute.rb, line 302 def file_path?(command) return false unless command.size == 1 possible_path = command.first possible_path.include?("file://") end
instance_id?(text)
click to toggle source
# File lib/sonic/execute.rb, line 339 def instance_id?(text) # new format is 17 characters long after i- # old format is 8 characters long after i- text =~ /i-.{17}/ || text =~ /i-.{8}/ end
send_command(options)
click to toggle source
# File lib/sonic/execute.rb, line 175 def send_command(options) retries = 0 begin resp = ssm.send_command(options) rescue Aws::SSM::Errors::UnsupportedPlatformType retries += 1 # toggle AWS-RunShellScript / AWS-RunPowerShellScript options[:document_name] = options[:document_name] == "AWS-RunShellScript" ? "AWS-RunPowerShellScript" : "AWS-RunShellScript" puts "#{$!}" puts "Retrying with document_name #{options[:document_name]}" puts "Retries: #{retries}" retries <= 1 ? retry : raise end resp end
settings()
click to toggle source
# File lib/sonic/execute.rb, line 226 def settings @settings ||= Setting.new.data end
ssm_invalid_instance_error_message(e)
click to toggle source
e = Aws::SSM::Errors::InvalidInstanceId
# File lib/sonic/execute.rb, line 289 def ssm_invalid_instance_error_message(e) # e.message is an empty string so not very helpful ssm_describe_command = 'aws ssm describe-instance-information --output text --query "InstanceInformationList[*]"' message = <<-EOS One of the instance ids: #{@filter.join(",")} is invalid according to SSM. This might be because the SSM agent on the instance has not yet checked in. You can use the following command to check registered instances to SSM. #{ssm_describe_command} EOS UI.warn(message) copy_paste_clipboard(ssm_describe_command, "Pro tip: ssm describe-instance-information already in your copy/paste clipboard.") end
ssm_output(resp, type)
click to toggle source
type: output or error
# File lib/sonic/execute.rb, line 146 def ssm_output(resp, type) content_key = "standard_#{type}_content" s3_key = "standard_#{type}_url" content = resp[content_key] return if content.empty? puts "Command standard #{type}:" # "https://s3.amazonaws.com/infra-prod/ssm/commands/sonic/0a4f4bef-8f63-4235-8b30-ae296477261a/i-0b2e6e187a3f9ada9/awsrunPowerShellScript/0.awsrunPowerShellScript/stderr"> if content.include?("--output truncated--") && !resp[s3_key].empty? s3_url = resp[s3_key] info = s3_url.sub('https://s3.amazonaws.com/', '').split('/') bucket = info[0] key = info[1..-1].join('/') resp = s3.get_object(bucket: bucket, key: key) data = resp.body.read puts data path = "/tmp/sonic-output.txt" puts "------" puts "Output also written to #{path}" IO.write(path, data) else puts content end # puts "#{s3_key}: #{resp[s3_key]}" end
tag_name()
click to toggle source
TODO: make configurable
# File lib/sonic/execute.rb, line 335 def tag_name "Name" end
transform_filter_option()
click to toggle source
Public: Transform the filter to the ssm send_command
equivalent options
filter - CLI
filter option. Example: hi-web-prod hi-worker-prod hi-clock-prod i-0f7f833131a51ce35
Examples
transform_filter_option # => { instance_ids: ["i-006a097bb10643e20"], targets: [{key: "Name", values: "hi-web-prod,hi-worker-prod"}] }
Returns the duplicated String.
# File lib/sonic/execute.rb, line 250 def transform_filter_option if @tags list = @tags.split(';') targets = list.inject([]) do |final,item| tag_name,value_list = item.split('=') values = value_list.split(',').map(&:strip) # structure expected by ssm send_command option = { key: "tag:#{tag_name}", values: values } final << option final end {targets: targets} else # @instance_ids instance_ids = @instance_ids.split(',') {instance_ids: instance_ids} end end
wait(command_id)
click to toggle source
# File lib/sonic/execute.rb, line 82 def wait(command_id) ongoing_states = ["Pending", "InProgress", "Delayed"] print "Waiting for ssm command to finish..." resp = ssm.list_commands(command_id: command_id) status = resp["commands"].first["status"] while ongoing_states.include?(status) resp = ssm.list_commands(command_id: command_id) status = resp["commands"].first["status"] sleep 1 print '.' end puts "\nCommand finished." puts status end