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)