Class: CapifyEc2

Inherits:
Object
  • Object
show all
Defined in:
lib/capify-ec2.rb

Constant Summary collapse

SLEEP_COUNT =
5

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ec2_config = "config/ec2.yml", stage = '') ⇒ CapifyEc2

Returns a new instance of CapifyEc2.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/capify-ec2.rb', line 16

def initialize(ec2_config = "config/ec2.yml", stage = '')
  case ec2_config
  when Hash
    @ec2_config = ec2_config
  when String
    @ec2_config = YAML.load_file ec2_config
  else
    raise ArgumentError, "Invalid ec2_config: #{ec2_config.inspect}"
  end
  @ec2_config[:stage] = stage

  # Maintain backward compatibility with previous config format
  @ec2_config[:project_tags] ||= []
  # User can change the Project tag string
  @ec2_config[:aws_project_tag] ||= "Project"
  # User can change the Roles tag string
  @ec2_config[:aws_roles_tag] ||= "Roles"
  # User can change the Options tag string.
  @ec2_config[:aws_options_tag] ||= "Options"
  # User can change the Stages tag string
  @ec2_config[:aws_stages_tag] ||= "Stages"

  @ec2_config[:project_tags] << @ec2_config[:project_tag] if @ec2_config[:project_tag]

  regions = determine_regions()

  @instances = []
  @elbs = elb.load_balancers

  regions.each do |region|
    begin
      servers = Fog::Compute.new( {:provider => 'AWS', :region => region}.merge!(security_credentials) ).servers
    rescue => e
      puts "[Capify-EC2] Unable to connect to AWS: #{e}.".red.bold
      exit 1
    end
    
    servers.each do |server|
      @instances << server if server.ready?
    end
  end
end

Instance Attribute Details

#ec2_configObject

Returns the value of attribute ec2_config.



10
11
12
# File 'lib/capify-ec2.rb', line 10

def ec2_config
  @ec2_config
end

#instancesObject

Returns the value of attribute instances.



10
11
12
# File 'lib/capify-ec2.rb', line 10

def instances
  @instances
end

#load_balancerObject

Returns the value of attribute load_balancer.



10
11
12
# File 'lib/capify-ec2.rb', line 10

def load_balancer
  @load_balancer
end

Instance Method Details

#aws_access_key_idObject



72
73
74
# File 'lib/capify-ec2.rb', line 72

def aws_access_key_id
  @ec2_config[:aws_access_key_id] || Fog.credentials[:aws_access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || @ec2_config[:use_iam_profile] || raise("Missing AWS Access Key ID")
end

#aws_secret_access_keyObject



76
77
78
# File 'lib/capify-ec2.rb', line 76

def aws_secret_access_key
  @ec2_config[:aws_secret_access_key] || Fog.credentials[:aws_secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || @ec2_config[:use_iam_profile] || raise("Missing AWS Secret Access Key")
end

#deregister_instance_from_elb(instance_name) ⇒ Object



255
256
257
258
259
260
261
262
263
# File 'lib/capify-ec2.rb', line 255

def deregister_instance_from_elb(instance_name)
  return unless @ec2_config[:load_balanced]
  instance = get_instance_by_name(instance_name)
  return if instance.nil?
  @@load_balancer = get_load_balancer_by_instance(instance.id)
  return if @@load_balancer.nil?

  elb.deregister_instances_from_load_balancer(instance.id, @@load_balancer.id)
end

#deregister_instance_from_elb_by_dns(server_dns) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/capify-ec2.rb', line 292

def deregister_instance_from_elb_by_dns(server_dns)
  instance = get_instance_by_dns(server_dns)
  load_balancer = get_load_balancer_by_instance(instance.id)

  if load_balancer
    puts "[Capify-EC2] Removing instance from ELB '#{load_balancer.id}'..."

    result = elb.deregister_instances_from_load_balancer(instance.id, load_balancer.id)
    raise "Unable to remove instance from ELB '#{load_balancer.id}'..." unless result.status == 200

    return load_balancer
  end
  false
end

#desired_instances(region = nil) ⇒ Object



204
205
206
207
# File 'lib/capify-ec2.rb', line 204

def desired_instances(region = nil)
  instances = @ec2_config[:project_tags].empty? ? @instances : project_instances
  @ec2_config[:stage].to_s.empty? ? instances : get_instances_by_stage(instances)
end

#determine_regionsObject



68
69
70
# File 'lib/capify-ec2.rb', line 68

def determine_regions()
  @ec2_config[:aws_params][:regions] || [@ec2_config[:aws_params][:region]]
end

#display_elbsObject



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/capify-ec2.rb', line 134

def display_elbs
  unless @elbs.any?
    puts "[Capify-EC2] No elastic load balancers were found using your 'ec2.yml' configuration.".red.bold
    return
  end
  puts "Elastic Load Balancers".bold
  puts "#{@ec2_config[:aws_project_tag].bold}: #{@ec2_config[:project_tags].join(', ')}." if @ec2_config[:project_tags].any?
  
  # Set minimum widths for the variable length lb attributes.
  column_widths = { :id_min => 4, :dns_min => 4, :zone_min => 5}

  # Find the longest attribute across all instances, to format the columns properly.
  column_widths[:id] = @elbs.map{|i| i.id.to_s.ljust( column_widths[:id_min] ) || ' ' * column_widths[:id_min]}.max_by(&:length).length
  column_widths[:dns] = @elbs.map{|i| i.dns_name || ' ' * column_widths[:dns_min]}.max_by(&:length).length
  column_widths[:zone] = @elbs.map{|i| i.availability_zones.join(",").to_s.ljust( column_widths[:zone_min] ) || ' ' * column_widths[:zone_min]}.max_by(&:length).length

  # Title row.
  status_output = []
  status_output << 'ID'                                .ljust( column_widths[:id]                      ).bold
  status_output << 'DNS'                               .ljust( column_widths[:dns]                     ).bold
  status_output << 'Zone'                              .ljust( column_widths[:zone]                    ).bold
  puts status_output.join("   ")

  elbs_found_for_project = false

  @elbs.each_with_index do |lb, i|
    
    status_output = []
    sub_output    = []
    lb.instances.each do |instance|
      first_match = @instances.select {|x| instance.include?(x.id)}.first

      # Skip any instances which don't match the current Project tag, if set.
      if @ec2_config[:project_tags].any?
        break unless @ec2_config[:project_tags].include?(first_match.tags[ @ec2_config[:aws_project_tag] ])
      end

      elbs_found_for_project = true

      instance_row = []
      instance_row << " ".ljust( column_widths[:id] )  # indent the length of the id column
      instance_row << "+"
      instance_row << (instance || '')                .ljust( 10 ).red
      instance_row << instance_health(lb, first_match)            .yellow
      instance_row << (first_match.name || '' )                   .cyan
      sub_output << instance_row.join("   ")
    end

    # Only display the ELB if instances matching the current Project tag were found.
    if sub_output.any?
      status_output << (lb.id || '')                   .ljust( column_widths[:id]   ).green
      status_output << lb.dns_name                     .ljust( column_widths[:dns]  ).blue.bold
      status_output << lb.availability_zones.join(",") .ljust( column_widths[:zone] ).magenta
    
      puts status_output.join("   ")
      puts sub_output.join("\n")
    end
  end

  puts "Capify-EC2] No elastic load balancers were found containing instances tagged with this project.".red.bold unless elbs_found_for_project
end

#display_instancesObject



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/capify-ec2.rb', line 80

def display_instances
  unless desired_instances and desired_instances.any?
    puts "[Capify-EC2] No instances were found using your 'ec2.yml' configuration.".red.bold
    return
  end

  # Set minimum widths for the variable length instance attributes.
  column_widths = { :name_min => 4, :type_min => 4, :dns_min => 5, :roles_min => @ec2_config[:aws_roles_tag].length, :stages_min => @ec2_config[:aws_stages_tag].length, :options_min => @ec2_config[:aws_options_tag].length }

  # Find the longest attribute across all instances, to format the columns properly.
  column_widths[:name]    = desired_instances.map{|i| i.name.to_s.ljust( column_widths[:name_min] )                                   || ' ' * column_widths[:name_min]    }.max_by(&:length).length
  column_widths[:type]    = desired_instances.map{|i| i.flavor_id                                                                     || ' ' * column_widths[:type_min]    }.max_by(&:length).length
  column_widths[:dns]     = desired_instances.map{|i| i.contact_point.to_s.ljust( column_widths[:dns_min] )                           || ' ' * column_widths[:dns_min]     }.max_by(&:length).length
  column_widths[:roles]   = desired_instances.map{|i| i.tags[@ec2_config[:aws_roles_tag]].to_s.ljust( column_widths[:roles_min] )     || ' ' * column_widths[:roles_min]   }.max_by(&:length).length
  column_widths[:stages]  = desired_instances.map{|i| i.tags[@ec2_config[:aws_stages_tag]].to_s.ljust( column_widths[:stages_min] )   || ' ' * column_widths[:stages_min]  }.max_by(&:length).length
  column_widths[:options] = desired_instances.map{|i| i.tags[@ec2_config[:aws_options_tag]].to_s.ljust( column_widths[:options_min] ) || ' ' * column_widths[:options_min] }.max_by(&:length).length

  roles_present   = desired_instances.map{|i| i.tags[@ec2_config[:aws_roles_tag]].to_s}.max_by(&:length).length > 0
  options_present = desired_instances.map{|i| i.tags[@ec2_config[:aws_options_tag]].to_s}.max_by(&:length).length > 0
  stages_present  = desired_instances.map{|i| i.tags[@ec2_config[:aws_stages_tag]].to_s}.max_by(&:length).length > 0

  # Project and Stages info..
  info_label_width = [@ec2_config[:aws_project_tag], @ec2_config[:aws_stages_tag]].map(&:length).max
  puts "#{@ec2_config[:aws_project_tag].rjust( info_label_width ).bold}: #{@ec2_config[:project_tags].join(', ')}." if @ec2_config[:project_tags].any?
  puts "#{@ec2_config[:aws_stages_tag].rjust( info_label_width ).bold}: #{@ec2_config[:stage]}." unless @ec2_config[:stage].to_s.empty?
  
  # Title row.
  status_output = []
  status_output << 'Num'                                                         .bold
  status_output << 'Name'                       .ljust( column_widths[:name]    ).bold
  status_output << 'ID'                         .ljust( 10                      ).bold
  status_output << 'Type'                       .ljust( column_widths[:type]    ).bold
  status_output << 'DNS'                        .ljust( column_widths[:dns]     ).bold
  status_output << 'Zone'                       .ljust( 10                      ).bold
  status_output << @ec2_config[:aws_stages_tag] .ljust( column_widths[:stages]  ).bold if stages_present
  status_output << @ec2_config[:aws_roles_tag]  .ljust( column_widths[:roles]   ).bold if roles_present
  status_output << @ec2_config[:aws_options_tag].ljust( column_widths[:options] ).bold if options_present
  puts status_output.join("   ")
  
  desired_instances.each_with_index do |instance, i|
    status_output = []
    status_output << "%02d:" % i
    status_output << (instance.name || '')                               .ljust( column_widths[:name]    ).green
    status_output << instance.id                                         .ljust( 2                       ).red
    status_output << instance.flavor_id                                  .ljust( column_widths[:type]    ).cyan
    status_output << instance.contact_point                              .ljust( column_widths[:dns]     ).blue.bold
    status_output << instance.availability_zone                          .ljust( 10                      ).magenta
    status_output << (instance.tags[@ec2_config[:aws_stages_tag]]  || '').ljust( column_widths[:stages]  ).yellow if stages_present
    status_output << (instance.tags[@ec2_config[:aws_roles_tag]]   || '').ljust( column_widths[:roles]   ).yellow if roles_present
    status_output << (instance.tags[@ec2_config[:aws_options_tag]] || '').ljust( column_widths[:options] ).yellow if options_present
    puts status_output.join("   ")
  end
end

#elbObject



234
235
236
# File 'lib/capify-ec2.rb', line 234

def elb
  Fog::AWS::ELB.new({:region => @ec2_config[:aws_params][:region]}.merge!(security_credentials))
end

#get_instance_by_dns(dns) ⇒ Object



226
227
228
# File 'lib/capify-ec2.rb', line 226

def get_instance_by_dns(dns)
  desired_instances.select {|instance| instance.contact_point == dns}.first
end

#get_instance_by_name(name) ⇒ Object



222
223
224
# File 'lib/capify-ec2.rb', line 222

def get_instance_by_name(name)
  desired_instances.select {|instance| instance.name == name}.first
end

#get_instances_by_region(roles, region) ⇒ Object



217
218
219
220
# File 'lib/capify-ec2.rb', line 217

def get_instances_by_region(roles, region)
  return unless region
  desired_instances.select {|instance| instance.availability_zone.match(region) && instance.tags[@ec2_config[:aws_roles_tag]].split(%r{,\s*}).include?(roles.to_s) rescue false}
end

#get_instances_by_role(role) ⇒ Object



209
210
211
# File 'lib/capify-ec2.rb', line 209

def get_instances_by_role(role)
  desired_instances.select {|instance| instance.tags[@ec2_config[:aws_roles_tag]].split(%r{,\s*}).include?(role.to_s) rescue false}
end

#get_instances_by_stage(instances = @instances) ⇒ Object



213
214
215
# File 'lib/capify-ec2.rb', line 213

def get_instances_by_stage(instances=@instances)
  instances.select {|instance| instance.tags[@ec2_config[:aws_stages_tag]].split(%r{,\s*}).include?(@ec2_config[:stage].to_s) rescue false}
end

#get_load_balancer_by_instance(instance_id) ⇒ Object



238
239
240
241
242
243
244
# File 'lib/capify-ec2.rb', line 238

def get_load_balancer_by_instance(instance_id)
  hash = elb.load_balancers.inject({}) do |collect, load_balancer|
    load_balancer.instances.each {|load_balancer_instance_id| collect[load_balancer_instance_id] = load_balancer}
    collect
  end
  hash[instance_id]
end

#get_load_balancer_by_name(load_balancer_name) ⇒ Object



246
247
248
249
250
251
252
253
# File 'lib/capify-ec2.rb', line 246

def get_load_balancer_by_name(load_balancer_name)
  lbs = {}
  elb.load_balancers.each do |load_balancer|
    lbs[load_balancer.id] = load_balancer
  end
  lbs[load_balancer_name]

end

#instance_health(load_balancer, instance) ⇒ Object



230
231
232
# File 'lib/capify-ec2.rb', line 230

def instance_health(load_balancer, instance)
  elb.describe_instance_health(load_balancer.id, instance.id).body['DescribeInstanceHealthResult']['InstanceStates'][0]['State']
end

#instance_health_by_url(dns, port, path, expected_response, options = {}) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/capify-ec2.rb', line 335

def instance_health_by_url(dns, port, path, expected_response, options = {})
  def response_matches_expected?(response, expected_response)
    if expected_response.kind_of?(Array)
      expected_response.any?{ |r| response_matches_expected?(response, r) }
    elsif expected_response.kind_of?(Regexp)
      (response =~ expected_response) != nil
    else
      response == expected_response
    end
  end

  protocol = options[:https] ? 'https://' : 'http://'
  uri = URI("#{protocol}#{dns}:#{port}#{path}")

  puts "[Capify-EC2] Checking '#{uri}' for the content '#{expected_response.inspect}'..."

  http = Net::HTTP.new(uri.host, uri.port)

  if uri.scheme == 'https'
   http.use_ssl = true
   http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end

  result = nil

  begin
    Timeout::timeout(options[:timeout]) do
      begin
        if(options[:via].to_s.downcase == "post")
          result = http.post(uri.path, options[:data])
        else
          result = http.get(uri.path)
        end
        raise "Server responded with '#{result.code}: #{result.body}', expected '#{expected_response}'" unless response_matches_expected?(result.body, expected_response)
      rescue => e
        puts "[Capify-EC2] Unexpected response: #{e}..."
        sleep 1
        retry
      end
    end
  rescue Timeout::Error => e
  end
  result ? response_matches_expected?(result.body, expected_response) : false
end

#project_instancesObject



200
201
202
# File 'lib/capify-ec2.rb', line 200

def project_instances
  @instances.select {|instance| @ec2_config[:project_tags].include?(instance.tags[@ec2_config[:aws_project_tag]])}
end

#register_instance_in_elb(instance_name, load_balancer_name = '') ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/capify-ec2.rb', line 265

def register_instance_in_elb(instance_name, load_balancer_name = '')
  return if !@ec2_config[:load_balanced]
  instance = get_instance_by_name(instance_name)
  return if instance.nil?
  load_balancer =  get_load_balancer_by_name(load_balancer_name) || @@load_balancer
  return if load_balancer.nil?

  elb.register_instances_with_load_balancer(instance.id, load_balancer.id)

  fail_after = @ec2_config[:fail_after] || 30
  state = instance_health(load_balancer, instance)
  time_elapsed = 0

  while time_elapsed < fail_after
    break if state == "InService"
    sleep SLEEP_COUNT
    time_elapsed += SLEEP_COUNT
    STDERR.puts 'Verifying Instance Health'
    state = instance_health(load_balancer, instance)
  end
  if state == 'InService'
    STDERR.puts "#{instance.name}: Healthy"
  else
    STDERR.puts "#{instance.name}: tests timed out after #{time_elapsed} seconds."
  end
end

#reregister_instance_with_elb_by_dns(server_dns, load_balancer, timeout) ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/capify-ec2.rb', line 307

def reregister_instance_with_elb_by_dns(server_dns, load_balancer, timeout)
  instance = get_instance_by_dns(server_dns)

  sleep 10

  puts "[Capify-EC2] Re-registering instance with ELB '#{load_balancer.id}'..."
  result = elb.register_instances_with_load_balancer(instance.id, load_balancer.id)

  raise "Unable to re-register instance with ELB '#{load_balancer.id}'..." unless result.status == 200

  state = nil

  begin
    Timeout::timeout(timeout) do
      begin
        state = instance_health(load_balancer, instance)
        raise "Instance not ready" unless state == 'InService'
      rescue => e
        puts "[Capify-EC2] Unexpected response: #{e}..."
        sleep 1
        retry
      end
    end
  rescue Timeout::Error => e
  end
  state ? state == 'InService' : false
end

#response_matches_expected?(response, expected_response) ⇒ Boolean

Returns:

  • (Boolean)


336
337
338
339
340
341
342
343
344
# File 'lib/capify-ec2.rb', line 336

def response_matches_expected?(response, expected_response)
  if expected_response.kind_of?(Array)
    expected_response.any?{ |r| response_matches_expected?(response, r) }
  elsif expected_response.kind_of?(Regexp)
    (response =~ expected_response) != nil
  else
    response == expected_response
  end
end

#security_credentialsObject



59
60
61
62
63
64
65
66
# File 'lib/capify-ec2.rb', line 59

def security_credentials
  if @ec2_config[:use_iam_profile]
    { :use_iam_profile       => true }
  else
    { :aws_access_key_id     => aws_access_key_id,
      :aws_secret_access_key => aws_secret_access_key }
  end
end

#server_namesObject



196
197
198
# File 'lib/capify-ec2.rb', line 196

def server_names
  desired_instances.map {|instance| instance.name}
end