class EC2Ctl::Client
Constants
- COMMAND_STATUS_FINISHED
- COMMAND_STATUS_NOT_SUCCEEDED
- CommandNotSucceeded
- INSTANCE_IDS_LIMIT
- InvalidFilter
- NoInstanceForExecCommand
- PingCheckError
- PrivateKeyRequired
- SSM_DOCUMENT_NAMES
Public Class Methods
new( logger: nil, load_balancer_name: nil, platform_type: "Linux", skip_ping_check: false, commands: [], working_directory: nil, execution_timeout: 3600, skip_command_waits: false, wait_interval: 5, timeout_seconds: nil, comment: "Started at
click to toggle source
# File lib/ec2ctl/client.rb, line 23 def initialize( logger: nil, load_balancer_name: nil, platform_type: "Linux", skip_ping_check: false, commands: [], working_directory: nil, execution_timeout: 3600, skip_command_waits: false, wait_interval: 5, timeout_seconds: nil, comment: "Started at #{Time.now} (#{self.class}@#{VERSION})", output_s3_bucket_name: nil, output_s3_key_prefix: nil, service_role_arn: nil, notification_arn: nil, notification_events: nil, notification_type: nil, rolling_group_size: 1, skip_draining_waits: false, skip_inservice_waits: false, inservice_wait_timeout: 180, instance_ids: [], filters: [], attributes: [], search: [], count: false, sort: nil, private_key_file: nil ) @logger = logger @ec2_resource = Aws::EC2::Resource.new @elb_client = Aws::ElasticLoadBalancing::Client.new @ssm_client = Aws::SSM::Client.new @load_balancer_name = load_balancer_name @platform_type = platform_type @skip_ping_check = skip_ping_check @commands = commands @working_directory = working_directory @execution_timeout = execution_timeout @skip_command_waits = skip_command_waits @wait_interval = wait_interval @timeout_seconds = timeout_seconds @comment = comment @output_s3_bucket_name = output_s3_bucket_name @output_s3_key_prefix = output_s3_key_prefix @service_role_arn = service_role_arn @notification_arn = notification_arn @notification_events = notification_events @notification_type = notification_type @skip_draining_waits = skip_draining_waits @skip_inservice_waits = skip_inservice_waits @inservice_wait_timeout = inservice_wait_timeout @instance_ids = instance_ids @filters = filters @attributes = attributes @search = search @count = count @sort = sort @private_key_file = private_key_file if @load_balancer_name @elb_instance_ids = elb_instance_states.map(&:instance_id).select do |instance_id| if instance_ids.empty? true else instance_ids.include? instance_id end end @rolling_group_size = [rolling_group_size, @elb_instance_ids.size].min end end
Public Instance Methods
attach_instances(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 234 def attach_instances(instance_ids = []) response = @elb_client.register_instances_with_load_balancer( load_balancer_name: @load_balancer_name, instances: instance_ids.map {|i| {instance_id: i}}, ) status = { attached: instance_ids, registered: response.instances.map(&:instance_id), } if @logger if @logger.debug? @logger.debug status: status else @logger.info attached: instance_ids end end status end
detach_instances(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 212 def detach_instances(instance_ids = []) response = @elb_client.deregister_instances_from_load_balancer( load_balancer_name: @load_balancer_name, instances: instance_ids.map {|i| {instance_id: i}}, ) status = { detached: instance_ids, registered: response.instances.map(&:instance_id), } if @logger if @logger.debug? @logger.debug status: status else @logger.info detached: instance_ids end end status end
ec2_execute()
click to toggle source
# File lib/ec2ctl/client.rb, line 132 def ec2_execute ping_check ec2_instances.map(&:instance_id) unless @skip_ping_check execute_commands ec2_instances.map(&:instance_id) end
ec2_list()
click to toggle source
# File lib/ec2ctl/client.rb, line 99 def ec2_list if @logger if @logger.debug? @logger.debug ec2_instances: ec2_instances @logger.debug ssm_instance_states: ssm_instance_states(ec2_instances.map(&:instance_id)) else ec2_instances_summary = if @count @attributes.each_with_object Hash.new do |attribute, attributes_memo| attribute_hash = ec2_instances.each_with_object Hash.new(0) do |instance, instances_memo| instances_memo[query_instance_attribute(instance, attribute)] += 1 end attributes_memo[attribute] = attribute_hash end else ec2_instances.each_with_object Array.new do |instance, instances_memo| instance_summary = @attributes.each_with_object Hash.new do |attribute, attributes_memo| attributes_memo[attribute] = query_instance_attribute(instance, attribute) end instances_memo.push instance_summary end end @logger.info ec2_instances_summary: ec2_instances_summary end end { ec2_instances: ec2_instances, } end
elb_attach()
click to toggle source
# File lib/ec2ctl/client.rb, line 177 def elb_attach attach_instances @instance_ids end
elb_detach()
click to toggle source
# File lib/ec2ctl/client.rb, line 181 def elb_detach detach_instances @instance_ids end
elb_execute()
click to toggle source
# File lib/ec2ctl/client.rb, line 185 def elb_execute ping_check @elb_instance_ids unless @skip_ping_check execute_commands @elb_instance_ids end
elb_graceful()
click to toggle source
# File lib/ec2ctl/client.rb, line 190 def elb_graceful ping_check @elb_instance_ids unless @skip_ping_check @elb_instance_ids.each_slice(@rolling_group_size).to_a.tap do |instance_id_groups| instance_id_groups.each.with_index do |instance_id_group, group_index| @logger.info(progress: { completed: @rolling_group_size * group_index, remaining: @elb_instance_ids.size - @rolling_group_size * group_index, total: @elb_instance_ids.size, }) if @logger detach_instances instance_id_group wait_draining instance_id_group unless @skip_draining_waits execute_commands instance_id_group attach_instances instance_id_group wait_inservice instance_id_group unless @skip_inservice_waits end @logger.info "Everything done!" if @logger end end
elb_list()
click to toggle source
# File lib/ec2ctl/client.rb, line 137 def elb_list if @logger if @logger.debug? @logger.debug load_balancer_descriptions: load_balancer_descriptions else @logger.info load_balancer_descriptions_summary: load_balancer_descriptions.map {|lb| { load_balancer_name: lb.load_balancer_name, dns_name: lb.dns_name, instances: lb.instances.size, } } end end {load_balancer_descriptions: load_balancer_descriptions} end
elb_status()
click to toggle source
# File lib/ec2ctl/client.rb, line 155 def elb_status load_balancer_description = load_balancer_descriptions.first if @logger @logger.info elb_instance_counts: elb_instance_counts @logger.info elb_instance_states: elb_instance_states instance_ids = elb_instance_states.map(&:instance_id) @logger.debug ssm_instance_ping_counts: ssm_instance_ping_counts(instance_ids) @logger.debug ssm_instance_states: ssm_instance_states(instance_ids) @logger.debug load_balancer_description: load_balancer_description @logger.debug load_balancer_attributes: load_balancer_attributes end { load_balancer_description: load_balancer_description, load_balancer_attributes: load_balancer_attributes, elb_instance_states: elb_instance_states, } end
wait_draining(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 256 def wait_draining(instance_ids = []) connection_draining = load_balancer_attributes.connection_draining unless connection_draining.enabled @logger.info wait_draining_timeout: "Disabled.".freeze if @logger return end if @logger @logger.debug detached_instances: elb_instance_states!(instance_ids) @logger.info wait_draining_timeout: connection_draining.timeout end sleep connection_draining.timeout end
wait_inservice(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 272 def wait_inservice(instance_ids = []) s = elb_instance_states! instance_ids @logger.info wait_instance_inservice: s if @logger Timeout.timeout @inservice_wait_timeout do loop do sleep @wait_interval s = elb_instance_states! instance_ids @logger.debug wait_instance_inservice: s if @logger break if s.all? {|i| i.state == "InService".freeze} end end @logger.info wait_instance_inservice: s if @logger end
Private Instance Methods
command_console_url(command_id)
click to toggle source
# File lib/ec2ctl/client.rb, line 307 def command_console_url(command_id) [ "https://".freeze, @ssm_client.config.region, ".console.aws.amazon.com/ec2/v2/home?region=".freeze, @ssm_client.config.region, "#Commands:CommandId=".freeze, command_id, ].join end
command_params()
click to toggle source
# File lib/ec2ctl/client.rb, line 350 def command_params return @command_params if @command_params @command_params = { commands: @commands, } @command_params.update workingDirectory: [@working_directory] if @working_directory @command_params.update executionTimeout: [@execution_timeout.to_s] if @execution_timeout @command_params end
ec2_instances()
click to toggle source
# File lib/ec2ctl/client.rb, line 539 def ec2_instances return @ec2_instances if @ec2_instances params = {} params.update instance_ids: @instance_ids unless @instance_ids.empty? unless @filters.empty? filters = @filters.map do |f| match = f.match(/\A(.*)=(.*)\z/) fail InvalidFilter, "Filter should be `key=value` format (got `#{f}`)." unless match { name: match[1], values: [match[2]], } end params.update filters: filters end @ec2_instances = @ec2_resource.instances(params).to_a unless @search.empty? search_hash = @search.each_with_object Hash.new do |s, search_memo| match = s.match(/\A(.*)=(.*)\z/) fail InvalidFilter, "Search should be `key=value` format (got `#{s}`)." unless match search_memo.update match[1] => match[2] end @ec2_instances = @ec2_instances.select do |instance| search_hash.all? do |k, v| Regexp.new(v).match query_instance_attribute(instance, k) end end end @ec2_instances.sort_by! {|i| query_instance_attribute(i, @sort)} if @sort @ec2_instances end
elb_instance_counts()
click to toggle source
# File lib/ec2ctl/client.rb, line 527 def elb_instance_counts elb_instance_states.each_with_object Hash.new(0) do |i, memo| memo[i.state] += 1 end end
elb_instance_states()
click to toggle source
# File lib/ec2ctl/client.rb, line 460 def elb_instance_states @elb_instance_states ||= elb_instance_states! end
elb_instance_states!(instance_ids = nil)
click to toggle source
# File lib/ec2ctl/client.rb, line 293 def elb_instance_states!(instance_ids = nil) @elb_instance_states = @elb_client.describe_instance_health( load_balancer_name: @load_balancer_name ).instance_states if instance_ids elb_instance_states.select do |i| instance_ids.include? i.instance_id end else @elb_instance_states end end
execute_commands(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 404 def execute_commands(instance_ids = []) fail NoInstanceForExecCommand, "No instance for executing commands." if instance_ids.empty? instance_ids.each_slice INSTANCE_IDS_LIMIT do |instance_ids_slice| send_command_result = @ssm_client.send_command(send_command_params(instance_ids_slice)) command_id = send_command_result.command.command_id if @logger if @logger.debug? @logger.debug command: send_command_result.command else @logger.info( command_summary: { command_id: command_id, console_url: command_console_url(command_id), commands: @commands, instance_ids: instance_ids_slice, } ) end end unless @skip_command_waits command_invocations = wait_command_finish(command_id) if @logger if @logger.debug? @logger.debug command_invocations: command_invocations else @logger.info( command_invocations_status_counts: command_invocations.each_with_object(Hash.new(0)) {|invocation, memo| memo[invocation.status] += 1 } ) @logger.info( command_invocations_summary: command_invocations.map {|invocation| { instance_id: invocation.instance_id, status: invocation.status, output: invocation.command_plugins.first.output, } } ) end end if command_invocations.any? {|i| COMMAND_STATUS_NOT_SUCCEEDED.include? i.status} fail CommandNotSucceeded, "One or more command invocation(s) has not succeeded." end end get_command_invocations(command_id) end end
get_command_invocations(command_id)
click to toggle source
# File lib/ec2ctl/client.rb, line 332 def get_command_invocations(command_id) command_invocations = [] next_token = nil loop do response = @ssm_client.list_command_invocations( command_id: command_id, details: true, next_token: next_token, ) command_invocations += response.command_invocations next_token = response.next_token return command_invocations unless next_token end end
load_balancer_attributes()
click to toggle source
# File lib/ec2ctl/client.rb, line 521 def load_balancer_attributes @load_balancer_attributes ||= @elb_client.describe_load_balancer_attributes( load_balancer_name: @load_balancer_name, ).load_balancer_attributes end
load_balancer_descriptions()
click to toggle source
# File lib/ec2ctl/client.rb, line 500 def load_balancer_descriptions return @load_balancer_descriptions if @load_balancer_descriptions @load_balancer_descriptions = [] next_marker = nil loop do params = {marker: next_marker} params.update load_balancer_names: [@load_balancer_name] if @load_balancer_name response = @elb_client.describe_load_balancers params @load_balancer_descriptions += response.load_balancer_descriptions next_marker = response.next_marker break unless next_marker end @load_balancer_descriptions end
nortification_config()
click to toggle source
# File lib/ec2ctl/client.rb, line 362 def nortification_config return @nortification_config if @nortification_config @nortification_config = {} @nortification_config.update notification_arn: @notification_arn if @notification_arn @nortification_config.update notification_events: @notification_events if @notification_events @nortification_config.update notification_type: @notification_type if @notification_type @nortification_config end
ping_check(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 385 def ping_check(instance_ids = []) messages = [] check = instance_ids.each_with_object(Hash.new(0)) do |instance_id, memo| instance = ssm_instance_states(instance_ids).find {|si| si.instance_id == instance_id} status = instance.nil? ? "NotFound" : instance.ping_status messages.push "#{instance_id}'s SSM agent is #{status}." unless status == "Online" memo[status] += 1 end @logger.debug ping_check: check if @logger unless check.reject {|k, v| k == "Online"}.empty? fail PingCheckError, messages.join(" ") end end
query_instance_attribute(instance, attribute)
click to toggle source
# File lib/ec2ctl/client.rb, line 584 def query_instance_attribute(instance, attribute) case attribute when /\Atag:/i tag = instance.tags.find {|t| t.key == attribute.split(":")[1..-1].join(":")} tag ? tag.value : nil when /\Assm:/i ssm_instance_state = ssm_instance_states(ec2_instances.map(&:instance_id)).find do |i| i.instance_id == instance.instance_id end if ssm_instance_state query_struct(ssm_instance_state, attribute.split(":")[1..-1].join(":")) end when /\Apassword\z/i fail PrivateKeyRequired, "Private key required to decrypt password" unless @private_key_file @private_key = OpenSSL::PKey::RSA.new(File.read(File.expand_path(@private_key_file))) unless @private_key @private_key.private_decrypt Base64.decode64(instance.password_data.password_data) else query_struct(instance.data, attribute) end end
query_struct(struct, attribute)
click to toggle source
# File lib/ec2ctl/client.rb, line 608 def query_struct(struct, attribute) attribute.split(".").map(&:intern).inject struct do |_acc, method| _acc.send method end end
send_command_params(instance_ids)
click to toggle source
# File lib/ec2ctl/client.rb, line 372 def send_command_params(instance_ids) { document_name: SSM_DOCUMENT_NAMES[@platform_type.intern], instance_ids: instance_ids, parameters: command_params, comment: @comment, output_s3_bucket_name: @output_s3_bucket_name, output_s3_key_prefix: @output_s3_key_prefix, service_role_arn: @service_role_arn, notification_config: @notification_config, } end
ssm_instance_ping_counts(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 533 def ssm_instance_ping_counts(instance_ids = []) ssm_instance_states(instance_ids).each_with_object Hash.new(0) do |i, memo| memo[i.ping_status] += 1 end end
ssm_instance_states(instance_ids = [])
click to toggle source
# File lib/ec2ctl/client.rb, line 464 def ssm_instance_states(instance_ids = []) return @ssm_instance_states if @ssm_instance_states @ssm_instance_states = [] return @ssm_instance_states if instance_ids.empty? instance_ids.each_slice 50 do |slice| next_token = nil loop do response = @ssm_client.describe_instance_information( next_token: next_token, instance_information_filter_list: [ { key: "PlatformTypes".freeze, value_set: [@platform_type], }, { key: "InstanceIds".freeze, value_set: slice, }, ], ) @ssm_instance_states += response.instance_information_list next_token = response.next_token break unless next_token end end @ssm_instance_states end
wait_command_finish(command_id)
click to toggle source
# File lib/ec2ctl/client.rb, line 318 def wait_command_finish(command_id) Timeout.timeout(@execution_timeout + @wait_interval * 3) do loop do command_invocations = get_command_invocations(command_id) @logger.debug command_invocations: command_invocations if @logger return command_invocations if command_invocations.all? {|i| COMMAND_STATUS_FINISHED.include? i.status} sleep @wait_interval end end end