lib/cloud_providers/ec2/ec2.rb in poolparty-1.3.15 vs lib/cloud_providers/ec2/ec2.rb in poolparty-1.4.0

- old
+ new

@@ -1,35 +1,19 @@ =begin rdoc EC2 CloudProvider This serves as the basis for running PoolParty on Amazon's ec2 cloud. =end -require "openssl" -if OpenSSL::OPENSSL_VERSION_NUMBER < 0x00908000 - warn "the ec2 cloud provider may not work with your version of ruby and OpenSSL. Consider upgrading if you encoutner authentication errors." -end begin - require 'right_aws' + require 'AWS' rescue LoadError puts <<-EOM -Error: In order to use ec2, you need to install the right_aws gem - -Ec2 is the default cloud provider for PoolParty. If you intend on using -a different provider, specify it with: - -using :provider_name + There was an error requiring AWS EOM end -require "#{File.dirname(__FILE__)}/ec2_helpers" -require "#{File.dirname(__FILE__)}/ec2_response" -require "#{File.dirname(__FILE__)}/ec2_instance" - module CloudProviders class Ec2 < CloudProvider - - include CloudProviders::Ec2Helpers - # Set the aws keys from the environment, or load from /etc/poolparty/env.yml if the environment variable is not set def self.default_access_key ENV['EC2_ACCESS_KEY'] || load_keys_from_file[:access_key] end @@ -60,162 +44,293 @@ def self.default_cloud_cert ENV['CLOUD_CERT'] || ENV['EUCALYPTUS_CERT'] || load_keys_from_file[:cloud_cert] end # Load the yaml file containing keys. If the file does not exist, return an empty hash - def self.load_keys_from_file(filename='/etc/poolparty/env.yml', caching=true) + def self.load_keys_from_file(filename="#{ENV["HOME"]}/.poolparty/aws", caching=true) return @aws_yml if @aws_yml && caching==true return {} unless File.exists?(filename) - ddputs("Reading keys from file: #{filename}") + puts("Reading keys from file: #{filename}") @aws_yml = YAML::load( open(filename).read ) || {} end - default_options({ - :image_id => 'ami-bf5eb9d6', - :instance_type => 'm1.small', - :addressing_type => "public", - :availability_zone => "us-east-1a", - :security_group => ["default"], - :user_id => default_user_id, - :private_key => default_private_key, - :cert => default_cert, - :cloud_cert => default_cloud_cert, - :access_key => default_access_key, - :secret_access_key => default_secret_access_key, - :ec2_url => default_ec2_url, - :s3_url => default_s3_url, - :min_count => 1, - :max_count => 1, - :user_data => '', - :addressing_type => nil, - :kernel_id => nil, - :ramdisk_id => nil, - :availability_zone => nil, - :block_device_mappings => nil, - :elastic_ips => [], # An array of the elastic ips - :ebs_volumes => [] # The volume id of an ebs volume # TODO: ensure this is consistent with :block_device_mappings - }) - - - def ec2(o={}) - @ec2 ||= Rightscale::Ec2.new(access_key, secret_access_key, o.merge(:logger => PoolParty::PoolPartyLog, :endpoint_url => ec2_url)) + default_options( + :image_id => 'ami-ed46a784', + :instance_type => 'm1.small', + :addressing_type => "public", + :availability_zones => ["us-east-1a"], + :user_id => default_user_id, + :private_key => default_private_key, + :cert => default_cert, + :cloud_cert => default_cloud_cert, + :access_key => default_access_key, + :secret_access_key => default_secret_access_key, + :ec2_url => default_ec2_url, + :s3_url => default_s3_url, + :min_count => 1, + :max_count => 1, + :user_data => '', + :addressing_type => nil, + :kernel_id => nil, + :ramdisk_id => nil, + :block_device_mappings => nil, + :elastic_ips => [], # An array of the elastic ips + :ebs_volumes => [] # The volume id of an ebs volume # TODO: ensure this is consistent with :block_device_mappings + ) + + # Called when the create command is called on the cloud + def create! + [:security_groups, :load_balancers].each do |type| + self.send(type).each {|ele| ele.create! } + end end - # Start a new instance with the given options - def run_instance(o={}) - number_of_instances = o[:number_of_instances] || 1 - set_vars_from_options o - raise StandardError.new("You must pass a keypair to launch an instance, or else you will not be able to login. options = #{o.inspect}") if !keypair_name - response_array = ec2(o).run_instances(image_id, - min_count, - number_of_instances, - security_group, - keypair.basename, - user_data, - addressing_type, - instance_type, - kernel_id, - ramdisk_id, - availability_zone, - block_device_mappings - ) - instances = response_array.collect do |aws_response_hash| - Ec2Instance.new( Ec2Response.pp_format(aws_response_hash).merge(o) ) + def run + puts " for cloud: #{cloud.name}" + puts " minimum_instances: #{minimum_instances}" + puts " maximum_instances: #{maximum_instances}" + puts " security_groups: #{security_group_names.join(", ")}" + puts " running on keypair: #{keypair}" + + security_groups.each do |sg| + sg.run end - after_run_instance(instances) + unless load_balancers.empty? + load_balancers.each do |lb| + puts " load balancer: #{lb.name}" + lb.run + end + end + if autoscalers.empty? + puts "---- live, running instances (#{nodes.size}) ----" + if nodes.size < minimum_instances + expansion_count = minimum_instances - nodes.size + puts "-----> expanding the cloud because the minimum_instances is not satisified: #{expansion_count}" + expand_by(expansion_count) + elsif nodes.size > maximum_instances + contraction_count = nodes.size - maximum_instances + puts "-----> contracting the cloud because the instances count exceeds the maximum_instances by #{contraction_count}" + contract_by(contraction_count) + end + + progress_bar_until("Waiting for the instances to be launched") do + reset! + running_nodes = nodes.select {|n| n.running? } + running_nodes.size >= minimum_instances + end + reset! + # ELASTIC IPS + unless _elastic_ips.empty? + unused_elastic_ip_addresses = ElasticIp.unused_elastic_ips(self).map {|i| i.public_ip } + used_elastic_ip_addresses = ElasticIp.elastic_ips(self).map {|i| i.public_ip } + + elastic_ip_objects = ElasticIp.unused_elastic_ips(self).select {|ip_obj| _elastic_ips.include?(ip_obj.public_ip) } + + assignee_nodes = nodes.select {|n| !ElasticIp.elastic_ips(self).include?(n.public_ip) } + + elastic_ip_objects.each_with_index do |eip, idx| + # Only get the nodes that do not have elastic ips associated with them + begin + if assignee_nodes[idx] + puts "Assigning elastic ip: #{eip.public_ip} to node: #{assignee_nodes[idx].instance_id}" + ec2.associate_address(:instance_id => assignee_nodes[idx].instance_id, :public_ip => eip.public_ip) + end + rescue Exception => e + p [:error, e.inspect] + end + reset! + end + end + else + autoscalers.each do |a| + puts " autoscaler: #{a.name}" + puts "-----> The autoscaling groups will launch the instances" + a.run + + progress_bar_until("Waiting for autoscaler to launch instances") do + reset! + running_nodes = nodes.select {|n| n.running? } + running_nodes.size >= minimum_instances + end + reset! + end + end - instances.first + reset! + from_ports = security_groups.map {|a| a.authorizes.map {|t| t.from_port.to_i }.flatten }.flatten + + if from_ports.include?(22) + progress_bar_until("Waiting for the instances to be accessible by ssh") do + running_nodes = nodes.select {|n| n.running? } + accessible_count = running_nodes.map do |node| + node.accessible? + end.size + accessible_count == running_nodes.size + end + end + end - # Will select the first instance matching the provided criteria hash - def describe_instance(hash_of_criteria_to_select_instance_against) - describe_instances(hash_of_criteria_to_select_instance_against).first + def teardown + puts "------ Tearing down and cleaning up #{cloud.name} cloud" + unless autoscalers.empty? + puts "Tearing down autoscalers" + end end - # Describe instances - def describe_instances(o={}) - instants = Ec2Response.describe_instances(ec2.describe_instances).select_with_hash(o) - return [] if instants.empty? - ec2_instances = instants.collect{|i| Ec2Instance.new(dsl_options.merge(i))} - ec2_instances.sort {|a,b| a[:launch_time].to_i <=> b[:launch_time].to_i } + def expand_by(num=1) + e = Ec2Instance.run!({ + :image_id => image_id, + :min_count => num, + :max_count => num, + :key_name => keypair.basename, + :security_groups => security_groups, + :user_data => user_data, + :instance_type => instance_type, + :availability_zone => availability_zones.first, + :base64_encoded => true, + :cloud => cloud + }) + reset! + e end - # Terminate an instance (or instances) by passing :instance_id and :instance_ids - def terminate_instance!(o={}) - raise StandardError.new("You must pass an instance_id when terminating an instance with ec2") unless o[:instance_id] || o[:instance_ids] - instance_ids = o[:instance_ids] || [o[:instance_id]] - response = ec2.terminate_instances(instance_ids) - response.collect{|i| Ec2Instance.new(Ec2Response.pp_format(i)) } + def contract_by(num=1) + num.times do |i| + id = nodes[-num].instance_id + Ec2Instance.terminate!(:instance_id => id, :cloud => cloud) + end + reset! end + def bootstrap_nodes!(tmp_path=nil) + tmp_path ||= cloud.tmp_path + nodes.each do |node| + next unless node.in_service? + node.cloud_provider = self + node.rsync_dir(tmp_path) + node.bootstrap_chef! + node.run_chef! + end + end -=begin rdoc - Helper methods for the Ec2 Cloud Provider. Helpers are not necessarily supported across all CloudProviders -=end - # Are we running on amazon? - def amazon? - !['https://ec2.amazonaws.com', - 'https://us-east-1.ec2.amazonaws.com', - 'https://eu-west-1.ec2.amazonaws.com' - ].include?(ec2_url) + def configure_nodes!(tmp_path=nil) + tmp_path ||= cloud.tmp_path + nodes.each do |node| + next unless node.in_service? + node.cloud_provider = self + node.rsync_dir(tmp_path) if tmp_path + node.run_chef! + end end - # Callbacks - def before_compile(cld) + def nodes + all_nodes.select {|i| i.in_service? }#describe_instances.select {|i| i.in_service? && security_groups.include?(i.security_groups) } end - def after_compile(cld) - save_aws_env_to_yml(cld.tmp_path/"etc"/"poolparty"/"env.yml") rescue nil + def all_nodes + #TODO: need to sort by launch time + # + @nodes ||= describe_instances.select {|i| security_group_names.include?(i.security_groups) } end - # Run after all the instances are run - def after_run_instance(instances_list) - instances_list.each do |inst| - associate_address(inst.instance_id) if next_unused_elastic_ip - attach_volume(inst.instance_id) if next_unused_volume - end + # Describe instances + # Describe the instances that are available on this cloud + # @params id (optional) if present, details about the instance + # with the id given will be returned + # if not given, details for all instances will be returned + def describe_instances(id=nil) + @describe_instances = ec2.describe_instances.reservationSet.item.map do |r| + r.instancesSet.item.map do |i| + inst_options = i.merge(r.merge(:cloud => cloud)).merge(cloud.cloud_provider.dsl_options) + Ec2Instance.new(inst_options) + end + end.flatten + # id.nil? ? @describe_instances : @describe_instances.select {|a| a.instance_id == id }.first end - # Read yaml file and use it to set environment variables and local variables. - def set_aws_env_from_yml_file(filename='/etc/poolparty/env.yml') - aws = self.class.load_keys_from_file(filename) - aws.each{|k,v| ENV[k.upcase]=v.to_s} - set_vars_from_options aws + # Extras! + + def load_balancer(name=cloud.proper_name, o={}, &block) + load_balancers << ElasticLoadBalancer.new(name, sub_opts.merge(o || {}), &block) end + def autoscale(name=cloud.proper_name, o={}, &block) + autoscalers << ElasticAutoScaler.new(name, sub_opts.merge(o || {}), &block) + end + def security_group(name=cloud.proper_name, o={}, &block) + security_groups << SecurityGroup.new(name, sub_opts.merge(o || {}), &block) + end + def elastic_ip(*ips) + ips.each {|ip| _elastic_ips << ip} + end + + # Proxy to the raw Grempe amazon-aws @ec2 instance + def ec2 + @ec2 ||= AWS::EC2::Base.new( :access_key_id => access_key, :secret_access_key => secret_access_key ) + end - # Save aws keys and env variables to a yaml file - def save_aws_env_to_yml(filename='/etc/poolparty/env.yml') - hsh = aws_hash(default_options, "/etc/poolparty/ec2") - File.open(filename, 'w') {|f| f<<YAML::dump(hsh) } + # Proxy to the raw Grempe amazon-aws autoscaling instance + def as + @as = AWS::Autoscaling::Base.new( :access_key_id => access_key, :secret_access_key => secret_access_key ) end - # Return a hash of the aws keys and environment variables - # If base_dir string is provided as second argument, replace path to - # file based variables, such as cert, with the base_dir. - def aws_hash(opts={}, base_dir=nil) - aws={ - :user_id => user_id, - :private_key => private_key, - :cert => cert, - :access_key => access_key, - :secret_access_key => secret_access_key, - :ec2_url => ec2_url, - :s3_url => s3_url, - :cloud_cert => cloud_cert - }.merge(opts) - if base_dir - aws[:cert] = "#{base_dir}/#{File.basename(cert)}" if cert - aws[:private_key] = "#{base_dir}/#{File.basename(private_key)}" if private_key - aws[:cloud_cert] = "#{base_dir}/#{File.basename(cloud_cert)}" if cloud_cert - end - aws.reject{|k,v| v.nil?} + # Proxy to the raw Grempe amazon-aws elastic_load_balancing instance + def elb + @elb ||= AWS::ELB::Base.new( :access_key_id => access_key, :secret_access_key => secret_access_key ) end + def security_group_names + security_groups.map {|a| a.to_s } + end + def security_groups + @security_groups ||= [] + end + def load_balancers + @load_balancers ||= [] + end + def autoscalers + @autoscalers ||= [] + end - # shortcut to - # ec2-add-keypair name > ~./.ec2/kname - def create_keypair(kname, path='~/.ec2') - ` ec2-add-keypair #{kname} > #{path}/#{kname} && chmod 600 #{path}/#{kname}` + # Clear the cache + def reset! + @nodes = @describe_instances = nil end + private + # Helper to get the options with self as parent + def sub_opts + dsl_options.merge(:parent => self, :cloud => cloud) + end + def _elastic_ips + @_elastic_ips ||= [] + end + def generate_keypair(n=nil) + puts "[EC2] generate_keypair is called with #{default_keypair_path/n}" + begin + hsh = ec2.create_keypair(:key_name => n) + string = hsh.keyMaterial + FileUtils.mkdir_p default_keypair_path unless File.directory?(default_keypair_path) + puts "[EC2] Generated keypair #{default_keypair_path/n}" + puts "[EC2] #{string}" + File.open(default_keypair_path/n, "w") {|f| f << string } + File.chmod 0600, default_keypair_path/n + rescue Exception => e + puts "[EC2] The keypair exists in EC2, but we cannot find the keypair locally: #{n} (#{e.inspect})" + end + keypair n + end + end +end + +require "#{File.dirname(__FILE__)}/ec2_instance" +require "#{File.dirname(__FILE__)}/helpers/ec2_helper" +%w( security_group + authorize + elastic_auto_scaler + elastic_block_store + elastic_load_balancer + elastic_ip + revoke).each do |lib| + require "#{File.dirname(__FILE__)}/helpers/#{lib}" end \ No newline at end of file