theforeman/foreman

View on GitHub
app/models/compute_resources/foreman/model/ec2.rb

Summary

Maintainability
C
7 hrs
Test Coverage
module Foreman::Model
  class EC2 < ComputeResource
    GOV_CLOUD_REGION = 'us-gov-west-1'

    include KeyPairComputeResource
    delegate :flavors, :subnets, :to => :client
    validates :user, :password, :presence => true

    alias_attribute :access_key, :user
    alias_attribute :region, :url

    alias_method :available_flavors, :flavors

    def to_label
      "#{name} (#{region}-#{provider_friendly_name})"
    end

    def gov_cloud=(enable_gov_cloud)
      if enable_gov_cloud == '1'
        self.url ||= GOV_CLOUD_REGION
      elsif gov_cloud?
        self.url = nil
      end
    end

    def gov_cloud
      ['us-gov-west-1', 'us-gov-east-1'].include? url
    end
    alias_method :gov_cloud?, :gov_cloud

    def provided_attributes
      super.merge({ :ip => :vm_ip_address })
    end

    def self.available?
      Fog::Compute.providers.include?(:aws)
    end

    def self.model_name
      ComputeResource.model_name
    end

    def capabilities
      [:image]
    end

    def find_vm_by_uuid(uuid)
      super
    rescue Fog::AWS::Compute::Error
      raise(ActiveRecord::RecordNotFound)
    end

    # Tag Example - <Fog::AWS::Compute::Tag key=nil, value=nil, resource_id=nil, resource_type=nil #>
    def new_tag(attrs = {})
      client.tags.new(attrs)
    end

    def parse_tags(args)
      # Merge AWS EC2 tags
      tags = {}
      if (name = args[:name])
        tags = {:Name => name}
      end

      nested_attrs = args.delete(:tags_attributes)
      args[:tags] = nested_attributes_for(:tags, nested_attrs.deep_symbolize_keys) if nested_attrs

      if args[:tags].present?
        # Validation, must be [{"key": "tags_name", "value": "tag_value"}]
        if !args[:tags].is_a?(Array) || !args[:tags].all? { |item| item.is_a?(Hash) && item.has_key?(:key) && item.has_key?(:value) }
          raise Foreman::Exception.new(N_("Invalid tags, must be an array of maps with key and value properties"))
        end
        args[:tags].each_with_index do |tag, i|
          # Validation against AWS rules
          # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html
          next if tag[:key] =~ /^aws:/
          next unless tag[:key] =~ /^[\p{L}\p{N} +-=._:\/@]{1,128}$/
          next unless tag[:value] =~ /^[\p{L}\p{N} +-=._:\/@]{1,256}$/
          tags[tag[:key]] = tag[:value]
        end
      end
      Foreman::Logging.logger('app').info "AWS machine #{args[:name]} will be created with tags #{tags}"
      tags
    end

    def create_vm(args = { })
      args[:tags] = parse_tags(args)
      args = vm_instance_defaults.merge(args.to_h.symbolize_keys).deep_symbolize_keys
      if (image_id = args[:image_id])
        image = images.find_by_uuid(image_id.to_s)
        iam_hash = image.iam_role.present? ? {:iam_instance_profile_name => image.iam_role} : {}
        args.merge!(iam_hash)
      end
      args[:groups].reject!(&:empty?) if args.has_key?(:groups)
      args[:security_group_ids].reject!(&:empty?) if args.has_key?(:security_group_ids)
      args[:associate_public_ip] = subnet_implies_is_vpc?(args) && args[:managed_ip] == 'public'
      args[:private_ip_address] = args[:interfaces_attributes][:"0"][:ip]
      super(args)
    rescue Fog::Errors::Error => e
      Foreman::Logging.exception("Unhandled EC2 error", e)
      raise e
    end

    def security_groups(vpc = nil)
      groups = client.security_groups
      groups.reject! { |sg| sg.vpc_id != vpc } if vpc
      groups
    end
    alias_method :available_security_groups, :security_groups

    def regions
      return [] if user.blank? || password.blank?
      @regions ||= client.describe_regions.body["regionInfo"].map { |r| r["regionName"] }
    end

    def zones
      @zones ||= client.describe_availability_zones.body["availabilityZoneInfo"].map { |r| r["zoneName"] if r["regionName"] == region }.compact
    end
    alias_method :available_zones, :zones

    def test_connection(options = {})
      super
      errors[:user].empty? && errors[:password].empty? && regions
    rescue Fog::AWS::Compute::Error => e
      errors[:base] << e.message
    rescue Excon::Error::Socket => e
      errors[:base] << e.message
    end

    def console(uuid)
      vm = find_vm_by_uuid(uuid)
      vm.console_output.body.merge(:type => 'log', :name => vm.name)
    end

    def destroy_vm(uuid)
      vm = find_vm_by_uuid(uuid)
      vm&.destroy
      true
    end

    # not supporting update at the moment
    def update_required?(old_attrs, new_attrs)
      false
    end

    def associated_host(vm)
      associate_by("ip", [vm.public_ip_address, vm.private_ip_address])
    end

    def user_data_supported?
      true
    end

    def image_exists?(image)
      client.images.get(image).present?
    end

    def normalize_vm_attrs(vm_attrs)
      normalized = slice_vm_attributes(vm_attrs, ['flavor_id', 'availability_zone', 'subnet_id', 'image_id', 'managed_ip'])

      normalized['flavor_name'] =  flavors.detect { |f| f.id == normalized['flavor_id'] }.try(:name)
      normalized['subnet_name'] =  subnets.detect { |f| f.subnet_id == normalized['subnet_id'] }.try(:cidr_block)
      normalized['image_name'] = images.find_by(:uuid => vm_attrs['image_id']).try(:name)

      group_ids = vm_attrs['security_group_ids'] || []
      group_ids = group_ids.select { |gid| gid != '' }
      normalized['security_groups'] = group_ids.map.with_index do |gid, idx|
        [idx.to_s, {
          'id' => gid,
          'name' => security_groups.detect { |g| g.group_id == gid }.try(:name),
        }]
      end.to_h

      normalized
    rescue Fog::AWS::Compute::Error => e
      Foreman::Logging.exception("Unhandled EC2 error", e)
      {}
    end

    private

    def subnet_implies_is_vpc?(args)
      args[:subnet_id].present?
    end

    def client
      self.url = region if gov_cloud
      @client ||= Fog::AWS::Compute.new(:aws_access_key_id => user, :aws_secret_access_key => password, :region => region, :connection_options => connection_options)
    end

    def vm_instance_defaults
      super.merge(
        :flavor_id => "m1.small",
        :key_pair  => key_pair
      )
    end
  end
end