ManageIQ/manageiq-providers-openstack

View on GitHub
app/models/manageiq/providers/openstack/cloud_manager.rb

Summary

Maintainability
B
4 hrs
Test Coverage
D
62%
class ManageIQ::Providers::Openstack::CloudManager < ManageIQ::Providers::CloudManager
  has_one :network_manager,
          :foreign_key => :parent_ems_id,
          :class_name  => "ManageIQ::Providers::Openstack::NetworkManager",
          :autosave    => true,
          :dependent   => :destroy
  has_many :storage_managers,
           :foreign_key => :parent_ems_id,
           :class_name  => "ManageIQ::Providers::StorageManager",
           :autosave    => true,
           :dependent   => :destroy
  has_many :snapshots, :through => :vms_and_templates
  include ManageIQ::Providers::Openstack::CinderManagerMixin
  include ManageIQ::Providers::Openstack::SwiftManagerMixin
  include ManageIQ::Providers::Openstack::ManagerMixin
  include ManageIQ::Providers::Openstack::IdentitySyncMixin

  supports :catalog
  supports :create
  supports :provisioning
  supports :cloud_tenants
  supports :cloud_tenant_mapping do
    if defined?(self.class.module_parent::CloudManager::CloudTenant) && !tenant_mapping_enabled?
      _("Tenant mapping is disabled on the Provider")
    elsif !defined?(self.class.module_parent::CloudManager::CloudTenant)
      _("Tenant mapping is supported only when CloudTenant exists on the CloudManager")
    end
  end
  supports :create_flavor
  supports :label_mapping
  supports :events do
    _("Events are not supported") unless capabilities["events"]
  end
  supports :metrics
  supports :storage_manager

  supports :cinder_service do
    "Cinder service unavailable" unless openstack_handle.detect_volume_service.name == :cinder
  end

  supports :swift_service do
    "Swift service unavailable" unless openstack_handle.detect_volume_service.name == :swift
  end

  before_create :ensure_managers

  before_update :ensure_managers_zone_and_provider_region
  after_save :refresh_parent_infra_manager

  private_class_method def self.provider_id_options
    t = ManageIQ::Providers::Openstack::Provider
    Rbac
      .filtered(t.order(t.arel_table[:name].lower))
      .pluck(:name, :id)
      .map do |name, id|
        {
          :label => name,
          :value => id.to_s,
        }
      end
  end

  def self.params_for_create
    {
      :fields => [
        {
          :component => "text-field",
          :id        => "provider_region",
          :name      => "provider_region",
          :label     => _("Provider Region"),
        },
        {
          :component   => "select",
          :id          => "provider_id",
          :name        => "provider_id",
          :label       => _("Openstack Infra Provider"),
          :isClearable => true,
          :simpleValue => true,
          :options     => provider_id_options,
        },
        {
          :component    => "select",
          :id           => "api_version",
          :name         => "api_version",
          :label        => _("API Version"),
          :initialValue => 'v3',
          :isRequired   => true,
          :validate     => [{:type => "required"}],
          :options      => [
            {
              :label => 'Keystone V2',
              :value => 'v2',
            },
            {
              :label => 'Keystone V3',
              :value => 'v3',
            },
          ],
        },
        {
          :component  => 'text-field',
          :id         => 'uid_ems',
          :name       => 'uid_ems',
          :label      => _('Domain ID'),
          :isRequired => true,
          :condition  => {
            :when => 'api_version',
            :is   => 'v3',
          },
          :validate   => [{
            :type      => "required",
            :condition => {
              :when => 'api_version',
              :is   => 'v3',
            }
          }],
        },
        {
          :component => 'switch',
          :id        => 'tenant_mapping_enabled',
          :name      => 'tenant_mapping_enabled',
          :label     => _('Tenant Mapping Enabled'),
        },
        {
          :component => 'sub-form',
          :id        => 'endpoints-subform',
          :name      => 'endpoints-subform',
          :title     => _('Endpoints'),
          :fields    => [
            {
              :component => 'sub-form',
              :id        => 'default-tab',
              :name      => 'default-tab',
              :title     => _('Default'),
              :fields    => [
                {
                  :component              => 'validate-provider-credentials',
                  :id                     => 'authentications.default.valid',
                  :name                   => 'authentications.default.valid',
                  :skipSubmit             => true,
                  :isRequired             => true,
                  :validationDependencies => %w[name type zone_id api_version provider_region uid_ems],
                  :fields                 => [
                    {
                      :component    => "select",
                      :id           => "endpoints.default.security_protocol",
                      :name         => "endpoints.default.security_protocol",
                      :label        => _("Security Protocol"),
                      :isRequired   => true,
                      :initialValue => 'ssl-with-validation',
                      :validate     => [{:type => "required"}],
                      :options      => [
                        {
                          :label => _("SSL without validation"),
                          :value => "ssl-no-validation"
                        },
                        {
                          :label => _("SSL"),
                          :value => "ssl-with-validation"
                        },
                        {
                          :label => _("Non-SSL"),
                          :value => "non-ssl"
                        }
                      ]
                    },
                    {
                      :component  => "text-field",
                      :id         => "endpoints.default.hostname",
                      :name       => "endpoints.default.hostname",
                      :label      => _("Hostname (or IPv4 or IPv6 address)"),
                      :isRequired => true,
                      :validate   => [{:type => "required"}],
                    },
                    {
                      :component    => "text-field",
                      :id           => "endpoints.default.port",
                      :name         => "endpoints.default.port",
                      :label        => _("API Port"),
                      :type         => "number",
                      :initialValue => 13_000,
                      :isRequired   => true,
                      :validate     => [{:type => "required"}],
                    },
                    {
                      :component  => "text-field",
                      :id         => "authentications.default.userid",
                      :name       => "authentications.default.userid",
                      :label      => _("Username"),
                      :isRequired => true,
                      :validate   => [{:type => "required"}],
                    },
                    {
                      :component  => "password-field",
                      :id         => "authentications.default.password",
                      :name       => "authentications.default.password",
                      :label      => _("Password"),
                      :type       => "password",
                      :isRequired => true,
                      :validate   => [{:type => "required"}],
                    },
                    {
                      :component => 'sub-form',
                      :id        => 'events-tab',
                      :name      => 'events-tab',
                      :title     => _('Events'),
                      :fields    => [
                        {
                          :component    => 'protocol-selector',
                          :id           => 'event_stream_selection',
                          :name         => 'event_stream_selection',
                          :initialValue => 'none',
                          :label        => _('Type'),
                          :options      => [
                            {
                              :label => _('Disabled'),
                              :value => 'none',
                            },
                            {
                              :label => _('Ceilometer'),
                              :value => 'ceilometer',
                              :pivot => 'endpoints.ceilometer.hostname'
                            },
                            {
                              :label => _('STF'),
                              :value => 'stf',
                              :pivot => 'endpoints.stf.hostname',
                            },
                            {
                              :label => _('AMQP'),
                              :value => 'amqp',
                              :pivot => 'endpoints.amqp.hostname',
                            },
                          ],
                        },
                        {
                          :component              => 'sub-form',
                          :id                     => 'endpoints.amqp.valid',
                          :name                   => 'endpoints.amqp.valid',
                          :validationDependencies => %w[type event_stream_selection],
                          :condition              => {
                            :when => 'event_stream_selection',
                            :is   => 'amqp',
                          },
                          :fields                 => [
                            {
                              :component  => "text-field",
                              :id         => "endpoints.amqp.hostname",
                              :name       => "endpoints.amqp.hostname",
                              :label      => _("Hostname (or IPv4 or IPv6 address)"),
                              :isRequired => true,
                              :validate   => [{:type => "required"}],
                            },
                            {
                              :component    => "text-field",
                              :id           => "endpoints.amqp.port",
                              :name         => "endpoints.amqp.port",
                              :label        => _("API Port"),
                              :type         => "number",
                              :isRequired   => true,
                              :initialValue => 5672,
                              :validate     => [{:type => "required"}],
                            },
                            {
                              :component  => "text-field",
                              :id         => "authentications.amqp.userid",
                              :name       => "authentications.amqp.userid",
                              :label      => _("Username"),
                              :isRequired => true,
                              :validate   => [{:type => "required"}],
                            },
                            {
                              :component  => "password-field",
                              :id         => "authentications.amqp.password",
                              :name       => "authentications.amqp.password",
                              :label      => _("Password"),
                              :type       => "password",
                              :isRequired => true,
                              :validate   => [{:type => "required"}],
                            },
                          ],
                        },
                        {
                          :component              => 'sub-form',
                          :id                     => 'endpoints.stf.valid',
                          :name                   => 'endpoints.stf.valid',
                          :validationDependencies => %w[type event_stream_selection],
                          :condition              => {
                            :when => 'event_stream_selection',
                            :is   => 'stf',
                          },
                          :fields                 => [
                            {
                              :component    => "select",
                              :id           => "endpoints.stf.security_protocol",
                              :name         => "endpoints.stf.security_protocol",
                              :label        => _("Security Protocol"),
                              :isRequired   => true,
                              :initialValue => 'ssl-with-validation',
                              :validate     => [{:type => "required"}],
                              :options      => [
                                {
                                  :label => _("SSL without validation"),
                                  :value => "ssl-no-validation"
                                },
                                {
                                  :label => _("SSL"),
                                  :value => "ssl-with-validation"
                                },
                                {
                                  :label => _("Non-SSL"),
                                  :value => "non-ssl"
                                }
                              ]
                            },
                            {
                              :component  => "text-field",
                              :id         => "endpoints.stf.hostname",
                              :name       => "endpoints.stf.hostname",
                              :label      => _("Hostname (or IPv4 or IPv6 address)"),
                              :isRequired => true,
                              :validate   => [{:type => "required"}],
                            },
                            {
                              :component    => "text-field",
                              :id           => "endpoints.stf.port",
                              :name         => "endpoints.stf.port",
                              :label        => _("API Port"),
                              :type         => "number",
                              :isRequired   => true,
                              :initialValue => 5666,
                              :validate     => [{:type => "required"}],
                            },
                          ]
                        }
                      ],
                    },
                  ]
                },
              ]
            },
            {
              :component => 'sub-form',
              :id        => 'ssh_keypair-tab',
              :name      => 'ssh_keypair-tab',
              :title     => _('RSA key pair'),
              :fields    => [
                :component => 'provider-credentials',
                :id        => 'endpoints.ssh_keypair.valid',
                :name      => 'endpoints.ssh_keypair.valid',
                :fields    => [
                  {
                    :component    => 'text-field',
                    :hideField    => true,
                    :label        => 'ssh_keypair',
                    :id           => 'endpoints.ssh_keypair',
                    :name         => 'endpoints.ssh_keypair',
                    :initialValue => '',
                    :condition    => {
                      :when       => 'authentications.ssh_keypair.userid',
                      :isNotEmpty => true,
                    },
                  },
                  {
                    :component => "text-field",
                    :id        => "authentications.ssh_keypair.userid",
                    :name      => "authentications.ssh_keypair.userid",
                    :label     => _("Username"),
                  },
                  {
                    :component      => "password-field",
                    :id             => "authentications.ssh_keypair.auth_key",
                    :name           => "authentications.ssh_keypair.auth_key",
                    :componentClass => 'textarea',
                    :rows           => 10,
                    :label          => _("Private Key"),
                  },
                ],
              ],
            },
          ],
        },
      ]
    }
  end

  def self.build_ceilometer_endpoint!(params, endpoints, _authentications)
    if params.delete("event_stream_selection") == "ceilometer"
      default_endpoint = endpoints.detect { |ep| ep["role"] == "default" }
      endpoints << {"role" => "ceilometer", "hostname" => default_endpoint["hostname"]}
    end
  end

  def self.create_from_params(params, endpoints, authentications)
    build_ceilometer_endpoint!(params, endpoints, authentications)

    super
  end

  def edit_with_params(params, endpoints, authentications)
    self.class.build_ceilometer_endpoint!(params, endpoints, authentications)

    super
  end

  def hostname_required?
    enabled?
  end

  def refresh_parent_infra_manager
    # If the cloud manager had a new/different infra manager attached to it
    # during this save, refresh the infra manager.
    if provider_id && (attribute_before_last_save(:provider_id) != provider_id) && provider.infra_ems
      EmsRefresh.queue_refresh(provider.infra_ems)
      _log.info("EMS: [#{name}] refreshing attached infra manager [#{provider.infra_ems.name}]")
    end
  end

  def ensure_managers
    ensure_network_manager
    ensure_cinder_manager
    ensure_swift_manager
    ensure_managers_zone_and_provider_region
  end

  def ensure_managers_zone_and_provider_region
    if network_manager
      network_manager.enabled         = enabled
      network_manager.zone_id         = zone_id
      network_manager.tenant_id       = tenant_id
      network_manager.provider_region = provider_region
    end

    if cinder_manager
      cinder_manager.enabled         = enabled
      cinder_manager.zone_id         = zone_id
      cinder_manager.tenant_id       = tenant_id
      cinder_manager.provider_region = provider_region
    end

    if swift_manager
      swift_manager.enabled         = enabled
      swift_manager.zone_id         = zone_id
      swift_manager.tenant_id       = tenant_id
      swift_manager.provider_region = provider_region
    end
  end

  def ensure_network_manager
    build_network_manager unless network_manager
  end

  def ensure_cinder_manager
    build_cinder_manager unless cinder_manager
  end

  def ensure_swift_manager
    build_swift_manager unless swift_manager
  end

  after_save :save_on_other_managers

  def save_on_other_managers
    storage_managers.update_all(:tenant_mapping_enabled => tenant_mapping_enabled)
    if network_manager
      network_manager.tenant_mapping_enabled = tenant_mapping_enabled
      network_manager.save!
    end
  end

  def cinder_service
    vs = openstack_handle.detect_volume_service
    vs&.name == :cinder ? vs : nil
  end

  def swift_service
    vs = openstack_handle.detect_storage_service
    vs&.name == :swift ? vs : nil
  end

  def verify_credentials(auth_type = nil, options = {})
    options[:service] ||= "Compute"
    ret = super
    return ret unless auth_type.nil?

    capabilities["events"] = !!event_monitor_available?
    save! if changed?

    true
  end

  def self.ems_type
    @ems_type ||= "openstack".freeze
  end

  def self.description
    @description ||= "OpenStack".freeze
  end

  def self.vm_vendor
    @vm_vendor ||= "openstack".freeze
  end

  def self.default_blacklisted_event_names
    %w(
      identity.authenticate
      scheduler.run_instance.start
      scheduler.run_instance.scheduled
      scheduler.run_instance.end
    )
  end

  def self.api_allowed_attributes
    %w[keystone_v3_domain_id].freeze
  end

  def hostname_uniqueness_valid?
    return unless hostname_required?
    return unless hostname.present? # Presence is checked elsewhere

    existing_providers =
      self.class
          .joins(:endpoints)
          .where.not(:id => id)
          .where(
            :endpoints       => {:hostname => hostname.downcase},
            :uid_ems         => keystone_v3_domain_id,
            :provider_region => provider_region
          )

    errors.add(:hostname, "has already been taken") if existing_providers.any?
  end

  def supported_auth_types
    %w(default amqp ssh_keypair)
  end

  def block_storage_manager
    cinder_manager
  end

  def self.catalog_types
    {"openstack" => N_("OpenStack")}
  end

  def required_credential_fields(type)
    case type.to_s
    when 'ssh_keypair' then [:userid, :auth_key]
    else                    [:userid, :password]
    end
  end

  def authentication_status_ok?(type = nil)
    return true if type == :ssh_keypair
    super
  end

  def authentications_to_validate
    authentication_for_providers.collect(&:authentication_type) - [:ssh_keypair]
  end

  def volume_availability_zones
    availability_zones.where("'volume' = ANY(provider_services_supported)")
  end

  def allow_targeted_refresh?
    true
  end

  #
  # Operations
  #

  def vm_create_snapshot(vm, options = {})
    log_prefix = "vm=[#{vm.name}]"

    compute_service = openstack_handle.compute_service(vm.cloud_tenant.name)
    snapshot = compute_service.create_image(vm.ems_ref, options[:name], :description   => options[:desc],
                                                                        :instance_uuid => vm.ems_ref).body["image"]

    Notification.create(:type => :vm_snapshot_success, :subject => vm, :options => {:snapshot_op => 'create'})
    snapshot_id = snapshot["id"]

    return snapshot_id
  rescue => err
    _log.error "#{log_prefix}, error: #{err}"
    _log.debug { err.backtrace.join("\n") }
    raise MiqException::MiqOpenstackApiRequestError, parse_error_message_from_fog_response(err)
  end

  def vm_remove_snapshot(vm, options = {})
    require 'OpenStackExtract/MiqOpenStackVm/MiqOpenStackInstance'

    snapshot_uid = options[:snMor]

    log_prefix = "snapshot=[#{snapshot_uid}]"

    miq_openstack_instance = MiqOpenStackInstance.new(vm.ems_ref, openstack_handle)
    miq_openstack_instance.delete_evm_snapshot(snapshot_uid)
    Notification.create(:type => :vm_snapshot_success, :subject => vm, :options => {:snapshot_op => 'remove'})

    # Remove from the snapshots table.
    ar_snapshot = vm.snapshots.find_by(:ems_ref  => snapshot_uid)
    _log.debug "#{log_prefix}: ar_snapshot = #{ar_snapshot.class.name}"
    ar_snapshot.destroy if ar_snapshot

    # Remove from the vms table.
    ar_template = miq_templates.find_by(:ems_ref  => snapshot_uid)
    _log.debug "#{log_prefix}: ar_template = #{ar_template.class.name}"
    ar_template.destroy if ar_template
  rescue => err
    _log.error "#{log_prefix}, error: #{err}"
    _log.debug { err.backtrace.join("\n") }
    raise
  end

  def vm_remove_all_snapshots(vm, _options = {})
    vm.snapshots.each { |snapshot| vm_remove_snapshot(vm, :snMor => snapshot.uid) }
  end

  # TODO: Should this be in a VM-specific subclass or mixin?
  #       This is a general EMS question.
  def vm_create_evm_snapshot(vm, options = {})
    require "OpenStackExtract/MiqOpenStackVm/MiqOpenStackInstance"

    log_prefix = "vm=[#{vm.name}]"

    miq_openstack_instance = MiqOpenStackInstance.new(vm.ems_ref, openstack_handle)
    miq_snapshot = miq_openstack_instance.create_evm_snapshot(options)

    # Add new snapshot image to the vms table. Type is TemplateOpenstack.
    miq_templates.create!(
      :type     => "ManageIQ::Providers::Openstack::CloudManager::Template",
      :vendor   => "openstack",
      :name     => miq_snapshot.name,
      :uid_ems  => miq_snapshot.id,
      :ems_ref  => miq_snapshot.id,
      :template => true,
      :location => "unknown"
    )

    # Add new snapshot to the snapshots table.
    vm.snapshots.create!(
      :name        => miq_snapshot.name,
      :description => options[:desc],
      :uid         => miq_snapshot.id,
      :uid_ems     => miq_snapshot.id,
      :ems_ref     => miq_snapshot.id
    )
    return miq_snapshot.id
  rescue => err
    _log.error "#{log_prefix}, error: #{err}"
    _log.debug { err.backtrace.join("\n") }
    raise
  end

  def vm_delete_evm_snapshot(vm, image_id)
    require "OpenStackExtract/MiqOpenStackVm/MiqOpenStackInstance"

    log_prefix = "snapshot=[#{image_id}]"

    miq_openstack_instance = MiqOpenStackInstance.new(vm.ems_ref, openstack_handle)
    miq_openstack_instance.delete_evm_snapshot(image_id)

    # Remove from the snapshots table.
    ar_snapshot = vm.snapshots.find_by(:ems_ref  => image_id)
    _log.debug "#{log_prefix}: ar_snapshot = #{ar_snapshot.class.name}"
    ar_snapshot.destroy if ar_snapshot

    # Remove from the vms table.
    ar_template = miq_templates.find_by(:ems_ref  => image_id)
    _log.debug "#{log_prefix}: ar_template = #{ar_template.class.name}"
    ar_template.destroy if ar_template
  rescue => err
    _log.error "#{log_prefix}, error: #{err}"
    _log.debug { err.backtrace.join("\n") }
    raise
  end

  def vm_attach_volume(vm, options)
    volume = CloudVolume.find_by(:id => options[:volume_id])
    volume.raw_attach_volume(vm.ems_ref, options[:device])
  end

  def vm_detach_volume(vm, options)
    volume = CloudVolume.find_by(:id => options[:volume_id])
    volume.raw_detach_volume(vm.ems_ref)
  end

  def create_host_aggregate(options)
    ManageIQ::Providers::Openstack::CloudManager::HostAggregate.create_aggregate(self, options)
  end

  def create_host_aggregate_queue(userid, options)
    ManageIQ::Providers::Openstack::CloudManager::HostAggregate.create_aggregate_queue(userid, self, options)
  end

  def self.event_monitor_class
    ManageIQ::Providers::Openstack::CloudManager::EventCatcher
  end

  #
  # Statistics
  #

  def block_storage_disk_usage
    cloud_volumes.where.not(:status => "error").sum(:size).to_f +
      cloud_volume_snapshots.where.not(:status => "error").sum(:size).to_f
  end

  def object_storage_disk_usage(swift_replicas = 1)
    cloud_object_store_containers.sum(:bytes).to_f * swift_replicas
  end

  def self.display_name(number = 1)
    n_('Cloud Provider (OpenStack)', 'Cloud Providers (OpenStack)', number)
  end

  LABEL_MAPPING_ENTITIES = {
    "VmOpenstack" => "ManageIQ::Providers::Openstack::CloudManager::Vm"
  }.freeze

  def self.entities_for_label_mapping
    LABEL_MAPPING_ENTITIES
  end

  def self.label_mapping_prefix
    ems_type
  end
end