412 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			412 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| require "exceptions/conflict_exception"
 | ||
| require "providers/base_provider"
 | ||
| require "db/mongo/models/provider_accounts/ec2_provider_account"
 | ||
| require_relative "ec2_accounts_factory"
 | ||
| 
 | ||
| module Provider
 | ||
|   # Provider for Amazon EC2
 | ||
|   class Ec2 < BaseProvider
 | ||
| 
 | ||
|     PROVIDER = "ec2"
 | ||
| 
 | ||
|     attr_accessor :availability_zone
 | ||
| 
 | ||
|     def initialize config
 | ||
|       self.certificate_path = config[:aws_certificate]
 | ||
|       self.ssh_key = config[:aws_ssh_key]
 | ||
|       options = {
 | ||
|         :provider => "aws"
 | ||
|       }
 | ||
|       if config[:aws_use_iam_profile]
 | ||
|         options[:use_iam_profile] = true
 | ||
|       else
 | ||
|         options[:aws_access_key_id] = config[:aws_access_key_id]
 | ||
|         options[:aws_secret_access_key] = config[:aws_secret_access_key]
 | ||
|       end
 | ||
|       if config[:aws_proxy] and config[:aws_no_proxy]
 | ||
|         options[:connection_options] = {
 | ||
|           :proxy => config[:aws_proxy],
 | ||
|           :no_proxy => config[:no_proxy]
 | ||
|         }
 | ||
|       end
 | ||
|       self.connection_options = options
 | ||
|       self.availability_zone = config[:aws_availability_zone] || "us-east-1a"
 | ||
|       self.run_list = config[:aws_integration_run_list] || []
 | ||
|     end
 | ||
| 
 | ||
|     def configured?
 | ||
|       o = self.connection_options
 | ||
|       super and !(empty_param?(o[:aws_access_key_id]) or empty_param?(o[:aws_secret_access_key]))
 | ||
|     end
 | ||
| 
 | ||
|     def name
 | ||
|       PROVIDER
 | ||
|     end
 | ||
| 
 | ||
|     def flavors
 | ||
|       self.compute.flavors.all.map do |f|
 | ||
|         {
 | ||
|           "id" => f.id,
 | ||
|           "cores" => f.cores,
 | ||
|           "disk" => f.disk,
 | ||
|           "name" => f.name,
 | ||
|           "ram" => f.ram
 | ||
|         }
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def groups filters={}
 | ||
|       g = self.compute.describe_security_groups(filters || {})
 | ||
|       convert_groups(g.body["securityGroupInfo"])
 | ||
|     end
 | ||
| 
 | ||
|     def images filters
 | ||
|       self.compute.describe_images({"image-id" => filters}).body["imagesSet"].map do |i|
 | ||
|         {
 | ||
|           "id" => i["imageId"],
 | ||
|           "name" => i["name"],
 | ||
|           "status" => i["imageState"]
 | ||
|         }
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def networks filters={}
 | ||
|       self.compute.describe_subnets(filters).body["subnetSet"].select{|n| n["state"] == "available"}.map do |n|
 | ||
|         {
 | ||
|           "cidr" => n["cidrBlock"],
 | ||
|           "vpcId" => n["vpcId"],
 | ||
|           "subnetId" => n["subnetId"],
 | ||
|           "name" => n["subnetId"],
 | ||
|           "zone" => n["availabilityZone"]
 | ||
|         }
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def servers
 | ||
|       list = self.compute.describe_instances.body["reservationSet"]
 | ||
|       list.select{|l| l["instancesSet"][0]["instanceState"]["name"].to_s == "running"}.map do |server|
 | ||
|         convert_server server["instancesSet"][0]
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def server id
 | ||
|       list = self.compute.describe_instances('instance-id' => [id]).body["reservationSet"]
 | ||
|       convert_server list[0]["instancesSet"][0]
 | ||
|     end
 | ||
| 
 | ||
|     def create_server s, image, flavor, subnets, groups, out, options={}
 | ||
|       out << "Creating server for project '#{s.project} - #{s.deploy_env}'\n"
 | ||
|       options = {
 | ||
|         "InstanceType" => flavor,
 | ||
| #        "Placement.AvailabilityZone" => s.options[:availability_zone],
 | ||
|         "KeyName" => self.ssh_key,
 | ||
|         "PrivateIpAddress" => s.private_ip
 | ||
|       }
 | ||
|       vpcId = nil
 | ||
|       unless subnets.empty?
 | ||
|         options["SubnetId"] = subnets[0]
 | ||
|         network = self.networks.detect{|n| n["name"] == options["SubnetId"]}
 | ||
|         vpcId = network["vpcId"] if network
 | ||
|         if vpcId.nil?
 | ||
|           out << "Can not get 'vpcId' by subnet name '#{options["SubnetId"]}'\n"
 | ||
|           return false
 | ||
|         end
 | ||
|       end
 | ||
|       options["SecurityGroupId"] = extract_group_ids(groups, vpcId).join(",")
 | ||
| 
 | ||
|       aws_server = nil
 | ||
|       compute = self.compute
 | ||
|       begin
 | ||
|         aws_server = compute.run_instances(image, 1, 1, options)
 | ||
|       rescue Excon::Errors::Unauthorized => ue
 | ||
|         #root = XML::Parser.string(ue.response.body).parse.root
 | ||
|         #msg = root.children.find { |node| node.name == "Message" }
 | ||
|         #code = root.children.find { |node| node.name == "Code" }
 | ||
|         code = "TODO"
 | ||
|         msg = ue.response.body
 | ||
|         out << "\nERROR: Unauthorized (#{code}: #{msg})"
 | ||
|         return false
 | ||
|       rescue Fog::Compute::AWS::Error => e
 | ||
|         out << e.message
 | ||
|         return false
 | ||
|       end
 | ||
| 
 | ||
|       abody = aws_server.body
 | ||
|       instance = abody["instancesSet"][0]
 | ||
|       s.id = instance["instanceId"]
 | ||
| 
 | ||
|       out << "\nWaiting for server..."
 | ||
| 
 | ||
|       details, state = nil, instance["instanceState"]["name"]
 | ||
|       until state == "running"
 | ||
|         sleep(2)
 | ||
|         details = compute.describe_instances("instance-id" => [s.id]).body["reservationSet"][0]["instancesSet"][0]
 | ||
|         state = details["instanceState"]["name"].to_s
 | ||
|         next if state == "pending" or state == "running"
 | ||
|         out << "Server returns state '#{state}'"
 | ||
|         return false
 | ||
|       end
 | ||
|       s.public_ip = details["ipAddress"]
 | ||
|       s.private_ip = details["privateIpAddress"]
 | ||
|       tags = server_tags(s)
 | ||
|       compute.create_tags(s.id, tags)
 | ||
|       out << "\nServer tags: #{tags.inspect}\n"
 | ||
|       out << "\nDone\n\n"
 | ||
|       out << s.info
 | ||
| 
 | ||
|       true
 | ||
|     end
 | ||
| 
 | ||
|     def server_tags server
 | ||
|       {
 | ||
|         "Name" => server.chef_node_name,
 | ||
|         "cid:project" => server.project,
 | ||
|         "cid:deployEnv" => server.deploy_env,
 | ||
|         "cid:user" => server.created_by,
 | ||
|         "cid:remoteUser" => server.remote_user
 | ||
|       }
 | ||
|     end
 | ||
| 
 | ||
|     def delete_server s
 | ||
|       r = self.compute.terminate_instances(s.id)
 | ||
|       i = r.body["instancesSet"][0]
 | ||
|       old_state = i["previousState"]["name"]
 | ||
|       state = i["currentState"]["name"]
 | ||
|       return r.status == 200 ? "Server with id '#{s.id}' changed state '#{old_state}' to '#{state}'" : r.body
 | ||
|     end
 | ||
| 
 | ||
|     def pause_server s
 | ||
|       es = self.server s.id
 | ||
|       if es["state"] == "running"
 | ||
|         self.compute.stop_instances [ s.id ]
 | ||
|         return nil
 | ||
|       else
 | ||
|         return es["state"]
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def unpause_server s
 | ||
|       es = self.server s.id
 | ||
|       if es["state"] == "stopped"
 | ||
|         self.compute.start_instances [ s.id ]
 | ||
|         return nil
 | ||
|       else
 | ||
|         return es["state"]
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def set_tags instance_id, tags
 | ||
|       raise ConflictException.new("You can not change 'Name' tag") if tags.key?("Name")
 | ||
|       compute.create_tags(instance_id, tags)
 | ||
|     end
 | ||
| 
 | ||
|     def unset_tags instance_id, tags
 | ||
|       raise ConflictException.new("You can not change 'Name' tag") if tags.key?("Name")
 | ||
|       compute.delete_tags(instance_id, tags)
 | ||
|     end
 | ||
| 
 | ||
|     def compute
 | ||
|       connection_compute(connection_options)
 | ||
|     end
 | ||
| 
 | ||
|     def cloud_formation
 | ||
|       @cloud_formation ||= Fog::AWS::CloudFormation.new(connection_options)
 | ||
|     end
 | ||
| 
 | ||
|     def create_stack(stack, out)
 | ||
|       begin
 | ||
|         out << "Creating stack for project '#{stack.project}' and environment '#{stack.deploy_env}'...\n"
 | ||
|         stack.name = create_default_stack_name(stack) unless stack.name
 | ||
|         out << "Stack name: #{stack.name}\n"
 | ||
|         out << "Stack template: #{stack.stack_template}\n"
 | ||
|         out << "Stack parameters: #{stack.parameters}\n"
 | ||
|         out.flush
 | ||
|         response = cloud_formation.create_stack(stack.name,
 | ||
|           {
 | ||
|             'TemplateURL' => stack.stack_template_model.template_url,
 | ||
|             'Parameters' => stack.parameters || {},
 | ||
|             'Capabilities' => ['CAPABILITY_IAM'],
 | ||
|             'Tags' => stack_tags(stack)
 | ||
|           }
 | ||
|         )
 | ||
|         stack.id = response.body['StackId']
 | ||
|         out << "Stack id: #{stack.id}\n"
 | ||
|         out.flush
 | ||
|       rescue Excon::Errors::Conflict => e
 | ||
|         raise ProviderErrors::NameConflict
 | ||
|       rescue Excon::Errors::BadRequest => br
 | ||
|         response = ::Chef::JSONCompat.from_json(br.response.body)
 | ||
|         if response['code'] == 400
 | ||
|           out << "\nERROR: Bad request (400): #{response['explanation']}"
 | ||
|           out << "\n"
 | ||
|           raise InvalidRecord.new(response['explanation'])
 | ||
|         else
 | ||
|           out << "\nERROR: Unknown server error (#{response['code']}): #{response['explanation']}"
 | ||
|           out << "\n"
 | ||
|           raise InvalidRecord.new(response['explanation'])
 | ||
|         end
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def stack_tags stack
 | ||
|       {
 | ||
|         "StackTemplate" => stack.stack_template,
 | ||
|         "cid:project" => stack.project,
 | ||
|         "cid:deployEnv" => stack.deploy_env,
 | ||
|         "cid:user" => stack.owner
 | ||
|       }
 | ||
|     end
 | ||
| 
 | ||
|     def update_stack(stack, params)
 | ||
|       cloud_formation.update_stack(stack.name, params)
 | ||
|     end
 | ||
| 
 | ||
|     def validate_stack_template template
 | ||
|       #r = cloud_formation.validate_template({'TemplateBody' => template})
 | ||
|       #pp r.body
 | ||
|       true
 | ||
|     end
 | ||
| 
 | ||
|     def delete_stack(stack)
 | ||
|       cloud_formation.delete_stack(stack.name)
 | ||
|     end
 | ||
| 
 | ||
|     def stack_details(stack)
 | ||
|       b = cloud_formation.describe_stacks({'StackName' => stack.name}).body
 | ||
|       b['Stacks'].detect{|s| s.key?("StackStatus")} || {}
 | ||
|     end
 | ||
| 
 | ||
|     def stack_resources(stack)
 | ||
|       cloud_formation.describe_stack_resources({'StackName' => stack.name}).body['StackResources']
 | ||
|     end
 | ||
| 
 | ||
|     def stack_events(stack)
 | ||
|       cloud_formation.describe_stack_events(stack.name).body['StackEvents'].map{|se| {"timestamp" => se["Timestamp"], "stack_name" => se["StackName"], "stack_id" => se["StackId"], "event_id" => se["EventId"], "reason" => se["ResourceStatusReason"], "status" => se["ResourceStatus"]}}.sort{|se1, se2| se1["timestamp"] <=> se2["timestamp"]}
 | ||
|     end
 | ||
| 
 | ||
|     # не работает, не используется
 | ||
|     # def stack_resource(stack, resource_id)
 | ||
|     #   physical_id = fog_stack(stack).resources.get(resource_id).physical_resource_id
 | ||
|     #   compute.servers.get(physical_id)
 | ||
|     # end
 | ||
| 
 | ||
|     def stack_servers(stack)
 | ||
|       # orchestration.describe_stack_resources возвращает мало информации
 | ||
|       resources = compute.describe_instances(
 | ||
|         'tag-key' => 'aws:cloudformation:stack-id',
 | ||
|         'tag-value' => stack.id
 | ||
|       ).body["reservationSet"]
 | ||
| 
 | ||
|       # В ресурсах могут лежать не только конкретные инстансы, но и MasterNodesGroup, которые управляют
 | ||
|       # несколькими инстансами. Обрабатываем эту ситуацию.
 | ||
|       instances = resources.map { |resource| resource["instancesSet"] }.flatten
 | ||
| 
 | ||
|       instances.map do |instance|
 | ||
|         {
 | ||
|           # 'name' => instance["tagSet"]["Name"],
 | ||
|           'name' => [stack.name, instance_name(instance)].join('-'),
 | ||
|           'id' => instance["instanceId"],
 | ||
|           'key_name' => instance["keyName"],
 | ||
|           'private_ip' => instance["privateIpAddress"],
 | ||
|           'public_ip' => instance["ipAddress"],
 | ||
|           'tags' => instance["tagSet"]
 | ||
|         }
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|     def create_default_stack_name s
 | ||
|       "stack-#{self.ssh_key}-#{s.project}-#{s.deploy_env}-#{Time.now.to_i}".gsub('_', '-')
 | ||
|     end
 | ||
| 
 | ||
|     def describe_vpcs
 | ||
|       self.compute.describe_vpcs.body["vpcSet"].select{|v| v["state"] == "available"}.map{|v| {"vpc_id" => v["vpcId"], "cidr" => v["cidrBlock"] } }
 | ||
|     end
 | ||
| 
 | ||
|     def store_stack_template(filename, json)
 | ||
|       store_file(stack_templates_bucket, filename, json)
 | ||
|     end
 | ||
| 
 | ||
|     def store_file(bucket, filename, body)
 | ||
|       {
 | ||
|         'url' => bucket.files.create(key: filename, body: body, public: true).public_url
 | ||
|       }
 | ||
|     end
 | ||
| 
 | ||
|   private
 | ||
| 
 | ||
|     def convert_groups list
 | ||
|       res = {}
 | ||
|       list.each do |g|
 | ||
|         next if g["groupName"].nil?
 | ||
|         res[g["groupName"]] = {
 | ||
|           "description" => g["groupDescription"],
 | ||
|           "id" => g["groupId"]
 | ||
|         }
 | ||
|         rules = []
 | ||
|         g["ipPermissions"].each do |r|
 | ||
|           cidr = r["ipRanges"][0] || {}
 | ||
|           rules.push({
 | ||
|             "protocol" => r["ipProtocol"],
 | ||
|             "from" => r["fromPort"],
 | ||
|             "to" => r["toPort"],
 | ||
|             "cidr" => cidr["cidrIp"]
 | ||
|           })
 | ||
|         end
 | ||
|         res[g["groupName"]]["rules"] = rules
 | ||
|       end
 | ||
|       res
 | ||
|     end
 | ||
| 
 | ||
|     def convert_server s
 | ||
|       {
 | ||
|         "state" => s["instanceState"]["name"],
 | ||
|         "name" => s["tagSet"]["Name"],
 | ||
|         "image" => s["imageId"],
 | ||
|         "flavor" => s["instanceType"],
 | ||
|         "keypair" => s["keyName"],
 | ||
|         "instance_id" => s["instanceId"],
 | ||
|         "dns_name" => s["dnsName"],
 | ||
|         "zone" => s["placement"]["availabilityZone"],
 | ||
|         "private_ip" => s["privateIpAddress"],
 | ||
|         "public_ip" => s["ipAddress"],
 | ||
|         "launched_at" => s["launchTime"]
 | ||
|       }
 | ||
|     end
 | ||
| 
 | ||
|     def extract_group_ids names, vpcId
 | ||
|       return [] if names.nil?
 | ||
|       p = nil
 | ||
|       p = {"vpc-id" => vpcId} unless vpcId.nil?
 | ||
|       groups = self.groups(p)
 | ||
|       r = names.map do |name|
 | ||
|         groups[name]["id"]
 | ||
|       end
 | ||
|       r
 | ||
|     end
 | ||
| 
 | ||
|     def orchestration
 | ||
|       @orchestration ||= Fog::AWS::CloudFormation.new(connection_options)
 | ||
|     end
 | ||
| 
 | ||
|     def storage
 | ||
|       @storage ||= Fog::Storage.new(connection_options)
 | ||
|     end
 | ||
| 
 | ||
|     def stack_templates_bucket
 | ||
|       bucket_name = DevopsConfig.config[:aws_stack_templates_bucket] || 'stacktemplatesnibrdev'
 | ||
|       bucket = storage.directories.get(bucket_name)
 | ||
|       bucket ||= storage.directories.create(key: bucket_name)
 | ||
|     end
 | ||
| 
 | ||
|     def instance_name(instance)
 | ||
|       return instance["tagSet"]["Name"] if instance["tagSet"]["Name"]
 | ||
|       if instance['tagSet']['aws:autoscaling:groupName']
 | ||
|         instance["instanceId"]
 | ||
|       else
 | ||
|         instance['tagSet']['aws:cloudformation:logical-id']
 | ||
|       end
 | ||
|     end
 | ||
| 
 | ||
|   end
 | ||
| end
 | 
