manageiq-appliance_console 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +47 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +4 -0
  5. data/.rspec_ci +4 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +5 -0
  8. data/.rubocop_local.yml +2 -0
  9. data/.travis.yml +19 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +202 -0
  12. data/README.md +45 -0
  13. data/Rakefile +6 -0
  14. data/bin/appliance_console +661 -0
  15. data/bin/appliance_console_cli +7 -0
  16. data/lib/manageiq-appliance_console.rb +51 -0
  17. data/lib/manageiq/appliance_console/certificate.rb +146 -0
  18. data/lib/manageiq/appliance_console/certificate_authority.rb +140 -0
  19. data/lib/manageiq/appliance_console/cli.rb +363 -0
  20. data/lib/manageiq/appliance_console/database_configuration.rb +286 -0
  21. data/lib/manageiq/appliance_console/database_maintenance.rb +35 -0
  22. data/lib/manageiq/appliance_console/database_maintenance_hourly.rb +58 -0
  23. data/lib/manageiq/appliance_console/database_maintenance_periodic.rb +84 -0
  24. data/lib/manageiq/appliance_console/database_replication.rb +146 -0
  25. data/lib/manageiq/appliance_console/database_replication_primary.rb +59 -0
  26. data/lib/manageiq/appliance_console/database_replication_standby.rb +166 -0
  27. data/lib/manageiq/appliance_console/date_time_configuration.rb +117 -0
  28. data/lib/manageiq/appliance_console/errors.rb +5 -0
  29. data/lib/manageiq/appliance_console/external_auth_options.rb +153 -0
  30. data/lib/manageiq/appliance_console/external_database_configuration.rb +34 -0
  31. data/lib/manageiq/appliance_console/external_httpd_authentication.rb +157 -0
  32. data/lib/manageiq/appliance_console/external_httpd_authentication/external_httpd_configuration.rb +249 -0
  33. data/lib/manageiq/appliance_console/internal_database_configuration.rb +187 -0
  34. data/lib/manageiq/appliance_console/key_configuration.rb +118 -0
  35. data/lib/manageiq/appliance_console/logfile_configuration.rb +117 -0
  36. data/lib/manageiq/appliance_console/logger.rb +23 -0
  37. data/lib/manageiq/appliance_console/logging.rb +102 -0
  38. data/lib/manageiq/appliance_console/logical_volume_management.rb +94 -0
  39. data/lib/manageiq/appliance_console/principal.rb +46 -0
  40. data/lib/manageiq/appliance_console/prompts.rb +211 -0
  41. data/lib/manageiq/appliance_console/scap.rb +53 -0
  42. data/lib/manageiq/appliance_console/temp_storage_configuration.rb +79 -0
  43. data/lib/manageiq/appliance_console/timezone_configuration.rb +58 -0
  44. data/lib/manageiq/appliance_console/utilities.rb +67 -0
  45. data/lib/manageiq/appliance_console/version.rb +5 -0
  46. data/locales/appliance/en.yml +42 -0
  47. data/locales/container/en.yml +30 -0
  48. data/manageiq-appliance_console.gemspec +40 -0
  49. data/zanata.xml +7 -0
  50. metadata +317 -0
@@ -0,0 +1,187 @@
1
+ require "pathname"
2
+ require "util/postgres_admin"
3
+ require "pg"
4
+ require "linux_admin"
5
+
6
+ module ManageIQ
7
+ module ApplianceConsole
8
+ class InternalDatabaseConfiguration < DatabaseConfiguration
9
+ attr_accessor :disk, :ssl, :run_as_evm_server
10
+
11
+ DEDICATED_DB_SHARED_BUFFERS = "'1GB'".freeze
12
+ SHARED_DB_SHARED_BUFFERS = "'128MB'".freeze
13
+
14
+ def self.postgres_dir
15
+ PostgresAdmin.data_directory.relative_path_from(Pathname.new("/"))
16
+ end
17
+
18
+ def self.postgresql_template
19
+ PostgresAdmin.template_directory.join(postgres_dir)
20
+ end
21
+
22
+ def initialize(hash = {})
23
+ set_defaults
24
+ super
25
+ end
26
+
27
+ def set_defaults
28
+ self.host = 'localhost'
29
+ self.username = "root"
30
+ self.database = "vmdb_production"
31
+ self.run_as_evm_server = true
32
+ end
33
+
34
+ def activate
35
+ if PostgresAdmin.initialized?
36
+ say(<<-EOF.gsub!(/^\s+/, ""))
37
+ An internal database already exists.
38
+ Choose "Reset Configured Database" to reset the existing installation
39
+ EOF
40
+ return false
41
+ end
42
+ initialize_postgresql_disk if disk
43
+ initialize_postgresql
44
+ return super if run_as_evm_server
45
+ true
46
+ end
47
+
48
+ def ask_questions
49
+ choose_disk
50
+ check_disk_is_mount_point
51
+ self.run_as_evm_server = !ask_yn?(<<-EOS.gsub!(/^ +/m, ""), "N")
52
+
53
+ Should this appliance run as a standalone database server?
54
+
55
+ NOTE:
56
+ * The #{I18n.t("product.name")} application will not be running.
57
+ * This is required when using highly available database deployments.
58
+ * CAUTION: This is not reversible.
59
+
60
+ EOS
61
+ # TODO: Assume we want to create a region for a new internal database disk
62
+ # until we allow for the internal selection against an already initialized disk.
63
+ create_new_region_questions(false) if run_as_evm_server
64
+ ask_for_database_credentials
65
+ end
66
+
67
+ def choose_disk
68
+ @disk = ask_for_disk("database disk")
69
+ end
70
+
71
+ def check_disk_is_mount_point
72
+ error_message = "The disk for database must be a mount point"
73
+ raise error_message unless disk || pg_mount_point?
74
+ end
75
+
76
+ def initialize_postgresql_disk
77
+ log_and_feedback(__method__) do
78
+ LogicalVolumeManagement.new(:disk => disk,
79
+ :mount_point => mount_point,
80
+ :name => "pg",
81
+ :volume_group_name => PostgresAdmin.volume_group_name,
82
+ :filesystem_type => PostgresAdmin.database_disk_filesystem,
83
+ :logical_volume_path => PostgresAdmin.logical_volume_path).setup
84
+ end
85
+ end
86
+
87
+ def initialize_postgresql
88
+ log_and_feedback(__method__) do
89
+ PostgresAdmin.prep_data_directory
90
+ run_initdb
91
+ relabel_postgresql_dir
92
+ configure_postgres
93
+ start_postgres
94
+ create_postgres_root_user
95
+ create_postgres_database
96
+ apply_initial_configuration
97
+ end
98
+ end
99
+
100
+ def configure_postgres
101
+ self.ssl = File.exist?(PostgresAdmin.certificate_location.join("postgres.key"))
102
+
103
+ copy_template "postgresql.conf"
104
+ copy_template "pg_hba.conf.erb"
105
+ copy_template "pg_ident.conf"
106
+ end
107
+
108
+ def post_activation
109
+ start_evm if run_as_evm_server
110
+ end
111
+
112
+ private
113
+
114
+ def mount_point
115
+ PostgresAdmin.mount_point
116
+ end
117
+
118
+ def copy_template(src, src_dir = self.class.postgresql_template, dest_dir = PostgresAdmin.data_directory)
119
+ full_src = src_dir.join(src)
120
+ if src.include?(".erb")
121
+ full_dest = dest_dir.join(src.gsub(".erb", ""))
122
+ File.open(full_dest, "w") { |f| f.puts ERB.new(File.read(full_src), nil, '-').result(binding) }
123
+ else
124
+ FileUtils.cp full_src, dest_dir
125
+ end
126
+ end
127
+
128
+ def pg_mount_point?
129
+ LinuxAdmin::LogicalVolume.mount_point_exists?(mount_point.to_s)
130
+ end
131
+
132
+ def run_initdb
133
+ AwesomeSpawn.run!("service", :params => {nil => [PostgresAdmin.service_name, "initdb"]})
134
+ end
135
+
136
+ def start_postgres
137
+ LinuxAdmin::Service.new(PostgresAdmin.service_name).enable.start
138
+ block_until_postgres_accepts_connections
139
+ end
140
+
141
+ def restart_postgres
142
+ LinuxAdmin::Service.new(PostgresAdmin.service_name).restart
143
+ block_until_postgres_accepts_connections
144
+ end
145
+
146
+ def block_until_postgres_accepts_connections
147
+ loop do
148
+ break if AwesomeSpawn.run("psql -U postgres -c 'select 1'").success?
149
+ end
150
+ end
151
+
152
+ def create_postgres_root_user
153
+ with_pg_connection do |conn|
154
+ esc_pass = conn.escape_string(password)
155
+ conn.exec("CREATE ROLE #{username} WITH LOGIN CREATEDB SUPERUSER PASSWORD '#{esc_pass}'")
156
+ end
157
+ end
158
+
159
+ def create_postgres_database
160
+ with_pg_connection do |conn|
161
+ conn.exec("CREATE DATABASE #{database} OWNER #{username} ENCODING 'utf8'")
162
+ end
163
+ end
164
+
165
+ def relabel_postgresql_dir
166
+ AwesomeSpawn.run!("/sbin/restorecon -R -v #{mount_point}")
167
+ end
168
+
169
+ def with_pg_connection
170
+ conn = PG.connect(:user => "postgres", :dbname => "postgres")
171
+ yield conn
172
+ ensure
173
+ conn.close
174
+ end
175
+
176
+ def apply_initial_configuration
177
+ shared_buffers = run_as_evm_server ? SHARED_DB_SHARED_BUFFERS : DEDICATED_DB_SHARED_BUFFERS
178
+ with_pg_connection do |conn|
179
+ conn.exec("ALTER SYSTEM SET ssl TO on") if ssl
180
+ conn.exec("ALTER SYSTEM SET shared_buffers TO #{shared_buffers}")
181
+ end
182
+
183
+ restart_postgres
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,118 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'net/scp'
4
+ require 'active_support/all'
5
+ require 'util/miq-password'
6
+
7
+ module ManageIQ
8
+ module ApplianceConsole
9
+ CERT_DIR = ENV['KEY_ROOT'] || ManageIQ::ApplianceConsole::RAILS_ROOT.join("certs")
10
+ KEY_FILE = "#{CERT_DIR}/v2_key".freeze
11
+ NEW_KEY_FILE = "#{KEY_FILE}.tmp".freeze
12
+
13
+ class KeyConfiguration
14
+ attr_accessor :host, :login, :password, :key_path, :action, :force
15
+
16
+ def initialize(options = {})
17
+ options.each { |k, v| public_send("#{k}=", v) }
18
+ @action ||= :create
19
+ @login ||= "root"
20
+ @key_path ||= KEY_FILE
21
+ end
22
+
23
+ def ask_questions
24
+ if key_exist?
25
+ @force = agree("Overwrite existing encryption key (v2_key)? (Y/N): ")
26
+ return false unless @force
27
+ end
28
+
29
+ @action = ask_for_action(@action)
30
+
31
+ if fetch_key?
32
+ say("")
33
+ @host = ask_for_ip_or_hostname("hostname for appliance with encryption key", @host)
34
+ @login = ask_for_string("appliance SSH login", @login)
35
+ @password = ask_for_password("appliance SSH password", @password)
36
+ @key_path = ask_for_string("path of remote encryption key", @key_path)
37
+ end
38
+ @action
39
+ end
40
+
41
+ def ask_question_loop
42
+ loop do
43
+ return false unless ask_questions
44
+ return true if activate
45
+ return false unless agree("Try again? (Y/N) ")
46
+ end
47
+ end
48
+
49
+ def activate
50
+ if !key_exist? || force
51
+ if get_new_key
52
+ save_new_key
53
+ else
54
+ remove_new_key_if_any
55
+ false
56
+ end
57
+ else
58
+ # probably only got here via the cli
59
+ $stderr.puts
60
+ $stderr.puts "Only generate one encryption key (v2_key) per installation."
61
+ $stderr.puts "Chances are you did not want to overwrite this file."
62
+ $stderr.puts "If you do this all encrypted secrets in the database will not be readable."
63
+ $stderr.puts "Please backup your key and run this command again with --force-key."
64
+ $stderr.puts
65
+ false
66
+ end
67
+ end
68
+
69
+ def save_new_key
70
+ begin
71
+ FileUtils.mv(NEW_KEY_FILE, KEY_FILE, :force => true)
72
+ rescue => e
73
+ say("Failed to overwrite original key, original key kept. #{e.message}")
74
+ return false
75
+ end
76
+ FileUtils.chmod(0o400, KEY_FILE)
77
+ end
78
+
79
+ def remove_new_key_if_any
80
+ FileUtils.rm(NEW_KEY_FILE) if File.exist?(NEW_KEY_FILE)
81
+ end
82
+
83
+ def key_exist?
84
+ File.exist?(KEY_FILE)
85
+ end
86
+
87
+ def fetch_key?
88
+ @action == :fetch
89
+ end
90
+
91
+ def create_key
92
+ MiqPassword.generate_symmetric(NEW_KEY_FILE) && true
93
+ end
94
+
95
+ def fetch_key
96
+ # use :verbose => 1 (or :debug for later versions) to see actual errors
97
+ Net::SCP.start(host, login, :password => password) do |scp|
98
+ scp.download!(key_path, NEW_KEY_FILE)
99
+ end
100
+ File.exist?(NEW_KEY_FILE)
101
+ rescue => e
102
+ say("Failed to fetch key: #{e.message}")
103
+ false
104
+ end
105
+
106
+ private
107
+
108
+ def ask_for_action(default_action)
109
+ options = {'Create key' => :create, 'Fetch key from remote machine' => :fetch}
110
+ ask_with_menu("Encryption Key", options, default_action, false)
111
+ end
112
+
113
+ def get_new_key
114
+ fetch_key? ? fetch_key : create_key
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,117 @@
1
+ require 'linux_admin'
2
+ require 'pathname'
3
+ require 'fileutils'
4
+ require 'util/miq-system.rb'
5
+
6
+ module ManageIQ
7
+ module ApplianceConsole
8
+ class LogfileConfiguration
9
+ LOGFILE_DIRECTORY = Pathname.new("/var/www/miq/vmdb/log").freeze
10
+ LOGFILE_NAME = "miq_logs".freeze
11
+ MIQ_LOGS_CONF = Pathname.new("/etc/logrotate.d/miq_logs.conf").freeze
12
+
13
+ attr_accessor :size, :disk, :current_logrotate_count, :new_logrotate_count, :evm_was_running
14
+
15
+ include ManageIQ::ApplianceConsole::Logging
16
+
17
+ def initialize(config = {})
18
+ self.disk = config[:disk]
19
+ self.new_logrotate_count = nil
20
+
21
+ self.size = MiqSystem.disk_usage(LOGFILE_DIRECTORY)[0][:total_bytes]
22
+ self.current_logrotate_count = /rotate\s+(\d+)/.match(File.read(MIQ_LOGS_CONF))[1]
23
+ self.evm_was_running = LinuxAdmin::Service.new("evmserverd").running?
24
+ end
25
+
26
+ def activate
27
+ activate_new_disk && activate_new_logrotate_count
28
+ end
29
+
30
+ def ask_questions
31
+ clear_screen
32
+ choose_disk if use_new_disk
33
+ choose_logrotate_count if set_new_logrotate_count?
34
+ confirm_selection
35
+ end
36
+
37
+ private
38
+
39
+ def confirm_selection
40
+ return false unless disk || new_logrotate_count
41
+
42
+ clear_screen
43
+ if disk
44
+ say("\t#{disk.path} with #{disk.size.to_i / 1.gigabyte} GB will be configured as the new logfile disk.")
45
+ end
46
+
47
+ if new_logrotate_count
48
+ say("\tThe number of saved logratations will be updated to: #{new_logrotate_count}")
49
+ end
50
+
51
+ agree("Confirm continue with these updates (Y/N):")
52
+ end
53
+
54
+ def use_new_disk
55
+ agree("Configure a new logfile disk volume? (Y/N):")
56
+ end
57
+
58
+ def choose_disk
59
+ self.disk = ask_for_disk("logfile disk")
60
+ end
61
+
62
+ def set_new_logrotate_count?
63
+ agree("Change the saved logrotate count from #{current_logrotate_count}? (Y/N):")
64
+ end
65
+
66
+ def choose_logrotate_count
67
+ say "\t1 GB of disk space is recommended for each saved log rotation."
68
+ if disk
69
+ say "\tThe proposed new disk is #{disk.size.to_i / 1.gigabyte} GB"
70
+ else
71
+ say "\tThe current log disk is #{size.to_i / 1.gigabyte} GB"
72
+ end
73
+
74
+ self.new_logrotate_count = ask_for_integer("new log rotate count")
75
+ end
76
+
77
+ def activate_new_logrotate_count
78
+ return true unless new_logrotate_count
79
+ say 'Activating new logrotate count'
80
+ data = File.read(MIQ_LOGS_CONF)
81
+ data.gsub!(/rotate\s+\d+/, "rotate #{new_logrotate_count}")
82
+ File.write(MIQ_LOGS_CONF, data)
83
+ true
84
+ end
85
+
86
+ def activate_new_disk
87
+ return true unless disk
88
+ stop_evm if evm_was_running
89
+ initialize_logfile_disk
90
+ start_evm if evm_was_running
91
+ true
92
+ end
93
+
94
+ def initialize_logfile_disk
95
+ say 'Initializing logfile disk'
96
+ LogicalVolumeManagement.new(:disk => disk, :mount_point => LOGFILE_DIRECTORY, :name => LOGFILE_NAME).setup
97
+
98
+ FileUtils.mkdir_p("#{LOGFILE_DIRECTORY}/apache")
99
+ AwesomeSpawn.run!('/usr/sbin/semanage fcontext -a -t httpd_log_t "#{LOGFILE_DIRECTORY.to_path}(/.*)?"')
100
+ AwesomeSpawn.run!("/sbin/restorecon -R -v #{LOGFILE_DIRECTORY.to_path}") if File.executable?("/sbin/restorecon")
101
+ true
102
+ end
103
+
104
+ def start_evm
105
+ say 'Starting EVM'
106
+ LinuxAdmin::Service.new("evmserverd").enable.start
107
+ LinuxAdmin::Service.new("httpd").enable.start
108
+ end
109
+
110
+ def stop_evm
111
+ say 'Stopping EVM'
112
+ LinuxAdmin::Service.new("evmserverd").stop
113
+ LinuxAdmin::Service.new("httpd").stop
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+
3
+ module ManageIQ
4
+ module ApplianceConsole
5
+ class Logger < ::Logger
6
+ def self.log_dir
7
+ @log_dir ||= ManageIQ::ApplianceConsole::RAILS_ROOT.join("log")
8
+ end
9
+
10
+ def self.log_file
11
+ @log_file ||= log_dir.join("appliance_console.log").to_s
12
+ end
13
+
14
+ def self.instance
15
+ @instance ||= begin
16
+ require 'fileutils'
17
+ FileUtils.mkdir_p(log_dir.to_s)
18
+ new(log_file).tap { |l| l.level = Logger::INFO }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,102 @@
1
+ require 'awesome_spawn'
2
+ require 'active_support/all'
3
+
4
+ module ManageIQ
5
+ module ApplianceConsole
6
+ module Logging
7
+ class << self
8
+ attr_accessor :interactive
9
+
10
+ def interactive?
11
+ @interactive != false
12
+ end
13
+ end
14
+
15
+ def interactive=(interactive)
16
+ ManageIQ::ApplianceConsole::Logging.interactive = interactive
17
+ end
18
+
19
+ def interactive?
20
+ ManageIQ::ApplianceConsole::Logging.interactive?
21
+ end
22
+
23
+ def interactive
24
+ ManageIQ::ApplianceConsole::Logging.interactive
25
+ end
26
+
27
+ def logger=(logger)
28
+ ManageIQ::ApplianceConsole.logger = logger
29
+ end
30
+
31
+ def logger
32
+ ManageIQ::ApplianceConsole.logger
33
+ end
34
+
35
+ # TODO: move say_error and say_info to prompting module?
36
+ def say_error(method, output)
37
+ log = "\nSee #{ManageIQ::ApplianceConsole::Logger.log_file} for details."
38
+ text = "#{method.to_s.humanize} failed with error - #{output.truncate(200)}.#{log}"
39
+ say(text)
40
+ press_any_key if interactive?
41
+ raise ManageIQ::ApplianceConsole::MiqSignalError
42
+ end
43
+
44
+ def say_info(method, output)
45
+ say("#{method.to_s.humanize} #{output}")
46
+ end
47
+
48
+ def log_and_feedback(method)
49
+ raise ArgumentError, "No block given" unless block_given?
50
+
51
+ log_and_feedback_info(method, "starting")
52
+
53
+ result = nil
54
+ begin
55
+ result = yield
56
+ rescue => err
57
+ log_and_feedback_exception(err, method)
58
+ else
59
+ log_and_feedback_info(method, "complete")
60
+ end
61
+ result
62
+ end
63
+
64
+ def log_prefix(method)
65
+ "MIQ(#{self.class.name}##{method}) "
66
+ end
67
+
68
+ def log_and_feedback_info(method, message)
69
+ logger.info("#{log_prefix(method)}: #{message}")
70
+ say_info(method, message)
71
+ end
72
+
73
+ def log_and_feedback_exception(error, failed_method)
74
+ feedback_error, logging = case error
75
+ when AwesomeSpawn::CommandResultError
76
+ error_and_logging_from_command_result_error(error)
77
+ else
78
+ error_and_logging_from_standard_error(error)
79
+ end
80
+
81
+ log_error(failed_method, logging)
82
+ say_error(failed_method, feedback_error)
83
+ end
84
+
85
+ def error_and_logging_from_command_result_error(error)
86
+ result = error.result
87
+ location = error.backtrace.detect { |loc| !loc.match(/(linux_admin|awesome_spawn)/) }
88
+ return error.message, "Command failed: #{error.message}. Error: #{result.error}. Output: #{result.output}. At: #{location}"
89
+ end
90
+
91
+ def error_and_logging_from_standard_error(error)
92
+ debugging = "Error: #{error.class.name} with message: #{error.message}"
93
+ logging = "#{debugging}. Failed at: #{error.backtrace[0]}"
94
+ return debugging, logging
95
+ end
96
+
97
+ def log_error(failed_method, debugging)
98
+ logger.error("#{log_prefix(failed_method)} #{debugging}")
99
+ end
100
+ end # module Logging
101
+ end # module ApplicationConsole
102
+ end