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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +47 -0
- data/.gitignore +12 -0
- data/.rspec +4 -0
- data/.rspec_ci +4 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +5 -0
- data/.rubocop_local.yml +2 -0
- data/.travis.yml +19 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +202 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/appliance_console +661 -0
- data/bin/appliance_console_cli +7 -0
- data/lib/manageiq-appliance_console.rb +51 -0
- data/lib/manageiq/appliance_console/certificate.rb +146 -0
- data/lib/manageiq/appliance_console/certificate_authority.rb +140 -0
- data/lib/manageiq/appliance_console/cli.rb +363 -0
- data/lib/manageiq/appliance_console/database_configuration.rb +286 -0
- data/lib/manageiq/appliance_console/database_maintenance.rb +35 -0
- data/lib/manageiq/appliance_console/database_maintenance_hourly.rb +58 -0
- data/lib/manageiq/appliance_console/database_maintenance_periodic.rb +84 -0
- data/lib/manageiq/appliance_console/database_replication.rb +146 -0
- data/lib/manageiq/appliance_console/database_replication_primary.rb +59 -0
- data/lib/manageiq/appliance_console/database_replication_standby.rb +166 -0
- data/lib/manageiq/appliance_console/date_time_configuration.rb +117 -0
- data/lib/manageiq/appliance_console/errors.rb +5 -0
- data/lib/manageiq/appliance_console/external_auth_options.rb +153 -0
- data/lib/manageiq/appliance_console/external_database_configuration.rb +34 -0
- data/lib/manageiq/appliance_console/external_httpd_authentication.rb +157 -0
- data/lib/manageiq/appliance_console/external_httpd_authentication/external_httpd_configuration.rb +249 -0
- data/lib/manageiq/appliance_console/internal_database_configuration.rb +187 -0
- data/lib/manageiq/appliance_console/key_configuration.rb +118 -0
- data/lib/manageiq/appliance_console/logfile_configuration.rb +117 -0
- data/lib/manageiq/appliance_console/logger.rb +23 -0
- data/lib/manageiq/appliance_console/logging.rb +102 -0
- data/lib/manageiq/appliance_console/logical_volume_management.rb +94 -0
- data/lib/manageiq/appliance_console/principal.rb +46 -0
- data/lib/manageiq/appliance_console/prompts.rb +211 -0
- data/lib/manageiq/appliance_console/scap.rb +53 -0
- data/lib/manageiq/appliance_console/temp_storage_configuration.rb +79 -0
- data/lib/manageiq/appliance_console/timezone_configuration.rb +58 -0
- data/lib/manageiq/appliance_console/utilities.rb +67 -0
- data/lib/manageiq/appliance_console/version.rb +5 -0
- data/locales/appliance/en.yml +42 -0
- data/locales/container/en.yml +30 -0
- data/manageiq-appliance_console.gemspec +40 -0
- data/zanata.xml +7 -0
- metadata +317 -0
@@ -0,0 +1,286 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'linux_admin'
|
4
|
+
require 'pathname'
|
5
|
+
require 'util/miq-password'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module ManageIQ
|
9
|
+
module ApplianceConsole
|
10
|
+
class DatabaseConfiguration
|
11
|
+
attr_accessor :adapter, :host, :username, :database, :password, :port, :region
|
12
|
+
|
13
|
+
class ModelWithNoBackingTable < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
DB_YML = ManageIQ::ApplianceConsole::RAILS_ROOT.join("config/database.yml")
|
17
|
+
DB_YML_TMPL = ManageIQ::ApplianceConsole::RAILS_ROOT.join("config/database.pg.yml")
|
18
|
+
|
19
|
+
CREATE_REGION_AGREE = "WARNING: Creating a database region will destroy any existing data and cannot be undone.\n\nAre you sure you want to continue? (Y/N):".freeze
|
20
|
+
FAILED_WITH_ERROR_HYPHEN = "failed with error -".freeze
|
21
|
+
|
22
|
+
# PG 9.2 bigint max 9223372036854775807 / ArRegion::DEFAULT_RAILS_SEQUENCE_FACTOR = 9223372
|
23
|
+
# http://www.postgresql.org/docs/9.2/static/datatype-numeric.html
|
24
|
+
# 9223372 won't be a full region though, so we're not including it.
|
25
|
+
# TODO: This information should be shared outside of appliance console code and MiqRegion.
|
26
|
+
REGION_RANGE = 0..9223371
|
27
|
+
DEFAULT_PORT = 5432
|
28
|
+
|
29
|
+
include ManageIQ::ApplianceConsole::Logging
|
30
|
+
|
31
|
+
def initialize(hash = {})
|
32
|
+
initialize_from_hash(hash)
|
33
|
+
@adapter ||= "postgresql"
|
34
|
+
# introduced by Logging
|
35
|
+
self.interactive = true unless hash.key?(:interactive)
|
36
|
+
end
|
37
|
+
|
38
|
+
def run_interactive
|
39
|
+
ask_questions
|
40
|
+
|
41
|
+
clear_screen
|
42
|
+
say "Activating the configuration using the following settings...\n#{friendly_inspect}\n"
|
43
|
+
|
44
|
+
raise MiqSignalError unless activate
|
45
|
+
|
46
|
+
post_activation
|
47
|
+
say("\nConfiguration activated successfully.\n")
|
48
|
+
rescue RuntimeError => e
|
49
|
+
puts "Configuration failed#{": " + e.message unless e.class == MiqSignalError}"
|
50
|
+
press_any_key
|
51
|
+
raise MiqSignalError
|
52
|
+
end
|
53
|
+
|
54
|
+
def local?
|
55
|
+
host.blank? || host.in?(%w(localhost 127.0.0.1))
|
56
|
+
end
|
57
|
+
|
58
|
+
def password=(value)
|
59
|
+
@password = MiqPassword.try_decrypt(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def activate
|
63
|
+
return false unless validated
|
64
|
+
|
65
|
+
original = self.class.current
|
66
|
+
success = false
|
67
|
+
|
68
|
+
begin
|
69
|
+
save
|
70
|
+
success = create_or_join_region
|
71
|
+
rescue
|
72
|
+
success = false
|
73
|
+
ensure
|
74
|
+
save(original) unless success
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_or_join_region
|
79
|
+
region ? create_region : join_region
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_region
|
83
|
+
ManageIQ::ApplianceConsole::Utilities.bail_if_db_connections("preventing the setup of a database region")
|
84
|
+
log_and_feedback(__method__) do
|
85
|
+
ManageIQ::ApplianceConsole::Utilities.rake("evm:db:region", ["--", {:region => region}])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def join_region
|
90
|
+
ManageIQ::ApplianceConsole::Utilities.rake("evm:join_region", {})
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_region
|
94
|
+
say("Warning: RESETTING A DATABASE WILL DESTROY ANY EXISTING DATA AND CANNOT BE UNDONE.\n\n")
|
95
|
+
raise MiqSignalError unless are_you_sure?("reset the configured database")
|
96
|
+
|
97
|
+
create_new_region_questions(false)
|
98
|
+
ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] = "1"
|
99
|
+
create_region
|
100
|
+
ensure
|
101
|
+
ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] = nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_new_region_questions(warn = true)
|
105
|
+
clear_screen
|
106
|
+
say("\n\nNote: Creating a new database region requires an empty database.") if warn
|
107
|
+
say("Each database region number must be unique.\n")
|
108
|
+
self.region = ask_for_integer("database region number", REGION_RANGE)
|
109
|
+
raise MiqSignalError if warn && !agree(CREATE_REGION_AGREE)
|
110
|
+
end
|
111
|
+
|
112
|
+
def ask_for_database_credentials(password_twice = true)
|
113
|
+
self.host = ask_for_ip_or_hostname("database hostname or IP address", host) if host.blank? || !local?
|
114
|
+
self.port = ask_for_integer("port number", nil, port) unless local?
|
115
|
+
self.database = just_ask("name of the database on #{host}", database) unless local?
|
116
|
+
self.username = just_ask("username", username) unless local?
|
117
|
+
count = 0
|
118
|
+
loop do
|
119
|
+
password1 = ask_for_password("database password on #{host}", password)
|
120
|
+
# if they took the default, just bail
|
121
|
+
break if (password1 == password)
|
122
|
+
|
123
|
+
if password1.strip.length == 0
|
124
|
+
say("\nPassword can not be empty, please try again")
|
125
|
+
next
|
126
|
+
end
|
127
|
+
if password_twice
|
128
|
+
password2 = ask_for_password("database password again")
|
129
|
+
if password1 == password2
|
130
|
+
self.password = password1
|
131
|
+
break
|
132
|
+
elsif count > 0 # only reprompt password once
|
133
|
+
raise "passwords did not match"
|
134
|
+
else
|
135
|
+
count += 1
|
136
|
+
say("\nThe passwords did not match, please try again")
|
137
|
+
end
|
138
|
+
else
|
139
|
+
self.password = password1
|
140
|
+
break
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def friendly_inspect
|
146
|
+
output = <<-FRIENDLY
|
147
|
+
Host: #{host}
|
148
|
+
Username: #{username}
|
149
|
+
Database: #{database}
|
150
|
+
FRIENDLY
|
151
|
+
output << "Port: #{port}\n" if port
|
152
|
+
output << "Region: #{region}\n" if region
|
153
|
+
output
|
154
|
+
end
|
155
|
+
|
156
|
+
def settings_hash
|
157
|
+
{
|
158
|
+
'adapter' => 'postgresql',
|
159
|
+
'host' => local? ? "localhost" : host,
|
160
|
+
'port' => port,
|
161
|
+
'username' => username,
|
162
|
+
'password' => password.presence,
|
163
|
+
'database' => database
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
# merge all the non specified setings
|
168
|
+
# for all the basic attributes, overwrite from this object (including blank values)
|
169
|
+
def merged_settings
|
170
|
+
merged = self.class.current
|
171
|
+
settings_hash.each do |k, v|
|
172
|
+
if v.present?
|
173
|
+
merged['production'][k] = v
|
174
|
+
else
|
175
|
+
merged['production'].delete(k)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
merged
|
179
|
+
end
|
180
|
+
|
181
|
+
def save(settings = nil)
|
182
|
+
settings ||= merged_settings
|
183
|
+
settings = self.class.encrypt_password(settings)
|
184
|
+
do_save(settings)
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.encrypt_password(settings)
|
188
|
+
encrypt_decrypt_password(settings) { |pass| MiqPassword.try_encrypt(pass) }
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.decrypt_password(settings)
|
192
|
+
encrypt_decrypt_password(settings) { |pass| MiqPassword.try_decrypt(pass) }
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.current
|
196
|
+
decrypt_password(load_current)
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.database_yml_configured?
|
200
|
+
File.exist?(DB_YML) && File.exist?(KEY_FILE)
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.database_host
|
204
|
+
database_yml_configured? ? current[rails_env]['host'] || "localhost" : nil
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.database_name
|
208
|
+
database_yml_configured? ? current[rails_env]['database'] : nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.region
|
212
|
+
database_yml_configured? ? ManageIQ::ApplianceConsole::Utilities.db_region : nil
|
213
|
+
end
|
214
|
+
|
215
|
+
def validated
|
216
|
+
!!validate!
|
217
|
+
rescue => err
|
218
|
+
say_error(__method__, err.message)
|
219
|
+
log_error(__method__, err.message)
|
220
|
+
false
|
221
|
+
end
|
222
|
+
|
223
|
+
def validate!
|
224
|
+
pool = ModelWithNoBackingTable.establish_connection(settings_hash.delete_if { |_n, v| v.blank? })
|
225
|
+
begin
|
226
|
+
pool.connection
|
227
|
+
ensure
|
228
|
+
ModelWithNoBackingTable.remove_connection
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def start_evm
|
233
|
+
pid = fork do
|
234
|
+
begin
|
235
|
+
LinuxAdmin::Service.new("evmserverd").enable.start
|
236
|
+
rescue => e
|
237
|
+
logger.error("Failed to enable and start evmserverd service: #{e.message}")
|
238
|
+
logger.error(e.backtrace.join("\n"))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
Process.detach(pid)
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def self.rails_env
|
247
|
+
ENV["RAILS_ENV"] || "development"
|
248
|
+
end
|
249
|
+
private_class_method :rails_env
|
250
|
+
|
251
|
+
def self.encrypt_decrypt_password(settings)
|
252
|
+
new_settings = {}
|
253
|
+
settings.each_key { |section| new_settings[section] = settings[section].dup }
|
254
|
+
pass = new_settings["production"]["password"]
|
255
|
+
new_settings["production"]["password"] = yield(pass) if pass
|
256
|
+
new_settings
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.load_current
|
260
|
+
require 'yaml'
|
261
|
+
unless File.exist?(DB_YML)
|
262
|
+
require 'fileutils'
|
263
|
+
FileUtils.cp(DB_YML_TMPL, DB_YML) if File.exist?(DB_YML_TMPL)
|
264
|
+
end
|
265
|
+
YAML.load_file(DB_YML)
|
266
|
+
end
|
267
|
+
|
268
|
+
def do_save(settings)
|
269
|
+
require 'yaml'
|
270
|
+
File.write(DB_YML, YAML.dump(settings))
|
271
|
+
end
|
272
|
+
|
273
|
+
def initialize_from_hash(hash)
|
274
|
+
hash.each do |k, v|
|
275
|
+
next if v.nil?
|
276
|
+
setter = "#{k}="
|
277
|
+
if self.respond_to?(setter)
|
278
|
+
public_send(setter, v)
|
279
|
+
else
|
280
|
+
raise ArgumentError, "Invalid argument: #{k}"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module ApplianceConsole
|
5
|
+
class DatabaseMaintenance
|
6
|
+
include ManageIQ::ApplianceConsole::Logging
|
7
|
+
|
8
|
+
attr_accessor :hourly, :executed_hourly_action, :requested_hourly_action
|
9
|
+
attr_accessor :periodic, :executed_periodic_action, :requested_periodic_action
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.hourly = ManageIQ::ApplianceConsole::DatabaseMaintenanceHourly.new
|
13
|
+
self.periodic = ManageIQ::ApplianceConsole::DatabaseMaintenancePeriodic.new
|
14
|
+
self.requested_hourly_action = false
|
15
|
+
self.requested_periodic_action = false
|
16
|
+
self.executed_hourly_action = false
|
17
|
+
self.executed_periodic_action = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def ask_questions
|
21
|
+
clear_screen
|
22
|
+
self.requested_hourly_action = hourly.confirm
|
23
|
+
self.requested_periodic_action = periodic.confirm
|
24
|
+
requested_hourly_action || requested_periodic_action
|
25
|
+
end
|
26
|
+
|
27
|
+
def activate
|
28
|
+
say("Configuring Database Maintenance...")
|
29
|
+
self.executed_hourly_action = hourly.activate
|
30
|
+
self.executed_periodic_action = periodic.activate
|
31
|
+
executed_hourly_action || executed_periodic_action
|
32
|
+
end
|
33
|
+
end # class DatabaseMaintenance < DatabaseConfiguration
|
34
|
+
end # module ApplianceConsole
|
35
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module ApplianceConsole
|
5
|
+
class DatabaseMaintenanceHourly
|
6
|
+
include ManageIQ::ApplianceConsole::Logging
|
7
|
+
|
8
|
+
HOURLY_CRON = "/etc/cron.hourly/miq-pg-maintenance-hourly.cron".freeze
|
9
|
+
|
10
|
+
attr_accessor :already_configured, :requested_deactivate, :requested_activate
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
self.already_configured = File.exist?(HOURLY_CRON)
|
14
|
+
self.requested_deactivate = false
|
15
|
+
self.requested_activate = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def activate
|
19
|
+
return deactivate if requested_deactivate
|
20
|
+
return configure if requested_activate
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def confirm
|
25
|
+
if already_configured
|
26
|
+
self.requested_deactivate = agree("Hourly Database Maintenance is already configured, Un-Configure (Y/N):")
|
27
|
+
else
|
28
|
+
self.requested_activate = agree("Configure Hourly Database Maintenance? (Y/N): ")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def configure
|
35
|
+
say("Configuring Hourly Database Maintenance...")
|
36
|
+
write_hourly_cron
|
37
|
+
FileUtils.chmod(0755, HOURLY_CRON)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def deactivate
|
42
|
+
say("Un-Configuring Hourly Database Maintenance...")
|
43
|
+
FileUtils.rm_f(HOURLY_CRON)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_hourly_cron
|
48
|
+
File.open(HOURLY_CRON, "w") do |f|
|
49
|
+
f.write("#!/bin/sh\n")
|
50
|
+
f.write("/usr/bin/hourly_reindex_metrics_tables\n")
|
51
|
+
f.write("/usr/bin/hourly_reindex_miq_queue_table\n")
|
52
|
+
f.write("/usr/bin/hourly_reindex_miq_workers_table\n")
|
53
|
+
f.write("exit 0\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end # class DatabaseMaintenance < DatabaseConfiguration
|
57
|
+
end # module ApplianceConsole
|
58
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module ApplianceConsole
|
3
|
+
class DatabaseMaintenancePeriodic
|
4
|
+
include ManageIQ::ApplianceConsole::Logging
|
5
|
+
|
6
|
+
RUN_AS = 'root'.freeze
|
7
|
+
PERIODIC_CMD = '/usr/bin/periodic_vacuum_full_tables'.freeze
|
8
|
+
CRONTAB_FILE = '/etc/crontab'.freeze
|
9
|
+
SCHEDULE_PROMPT = 'frequency periodic database maintenance should run (hourly daily weekly monthly)'.freeze
|
10
|
+
HOUR_PROMPT = 'hour number (0..23)'.freeze
|
11
|
+
WEEK_DAY_PROMPT = 'week day number (0..6, where Sunday is 0)'.freeze
|
12
|
+
MONTH_DAY_PROMPT = 'month day number (1..31)'.freeze
|
13
|
+
|
14
|
+
attr_accessor :crontab_schedule_expression, :already_configured, :requested_deactivate, :requested_activate
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.crontab_schedule_expression = nil
|
18
|
+
self.already_configured = File.readlines(CRONTAB_FILE).detect { |line| line =~ /#{PERIODIC_CMD}/ }.present?
|
19
|
+
self.requested_deactivate = false
|
20
|
+
self.requested_activate = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def activate
|
24
|
+
return deactivate if requested_deactivate
|
25
|
+
return configure if requested_activate
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def confirm
|
30
|
+
if already_configured
|
31
|
+
self.requested_deactivate = agree("Periodic Database Maintenance is already configured, Un-Configure (Y/N):")
|
32
|
+
else
|
33
|
+
self.requested_activate = agree("Configure Periodic Database Maintenance? (Y/N): ")
|
34
|
+
ask_for_schedule if requested_activate
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def ask_for_schedule
|
41
|
+
self.crontab_schedule_expression =
|
42
|
+
case ask_for_schedule_frequency(SCHEDULE_PROMPT, 'monthly')
|
43
|
+
when 'hourly'
|
44
|
+
generate_hourly_crontab_expression
|
45
|
+
when 'daily'
|
46
|
+
generate_daily_crontab_expression
|
47
|
+
when 'weekly'
|
48
|
+
generate_weekly_crontab_expression
|
49
|
+
when 'monthly'
|
50
|
+
generate_monthly_crontab_expression
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure
|
55
|
+
File.open(CRONTAB_FILE, "a") do |f|
|
56
|
+
f.write("#{crontab_schedule_expression} #{RUN_AS} #{PERIODIC_CMD}\n")
|
57
|
+
end
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def deactivate
|
62
|
+
keep_content = File.readlines(CRONTAB_FILE).reject { |line| line =~ /#{PERIODIC_CMD}/ }
|
63
|
+
File.open(CRONTAB_FILE, "w") { |f| keep_content.each { |line| f.puts line } }
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def generate_hourly_crontab_expression
|
68
|
+
"0 * * * *"
|
69
|
+
end
|
70
|
+
|
71
|
+
def generate_daily_crontab_expression
|
72
|
+
"0 #{ask_for_hour_number(HOUR_PROMPT)} * * *"
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_weekly_crontab_expression
|
76
|
+
"0 #{ask_for_hour_number(HOUR_PROMPT)} * * #{ask_for_week_day_number(WEEK_DAY_PROMPT)}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_monthly_crontab_expression
|
80
|
+
"0 #{ask_for_hour_number(HOUR_PROMPT)} #{ask_for_month_day_number(MONTH_DAY_PROMPT)} * *"
|
81
|
+
end
|
82
|
+
end # class DatabaseMaintenancePeriodic
|
83
|
+
end # module ApplianceConsole
|
84
|
+
end
|