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