ManageIQ/manageiq-providers-openstack

View on GitHub
lib/manageiq/providers/openstack/legacy/openstack_handle/handle.rb

Summary

Maintainability
B
4 hrs
Test Coverage
B
86%
require 'active_support/inflector'
require 'util/miq-exception'
require 'parallel'

module OpenstackHandle
  class Handle
    attr_accessor :username, :password, :address, :port, :api_version, :security_protocol, :connection_options
    attr_reader :project_name
    attr_writer   :default_tenant_name

    SERVICE_NAME_MAP = {
      "Compute"       => :nova,
      "Network"       => :neutron,
      "NFV"           => :nfv,
      "Image"         => :glance,
      "Volume"        => :cinder,
      "Storage"       => :swift,
      "Metering"      => :ceilometer,
      "Baremetal"     => :baremetal,
      "Orchestration" => :orchestration,
      "Planning"      => :planning,
      "Introspection" => :introspection,
      "Workflow"      => :workflow,
      "Metric"        => :metric,
      "Event"         => :event,
    }

    def self.try_connection(security_protocol, ssl_options = {})
      # For backwards compatibility take blank security_protocol as SSL
      if security_protocol.blank? || security_protocol == 'ssl' || security_protocol == 'ssl-no-validation'
        yield "https", {:ssl_verify_peer => false}
      elsif security_protocol == 'ssl-with-validation'
        excon_ssl_options = {:ssl_verify_peer => true}.merge(ssl_options)
        yield "https", excon_ssl_options
      else
        yield "http", {}
      end
    end

    def self.raw_connect_try_ssl(username, password, address, port, service = "Compute", options = nil,
                                 security_protocol = nil)
      opts = options.dup
      ssl_options = opts.delete(:ssl_options) || {}
      try_connection(security_protocol, ssl_options) do |scheme, connection_options|
        auth_url = auth_url(address, port, scheme)
        opts[:connection_options] = (opts[:connection_options] || {}).merge(connection_options)
        raw_connect(username, password, auth_url, service, opts)
      end
    end

    def self.raw_connect(username, password, auth_url, service = "Compute", extra_opts = nil)
      opts = {
        :openstack_auth_url      => auth_url,
        :openstack_username      => username,
        :openstack_api_key       => password,
        :openstack_endpoint_type => 'publicURL',
      }
      opts.merge!(extra_opts) if extra_opts

      opts[:openstack_service_type] = ["nfv-orchestration"] if service == "NFV"
      opts[:openstack_service_type] = ["workflowv2"] if service == "Workflow"

      if service == "Planning"
        Fog::OpenStack::Planning.new(opts)
      elsif service == "Workflow"
        Fog::OpenStack::Workflow.new(opts)
      elsif service == "Metric"
        Fog::OpenStack::Metric.new(opts)
      elsif service == "Event"
        Fog::OpenStack::Event.new(opts)
      else
        Fog::OpenStack.const_get(service).new(opts)
      end
    rescue Fog::OpenStack::Auth::Catalog::ServiceTypeError, Fog::Service::NotFound
      $fog_log.warn("MIQ(#{self.class.name}##{__method__}) "\
                    "Service #{service} not available for openstack provider #{auth_url}")
      raise MiqException::ServiceNotAvailable
    end

    def self.auth_url(address, port = 5000, scheme = "http", path = '')
      URI::Generic.build(:scheme => scheme, :host => address, :port => port.to_i, :path => path).to_s
    end

    class << self
      attr_writer :connection_options
    end

    class << self
      attr_reader :connection_options
    end

    def initialize(username, password, address, port = nil, api_version = nil, security_protocol = nil,
                   extra_options = {})
      @username          = username
      @password          = password
      @address           = address
      @port              = port || 5000
      @api_version       = api_version || 'v2'
      @security_protocol = security_protocol || 'ssl'
      @extra_options     = extra_options
      @thread_limit      = Settings.ems_refresh.openstack.parallel_thread_limit || 0

      @connection_cache   = {}
      @connection_options = self.class.connection_options
    end

    def thread_limit
      Rails.env.test? ? 0 : @thread_limit
    end

    def ssl_options
      @ssl_options ||= {}
      return @ssl_options unless @ssl_options.blank?

      @ssl_options[:ssl_ca_file]    = @extra_options[:ssl_ca_file] unless @extra_options[:ssl_ca_file].blank?
      @ssl_options[:ssl_ca_path]    = @extra_options[:ssl_ca_path] unless @extra_options[:ssl_ca_path].blank?
      # ssl_cert_store is dependent on the presence of ssl_ca_file
      @ssl_options[:ssl_cert_store] = @extra_options[:ssl_cert_store] unless @extra_options[:ssl_ca_file].blank?
      @ssl_options
    end

    def excon_options
      @excon_options ||= {}
      return @excon_options unless @excon_options.blank?

      @excon_options[:omit_default_port] = @extra_options[:omit_default_port] unless
                                           @extra_options[:omit_default_port].blank?
      @excon_options[:read_timeout]      = @extra_options[:read_timeout] unless @extra_options[:read_timeout].blank?

      # Beware of proxies that lie about gzip encoding and content length.
      if @extra_options[:proxy].present?
        @excon_options[:proxy] = @extra_options[:proxy]
        @excon_options[:headers] = {'Accept-Encoding' => 'identity,deflate'}
      end

      @excon_options
    end

    def domain_id
      @extra_options[:domain_id]
    end

    def region
      @extra_options[:region]
    end

    def browser_url
      "http://#{address}/dashboard"
    end

    def connect(options = {})
      opts     = options.dup
      service  = (opts.delete(:service) || "Compute").to_s.camelize
      tenant   = opts.delete(:tenant_name)
      discover_tenants = opts.fetch(:discover_tenants, true)
      opts.delete(:discover_tenants)
      domain   = domain_id

      # Do not send auth_type to fog, it throws warning
      opts.delete(:auth_type)

      if discover_tenants && tenant.blank?
        tenant = default_tenant_name
      end

      if api_version == 'v2'
        opts[:openstack_tenant] = tenant if tenant
        opts[:openstack_identity_api_version] = 'v2.0'
      else # "v3"
        opts[:openstack_project_name] = @project_name = tenant if tenant
        opts[:openstack_project_domain_id] = domain
        opts[:openstack_user_domain_id]    = domain
      end

      opts[:openstack_region] = region unless service == "Identity"

      svc_cache = (@connection_cache[service] ||= {})
      svc_cache[tenant] ||= begin
        opts[:connection_options] = (connection_options || {}).merge(excon_options)
        opts[:ssl_options]        = ssl_options

        raw_service = self.class.raw_connect_try_ssl(username, password, address, port, service, opts,
                                                     security_protocol)

        service_wrapper_name = "#{service}Delegate"
        # Allow openstack to define new services without explicitly requiring a
        # service wrapper.
        if OpenstackHandle.const_defined?(service_wrapper_name)
          OpenstackHandle.const_get(service_wrapper_name).new(raw_service, self, SERVICE_NAME_MAP[service])
        else
          raw_service
        end
      end
    end

    def baremetal_service(tenant_name = nil)
      connect(:service => "Baremetal", :tenant_name => tenant_name)
    end

    def detect_baremetal_service(tenant_name = nil)
      detect_service("Baremetal", tenant_name)
    end

    def orchestration_service(tenant_name = nil)
      connect(:service => "Orchestration", :tenant_name => tenant_name)
    end

    def detect_orchestration_service(tenant_name = nil)
      detect_service("Orchestration", tenant_name)
    end

    def planning_service(tenant_name = nil)
      connect(:service => "Planning", :tenant_name => tenant_name)
    end

    def detect_planning_service(tenant_name = nil)
      detect_service("Planning", tenant_name)
    end

    def compute_service(tenant_name = nil)
      connect(:service => "Compute", :tenant_name => tenant_name)
    end
    alias_method :connect_compute, :compute_service

    def identity_service(discover_tenants = true)
      connect(:service => "Identity", :discover_tenants => discover_tenants)
    end
    alias_method :connect_identity, :identity_service

    def network_service(tenant_name = nil)
      connect(:service => "Network", :tenant_name => tenant_name)
    end
    alias_method :connect_network, :network_service

    def detect_network_service(tenant_name = nil)
      detect_service("Network", tenant_name)
    end

    def nfv_service(tenant_name = nil)
      connect(:service => "NFV", :tenant_name => tenant_name)
    end
    alias_method :connect_nfv, :nfv_service

    def detect_nfv_service(tenant_name = nil)
      detect_service("NFV", tenant_name)
    end

    def image_service(tenant_name = nil)
      connect(:service => "Image", :tenant_name => tenant_name)
    end
    alias_method :connect_image, :image_service

    def detect_image_service(tenant_name = nil)
      detect_service("Image", tenant_name)
    end

    def volume_service(tenant_name = nil)
      connect(:service => "Volume", :tenant_name => tenant_name)
    end
    alias_method :connect_volume, :volume_service
    alias_method :cinder_service, :volume_service

    def detect_volume_service(tenant_name = nil)
      detect_service("Volume", tenant_name)
    end

    def storage_service(tenant_name = nil)
      connect(:service => "Storage", :tenant_name => tenant_name)
    end
    alias_method :connect_storage, :storage_service
    alias_method :swift_service,   :storage_service

    def detect_storage_service(tenant_name = nil)
      detect_service("Storage", tenant_name)
    end

    def metering_service(tenant_name = nil)
      connect(:service => "Metering", :tenant_name => tenant_name)
    end
    alias_method :connect_metering, :metering_service

    def detect_metering_service(tenant_name = nil)
      detect_service("Metering", tenant_name)
    end

    def introspection_service(tenant_name = nil)
      connect(:service => "Introspection", :tenant_name => tenant_name)
    end
    alias_method :connect_introspection, :introspection_service

    def detect_introspection_service(tenant_name = nil)
      detect_service("Introspection", tenant_name)
    end

    def workflow_service(tenant_name = nil)
      connect(:service => "Workflow", :tenant_name => tenant_name)
    end
    alias_method :connect_workflow, :workflow_service

    def metric_service(tenant_name = nil)
      connect(:service => "Metric", :tenant_name => tenant_name)
    end
    alias_method :connect_metric, :metric_service

    def event_service(tenant_name = nil)
      connect(:service => "Event", :tenant_name => tenant_name)
    end
    alias_method :connect_event, :event_service

    def detect_metric_service(tenant_name = nil)
      detect_service("Metric", tenant_name)
    end

    def detect_event_service(tenant_name = nil)
      detect_service("Event", tenant_name)
    end

    def detect_workflow_service(tenant_name = nil)
      detect_service("Workflow", tenant_name)
    end

    def detect_service(service, tenant_name = nil)
      connect(:service => service, :tenant_name => tenant_name)
    rescue => err
      $fog_log.warn("MIQ(#{self.class.name}.#{__method__}) detect service error: #{err}")
      return nil
    end

    def tenants
      @tenants ||= identity_service(false).visible_tenants
    end

    def tenant_names
      @tenant_names ||= tenants.collect(&:name)
    end

    def tenant_accessible?(name)
      begin
        compute_service(name)
        true
      rescue Excon::Errors::Unauthorized
        false
      end
    end

    def default_tenant_name
      @default_tenant_name ||= detect_default_tenant_name
    end

    def detect_default_tenant_name
      return "admin" if tenant_accessible?("admin")

      tenant_names.each do |name|
        next if name == "services"
        return name if tenant_accessible?(name)
      end

      nil
    end

    def service_for_each_accessible_tenant(service_name)
      services = []
      all_tenants = tenants
      all_tenants.delete("services")
      ::Parallel.each(all_tenants, :in_threads => thread_limit) do |tenant|
        service = detect_service(service_name, tenant.name)
        if service
          services << [service, tenant]
        else
          $fog_log.warn("MIQ(#{self.class.name}##{__method__}) "\
                        "Could not access service #{service_name} for tenant #{tenant.name} on OpenStack #{@address}")
        end
      end
      services
    end

    def accessor_for_accessible_tenants(service, accessor, unique_id, array_accessor = true)
      results = []
      not_found_error = Fog::OpenStack.const_get(service)::NotFound
      ::Parallel.each(service_for_each_accessible_tenant(service), :in_threads => thread_limit) do |svc, project|

        response = begin
          if accessor.kind_of?(Proc)
            accessor.call(svc)
          else
            array_accessor ? svc.send(accessor).to_a : svc.send(accessor)
          end
        rescue not_found_error, Excon::Errors::NotFound => err
          $fog_log.warn("MIQ(#{self.class.name}.#{__method__}) HTTP 404 Error during OpenStack request. " \
                        "Skipping inventory item #{service} #{accessor}\n#{err}")
          nil
        rescue Excon::Error::Timeout, Fog::Errors::TimeoutError => err
          $fog_log.warn("MIQ(#{self.class.name}.#{__method__}) timeout during OpenStack request. " \
                        "Skipping inventory item #{service} #{accessor}\n#{err}")
          nil
        rescue Excon::Error::Socket, Fog::Errors::Error => err
          # Neutron probably not setup or failed
          $fog_log.warn("MIQ(#{self.class.name}.#{__method__}) failed to connect during OpenStack request. " \
                        "Skipping inventory item #{service} #{accessor}\n#{err}")
          nil
        end

        if !response.nil? && array_accessor && response.last.kind_of?(Fog::Model)
          response.map { |item| item.project = project }
        end

        if response
          array_accessor ? results.concat(response) : results << response
        end
      end

      if unique_id.blank? && array_accessor && !results.nil?
        last_object = results.last
        unique_id = last_object.identity_name if last_object.kind_of?(Fog::Model)
      end

      if unique_id
        results.uniq! { |item| item.kind_of?(Hash) ? item[unique_id] : item.send(unique_id) }
      end
      results
    end
  end
end