ManageIQ/manageiq-appliance_console

View on GitHub
lib/manageiq/appliance_console/cli.rb

Summary

Maintainability
F
4 days
Test Coverage
A
95%
require 'optimist'
require 'pathname'

# support for appliance_console methods
unless defined?(say)
  def say(arg)
    puts(arg)
  end
end

module ManageIQ
module ApplianceConsole
  class CliError < StandardError; end

  class Cli
    attr_accessor :options

    # machine host
    def host
      options[:host] || LinuxAdmin::Hosts.new.hostname
    end

    # database hostname
    def hostname
      options[:internal] ? "localhost" : options[:hostname]
    end

    def local?(name = hostname)
      name.presence.in?(["localhost", "127.0.0.1", nil])
    end

    def set_host?
      options[:host]
    end

    def key?
      options[:key] || options[:fetch_key] || (local_database? && !key_configuration.key_exist?)
    end

    def database?
      (options[:standalone] || hostname) && !database_admin?
    end

    def database_admin?
      db_dump? || db_backup? || db_restore?
    end

    def db_dump?
      options[:dump]
    end

    def db_backup?
      options[:backup]
    end

    def db_restore?
      options[:restore]
    end

    def local_database?
      database? && (local?(hostname) || options[:standalone])
    end

    def certs?
      options[:http_cert]
    end

    def uninstall_ipa?
      options[:uninstall_ipa]
    end

    def install_ipa?
      options[:ipaserver]
    end

    def tmp_disk?
      options[:tmpdisk]
    end

    def log_disk?
      options[:logdisk]
    end

    def extauth_opts?
      options[:extauth_opts]
    end

    def saml_config?
      options[:saml_config]
    end

    def saml_unconfig?
      options[:saml_unconfig]
    end

    def message_server_config?
      options[:message_server_config]
    end

    def message_server_unconfig?
      options[:message_server_unconfig]
    end

    def message_client_config?
      options[:message_client_config]
    end

    def message_client_unconfig?
      options[:message_client_unconfig]
    end

    def oidc_config?
      options[:oidc_config]
    end

    def oidc_unconfig?
      options[:oidc_unconfig]
    end

    def set_server_state?
      options[:server]
    end

    def set_replication?
      options[:cluster_node_number] && options[:password] && replication_params?
    end

    def replication_params?
      options[:replication] == "primary" || (options[:replication] == "standby" && options[:primary_host])
    end

    def openscap?
      options[:openscap]
    end

    def initialize(options = {})
      self.options = options
    end

    def disk_from_string(path)
      return if path.blank?
      path == "auto" ? disk : disk_by_path(path)
    end

    def disk
      LinuxAdmin::Disk.local.detect { |d| d.partitions.empty? }
    end

    def disk_by_path(path)
      LinuxAdmin::Disk.local.detect { |d| d.path == path }
    end

    def parse(args)
      args.shift if args.first == "--" # Handle when called through script/runner
      self.options = Optimist.options(args) do
        banner "Usage: appliance_console_cli [options]"

        opt :host,                        "/etc/hosts name",                                                :type => :string,  :short => 'H'
        opt :region,                      "Region Number",                                                  :type => :integer, :short => "r"
        opt :internal,                    "Internal Database",                                                                 :short => 'i'
        opt :hostname,                    "Database Hostname",                                              :type => :string,  :short => 'h'
        opt :port,                        "Database Port",                                                  :type => :integer,                :default => 5432
        opt :username,                    "Database Username",                                              :type => :string,  :short => 'U', :default => "root"
        opt :password,                    "Database Password",                                              :type => :string,  :short => "p"
        opt :dbname,                      "Database Name",                                                  :type => :string,  :short => "d", :default => "vmdb_production"
        opt :local_file,                  "Source/Destination file for DB dump/backup/restore",             :type => :string,  :shoft => "l"
        opt :dump,                        "Perform a pg-dump"
        opt :backup,                      "Perform a pg-basebackup"
        opt :restore,                     "Restore a database dump/backup"
        opt :standalone,                  "Run this server as a standalone database server",                :type => :bool,    :short => 'S'
        opt :key,                         "Create encryption key",                                          :type => :boolean, :short => "k"
        opt :fetch_key,                   "SSH host with encryption key",                                   :type => :string,  :short => "K"
        opt :force_key,                   "Forcefully create encryption key",                               :type => :boolean, :short => "f"
        opt :sshlogin,                    "SSH login",                                                      :type => :string,  :default => "root"
        opt :sshpassword,                 "SSH password",                                                   :type => :string
        opt :replication,                 "Configure database replication as primary or standby",           :type => :string,  :short => :none
        opt :primary_host,                "Primary database host IP address",                               :type => :string,  :short => :none
        opt :standby_host,                "Standby database host IP address",                               :type => :string,  :short => :none
        opt :auto_failover,               "Configure Replication Manager (repmgrd) for automatic failover", :type => :bool,    :short => :none
        opt :cluster_node_number,         "Database unique cluster node number",                            :type => :integer, :short => :none
        opt :verbose,                     "Verbose",                                                        :type => :boolean, :short => "v"
        opt :dbdisk,                      "Database Disk Path",                                             :type => :string
        opt :logdisk,                     "Log Disk Path",                                                  :type => :string
        opt :tmpdisk,                     "Temp storage Disk Path",                                         :type => :string
        opt :uninstall_ipa,               "Uninstall IPA Client",                                           :type => :boolean, :default => false
        opt :ipaserver,                   "IPA Server FQDN",                                                :type => :string
        opt :ipaprincipal,                "IPA Server principal",                                           :type => :string,  :default => "admin"
        opt :ipapassword,                 "IPA Server password",                                            :type => :string
        opt :ipadomain,                   "IPA Server domain (optional)",                                   :type => :string
        opt :iparealm,                    "IPA Server realm (optional)",                                    :type => :string
        opt :ca,                          "CA name used for certmonger",                                    :type => :string,  :default => "ipa"
        opt :http_cert,                   "install certs for http server",                                  :type => :boolean
        opt :extauth_opts,                "External Authentication Options",                                :type => :string
        opt :saml_config,                 "Configure Appliance for SAML Authentication",                    :type => :boolean, :default => false
        opt :saml_client_host,            "Optional Appliance host used for SAML registration",             :type => :string
        opt :saml_idp_metadata,           "The file path or URL of the SAML IDP Metadata",                  :type => :string
        opt :saml_enable_sso,             "Optionally enable SSO with SAML Authentication",                 :type => :boolean, :default => false
        opt :saml_unconfig,               "Unconfigure Appliance SAML Authentication",                      :type => :boolean, :default => false
        opt :oidc_config,                 "Configure Appliance for OpenID-Connect Authentication",          :type => :boolean, :default => false
        opt :oidc_url,                    "The OpenID-Connect Provider URL",                                :type => :string
        opt :oidc_client_host,            "Optional Appliance host used for OpenID-Connect Authentication", :type => :string
        opt :oidc_client_id,              "The OpenID-Connect Provider Client ID",                          :type => :string
        opt :oidc_client_secret,          "The OpenID-Connect Provider Client Secret",                      :type => :string
        opt :oidc_insecure,               "OpenID-Connect Insecure No SSL Verify (development)",            :type => :boolean, :default => false
        opt :oidc_introspection_endpoint, "The OpenID-Connect Provider Introspect Endpoint",                :type => :string
        opt :oidc_enable_sso,             "Optionally enable SSO with OpenID-Connect Authentication",       :type => :boolean, :default => false
        opt :oidc_unconfig,               "Unconfigure Appliance OpenID-Connect Authentication",            :type => :boolean, :default => false
        opt :server,                      "{start|stop|restart} actions on evmserverd Server",              :type => :string
        opt :openscap,                    "Setup OpenScap",                                                 :type => :boolean, :default => false
        opt :message_server_config,       "Subcommand to   Configure Appliance as a Kafka Message Server",  :type => :boolean, :default => false
        opt :message_server_unconfig,     "Subcommand to Unconfigure Appliance as a Kafka Message Server",  :type => :boolean, :default => false
        opt :message_client_config,       "Subcommand to   Configure Appliance as a Kafka Message Client",  :type => :boolean, :default => false
        opt :message_client_unconfig,     "Subcommand to Unconfigure Appliance as a Kafka Message Client",  :type => :boolean, :default => false
        opt :message_keystore_username,   "Message Server Keystore Username",                               :type => :string
        opt :message_keystore_password,   "Message Server Keystore Password",                               :type => :string
        opt :message_server_username,     "Message Server Username",                                        :type => :string
        opt :message_server_password,     "Message Server password",                                        :type => :string
        opt :message_server_port,         "Message Server Port",                                            :type => :integer
        opt :message_server_use_ipaddr,   "Message Server Use Address",                                     :type => :boolean, :default => false
        opt :message_server_host,         "Message Server Hostname or IP Address",                          :type => :string
        opt :message_truststore_path_src, "Message Server Truststore Path",                                 :type => :string
        opt :message_ca_cert_path_src,    "Message Server CA Cert Path",                                    :type => :string
        opt :message_persistent_disk,     "Message Persistent Disk Path",                                   :type => :string
      end
      Optimist.die :region, "needed when setting up a local database" if region_number_required? && options[:region].nil?
      Optimist.die "Supply only one of --message-server-host or --message-server-use-ipaddr=true" if both_host_and_use_ip_addr_specified?
      Optimist.die "Supply only one of --message-server-config, --message-server-unconfig, --message-client-config or --message-client-unconfig" if multiple_message_subcommands?
      self
    end

    def both_host_and_use_ip_addr_specified?
      !options[:message_server_host].nil? && options[:message_server_use_ipaddr] == true
    end

    def multiple_message_subcommands?
      a = [options[:message_server_config], options[:message_server_unconfig], options[:message_client_config], options[:message_client_unconfig]]
      a.each_with_object(Hash.new(0)) { |o, h| h[o] += 1 }[true] > 1
    end

    def region_number_required?
      !options[:standalone] && local_database? && !database_admin?
    end

    def run
      Optimist.educate unless set_host? || key? || database? || db_dump? || db_backup? ||
                              db_restore? || tmp_disk? || log_disk? ||
                              uninstall_ipa? || install_ipa? || certs? || extauth_opts? ||
                              set_server_state? || set_replication? || openscap? ||
                              saml_config? || saml_unconfig? ||
                              oidc_config? || oidc_unconfig? ||
                              message_server_config? || message_server_unconfig? ||
                              message_client_config? || message_client_unconfig?

      if set_host?
        system_hosts = LinuxAdmin::Hosts.new
        system_hosts.hostname = options[:host]
        system_hosts.set_loopback_hostname(options[:host])
        system_hosts.save
      end
      create_key if key?
      set_db if database?
      set_replication if set_replication?
      db_dump if db_dump?
      db_backup if db_backup?
      db_restore if db_restore?
      config_tmp_disk if tmp_disk?
      config_log_disk if log_disk?
      uninstall_ipa if uninstall_ipa?
      install_ipa if install_ipa?
      install_certs if certs?
      extauth_opts if extauth_opts?
      saml_config if saml_config?
      saml_unconfig if saml_unconfig?
      oidc_config if oidc_config?
      oidc_unconfig if oidc_unconfig?
      openscap if openscap?
      message_server_config if message_server_config?
      message_server_unconfig if message_server_unconfig?
      message_client_config if message_client_config?
      message_client_unconfig if message_client_unconfig?
      # set_server_state must be after set_db and message_*_config so that a user
      # can configure database, messaging, and start the server in one command
      set_server_state if set_server_state?
    rescue CliError => e
      say(e.message)
      say("")
      exit(1)
    rescue AwesomeSpawn::CommandResultError => e
      say e.result.output
      say e.result.error
      say ""
      raise
    end

    def set_db
      raise "No encryption key (v2_key) present" unless key_configuration.key_exist?
      raise "A password is required to configure a database" unless password?
      if local?
        set_internal_db
      else
        set_external_db
      end
    end

    def password?
      options[:password] && !options[:password].strip.empty?
    end

    def set_internal_db
      say "configuring internal database"
      config = ManageIQ::ApplianceConsole::InternalDatabaseConfiguration.new({
        :database          => options[:dbname],
        :region            => options[:region],
        :username          => options[:username],
        :password          => options[:password],
        :interactive       => false,
        :disk              => disk_from_string(options[:dbdisk]),
        :run_as_evm_server => !options[:standalone]
      }.delete_if { |_n, v| v.nil? })
      config.check_disk_is_mount_point

      # create partition, pv, vg, lv, ext4, update fstab, mount disk
      # initdb, relabel log directory for selinux, update configs,
      # start pg, create user, create db update the rails configuration,
      # verify, set up the database with region. activate does it all!
      raise CliError, "Failed to configure internal database" unless config.activate
    rescue RuntimeError => e
      raise CliError, "Failed to configure internal database #{e.message}"
    end

    def set_external_db
      say "configuring external database"
      config = ManageIQ::ApplianceConsole::ExternalDatabaseConfiguration.new({
        :host        => options[:hostname],
        :port        => options[:port],
        :database    => options[:dbname],
        :region      => options[:region],
        :username    => options[:username],
        :password    => options[:password],
        :interactive => false,
      }.delete_if { |_n, v| v.nil? })

      # call create_or_join_region (depends on region value)
      raise CliError, "Failed to configure external database" unless config.activate
    end

    def set_replication
      if options[:replication] == "primary"
        db_replication = ManageIQ::ApplianceConsole::DatabaseReplicationPrimary.new
        say("Configuring Server as Primary")
      else
        db_replication = ManageIQ::ApplianceConsole::DatabaseReplicationStandby.new
        say("Configuring Server as Standby")
        db_replication.disk = disk_from_string(options[:dbdisk])
        db_replication.primary_host = options[:primary_host]
        db_replication.standby_host = options[:standby_host] if options[:standby_host]
        db_replication.run_repmgrd_configuration = options[:auto_failover] ? true : false
      end
      db_replication.database_name = options[:dbname] if options[:dbname]
      db_replication.database_user = options[:username] if options[:username]
      db_replication.node_number = options[:cluster_node_number]
      db_replication.database_password = options[:password]
      db_replication.activate
    end

    def db_dump
      PostgresAdmin.backup_pg_dump(extract_db_opts(options))
    end

    def db_backup
      PostgresAdmin.backup(extract_db_opts(options))
    end

    def db_restore
      PostgresAdmin.restore(extract_db_opts(options))
    end

    DB_OPT_KEYS = %i[dbname username password hostname port local_file].freeze
    def extract_db_opts(options)
      require 'manageiq/appliance_console/postgres_admin'

      db_opts = {}

      DB_OPT_KEYS.each { |k| db_opts[k] = options[k] if options[k] }

      if db_dump? && options[:exclude_table_data]
        db_opts[:exclude_table_data] = options[:exclude_table_data]
      end

      db_opts
    end

    def key_configuration
      @key_configuration ||= KeyConfiguration.new(
        :action   => options[:fetch_key] ? :fetch : :create,
        :force    => options[:fetch_key] ? true : options[:force_key],
        :host     => options[:fetch_key],
        :login    => options[:sshlogin],
        :password => options[:sshpassword],
      )
    end

    def create_key
      say "#{key_configuration.action} encryption key"
      unless key_configuration.activate
        say("Could not create encryption key (v2_key)")
        exit(1)
      end
    end

    def install_certs
      say "creating ssl certificates"
      config = CertificateAuthority.new(
        :hostname => host,
        :realm    => options[:iparealm],
        :ca_name  => options[:ca],
        :http     => options[:http_cert],
        :verbose  => options[:verbose],
      )

      config.activate
      say "\ncertificate result: #{config.status_string}"
      unless config.complete?
        say "After the certificates are retrieved, rerun to update service configuration files"
      end
    end

    def install_ipa
      raise "please uninstall ipa before reinstalling" if ExternalHttpdAuthentication.ipa_client_configured?
      config = ExternalHttpdAuthentication.new(
        host,
        :ipaserver => options[:ipaserver],
        :domain    => options[:ipadomain],
        :realm     => options[:iparealm],
        :principal => options[:ipaprincipal],
        :password  => options[:ipapassword],
      )

      config.post_activation if config.activate
    end

    def uninstall_ipa
      say "Uninstalling IPA-client"
      config = ExternalHttpdAuthentication.new
      config.deactivate if config.ipa_client_configured?
    end

    def openscap
      say("Configuring Openscap")
      ManageIQ::ApplianceConsole::Scap.new.lockdown
    end

    def config_tmp_disk
      if (tmp_disk = disk_from_string(options[:tmpdisk]))
        say "creating temp disk"
        config = ManageIQ::ApplianceConsole::TempStorageConfiguration.new(:disk => tmp_disk)
        config.activate
      else
        report_disk_error(options[:tmpdisk])
      end
    end

    def config_log_disk
      if (log_disk = disk_from_string(options[:logdisk]))
        say "creating log disk"
        config = ManageIQ::ApplianceConsole::LogfileConfiguration.new(:disk => log_disk)
        config.activate
      else
        report_disk_error(options[:logdisk])
      end
    end

    def report_disk_error(missing_disk)
      choose_disk = disk.try(:path)
      if choose_disk
        say "could not find disk #{missing_disk}"
        say "if you pass auto, it will choose: #{choose_disk}"
      else
        say "no disks with a free partition"
      end
    end

    def extauth_opts
      extauthopts = ExternalAuthOptions.new
      extauthopts_hash = extauthopts.parse(options[:extauth_opts])
      raise "Must specify at least one external authentication option to set" unless extauthopts_hash.present?
      extauthopts.update_configuration(extauthopts_hash)
    end

    def saml_config
      SamlAuthentication.new(options).configure(options[:saml_client_host] || host)
    end

    def saml_unconfig
      SamlAuthentication.new(options).unconfigure
    end

    def oidc_config
      OIDCAuthentication.new(options).configure(options[:oidc_client_host] || host)
    end

    def oidc_unconfig
      OIDCAuthentication.new(options).unconfigure
    end

    def message_server_config
      raise "Message Server Configuration is not available" unless MessageServerConfiguration.available?

      MessageServerConfiguration.new(options).configure
    end

    def message_server_unconfig
      MessageServerConfiguration.new(options).unconfigure
    end

    def message_client_config
      raise "Message Client Configuration is not available" unless MessageClientConfiguration.available?

      MessageClientConfiguration.new(options).configure
    end

    def message_client_unconfig
      MessageClientConfiguration.new(options).unconfigure
    end

    def set_server_state
      case options[:server]
      when "start"
        EvmServer.start unless EvmServer.running?
      when "stop"
        EvmServer.stop if EvmServer.running?
      when "restart"
        EvmServer.restart
      else
        raise "Invalid server action"
      end
    end

    def self.parse(args)
      new.parse(args).run
    end
  end
end
end