ManageIQ/manageiq-providers-nuage

View on GitHub
app/models/manageiq/providers/nuage/manager_mixin.rb

Summary

Maintainability
A
0 mins
Test Coverage
C
78%
module ManageIQ::Providers::Nuage::ManagerMixin
  extend ActiveSupport::Concern

  module ClassMethods
    def params_for_create
      {
        :fields => [
          {
            :component    => "select",
            :id           => "api_version",
            :name         => "api_version",
            :label        => _("API Version"),
            :initialValue => 'v3',
            :isRequired   => true,
            :validate     => [{:type => "required"}],
            :options      => [
              {
                :label => _('Version 3.2'),
                :value => 'v3_2',
              },
              {
                :label => _('Version 4.0'),
                :value => 'v4_0',
              },
              {
                :label => _('Version 5.0'),
                :value => 'v5_0',
              },
            ],
          },
          {
            :component => 'sub-form',
            :id        => 'endpoints-subform',
            :name      => 'endpoints-subform',
            :title     => _('Endpoints'),
            :fields    => [
              :component => 'tabs',
              :name      => 'tabs',
              :fields    => [
                {
                  :component => 'tab-item',
                  :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[type zone_id api_version],
                      :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",
                          :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 => 'tab-item',
                  :id        => 'events-tab',
                  :name      => 'events-tab',
                  :title     => _('Events'),
                  :fields    => [
                    {
                      :component    => 'protocol-selector',
                      :id           => 'event_stream_selection',
                      :name         => 'event_stream_selection',
                      :skipSubmit   => true,
                      :initialValue => 'none',
                      :label        => _('Type'),
                      :options      => [
                        {
                          :label => _('None'),
                          :value => 'none',
                        },
                        {
                          :label => _('AMQP'),
                          :value => _('amqp'),
                          :pivot => 'endpoints.amqp.hostname',
                        },
                      ],
                    },
                    {
                      :component              => 'validate-provider-credentials',
                      :id                     => 'endpoints.amqp.valid',
                      :name                   => 'endpoints.amqp.valid',
                      :skipSubmit             => true,
                      :validationDependencies => %w[type zone_id 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)"),
                          :helperText => _("Used to authenticate with Nuage AMQP Messaging Bus for event handling."),
                          :isRequired => true,
                          :validate   => [{:type => "required"}],
                        },
                        {
                          :component   => "text-field",
                          :id          => "endpoints.amqp_fallback1.hostname",
                          :name        => "endpoints.amqp_fallback1.hostname",
                          :placeholder => _("Hostname (or IPv4 or IPv6 address)"),
                          :label       => _("Fallback Hostname 1"),
                        },
                        {
                          :component   => "text-field",
                          :id          => "endpoints.amqp_fallback2.hostname",
                          :name        => "endpoints.amqp_fallback2.hostname",
                          :placeholder => _("Hostname (or IPv4 or IPv6 address)"),
                          :label       => _("Fallback Hostname 2"),
                        },
                        {
                          :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"}],
                        },
                      ],
                    },
                  ],
                },
              ],
            ],
          },
        ]
      }.freeze
    end

    # Verify Credentials
    #
    # args: {
    #   "api_version" => String,
    #   "endpoints" => {
    #     "default" => {
    #       "hostname" => String,
    #       "port" => Integer,
    #       "security_protocol" => String,
    #     },
    #     "amqp" => {
    #       "hostname" => String,
    #       "port" => String,
    #     },
    #     "amqp_fallback1" => {
    #       "hostname" => String,
    #     },
    #     "amqp_fallback2" => {
    #       "hostname" => String,
    #     },
    #   },
    #   "authentications" => {
    #     "default" =>
    #       "userid" => String,
    #       "password" => String,
    #     }
    #     "amqp" => {
    #       "userid" => String,
    #       "password" => String,
    #     }
    #   }
    # }
    def verify_credentials(args)
      endpoint_name = args.dig("endpoints").keys.first
      endpoint = args.dig("endpoints", endpoint_name)
      authentication = args.dig("authentications", endpoint_name)

      userid, password = authentication&.values_at('userid', 'password')
      password = ManageIQ::Password.try_decrypt(password)
      password ||= find(args["id"]).authentication_password(endpoint_name) if args["id"]

      hostname, port, security_protocol = endpoint&.values_at('hostname', 'port', 'security_protocol')

      if endpoint_name == 'default'
        endpoint_opts = {
          :protocol => security_protocol,
          :hostname => hostname,
          :api_port => port
        }

        !!raw_connect(userid, password, endpoint_opts)
      else
        amqp_hosts = [hostname] + (1..2).map { |i| args.dig("endpoints", "amqp_fallback#{i}", "hostname") }.compact

        ManageIQ::Providers::Nuage::NetworkManager::EventCatcher::Stream.test_amqp_connection(
          :urls                      => amqp_hosts.map { |host| [host, port].join(':') },
          :sasl_allow_insecure_mechs => true, # Only plain (insecure) mechanism currently supported
          :user                      => userid,
          :password                  => password
        )
      end
    end

    def raw_connect(username, password, endpoint_opts)
      protocol    = endpoint_opts[:protocol].strip if endpoint_opts[:protocol]
      hostname    = endpoint_opts[:hostname].strip
      api_port    = endpoint_opts[:api_port]
      # In case API port is represented as a string, ensure it has no whitespaces.
      api_port.strip! if api_port.kind_of?(String)
      api_version = endpoint_opts[:api_version].to_s.strip

      # TODO(miha-plesko): Update UI to never pass in invalid version string like '? string:v2 ?'
      raise MiqException::MiqInvalidCredentialsError, 'Invalid API Version' unless api_version_valid?(api_version)

      url = auth_url(protocol, hostname, api_port, api_version)
      $nuage_log.debug("Connecting to Nuage VSD with url #{url}")

      connection_rescue_block do
        ManageIQ::Providers::Nuage::NetworkManager::VsdClient.new(url, username, password)
      end
    end

    def base_url(protocol, server, port)
      scheme = %w(ssl ssl-with-validation).include?(protocol) ? "https" : "http"
      URI::Generic.build(:scheme => scheme, :host => server, :port => port).to_s
    end

    def auth_url(protocol, server, port, version)
      URI(base_url(protocol, server, port)).tap { |url| url.path = "/nuage/api/#{version}" }.to_s
    end

    def translate_exception(err)
      require 'excon'
      case err
      when Excon::Errors::Unauthorized
        MiqException::MiqInvalidCredentialsError.new("Login failed due to a bad username or password.")
      when Excon::Errors::Timeout
        MiqException::MiqUnreachableError.new("Login attempt timed out")
      when Excon::Errors::SocketError
        MiqException::MiqHostError.new("Socket error: #{err.message}")
      when MiqException::MiqInvalidCredentialsError, MiqException::MiqHostError
        err
      when Net::HTTPBadResponse
        MiqException::MiqEVMLoginError.new("Login failed due to a bad security protocol, hostname or port.")
      else
        MiqException::MiqEVMLoginError.new("Unexpected response returned from system: #{err.message}")
      end
    end

    def connection_rescue_block
      yield
    rescue => err
      miq_exception = translate_exception(err)

      $nuage_log.error("Error Class=#{err.class.name}, Message=#{err.message}")
      raise miq_exception
    end

    def api_version_valid?(value)
      !!(value =~ /^v\d+[_.]\d+/) # e.g. 'v5_0' or 'v5.0'
    end
  end

  def connect(options = {})
    raise "no credentials defined" if self.missing_credentials?(options[:auth_type])

    username = options[:user] || authentication_userid(options[:auth_type])
    password = options[:pass] || authentication_password(options[:auth_type])
    protocol = options[:protocol] || security_protocol
    server   = options[:ip] || address
    port     = options[:port] || self.port
    version  = options[:version] || api_version

    endpoint_opts = {:protocol => protocol, :hostname => server, :api_port => port, :api_version => version}
    self.class.raw_connect(username, password, endpoint_opts)
  end

  def verify_credentials(auth_type = nil, options = {})
    auth_type ||= 'default'

    raise MiqException::MiqInvalidCredentialsError, "Unsupported auth type: #{auth_type}" unless supported_auth_types.include?(auth_type.to_s)
    raise MiqException::MiqHostError, "No credentials defined" if missing_credentials?(auth_type)

    options[:auth_type] = auth_type
    case auth_type.to_s
    when 'default' then verify_api_credentials(options)
    when 'amqp'    then verify_amqp_credentials(options)
    end
  end

  def event_monitor_options
    @event_monitor_options ||= begin
      amqp_auth = authentications.detect { |a| a.authtype == 'amqp' }
      {
        :ems                       => self,
        :urls                      => amqp_urls,
        :sasl_allow_insecure_mechs => true, # Only plain (insecure) mechanism currently supported
        :user                      => amqp_auth.userid,
        :password                  => amqp_auth.password
      }
    end
  end

  private

  def amqp_urls
    amqp_endpoints = endpoints.select { |e| e.role == 'amqp' || e.role.start_with?('amqp_fallback') }
    amqp_endpoints.map do |e|
      "#{e.hostname}:#{e.port}"
    end
  end

  def verify_api_credentials(options = {})
    self.class.connection_rescue_block do
      with_provider_connection(options) {}
      true
    end
  end

  def verify_amqp_credentials(_options = {})
    ManageIQ::Providers::Nuage::NetworkManager::EventCatcher::Stream.test_amqp_connection(event_monitor_options)
  end

  def stop_event_monitor_queue_on_change
    if event_monitor_class && !new_record? && (authentications.detect(&:changed?) || endpoints.detect(&:changed?))
      $nuage_log.info("EMS: [#{name}], Credentials or endpoints have changed, stopping Event Monitor. It will be restarted by the WorkerMonitor.")
      stop_event_monitor_queue
    end
  end

  def stop_event_monitor_queue_on_credential_change
    stop_event_monitor_queue_on_change
  end
end