# # Copyright (c) 2007-2009 RightScale Inc # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # module RightAws # = RightAWS::EC2 -- RightScale Amazon EC2 interface # The RightAws::EC2 class provides a complete interface to Amazon's # Elastic Compute Cloud service, as well as the associated EBS (Elastic Block # Store). # For explanations of the semantics # of each call, please refer to Amazon's documentation at # http://developer.amazonwebservices.com/connect/kbcategory.jspa?categoryID=87 # # Examples: # # Create an EC2 interface handle: # # @ec2 = RightAws::Ec2.new(aws_access_key_id, # aws_secret_access_key) # Create a new SSH key pair: # @key = 'right_ec2_awesome_test_key' # new_key = @ec2.create_key_pair(@key) # keys = @ec2.describe_key_pairs # # Create a security group: # @group = 'right_ec2_awesome_test_security_group' # @ec2.create_security_group(@group,'My awesome test group') # group = @ec2.describe_security_groups([@group])[0] # # Configure a security group: # @ec2.authorize_security_group_named_ingress(@group, account_number, 'default') # @ec2.authorize_security_group_IP_ingress(@group, 80,80,'udp','192.168.1.0/8') # # Describe the available images: # images = @ec2.describe_images # # Launch an instance: # ec2.run_instances('ami-9a9e7bf3', 1, 1, ['default'], @key, 'SomeImportantUserData', 'public') # # # Describe running instances: # @ec2.describe_instances # # Error handling: all operations raise an RightAws::AwsError in case # of problems. Note that transient errors are automatically retried. class Ec2 < RightAwsBase include RightAwsBaseInterface # Amazon EC2 API version being used API_VERSION = "2009-11-30" DEFAULT_HOST = "ec2.amazonaws.com" DEFAULT_PATH = '/' DEFAULT_PROTOCOL = 'https' DEFAULT_PORT = 443 # Default addressing type (public=NAT, direct=no-NAT) used when launching instances. DEFAULT_ADDRESSING_TYPE = 'public' DNS_ADDRESSING_SET = ['public','direct'] # Amazon EC2 Instance Types : http://www.amazon.com/b?ie=UTF8&node=370375011 # Default EC2 instance type (platform) DEFAULT_INSTANCE_TYPE = 'm1.small' INSTANCE_TYPES = ['m1.small','c1.medium','m1.large','m1.xlarge','c1.xlarge', 'm2.xlarge', 'm2.2xlarge', 'm2.4xlarge'] @@bench = AwsBenchmarkingBlock.new def self.bench_xml @@bench.xml end def self.bench_ec2 @@bench.service end # Current API version (sometimes we have to check it outside the GEM). @@api = ENV['EC2_API_VERSION'] || API_VERSION def self.api @@api end # Create a new handle to an EC2 account. All handles share the same per process or per thread # HTTP connection to Amazon EC2. Each handle is for a specific account. The params have the # following options: # * :endpoint_url a fully qualified url to Amazon API endpoint (this overwrites: :server, :port, :service, :protocol and :region). Example: 'https://eu-west-1.ec2.amazonaws.com/' # * :server: EC2 service host, default: DEFAULT_HOST # * :region: EC2 region (North America by default) # * :port: EC2 service port, default: DEFAULT_PORT # * :protocol: 'http' or 'https', default: DEFAULT_PROTOCOL # * :multi_thread: true=HTTP connection per thread, false=per process # * :logger: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT # * :signature_version: The signature version : '0','1' or '2'(default) # * :cache: true/false: caching for: ec2_describe_images, describe_instances, # describe_images_by_owner, describe_images_by_executable_by, describe_availability_zones, # describe_security_groups, describe_key_pairs, describe_addresses, # describe_volumes, describe_snapshots methods, default: false. # def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={}) init({ :name => 'EC2', :default_host => ENV['EC2_URL'] ? URI.parse(ENV['EC2_URL']).host : DEFAULT_HOST, :default_port => ENV['EC2_URL'] ? URI.parse(ENV['EC2_URL']).port : DEFAULT_PORT, :default_service => ENV['EC2_URL'] ? URI.parse(ENV['EC2_URL']).path : DEFAULT_PATH, :default_protocol => ENV['EC2_URL'] ? URI.parse(ENV['EC2_URL']).scheme : DEFAULT_PROTOCOL, :default_api_version => @@api }, aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'] , aws_secret_access_key|| ENV['AWS_SECRET_ACCESS_KEY'], params) # Eucalyptus supports some yummy features but Amazon does not if @params[:eucalyptus] @params[:port_based_group_ingress] = true unless @params.has_key?(:port_based_group_ingress) end end def generate_request(action, params={}) #:nodoc: generate_request_impl(:get, action, params ) end # Sends request to Amazon and parses the response # Raises AwsError if any banana happened def request_info(request, parser) #:nodoc: request_info_impl(:ec2_connection, @@bench, request, parser) end #----------------------------------------------------------------- # Keys #----------------------------------------------------------------- # Retrieve a list of SSH keys. Returns an array of keys or an exception. Each key is # represented as a two-element hash. # # ec2.describe_key_pairs #=> # [{:aws_fingerprint=> "01:02:03:f4:25:e6:97:e8:9b:02:1a:26:32:4e:58:6b:7a:8c:9f:03", :aws_key_name=>"key-1"}, # {:aws_fingerprint=> "1e:29:30:47:58:6d:7b:8c:9f:08:11:20:3c:44:52:69:74:80:97:08", :aws_key_name=>"key-2"}, # ..., {...} ] # def describe_key_pairs(*key_pairs) key_pairs = key_pairs.flatten link = generate_request("DescribeKeyPairs", amazonize_list('KeyName', key_pairs)) request_cache_or_info :describe_key_pairs, link, QEc2DescribeKeyPairParser, @@bench, key_pairs.blank? rescue Exception on_exception end # Create new SSH key. Returns a hash of the key's data or an exception. # # ec2.create_key_pair('my_awesome_key') #=> # {:aws_key_name => "my_awesome_key", # :aws_fingerprint => "01:02:03:f4:25:e6:97:e8:9b:02:1a:26:32:4e:58:6b:7a:8c:9f:03", # :aws_material => "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAK...Q8MDrCbuQ=\n-----END RSA PRIVATE KEY-----"} # def create_key_pair(name) link = generate_request("CreateKeyPair", 'KeyName' => name.to_s) request_info(link, QEc2CreateKeyPairParser.new(:logger => @logger)) rescue Exception on_exception end # Delete a key pair. Returns +true+ or an exception. # # ec2.delete_key_pair('my_awesome_key') #=> true # def delete_key_pair(name) link = generate_request("DeleteKeyPair", 'KeyName' => name.to_s) request_info(link, RightBoolResponseParser.new(:logger => @logger)) rescue Exception on_exception end #----------------------------------------------------------------- # Elastic IPs #----------------------------------------------------------------- # Acquire a new elastic IP address for use with your account. # Returns allocated IP address or an exception. # # ec2.allocate_address #=> '75.101.154.140' # def allocate_address link = generate_request("AllocateAddress") request_info(link, QEc2AllocateAddressParser.new(:logger => @logger)) rescue Exception on_exception end # Associate an elastic IP address with an instance. # Returns +true+ or an exception. # # ec2.associate_address('i-d630cbbf', '75.101.154.140') #=> true # def associate_address(instance_id, public_ip) link = generate_request("AssociateAddress", "InstanceId" => instance_id.to_s, "PublicIp" => public_ip.to_s) request_info(link, RightBoolResponseParser.new(:logger => @logger)) rescue Exception on_exception end # List elastic IP addresses assigned to your account. # Returns an array of 2 keys (:instance_id and :public_ip) hashes: # # ec2.describe_addresses #=> [{:instance_id=>"i-d630cbbf", :public_ip=>"75.101.154.140"}, # {:instance_id=>nil, :public_ip=>"75.101.154.141"}] # # ec2.describe_addresses('75.101.154.140') #=> [{:instance_id=>"i-d630cbbf", :public_ip=>"75.101.154.140"}] # def describe_addresses(*addresses) addresses = addresses.flatten link = generate_request("DescribeAddresses", amazonize_list('PublicIp', addresses)) request_cache_or_info :describe_addresses, link, QEc2DescribeAddressesParser, @@bench, addresses.blank? rescue Exception on_exception end # Disassociate the specified elastic IP address from the instance to which it is assigned. # Returns +true+ or an exception. # # ec2.disassociate_address('75.101.154.140') #=> true # def disassociate_address(public_ip) link = generate_request("DisassociateAddress", "PublicIp" => public_ip.to_s) request_info(link, RightBoolResponseParser.new(:logger => @logger)) rescue Exception on_exception end # Release an elastic IP address associated with your account. # Returns +true+ or an exception. # # ec2.release_address('75.101.154.140') #=> true # def release_address(public_ip) link = generate_request("ReleaseAddress", "PublicIp" => public_ip.to_s) request_info(link, RightBoolResponseParser.new(:logger => @logger)) rescue Exception on_exception end #----------------------------------------------------------------- # Availability zones #----------------------------------------------------------------- # Describes availability zones that are currently available to the account and their states. # Returns an array of 2 keys (:zone_name and :zone_state) hashes: # # ec2.describe_availability_zones #=> [{:region_name=>"us-east-1", # :zone_name=>"us-east-1a", # :zone_state=>"available"}, ... ] # # ec2.describe_availability_zones('us-east-1c') #=> [{:region_name=>"us-east-1", # :zone_state=>"available", # :zone_name=>"us-east-1c"}] # def describe_availability_zones(*availability_zones) availability_zones = availability_zones.flatten link = generate_request("DescribeAvailabilityZones", amazonize_list('ZoneName', availability_zones)) request_cache_or_info :describe_availability_zones, link, QEc2DescribeAvailabilityZonesParser, @@bench, availability_zones.blank? rescue Exception on_exception end #----------------------------------------------------------------- # Regions #----------------------------------------------------------------- # Describe regions. # # ec2.describe_regions #=> ["eu-west-1", "us-east-1"] # def describe_regions(*regions) regions = regions.flatten link = generate_request("DescribeRegions", amazonize_list('RegionName', regions)) request_cache_or_info :describe_regions, link, QEc2DescribeRegionsParser, @@bench, regions.blank? rescue Exception on_exception end #----------------------------------------------------------------- # PARSERS: Key Pair #----------------------------------------------------------------- class QEc2DescribeKeyPairParser < RightAWSParser #:nodoc: def tagstart(name, attributes) @item = {} if name == 'item' end def tagend(name) case name when 'keyName' then @item[:aws_key_name] = @text when 'keyFingerprint' then @item[:aws_fingerprint] = @text when 'item' then @result << @item end end def reset @result = []; end end class QEc2CreateKeyPairParser < RightAWSParser #:nodoc: def tagstart(name, attributes) @result = {} if name == 'CreateKeyPairResponse' end def tagend(name) case name when 'keyName' then @result[:aws_key_name] = @text when 'keyFingerprint' then @result[:aws_fingerprint] = @text when 'keyMaterial' then @result[:aws_material] = @text end end end #----------------------------------------------------------------- # PARSERS: Elastic IPs #----------------------------------------------------------------- class QEc2AllocateAddressParser < RightAWSParser #:nodoc: def tagend(name) @result = @text if name == 'publicIp' end end class QEc2DescribeAddressesParser < RightAWSParser #:nodoc: def tagstart(name, attributes) @address = {} if name == 'item' end def tagend(name) case name when 'instanceId' then @address[:instance_id] = @text.blank? ? nil : @text when 'publicIp' then @address[:public_ip] = @text when 'item' then @result << @address end end def reset @result = [] end end #----------------------------------------------------------------- # PARSERS: AvailabilityZones #----------------------------------------------------------------- class QEc2DescribeAvailabilityZonesParser < RightAWSParser #:nodoc: def tagstart(name, attributes) @zone = {} if name == 'item' end def tagend(name) case name when 'regionName' then @zone[:region_name] = @text when 'zoneName' then @zone[:zone_name] = @text when 'zoneState' then @zone[:zone_state] = @text when 'item' then @result << @zone end end def reset @result = [] end end #----------------------------------------------------------------- # PARSERS: Regions #----------------------------------------------------------------- class QEc2DescribeRegionsParser < RightAWSParser #:nodoc: def tagend(name) @result << @text if name == 'regionName' end def reset @result = [] end end end end