From c55b258b63c8fae698f0fb8dee0ac3aa2f623073 Mon Sep 17 00:00:00 2001 From: Vinzenz Feenstra Date: Fri, 26 Mar 2021 14:55:16 +0100 Subject: [PATCH] cli: Add command rerun Per request this patch adds support for rerunning leapp from a certain point. As for now it only supports the FirstBoot phase to be re-run. In order to accomplish the re-run functionality leapp clones the context of the last upgrade execution and removes all checkpoints and errors from it. Cloning means it will duplicate all the context aware data in the database in order to allow a post mortem inspection of what has happened. So it will not blindly delete data but remove the errors and checkpoints only from the cloned part of the data. Additionally it allows to select only actors which have a certain tag specified during the re-run. This will help users that want to re-run their own set of actors only during the FirstBoot phase without having to create a complicated workaround using snactor and somehow getting it to use the data from the upgrade. This allows the actors that are re-run to consume messages that have been generated during the upgrade before this phase has been run. Signed-off-by: Vinzenz Feenstra --- leapp/cli/__init__.py | 2 +- leapp/cli/__main__.py | 1 + leapp/cli/upgrade/__init__.py | 89 +++++++++++++- leapp/utils/audit/contextclone.py | 84 +++++++++++++ leapp/workflows/__init__.py | 21 +++- .../ipu-rerun-repo/.leapp/info | 1 + .../ipu-rerun-repo/.leapp/leapp.conf | 6 + .../actors/firstbootactor/actor.py | 16 +++ .../actors/phaseaactor/actor.py | 16 +++ .../actors/phasebactor/actor.py | 16 +++ .../ipu-rerun-repo/actors/rerunactor/actor.py | 16 +++ .../actors/rerunactorother/actor.py | 16 +++ .../ipu-rerun-repo/tags/firstboot.py | 5 + .../tags/inplaceupgradeworkflow.py | 5 + .../ipu-rerun-repo/tags/phasea.py | 5 + .../ipu-rerun-repo/tags/phaseb.py | 5 + .../ipu-rerun-repo/tags/rerunverify.py | 5 + .../ipu-rerun-repo/tags/rerunverifyother.py | 5 + .../workflows/inplace_upgrade.py | 41 +++++++ .../leapp-rerun-tests-repos/noroot_leapp.py | 5 + tests/scripts/test_rerun.py | 113 ++++++++++++++++++ 21 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 leapp/utils/audit/contextclone.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/info create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/leapp.conf create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/firstbootactor/actor.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phaseaactor/actor.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phasebactor/actor.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactor/actor.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactorother/actor.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/firstboot.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/inplaceupgradeworkflow.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phasea.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phaseb.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverify.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverifyother.py create mode 100644 tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/workflows/inplace_upgrade.py create mode 100755 tests/data/leapp-rerun-tests-repos/noroot_leapp.py create mode 100644 tests/scripts/test_rerun.py diff --git a/leapp/cli/__init__.py b/leapp/cli/__init__.py index a09e624dd..95d5d9731 100644 --- a/leapp/cli/__init__.py +++ b/leapp/cli/__init__.py @@ -13,6 +13,6 @@ def cli(args): # noqa; pylint: disable=unused-argument def main(): os.environ['LEAPP_HOSTNAME'] = socket.getfqdn() - for cmd in [upgrade.list_runs, upgrade.preupgrade, upgrade.upgrade, upgrade.answer]: + for cmd in [upgrade.list_runs, upgrade.preupgrade, upgrade.upgrade, upgrade.answer, upgrade.rerun]: cli.command.add_sub(cmd.command) cli.command.execute('leapp version {}'.format(VERSION)) diff --git a/leapp/cli/__main__.py b/leapp/cli/__main__.py index e79c1523e..b3ae6dfb9 100644 --- a/leapp/cli/__main__.py +++ b/leapp/cli/__main__.py @@ -1,3 +1,4 @@ + from leapp.cli import main import leapp.utils.i18n # noqa: F401; pylint: disable=unused-import diff --git a/leapp/cli/upgrade/__init__.py b/leapp/cli/upgrade/__init__.py index a6a993544..4d29439bf 100644 --- a/leapp/cli/upgrade/__init__.py +++ b/leapp/cli/upgrade/__init__.py @@ -6,6 +6,7 @@ import sys import tarfile import uuid +from argparse import Namespace from datetime import datetime from leapp.config import get_config @@ -14,12 +15,16 @@ from leapp.messaging.answerstore import AnswerStore from leapp.repository.scan import find_and_scan_repositories from leapp.utils.audit import Execution, get_connection, get_checkpoints -from leapp.utils.clicmd import command, command_opt +from leapp.utils.audit.contextclone import clone_context +from leapp.utils.clicmd import command, command_arg, command_opt from leapp.utils.output import (report_errors, report_info, beautify_actor_exception, report_unsupported, report_inhibitors) from leapp.utils.report import fetch_upgrade_report_messages, generate_report_file +RERUN_SUPPORTED_PHASES = ('FirstBoot',) + + def archive_logfiles(): """ Archive log files from a previous run of Leapp """ cfg = get_config() @@ -67,13 +72,17 @@ def load_repositories(): return manager -def fetch_last_upgrade_context(): +def fetch_last_upgrade_context(use_context=None): """ :return: Context of the last execution """ with get_connection(None) as db: - cursor = db.execute( - "SELECT context, stamp, configuration FROM execution WHERE kind = 'upgrade' ORDER BY id DESC LIMIT 1") + if use_context: + cursor = db.execute( + "SELECT context, stamp, configuration FROM execution WHERE context = ?", (use_context,)) + else: + cursor = db.execute( + "SELECT context, stamp, configuration FROM execution WHERE kind = 'upgrade' ORDER BY id DESC LIMIT 1") row = cursor.fetchone() if row: return row[0], json.loads(row[2]) @@ -183,6 +192,68 @@ def process_whitelist_experimental(repositories, workflow, configuration, logger raise CommandError(msg) +@command('rerun', help='Re-runs the upgrade from the given phase and using the information and progress ' + 'from the last invocation of leapp upgrade.') +@command_arg('from-phase', + help='Phase to start running from again. Supported values: {}'.format(', '.join(RERUN_SUPPORTED_PHASES))) +@command_opt('only-actors-with-tag', action='append', metavar='TagName', + help='Restrict actors to be re-run only with given tags. Others will not be executed') +@command_opt('debug', is_flag=True, help='Enable debug mode', inherit=False) +@command_opt('verbose', is_flag=True, help='Enable verbose logging', inherit=False) +def rerun(args): + + if os.environ.get('LEAPP_UNSUPPORTED') != '1': + raise CommandError('This command requires the environment variable LEAPP_UNSUPPORTED="1" to be set!') + + if args.from_phase not in RERUN_SUPPORTED_PHASES: + raise CommandError('This command is only supported for {}'.format(', '.join(RERUN_SUPPORTED_PHASES))) + + context = str(uuid.uuid4()) + last_context, configuration = fetch_last_upgrade_context() + if args.from_phase not in set([chkpt['phase'] for chkpt in get_checkpoints(context=last_context)]): + raise CommandError('Phase {} has not been executed in the last leapp upgrade execution. ' + 'Cannot rerun not executed phase'.format(args.from_phase)) + + if not last_context: + raise CommandError('No previous upgrade run to rerun - ' + 'leapp upgrade has to be run before leapp rerun can be used') + + with get_connection(None) as db: + e = Execution(context=context, kind='rerun', configuration=configuration) + + e.store(db) + + clone_context(last_context, context, db) + db.execute(''' + DELETE FROM audit WHERE id IN ( + SELECT + audit.id AS id + FROM + audit + JOIN + data_source ON data_source.id = audit.data_source_id + WHERE + audit.context = ? AND audit.event = 'checkpoint' + AND data_source.phase LIKE 'FirstBoot%' + ); + ''', (context,)) + db.execute('''DELETE FROM message WHERE context = ? and type = 'ErrorModel';''', (context,)) + + archive_logfiles() + upgrade(Namespace( + resume=True, + resume_context=context, + only_with_tags=args.only_actors_with_tag or [], + debug=args.debug, + verbose=args.verbose, + reboot=False, + no_rhsm=False, + whitelist_experimental=[], + enablerepo=[])) + + +# If you are adding new parameters please ensure that they are set in the upgrade function invocation in `rerun` +# otherwise there might be errors. @command('upgrade', help='Upgrade the current system to the next available major version.') @command_opt('resume', is_flag=True, help='Continue the last execution after it was stopped (e.g. after reboot)') @command_opt('reboot', is_flag=True, help='Automatically performs reboot when requested.') @@ -202,11 +273,16 @@ def upgrade(args): answerfile_path = cfg.get('report', 'answerfile') userchoices_path = cfg.get('report', 'userchoices') + # Processing of parameters passed by the rerun call, these aren't actually command line arguments + # therefore we have to assume that they aren't even in `args` as they are added only by rerun. + only_with_tags = args.only_with_tags if 'only_with_tags' in args else None + resume_context = args.resume_context if 'resume_context' in args else None + if os.getuid(): raise CommandError('This command has to be run under the root user.') if args.resume: - context, configuration = fetch_last_upgrade_context() + context, configuration = fetch_last_upgrade_context(resume_context) if not context: raise CommandError('No previous upgrade run to continue, remove `--resume` from leapp invocation to' ' start a new upgrade flow') @@ -238,7 +314,8 @@ def upgrade(args): with beautify_actor_exception(): logger.info("Using answerfile at %s", answerfile_path) workflow.load_answers(answerfile_path, userchoices_path) - workflow.run(context=context, skip_phases_until=skip_phases_until, skip_dialogs=True) + workflow.run(context=context, skip_phases_until=skip_phases_until, skip_dialogs=True, + only_with_tags=only_with_tags) logger.info("Answerfile will be created at %s", answerfile_path) workflow.save_answers(answerfile_path, userchoices_path) diff --git a/leapp/utils/audit/contextclone.py b/leapp/utils/audit/contextclone.py new file mode 100644 index 000000000..8719b559c --- /dev/null +++ b/leapp/utils/audit/contextclone.py @@ -0,0 +1,84 @@ +from leapp.utils.audit import dict_factory, get_connection + + +def _fetch_table_for_context(db, table, context): + cursor = db.execute(''' + SELECT * FROM {table} WHERE context = ? + '''.format(table=table), (context,)) + cursor.row_factory = dict_factory + while True: + row = cursor.fetchone() + if not row: + break + yield row + del cursor + + +def _row_tuple(row, *fields): + return tuple([row[name] for name in fields or row.keys()]) + + +def _dup_host(db, newcontext, oldcontext): + lookup = {} + for row in _fetch_table_for_context(db, 'host', oldcontext): + # id, context, hostname + row_id, hostname = _row_tuple(row, 'id', 'hostname') + cursor = db.execute('INSERT INTO host (context, hostname) VALUES(?, ?)', + (newcontext, hostname)) + lookup[row_id] = cursor.lastrowid + return lookup + + +def _dup_data_source(db, host, newcontext, oldcontext): + lookup = {} + for row in _fetch_table_for_context(db, 'data_source', oldcontext): + # id, context, hostname + row_id, host_id, actor, phase = _row_tuple(row, 'id', 'host_id', 'actor', 'phase') + cursor = db.execute('INSERT INTO data_source (context, host_id, actor, phase) VALUES(?, ?, ?, ?)', + (newcontext, host[host_id], actor, phase)) + lookup[row_id] = cursor.lastrowid + return lookup + + +def _dup_message(db, data_source, newcontext, oldcontext): + lookup = {} + for row in _fetch_table_for_context(db, 'message', oldcontext): + # id, context, data_source_id, stamp, topic, type, message_data_hash + row_id, data_source_id, stamp, topic, type_, message_data_hash = _row_tuple( + row, 'id', 'data_source_id', 'stamp', 'topic', 'type', 'message_data_hash') + cursor = db.execute( + 'INSERT INTO message (context, data_source_id, stamp, topic, type, message_data_hash) ' + ' VALUES(?, ?, ?, ?, ?, ?)', + (newcontext, data_source[data_source_id], stamp, topic, type_, message_data_hash)) + lookup[row_id] = cursor.lastrowid + return lookup + + +def _dup_audit(db, message, data_source, newcontext, oldcontext): + lookup = {} + for row in _fetch_table_for_context(db, 'audit', oldcontext): + # id, context, event, stamp, data_source_id, message_id, data + row_id, event, stamp, data_source_id, message_id, data = _row_tuple( + row, 'id', 'event', 'stamp', 'data_source_id', 'message_id', 'data') + if message_id is not None: + message_id = message[message_id] + + cursor = db.execute( + 'INSERT INTO audit (context, event, stamp, data_source_id, message_id, data) VALUES(?, ?, ?, ?, ?, ?)', + (newcontext, event, stamp, data_source[data_source_id], message_id, data)) + lookup[row_id] = cursor.lastrowid + return lookup + + +def clone_context(oldcontext, newcontext, use_db=None): + # Enter transaction - In case of any exception automatic rollback is issued + # and it is automatically committed if there was no exception + with get_connection(use_db) as db: + # First clone host entries + host = _dup_host(db=db, newcontext=newcontext, oldcontext=oldcontext) + # Next clone data_source entries and use the lookup table generated by the host duplication + data_source = _dup_data_source(db=db, host=host, newcontext=newcontext, oldcontext=oldcontext) + # Next clone message entries and use the lookup table generated by the data_source duplication + message = _dup_message(db=db, data_source=data_source, newcontext=newcontext, oldcontext=oldcontext) + # Last clone message entries and use the lookup table generated by the data_source and message duplications + _dup_audit(db=db, data_source=data_source, message=message, newcontext=newcontext, oldcontext=oldcontext) diff --git a/leapp/workflows/__init__.py b/leapp/workflows/__init__.py index 7d8535e21..b909bb142 100644 --- a/leapp/workflows/__init__.py +++ b/leapp/workflows/__init__.py @@ -39,6 +39,17 @@ def phase_names(phase=None): return (phase[0].__name__.lower(), phase[0].name.lower()) if phase else () +def tag_names(tag=None): + return (tag.__name__.lower(), tag.name.lower()) if tag else () + + +def contains_tag(needle_tags, actor_tags): + hay = set() + for tag in actor_tags: + hay.update(tag_names(tag)) + return bool(hay.intersection([tag.lower() for tag in needle_tags])) + + class WorkflowMeta(type): """ Meta class for the registration of workflows @@ -230,7 +241,8 @@ def is_valid_phase(self, phase=None): if phase: return phase in [name for phs in self._phase_actors for name in phase_names(phs)] - def run(self, context=None, until_phase=None, until_actor=None, skip_phases_until=None, skip_dialogs=False): + def run(self, context=None, until_phase=None, until_actor=None, skip_phases_until=None, skip_dialogs=False, + only_with_tags=None): """ Executes the workflow @@ -255,6 +267,8 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti The value of skip_dialogs will be passed to the actors that can theoretically use it for their purposes. :type skip_dialogs: bool + :param only_with_tags: Executes only actors with the given tag, any other actor is going to get skipped. + :type only_with_tags: List[str] """ context = context or str(uuid.uuid4()) @@ -310,6 +324,11 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti current_logger.info("Skipping experimental actor {actor}".format(actor=actor.name)) continue + if only_with_tags and not contains_tag(only_with_tags, actor.tags): + current_logger.info( + "Actor {actor} does not contain any required tag. Skipping.".format(actor=actor.name)) + continue + display_status_current_actor(actor, designation=designation) current_logger.info("Executing actor {actor} {designation}".format(designation=designation, actor=actor.name)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/info b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/info new file mode 100644 index 000000000..212a15a2c --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/info @@ -0,0 +1 @@ +{"name": "ipu-rerun-repo", "id": "bae57f03-85e0-4dea-9a6d-88c40205c7a3"} \ No newline at end of file diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/leapp.conf b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/leapp.conf new file mode 100644 index 000000000..b4591347f --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/.leapp/leapp.conf @@ -0,0 +1,6 @@ + +[repositories] +repo_path=${repository:root_dir} + +[database] +path=${repository:state_dir}/leapp.db diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/firstbootactor/actor.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/firstbootactor/actor.py new file mode 100644 index 000000000..d2b7eb2c6 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/firstbootactor/actor.py @@ -0,0 +1,16 @@ +from leapp.actors import Actor +from leapp.tags import InplaceUpgradeWorkflowTag, FirstBootTag + + +class FirstBootActor(Actor): + """ + No documentation has been provided for the first_boot_actor actor. + """ + + name = 'first_boot_actor' + consumes = () + produces = () + tags = (InplaceUpgradeWorkflowTag, FirstBootTag) + + def process(self): + print('<<>>: {}'.format(self.__class__.__name__)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phaseaactor/actor.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phaseaactor/actor.py new file mode 100644 index 000000000..a47229407 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phaseaactor/actor.py @@ -0,0 +1,16 @@ +from leapp.actors import Actor +from leapp.tags import InplaceUpgradeWorkflowTag, PhaseATag + + +class PhaseAActor(Actor): + """ + No documentation has been provided for the phase_a_actor actor. + """ + + name = 'phase_a_actor' + consumes = () + produces = () + tags = (InplaceUpgradeWorkflowTag, PhaseATag) + + def process(self): + print('<<>>: {}'.format(self.__class__.__name__)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phasebactor/actor.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phasebactor/actor.py new file mode 100644 index 000000000..4be0759ed --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/phasebactor/actor.py @@ -0,0 +1,16 @@ +from leapp.actors import Actor +from leapp.tags import InplaceUpgradeWorkflowTag, PhaseBTag + + +class PhaseBActor(Actor): + """ + No documentation has been provided for the phase_b_actor actor. + """ + + name = 'phase_b_actor' + consumes = () + produces = () + tags = (InplaceUpgradeWorkflowTag, PhaseBTag) + + def process(self): + print('<<>>: {}'.format(self.__class__.__name__)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactor/actor.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactor/actor.py new file mode 100644 index 000000000..3e4cd0dcd --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactor/actor.py @@ -0,0 +1,16 @@ +from leapp.actors import Actor +from leapp.tags import InplaceUpgradeWorkflowTag, FirstBootTag, ReRunVerifyTag + + +class ReRunActor(Actor): + """ + No documentation has been provided for the re_run_actor actor. + """ + + name = 're_run_actor' + consumes = () + produces = () + tags = (InplaceUpgradeWorkflowTag, FirstBootTag, ReRunVerifyTag) + + def process(self): + print('<<>>: {}'.format(self.__class__.__name__)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactorother/actor.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactorother/actor.py new file mode 100644 index 000000000..13a843861 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/actors/rerunactorother/actor.py @@ -0,0 +1,16 @@ +from leapp.actors import Actor +from leapp.tags import InplaceUpgradeWorkflowTag, FirstBootTag, ReRunVerifyOtherTag + + +class ReRunActorOther(Actor): + """ + No documentation has been provided for the re_run_actor_other actor. + """ + + name = 're_run_actor_other' + consumes = () + produces = () + tags = (InplaceUpgradeWorkflowTag, FirstBootTag, ReRunVerifyOtherTag) + + def process(self): + print('<<>>: {}'.format(self.__class__.__name__)) diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/firstboot.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/firstboot.py new file mode 100644 index 000000000..888979f74 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/firstboot.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class FirstBootTag(Tag): + name = 'first_boot' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/inplaceupgradeworkflow.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/inplaceupgradeworkflow.py new file mode 100644 index 000000000..97b6bac6d --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/inplaceupgradeworkflow.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class InplaceUpgradeWorkflowTag(Tag): + name = 'inplace_upgrade_workflow' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phasea.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phasea.py new file mode 100644 index 000000000..d90ee25d1 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phasea.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class PhaseATag(Tag): + name = 'phase_a' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phaseb.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phaseb.py new file mode 100644 index 000000000..e79e0928d --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/phaseb.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class PhaseBTag(Tag): + name = 'phase_b' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverify.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverify.py new file mode 100644 index 000000000..8d0a805af --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverify.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class ReRunVerifyTag(Tag): + name = 're_run_verify' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverifyother.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverifyother.py new file mode 100644 index 000000000..782ed0cdc --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/tags/rerunverifyother.py @@ -0,0 +1,5 @@ +from leapp.tags import Tag + + +class ReRunVerifyOtherTag(Tag): + name = 're_run_verify_other' diff --git a/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/workflows/inplace_upgrade.py b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/workflows/inplace_upgrade.py new file mode 100644 index 000000000..e4750eba4 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/ipu-rerun-repo/workflows/inplace_upgrade.py @@ -0,0 +1,41 @@ +from leapp.workflows import Workflow +from leapp.workflows.phases import Phase +from leapp.workflows.flags import Flags +from leapp.workflows.tagfilters import TagFilter +from leapp.workflows.policies import Policies +from leapp.tags import InplaceUpgradeWorkflowTag, PhaseATag, PhaseBTag, FirstBootTag + + +class IPUWorkflow(Workflow): + name = 'IPUWorkflow' + tag = InplaceUpgradeWorkflowTag + short_name = 'ipu' + description = '''No description has been provided for the InplaceUpgrade workflow.''' + + # Template for phase definition - The order in which the phase classes are defined + # within the Workflow class represents the execution order + # + # class PhaseName(Phase): + # name = 'phase_name' + # filter = TagFilter(PhaseTag) + # policies = Policies(Policies.Errors.FailPhase, + # Policies.Retry.Phase) + # flags = Flags() + + class PhaseA(Phase): + name = 'phase_a' + filter = TagFilter(PhaseATag) + policies = Policies(Policies.Errors.FailPhase, Policies.Retry.Phase) + flags = Flags() + + class PhaseB(Phase): + name = 'phase_b' + filter = TagFilter(PhaseBTag) + policies = Policies(Policies.Errors.FailPhase, Policies.Retry.Phase) + flags = Flags() + + class FirstBoot(Phase): + name = 'FirstBoot' + filter = TagFilter(FirstBootTag) + policies = Policies(Policies.Errors.FailPhase, Policies.Retry.Phase) + flags = Flags() diff --git a/tests/data/leapp-rerun-tests-repos/noroot_leapp.py b/tests/data/leapp-rerun-tests-repos/noroot_leapp.py new file mode 100755 index 000000000..560ab2ad4 --- /dev/null +++ b/tests/data/leapp-rerun-tests-repos/noroot_leapp.py @@ -0,0 +1,5 @@ +import leapp.cli +import os + +os.getuid = lambda: 0 +leapp.cli.main() diff --git a/tests/scripts/test_rerun.py b/tests/scripts/test_rerun.py new file mode 100644 index 000000000..1b26e4585 --- /dev/null +++ b/tests/scripts/test_rerun.py @@ -0,0 +1,113 @@ +import os +import subprocess +import sys + +import pytest +from six import PY3 + + +def _evaluate_results(results, *expected): + if PY3: + results = results.decode('utf-8') + expected = set(expected) + lines = set([line.split()[-1] for line in results.split('\n') if line.startswith('<<>>: ')]) + assert lines == expected + + +_LEAPP_RERUN_CONFIG = None +_LEAPP_DB_PATH = None + + +def setup_module(): + os.environ['PYTHONDONTWRITEBYTECODE'] = '1' + + +@pytest.fixture(autouse=True, scope='module') +def create_rerun_tempdir(tmpdir_factory): + global _LEAPP_RERUN_CONFIG + global _LEAPP_DB_PATH + tmpdir = tmpdir_factory.mktemp('leapp-rerun') + _LEAPP_DB_PATH = str(tmpdir.join('leapp.db')) + config = tmpdir.join('leappconfig') + repo_test_path = os.path.join(os.path.dirname(os.path.dirname( + os.path.realpath(__file__))), 'data/leapp-rerun-tests-repos') + _LEAPP_RERUN_CONFIG = str(config) + config.write(''' +[repositories] +repo_path={repo_test_path} + +[database] +path={test_data_path}/leapp.db + +[archive] +dir={test_data_path}/logs/archive + +[files_to_archive] +dir={test_data_path}/logs/archive +files=leapp-upgrade.log,leapp-report.json,leapp-report.txt + +[logs] +dir={test_data_path}/logs +files=leapp-upgrade.log,leapp-preupgrade.log + +[report] +dir={test_data_path}/logs +files=leapp-report.json,leapp-report.txt +answerfile={test_data_path}/logs/answerfile +userchoices={test_data_path}/logs/answerfile.userchoices + '''.format(test_data_path=str(tmpdir), repo_test_path=repo_test_path)) + + +def _perform_rerun(tags=(), unsupported=True, run_upgrade=True): + + leapp_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + 'data/leapp-rerun-tests-repos/noroot_leapp.py') + env = {'LEAPP_CONFIG': _LEAPP_RERUN_CONFIG} + if os.path.exists(_LEAPP_DB_PATH): + os.unlink(_LEAPP_DB_PATH) + args = [] + + for tag in tags: + args.extend(['--only-actors-with-tag', tag]) + + try: + os.environ.update(env) + if run_upgrade: + subprocess.check_call([sys.executable, leapp_path, 'upgrade'], env=os.environ) + if unsupported: + env['LEAPP_UNSUPPORTED'] = '1' + os.environ.update(env) + return subprocess.check_output([sys.executable, leapp_path, 'rerun'] + args + ['FirstBoot'], env=os.environ) + finally: + os.environ.pop('LEAPP_CONFIG', None) + os.environ.pop('LEAPP_UNSUPPORTED', None) + + +def test_leapp_rerun_no_unsupported_environment_var(): + with pytest.raises(subprocess.CalledProcessError): + _perform_rerun(unsupported=False, run_upgrade=False) + + +def test_leapp_rerun_no_upgrade(): + with pytest.raises(subprocess.CalledProcessError): + _perform_rerun(run_upgrade=False) + + +def test_leapp_rerun_no_parameters(): + result = _perform_rerun(run_upgrade=True) + _evaluate_results(result, 'FirstBootActor', 'ReRunActor', 'ReRunActorOther') + + +def test_leapp_rerun_only_actors_with_tag(): + result = _perform_rerun(tags=('ReRunVerifyTag',), run_upgrade=True) + _evaluate_results(result, 'ReRunActor') + + +def test_leapp_rerun_only_actors_with_multiple_tags(): + result = _perform_rerun(tags=('ReRunVerifyTag', 'ReRunVerifyOtherTag'), run_upgrade=True) + _evaluate_results(result, 'ReRunActor', 'ReRunActorOther') + + +def test_leapp_rerun_only_actors_with_not_used_tag(): + result = _perform_rerun(tags=('NoSuchTag',), run_upgrade=True) + _evaluate_results(result)