foreman_maintain 0.0.1
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/LICENSE +674 -0
- data/README.md +193 -0
- data/bin/foreman-maintain +9 -0
- data/definitions/checks/foreman_tasks_not_paused.rb +14 -0
- data/definitions/checks/foreman_tasks_not_running.rb +10 -0
- data/definitions/features/downstream.rb +17 -0
- data/definitions/features/foreman_1_11_x.rb +7 -0
- data/definitions/features/foreman_1_7_x.rb +7 -0
- data/definitions/features/foreman_database.rb +15 -0
- data/definitions/features/foreman_tasks.rb +22 -0
- data/definitions/features/upstream.rb +7 -0
- data/definitions/procedures/foreman_tasks_resume.rb +13 -0
- data/definitions/scenarios/pre_upgrade_check_foreman_1_14.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_0_z.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_1.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_1_z.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_2.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_2_z.rb +12 -0
- data/definitions/scenarios/pre_upgrade_check_satellite_6_3.rb +12 -0
- data/lib/foreman_maintain.rb +54 -0
- data/lib/foreman_maintain/check.rb +34 -0
- data/lib/foreman_maintain/cli.rb +14 -0
- data/lib/foreman_maintain/cli/base.rb +47 -0
- data/lib/foreman_maintain/cli/health_command.rb +39 -0
- data/lib/foreman_maintain/cli/upgrade_command.rb +57 -0
- data/lib/foreman_maintain/concerns/finders.rb +69 -0
- data/lib/foreman_maintain/concerns/logger.rb +13 -0
- data/lib/foreman_maintain/concerns/metadata.rb +124 -0
- data/lib/foreman_maintain/concerns/system_helpers.rb +93 -0
- data/lib/foreman_maintain/config.rb +17 -0
- data/lib/foreman_maintain/detector.rb +146 -0
- data/lib/foreman_maintain/executable.rb +44 -0
- data/lib/foreman_maintain/feature.rb +22 -0
- data/lib/foreman_maintain/logger.rb +11 -0
- data/lib/foreman_maintain/procedure.rb +8 -0
- data/lib/foreman_maintain/reporter.rb +18 -0
- data/lib/foreman_maintain/reporter/cli_reporter.rb +177 -0
- data/lib/foreman_maintain/runner.rb +39 -0
- data/lib/foreman_maintain/runner/execution.rb +74 -0
- data/lib/foreman_maintain/scenario.rb +46 -0
- data/lib/foreman_maintain/top_level_modules.rb +13 -0
- data/lib/foreman_maintain/version.rb +3 -0
- metadata +173 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module ForemanMaintain
|
2
|
+
class Config
|
3
|
+
attr_accessor :definitions_dirs, :log_level
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@definitions_dirs = options.fetch(:definitions_dirs,
|
7
|
+
[File.join(source_path, 'definitions')])
|
8
|
+
@log_level = options.fetch(:log_level, ::Logger::ERROR)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def source_path
|
14
|
+
File.expand_path('../../..', __FILE__)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module ForemanMaintain
|
2
|
+
class Detector
|
3
|
+
include Concerns::Logger
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
refresh
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns instance of feature detected on system by label
|
10
|
+
def feature(label)
|
11
|
+
detect_feature(label)
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter(collection, conditions)
|
15
|
+
if conditions
|
16
|
+
collection = collection.find_all { |object| match_object?(object, conditions) }
|
17
|
+
end
|
18
|
+
sort(collection)
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh
|
22
|
+
@features_by_label = {}
|
23
|
+
@available_features = []
|
24
|
+
@all_features_scanned = false
|
25
|
+
@available_checks = nil
|
26
|
+
@available_scenarios = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def available_features(filter_conditions = nil)
|
30
|
+
ensure_features_detected
|
31
|
+
filter(@available_features, filter_conditions)
|
32
|
+
end
|
33
|
+
|
34
|
+
def available_checks(filter_conditions = nil)
|
35
|
+
unless @available_checks
|
36
|
+
ensure_features_detected
|
37
|
+
@available_checks = find_present_objects(Check)
|
38
|
+
end
|
39
|
+
filter(@available_checks, filter_conditions)
|
40
|
+
end
|
41
|
+
|
42
|
+
def available_procedures(filter_conditions = nil)
|
43
|
+
unless @available_procedures
|
44
|
+
ensure_features_detected
|
45
|
+
@available_procedures = find_present_objects(Procedure)
|
46
|
+
end
|
47
|
+
filter(@available_procedures, filter_conditions)
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_present_objects(object_base_class)
|
51
|
+
object_base_class.all_sub_classes.reduce([]) do |array, object_class|
|
52
|
+
feature_label = object_class.metadata[:for_feature]
|
53
|
+
object = object_class.new(feature_label && feature(feature_label))
|
54
|
+
array << object if object.present?
|
55
|
+
array
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def available_scenarios(filter_conditions = nil)
|
60
|
+
unless @available_scenarios
|
61
|
+
ensure_features_detected
|
62
|
+
@available_scenarios = Scenario.all_sub_classes.select(&:autodetect?).
|
63
|
+
map(&:new).select(&:present?)
|
64
|
+
end
|
65
|
+
filter(@available_scenarios, filter_conditions)
|
66
|
+
end
|
67
|
+
|
68
|
+
def ensure_features_detected
|
69
|
+
return if @all_features_scanned
|
70
|
+
@available_features = []
|
71
|
+
@features_by_label = {}
|
72
|
+
autodetect_features.keys.each do |label|
|
73
|
+
detect_feature(label)
|
74
|
+
end
|
75
|
+
@all_features_scanned = true
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def sort(collection)
|
81
|
+
collection.sort_by { |item| item.label.to_s }
|
82
|
+
end
|
83
|
+
|
84
|
+
def match_object?(object, conditions)
|
85
|
+
conditions = normalize_filter_conditions(conditions)
|
86
|
+
return false if conditions[:label] && object.label != conditions[:label]
|
87
|
+
return false if conditions[:class] && object.class != conditions[:class]
|
88
|
+
conditions[:tags].all? { |tag| object.metadata[:tags].include?(tag) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:disable Metrics/MethodLength
|
92
|
+
def normalize_filter_conditions(conditions)
|
93
|
+
ret = conditions.is_a?(Hash) ? conditions.dup : {}
|
94
|
+
ret[:tags] = case conditions
|
95
|
+
when Symbol
|
96
|
+
[conditions]
|
97
|
+
when Array
|
98
|
+
conditions
|
99
|
+
when Hash
|
100
|
+
ret[:tags]
|
101
|
+
end
|
102
|
+
ret[:tags] = Array(ret.fetch(:tags, []))
|
103
|
+
ret
|
104
|
+
end
|
105
|
+
|
106
|
+
# rubocop:disable Metrics/AbcSize
|
107
|
+
def detect_feature(label)
|
108
|
+
return @features_by_label[label] if @features_by_label.key?(label)
|
109
|
+
return unless autodetect_features.key?(label)
|
110
|
+
present_feature = autodetect_features[label].find(&:present?)
|
111
|
+
return unless present_feature
|
112
|
+
@available_features << present_feature
|
113
|
+
# we don't allow duplicities of features that are autodetected
|
114
|
+
add_feature_by_label(label, present_feature, false)
|
115
|
+
additional_features = present_feature.additional_features
|
116
|
+
@available_features.concat(additional_features)
|
117
|
+
additional_features.each do |feature|
|
118
|
+
# we allow duplicities if the feature is added via additional_features
|
119
|
+
add_feature_by_label(feature.metadata[:label], feature, true)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def autodetect_features
|
124
|
+
@autodetect_features ||= Feature.sub_classes.reduce({}) do |hash, feature_class|
|
125
|
+
hash.update(feature_class.metadata[:label] =>
|
126
|
+
feature_class.all_sub_classes.select(&:autodetect?).reverse.map(&:new))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_feature_by_label(feature_label, feature, allow_duplicities)
|
131
|
+
if @features_by_label.key?(feature_label)
|
132
|
+
if allow_duplicities
|
133
|
+
unless @features_by_label[feature_label].is_a? Array
|
134
|
+
@features_by_label[feature_label] = Array(@features_by_label[feature_label])
|
135
|
+
end
|
136
|
+
@features_by_label[feature_label] << feature
|
137
|
+
else
|
138
|
+
raise "Double detection of feature with the same label #{feature_label}:" \
|
139
|
+
"#{feature.inspect} vs. #{@features_by_label[feature_label].inspect}"
|
140
|
+
end
|
141
|
+
else
|
142
|
+
@features_by_label[feature_label] = feature
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ForemanMaintain
|
2
|
+
class Executable
|
3
|
+
attr_accessor :associated_feature
|
4
|
+
|
5
|
+
def initialize(associated_feature)
|
6
|
+
@associated_feature = associated_feature
|
7
|
+
end
|
8
|
+
|
9
|
+
# public method to be overriden
|
10
|
+
def run
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
# override to offer steps to be executed after this one
|
15
|
+
def next_steps; end
|
16
|
+
|
17
|
+
def execution
|
18
|
+
if @_execution
|
19
|
+
@_execution
|
20
|
+
else
|
21
|
+
raise 'Trying to get execution information before the run started happened'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def success?
|
26
|
+
execution.success?
|
27
|
+
end
|
28
|
+
|
29
|
+
def fail?
|
30
|
+
execution.fail?
|
31
|
+
end
|
32
|
+
|
33
|
+
# update reporter about the current message
|
34
|
+
def say(message)
|
35
|
+
@_execution.update(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
# internal method called by executor
|
39
|
+
def __run__(execution)
|
40
|
+
@_execution = execution
|
41
|
+
run
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ForemanMaintain
|
2
|
+
class Feature
|
3
|
+
include Concerns::Logger
|
4
|
+
include Concerns::SystemHelpers
|
5
|
+
include Concerns::Metadata
|
6
|
+
include Concerns::Finders
|
7
|
+
|
8
|
+
def self.inspect
|
9
|
+
"Feature Class #{metadata[:label]}<#{name}>"
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"#{self.class.metadata[:label]}<#{self.class.name}>"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Override to generate additional feature instances that can't be
|
17
|
+
# autodetected directly
|
18
|
+
def additional_features
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ForemanMaintain
|
2
|
+
class Reporter
|
3
|
+
require 'foreman_maintain/reporter/cli_reporter'
|
4
|
+
|
5
|
+
# Each public method is a hook called by executor at the specific point
|
6
|
+
def before_scenario_starts(_scenario); end
|
7
|
+
|
8
|
+
def before_execution_starts(_execution); end
|
9
|
+
|
10
|
+
def on_execution_update(_execution, _update); end
|
11
|
+
|
12
|
+
def after_execution_finishes(_execution); end
|
13
|
+
|
14
|
+
def after_scenario_finishes(_scenario); end
|
15
|
+
|
16
|
+
def on_next_steps_offer(_runner, _steps); end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'highline'
|
3
|
+
|
4
|
+
module ForemanMaintain
|
5
|
+
class Reporter
|
6
|
+
class CLIReporter < Reporter
|
7
|
+
# Simple spinner able to keep updating current line
|
8
|
+
class Spinner
|
9
|
+
def initialize(reporter, interval = 0.1)
|
10
|
+
@reporter = reporter
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@active = false
|
13
|
+
@interval = interval
|
14
|
+
@spinner_index = 0
|
15
|
+
@spinner_chars = %w(| / - \\)
|
16
|
+
@current_line = ''
|
17
|
+
@puts_needed = false
|
18
|
+
start_spinner
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(line)
|
22
|
+
@mutex.synchronize { @current_line = line }
|
23
|
+
end
|
24
|
+
|
25
|
+
def activate
|
26
|
+
@mutex.synchronize { @active = true }
|
27
|
+
spin
|
28
|
+
end
|
29
|
+
|
30
|
+
def deactivate
|
31
|
+
@mutex.synchronize do
|
32
|
+
@active = false
|
33
|
+
@reporter.print "\r"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def start_spinner
|
40
|
+
@thread = Thread.new do
|
41
|
+
loop do
|
42
|
+
spin
|
43
|
+
sleep @interval
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def spin
|
49
|
+
@mutex.synchronize do
|
50
|
+
return unless @active
|
51
|
+
@reporter.clear_line
|
52
|
+
@reporter.print "\r"
|
53
|
+
line = "#{@spinner_chars[@spinner_index]} #{@current_line}"
|
54
|
+
@reporter.print(line)
|
55
|
+
@spinner_index = (@spinner_index + 1) % @spinner_chars.size
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(stdout = STDOUT, stdin = STDIN)
|
61
|
+
@stdout = stdout
|
62
|
+
@stdin = stdin
|
63
|
+
@hl = HighLine.new
|
64
|
+
@max_length = 80
|
65
|
+
@line_char = '-'
|
66
|
+
@cell_char = '|'
|
67
|
+
@spinner = Spinner.new(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
def before_scenario_starts(scenario)
|
71
|
+
puts "Running #{scenario.description || scenario.class}"
|
72
|
+
hline
|
73
|
+
end
|
74
|
+
|
75
|
+
def before_execution_starts(execution)
|
76
|
+
@spinner.update(execution_info(execution, 'running'))
|
77
|
+
@spinner.activate
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_execution_update(execution, update)
|
81
|
+
@spinner.update(execution_info(execution, update))
|
82
|
+
end
|
83
|
+
|
84
|
+
def after_execution_finishes(execution)
|
85
|
+
@spinner.deactivate
|
86
|
+
cell(execution_info(execution, status_label(execution, 11), @max_length - 15))
|
87
|
+
cell(execution.output) if execution.fail?
|
88
|
+
hline
|
89
|
+
end
|
90
|
+
|
91
|
+
def after_scenario_finishes(_scenario); end
|
92
|
+
|
93
|
+
def on_next_steps(runner, steps)
|
94
|
+
choice = if steps.size > 1
|
95
|
+
multiple_steps_selection(steps)
|
96
|
+
elsif ask_to_confirm("Continue with step [#{steps.first.description}]?")
|
97
|
+
steps.first
|
98
|
+
else
|
99
|
+
:quit
|
100
|
+
end
|
101
|
+
choice == :quit ? runner.ask_to_quit : runner.add_step(choice)
|
102
|
+
end
|
103
|
+
|
104
|
+
def multiple_steps_selection(steps)
|
105
|
+
puts 'There are multiple steps to proceed:'
|
106
|
+
steps.each_with_index do |step, index|
|
107
|
+
puts "#{index + 1}) #{step.description}"
|
108
|
+
end
|
109
|
+
ask_to_select('Select step to continue', steps, &:description)
|
110
|
+
end
|
111
|
+
|
112
|
+
def ask_to_confirm(message)
|
113
|
+
print "#{message}, [yN]"
|
114
|
+
answer = @stdin.gets.chomp
|
115
|
+
case answer
|
116
|
+
when 'y'
|
117
|
+
true
|
118
|
+
when 'n'
|
119
|
+
false
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
clear_line
|
123
|
+
end
|
124
|
+
|
125
|
+
def ask_to_select(message, steps)
|
126
|
+
print "#{message}, (q) for quit"
|
127
|
+
answer = @stdin.gets.chomp
|
128
|
+
case answer
|
129
|
+
when 'q'
|
130
|
+
:quit
|
131
|
+
when /^\d+$/
|
132
|
+
steps[answer.to_i - 1]
|
133
|
+
end
|
134
|
+
ensure
|
135
|
+
clear_line
|
136
|
+
end
|
137
|
+
|
138
|
+
def clear_line
|
139
|
+
print "\r" + ' ' * @max_length + "\r"
|
140
|
+
end
|
141
|
+
|
142
|
+
def execution_info(execution, text, ljust = nil)
|
143
|
+
prefix = "#{execution.name}:"
|
144
|
+
prefix = prefix.ljust(ljust) if ljust
|
145
|
+
"#{prefix} #{text}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def status_label(execution, ljust)
|
149
|
+
mapping = { :success => { :label => '[OK]', :color => :green },
|
150
|
+
:fail => { :label => '[FAIL]', :color => :red },
|
151
|
+
:running => { :label => '[RUNNING]', :color => :blue },
|
152
|
+
:skipped => { :label => '[SKIPPED]', :color => :yellow } }
|
153
|
+
properties = mapping[execution.status]
|
154
|
+
@hl.color(properties[:label].ljust(ljust), properties[:color], :bold)
|
155
|
+
end
|
156
|
+
|
157
|
+
def hline
|
158
|
+
puts @line_char * @max_length
|
159
|
+
end
|
160
|
+
|
161
|
+
def cell(content)
|
162
|
+
print "#{@cell_char} #{content}".ljust(@max_length - 1)
|
163
|
+
puts @cell_char
|
164
|
+
end
|
165
|
+
|
166
|
+
def print(string)
|
167
|
+
@stdout.print(string)
|
168
|
+
@stdout.flush
|
169
|
+
end
|
170
|
+
|
171
|
+
def puts(string)
|
172
|
+
@stdout.puts(string)
|
173
|
+
@stdout.flush
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|