martinpovolny/conductor

View on GitHub
src/app/models/provider_account.rb

Summary

Maintainability
D
1 day
Test Coverage
#
#   Copyright 2011 Red Hat, Inc.
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#

# == Schema Information
# Schema version: 20110309105149
#
# Table name: provider_accounts
#
#  id           :integer         not null, primary key
#  label        :string(255)     not null
#  provider_id  :integer         not null
#  quota_id     :integer
#  lock_version :integer         default(0)
#  created_at   :datetime
#  updated_at   :datetime
#

# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ProviderAccount < ActiveRecord::Base

  class << self
    include CommonFilterMethods
  end
  include PermissionedObject

  before_destroy :check_destroyable_instances!
  before_destroy :check_provider_images!
  before_destroy :remove_pool_family_assoc

  # Relations
  belongs_to :provider
  belongs_to :quota, :autosave => true, :dependent => :destroy
  has_many :instances
  has_and_belongs_to_many :pool_families, :uniq => true
  has_and_belongs_to_many :provider_realms, :uniq => true
  has_many :permissions, :as => :permission_object, :dependent => :destroy,
           :include => [:role],
           :order => "permissions.id ASC"
  has_many :derived_permissions, :as => :permission_object, :dependent => :destroy,
           :include => [:role],
           :order => "derived_permissions.id ASC"
  has_many :credentials, :dependent => :destroy
  # eventually, this might be "has_many", but first pass is one-to-one
  has_one :config_server, :dependent => :destroy
  has_many :provider_priority_group_elements, :as => :value, :dependent => :destroy
  has_many :events, :as => :source, :dependent => :destroy, :order => 'events.id ASC'

  # Scopes
  scope :enabled, lambda { where(:provider_id => Provider.enabled) }
  has_many :events, :as => :source, :dependent => :destroy,
           :order => 'events.id ASC'
  has_many :provider_images, :class_name => "Tim::ProviderImage"

  # Helpers
  attr_accessor :x509_cert_priv_file, :x509_cert_pub_file
  accepts_nested_attributes_for :credentials
  accepts_nested_attributes_for :quota

  # We set credentials hash as protected so that it is not set during mass assign on new
  # This is to avoid the scenario where the credentials are set before provider which
  # will result in an exception.
  attr_protected :credentials_hash

  # Validations
  validates :label, :presence => true,
                    :uniqueness => true,
                    :length => { :within => 1..100 }
  validates :priority,
            :numericality => { :only_integer => true,
                               :greater_than_or_equal_to => -100,
                               :less_than_or_equal_to => 100 },
            :allow_blank => true
  validates :provider, :presence => true
  validates :quota, :presence => true
  validate :validate_presence_of_credentials
  validate :validate_credentials
  validate :validate_unique_username

  # Callbacks
  before_create :populate_profiles_and_validate
  after_create :populate_realms_and_validate

  def self.additional_privilege_target_types
    [Quota]
  end

  def self.xml_export(accounts)
    doc = Nokogiri::XML('')
    doc.root = Nokogiri::XML::Node.new('provider_accounts', doc)
    root = doc.root.at_xpath('/provider_accounts')
    accounts.each do |account|
      root << account.to_xml
    end
    doc.to_xml
  end

  PRESET_FILTERS_OPTIONS = []

  def self.group_by_type(pool_family)
    res = {}
    family_accounts = pool_family.nil? ? [] : pool_family.provider_accounts
    ProviderAccount.enabled.each do |account|
      ptype = account.provider.provider_type
      res[ptype.deltacloud_driver] ||= {:type => ptype, :accounts => []}
      res[ptype.deltacloud_driver][:accounts] << {:account => account,
                                                  :included => family_accounts.include?(account)}
    end
    res.each do |driver, group|
      group[:included] = (group[:accounts].count{|a| a[:included]} > 0)
    end
    res
  end

  # This is to allow us to look up the ProviderAccount for a given provider image
  def self.find_by_provider_name_and_login(provider_name, login)
    begin
      provider = Provider.find_by_name(provider_name)
      credential_definition = CredentialDefinition.find_by_provider_type_id_and_name(provider.provider_type.id, 'username')
      where_hash = {:credential_definition_id => credential_definition.id, :value => login}
      cred = Credential.where(where_hash).includes(:provider_account).where('provider_accounts.provider_id' => provider.id)
      # The above should always return an array with zero (if an error) or one element, but rescue nil to be safe:
      cred.first.provider_account rescue nil
    rescue
      nil
    end
  end

  def validate_presence_of_credentials
    provider.provider_type.credential_definitions.each do |cd|
      errors.add(:base, "#{I18n.t("provider_accounts.credentials.labels.#{cd.label}")} #{_('can\'t be blank')}") if credentials_hash[cd.name].blank?
    end
  end

  def validate_credentials
    begin
      errors.add(:base, _('Login credentials are invalid for this Provider.')) unless valid_credentials?
    rescue
      errors.add(:base, _('An error occurred when checking Provider credentials. Please check your setup and try again.'))
    end
  end

  def validate_unique_username
    username_cred_def = CredentialDefinition.where(:name => 'username', :provider_type_id => provider.provider_type.id).first!
    username_cred = credentials.detect{ |credential| credential.credential_definition_id == username_cred_def.id }
    same_username_creds =
      Credential.where(:credential_definition_id => username_cred_def.id,
                       :value => username_cred.value).
                 where("credentials.id != ?", username_cred.id).all

    if same_username_creds.any?{ |c| c.provider_account.provider_id == username_cred.provider_account.provider_id  }
      username_cred.errors.add(:value, _('Username has already been taken'))
      errors.add(:base, _('Username has already been taken'))
    end
  end

  def perm_ancestors
    super + [provider]
  end

  def check_provider_images!
    imgs = provider_images.map {|pi| pi.target_image.image_version.base_image.name}
    if imgs.empty?
      true
    else
      raise Aeolus::Conductor::Base::NotDestroyable,
        _('There are following associated provider images: %s. Delete them first.') % imgs.join(', ')
    end
  end

  def check_destroyable_instances!
    not_destroyable_instances = instances.find_all {|i| !i.destroyable?}
    if not_destroyable_instances.empty?
      true
    else
      raise Aeolus::Conductor::Base::NotDestroyable,
        _('The following Deployments have not been stopped: %s') % not_destroyable_instances.
            map{|i| i.deployment.nil? ? i.name : i.deployment.name}.uniq.join(', ')
    end
  end

  def remove_pool_family_assoc
    pool_families.clear
  end

  def connect
    begin
      opts = {
        :username => credentials_hash['username'],
        :password => credentials_hash['password'],
        :driver   => provider.provider_type.deltacloud_driver
      }
      opts[:provider] = provider.deltacloud_provider if provider.deltacloud_provider
      client = DeltaCloud.new(credentials_hash['username'],
                              credentials_hash['password'],
                              provider.url)
      client.with_config(opts)
    rescue Exception => ex
      log_backtrace(ex, 'Error connecting to framework')
      nil
    end
  end

  def pools
    instances.map(&:pool)
  end

  def name
    label.blank? ? credentials_hash['username'] : label
  end

  def populate_profiles_and_validate
    begin
      populate_hardware_profiles
      true
    rescue
      errors.add(:base, _('Failed to populate hardware_profiles: %s') % $!.message)
      false
    end
  end

  def populate_realms_and_validate
    begin
      populate_realms
    rescue
      errors.add(:base, _('Failed to populate Realms: %s') % $!.message)
      raise
    end
    true
  end

  def populate_realms
    provider.populate_realms
  end

  def valid_credentials?
    return false if credentials_hash['username'].blank? || credentials_hash['password'].blank?
    opts = {:driver => provider.provider_type.deltacloud_driver }
    opts[:provider] = provider.deltacloud_provider if provider.deltacloud_provider
    DeltaCloud::valid_credentials?(credentials_hash['username'].to_s,
                                   credentials_hash['password'].to_s,
                                   provider.url,
                                   opts)
  end

  def creds_label_hash
    label_value_pairs = credentials.map do |c|
      { :label => c.credential_definition.label.downcase.split.join('_'),
        :value => c.value }
    end

    apply_provider_specific_creds!(label_value_pairs)

    # The list is ordered by labels. That way we guarantee that the resulting
    # XML is always the same which makes it easier to verify in tests.
    label_value_pairs.sort { |a, b| a[:label] <=> b[:label] }
  end

  def credentials_hash
    credentials.inject({}) do |hash, cred|
      hash[cred.credential_definition.name] = cred.value
      hash
    end
  end

  def credentials_hash=(hash={})
    if provider
      cred_defs = provider.provider_type.credential_definitions
      hash.each do |k,v|
        cred_def = cred_defs.detect {|d| d.name == k.to_s}
        raise "Key #{k} not found" unless cred_def
        unless cred = credentials.detect{ |c| c.credential_definition_id == cred_def.id }
            cred = Credential.new(:provider_account_id => id, :credential_definition_id => cred_def.id)
            credentials << cred
        end
        # we need to handle uploaded files:
        cred.value = v.respond_to?(:read) ? v.read : v
      end
    end
  end

  def build_credentials
    creds = provider.provider_type.credential_definitions.map do |cd|
      cred = credentials.detect {|c| c.credential_definition_id == cd.id}
      cred ||= Credential.new(:credential_definition => cd, :value => nil)
    end

    self.credentials = creds.sort_by do |cred|
      CredentialDefinition::CREDENTIAL_DEFINITIONS_ORDER.index(cred.credential_definition.name)
    end
  end

  # Some providers don't allow fetching HWPs without authentication,
  # so we cannot populate them until after a provider account is added.
  def populate_hardware_profiles
    # If the provider already has hardware profiles, do not refetch them:
    return provider.hardware_profiles if provider.hardware_profiles.present?
    # FIXME: once API has hw profiles, change the below
    hardware_profiles = connect.hardware_profiles
    _provider = provider
    self.transaction do
      hardware_profiles.each do |hardware_profile|
        ar_hardware_profile = HardwareProfile.new(:external_key =>
                                                  hardware_profile.id,
                                                  :name => hardware_profile.id,
                                                  :provider_id => _provider.id)
        ar_hardware_profile.add_properties(hardware_profile)
        ar_hardware_profile.save!
      end
    end
  end

  # Returns XML representation of ProviderAccount
  #
  # @param [Hash] options Options hash
  # @option options [Boolean] :with_credentials (false) Whether to include credentials or not
  # @return [String] XML
  def to_xml(options = {})
    with_credentials = options[:with_credentials] || false

    doc = Nokogiri::XML('')
    doc.root = Nokogiri::XML::Node.new('provider_account', doc)
    root = doc.root.at_xpath('/provider_account')

    node = Nokogiri::XML::Node.new('name', doc)
    node.content = self.name
    root << node

    node = Nokogiri::XML::Node.new('provider', doc)
    node.content = self.provider.name
    root << node

    node = Nokogiri::XML::Node.new('provider_type', doc)
    node.content = self.provider.provider_type.deltacloud_driver
    root << node

    if with_credentials
      credential_node_name = provider.provider_type.deltacloud_driver + '_credentials'
      credential_node = Nokogiri::XML::Node.new(credential_node_name, doc)
      node = Nokogiri::XML::Node.new('provider_credentials', doc)
      node << credential_node
      root << node

      creds_label_hash.each do |h|
        element = Nokogiri::XML::Node.new(h[:label], doc)
        element.content = h[:value]
        credential_node << element
      end
    end

    doc.root.to_xml
  end

  def instance_matches(instance, matched, errors)
    if !provider.enabled?
      errors << _('%s: Provider must be enabled') % name
    elsif !provider.available?
      errors << _('%s: Provider is not available') % name
    elsif quota.reached?
      errors << _('%s: Provider Account quota reached') % name
    # match_provider_hardware_profile returns a single provider
    # hardware_profile that can satisfy the input hardware_profile
    elsif !(hwp = HardwareProfile.match_provider_hardware_profile(provider, instance.hardware_profile))
      errors << _('%s: Hardware Profile match not found') % name
    elsif !(account_image = instance.provider_image_for_account(self))
      errors << _('%s: Image is not pushed to this Provider Account') % name
    elsif instance.requires_config_server? and config_server.nil?
      errors << _('%s: no Config Server available for Provider Account') % name
    else
      if not instance.frontend_realm.nil?
        brealms = instance.frontend_realm.realm_backend_targets.select do |brealm_target|
          brealm_target.target_provider == provider &&
            (brealm_target.target_realm.nil? || (brealm_target.target_realm.available &&
                                                 provider_realms.include?(brealm_target.target_realm)))
        end
        if brealms.empty?
          errors << _('%s: Frontend Realm %s is not mapped to an applicable Provider or Provider Realm') % [name, instance.frontend_realm.name]
        else
          brealms.each do |brealm_target|
            # add match if realm is mapped to provider or if it's mapped to
            # backend realm which is available and is accessible for this
            # provider account
            matched << InstanceMatch.new(
              :pool_family => instance.pool.pool_family,
              :provider_account => self,
              :hardware_profile => hwp,
              :provider_image => account_image.external_image_id,
              :provider_realm => brealm_target.target_realm,
              :instance => instance
            )
          end
        end
      else
        matched << InstanceMatch.new(
          :pool_family => instance.pool.pool_family,
          :provider_account => self,
          :hardware_profile => hwp,
          :provider_image => account_image.external_image_id,
          :provider_realm => nil,
          :instance => instance
        )
      end
    end
  end

  def failure_count(options = {})
    relation = self.events.where(:status_code => 'provider_account_failure')

    if options[:from].present?
      relation = relation.where('events.event_time >= :from', :from => options[:from])
    end

    if options[:to].present?
      relation = relation.where('events.event_time <= :to', :to => options[:to])
    end

    relation.count
  end

  def to_polymorphic_path_param(polymorphic_path_extras)
    [provider, self]
  end

  private

  def self.apply_search_filter(search)
    return scoped unless search
    includes(:provider => [:provider_type]).where("lower(provider_accounts.label) LIKE :search OR lower(providers.name) LIKE :search OR lower(provider_types.name) LIKE :search", :search => "%#{search.downcase}%")
  end

  def apply_provider_specific_creds!(label_value_pairs)
    # for openstack we keep username and tenant in username field because of
    # deltacloud, imagefactory expects separate fields,
    # it also requires authentication strategy field
    if provider.provider_type.deltacloud_driver == 'openstack' &&
      userhash = label_value_pairs.find {|i| i[:label] == 'username'}

      username, tenant = userhash[:value].split('+')
      userhash[:value] = username
      label_value_pairs << { :label => 'strategy', :value => 'keystone' }
      label_value_pairs << { :label => 'tenant', :value => tenant }

      # Also add Keystone URL as auth_url for Factory:
      label_value_pairs << { :label => 'auth_url', :value => provider.deltacloud_provider }
    end
  end
end