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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +193 -0
  4. data/bin/foreman-maintain +9 -0
  5. data/definitions/checks/foreman_tasks_not_paused.rb +14 -0
  6. data/definitions/checks/foreman_tasks_not_running.rb +10 -0
  7. data/definitions/features/downstream.rb +17 -0
  8. data/definitions/features/foreman_1_11_x.rb +7 -0
  9. data/definitions/features/foreman_1_7_x.rb +7 -0
  10. data/definitions/features/foreman_database.rb +15 -0
  11. data/definitions/features/foreman_tasks.rb +22 -0
  12. data/definitions/features/upstream.rb +7 -0
  13. data/definitions/procedures/foreman_tasks_resume.rb +13 -0
  14. data/definitions/scenarios/pre_upgrade_check_foreman_1_14.rb +12 -0
  15. data/definitions/scenarios/pre_upgrade_check_satellite_6_0_z.rb +12 -0
  16. data/definitions/scenarios/pre_upgrade_check_satellite_6_1.rb +12 -0
  17. data/definitions/scenarios/pre_upgrade_check_satellite_6_1_z.rb +12 -0
  18. data/definitions/scenarios/pre_upgrade_check_satellite_6_2.rb +12 -0
  19. data/definitions/scenarios/pre_upgrade_check_satellite_6_2_z.rb +12 -0
  20. data/definitions/scenarios/pre_upgrade_check_satellite_6_3.rb +12 -0
  21. data/lib/foreman_maintain.rb +54 -0
  22. data/lib/foreman_maintain/check.rb +34 -0
  23. data/lib/foreman_maintain/cli.rb +14 -0
  24. data/lib/foreman_maintain/cli/base.rb +47 -0
  25. data/lib/foreman_maintain/cli/health_command.rb +39 -0
  26. data/lib/foreman_maintain/cli/upgrade_command.rb +57 -0
  27. data/lib/foreman_maintain/concerns/finders.rb +69 -0
  28. data/lib/foreman_maintain/concerns/logger.rb +13 -0
  29. data/lib/foreman_maintain/concerns/metadata.rb +124 -0
  30. data/lib/foreman_maintain/concerns/system_helpers.rb +93 -0
  31. data/lib/foreman_maintain/config.rb +17 -0
  32. data/lib/foreman_maintain/detector.rb +146 -0
  33. data/lib/foreman_maintain/executable.rb +44 -0
  34. data/lib/foreman_maintain/feature.rb +22 -0
  35. data/lib/foreman_maintain/logger.rb +11 -0
  36. data/lib/foreman_maintain/procedure.rb +8 -0
  37. data/lib/foreman_maintain/reporter.rb +18 -0
  38. data/lib/foreman_maintain/reporter/cli_reporter.rb +177 -0
  39. data/lib/foreman_maintain/runner.rb +39 -0
  40. data/lib/foreman_maintain/runner/execution.rb +74 -0
  41. data/lib/foreman_maintain/scenario.rb +46 -0
  42. data/lib/foreman_maintain/top_level_modules.rb +13 -0
  43. data/lib/foreman_maintain/version.rb +3 -0
  44. 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,11 @@
1
+ require 'logger'
2
+
3
+ module ForemanMaintain
4
+ module Logger
5
+ def logger
6
+ @logger ||= ::Logger.new($stderr).tap do |logger|
7
+ logger.level = ForemanMaintain.config.log_level
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module ForemanMaintain
2
+ class Procedure < Executable
3
+ include Concerns::Logger
4
+ include Concerns::SystemHelpers
5
+ include Concerns::Metadata
6
+ include Concerns::Finders
7
+ end
8
+ 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