Project

General

Profile

Download (6.79 KB) Statistics
| Branch: | Tag: | Revision:
module ForemanMaintain
class UpgradeRunner < Runner
include Concerns::Finders

# Phases of the upgrade, see README.md for more info
PHASES = [:pre_upgrade_checks,
:pre_migrations,
:migrations,
:post_migrations,
:post_upgrade_checks].freeze

class << self
include Concerns::Finders

def available_targets
# when some upgrade is in progress, we don't allow upgrade to different version
return [current_target_version] if current_target_version
versions_to_tags.inject([]) do |available_targets, (version, tag)|
if !find_scenarios(:tags => [tag]).empty?
available_targets << version
else
available_targets
end
end.sort
end

def versions_to_tags
@versions_to_tags ||= {}
end

# registers target version to specific tag
def register_version(version, tag)
if versions_to_tags.key?(version) && versions_to_tags[version] != tag
raise "Version #{version} already registered to tag #{versions_to_tags[version]}"
end
@versions_to_tags[version] = tag
end

def clear_register
versions_to_tags.lear
end

def current_target_version
ForemanMaintain.storage[:upgrade_target_version]
end

def current_target_version=(value)
ForemanMaintain.storage.update_and_save(:upgrade_target_version => value)
end

def clear_current_target_version
ForemanMaintain.storage.update_and_save(:upgrade_target_version => nil)
end
end

attr_reader :version, :tag, :phase

def initialize(version, reporter, options = {})
super(reporter, [], options)
@tag = self.class.versions_to_tags[version]
raise "Unknown version #{version}" unless tag
@version = version
@scenario_cache = {}
self.phase = :pre_upgrade_checks
end

def scenario(phase)
return @scenario_cache[phase] if @scenario_cache.key?(phase)
condition = { :tags => [tag, phase] }
matching_scenarios = find_scenarios(condition)
raise "Too many scenarios match #{condition.inspect}" if matching_scenarios.size > 1
@scenario_cache[phase] = matching_scenarios.first
end

def run
self.class.current_target_version = @version
PHASES.each do |phase|
return run_rollback if quit?
if skip?(phase)
skip_phase(phase)
else
run_phase(phase)
end
end
unless quit?
finish_upgrade
end
ensure
update_current_target_version
end

def update_current_target_version
if phase == :pre_upgrade_checks || @finished
UpgradeRunner.clear_current_target_version
end
end

def run_rollback
# we only are able to rollback from pre_migrations phase
if phase == :pre_migrations
rollback_pre_migrations
end
end

def finish_upgrade
@finished = true
@reporter.hline
@reporter.puts <<-MESSAGE.strip_heredoc
Upgrade finished.
MESSAGE
end

def storage
ForemanMaintain.storage("upgrade_#{version}")
end

# serializes the state of the run to storage
def save
if @finished
storage.delete(:serialized)
else
storage[:serialized] = to_hash
end
storage.save
end

# deserializes the state of the run from the storage
def load
return unless storage[:serialized]
load_from_hash(storage[:serialized])
end

def run_phase(phase)
with_non_empty_scenario(phase) do |scenario|
confirm_scenario(scenario)
return if quit?
self.phase = phase
run_scenario(scenario, false)
# if we started from the :pre_upgrade_checks, ensure to ask before
# continuing with the rest of the upgrade
@ask_to_confirm_upgrade = phase == :pre_upgrade_checks
end
end

def skip_phase(skipped_phase)
with_non_empty_scenario(skipped_phase) do |scenario|
@reporter.before_scenario_starts(scenario)
@reporter.puts <<-MESSAGE.strip_heredoc
Skipping #{skipped_phase} phase as it was already run before.
To enforce to run the phase, use `upgrade run --phase #{skipped_phase}`
MESSAGE
@reporter.after_scenario_finishes(scenario)
end
end

private

# rubocop:disable Metrics/AbcSize
def rollback_pre_migrations
raise "Unexpected phase #{phase}, expecting pre_migrations" unless phase == :pre_migrations
rollback_needed = scenario(:pre_migrations).steps.any? { |s| s.executed? && s.success? }
if rollback_needed
@quit = false
@last_scenario = nil # to prevent the unnecessary confirmation questions
[:post_migrations, :post_upgrade_checks].each do |phase|
if quit? && phase == :post_upgrade_checks
self.phase = :pre_migrations
return # rubocop:disable Lint/NonLocalExitFromIterator
end
run_phase(phase)
end
end
self.phase = :pre_upgrade_checks # rollback finished
@reporter.puts <<-MESSAGE.strip_heredoc
The upgrade failed and system was restored to pre-upgrade state.
MESSAGE
end

def with_non_empty_scenario(phase)
next_scenario = scenario(phase)
unless next_scenario.nil? || next_scenario.steps.empty?
yield next_scenario
end
end

def to_hash
ret = { :phase => phase, :scenarios => {} }
@scenario_cache.each do |key, scenario|
ret[:scenarios][key] = scenario.to_hash
end
ret
end

def load_from_hash(hash)
unless @scenario_cache.empty?
raise "Some scenarios are already initialized: #{@scenario_cache.keys}"
end
self.phase = hash[:phase]
hash[:scenarios].each do |key, scenario_hash|
@scenario_cache[key] = Scenario.new_from_hash(scenario_hash)
end
end

def confirm_scenario(scenario)
decision = super(scenario)
# we have not asked the user already about next steps
if decision.nil? && @ask_to_confirm_upgrade
response = reporter.ask_decision(<<-MESSAGE.strip_heredoc.strip)
The pre-upgrade checks indicate that the system is ready for upgrade.
It's recommended to perform a backup at this stage.
Confirm to continue with the the modification part of the upgrade
MESSAGE
if [:no, :quit].include?(response)
ask_to_quit
end
end
response
ensure
@ask_to_confirm_upgrade = false
end

def skip?(next_phase)
# the next_phase was run before the current phase
PHASES.index(next_phase) < PHASES.index(phase)
end

def phase=(phase)
raise "Unknown phase #{phase}" unless PHASES.include?(phase)
@phase = phase
end
end
end
(17-17/20)