manageiq-appliance_console 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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