app/models/manageiq/providers/ovirt/infra_manager/api_integration.rb
require 'openssl'require 'resolv' module ManageIQ::Providers::Ovirt::InfraManager::ApiIntegration extend ActiveSupport::Concern included do process_api_features_support end SUPPORTED_FEATURES = [ :migrate, :quick_stats, :reconfigure_disks, :snapshots, :publish ].freeze def authentication_status_ok?(type = nil) return true if type == :ssh_keypair super end def apply_connection_options_defaults(options) { :id => id, :scheme => options[:scheme] || 'https', :server => options[:ip] || address, :port => options[:port] || port, :path => options[:path] || '/ovirt-engine/api', :username => options[:user] || authentication_userid(options[:auth_type]), :password => options[:pass] || authentication_password(options[:auth_type]), :service => options[:service] || "Service", :verify_ssl => default_endpoint.verify_ssl, :ca_certs => default_endpoint.certificate_authority } end def connect(options = {}) raise "no credentials defined" if missing_credentials?(options[:auth_type]) # Prepare the options to call the method that creates the actual connection: connect_options = apply_connection_options_defaults(options) # Starting with version 4 of oVirt authentication doesn't work when using directly the IP address, it requires # the fully qualified host name, so if we received an IP address we try to convert it into the corresponding # host name: if self.class.resolve_ip_addresses? resolved = self.class.resolve_ip_address(connect_options[:server]) if resolved != connect_options[:server] _log.info("IP address '#{connect_options[:server]}' has been resolved to host name '#{resolved}'.") default_endpoint.hostname = resolved connect_options[:server] = resolved end end connection = self.class.raw_connect_v4(connect_options) # Copy the API path to the endpoints table: default_endpoint.path = connect_options[:path] connection end def supported_auth_types %w[default metrics ssh_keypair] end def ovirt_services @ovirt_services ||= self.class::OvirtServices::V4.new(:ems => self) end def verify_credentials_for_rhevm(options = {}) with_provider_connection(options) { |connection| connection.test(true) }Avoid rescuing the `Exception` class. Perhaps you meant to rescue `StandardError`? rescue Exception => e self.class.handle_credentials_verification_error(e) end def rhevm_metrics_connect_options(options = {}) metrics_hostname = connection_configuration_by_role('metrics') .try(:endpoint) .try(:hostname) server = options[:hostname] || metrics_hostname || hostname username = options[:user] || authentication_userid(:metrics) password = options[:pass] || authentication_password(:metrics) database = options[:database] || history_database_name { :host => server, :database => database, :username => username, :password => password } end def verify_credentials_for_rhevm_metrics(options = {}) require 'ovirt_metrics' OvirtMetrics.connect(rhevm_metrics_connect_options(options)) OvirtMetrics.connected? rescue StandardError => error raise self.class.adapt_metrics_error(error) ensure begin OvirtMetrics.disconnect rescue nil end end def authentications_to_validate at = [:default] at << :metrics if has_authentication_type?(:metrics) at end def verify_credentials(auth_type = nil, options = {}) options[:skip_supported_api_validation] = true auth_type ||= 'default' case auth_type.to_s when 'default' then verify_credentials_for_rhevm(options) when 'metrics' then verify_credentials_for_rhevm_metrics(options) else; raise "Invalid Authentication Type: #{auth_type.inspect}" end end def history_database_name connection_configurations.try(:metrics).try(:endpoint).try(:path) || self.class.default_history_database_name end # Adding disks is supported only by API version 4.0 def with_disk_attachments_service(vm) with_vm_service(vm) do |service| disk_service = service.disk_attachments_service yield disk_service end end def with_vm_service(vm) service = connect.system_service.vms_service.vm_service(vm.uid_ems) yield service end def use_ovirt_sdk? true end class_methods do def process_api_features_support SUPPORTED_FEATURES.each do |f| supports f end end def rethrow_as_a_miq_error(ovirt_sdk_4_error) case ovirt_sdk_4_error.message when /The username or password is incorrect/ raise MiqException::MiqInvalidCredentialsError, "Incorrect user name or password." when /Couldn't connect to server/, /Couldn't resolve host name/ raise MiqException::MiqUnreachableError, $ERROR_INFO else _log.error("Error while verifying credentials #{$ERROR_INFO}") raise MiqException::MiqEVMLoginError, $ERROR_INFO end end def handle_credentials_verification_error(err) require 'ovirtsdk4' case err when SocketError, Errno::EHOSTUNREACH, Errno::ENETUNREACH _log.warn($ERROR_INFO) raise MiqException::MiqUnreachableError, $ERROR_INFO when MiqException::MiqUnreachableError raise err when OvirtSDK4::Error rethrow_as_a_miq_error(err) else _log.error("Error while verifying credentials #{$ERROR_INFO}") raise MiqException::MiqEVMLoginError, $ERROR_INFO end end # Verify Credentials # # args: { # "authentications" => { # "default" => { # "username" => String, # "password" => String, # }, # "metrics" => { # "metrics_username" => String, # "metrics_password" => String, # } # }, # "endpoints" => { # "default" => { # "hostname" => String, # "port" => Integer, # "verify_ssl" => [VERIFY_NONE, VERIFY_PEER], # "ca_certs" => String # }, # "metrics" => { # "metrics_username" => String, # "metrics_password" => String, # "metrics_port" => Integer, # "metrics_database" => String # } # } # } def verify_credentials(args) default_endpoint = args.dig("endpoints", "default") metrics_endpoint = args.dig("endpoints", "metrics") default_authentication = args.dig("authentications", "default") metrics_authentication = args.dig("authentications", "metrics") username, password = default_authentication&.values_at("userid", "password") server, port, verify_ssl, certificate_authority = default_endpoint&.values_at( "hostname", "port", "verify_ssl", "certificate_authority" ) metrics_username, metrics_password = metrics_authentication&.values_at("userid", "password") metrics_server, metrics_port, metrics_database = metrics_endpoint&.values_at( "hostname", "port", "path" ) !!raw_connect( :username => username, :password => ManageIQ::Password.try_decrypt(password), :server => server, :port => port, :verify_ssl => verify_ssl, :ca_certs => certificate_authority, :metrics_username => metrics_username, :metrics_password => ManageIQ::Password.try_decrypt(metrics_password), :metrics_server => metrics_server, :metrics_port => metrics_port, :metrics_database => metrics_database ) end # # This method is called only when the UI button to verify the connection details is clicked. It isn't used create # the connections actually used by the provider. # # Note that the protocol (HTTP or HTTPS) and the version of the API are *not* options of this method, if they are # provided they will be silently ignored. # # @param opts [Hash] A hash containing the connection details and the credentials. # @option opts [String] :username The name of the API user. # @option opts [String] :password The password of the API user. # @option opts [String] :server The host name or IP address of the API server. # @option opts [Integer] :port ('443') The port number of the API server. # @option opts [Integer] :verify_ssl ('1') A numeric flag indicating if the TLS certificates of the API server # should be checked. Value `0` indicates that the should not be checked, value `1` indicates that they should # be checked. # @option opts [String] :ca_certs The custom trusted CA certificates used to check the TLS certificates of the # API server, in PEM format. A blank or nil value means that no custom CA certificates should be used. # @option opts [String] :metrics_username The name of the metrics database user. # @option opts [String] :metrics_password The password of the metrics database user. # @options opts [String] :metrics_server The host name or IP address of the metrics database server. # @options opts [Integer] :metrics_port ('5432') The port number of the metrics database server. # @options opts [String] :metrics_database The name of the metrics database. # @return [Boolean] Returns `true` if the connection details and credentials are valid, or `false` otherwise. # def raw_connect(opts = {}) check_connect_api(opts) check_connect_metrics(opts)Avoid rescuing the `Exception` class. Perhaps you meant to rescue `StandardError`? rescue Exception => e handle_credentials_verification_error(e) end # # Checks the API connection details. # # @api private # def check_connect_api(opts = {}) # Get options and assign default values: username = opts[:username] password = opts[:password] server = opts[:server] port = opts[:port] || 443 verify_ssl = opts[:verify_ssl] || 1 ca_certs = opts[:ca_certs] return true if server.blank? # Decrypt the password: password = ManageIQ::Password.try_decrypt(password) # Starting with version 4 of oVirt authentication doesn't work when using directly the IP address, it requires # the fully qualified host name, so if we received an IP address we try to convert it into the corresponding # host name: resolved = server if resolve_ip_addresses? resolved = resolve_ip_address(server) if resolved != server _log.info("IP address '#{server}' has been resolved to host name '#{resolved}'.") end end # Build the options that will be used to call the methods that create the connection with specific versions # of the API: opts = { :username => username, :password => password, :server => resolved, :port => port, :verify_ssl => verify_ssl, :ca_certs => ca_certs, :service => 'Inventory' # This is needed only for version 3 of the API. } # Try to verify the details using version 4 of the API. If this succeeds or fails with an authentication # exception, then we don't need to do anything else. Note that the connection should not be closed, because # that is handled by the `ConnectionManager` class. begin connection = raw_connect_v4(opts) connection.test(:raise_exception => true) true end end # # Checks the metrics connection details. # # @api private # def check_connect_metrics(opts = {}) # Get the options and assign defaults: username = opts[:metrics_username] password = opts[:metrics_password] server = opts[:metrics_server] port = opts[:metrics_port] || 5432 database = opts[:metrics_database] || default_history_database_name # Metrics are optional, so we should only check the details if the server has been specified: return true if server.blank? # Decrypt the password: password = ManageIQ::Password.try_decrypt(password) # Build the options that will be used to call the methods that checks that the metrics connection can # be created: opts = { :username => username, :password => password, :host => server, :port => port, :database => database } begin require 'ovirt_metrics' OvirtMetrics.connect(opts) OvirtMetrics.connected? rescue StandardError => error raise adapt_metrics_error(error) ensure begin OvirtMetrics.disconnect rescue nil end end end # Connect to the engine using version 4 of the API and the `ovirt-engine-sdk` gem. def raw_connect_v4(options = {}) # Get the timeouts from the configuration: read_timeout, open_timeout = ems_timeouts(:ems_ovirt, options[:service]) # The constructor of the SDK expects a list of certificates, but that list can't be empty, or contain only 'nil' # values, so we need to check the value passed and make a list only if it won't be empty. If it will be empty then # we should just pass 'nil'. ca_certs = options[:ca_certs] ca_certs = nil if ca_certs.blank? ca_certs = [ca_certs] if ca_certs url = URI::Generic.build( :scheme => 'https', :host => options[:server], :port => options[:port], :path => '/ovirt-engine/api' ) module_parent::ConnectionManager.instance.get( options[:id], :url => url.to_s, :username => options[:username], :password => options[:password], :timeout => read_timeout, :connect_timeout => open_timeout, :insecure => options[:verify_ssl] == OpenSSL::SSL::VERIFY_NONE, :ca_certs => ca_certs, :log => $rhevm_log, :connections => options[:connections] || ems_refresh_settings.connections, :pipeline => options[:pipeline] || ems_refresh_settings.pipeline ) end def default_history_database_name require 'ovirt_metrics' OvirtMetrics::DEFAULT_HISTORY_DATABASE_NAME end # Calculates an "ems_ref" from the "href" attribute provided by the oVirt REST API, removing the # "/ovirt-engine/" prefix, as for historic reasons the "ems_ref" stored in the database does not # contain it, it only contains the "/api" prefix which was used by older versions of the engine. def make_ems_ref(href) href&.sub(%r{^/ovirt-engine/}, '/') end def extract_ems_ref_id(href) href&.split("/")&.last end # # Checks if IP address to host name resolving is enabled. # # @return [Boolean] `true` if host name resolving is enabled in the configuration, `false` otherwise. # # @api private # def resolve_ip_addresses? ems_settings.resolve_ip_addresses end # # Tries to convert the given IP address into a host name, doing a reverse DNS lookup if needed. If it # isn't possible to find the host name the original IP address will be returned, and a warning will be # written to the log. # # @param address [String] The IP address. # @return [String] The host name. # # @api private # def resolve_ip_address(address) # Don't try to resolve unless the string is really an IP address and not a host name: return address unless address =~ Resolv::IPv4::Regex || address =~ Resolv::IPv6::Regex # Try to do a reverse resolve of the address to find the host name, using the default resolver, which # means first using the local hosts file and then DNS: begin Resolv.getname(address) rescue Resolv::ResolvError _log.warn( "Can't find fully qualified host name for IP address '#{address}', will use the IP address " \ "directly." ) address end end # # Adapts the given error raised by the metrics connection into the exceptions that the ManageIQ core expects. # # @param error [Exception] The exception generated by the attempt to connect to the metrics database. # @return [Exception] The exception that the ManageIQ expects. # # @api private # rhevm_metrics_connect_options def adapt_metrics_error(error) case error when PG::Error message = error.message message = error.message[6..-1] if message.starts_with?('FATAL:') message = message.strip _log.warn("#{error.class.name}: #{message}") MiqException::MiqEVMLoginError.new(message) else MiqException::MiqEVMLoginError.new(error.to_s) end end endend