From 0f8750e38fb57c19e31b5af85fa6f78e5ee7659e Mon Sep 17 00:00:00 2001 From: Rodolfo Olivieri Date: Thu, 19 Sep 2024 08:15:14 -0300 Subject: [PATCH] [RHELC-1507] Port environment variables to config file (#1272) * Modify convert2rhel.ini with new fields and values * Split toolopts from cli both in module as code In the past, we used to have this portion of code all together inside the toolopts module, making it very difficult to understand and improve as the separation was not intuitive enough. With this patch, we are splitting the cli related functions and classes to its own module, making it an entrypoint for everything related to CLI usage. Toolopts also got its own module to further separate the logic between cli stuff and options stuff. Historically we call all options from our source code as "toolopts", so to preserve the naming convention and minimize as much as possible the changes throughout the codebase, the same name was preserved. A new addition was also made which is the config module. This module is responsible to gather all the configuration options we could ever define for the tool. Currently, it only has CLIConfig and FileConfig, which are the two supported options. Toolopts, at this point, is our final class who will hold all the options that are composed from the cli and config file (being able to be extended in the future to other options). * Move code to their related utils modules A couple of functions and constants were moved to their related utils modules to get them out of the way from circle dependency and make the codebase a bit more clear * Update code reference about toolopts and cli on codebase This patch introduces the updated references to both toolopts and cli throughout the codebase as some of the things we had before could be changed to the new format, especially because no code get executed before the CLI class parse all the options necessary. We are not safe to call tool_opts even if nothing is populated inside the class. Beware that it will still fail if we try to access any attributes before they get created, but that should be a implementation problem, not really a workflow one, since we expect that the CLI get executed very early in the process. * Update man script and vulture script Add a couple of missing variables to the vulture whitelist script so the hook can stop warning about unused variable. * Update the unit_tests to reflect the changes in the refactor * Integration tests fixup * Fix convert2rhel.ini with defaults values and parsing Parsing the values for inhibitor_overrides now convert it to a boolean value by default. --- config/convert2rhel.ini | 12 + .../post_conversion/kernel_boot_files.py | 2 + .../modified_rpm_files_diff.py | 7 +- .../actions/pre_ponr_changes/backup_system.py | 2 +- convert2rhel/breadcrumbs.py | 5 +- convert2rhel/cli.py | 322 ++++++++ convert2rhel/logger.py | 7 +- convert2rhel/main.py | 13 +- convert2rhel/subscription.py | 3 +- convert2rhel/systeminfo.py | 3 +- convert2rhel/toolopts.py | 666 ---------------- convert2rhel/toolopts/__init__.py | 147 ++++ convert2rhel/toolopts/config.py | 319 ++++++++ convert2rhel/unit_tests/__init__.py | 5 +- .../conversion/lock_releasever_test.py | 5 + .../preserve_only_rhel_kernel_test.py | 5 + .../post_conversion/kernel_boot_files_test.py | 76 +- .../modified_rpm_files_diff_test.py | 9 +- .../pre_ponr_changes/backup_system_test.py | 2 +- .../pre_ponr_changes/handle_packages_test.py | 7 +- .../pre_ponr_changes/subscription_test.py | 12 +- .../custom_repos_are_valid_test.py | 5 + .../actions/system_checks/els_test.py | 5 + .../actions/system_checks/eus_test.py | 5 + .../is_loaded_kernel_latest_test.py | 5 + .../unit_tests/backup/packages_test.py | 7 +- .../unit_tests/backup/subscription_test.py | 10 +- convert2rhel/unit_tests/breadcrumbs_test.py | 8 +- convert2rhel/unit_tests/checks_test.py | 6 - convert2rhel/unit_tests/cli_test.py | 631 +++++++++++++++ convert2rhel/unit_tests/conftest.py | 39 +- convert2rhel/unit_tests/logger_test.py | 24 - convert2rhel/unit_tests/main_test.py | 58 +- convert2rhel/unit_tests/pkghandler_test.py | 24 +- .../unit_tests/pkgmanager/pkgmanager_test.py | 29 +- convert2rhel/unit_tests/subscription_test.py | 5 + convert2rhel/unit_tests/systeminfo_test.py | 3 +- convert2rhel/unit_tests/toolopts/__init__.py | 0 .../unit_tests/toolopts/config_test.py | 293 +++++++ .../unit_tests/toolopts/toolopts_test.py | 356 +++++++++ convert2rhel/unit_tests/toolopts_test.py | 716 ------------------ convert2rhel/unit_tests/utils/__init__.py | 0 .../unit_tests/utils/subscription_test.py | 65 ++ .../unit_tests/{ => utils}/utils_test.py | 12 +- convert2rhel/utils/__init__.py | 5 +- convert2rhel/utils/rpm.py | 22 + convert2rhel/utils/subscription.py | 137 ++++ man/__init__.py | 4 +- scripts/whitelist.py | 2 + .../config-file/test_config_file.py | 32 +- 50 files changed, 2560 insertions(+), 1577 deletions(-) create mode 100644 convert2rhel/cli.py delete mode 100644 convert2rhel/toolopts.py create mode 100644 convert2rhel/toolopts/__init__.py create mode 100644 convert2rhel/toolopts/config.py create mode 100644 convert2rhel/unit_tests/cli_test.py create mode 100644 convert2rhel/unit_tests/toolopts/__init__.py create mode 100644 convert2rhel/unit_tests/toolopts/config_test.py create mode 100644 convert2rhel/unit_tests/toolopts/toolopts_test.py delete mode 100644 convert2rhel/unit_tests/toolopts_test.py create mode 100644 convert2rhel/unit_tests/utils/__init__.py create mode 100644 convert2rhel/unit_tests/utils/subscription_test.py rename convert2rhel/unit_tests/{ => utils}/utils_test.py (98%) create mode 100644 convert2rhel/utils/rpm.py create mode 100644 convert2rhel/utils/subscription.py diff --git a/config/convert2rhel.ini b/config/convert2rhel.ini index ea2ade660b..04054fbc43 100644 --- a/config/convert2rhel.ini +++ b/config/convert2rhel.ini @@ -11,3 +11,15 @@ # password = # activation_key = # org = + +[host_metering] +# Possible values here are "auto" or "force" +# configure_host_metering = "auto" + +[inhibitor_overrides] +# incomplete_rollback = false +# tainted_kernel_module_check_skip = false +# outdated_package_check_skip = false +# allow_older_version = false +# allow_unavailable_kmods = false +# skip_kernel_currency_check = false diff --git a/convert2rhel/actions/post_conversion/kernel_boot_files.py b/convert2rhel/actions/post_conversion/kernel_boot_files.py index 7382b6157c..5c1d400ebd 100644 --- a/convert2rhel/actions/post_conversion/kernel_boot_files.py +++ b/convert2rhel/actions/post_conversion/kernel_boot_files.py @@ -54,8 +54,10 @@ def run(self): # from `uname -r`, as it requires a reboot in order to take place, we are # detecting the latest kernel by using `rpm` and figuring out which was the # latest kernel installed. + latest_installed_kernel = output.split("\n")[0].split(" ")[0] latest_installed_kernel = latest_installed_kernel[len(kernel_name + "-") :] + grub2_config_file = grub.get_grub_config_file() initramfs_file = INITRAMFS_FILEPATH % latest_installed_kernel vmlinuz_file = VMLINUZ_FILEPATH % latest_installed_kernel diff --git a/convert2rhel/actions/post_conversion/modified_rpm_files_diff.py b/convert2rhel/actions/post_conversion/modified_rpm_files_diff.py index 7ad1ebbff2..7d7025a4e1 100644 --- a/convert2rhel/actions/post_conversion/modified_rpm_files_diff.py +++ b/convert2rhel/actions/post_conversion/modified_rpm_files_diff.py @@ -21,7 +21,6 @@ from convert2rhel import actions, utils from convert2rhel.logger import LOG_DIR, root_logger from convert2rhel.systeminfo import system_info -from convert2rhel.toolopts import POST_RPM_VA_LOG_FILENAME, PRE_RPM_VA_LOG_FILENAME logger = root_logger.getChild(__name__) @@ -39,9 +38,9 @@ def run(self): logger.task("Final: Show RPM files modified by the conversion") - system_info.generate_rpm_va(log_filename=POST_RPM_VA_LOG_FILENAME) + system_info.generate_rpm_va(log_filename=utils.rpm.POST_RPM_VA_LOG_FILENAME) - pre_rpm_va_log_path = os.path.join(LOG_DIR, PRE_RPM_VA_LOG_FILENAME) + pre_rpm_va_log_path = os.path.join(LOG_DIR, utils.rpm.PRE_RPM_VA_LOG_FILENAME) if not os.path.exists(pre_rpm_va_log_path): logger.info("Skipping comparison of the 'rpm -Va' output from before and after the conversion.") self.add_message( @@ -55,7 +54,7 @@ def run(self): return pre_rpm_va = utils.get_file_content(pre_rpm_va_log_path, True) - post_rpm_va_log_path = os.path.join(LOG_DIR, POST_RPM_VA_LOG_FILENAME) + post_rpm_va_log_path = os.path.join(LOG_DIR, utils.rpm.POST_RPM_VA_LOG_FILENAME) post_rpm_va = utils.get_file_content(post_rpm_va_log_path, True) modified_rpm_files_diff = "\n".join( difflib.unified_diff( diff --git a/convert2rhel/actions/pre_ponr_changes/backup_system.py b/convert2rhel/actions/pre_ponr_changes/backup_system.py index 2635936d1e..16b8052735 100644 --- a/convert2rhel/actions/pre_ponr_changes/backup_system.py +++ b/convert2rhel/actions/pre_ponr_changes/backup_system.py @@ -26,7 +26,7 @@ from convert2rhel.redhatrelease import os_release_file, system_release_file from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR from convert2rhel.systeminfo import system_info -from convert2rhel.toolopts import PRE_RPM_VA_LOG_FILENAME +from convert2rhel.utils.rpm import PRE_RPM_VA_LOG_FILENAME # Regex explanation: diff --git a/convert2rhel/breadcrumbs.py b/convert2rhel/breadcrumbs.py index a2852d1fe4..64e3862821 100644 --- a/convert2rhel/breadcrumbs.py +++ b/convert2rhel/breadcrumbs.py @@ -24,9 +24,10 @@ from datetime import datetime -from convert2rhel import pkghandler, toolopts, utils +from convert2rhel import pkghandler, utils from convert2rhel.logger import root_logger from convert2rhel.systeminfo import system_info +from convert2rhel.toolopts import tool_opts from convert2rhel.utils import files @@ -104,7 +105,7 @@ def finish_collection(self, success=False): def _set_activity(self): """Set the activity that convert2rhel is going to perform""" - self.activity = toolopts.tool_opts.activity + self.activity = tool_opts.activity def _set_pkg_object(self): """Set pkg_object which is used to get information about installed Convert2RHEL""" diff --git a/convert2rhel/cli.py b/convert2rhel/cli.py new file mode 100644 index 0000000000..8460181291 --- /dev/null +++ b/convert2rhel/cli.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2024 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + + +import argparse +import logging +import sys + +from convert2rhel import __version__, utils +from convert2rhel.toolopts import tool_opts +from convert2rhel.toolopts.config import CliConfig, FileConfig + + +loggerinst = logging.getLogger(__name__) + +ARGS_WITH_VALUES = [ + "-u", + "--username", + "-p", + "--password", + "-k", + "--activationkey", + "-o", + "--org", + "--pool", + "--serverurl", +] +PARENT_ARGS = ["--debug", "--help", "-h", "--version"] + + +class CLI: + def __init__(self): + self._parser = self._get_argparser() + self._shared_options_parser = argparse.ArgumentParser(add_help=False) + # Duplicating parent options here as we want to make it + # available for any other basic operation that we run without a + # subcommand in mind, and, it is a shared option so we can share it + # between any subcommands we may create in the future. + self._register_parent_options(self._parser) + self._register_parent_options(self._shared_options_parser) + self._register_options() + self._process_cli_options() + + @staticmethod + def usage(subcommand_to_print=""): + # Override the subcommand_to_print parameter if the tool has been executed through CLI but without + # subcommand specified. This is to make sure that runnning `convert2rhel --help` on the CLI will print the + # usage with generic , while the manpage generated using argparse_manpage will be able to print the + # usage correctly for subcommands as it does not execute convert2rhel from the CLI. + subcommand_not_used_on_cli = "/usr/bin/convert2rhel" in sys.argv[0] and not _subcommand_used(sys.argv) + if subcommand_not_used_on_cli: + subcommand_to_print = "" + usage = ( + "\n" + " convert2rhel [--version] [-h]\n" + " convert2rhel {subcommand} [-u username] [-p password | -c conf_file_path] [--pool pool_id | -a] [--disablerepo repoid]" + " [--enablerepo repoid] [--serverurl url] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" + " convert2rhel {subcommand} [--no-rhsm] [--disablerepo repoid] [--enablerepo repoid] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" + " convert2rhel {subcommand} [-k activation_key | -c conf_file_path] [-o organization] [--pool pool_id | -a] [--disablerepo repoid] [--enablerepo" + " repoid] [--serverurl url] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" + ).format(subcommand=subcommand_to_print) + + if subcommand_not_used_on_cli: + usage = usage + "\n Subcommands: analyze, convert" + return usage + + def _get_argparser(self): + return argparse.ArgumentParser(conflict_handler="resolve", usage=self.usage()) + + def _register_commands(self): + """Configures parsers specific to the analyze and convert subcommands""" + subparsers = self._parser.add_subparsers(title="Subcommands", dest="command") + self._analyze_parser = subparsers.add_parser( + "analyze", + help="Run all Convert2RHEL initial checks up until the" + " Point of no Return (PONR) and generate a report with the findings." + " A rollback is initiated after the checks to put the system back" + " in the original state.", + parents=[self._shared_options_parser], + usage=self.usage(subcommand_to_print="analyze"), + ) + self._convert_parser = subparsers.add_parser( + "convert", + help="Convert the system. If no subcommand is given, 'convert' is used as a default.", + parents=[self._shared_options_parser], + usage=self.usage(subcommand_to_print="convert"), + ) + + @staticmethod + def _register_parent_options(parser): + """Prescribe what parent command line options the tool accepts.""" + parser.add_argument( + "--version", + action="version", + version=__version__, + help="Show convert2rhel version and exit.", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Print traceback in case of an abnormal exit and messages that could help find an issue.", + ) + + def _register_options(self): + """Prescribe what command line options the tool accepts.""" + self._parser.add_argument( + "-h", + "--help", + action="help", + help="Show help message and exit.", + ) + self._shared_options_parser.add_argument( + "--no-rpm-va", + action="store_true", + help="Skip gathering changed rpm files using" + " 'rpm -Va'. By default it's performed before and after the conversion with the output" + " stored in log files %s and %s. At the end of the conversion, these logs are compared" + " to show you what rpm files have been affected by the conversion." + " Cannot be used with analyze subcommand." + " The environment variable CONVERT2RHEL_INCOMPLETE_ROLLBACK" + " needs to be set to 1 to use this argument." + % (utils.rpm.PRE_RPM_VA_LOG_FILENAME, utils.rpm.POST_RPM_VA_LOG_FILENAME), + ) + self._shared_options_parser.add_argument( + "--eus", + action="store_true", + help="Explicitly recognize the system as eus, utilizing eus repos." + " This option is meant for el8.8+ systems.", + ) + self._shared_options_parser.add_argument( + "--els", + action="store_true", + help="Explicitly recognize the system as els, utilizing els repos." + " This option is meant for el7 systems.", + ) + self._shared_options_parser.add_argument( + "--enablerepo", + metavar="repoidglob", + action="append", + help="Enable specific" + " repositories by ID or glob. For more repositories to enable, use this option" + " multiple times. If you don't use the --no-rhsm option, you can use this option" + " to override the default RHEL repoids that convert2rhel enables through" + " subscription-manager.", + ) + self._shared_options_parser.add_argument( + "--disablerepo", + metavar="repoidglob", + action="append", + help="Disable specific" + " repositories by ID or glob. For more repositories to disable, use this option" + " multiple times. This option defaults to all repositories ('*').", + ) + self._shared_options_parser.add_argument( + "-r", + "--restart", + help="Restart the system when it is successfully converted to RHEL to boot the new RHEL kernel." + " It has no effect when used with the 'analyze' subcommand.", + action="store_true", + ) + self._shared_options_parser.add_argument( + "-y", + help="Answer yes to all yes/no questions the tool asks.", + dest="auto_accept", + action="store_true", + ) + self._add_subscription_manager_options() + self._add_alternative_installation_options() + self._register_commands() + + def _add_alternative_installation_options(self): + """Prescribe what alternative command line options the tool accepts.""" + group = self._shared_options_parser.add_argument_group( + title="Alternative Installation Options", + description="The following options are required if you do not intend on using subscription-manager.", + ) + group.add_argument( + "--no-rhsm", + action="store_true", + help="Do not use the subscription-manager, use custom repositories instead. See --enablerepo/--disablerepo" + " options. Without this option, the subscription-manager is used to access RHEL repositories by default." + " Using this option requires to have the --enablerepo specified.", + ) + + def _add_subscription_manager_options(self): + """Prescribe what subscription manager command line options the tool accepts.""" + group = self._shared_options_parser.add_argument_group( + title="Subscription Manager Options", + description="The following options are specific to using subscription-manager.", + ) + group.add_argument( + "-u", + "--username", + help="Username for the" + " subscription-manager. If neither --username nor" + " --activation-key option is used, the user" + " is asked to enter the username.", + ) + group.add_argument( + "-p", + "--password", + help="Password for the" + " subscription-manager. If --password, --config-file or --activationkey are not" + " used, the user is asked to enter the password." + " We recommend using the --config-file option instead to prevent leaking the password" + " through a list of running processes.", + ) + group.add_argument( + "-k", + "--activationkey", + help="Activation key used" + " for the system registration by the" + " subscription-manager. It requires to have the --org" + " option specified." + " We recommend using the --config-file option instead to prevent leaking the activation key" + " through a list of running processes.", + dest="activation_key", + ) + group.add_argument( + "-o", + "--org", + help="Organization with which the" + " system will be registered by the" + " subscription-manager. A list of available" + " organizations is possible to obtain by running" + " 'subscription-manager orgs'. From the listed pairs" + " Name:Key, use the Key here.", + ) + group.add_argument( + "-c", + "--config-file", + help="The configuration file is an optional way to safely pass either a user password or an activation key" + " to the subscription-manager to register the system. This is more secure than passing these values" + " through the --activationkey or --password option, which might leak the values" + " through a list of running processes." + " You can edit the pre-installed configuration file template at /etc/convert2rhel.ini or create a new" + " configuration file at ~/.convert2rhel.ini. The convert2rhel utility loads the configuration from either" + " of those locations, the latter having preference over the former. Alternatively, you can specify a path" + " to the configuration file using the --config-file option to override other configurations.", + ) + group.add_argument( + "-a", + "--auto-attach", + help="Automatically attach compatible subscriptions to the system.", + action="store_true", + ) + group.add_argument( + "--pool", + help="Subscription pool ID. A list of the available" + " subscriptions is possible to obtain by running" + " 'subscription-manager list --available'." + " If no pool ID is provided, the --auto option is used", + ) + group.add_argument( + "--serverurl", + help="Hostname of the subscription service to be used when registering the system with" + " subscription-manager. The default is the Customer Portal Subscription Management service" + " (subscription.rhsm.redhat.com). It is not to be used to specify a Satellite server. For that, read" + " the product documentation at https://access.redhat.com/.", + ) + + def _process_cli_options(self): + """Process command line options used with the tool.""" + _log_command_used() + + # algorithm function to properly organize all CLI args + argv = _add_default_command(sys.argv[1:]) + parsed_opts = self._parser.parse_args(argv) + + tool_opts.initialize( + config_sources=( + FileConfig(parsed_opts.config_file), + CliConfig(parsed_opts), + ) + ) + + +def _log_command_used(): + """We want to log the command used for convert2rhel to make it easier to know what command was used + when debugging the log files. Since we can't differentiate between the handlers we log to both stdout + and the logfile + """ + command = " ".join(utils.hide_secrets(sys.argv)) + loggerinst.info("convert2rhel command used:\n{0}".format(command)) + + +def _add_default_command(argv): + """Add the default command when none is given""" + subcommand = _subcommand_used(argv) + args = argv + if not subcommand: + args.insert(0, "convert") + + return args + + +def _subcommand_used(args): + """Return what subcommand has been used by the user. Return None if no subcommand has been used.""" + for index, argument in enumerate(args): + if argument in ("convert", "analyze"): + return argument + + if not argument in PARENT_ARGS and args[index - 1] in ARGS_WITH_VALUES: + return None + + return None diff --git a/convert2rhel/logger.py b/convert2rhel/logger.py index 12d009faed..2f3859ca2e 100644 --- a/convert2rhel/logger.py +++ b/convert2rhel/logger.py @@ -225,12 +225,7 @@ def _critical(self, msg, *args, **kwargs): def _debug(self, msg, *args, **kwargs): if self.isEnabledFor(logging.DEBUG): - from convert2rhel.toolopts import tool_opts - - if tool_opts.debug: - self._log(logging.DEBUG, msg, args, **kwargs) - else: - self._log(LogLevelFile.level, msg, args, **kwargs) + self._log(logging.DEBUG, msg, args, **kwargs) class bcolors: diff --git a/convert2rhel/main.py b/convert2rhel/main.py index dbeca425b1..c62a266900 100644 --- a/convert2rhel/main.py +++ b/convert2rhel/main.py @@ -20,10 +20,11 @@ import os -from convert2rhel import actions, applock, backup, breadcrumbs, exceptions +from convert2rhel import actions, applock, backup, breadcrumbs, cli, exceptions from convert2rhel import logger as logger_module -from convert2rhel import pkghandler, pkgmanager, subscription, systeminfo, toolopts, utils +from convert2rhel import pkghandler, pkgmanager, subscription, systeminfo, utils from convert2rhel.actions import level_for_raw_action_data, report +from convert2rhel.toolopts import tool_opts loggerinst = logger_module.root_logger.getChild(__name__) @@ -110,7 +111,7 @@ def main(): """ # handle command line arguments - toolopts.CLI() + cli.CLI() # Make sure we're being run by root utils.require_root() @@ -148,7 +149,7 @@ def main_locked(): process_phase = ConversionPhase.PRE_PONR_CHANGES pre_conversion_results = actions.run_pre_actions() - if toolopts.tool_opts.activity == "analysis": + if tool_opts.activity == "analysis": process_phase = ConversionPhase.ANALYZE_EXIT raise _AnalyzeExit() @@ -242,7 +243,7 @@ def _raise_for_skipped_failures(results): "The {method} process failed.\n\n" "A problem was encountered during {method} and a rollback will be " "initiated to restore the system as the previous state." - ).format(method=toolopts.tool_opts.activity) + ).format(method=tool_opts.activity) raise _InhibitorsFound(message) @@ -264,7 +265,7 @@ def _handle_main_exceptions(process_phase, results=None): breadcrumbs.breadcrumbs.finish_collection() no_changes_msg = "No changes were made to the system." - utils.log_traceback(toolopts.tool_opts.debug) + utils.log_traceback(tool_opts.debug) if process_phase == ConversionPhase.POST_CLI: loggerinst.info(no_changes_msg) diff --git a/convert2rhel/subscription.py b/convert2rhel/subscription.py index 80c0dec1e4..41a4d44222 100644 --- a/convert2rhel/subscription.py +++ b/convert2rhel/subscription.py @@ -35,7 +35,8 @@ from convert2rhel.redhatrelease import os_release_file from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR from convert2rhel.systeminfo import system_info -from convert2rhel.toolopts import _should_subscribe, tool_opts +from convert2rhel.toolopts import tool_opts +from convert2rhel.utils.subscription import _should_subscribe logger = root_logger.getChild(__name__) diff --git a/convert2rhel/systeminfo.py b/convert2rhel/systeminfo.py index 543d308737..606e75353d 100644 --- a/convert2rhel/systeminfo.py +++ b/convert2rhel/systeminfo.py @@ -27,8 +27,9 @@ from six.moves import configparser from convert2rhel import logger, utils -from convert2rhel.toolopts import PRE_RPM_VA_LOG_FILENAME, tool_opts +from convert2rhel.toolopts import tool_opts from convert2rhel.utils import run_subprocess +from convert2rhel.utils.rpm import PRE_RPM_VA_LOG_FILENAME # Number of times to retry checking the status of dbus diff --git a/convert2rhel/toolopts.py b/convert2rhel/toolopts.py deleted file mode 100644 index e440cafa29..0000000000 --- a/convert2rhel/toolopts.py +++ /dev/null @@ -1,666 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright(C) 2016 Red Hat, Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -__metaclass__ = type - -import argparse -import copy -import os -import re -import sys - -from six.moves import configparser, urllib - -from convert2rhel import __version__, utils -from convert2rhel.logger import root_logger - - -logger = root_logger.getChild(__name__) - -# Paths for configuration files -CONFIG_PATHS = ["~/.convert2rhel.ini", "/etc/convert2rhel.ini"] - -#: Map name of the convert2rhel mode to run in from the command line to the -#: activity name that we use in the code and breadcrumbs. CLI commands should -#: be verbs but an activity is a noun. -_COMMAND_TO_ACTIVITY = { - "convert": "conversion", - "analyze": "analysis", - "analyse": "analysis", -} - -ARGS_WITH_VALUES = [ - "-u", - "--username", - "-p", - "--password", - "-k", - "--activationkey", - "-o", - "--org", - "--pool", - "--serverurl", -] -PARENT_ARGS = ["--debug", "--help", "-h", "--version"] - -# For a list of modified rpm files before the conversion starts -PRE_RPM_VA_LOG_FILENAME = "rpm_va.log" - -# For a list of modified rpm files after the conversion finishes for comparison purposes -POST_RPM_VA_LOG_FILENAME = "rpm_va_after_conversion.log" - - -class ToolOpts: - def __init__(self): - self.debug = False - self.username = None - self.config_file = None - self.password = None - self.no_rhsm = False - self.enablerepo = [] - self.disablerepo = [] - self.pool = None - self.rhsm_hostname = None - self.rhsm_port = None - self.rhsm_prefix = None - self.autoaccept = None - self.auto_attach = None - self.restart = None - self.activation_key = None - self.org = None - self.arch = None - self.no_rpm_va = False - self.eus = False - self.els = False - self.activity = None - - def set_opts(self, supported_opts): - """Set ToolOpts data using dict with values from config file. - - :param supported_opts: Supported options in config file - """ - for key, value in supported_opts.items(): - if value is not None and hasattr(self, key): - setattr(self, key, value) - - -class CLI: - def __init__(self): - self._parser = self._get_argparser() - self._shared_options_parser = argparse.ArgumentParser(add_help=False) - # Duplicating parent options here as we want to make it - # available for any other basic operation that we run without a - # subcommand in mind, and, it is a shared option so we can share it - # between any subcommands we may create in the future. - self._register_parent_options(self._parser) - self._register_parent_options(self._shared_options_parser) - self._register_options() - self._process_cli_options() - - @staticmethod - def usage(subcommand_to_print=""): - # Override the subcommand_to_print parameter if the tool has been executed through CLI but without - # subcommand specified. This is to make sure that runnning `convert2rhel --help` on the CLI will print the - # usage with generic , while the manpage generated using argparse_manpage will be able to print the - # usage correctly for subcommands as it does not execute convert2rhel from the CLI. - subcommand_not_used_on_cli = "/usr/bin/convert2rhel" in sys.argv[0] and not _subcommand_used(sys.argv) - if subcommand_not_used_on_cli: - subcommand_to_print = "" - usage = ( - "\n" - " convert2rhel [--version] [-h]\n" - " convert2rhel {subcommand} [-u username] [-p password | -c conf_file_path] [--pool pool_id | -a] [--disablerepo repoid]" - " [--enablerepo repoid] [--serverurl url] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" - " convert2rhel {subcommand} [--no-rhsm] [--disablerepo repoid] [--enablerepo repoid] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" - " convert2rhel {subcommand} [-k activation_key | -c conf_file_path] [-o organization] [--pool pool_id | -a] [--disablerepo repoid] [--enablerepo" - " repoid] [--serverurl url] [--no-rpm-va] [--eus] [--els] [--debug] [--restart] [-y]\n" - ).format(subcommand=subcommand_to_print) - - if subcommand_not_used_on_cli: - usage = usage + "\n Subcommands: analyze, convert" - return usage - - def _get_argparser(self): - return argparse.ArgumentParser(conflict_handler="resolve", usage=self.usage()) - - def _register_commands(self): - """Configures parsers specific to the analyze and convert subcommands""" - subparsers = self._parser.add_subparsers(title="Subcommands", dest="command") - self._analyze_parser = subparsers.add_parser( - "analyze", - help="Run all Convert2RHEL initial checks up until the" - " Point of no Return (PONR) and generate a report with the findings." - " A rollback is initiated after the checks to put the system back" - " in the original state.", - parents=[self._shared_options_parser], - usage=self.usage(subcommand_to_print="analyze"), - ) - self._convert_parser = subparsers.add_parser( - "convert", - help="Convert the system. If no subcommand is given, 'convert' is used as a default.", - parents=[self._shared_options_parser], - usage=self.usage(subcommand_to_print="convert"), - ) - - @staticmethod - def _register_parent_options(parser): - """Prescribe what parent command line options the tool accepts.""" - parser.add_argument( - "--version", - action="version", - version=__version__, - help="Show convert2rhel version and exit.", - ) - parser.add_argument( - "--debug", - action="store_true", - help="Print traceback in case of an abnormal exit and messages that could help find an issue.", - ) - - def _register_options(self): - """Prescribe what command line options the tool accepts.""" - self._parser.add_argument( - "-h", - "--help", - action="help", - help="Show help message and exit.", - ) - self._shared_options_parser.add_argument( - "--no-rpm-va", - action="store_true", - help="Skip gathering changed rpm files using" - " 'rpm -Va'. By default it's performed before and after the conversion with the output" - " stored in log files %s and %s. At the end of the conversion, these logs are compared" - " to show you what rpm files have been affected by the conversion." - " Cannot be used with analyze subcommand." - " The environment variable CONVERT2RHEL_INCOMPLETE_ROLLBACK" - " needs to be set to 1 to use this argument." % (PRE_RPM_VA_LOG_FILENAME, POST_RPM_VA_LOG_FILENAME), - ) - self._shared_options_parser.add_argument( - "--eus", - action="store_true", - help="Explicitly recognize the system as eus, utilizing eus repos." - " This option is meant for el8.8+ systems.", - ) - self._shared_options_parser.add_argument( - "--els", - action="store_true", - help="Explicitly recognize the system as els, utilizing els repos." - " This option is meant for el7 systems.", - ) - self._shared_options_parser.add_argument( - "--enablerepo", - metavar="repoidglob", - action="append", - help="Enable specific" - " repositories by ID or glob. For more repositories to enable, use this option" - " multiple times. If you don't use the --no-rhsm option, you can use this option" - " to override the default RHEL repoids that convert2rhel enables through" - " subscription-manager.", - ) - self._shared_options_parser.add_argument( - "--disablerepo", - metavar="repoidglob", - action="append", - help="Disable specific" - " repositories by ID or glob. For more repositories to disable, use this option" - " multiple times. This option defaults to all repositories ('*').", - ) - self._shared_options_parser.add_argument( - "-r", - "--restart", - help="Restart the system when it is successfully converted to RHEL to boot the new RHEL kernel." - " It has no effect when used with the 'analyze' subcommand.", - action="store_true", - ) - self._shared_options_parser.add_argument( - "-y", - help="Answer yes to all yes/no questions the tool asks.", - action="store_true", - ) - self._add_subscription_manager_options() - self._add_alternative_installation_options() - self._register_commands() - - def _add_alternative_installation_options(self): - """Prescribe what alternative command line options the tool accepts.""" - group = self._shared_options_parser.add_argument_group( - title="Alternative Installation Options", - description="The following options are required if you do not intend on using subscription-manager.", - ) - group.add_argument( - "--no-rhsm", - action="store_true", - help="Do not use the subscription-manager, use custom repositories instead. See --enablerepo/--disablerepo" - " options. Without this option, the subscription-manager is used to access RHEL repositories by default." - " Using this option requires to have the --enablerepo specified.", - ) - - def _add_subscription_manager_options(self): - """Prescribe what subscription manager command line options the tool accepts.""" - group = self._shared_options_parser.add_argument_group( - title="Subscription Manager Options", - description="The following options are specific to using subscription-manager.", - ) - group.add_argument( - "-u", - "--username", - help="Username for the" - " subscription-manager. If neither --username nor" - " --activation-key option is used, the user" - " is asked to enter the username.", - ) - group.add_argument( - "-p", - "--password", - help="Password for the" - " subscription-manager. If --password, --config-file or --activationkey are not" - " used, the user is asked to enter the password." - " We recommend using the --config-file option instead to prevent leaking the password" - " through a list of running processes.", - ) - group.add_argument( - "-k", - "--activationkey", - help="Activation key used" - " for the system registration by the" - " subscription-manager. It requires to have the --org" - " option specified." - " We recommend using the --config-file option instead to prevent leaking the activation key" - " through a list of running processes.", - ) - group.add_argument( - "-o", - "--org", - help="Organization with which the" - " system will be registered by the" - " subscription-manager. A list of available" - " organizations is possible to obtain by running" - " 'subscription-manager orgs'. From the listed pairs" - " Name:Key, use the Key here.", - ) - group.add_argument( - "-c", - "--config-file", - help="The configuration file is an optional way to safely pass either a user password or an activation key" - " to the subscription-manager to register the system. This is more secure than passing these values" - " through the --activationkey or --password option, which might leak the values" - " through a list of running processes." - " You can edit the pre-installed configuration file template at /etc/convert2rhel.ini or create a new" - " configuration file at ~/.convert2rhel.ini. The convert2rhel utility loads the configuration from either" - " of those locations, the latter having preference over the former. Alternatively, you can specify a path" - " to the configuration file using the --config-file option to override other configurations.", - ) - group.add_argument( - "-a", - "--auto-attach", - help="Automatically attach compatible subscriptions to the system.", - action="store_true", - ) - group.add_argument( - "--pool", - help="Subscription pool ID. A list of the available" - " subscriptions is possible to obtain by running" - " 'subscription-manager list --available'." - " If no pool ID is provided, the --auto option is used", - ) - group.add_argument( - "--serverurl", - help="Hostname of the subscription service to be used when registering the system with" - " subscription-manager. The default is the Customer Portal Subscription Management service" - " (subscription.rhsm.redhat.com). It is not to be used to specify a Satellite server. For that, read" - " the product documentation at https://access.redhat.com/.", - ) - - def _process_cli_options(self): - """Process command line options used with the tool.""" - _log_command_used() - - # algorithm function to properly organize all CLI args - argv = _add_default_command(sys.argv[1:]) - parsed_opts = self._parser.parse_args(argv) - - if parsed_opts.debug: - tool_opts.debug = True - - # Once we use a subcommand to set the activity that convert2rhel will perform - tool_opts.activity = _COMMAND_TO_ACTIVITY[parsed_opts.command] - - # Processing the configuration file - conf_file_opts = options_from_config_files(parsed_opts.config_file) - tool_opts.set_opts(conf_file_opts) - config_opts = copy.copy(tool_opts) - tool_opts.config_file = parsed_opts.config_file - # corner case: password on CLI and activation-key in the config file - # password from CLI has precedence and activation_key and org must be deleted (unused) - if config_opts.activation_key and parsed_opts.password: - tool_opts.activation_key = None - tool_opts.org = None - - if parsed_opts.no_rpm_va: - if tool_opts.activity == "analysis": - logger.warning( - "We will proceed with ignoring the --no-rpm-va option as running rpm -Va" - " in the analysis mode is essential for a complete rollback to the original" - " system state at the end of the analysis." - ) - elif os.getenv("CONVERT2RHEL_INCOMPLETE_ROLLBACK", None): - tool_opts.no_rpm_va = True - else: - message = ( - "We need to run the 'rpm -Va' command to be able to perform a complete rollback of changes" - " done to the system during the pre-conversion analysis. If you accept the risk of an" - " incomplete rollback, set the CONVERT2RHEL_INCOMPLETE_ROLLBACK=1 environment" - " variable. Otherwise, remove the --no-rpm-va option." - ) - logger.critical(message) - - if parsed_opts.username: - tool_opts.username = parsed_opts.username - - if parsed_opts.password: - tool_opts.password = parsed_opts.password - - if parsed_opts.enablerepo: - tool_opts.enablerepo = parsed_opts.enablerepo - if parsed_opts.disablerepo: - tool_opts.disablerepo = parsed_opts.disablerepo - - # Check if we have duplicate repositories specified - if parsed_opts.enablerepo or parsed_opts.disablerepo: - duplicate_repos = set(tool_opts.disablerepo) & set(tool_opts.enablerepo) - if duplicate_repos: - message = "Duplicate repositories were found across disablerepo and enablerepo options:" - for repo in duplicate_repos: - message += "\n%s" % repo - message += "\nThis ambiguity may have unintended consequences." - logger.warning(message) - - if parsed_opts.no_rhsm: - tool_opts.no_rhsm = True - if not tool_opts.enablerepo: - logger.critical("The --enablerepo option is required when --no-rhsm is used.") - - if parsed_opts.eus: - tool_opts.eus = True - - if parsed_opts.els: - tool_opts.els = True - - if not tool_opts.disablerepo: - # Default to disable every repo except: - # - the ones passed through --enablerepo - # - the ones enabled through subscription-manager based on convert2rhel config files - tool_opts.disablerepo = ["*"] - - if parsed_opts.pool: - tool_opts.pool = parsed_opts.pool - - if parsed_opts.activationkey: - tool_opts.activation_key = parsed_opts.activationkey - - if parsed_opts.org: - tool_opts.org = parsed_opts.org - - if parsed_opts.serverurl: - if tool_opts.no_rhsm: - logger.warning("Ignoring the --serverurl option. It has no effect when --no-rhsm is used.") - # WARNING: We cannot use the following helper until after no_rhsm, - # username, password, activation_key, and organization have been set. - elif not _should_subscribe(tool_opts): - logger.warning( - "Ignoring the --serverurl option. It has no effect when no credentials to subscribe the system were given." - ) - else: - # Parse the serverurl and save the components. - try: - url_parts = _parse_subscription_manager_serverurl(parsed_opts.serverurl) - url_parts = _validate_serverurl_parsing(url_parts) - except ValueError as e: - # If we fail to parse, fail the conversion. The reason for - # this harsh treatment is that we will be submitting - # credentials to the server parsed from the serverurl. If - # the user is specifying an internal subscription-manager - # server but typo the url, we would fallback to the - # public red hat subscription-manager server. That would - # mean the user thinks the credentials are being passed - # to their internal subscription-manager server but it - # would really be passed externally. That's not a good - # security practice. - logger.critical( - "Failed to parse a valid subscription-manager server from the --serverurl option.\n" - "Please check for typos and run convert2rhel again with a corrected --serverurl.\n" - "Supplied serverurl: %s\nError: %s" % (parsed_opts.serverurl, e) - ) - - tool_opts.rhsm_hostname = url_parts.hostname - - if url_parts.port: - # urllib.parse.urlsplit() converts this into an int but we - # always use it as a str - tool_opts.rhsm_port = str(url_parts.port) - - if url_parts.path: - tool_opts.rhsm_prefix = url_parts.path - - tool_opts.autoaccept = parsed_opts.y - tool_opts.auto_attach = parsed_opts.auto_attach - - # conversion only options - if tool_opts.activity == "conversion": - tool_opts.restart = parsed_opts.restart - - # Security notice - if tool_opts.password or tool_opts.activation_key: - logger.warning( - "Passing the RHSM password or activation key through the --activationkey or --password options is" - " insecure as it leaks the values through the list of running processes. We recommend using the safer" - " --config-file option instead." - ) - - # Checks of multiple authentication sources - if tool_opts.password and tool_opts.activation_key: - logger.warning( - "Either a password or an activation key can be used for system registration." - " We're going to use the activation key." - ) - - # Config files matches - if config_opts.username and parsed_opts.username: - logger.warning( - "You have passed the RHSM username through both the command line and the" - " configuration file. We're going to use the command line values." - ) - - if config_opts.org and parsed_opts.org: - logger.warning( - "You have passed the RHSM org through both the command line and the" - " configuration file. We're going to use the command line values." - ) - - if (config_opts.activation_key or config_opts.password) and (parsed_opts.activationkey or parsed_opts.password): - logger.warning( - "You have passed either the RHSM password or activation key through both the command line and the" - " configuration file. We're going to use the command line values." - ) - - if (tool_opts.org and not tool_opts.activation_key) or (not tool_opts.org and tool_opts.activation_key): - logger.critical( - "Either the --org or the --activationkey option is missing. You can't use one without the other." - ) - - if (parsed_opts.password or config_opts.password) and not (parsed_opts.username or config_opts.username): - logger.warning( - "You have passed the RHSM password without an associated username. Please provide a username together" - " with the password." - ) - - if (parsed_opts.username or config_opts.username) and not (parsed_opts.password or config_opts.password): - logger.warning( - "You have passed the RHSM username without an associated password. Please provide a password together" - " with the username." - ) - - -def _log_command_used(): - """We want to log the command used for convert2rhel to make it easier to know what command was used - when debugging the log files. Since we can't differentiate between the handlers we log to both stdout - and the logfile - """ - command = " ".join(utils.hide_secrets(sys.argv)) - logger.info("convert2rhel command used:\n{0}".format(command)) - - -def options_from_config_files(cfg_path=None): - """Parse the convert2rhel.ini configuration file. - - This function will try to parse the convert2rhel.ini configuration file and - return a dictionary containing the values found in the file. - - .. note:: - This function will parse the configuration file following a specific - order, which is: - 1) Path provided by the user in cfg_path (Highest priority). - 2) ~/.convert2rhel.ini (The 2nd highest priority). - 3) /etc/convert2rhel.ini (The lowest priority). - - :param cfg_path: Path of a custom configuration file - :type cfg_path: str - - :return: Dict with the supported options alongside their values. - :rtype: dict[str, str | None] - """ - headers = ["subscription_manager"] # supported sections in config file - # Create dict with all supported options, all of them set to None - # needed for avoiding problems with files priority - # The name of supported option MUST correspond with the name in ToolOpts() - # Otherwise it won't be used - supported_opts = {"username": None, "password": None, "activation_key": None, "org": None} - - config_file = configparser.ConfigParser() - paths = [os.path.expanduser(path) for path in CONFIG_PATHS] - - if cfg_path: - cfg_path = os.path.expanduser(cfg_path) - if not os.path.exists(cfg_path): - raise OSError(2, "No such file or directory: '%s'" % cfg_path) - paths.insert(0, cfg_path) # highest priority - - for path in paths: - if os.path.exists(path): - if not oct(os.stat(path).st_mode)[-4:].endswith("00"): - logger.critical("The %s file must only be accessible by the owner (0600)" % path) - config_file.read(path) - - for header in config_file.sections(): - if header in headers: - for option in config_file.options(header): - if option.lower() in supported_opts: - # Solving priority - if supported_opts[option.lower()] is None: - supported_opts[option] = config_file.get(header, option) - logger.debug("Found %s in %s" % (option, path)) - else: - logger.warning("Unsupported option %s in %s" % (option, path)) - elif header not in headers and header != "DEFAULT": - logger.warning("Unsupported header %s in %s." % (header, path)) - - return supported_opts - - -def _parse_subscription_manager_serverurl(serverurl): - """Parse a url string in a manner mostly compatible with subscription-manager --serverurl.""" - # This is an adaptation of what subscription-manager's cli enforces: - # https://github.com/candlepin/subscription-manager/blob/main/src/rhsm/utils.py#L112 - - # Don't modify http:// and https:// as they are fine - if not re.match("https?://[^/]+", serverurl): - # Anthing that looks like a malformed scheme is immediately discarded - if re.match("^[^:]+:/.+", serverurl): - raise ValueError("Unable to parse --serverurl. Make sure it starts with http://HOST or https://HOST") - - # If there isn't a scheme, add one now - serverurl = "https://%s" % serverurl - - url_parts = urllib.parse.urlsplit(serverurl, allow_fragments=False) - - return url_parts - - -def _validate_serverurl_parsing(url_parts): - """ - Perform some tests that we parsed the subscription-manager serverurl successfully. - - :arg url_parts: The parsed serverurl as returned by urllib.parse.urlsplit() - :raises ValueError: If any of the checks fail. - :returns: url_parts If the check was successful. - """ - if url_parts.scheme not in ("https", "http"): - raise ValueError( - "Subscription manager must be accessed over http or https. %s is not valid" % url_parts.scheme - ) - - if not url_parts.hostname: - raise ValueError("A hostname must be specified in a subscription-manager serverurl") - - return url_parts - - -def _add_default_command(argv): - """Add the default command when none is given""" - subcommand = _subcommand_used(argv) - args = argv - if not subcommand: - args.insert(0, "convert") - - return args - - -def _subcommand_used(args): - """Return what subcommand has been used by the user. Return None if no subcommand has been used.""" - for index, argument in enumerate(args): - if argument in ("convert", "analyze"): - return argument - - if not argument in PARENT_ARGS and args[index - 1] in ARGS_WITH_VALUES: - return None - - -def _should_subscribe(tool_opts): - """ - Whether we should subscribe the system with subscription-manager. - - If there are no ways to authenticate to subscription-manager, then we will attempt to convert - without subscribing the system. The assumption is that the user has already subscribed the - system or that this machine does not need to subscribe to rhsm in order to get the RHEL rpm - packages. - """ - # No means to authenticate with rhsm. - if not (tool_opts.username and tool_opts.password) and not (tool_opts.activation_key and tool_opts.org): - return False - - # --no-rhsm means that there is no need to use any part of rhsm to - # convert this host. (Usually used when you configure - # your RHEL repos another way, like a local mirror and telling - # convert2rhel about it using --enablerepo) - if tool_opts.no_rhsm: - return False - - return True - - -# Code to be executed upon module import -tool_opts = ToolOpts() # pylint: disable=C0103 diff --git a/convert2rhel/toolopts/__init__.py b/convert2rhel/toolopts/__init__.py new file mode 100644 index 0000000000..deb7e83665 --- /dev/null +++ b/convert2rhel/toolopts/__init__.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2016 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +__metaclass__ = type + +import logging + +from convert2rhel.utils.subscription import setup_rhsm_parts + + +loggerinst = logging.getLogger(__name__) + + +class ToolOpts: + def _handle_config_conflict(self, config_sources): + file_config = next((config for config in config_sources if config.SOURCE == "configuration file"), None) + # No file config detected, let's just bail out. + if not file_config: + return + + cli_config = next(config for config in config_sources if config.SOURCE == "command line") + + # Config files matches + if file_config.username and cli_config.username: + loggerinst.warning( + "You have passed the RHSM username through both the command line and the" + " configuration file. We're going to use the command line values." + ) + self.username = cli_config.username + + if file_config.org and cli_config.org: + loggerinst.warning( + "You have passed the RHSM org through both the command line and the" + " configuration file. We're going to use the command line values." + ) + self.org = cli_config.org + + if file_config.activation_key and cli_config.activation_key: + loggerinst.warning( + "You have passed the RHSM activation key through both the command line and the" + " configuration file. We're going to use the command line values." + ) + self.activation_key = cli_config.activation_key + + if file_config.password and cli_config.password: + loggerinst.warning( + "You have passed the RHSM password through both the command line and the" + " configuration file. We're going to use the command line values." + ) + self.password = cli_config.password + + if (cli_config.password or file_config.password) and not (cli_config.username or file_config.username): + loggerinst.warning( + "You have passed the RHSM password without an associated username. Please provide a username together" + " with the password." + ) + + if (cli_config.username or file_config.username) and not (cli_config.password or file_config.password): + loggerinst.warning( + "You have passed the RHSM username without an associated password. Please provide a password together" + " with the username." + ) + + if self.password and self.activation_key: + loggerinst.warning( + "Either a password or an activation key can be used for system registration." + " We're going to use the activation key." + ) + + # Corner cases + if file_config.activation_key and cli_config.password: + loggerinst.warning( + "You have passed either the RHSM password or activation key through both the command line and" + " the configuration file. We're going to use the command line values." + ) + self.activation_key = None + self.org = None + + if self.no_rpm_va and self.activity != "analysis": + # If the incomplete_rollback option is not set in the config file, we will raise a SystemExit through + # logger.critical, otherwise, just set the no_rpm_va to False and move on. + if not file_config.incomplete_rollback: + message = ( + "We need to run the 'rpm -Va' command to be able to perform a complete rollback of changes" + " done to the system during the pre-conversion analysis. If you accept the risk of an" + " incomplete rollback, set the CONVERT2RHEL_INCOMPLETE_ROLLBACK=1 environment" + " variable. Otherwise, remove the --no-rpm-va option." + ) + loggerinst.critical(message) + + def _handle_missing_options(self): + if (self.org and not self.activation_key) or (not self.org and self.activation_key): + loggerinst.critical( + "Either the --org or the --activationkey option is missing. You can't use one without the other." + ) + + def set_opts(self, key, value): + if not hasattr(self, key): + setattr(self, key, value) + return + + current_attribute_value = getattr(self, key) + + if value and not current_attribute_value: + setattr(self, key, value) + + def _handle_rhsm_parts(self): + # Sending itself as the ToolOpts class contains all the attribute references. + rhsm_parts = setup_rhsm_parts(self) + + for key, value in rhsm_parts.items(): + self.set_opts(key, value) + + def initialize(self, config_sources): + # Populate the values for each config class before applying the attribute to the class. + [config.run() for config in config_sources] + + # Apply the attributes from config classes to ToolOpts. + for config in config_sources: + [self.set_opts(key, value) for key, value in vars(config).items()] + + # This is being handled here because we have conditions inside the `setup_rhsm_parts` that checks for + # username/password, and since that type of information can come from CLI or Config file, we are putting it + # here. + self._handle_rhsm_parts() + + # Handle the conflicts between FileConfig and other Config classes + self._handle_config_conflict(config_sources) + + # Handle critical conflicts before finalizing + self._handle_missing_options() + + +tool_opts = ToolOpts() diff --git a/convert2rhel/toolopts/config.py b/convert2rhel/toolopts/config.py new file mode 100644 index 0000000000..7b794fe8e6 --- /dev/null +++ b/convert2rhel/toolopts/config.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2016 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +__metaclass__ = type + +import abc +import copy +import logging +import os + +import six + +from six.moves import configparser + + +loggerinst = logging.getLogger(__name__) + +#: Map name of the convert2rhel mode to run in from the command line to the +#: activity name that we use in the code and breadcrumbs. CLI commands should +#: be verbs but an activity is a noun. +_COMMAND_TO_ACTIVITY = { + "convert": "conversion", + "analyze": "analysis", + "analyse": "analysis", +} + +# Mapping of supported headers and options for each configuration in the +# `convert2rhel.ini` file we support. +CONFIG_FILE_MAPPING_OPTIONS = { + "subscription_manager": ["username", "password", "org", "activation_key"], + "host_metering": ["configure_host_metering"], + "inhibitor_overrides": [ + "incomplete_rollback", + "tainted_kernel_module_check_skip", + "outdated_package_check_skip", + "allow_older_version", + "allow_unavailable_kmods", + "configure_host_metering", + "skip_kernel_currency_check", + ], +} + +BOOLEAN_OPTIONS_HEADERS = ["inhibitor_overrides"] + + +@six.add_metaclass(abc.ABCMeta) +class BaseConfig: + def set_opts(self, supported_opts): + """Set ToolOpts data using dict with values from Config classes. + + :param opts: Supported options in config file + """ + for key, value in supported_opts.items(): + if value and hasattr(self, key): + setattr(self, key, value) + + +class FileConfig(BaseConfig): + SOURCE = "configuration file" + DEFAULT_CONFIG_FILES = ["~/.convert2rhel.ini", "/etc/convert2rhel.ini"] + + def __init__(self, custom_config): + super(FileConfig, self).__init__() + + # Subscription Manager + self.username = None # type: str | None + self.password = None # type: str | None + self.org = None # type: str | None + self.activation_key = None # type: str | None + + # Inhibitor Override + self.incomplete_rollback = None # type: str | None + self.tainted_kernel_module_check_skip = None # type: str | None + self.outdated_package_check_skip = None # type: str | None + self.allow_older_version = None # type: str | None + self.allow_unavailable_kmods = None # type: str | None + self.skip_kernel_currency_check = None # type: str | None + + # Host metering + self.configure_host_metering = None # type: str | None + + self._config_files = self.DEFAULT_CONFIG_FILES + if custom_config: + self._config_files.insert(0, custom_config) + + def run(self): + unparsed_opts = self.options_from_config_files() + self.set_opts(unparsed_opts) + + # Cleanup + del self._config_files + + def options_from_config_files(self): + """Parse the convert2rhel.ini configuration file. + + This function will try to parse the convert2rhel.ini configuration file and + return a dictionary containing the values found in the file. + + .. note:: + This function will parse the configuration file in the following way: + + 1) If the path provided by the user in cfg_path is set (Highest + priority), then we use only that. + + Otherwise, if cfg_path is `None`, we proceed to check the following + paths: + + 2) ~/.convert2rhel.ini (The 2nd highest priority). + 3) /etc/convert2rhel.ini (The lowest priority). + + In any case, they are parsed in reversed order, meaning that we will + start with the lowest priority and go until the highest. + + :param cfg_path: Path of a custom configuration file + :type cfg_path: str + + :return: Dict with the supported options alongside their values. + :rtype: dict[str, str] + """ + # Paths for the configuration files. In case we have cfg_path defined + # (meaning that the user entered something through the `-c` option), we + # will use only that, as it has a higher priority over the rest + paths = [os.path.expanduser(path) for path in self._config_files if os.path.exists(os.path.expanduser(path))] + + if not paths: + raise FileNotFoundError("No such file or directory: %s" % ", ".join(paths)) + + found_opts = self._parse_options_from_config(paths) + return found_opts + + def _parse_options_from_config(self, paths): + """Parse the options from the given config files. + + .. note:: + If no configuration file is provided through the command line option + (`-c`), we will use the default paths and follow their priority. + + :param paths: List of paths to iterate through and gather the options from + them. + :type paths: list[str] + """ + config_file = configparser.ConfigParser() + found_opts = {} + + for path in reversed(paths): + loggerinst.debug("Checking configuration file at %s" % path) + # Check for correct permissions on file + if not oct(os.stat(path).st_mode)[-4:].endswith("00"): + loggerinst.critical("The %s file must only be accessible by the owner (0600)" % path) + + config_file.read(path) + + # Mapping of all supported options we can have in the config file + for supported_header, supported_opts in CONFIG_FILE_MAPPING_OPTIONS.items(): + loggerinst.debug("Checking for header '%s'" % supported_header) + if supported_header not in config_file.sections(): + loggerinst.warning( + "Couldn't find header '%s' in the configuration file %s." % (supported_header, path) + ) + continue + options = self._get_options_value(config_file, supported_header, supported_opts) + found_opts.update(options) + + return found_opts + + def _get_options_value(self, config_file, header, supported_opts): + """Helper function to iterate through the options in a config file. + + :param config_file: An instance of `py:ConfigParser` after reading the file + to iterate through the options. + :type config_file: configparser.ConfigParser + :param header: The header name to get options from. + :type header: str + :param supported_opts: List of supported options that can be parsed from + the config file. + :type supported_opts: list[str] + """ + options = {} + conf_options = config_file.options(header) + + if len(conf_options) == 0: + loggerinst.debug("No options found for %s. It seems to be empty or commented." % header) + return options + + for option in conf_options: + if option.lower() not in supported_opts: + loggerinst.warning("Unsupported option '%s' in '%s'" % (option, header)) + continue + + # This is the only header that can contain boolean values for now. + if header in BOOLEAN_OPTIONS_HEADERS: + options[option] = config_file.getboolean(header, option) + else: + options[option] = config_file.get(header, option) + + loggerinst.debug("Found %s in %s" % (option, header)) + + return options + + +class CliConfig(BaseConfig): + SOURCE = "command line" + + def __init__(self, opts): + super(CliConfig, self).__init__() + + self.debug = False # type: bool + self.username = None # type: str | None + self.password = None # type: str | None + self.org = None # type: str | None + self.activation_key = None # type: str | None + self.config_file = None # type: str | None + self.no_rhsm = False # type: bool + self.enablerepo = [] # type: list[str] + self.disablerepo = [] # type: list[str] + self.pool = None # type: str | None + self.autoaccept = False # type: bool + self.auto_attach = None # type: str | None + self.restart = False # type: bool + self.arch = None # type: str | None + self.no_rpm_va = False # type: bool + self.eus = False # type: bool + self.els = False # type: bool + self.activity = None # type: str | None + self.serverurl = None # type: str | None + + self._opts = opts # type: arpgparse.Namepsace + + def run(self): + + opts = vars(self._opts) + + opts = self._normalize_opts(opts) + self._validate(opts) + self.set_opts(opts) + + # Cleanup + del self._opts + + def _normalize_opts(self, opts): + unparsed_opts = copy.copy(opts) + unparsed_opts["activity"] = _COMMAND_TO_ACTIVITY[opts.get("command", "convert")] + unparsed_opts["disablerepo"] = opts.get("disablerepo") if opts["disablerepo"] else ["*"] + unparsed_opts["enablerepo"] = opts.get("enablerepo") if opts["enablerepo"] else [] + unparsed_opts["autoaccept"] = opts.get("auto_accept") if opts["auto_accept"] else False + + # Conversion only opts. + if unparsed_opts["activity"] == "conversion": + unparsed_opts["restart"] = opts.get("restart") + + if unparsed_opts["no_rpm_va"]: + if unparsed_opts["activity"] == "analysis": + loggerinst.warning( + "We will proceed with ignoring the --no-rpm-va option as running rpm -Va" + " in the analysis mode is essential for a complete rollback to the original" + " system state at the end of the analysis." + ) + unparsed_opts["no_rpm_va"] = False + + # This is not needed at the data structure. Command is something that comes from the argparse.Namespace + # strucutre. + del unparsed_opts["command"] + + return unparsed_opts + + def _validate(self, opts): + # Security notice + if opts["password"] or opts["activation_key"]: + loggerinst.warning( + "Passing the RHSM password or activation key through the --activationkey or --password options is" + " insecure as it leaks the values through the list of running processes. We recommend using the safer" + " --config-file option instead." + ) + + # Checks of multiple authentication sources + if opts["password"] and opts["activation_key"]: + loggerinst.warning( + "Either a password or an activation key can be used for system registration." + " We're going to use the activation key." + ) + + if opts["username"] and not opts["password"]: + loggerinst.warning( + "You have passed the RHSM username without an associated password. Please provide a password together" + " with the username." + ) + + if opts["password"] and not opts["username"]: + loggerinst.warning( + "You have passed the RHSM password without an associated username. Please provide a username together" + " with the password." + ) + + # Check if we have duplicate repositories specified + if opts["enablerepo"] or opts["disablerepo"]: + duplicate_repos = set(opts["disablerepo"]) & set(opts["enablerepo"]) + if duplicate_repos: + message = "Duplicate repositories were found across disablerepo and enablerepo options:" + for repo in duplicate_repos: + message += "\n%s" % repo + message += "\nThis ambiguity may have unintended consequences." + loggerinst.warning(message) + + if opts["no_rhsm"]: + if not opts["enablerepo"]: + loggerinst.critical("The --enablerepo option is required when --no-rhsm is used.") diff --git a/convert2rhel/unit_tests/__init__.py b/convert2rhel/unit_tests/__init__.py index 9242e6c8a4..517b87f573 100644 --- a/convert2rhel/unit_tests/__init__.py +++ b/convert2rhel/unit_tests/__init__.py @@ -30,6 +30,7 @@ from convert2rhel import ( backup, breadcrumbs, + cli, exceptions, grub, initialize, @@ -586,12 +587,12 @@ class ResolveSystemInfoMocked(MockFunctionObject): # -# toolopts mocks +# cli mocks # class CLIMocked(MockFunctionObject): - spec = toolopts.CLI + spec = cli.CLI # diff --git a/convert2rhel/unit_tests/actions/conversion/lock_releasever_test.py b/convert2rhel/unit_tests/actions/conversion/lock_releasever_test.py index 7ba64a4efc..306867d772 100644 --- a/convert2rhel/unit_tests/actions/conversion/lock_releasever_test.py +++ b/convert2rhel/unit_tests/actions/conversion/lock_releasever_test.py @@ -31,6 +31,11 @@ from convert2rhel import unit_tests +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(lock_releasever, "tool_opts", global_tool_opts) + + @pytest.fixture def lock_releasever_in_rhel_repositories_instance(): return lock_releasever.LockReleaseverInRHELRepositories() diff --git a/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py b/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py index 2586ef8f6b..64684d8726 100644 --- a/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py +++ b/convert2rhel/unit_tests/actions/conversion/preserve_only_rhel_kernel_test.py @@ -71,6 +71,11 @@ def update_kernel_instance(): return preserve_only_rhel_kernel.UpdateKernel() +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) + + class TestInstallRhelKernel: @pytest.mark.parametrize( ( diff --git a/convert2rhel/unit_tests/actions/post_conversion/kernel_boot_files_test.py b/convert2rhel/unit_tests/actions/post_conversion/kernel_boot_files_test.py index d3daf1eaab..5cf055a4b3 100644 --- a/convert2rhel/unit_tests/actions/post_conversion/kernel_boot_files_test.py +++ b/convert2rhel/unit_tests/actions/post_conversion/kernel_boot_files_test.py @@ -20,7 +20,7 @@ import pytest import six -from convert2rhel import actions, checks +from convert2rhel import actions, checks, grub from convert2rhel.actions.post_conversion import kernel_boot_files from convert2rhel.unit_tests import RunSubprocessMocked from convert2rhel.unit_tests.conftest import centos8 @@ -65,51 +65,38 @@ def test_check_kernel_boot_files(pretend_os, tmpdir, caplog, monkeypatch, kernel @pytest.mark.parametrize( - ("create_initramfs", "create_vmlinuz", "run_piped_subprocess", "rpm_last_kernel_output", "latest_installed_kernel"), + ("vmlinuz_exists", "initiramfs_exists", "rpm_last_kernel_output", "latest_installed_kernel"), ( pytest.param( False, - False, - ("", 0), + True, ("kernel-core-6.1.8-200.fc37.x86_64 Wed 01 Feb 2023 14:01:01 -03", 0), "6.1.8-200.fc37.x86_64", - id="both-files-missing", + id="vmlinuz-missing", ), pytest.param( True, False, - ("test", 0), ("kernel-core-6.1.8-200.fc37.x86_64 Wed 01 Feb 2023 14:01:01 -03", 0), "6.1.8-200.fc37.x86_64", id="vmlinuz-missing", ), pytest.param( False, - True, - ("test", 0), - ("kernel-core-6.1.8-200.fc37.x86_64 Wed 01 Feb 2023 14:01:01 -03", 0), - "6.1.8-200.fc37.x86_64", - id="initramfs-missing", - ), - pytest.param( - True, - True, - ("error", 1), + False, ("kernel-core-6.1.8-200.fc37.x86_64 Wed 01 Feb 2023 14:01:01 -03", 0), "6.1.8-200.fc37.x86_64", - id="initramfs-corrupted", + id="vmlinuz-missing", ), ), ) @centos8 def test_check_kernel_boot_files_missing( pretend_os, - create_initramfs, - create_vmlinuz, - run_piped_subprocess, + vmlinuz_exists, + initiramfs_exists, rpm_last_kernel_output, latest_installed_kernel, - tmpdir, caplog, monkeypatch, kernel_boot_files_instance, @@ -123,36 +110,16 @@ def test_check_kernel_boot_files_missing( # that the second iteration may not run sometimes, as this is specific for # when we want to check if a file is corrupted or not. monkeypatch.setattr( - checks, + kernel_boot_files, "run_subprocess", - mock.Mock( - side_effect=[ - rpm_last_kernel_output, - run_piped_subprocess, - ] - ), + mock.Mock(side_effect=[rpm_last_kernel_output]), ) - # monkeypatch.setattr(grub, "is_efi", mock.Mock(return_value=True)) - boot_folder = tmpdir.mkdir("/boot") - if create_initramfs: - initramfs_file = boot_folder.join("initramfs-%s.img") - initramfs_file = str(initramfs_file) - with open(initramfs_file % latest_installed_kernel, mode="w") as _: - pass - - monkeypatch.setattr(kernel_boot_files, "INITRAMFS_FILEPATH", initramfs_file) - else: - monkeypatch.setattr(kernel_boot_files, "INITRAMFS_FILEPATH", "/non-existing-%s.img") - - if create_vmlinuz: - vmlinuz_file = boot_folder.join("vmlinuz-%s") - vmlinuz_file = str(vmlinuz_file) - with open(vmlinuz_file % latest_installed_kernel, mode="w") as _: - pass - - monkeypatch.setattr(kernel_boot_files, "VMLINUZ_FILEPATH", vmlinuz_file) - else: - monkeypatch.setattr(kernel_boot_files, "VMLINUZ_FILEPATH", "/non-existing-%s") + + # We are always expecting that the grub config file is pointed to /boot/grub2/grub.cfg. We can change this in the + # future, but for this test is fine if the path is always the same. + monkeypatch.setattr(grub, "get_grub_config_file", lambda: "/boot/grub2/grub.cfg") + monkeypatch.setattr(checks, "is_initramfs_file_valid", lambda x: initiramfs_exists) + monkeypatch.setattr(os.path, "exists", lambda x: vmlinuz_exists) expected = set( ( @@ -163,10 +130,13 @@ def test_check_kernel_boot_files_missing( description="We failed to determine whether boot partition is configured correctly and that boot" " files exists. This may cause problems during the next boot of your system.", diagnosis=None, - remediations="In order to fix this problem you might need to free/increase space in your boot partition and then run the following commands in your terminal:\n" - "1. yum reinstall kernel-core- -y\n" - "2. grub2-mkconfig -o /boot/grub2/grub.cfg\n" - "3. reboot", + remediations=( + "In order to fix this problem you might need to free/increase space in your boot partition and then run the following commands in your terminal:\n" + "1. yum reinstall kernel-core-%s -y\n" + "2. grub2-mkconfig -o /boot/grub2/grub.cfg\n" + "3. reboot" + ) + % latest_installed_kernel, ), ) ) diff --git a/convert2rhel/unit_tests/actions/post_conversion/modified_rpm_files_diff_test.py b/convert2rhel/unit_tests/actions/post_conversion/modified_rpm_files_diff_test.py index 981102a468..69c36152dc 100644 --- a/convert2rhel/unit_tests/actions/post_conversion/modified_rpm_files_diff_test.py +++ b/convert2rhel/unit_tests/actions/post_conversion/modified_rpm_files_diff_test.py @@ -34,8 +34,11 @@ def modified_rpm_files_diff_instance(): return modified_rpm_files_diff.ModifiedRPMFilesDiff() -def test_modified_rpm_files_diff_with_no_rpm_va(monkeypatch, modified_rpm_files_diff_instance, caplog): - monkeypatch.setattr(toolopts.tool_opts, "no_rpm_va", mock.Mock(return_value=True)) +def test_modified_rpm_files_diff_with_no_rpm_va( + monkeypatch, modified_rpm_files_diff_instance, caplog, global_tool_opts +): + global_tool_opts.no_rpm_va = True + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) # This can be removed when systeminfo is ported to use global logger monkeypatch.setattr(systeminfo.system_info, "logger", logging.getLogger(__name__)) @@ -98,7 +101,9 @@ def test_modified_rpm_files_diff_without_differences_after_conversion( rpm_va_post_output, different, expected_raw, + global_tool_opts, ): + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) monkeypatch.setattr(utils, "run_subprocess", mock.Mock(return_value=(rpm_va_pre_output, 0))) monkeypatch.setattr(logger, "LOG_DIR", str(tmpdir)) # Need to patch explicitly since the modified_rpm_files_diff is already instanciated in the fixture diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py index ed8bbfa928..f9797bcd97 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/backup_system_test.py @@ -26,9 +26,9 @@ from convert2rhel.actions.pre_ponr_changes import backup_system from convert2rhel.backup import files from convert2rhel.backup.files import RestorableFile -from convert2rhel.toolopts import PRE_RPM_VA_LOG_FILENAME from convert2rhel.unit_tests import CriticalErrorCallableObject from convert2rhel.unit_tests.conftest import all_systems, centos7, centos8 +from convert2rhel.utils.rpm import PRE_RPM_VA_LOG_FILENAME six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py index b477ed8804..af285b53df 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/handle_packages_test.py @@ -18,7 +18,7 @@ import pytest import six -from convert2rhel import actions, pkghandler, pkgmanager, unit_tests, utils +from convert2rhel import actions, pkghandler, pkgmanager, repo, unit_tests, utils from convert2rhel.actions.pre_ponr_changes import handle_packages from convert2rhel.systeminfo import system_info from convert2rhel.unit_tests import ( @@ -44,6 +44,11 @@ def list_third_party_packages_instance(): return handle_packages.ListThirdPartyPackages() +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(repo, "tool_opts", global_tool_opts) + + def test_list_third_party_packages_no_packages(list_third_party_packages_instance, monkeypatch, caplog): monkeypatch.setattr(pkghandler, "get_third_party_pkgs", GetThirdPartyPkgsMocked(pkg_selection="empty")) diff --git a/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py b/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py index 8792e15f77..29c0072d63 100644 --- a/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py +++ b/convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py @@ -31,6 +31,7 @@ from convert2rhel.backup.subscription import RestorableDisableRepositories, RestorableSystemSubscription from convert2rhel.subscription import RefreshSubscriptionManagerError, SubscriptionAutoAttachmentError from convert2rhel.unit_tests import AutoAttachSubscriptionMocked, RefreshSubscriptionManagerMocked, RunSubprocessMocked +from convert2rhel.utils import subscription as subscription_utils six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) @@ -57,6 +58,11 @@ def install_gpg_key_instance(): return appc_subscription.InstallRedHatGpgKeyForRpm() +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(subscription, "tool_opts", global_tool_opts) + + class TestInstallRedHatCertForYumRepositories: def test_run(self, monkeypatch, install_repo_cert_instance, restorable): monkeypatch.setattr(appc_subscription, "RestorablePEMCert", lambda x, y: restorable) @@ -228,7 +234,9 @@ def test_subscribe_system_do_not_subscribe(self, global_tool_opts, subscribe_sys # partial saves the real copy of tool_opts to use with # _should_subscribe so we have to monkeypatch with the mocked version # of tool_opts. - monkeypatch.setattr(subscription, "should_subscribe", partial(toolopts._should_subscribe, global_tool_opts)) + monkeypatch.setattr( + subscription, "should_subscribe", partial(subscription_utils._should_subscribe, global_tool_opts) + ) monkeypatch.setattr(RestorableSystemSubscription, "enable", mock.Mock()) monkeypatch.setattr(repo, "get_rhel_repoids", mock.Mock()) monkeypatch.setattr(RestorableDisableRepositories, "enable", mock.Mock()) @@ -249,7 +257,7 @@ def test_subscribe_system_no_rhsm_option_detected( # partial saves the real copy of tool_opts to use with # _should_subscribe so we have to monkeypatch with the mocked version # of tool_opts. - monkeypatch.setattr(subscription, "should_subscribe", partial(toolopts._should_subscribe, global_tool_opts)) + monkeypatch.setattr(subscription, "should_subscribe", partial(subscription._should_subscribe, global_tool_opts)) expected = set( ( diff --git a/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py b/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py index a752f4d2bf..23cc65c54e 100644 --- a/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/custom_repos_are_valid_test.py @@ -28,6 +28,11 @@ def custom_repos_are_valid_action(): return custom_repos_are_valid.CustomReposAreValid() +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(custom_repos_are_valid, "tool_opts", global_tool_opts) + + def test_custom_repos_are_valid(custom_repos_are_valid_action, monkeypatch, caplog): monkeypatch.setattr( custom_repos_are_valid, diff --git a/convert2rhel/unit_tests/actions/system_checks/els_test.py b/convert2rhel/unit_tests/actions/system_checks/els_test.py index 0a75e07d30..064bb0e54d 100644 --- a/convert2rhel/unit_tests/actions/system_checks/els_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/els_test.py @@ -35,6 +35,11 @@ def today(cls): return cls(2024, 6, 13) +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(els, "tool_opts", global_tool_opts) + + class TestEus: @pytest.mark.parametrize( ("version_string", "message_reported"), diff --git a/convert2rhel/unit_tests/actions/system_checks/eus_test.py b/convert2rhel/unit_tests/actions/system_checks/eus_test.py index a72d4378be..e103ebd13d 100644 --- a/convert2rhel/unit_tests/actions/system_checks/eus_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/eus_test.py @@ -35,6 +35,11 @@ def today(cls): return cls(2023, 11, 15) +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(eus, "tool_opts", global_tool_opts) + + class TestEus: @pytest.mark.parametrize( ("version_string", "message_reported"), diff --git a/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py b/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py index 51e6623d7f..b334a4f8ff 100644 --- a/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py +++ b/convert2rhel/unit_tests/actions/system_checks/is_loaded_kernel_latest_test.py @@ -40,6 +40,11 @@ def is_loaded_kernel_latest_action(): return is_loaded_kernel_latest.IsLoadedKernelLatest() +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(repo, "tool_opts", global_tool_opts) + + class TestIsLoadedKernelLatest: @oracle8 def test_is_loaded_kernel_latest_skip_on_not_latest_ol( diff --git a/convert2rhel/unit_tests/backup/packages_test.py b/convert2rhel/unit_tests/backup/packages_test.py index 41aa08d22d..2408412d0d 100644 --- a/convert2rhel/unit_tests/backup/packages_test.py +++ b/convert2rhel/unit_tests/backup/packages_test.py @@ -22,7 +22,7 @@ import pytest import six -from convert2rhel import exceptions, pkghandler, pkgmanager, unit_tests, utils +from convert2rhel import exceptions, pkghandler, pkgmanager, repo, unit_tests, utils from convert2rhel.backup import packages from convert2rhel.backup.packages import RestorablePackage, RestorablePackageSet from convert2rhel.systeminfo import Version @@ -40,6 +40,11 @@ from six.moves import mock +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(repo, "tool_opts", global_tool_opts) + + class DownloadPkgsMocked(MockFunctionObject): spec = utils.download_pkgs diff --git a/convert2rhel/unit_tests/backup/subscription_test.py b/convert2rhel/unit_tests/backup/subscription_test.py index d2b1dc92ba..42ec3bcd40 100644 --- a/convert2rhel/unit_tests/backup/subscription_test.py +++ b/convert2rhel/unit_tests/backup/subscription_test.py @@ -48,10 +48,11 @@ def system_subscription(self): return RestorableSystemSubscription() def test_subscribe_system(self, system_subscription, global_tool_opts, monkeypatch): - monkeypatch.setattr(subscription, "register_system", RegisterSystemMocked()) - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) global_tool_opts.username = "user" global_tool_opts.password = "pass" + monkeypatch.setattr(subscription, "tool_opts", global_tool_opts) + monkeypatch.setattr(subscription, "register_system", RegisterSystemMocked()) + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) system_subscription.enable() @@ -66,10 +67,11 @@ def test_subscribe_system_already_enabled(self, monkeypatch, system_subscription assert not subscription.register_system.called def test_enable_fail_once(self, system_subscription, global_tool_opts, caplog, monkeypatch): - monkeypatch.setattr(subscription, "register_system", RegisterSystemMocked()) - monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_code=1)) global_tool_opts.username = "user" global_tool_opts.password = "pass" + monkeypatch.setattr(subscription, "tool_opts", global_tool_opts) + monkeypatch.setattr(subscription, "register_system", RegisterSystemMocked()) + monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_code=1)) with pytest.raises(exceptions.CriticalError): system_subscription.enable() diff --git a/convert2rhel/unit_tests/breadcrumbs_test.py b/convert2rhel/unit_tests/breadcrumbs_test.py index c721511428..5e99b47b11 100644 --- a/convert2rhel/unit_tests/breadcrumbs_test.py +++ b/convert2rhel/unit_tests/breadcrumbs_test.py @@ -49,12 +49,14 @@ def _mock_pkg_information(): @pytest.fixture -def breadcrumbs_instance(_mock_pkg_obj, _mock_pkg_information, monkeypatch): +def breadcrumbs_instance(_mock_pkg_obj, _mock_pkg_information, global_tool_opts, monkeypatch): monkeypatch.setattr(pkgmanager, "TYPE", "yum") monkeypatch.setattr(breadcrumbs.breadcrumbs, "_pkg_object", _mock_pkg_obj) monkeypatch.setattr(pkghandler, "get_installed_pkg_objects", lambda name: [_mock_pkg_obj]) monkeypatch.setattr(pkghandler, "get_installed_pkg_information", lambda name: [_mock_pkg_information]) monkeypatch.setenv("CONVERT2RHEL_FOO_BAR", "1") + global_tool_opts.activity = "analysis" + monkeypatch.setattr(breadcrumbs, "tool_opts", global_tool_opts) breadcrumbs.breadcrumbs.collect_early_data() yield breadcrumbs.Breadcrumbs() @@ -69,9 +71,7 @@ def finish_collection_mocks(): @centos7 -def test_collect_early_data(pretend_os, breadcrumbs_instance, global_tool_opts): - global_tool_opts.activity = "analysis" - +def test_collect_early_data(pretend_os, breadcrumbs_instance): breadcrumbs_instance.collect_early_data() # Asserting that the populated fields are not null (or None), the value diff --git a/convert2rhel/unit_tests/checks_test.py b/convert2rhel/unit_tests/checks_test.py index 5ad79b24cb..271dd265d3 100644 --- a/convert2rhel/unit_tests/checks_test.py +++ b/convert2rhel/unit_tests/checks_test.py @@ -17,15 +17,9 @@ __metaclass__ = type import pytest -import six from convert2rhel import checks from convert2rhel.unit_tests import RunSubprocessMocked -from convert2rhel.unit_tests.conftest import centos8 - - -six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) -from six.moves import mock @pytest.mark.parametrize( diff --git a/convert2rhel/unit_tests/cli_test.py b/convert2rhel/unit_tests/cli_test.py new file mode 100644 index 0000000000..2525ee2cd6 --- /dev/null +++ b/convert2rhel/unit_tests/cli_test.py @@ -0,0 +1,631 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2016 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +import os +import sys + +import pytest +import six + +from convert2rhel import cli, toolopts + + +six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) +from six.moves import mock + + +def mock_cli_arguments(args): + """ + Return a list of cli arguments where the first one is always the name of + the executable, followed by 'args'. + """ + return sys.argv[0:1] + args + + +@pytest.fixture(autouse=True) +def reset_tool_opts(monkeypatch): + monkeypatch.setattr(cli, "tool_opts", toolopts.ToolOpts()) + + +@pytest.fixture(autouse=True) +def apply_fileconfig_mock(monkeypatch): + monkeypatch.setattr(cli, "FileConfig", mock.Mock()) + + +class TestTooloptsParseFromCLI: + def test_cmdline_interactive_username_without_passwd(self, monkeypatch): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--username", "uname"])) + cli.CLI() + assert cli.tool_opts.username == "uname" + + def test_cmdline_interactive_passwd_without_uname(self, monkeypatch): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--password", "passwd"])) + cli.CLI() + assert cli.tool_opts.password == "passwd" + + def test_cmdline_non_interactive_with_credentials(self, monkeypatch): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--username", "uname", "--password", "passwd"])) + cli.CLI() + assert cli.tool_opts.username == "uname" + assert cli.tool_opts.password == "passwd" + + def test_cmdline_disablerepo_defaults_to_asterisk(self, monkeypatch, global_tool_opts): + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--enablerepo", "foo"])) + cli.CLI() + assert cli.tool_opts.enablerepo == ["foo"] + assert cli.tool_opts.disablerepo == ["*"] + + # Parsing of serverurl + + @pytest.mark.parametrize( + ("serverurl", "hostname", "port", "prefix"), + ( + ("https://rhsm.redhat.com:443/", "rhsm.redhat.com", "443", "/"), + ("https://localhost/rhsm/", "localhost", None, "/rhsm/"), + ("https://rhsm.redhat.com/", "rhsm.redhat.com", None, "/"), + ("https://rhsm.redhat.com", "rhsm.redhat.com", None, None), + ("https://rhsm.redhat.com:8443", "rhsm.redhat.com", "8443", None), + ("subscription.redhat.com", "subscription.redhat.com", None, None), + ), + ) + def test_custom_serverurl(self, monkeypatch, global_tool_opts, serverurl, hostname, port, prefix): + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr( + sys, + "argv", + mock_cli_arguments(["--serverurl", serverurl, "--username", "User1", "--password", "Password1"]), + ) + cli.CLI() + assert global_tool_opts.rhsm_hostname == hostname + assert global_tool_opts.rhsm_port == port + assert global_tool_opts.rhsm_prefix == prefix + + def test_no_serverurl(self, monkeypatch, global_tool_opts): + monkeypatch.setattr(sys, "argv", mock_cli_arguments([])) + cli.CLI() + assert global_tool_opts.rhsm_hostname is None + assert global_tool_opts.rhsm_port is None + assert global_tool_opts.rhsm_prefix is None + + @pytest.mark.parametrize( + "serverurl", + ( + "gopher://subscription.rhsm.redhat.com/", + "https:///", + "https://", + "/", + ), + ) + def test_bad_serverurl(self, caplog, monkeypatch, serverurl): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--serverurl", serverurl, "-o", "MyOrg", "-k", "012335"])) + + with pytest.raises(SystemExit): + cli.CLI() + + message = ( + "Failed to parse a valid subscription-manager server from the --serverurl option.\n" + "Please check for typos and run convert2rhel again with a corrected --serverurl.\n" + "Supplied serverurl: %s\nError: " % serverurl + ) + assert message in caplog.records[-1].message + assert caplog.records[-1].levelname == "CRITICAL" + + def test_serverurl_with_no_rhsm(self, caplog, monkeypatch): + monkeypatch.setattr( + sys, "argv", mock_cli_arguments(["--serverurl", "localhost", "--no-rhsm", "--enablerepo", "testrepo"]) + ) + + cli.CLI() + + message = "Ignoring the --serverurl option. It has no effect when --no-rhsm is used." + assert message in caplog.text + + def test_serverurl_with_no_rhsm_credentials(self, caplog, monkeypatch): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--serverurl", "localhost"])) + + cli.CLI() + + message = ( + "Ignoring the --serverurl option. It has no effect when no credentials to" + " subscribe the system were given." + ) + assert message in caplog.text + + +def test_no_rhsm_option_system_exit_exception(monkeypatch, global_tool_opts): + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--no-rhsm"])) + + with pytest.raises(SystemExit, match="The --enablerepo option is required when --no-rhsm is used."): + cli.CLI() + + +@pytest.mark.parametrize( + ("argv", "no_rhsm_value"), + ((mock_cli_arguments(["--no-rhsm", "--enablerepo", "test_repo"]), True),), +) +def test_no_rhsm_option_work(argv, no_rhsm_value, monkeypatch, global_tool_opts): + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(sys, "argv", argv) + + cli.CLI() + + assert global_tool_opts.enablerepo == ["test_repo"] + assert global_tool_opts.no_rhsm == no_rhsm_value + + +@pytest.mark.parametrize( + ("argv", "content", "output", "message"), + ( + pytest.param( + mock_cli_arguments([]), + """\ +[subscription_manager] +username = conf_user +password = conf_pass +activation_key = conf_key +org = conf_org + +[host_metering] +configure_host_metering = "auto" + +[inhibitor_overrides] +incomplete_rollback = 0 +tainted_kernel_module_check_skip = 0 +outdated_package_check_skip = 0 +allow_older_version = 0 +allow_unavailable_kmods = 0 +skip_kernel_currency_check = 0 + """, + { + "username": "conf_user", + "password": "conf_pass", + "activation_key": "conf_key", + "org": "conf_org", + "configure_host_metering": False, + "incomplete_rollback": False, + "tainted_kernel_module_check_skip": False, + "outdated_package_skip": False, + "allow_older_version": False, + "allow_unavailable_kmods": False, + "skip_kernel_currency_check": False, + }, + None, + id="All values set in config", + ), + ( + mock_cli_arguments([]), + """\ +[subscription_manager] +password = conf_pass + """, + {"password": "conf_pass"}, + None, + ), + ( + mock_cli_arguments([]), + """\ +[subscription_manager] +password = conf_pass + +[inhibitor_overrides] +incomplete_rollback = 1 + """, + {"password": "conf_pass", "incomplete_rollback": True}, + None, + ), + ( + mock_cli_arguments(["-p", "password"]), + """\ +[subscription_manager] +activation_key = conf_key + """, + {"password": "password"}, + None, + ), + ( + mock_cli_arguments(["-k", "activation_key", "-o", "org"]), + """\ +[subscription_manager] +activation_key = conf_key + """, + {"activation_key": "activation_key", "org": "org"}, + "You have passed the RHSM activation key through both the command line and the" + " configuration file. We're going to use the command line values.", + ), + ( + mock_cli_arguments(["-k", "activation_key", "-o", "org"]), + """\ +[subscription_manager] +password = conf_pass + """, + {"password": "conf_pass", "activation_key": "activation_key"}, + None, + ), + ( + mock_cli_arguments(["-k", "activation_key", "-p", "password", "-o", "org"]), + """\ +[subscription_manager] +password = conf_pass +activation_key = conf_key + """, + {"password": "password"}, + "You have passed the RHSM password without an associated username. Please provide a username together with the password.", + ), + ( + mock_cli_arguments(["-o", "org"]), + """\ +[subscription_manager] +password = conf_pass +activation_key = conf_key + """, + {"password": "conf_pass", "activation_key": "conf_key", "org": "org"}, + "Either a password or an activation key can be used for system registration. We're going to use the" + " activation key.", + ), + ( + mock_cli_arguments(["-u", "McLOVIN"]), + """\ +[subscription_manager] +username = NotMcLOVIN + """, + {"username": "McLOVIN"}, + "You have passed the RHSM username through both the command line and" + " the configuration file. We're going to use the command line values.", + ), + ( + mock_cli_arguments(["-o", "some-org"]), + """\ +[subscription_manager] +org = a-different-org +activation_key = conf_key + """, + {"org": "some-org"}, + "You have passed the RHSM org through both the command line and" + " the configuration file. We're going to use the command line values.", + ), + ), +) +def test_config_file(argv, content, output, message, monkeypatch, tmpdir, caplog): + # After each test there were left data from previous + # Re-init needed delete the set data + path = os.path.join(str(tmpdir), "convert2rhel.ini") + with open(path, "w") as file: + file.write(content) + os.chmod(path, 0o600) + + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr(cli, "FileConfig", toolopts.config.FileConfig) + monkeypatch.setattr(toolopts.config.FileConfig, "DEFAULT_CONFIG_FILES", value=[path]) + cli.CLI() + + if "activation_key" in output: + assert cli.tool_opts.activation_key == output["activation_key"] + + if "password" in output: + assert cli.tool_opts.password == output["password"] + + if "username" in output: + assert cli.tool_opts.username == output["username"] + + if "org" in output: + assert cli.tool_opts.org == output["org"] + + if message: + assert message in caplog.text + + +@pytest.mark.parametrize( + ("argv", "content", "message", "output"), + ( + ( + mock_cli_arguments(["--password", "pass", "--config-file"]), + "[subscription_manager]\nactivation_key = key_cnf_file", + "You have passed either the RHSM password or activation key through both the command line and" + " the configuration file. We're going to use the command line values.", + {"password": "pass", "activation_key": None}, + ), + ), +) +def test_multiple_auth_src_combined(argv, content, message, output, caplog, monkeypatch, tmpdir): + """Test combination of password file or configuration file and CLI arguments.""" + path = os.path.join(str(tmpdir), "convert2rhel.file") + with open(path, "w") as file: + file.write(content) + os.chmod(path, 0o600) + # The path for file is the last argument + argv.append(path) + + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr(cli, "FileConfig", toolopts.config.FileConfig) + monkeypatch.setattr(toolopts.config.FileConfig, "DEFAULT_CONFIG_FILES", value=[path]) + cli.CLI() + + assert message in caplog.text + assert cli.tool_opts.activation_key == output["activation_key"] + assert cli.tool_opts.password == output["password"] + + +@pytest.mark.parametrize( + ("argv", "message", "output"), + ( + ( + mock_cli_arguments(["--password", "pass", "--activationkey", "key", "-o", "org"]), + "Either a password or an activation key can be used for system registration." + " We're going to use the activation key.", + {"password": "pass", "activation_key": "key"}, + ), + ), +) +def test_multiple_auth_src_cli(argv, message, output, caplog, monkeypatch): + """Test both auth methods in CLI.""" + monkeypatch.setattr(sys, "argv", argv) + cli.CLI() + + assert message in caplog.text + assert cli.tool_opts.activation_key == output["activation_key"] + assert cli.tool_opts.password == output["password"] + + +def test_log_command_used(caplog, monkeypatch): + obfuscation_string = "*" * 5 + input_command = mock_cli_arguments( + ["--username", "uname", "--password", "123", "--activationkey", "456", "--org", "789"] + ) + expected_command = mock_cli_arguments( + [ + "--username", + obfuscation_string, + "--password", + obfuscation_string, + "--activationkey", + obfuscation_string, + "--org", + obfuscation_string, + ] + ) + monkeypatch.setattr(sys, "argv", input_command) + cli._log_command_used() + + assert " ".join(expected_command) in caplog.records[-1].message + + +@pytest.mark.parametrize( + ("argv", "message"), + ( + ( + mock_cli_arguments(["-o", "org"]), + "Either the --org or the --activationkey option is missing. You can't use one without the other.", + ), + ( + mock_cli_arguments(["-k", "key"]), + "Either the --org or the --activationkey option is missing. You can't use one without the other.", + ), + ), +) +def test_org_activation_key_specified(argv, message, monkeypatch, caplog): + monkeypatch.setattr(sys, "argv", argv) + + try: + cli.CLI() + except SystemExit: + # Don't care about the exception, focus on output message + pass + + assert message in caplog.records[-1].message + + +@pytest.mark.parametrize( + ("argv", "expected"), + ( + (mock_cli_arguments(["convert"]), "conversion"), + (mock_cli_arguments(["analyze"]), "analysis"), + (mock_cli_arguments([]), "conversion"), + ), +) +def test_pre_assessment_set(argv, expected, monkeypatch, global_tool_opts): + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(sys, "argv", argv) + + cli.CLI() + + assert cli.tool_opts.activity == expected + + +@pytest.mark.parametrize( + ("argv", "expected"), + ( + ( + mock_cli_arguments(["--disablerepo", "*", "--enablerepo", "*"]), + "Duplicate repositories were found across disablerepo and enablerepo options", + ), + ( + mock_cli_arguments( + ["--disablerepo", "*", "--disablerepo", "rhel-7-extras-rpm", "--enablerepo", "rhel-7-extras-rpm"] + ), + "Duplicate repositories were found across disablerepo and enablerepo options", + ), + ( + mock_cli_arguments(["--disablerepo", "test", "--enablerepo", "test"]), + "Duplicate repositories were found across disablerepo and enablerepo options", + ), + ), +) +def test_disable_and_enable_repos_has_same_repo(argv, expected, monkeypatch, caplog): + monkeypatch.setattr(sys, "argv", argv) + cli.CLI() + + assert expected in caplog.records[-1].message + + +@pytest.mark.parametrize( + ("argv", "expected"), + ( + ( + mock_cli_arguments(["--disablerepo", "*", "--enablerepo", "test"]), + "Duplicate repositories were found across disablerepo and enablerepo options", + ), + ( + mock_cli_arguments(["--disablerepo", "test", "--enablerepo", "test1"]), + "Duplicate repositories were found across disablerepo and enablerepo options", + ), + ), +) +def test_disable_and_enable_repos_with_different_repos(argv, expected, monkeypatch, caplog): + monkeypatch.setattr(sys, "argv", argv) + cli.CLI() + + assert expected not in caplog.records[-1].message + + +@pytest.mark.parametrize( + ("argv", "expected"), + ( + ([], ["convert"]), + (["--debug"], ["convert", "--debug"]), + (["analyze", "--debug"], ["analyze", "--debug"]), + (["--password=convert", "--debug"], ["convert", "--password=convert", "--debug"]), + ), +) +def test_add_default_command(argv, expected, monkeypatch): + monkeypatch.setattr(sys, "argv", argv) + assert cli._add_default_command(argv) == expected + + +@pytest.mark.parametrize( + ("argv", "message"), + ( + ( + mock_cli_arguments(["analyze", "--no-rpm-va"]), + "We will proceed with ignoring the --no-rpm-va option as running rpm -Va in the analysis mode is essential for a complete rollback to the original system state at the end of the analysis.", + ), + ), +) +def test_override_no_rpm_va_setting(monkeypatch, argv, message, caplog): + monkeypatch.setattr(sys, "argv", argv) + cli.CLI() + + assert caplog.records[-1].message == message + assert not cli.tool_opts.no_rpm_va + + +def test_critical_exit_no_rpm_va_setting(monkeypatch, global_tool_opts, tmpdir): + # After each test there were left data from previous + # Re-init needed delete the set data + path = os.path.join(str(tmpdir), "convert2rhel.ini") + with open(path, "w") as file: + content = "" + file.write(content) + + os.chmod(path, 0o600) + + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(cli, "FileConfig", toolopts.config.FileConfig) + monkeypatch.setattr(toolopts.config.FileConfig, "DEFAULT_CONFIG_FILES", value=[path]) + monkeypatch.setattr(cli, "tool_opts", global_tool_opts) + monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--no-rpm-va"])) + with pytest.raises( + SystemExit, + match="We need to run the 'rpm -Va' command to be able to perform a complete rollback of changes done to the system during the pre-conversion analysis. If you accept the risk of an incomplete rollback, set the CONVERT2RHEL_INCOMPLETE_ROLLBACK=1 environment variable. Otherwise, remove the --no-rpm-va option.", + ): + cli.CLI() + + assert cli.tool_opts.no_rpm_va + + +@pytest.mark.parametrize( + ("argv", "expected", "message"), + ( + ( + ["analyze", "--no-rpm-va"], + False, + "We will proceed with ignoring the --no-rpm-va option as running rpm -Va in the analysis mode is essential for a complete rollback to the original system state at the end of the analysis.", + ), + (["--no-rpm-va"], True, ""), + ), +) +def test_setting_no_rpm_va(argv, expected, message, monkeypatch, caplog, tmpdir): + # After each test there were left data from previous + # Re-init needed delete the set data + path = os.path.join(str(tmpdir), "convert2rhel.ini") + with open(path, "w") as file: + content = """\ +[inhibitor_overrides] +incomplete_rollback = 1 +""" + file.write(content) + + os.chmod(path, 0o600) + + monkeypatch.setattr(cli, "FileConfig", toolopts.config.FileConfig) + monkeypatch.setattr(toolopts.config.FileConfig, "DEFAULT_CONFIG_FILES", value=[path]) + monkeypatch.setattr(sys, "argv", mock_cli_arguments(argv)) + + cli.CLI() + + assert cli.tool_opts.no_rpm_va == expected + assert cli.tool_opts.incomplete_rollback + + if message: + assert caplog.records[-1].message == message + + +@pytest.mark.parametrize( + ("argv", "message"), + ( + # The message is a log of used command + (mock_cli_arguments(["-u", "user", "-p", "pass"]), "-u ***** -p *****"), + ( + mock_cli_arguments(["-p", "pass"]), + "You have passed the RHSM password without an associated username. Please provide a username together with the password", + ), + ( + mock_cli_arguments(["-u", "user"]), + "You have passed the RHSM username without an associated password. Please provide a password together with the username", + ), + ), +) +def test_cli_userpass_specified(argv, message, monkeypatch, caplog): + monkeypatch.setattr(sys, "argv", argv) + + try: + cli.CLI() + except SystemExit: + # Don't care about the exception, focus on output message + pass + assert message in caplog.text + + +@pytest.mark.parametrize( + ("activation_key", "organization", "argv"), + ( + ("activation_key", "org", ["analyze", "-u name", "-p pass"]), + (None, None, ["analyze", "-u name", "-p pass"]), + ), +) +def test_cli_args_config_file_cornercase(activation_key, organization, argv, monkeypatch, global_tool_opts): + monkeypatch.setattr(sys, "argv", mock_cli_arguments(argv)) + global_tool_opts.org = organization + global_tool_opts.activation_key = activation_key + global_tool_opts.no_rhsm = True + monkeypatch.setattr(toolopts, "tool_opts", global_tool_opts) + + # Make sure it doesn't raise an exception + cli.CLI() diff --git a/convert2rhel/unit_tests/conftest.py b/convert2rhel/unit_tests/conftest.py index 06354a7d36..a461eb1e71 100644 --- a/convert2rhel/unit_tests/conftest.py +++ b/convert2rhel/unit_tests/conftest.py @@ -11,7 +11,6 @@ from convert2rhel.backup.certs import RestorablePEMCert from convert2rhel.logger import setup_logger_handler from convert2rhel.systeminfo import system_info -from convert2rhel.toolopts import tool_opts from convert2rhel.unit_tests import MinimalRestorable @@ -108,9 +107,41 @@ def sys_path(): sys.path = real_sys_path +class CliConfigMock: + SOURCE = "command line" + + def __init__(self): + self.debug = None + self.username = None + self.password = None + self.org = None + self.activation_key = None + self.config_file = None + self.no_rhsm = None + self.enablerepo = [] + self.disablerepo = [] + self.pool = None + self.rhsm_hostname = None + self.rhsm_port = None + self.rhsm_prefix = None + self.autoaccept = True + self.auto_attach = None + self.restart = None + self.arch = None + self.no_rpm_va = None + self.eus = None + self.els = None + self.activity = None + self.serverurl = None + + def run(self): + pass + + @pytest.fixture def global_tool_opts(monkeypatch): local_tool_opts = toolopts.ToolOpts() + local_tool_opts.initialize(config_sources=[CliConfigMock()]) monkeypatch.setattr(toolopts, "tool_opts", local_tool_opts) return local_tool_opts @@ -130,7 +161,7 @@ def global_backup_control(monkeypatch): @pytest.fixture() -def pretend_os(request, pkg_root, monkeypatch): +def pretend_os(request, pkg_root, monkeypatch, global_tool_opts): """Parametric fixture to pretend to be one of the available OSes for conversion. See https://docs.pytest.org/en/6.2.x/example/parametrize.html#indirect-parametrization @@ -189,6 +220,8 @@ def pretend_os(request, pkg_root, monkeypatch): system_version, system_name = request.param system_version_major, system_version_minor, _ = system_version.split(".") + global_tool_opts.no_rpm_va = True + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) monkeypatch.setattr( utils, "DATA_DIR", @@ -209,7 +242,6 @@ def pretend_os(request, pkg_root, monkeypatch): "_get_architecture", value=lambda: "x86_64", ) - monkeypatch.setattr(tool_opts, "no_rpm_va", True) # We can't depend on a test environment (containers) having an init system so we have to # disable probing for the right value by hardcoding an anwer @@ -218,7 +250,6 @@ def pretend_os(request, pkg_root, monkeypatch): "_is_dbus_running", value=lambda: True, ) - monkeypatch.setattr(system_info, "releasever", value=system_version_major) system_info.resolve_system_info() diff --git a/convert2rhel/unit_tests/logger_test.py b/convert2rhel/unit_tests/logger_test.py index a45461844a..ed7d536312 100644 --- a/convert2rhel/unit_tests/logger_test.py +++ b/convert2rhel/unit_tests/logger_test.py @@ -53,30 +53,6 @@ def test_logger_handlers(monkeypatch, tmpdir, read_std, global_tool_opts): assert "Test debug: other data" in log_f.readline().rstrip() -def test_tools_opts_debug(monkeypatch, read_std, is_py2, global_tool_opts): - monkeypatch.setattr("convert2rhel.toolopts.tool_opts", global_tool_opts) - logger_module.setup_logger_handler() - logger = logger_module.root_logger.getChild(__name__) - global_tool_opts.debug = True - logger.debug("debug entry 1: %s", "data") - stdouterr_out, stdouterr_err = read_std() - # TODO should be in stdout, but this only works when running this test - # alone (see https://github.com/pytest-dev/pytest/issues/5502) - try: - assert "debug entry 1: data" in stdouterr_out - except AssertionError: - if not is_py2: - assert "debug entry 1: data" in stdouterr_err - else: - # this workaround is not working for py2 - passing - pass - - global_tool_opts.debug = False - logger.debug("debug entry 2: %s", "data") - stdouterr_out, stdouterr_err = read_std() - assert "debug entry 2: data" not in stdouterr_out - - class Testroot_logger: @pytest.mark.parametrize( ("log_method_name", "level_name"), diff --git a/convert2rhel/unit_tests/main_test.py b/convert2rhel/unit_tests/main_test.py index a3348c234e..1715b1a481 100644 --- a/convert2rhel/unit_tests/main_test.py +++ b/convert2rhel/unit_tests/main_test.py @@ -27,7 +27,7 @@ six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) from six.moves import mock -from convert2rhel import actions, applock, backup, exceptions +from convert2rhel import actions, applock, backup, cli, exceptions from convert2rhel import logger as logger_module from convert2rhel import main, pkghandler, pkgmanager, subscription, toolopts, utils from convert2rhel.actions import report @@ -51,6 +51,11 @@ ) +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(main, "tool_opts", global_tool_opts) + + class TestRollbackChanges: def test_rollback_changes(self, monkeypatch, global_backup_control): monkeypatch.setattr(global_backup_control, "pop_all", mock.Mock()) @@ -225,10 +230,10 @@ def test_help_exit(monkeypatch, tmp_path): assert main.main_locked.call_count == 0 -def test_main(monkeypatch, tmp_path): +def test_main(monkeypatch, global_tool_opts, tmp_path): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock() print_data_collection_mock = mock.Mock() resolve_system_info_mock = mock.Mock() @@ -245,10 +250,11 @@ def test_main(monkeypatch, tmp_path): summary_as_json_mock = mock.Mock() summary_as_txt_mock = mock.Mock() + monkeypatch.setattr(subscription, "tool_opts", global_tool_opts) monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "print_data_collection", print_data_collection_mock) monkeypatch.setattr(system_info, "resolve_system_info", resolve_system_info_mock) @@ -268,7 +274,7 @@ def test_main(monkeypatch, tmp_path): assert main.main() == 0 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert print_data_collection_mock.call_count == 1 assert resolve_system_info_mock.call_count == 1 @@ -289,21 +295,21 @@ class TestRollbackFromMain: def test_main_rollback_post_cli_phase(self, monkeypatch, caplog, tmp_path): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock(side_effect=Exception) finish_collection_mock = mock.Mock() monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "finish_collection", finish_collection_mock) assert main.main() == 1 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert finish_collection_mock.call_count == 1 assert "No changes were made to the system." in caplog.records[-2].message @@ -312,7 +318,7 @@ def test_main_traceback_in_clear_versionlock(self, caplog, monkeypatch, tmp_path monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", RequireRootMocked()) monkeypatch.setattr(main, "initialize_file_logging", InitializeFileLoggingMocked()) - monkeypatch.setattr(toolopts, "CLI", CLIMocked()) + monkeypatch.setattr(cli, "CLI", CLIMocked()) monkeypatch.setattr(main, "show_eula", ShowEulaMocked()) monkeypatch.setattr(breadcrumbs, "print_data_collection", PrintDataCollectionMocked()) monkeypatch.setattr(system_info, "resolve_system_info", ResolveSystemInfoMocked()) @@ -337,7 +343,7 @@ def test_main_traceback_in_clear_versionlock(self, caplog, monkeypatch, tmp_path assert main.main() == 1 assert utils.require_root.call_count == 1 - assert toolopts.CLI.call_count == 1 + assert cli.CLI.call_count == 1 assert main.show_eula.call_count == 1 assert breadcrumbs.print_data_collection.call_count == 1 assert system_info.resolve_system_info.call_count == 1 @@ -357,7 +363,7 @@ def test_main_traceback_in_clear_versionlock(self, caplog, monkeypatch, tmp_path def test_main_traceback_before_action_completion(self, monkeypatch, caplog, tmp_path): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock() print_data_collection_mock = mock.Mock() resolve_system_info_mock = mock.Mock() @@ -378,7 +384,7 @@ def test_main_traceback_before_action_completion(self, monkeypatch, caplog, tmp_ monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "print_data_collection", print_data_collection_mock) monkeypatch.setattr(system_info, "resolve_system_info", resolve_system_info_mock) @@ -397,7 +403,7 @@ def test_main_traceback_before_action_completion(self, monkeypatch, caplog, tmp_ assert main.main() == 1 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert print_data_collection_mock.call_count == 1 assert resolve_system_info_mock.call_count == 1 @@ -417,10 +423,10 @@ def test_main_traceback_before_action_completion(self, monkeypatch, caplog, tmp_ ) assert "Action Framework Crashed" in caplog.records[-3].message - def test_main_rollback_pre_ponr_changes_phase(self, monkeypatch, caplog, tmp_path): + def test_main_rollback_pre_ponr_changes_phase(self, monkeypatch, tmp_path, global_tool_opts): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock() print_data_collection_mock = mock.Mock() resolve_system_info_mock = mock.Mock() @@ -443,7 +449,7 @@ def test_main_rollback_pre_ponr_changes_phase(self, monkeypatch, caplog, tmp_pat monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "print_data_collection", print_data_collection_mock) monkeypatch.setattr(system_info, "resolve_system_info", resolve_system_info_mock) @@ -464,7 +470,7 @@ def test_main_rollback_pre_ponr_changes_phase(self, monkeypatch, caplog, tmp_pat assert main.main() == 2 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert print_data_collection_mock.call_count == 1 assert resolve_system_info_mock.call_count == 1 @@ -494,7 +500,7 @@ def test_main_rollback_analyze_exit_phase_without_subman(self, global_tool_opts, (applock, "_DEFAULT_LOCK_DIR", str(tmp_path)), (utils, "require_root", mock.Mock()), (main, "initialize_file_logging", mock.Mock()), - (toolopts, "CLI", mock.Mock()), + (cli, "CLI", mock.Mock()), (main, "show_eula", mock.Mock()), (breadcrumbs, "print_data_collection", mock.Mock()), (system_info, "resolve_system_info", mock.Mock()), @@ -517,7 +523,7 @@ def test_main_rollback_analyze_exit_phase_without_subman(self, global_tool_opts, assert main.main() == 0 assert utils.require_root.call_count == 1 - assert toolopts.CLI.call_count == 1 + assert cli.CLI.call_count == 1 assert main.show_eula.call_count == 1 assert breadcrumbs.print_data_collection.call_count == 1 assert system_info.resolve_system_info.call_count == 1 @@ -537,7 +543,7 @@ def test_main_rollback_analyze_exit_phase_without_subman(self, global_tool_opts, def test_main_rollback_analyze_exit_phase(self, global_tool_opts, monkeypatch, tmp_path): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock() print_data_collection_mock = mock.Mock() resolve_system_info_mock = mock.Mock() @@ -559,7 +565,7 @@ def test_main_rollback_analyze_exit_phase(self, global_tool_opts, monkeypatch, t monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "print_data_collection", print_data_collection_mock) monkeypatch.setattr(system_info, "resolve_system_info", resolve_system_info_mock) @@ -580,7 +586,7 @@ def test_main_rollback_analyze_exit_phase(self, global_tool_opts, monkeypatch, t assert main.main() == 0 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert print_data_collection_mock.call_count == 1 assert resolve_system_info_mock.call_count == 1 @@ -599,7 +605,7 @@ def test_main_rollback_analyze_exit_phase(self, global_tool_opts, monkeypatch, t def test_main_rollback_post_ponr_changes_phase(self, monkeypatch, caplog, tmp_path): require_root_mock = mock.Mock() initialize_file_logging_mock = mock.Mock() - toolopts_cli_mock = mock.Mock() + cli_cli_mock = mock.Mock() show_eula_mock = mock.Mock() print_data_collection_mock = mock.Mock() resolve_system_info_mock = mock.Mock() @@ -623,7 +629,7 @@ def test_main_rollback_post_ponr_changes_phase(self, monkeypatch, caplog, tmp_pa monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmp_path)) monkeypatch.setattr(utils, "require_root", require_root_mock) monkeypatch.setattr(main, "initialize_file_logging", initialize_file_logging_mock) - monkeypatch.setattr(toolopts, "CLI", toolopts_cli_mock) + monkeypatch.setattr(cli, "CLI", cli_cli_mock) monkeypatch.setattr(main, "show_eula", show_eula_mock) monkeypatch.setattr(breadcrumbs, "print_data_collection", print_data_collection_mock) monkeypatch.setattr(system_info, "resolve_system_info", resolve_system_info_mock) @@ -645,7 +651,7 @@ def test_main_rollback_post_ponr_changes_phase(self, monkeypatch, caplog, tmp_pa assert main.main() == 1 assert require_root_mock.call_count == 1 assert initialize_file_logging_mock.call_count == 1 - assert toolopts_cli_mock.call_count == 1 + assert cli_cli_mock.call_count == 1 assert show_eula_mock.call_count == 1 assert print_data_collection_mock.call_count == 1 assert resolve_system_info_mock.call_count == 1 @@ -745,7 +751,7 @@ def test_raise_for_skipped_failures(data, exception, match, activity, global_too def test_main_already_running_conversion(monkeypatch, caplog, tmpdir): - monkeypatch.setattr(toolopts, "CLI", mock.Mock()) + monkeypatch.setattr(cli, "CLI", mock.Mock()) monkeypatch.setattr(utils, "require_root", mock.Mock()) monkeypatch.setattr(applock, "_DEFAULT_LOCK_DIR", str(tmpdir)) monkeypatch.setattr(main, "main_locked", mock.Mock(side_effect=applock.ApplicationLockedError("failed"))) diff --git a/convert2rhel/unit_tests/pkghandler_test.py b/convert2rhel/unit_tests/pkghandler_test.py index ce16113877..c4ce17d9c0 100644 --- a/convert2rhel/unit_tests/pkghandler_test.py +++ b/convert2rhel/unit_tests/pkghandler_test.py @@ -27,7 +27,7 @@ import rpm import six -from convert2rhel import pkghandler, pkgmanager, unit_tests, utils +from convert2rhel import pkghandler, pkgmanager, repo, systeminfo, unit_tests, utils from convert2rhel.backup.certs import RestorableRpmKey from convert2rhel.backup.files import RestorableFile from convert2rhel.pkghandler import ( @@ -38,7 +38,6 @@ get_total_packages_to_update, ) from convert2rhel.systeminfo import system_info -from convert2rhel.toolopts import tool_opts from convert2rhel.unit_tests import ( CallYumCmdMocked, DownloadPkgMocked, @@ -229,6 +228,11 @@ def test_get_rpm_header_failure(self, monkeypatch): pkghandler.get_rpm_header(unknown_pkg) +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) + + class TestGetKernelAvailability: @pytest.mark.parametrize( ("subprocess_output", "expected_installed", "expected_available"), @@ -248,7 +252,7 @@ class TestGetKernelAvailability: ) @centos7 def test_get_kernel_availability( - self, pretend_os, subprocess_output, expected_installed, expected_available, monkeypatch + self, pretend_os, subprocess_output, expected_installed, expected_available, monkeypatch, global_tool_opts ): monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked(return_string=subprocess_output)) @@ -310,6 +314,11 @@ def test_handle_older_rhel_kernel_not_available_multiple_installed(self, pretend class TestReplaceNonRHELInstalledKernel: + @pytest.fixture(autouse=True) + def apply_global_tool_opts(self, monkeypatch, global_tool_opts): + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) + monkeypatch.setattr(pkghandler, "tool_opts", global_tool_opts) + def test_replace_non_rhel_installed_kernel_rhsm_repos(self, monkeypatch): monkeypatch.setattr(system_info, "submgr_enabled_repos", ["enabled_rhsm_repo"]) monkeypatch.setattr(utils, "ask_to_continue", mock.Mock()) @@ -332,10 +341,11 @@ def test_replace_non_rhel_installed_kernel_rhsm_repos(self, monkeypatch): "%skernel-4.7.4-200.fc24*" % utils.TMP_DIR, ] - def test_replace_non_rhel_installed_kernel_custom_repos(self, monkeypatch): + def test_replace_non_rhel_installed_kernel_custom_repos(self, monkeypatch, global_tool_opts): + global_tool_opts.enablerepo = ["custom_repo"] + global_tool_opts.no_rhsm = True + monkeypatch.setattr(pkghandler, "tool_opts", global_tool_opts) monkeypatch.setattr(system_info, "submgr_enabled_repos", []) - monkeypatch.setattr(tool_opts, "enablerepo", ["custom_repo"]) - monkeypatch.setattr(tool_opts, "no_rhsm", True) monkeypatch.setattr(utils, "ask_to_continue", mock.Mock()) monkeypatch.setattr(utils, "download_pkg", DownloadPkgMocked()) monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) @@ -729,7 +739,9 @@ def test_get_total_packages_to_update( expected, pretend_os, monkeypatch, + global_tool_opts, ): + monkeypatch.setattr(repo, "tool_opts", global_tool_opts) monkeypatch.setattr(pkgmanager, "TYPE", package_manager_type) if package_manager_type == "dnf": monkeypatch.setattr( diff --git a/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py b/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py index 4aed5321b3..d7fd21788b 100644 --- a/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py +++ b/convert2rhel/unit_tests/pkgmanager/pkgmanager_test.py @@ -20,7 +20,7 @@ import pytest import six -from convert2rhel import pkgmanager, utils +from convert2rhel import pkgmanager, systeminfo, utils from convert2rhel.systeminfo import Version, system_info from convert2rhel.toolopts import tool_opts from convert2rhel.unit_tests import RunSubprocessMocked, run_subprocess_side_effect @@ -31,6 +31,11 @@ from six.moves import mock +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) + + @pytest.mark.skipif( pkgmanager.TYPE != "yum", reason="No yum module detected on the system, skipping it.", @@ -89,7 +94,8 @@ def test_rpm_db_lock(): class TestCallYumCmd: - def test_call_yum_cmd(self, monkeypatch): + def test_call_yum_cmd(self, monkeypatch, global_tool_opts): + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) monkeypatch.setattr(system_info, "version", Version(8, 0)) monkeypatch.setattr(system_info, "releasever", "8") monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) @@ -114,11 +120,12 @@ def test_call_yum_cmd_not_setting_releasever(self, pretend_os, monkeypatch): assert utils.run_subprocess.cmd == ["yum", "install", "--setopt=exclude=", "-y"] @centos7 - def test_call_yum_cmd_with_disablerepo_and_enablerepo(self, pretend_os, monkeypatch): + def test_call_yum_cmd_with_disablerepo_and_enablerepo(self, pretend_os, monkeypatch, global_tool_opts): + global_tool_opts.disablerepo = ["*"] + global_tool_opts.enablerepo = ["rhel-7-extras-rpm"] + global_tool_opts.no_rhsm = True + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) - monkeypatch.setattr(tool_opts, "no_rhsm", True) - monkeypatch.setattr(tool_opts, "disablerepo", ["*"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["rhel-7-extras-rpm"]) pkgmanager.call_yum_cmd("install") @@ -133,10 +140,11 @@ def test_call_yum_cmd_with_disablerepo_and_enablerepo(self, pretend_os, monkeypa ] @centos7 - def test_call_yum_cmd_with_submgr_enabled_repos(self, pretend_os, monkeypatch): + def test_call_yum_cmd_with_submgr_enabled_repos(self, pretend_os, monkeypatch, global_tool_opts): + global_tool_opts.enablerepo = ["not-to-be-used-in-the-yum-call"] + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) monkeypatch.setattr(system_info, "submgr_enabled_repos", ["rhel-7-extras-rpm"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) pkgmanager.call_yum_cmd("install") @@ -150,10 +158,11 @@ def test_call_yum_cmd_with_submgr_enabled_repos(self, pretend_os, monkeypatch): ] @centos7 - def test_call_yum_cmd_with_repo_overrides(self, pretend_os, monkeypatch): + def test_call_yum_cmd_with_repo_overrides(self, pretend_os, monkeypatch, global_tool_opts): + global_tool_opts.enablerepo = ["not-to-be-used-in-the-yum-call"] + monkeypatch.setattr(pkgmanager, "tool_opts", global_tool_opts) monkeypatch.setattr(utils, "run_subprocess", RunSubprocessMocked()) monkeypatch.setattr(system_info, "submgr_enabled_repos", ["not-to-be-used-in-the-yum-call"]) - monkeypatch.setattr(tool_opts, "enablerepo", ["not-to-be-used-in-the-yum-call"]) pkgmanager.call_yum_cmd("install", ["pkg"], enable_repos=[], disable_repos=[]) diff --git a/convert2rhel/unit_tests/subscription_test.py b/convert2rhel/unit_tests/subscription_test.py index 55a3ce7dd6..9edc643057 100644 --- a/convert2rhel/unit_tests/subscription_test.py +++ b/convert2rhel/unit_tests/subscription_test.py @@ -46,6 +46,11 @@ from six.moves import mock +@pytest.fixture(autouse=True) +def apply_global_tool_opts(monkeypatch, global_tool_opts): + monkeypatch.setattr(subscription, "tool_opts", global_tool_opts) + + @pytest.fixture def mocked_rhsm_call_blocking(monkeypatch, request): rhsm_returns = get_pytest_marker(request, "rhsm_returns") diff --git a/convert2rhel/unit_tests/systeminfo_test.py b/convert2rhel/unit_tests/systeminfo_test.py index 88e8e448e1..802c0646a7 100644 --- a/convert2rhel/unit_tests/systeminfo_test.py +++ b/convert2rhel/unit_tests/systeminfo_test.py @@ -26,7 +26,6 @@ from convert2rhel import logger, systeminfo, utils from convert2rhel.systeminfo import RELEASE_VER_MAPPING, Version, system_info -from convert2rhel.toolopts import tool_opts from convert2rhel.unit_tests import RunSubprocessMocked from convert2rhel.unit_tests.conftest import all_systems, centos8 @@ -185,7 +184,7 @@ def test_get_dbus_status_in_progress(monkeypatch, states, expected): def test_corresponds_to_rhel_eus_release(major, minor, expected, monkeypatch, global_tool_opts): version = Version(major, minor) global_tool_opts.eus = True - monkeypatch.setattr(tool_opts, "eus", global_tool_opts) + monkeypatch.setattr(systeminfo, "tool_opts", global_tool_opts) monkeypatch.setattr(system_info, "version", version) assert system_info.corresponds_to_rhel_eus_release() == expected diff --git a/convert2rhel/unit_tests/toolopts/__init__.py b/convert2rhel/unit_tests/toolopts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/convert2rhel/unit_tests/toolopts/config_test.py b/convert2rhel/unit_tests/toolopts/config_test.py new file mode 100644 index 0000000000..3367444ba6 --- /dev/null +++ b/convert2rhel/unit_tests/toolopts/config_test.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2016 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +import os + +import pytest + +from convert2rhel.toolopts.config import FileConfig + + +class TestFileConfig: + @pytest.mark.parametrize( + ("content", "expected_message"), + ( + ( + """\ +[subscription_manager] +incorect_option = yes + """, + "Unsupported option", + ), + ( + """\ +[invalid_header] +username = correct_username + """, + "Couldn't find header", + ), + ( + """\ +[subscription_manager] +# username = + """, + "No options found for subscription_manager. It seems to be empty or commented.", + ), + ), + ) + def test_options_from_config_files_invalid_head_and_options(self, content, expected_message, tmpdir, caplog): + path = os.path.join(str(tmpdir), "convert2rhel.ini") + + with open(path, "w") as file: + file.write(content) + os.chmod(path, 0o600) + + file_config = FileConfig(path) + file_config.options_from_config_files() + + assert expected_message in caplog.text + + # Cleanup test file + os.remove(path) + + @pytest.mark.parametrize( + ("content", "output"), + ( + ( + """\ +[subscription_manager] +username = correct_username + """, + {"username": "correct_username"}, + ), + ( + """\ +[subscription_manager] +username = "correct_username" + """, + {"username": '"correct_username"'}, + ), + ( + """\ +[subscription_manager] +password = correct_password + """, + {"password": "correct_password"}, + ), + ( + """\ +[subscription_manager] +activation_key = correct_key +password = correct_password +username = correct_username +org = correct_org + """, + { + "username": "correct_username", + "password": "correct_password", + "activation_key": "correct_key", + "org": "correct_org", + }, + ), + ( + """\ +[subscription_manager] +org = correct_org + """, + {"org": "correct_org"}, + ), + ( + """\ +[inhibitor_overrides] +incomplete_rollback = false + """, + {"incomplete_rollback": False}, + ), + ( + """\ +[subscription_manager] +org = correct_org + +[inhibitor_overrides] +incomplete_rollback = false + """, + {"org": "correct_org", "incomplete_rollback": False}, + ), + ( + """\ +[inhibitor_overrides] +incomplete_rollback = false +tainted_kernel_module_check_skip = false +outdated_package_check_skip = false +allow_older_version = false +allow_unavailable_kmods = false +configure_host_metering = false +skip_kernel_currency_check = false + """, + { + "incomplete_rollback": False, + "tainted_kernel_module_check_skip": False, + "outdated_package_check_skip": False, + "allow_older_version": False, + "allow_unavailable_kmods": False, + "configure_host_metering": False, + "skip_kernel_currency_check": False, + }, + ), + ( + """\ +[inhibitor_overrides] +incomplete_rollback = on + """, + { + "incomplete_rollback": True, + }, + ), + ( + """\ +[inhibitor_overrides] +incomplete_rollback = 1 + """, + { + "incomplete_rollback": True, + }, + ), + ( + """\ +[inhibitor_overrides] +incomplete_rollback = yes + """, + { + "incomplete_rollback": True, + }, + ), + ), + ) + def test_options_from_config_files_default(self, content, output, monkeypatch, tmpdir): + """Test config files in default path.""" + path = os.path.join(str(tmpdir), "convert2rhel.ini") + + with open(path, "w") as file: + file.write(content) + os.chmod(path, 0o600) + + paths = ["/nonexisting/path", path] + monkeypatch.setattr(FileConfig, "DEFAULT_CONFIG_FILES", value=paths) + file_config = FileConfig(None) + opts = file_config.options_from_config_files() + for key in output.keys(): + assert opts[key] == output[key] + + @pytest.mark.parametrize( + ("content", "output", "content_lower_priority"), + ( + ( + """\ +[subscription_manager] +username = correct_username +activation_key = correct_key + """, + {"username": "correct_username", "password": None, "activation_key": "correct_key", "org": None}, + """\ +[subscription_manager] +username = low_prior_username + """, + ), + ( + """\ +[subscription_manager] +username = correct_username +activation_key = correct_key + """, + {"username": "correct_username", "password": None, "activation_key": "correct_key", "org": None}, + """\ +[subscription_manager] +activation_key = low_prior_key + """, + ), + ( + """\ +[subscription_manager] +activation_key = correct_key +org = correct_org""", + {"username": None, "password": None, "activation_key": "correct_key", "org": "correct_org"}, + """\ +[subscription_manager] +org = low_prior_org + """, + ), + ( + """\ +[subscription_manager] +activation_key = correct_key +Password = correct_password + """, + {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, + """\ +[subscription_manager] +password = low_prior_pass + """, + ), + ( + """\ +[subscription_manager] +activation_key = correct_key +Password = correct_password + """, + {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, + """\ +[INVALID_HEADER] +password = low_prior_pass + """, + ), + ( + """\ +[subscription_manager] +activation_key = correct_key +Password = correct_password + """, + {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, + """\ +[subscription_manager] +incorrect_option = incorrect_option + """, + ), + ), + ) + def test_options_from_config_files_specified(self, content, output, content_lower_priority, monkeypatch, tmpdir): + """Test user specified path for config file.""" + path_higher_priority = os.path.join(str(tmpdir), "convert2rhel.ini") + with open(path_higher_priority, "w") as file: + file.write(content) + os.chmod(path_higher_priority, 0o600) + + path_lower_priority = os.path.join(str(tmpdir), "convert2rhel_lower.ini") + with open(path_lower_priority, "w") as file: + file.write(content_lower_priority) + os.chmod(path_lower_priority, 0o600) + + paths = [path_higher_priority, path_lower_priority] + monkeypatch.setattr(FileConfig, "DEFAULT_CONFIG_FILES", value=paths) + + file_config = FileConfig(None) + opts = file_config.options_from_config_files() + + for key in ["username", "password", "activation_key", "org"]: + if key in opts: + assert opts[key] == output[key] diff --git a/convert2rhel/unit_tests/toolopts/toolopts_test.py b/convert2rhel/unit_tests/toolopts/toolopts_test.py new file mode 100644 index 0000000000..be73cba027 --- /dev/null +++ b/convert2rhel/unit_tests/toolopts/toolopts_test.py @@ -0,0 +1,356 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2016 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +import pytest +import six + +from convert2rhel import toolopts + + +six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) +from six.moves import mock + + +class MockConfig: + SOURCE = None + + def __init__(self, source, **kwds): + self.SOURCE = source + for key, value in kwds.items(): + setattr(self, key, value) + + def run(self): + pass + + +@pytest.mark.parametrize( + ("config_sources",), + ( + ([MockConfig(source="command line", serverurl=None, org="test", activation_key=None)],), + ([MockConfig(source="command line", serverurl=None, org=None, activation_key="test")],), + ([MockConfig(source="command line", serverurl=None, username="test", activation_key="test", org="blabla")],), + # Multiple configs + ( + [ + MockConfig(source="command line", serverurl=None, username="test", activation_key="test", org="blabla"), + MockConfig( + source="configuration file", + username="test", + activation_key="test", + org="blabla", + outdated_package_check_skip=True, + ), + ], + ), + ), +) +def test_apply_cls_attributes(config_sources, monkeypatch): + _handle_config_conflict_mock = mock.Mock() + _handle_missing_options_mock = mock.Mock() + monkeypatch.setattr(toolopts.ToolOpts, "_handle_config_conflict", _handle_config_conflict_mock) + monkeypatch.setattr(toolopts.ToolOpts, "_handle_missing_options", _handle_missing_options_mock) + + tool_opts = toolopts.ToolOpts() + tool_opts.initialize(config_sources) + + for config in config_sources: + assert all(hasattr(tool_opts, key) for key in vars(config).keys() if key != "SOURCE") + + assert _handle_config_conflict_mock.call_count == 1 + assert _handle_missing_options_mock.call_count == 1 + + +@pytest.mark.parametrize( + ( + "config_sources", + "expected_message", + "expected_output", + ), + ( + # Multiple configs + ( + [ + MockConfig( + source="command line", + serverurl=None, + username="test", + org=None, + activation_key=None, + password="test", + no_rpm_va=None, + ), + MockConfig( + source="configuration file", username="config_test", org=None, activation_key=None, password=None + ), + ], + "You have passed the RHSM username through both the command line and the" + " configuration file. We're going to use the command line values.", + {"username": "test"}, + ), + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org="test", + activation_key=None, + password=None, + no_rpm_va=None, + ), + MockConfig( + source="configuration file", username=None, org="config test", activation_key=None, password=None + ), + ], + "You have passed the RHSM org through both the command line and the" + " configuration file. We're going to use the command line values.", + {"org": "test"}, + ), + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key="test", + password=None, + no_rpm_va=None, + ), + MockConfig( + source="configuration file", username=None, org=None, activation_key="config test", password=None + ), + ], + "You have passed the RHSM activation key through both the command line and the" + " configuration file. We're going to use the command line values.", + {"activation_key": "test"}, + ), + ( + [ + MockConfig( + source="command line", + serverurl=None, + username="test", + org=None, + activation_key=None, + password="test", + no_rpm_va=None, + ), + MockConfig( + source="configuration file", username=None, org=None, activation_key=None, password="config test" + ), + ], + "You have passed the RHSM password through both the command line and the" + " configuration file. We're going to use the command line values.", + {"password": "test"}, + ), + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key=None, + password="test", + no_rpm_va=None, + ), + MockConfig( + source="configuration file", username="test", org=None, activation_key="test", password=None + ), + ], + "You have passed either the RHSM password or activation key through both the command line and" + " the configuration file. We're going to use the command line values.", + {"activation_key": None, "org": None}, + ), + ), +) +def test_handle_config_conflicts(config_sources, expected_message, expected_output, monkeypatch, caplog): + _handle_missing_options_mock = mock.Mock() + monkeypatch.setattr(toolopts.ToolOpts, "_handle_missing_options", _handle_missing_options_mock) + + tool_opts = toolopts.ToolOpts() + tool_opts.initialize(config_sources) + + assert _handle_missing_options_mock.call_count == 1 + + assert expected_message in caplog.records[-1].message + assert all(vars(tool_opts)[key] == value for key, value in expected_output.items()) + + +@pytest.mark.parametrize( + ( + "config_sources", + "expected_message", + ), + ( + # CLI - password without username + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key=None, + password="test", + no_rpm_va=None, + ), + MockConfig(source="configuration file", username=None, org=None, activation_key=None, password=None), + ], + "You have passed the RHSM password without an associated username. Please provide a username together" + " with the password.", + ), + # Config File - password without username + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key=None, + password=None, + no_rpm_va=None, + ), + MockConfig(source="configuration file", username=None, org=None, activation_key=None, password="test"), + ], + "You have passed the RHSM password without an associated username. Please provide a username together" + " with the password.", + ), + # CLI - username without password + ( + [ + MockConfig( + source="command line", + serverurl=None, + username="test", + org=None, + activation_key=None, + password=None, + no_rpm_va=None, + ), + MockConfig(source="configuration file", username=None, org=None, activation_key=None, password=None), + ], + "You have passed the RHSM username without an associated password. Please provide a password together" + " with the username.", + ), + # Config File - username without password + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key=None, + password=None, + no_rpm_va=None, + ), + MockConfig(source="configuration file", username="test", org=None, activation_key=None, password=None), + ], + "You have passed the RHSM username without an associated password. Please provide a password together" + " with the username.", + ), + ( + [ + MockConfig( + source="command line", + serverurl=None, + username=None, + org=None, + activation_key="test", + password="test", + no_rpm_va=None, + ), + MockConfig(source="configuration file", username=None, org=None, activation_key=None, password=None), + ], + "Either a password or an activation key can be used for system registration." + " We're going to use the activation key.", + ), + ), +) +def test_handle_config_conflicts_only_warnings(config_sources, expected_message, monkeypatch, caplog): + _handle_missing_options_mock = mock.Mock() + monkeypatch.setattr(toolopts.ToolOpts, "_handle_missing_options", _handle_missing_options_mock) + + tool_opts = toolopts.ToolOpts() + tool_opts.initialize(config_sources) + + assert _handle_missing_options_mock.call_count == 1 + + assert expected_message in caplog.records[-1].message + + +@pytest.mark.parametrize( + ("config_sources",), + ( + ( + [ + MockConfig( + source="command line", + activity="conversion", + username=None, + org=None, + activation_key=None, + password=None, + no_rpm_va=True, + serverurl=None, + ), + MockConfig( + source="configuration file", + username=None, + org=None, + activation_key=None, + password=None, + incomplete_rollback=False, + ), + ], + ), + ), +) +def test_handle_config_conflicts_system_exit(config_sources): + tool_opts = toolopts.ToolOpts() + + with pytest.raises( + SystemExit, + match=( + "We need to run the 'rpm -Va' command to be able to perform a complete rollback of changes" + " done to the system during the pre-conversion analysis. If you accept the risk of an" + " incomplete rollback, set the CONVERT2RHEL_INCOMPLETE_ROLLBACK=1 environment" + " variable. Otherwise, remove the --no-rpm-va option." + ), + ): + tool_opts.initialize(config_sources) + + +@pytest.mark.parametrize( + ("config_sources",), + ( + ([MockConfig(source="command line", serverurl=None, org="test", activation_key=None)],), + ([MockConfig(source="command line", serverurl=None, org=None, activation_key="test")],), + ), +) +def test_handle_missing_options(config_sources): + tool_opts = toolopts.ToolOpts() + with pytest.raises( + SystemExit, + match="Either the --org or the --activationkey option is missing. You can't use one without the other.", + ): + tool_opts.initialize(config_sources) diff --git a/convert2rhel/unit_tests/toolopts_test.py b/convert2rhel/unit_tests/toolopts_test.py deleted file mode 100644 index b7df8aabe1..0000000000 --- a/convert2rhel/unit_tests/toolopts_test.py +++ /dev/null @@ -1,716 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright(C) 2016 Red Hat, Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -__metaclass__ = type - -import os -import sys - -from collections import namedtuple - -import pytest -import six - -import convert2rhel.toolopts -import convert2rhel.utils - - -six.add_move(six.MovedModule("mock", "mock", "unittest.mock")) -from six.moves import mock - - -def mock_cli_arguments(args): - """ - Return a list of cli arguments where the first one is always the name of - the executable, followed by 'args'. - """ - return sys.argv[0:1] + args - - -class TestTooloptsParseFromCLI: - def test_cmdline_interactive_username_without_passwd(self, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--username", "uname"])) - convert2rhel.toolopts.CLI() - assert global_tool_opts.username == "uname" - - def test_cmdline_interactive_passwd_without_uname(self, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--password", "passwd"])) - convert2rhel.toolopts.CLI() - assert global_tool_opts.password == "passwd" - - def test_cmdline_non_interactive_with_credentials(self, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--username", "uname", "--password", "passwd"])) - convert2rhel.toolopts.CLI() - assert global_tool_opts.username == "uname" - assert global_tool_opts.password == "passwd" - - def test_cmdline_disablerepo_defaults_to_asterisk(self, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--enablerepo", "foo"])) - convert2rhel.toolopts.CLI() - assert global_tool_opts.enablerepo == ["foo"] - assert global_tool_opts.disablerepo == ["*"] - - # Parsing of serverurl - - @pytest.mark.parametrize( - ("serverurl", "hostname", "port", "prefix"), - ( - ("https://rhsm.redhat.com:443/", "rhsm.redhat.com", "443", "/"), - ("https://localhost/rhsm/", "localhost", None, "/rhsm/"), - ("https://rhsm.redhat.com/", "rhsm.redhat.com", None, "/"), - ("https://rhsm.redhat.com", "rhsm.redhat.com", None, None), - ("https://rhsm.redhat.com:8443", "rhsm.redhat.com", "8443", None), - ("subscription.redhat.com", "subscription.redhat.com", None, None), - ), - ) - def test_custom_serverurl(self, monkeypatch, global_tool_opts, serverurl, hostname, port, prefix): - monkeypatch.setattr( - sys, - "argv", - mock_cli_arguments(["--serverurl", serverurl, "--username", "User1", "--password", "Password1"]), - ) - convert2rhel.toolopts.CLI() - assert global_tool_opts.rhsm_hostname == hostname - assert global_tool_opts.rhsm_port == port - assert global_tool_opts.rhsm_prefix == prefix - - def test_no_serverurl(self, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments([])) - convert2rhel.toolopts.CLI() - assert global_tool_opts.rhsm_hostname is None - assert global_tool_opts.rhsm_port is None - assert global_tool_opts.rhsm_prefix is None - - @pytest.mark.parametrize( - "serverurl", - ( - "gopher://subscription.rhsm.redhat.com/", - "https:///", - "https://", - "/", - ), - ) - def test_bad_serverurl(self, caplog, monkeypatch, global_tool_opts, serverurl): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--serverurl", serverurl, "-o", "MyOrg", "-k", "012335"])) - - with pytest.raises(SystemExit): - convert2rhel.toolopts.CLI() - - message = ( - "Failed to parse a valid subscription-manager server from the --serverurl option.\n" - "Please check for typos and run convert2rhel again with a corrected --serverurl.\n" - "Supplied serverurl: %s\nError: " % serverurl - ) - assert message in caplog.records[-1].message - assert caplog.records[-1].levelname == "CRITICAL" - - def test_serverurl_with_no_rhsm(self, caplog, monkeypatch, global_tool_opts): - monkeypatch.setattr( - sys, "argv", mock_cli_arguments(["--serverurl", "localhost", "--no-rhsm", "--enablerepo", "testrepo"]) - ) - - convert2rhel.toolopts.CLI() - - message = "Ignoring the --serverurl option. It has no effect when --no-rhsm is used." - assert message in caplog.text - - def test_serverurl_with_no_rhsm_credentials(self, caplog, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["--serverurl", "localhost"])) - - convert2rhel.toolopts.CLI() - - message = ( - "Ignoring the --serverurl option. It has no effect when no credentials to" - " subscribe the system were given." - ) - assert message in caplog.text - - -@pytest.mark.parametrize( - ("argv", "raise_exception", "no_rhsm_value"), - ( - (mock_cli_arguments(["--no-rhsm"]), True, True), - (mock_cli_arguments(["--no-rhsm", "--enablerepo", "test_repo"]), False, True), - ), -) -@mock.patch("convert2rhel.toolopts.tool_opts.no_rhsm", False) -@mock.patch("convert2rhel.toolopts.tool_opts.enablerepo", []) -def test_no_rhsm_option_work(argv, raise_exception, no_rhsm_value, monkeypatch, caplog, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - - if raise_exception: - with pytest.raises(SystemExit): - convert2rhel.toolopts.CLI() - assert "The --enablerepo option is required when --no-rhsm is used." in caplog.text - else: - convert2rhel.toolopts.CLI() - - assert convert2rhel.toolopts.tool_opts.no_rhsm == no_rhsm_value - - -@pytest.mark.parametrize( - ("argv", "content", "output", "message"), - ( - pytest.param( - mock_cli_arguments([]), - "[subscription_manager]\nusername=conf_user\npassword=conf_pass\nactivation_key=conf_key\norg=conf_org", - {"username": "conf_user", "password": "conf_pass", "activation_key": "conf_key", "org": "conf_org"}, - None, - id="All values set in config", - ), - ( - mock_cli_arguments([]), - "[subscription_manager]\npassword=conf_pass", - {"password": "conf_pass", "activation_key": None}, - None, - ), - ( - mock_cli_arguments(["-p", "password"]), - "[subscription_manager]\nactivation_key=conf_key", - {"password": "password", "activation_key": None}, - "You have passed either the RHSM password or activation key through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ( - mock_cli_arguments(["-k", "activation_key", "-o", "org"]), - "[subscription_manager]\nactivation_key=conf_key", - {"password": None, "activation_key": "activation_key"}, - "You have passed either the RHSM password or activation key through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ( - mock_cli_arguments(["-k", "activation_key", "-o", "org"]), - "[subscription_manager]\npassword=conf_pass", - {"password": "conf_pass", "activation_key": "activation_key"}, - "You have passed either the RHSM password or activation key through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ( - mock_cli_arguments(["-k", "activation_key", "-p", "password", "-o", "org"]), - "[subscription_manager]\npassword=conf_pass\nactivation_key=conf_key", - {"password": "password", "activation_key": "activation_key"}, - "You have passed either the RHSM password or activation key through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ( - mock_cli_arguments(["-o", "org"]), - "[subscription_manager]\npassword=conf_pass\nactivation_key=conf_key", - {"password": "conf_pass", "activation_key": "conf_key"}, - "Either a password or an activation key can be used for system registration. We're going to use the" - " activation key.", - ), - ( - mock_cli_arguments(["-u", "McLOVIN"]), - "[subscription_manager]\nusername=NotMcLOVIN\n", - {"username": "McLOVIN"}, - "You have passed the RHSM username through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ( - mock_cli_arguments(["-o", "some-org"]), - "[subscription_manager]\norg=a-different-org\nactivation_key=conf_key", - {"org": "some-org"}, - "You have passed the RHSM org through both the command line and" - " the configuration file. We're going to use the command line values.", - ), - ), -) -def test_config_file(argv, content, output, message, monkeypatch, tmpdir, caplog, global_tool_opts): - # After each test there were left data from previous - # Re-init needed delete the set data - path = os.path.join(str(tmpdir), "convert2rhel.ini") - with open(path, "w") as file: - file.write(content) - os.chmod(path, 0o600) - - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=[path]) - convert2rhel.toolopts.CLI() - - if "activation_key" in output: - assert convert2rhel.toolopts.tool_opts.activation_key == output["activation_key"] - - if "password" in output: - assert convert2rhel.toolopts.tool_opts.password == output["password"] - - if "username" in output: - assert convert2rhel.toolopts.tool_opts.username == output["username"] - - if "org" in output: - assert convert2rhel.toolopts.tool_opts.org == output["org"] - - if message: - print(caplog.text) - assert message in caplog.text - - -@pytest.mark.parametrize( - ("argv", "content", "message", "output"), - ( - ( - mock_cli_arguments(["--password", "pass", "--config-file"]), - "[subscription_manager]\nactivation_key = key_cnf_file", - "You have passed either the RHSM password or activation key through both the command line and" - " the configuration file. We're going to use the command line values.", - {"password": "pass", "activation_key": None}, - ), - ), -) -def test_multiple_auth_src_combined(argv, content, message, output, caplog, monkeypatch, tmpdir, global_tool_opts): - """Test combination of password file or configuration file and CLI arguments.""" - path = os.path.join(str(tmpdir), "convert2rhel.file") - with open(path, "w") as file: - file.write(content) - os.chmod(path, 0o600) - # The path for file is the last argument - argv.append(path) - - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=[""]) - convert2rhel.toolopts.CLI() - - assert message in caplog.text - assert convert2rhel.toolopts.tool_opts.activation_key == output["activation_key"] - assert convert2rhel.toolopts.tool_opts.password == output["password"] - - -@pytest.mark.parametrize( - ("argv", "message", "output"), - ( - ( - mock_cli_arguments(["--password", "pass", "--activationkey", "key", "-o", "org"]), - "Either a password or an activation key can be used for system registration." - " We're going to use the activation key.", - {"password": "pass", "activation_key": "key"}, - ), - ), -) -def test_multiple_auth_src_cli(argv, message, output, caplog, monkeypatch, global_tool_opts): - """Test both auth methods in CLI.""" - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=[""]) - convert2rhel.toolopts.CLI() - - assert message in caplog.text - assert convert2rhel.toolopts.tool_opts.activation_key == output["activation_key"] - assert convert2rhel.toolopts.tool_opts.password == output["password"] - - -@pytest.mark.parametrize( - ("content", "output"), - ( - ( - "[subscription_manager]\nusername = correct_username", - {"username": "correct_username", "password": None, "activation_key": None, "org": None}, - ), - ( - "[subscription_manager]\npassword = correct_password", - {"username": None, "password": "correct_password", "activation_key": None, "org": None}, - ), - pytest.param( - "[subscription_manager]\n" - "activation_key = correct_key\n" - "Password = correct_password\n" - "username = correct_username\n" - "org = correct_org\n", - { - "username": "correct_username", - "password": "correct_password", - "activation_key": "correct_key", - "org": "correct_org", - }, - id="All options used together", - ), - ( - "[subscription_manager]\norg = correct_org", - {"username": None, "password": None, "activation_key": None, "org": "correct_org"}, - ), - ( - "[subscription_manager]\nincorrect_option = incorrect_content", - {"username": None, "password": None, "activation_key": None, "org": None}, - ), - ( - "[INVALID_HEADER]\nusername = correct_username\npassword = correct_password\nactivation_key = correct_key\norg = correct_org", - {"username": None, "password": None, "activation_key": None, "org": None}, - ), - (None, {"username": None, "password": None, "activation_key": None, "org": None}), - ), -) -def test_options_from_config_files_default(content, output, monkeypatch, tmpdir, caplog): - """Test config files in default path.""" - path = os.path.join(str(tmpdir), "convert2rhel.ini") - if content: - with open(path, "w") as file: - file.write(content) - os.chmod(path, 0o600) - - paths = ["/nonexisting/path", path] - monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=paths) - opts = convert2rhel.toolopts.options_from_config_files() - - assert opts["username"] == output["username"] - assert opts["password"] == output["password"] - assert opts["activation_key"] == output["activation_key"] - assert opts["org"] == output["org"] - - if content: - if "INVALID_HEADER" in content: - assert "Unsupported header" in caplog.text - if "incorrect_option" in content: - assert "Unsupported option" in caplog.text - - -@pytest.mark.parametrize( - ("content", "output", "content_lower_priority"), - ( - ( - "[subscription_manager]\nusername = correct_username\nactivation_key = correct_key", - {"username": "correct_username", "password": None, "activation_key": "correct_key", "org": None}, - "[subscription_manager]\nusername = low_prior_username", - ), - ( - "[subscription_manager]\nusername = correct_username\nactivation_key = correct_key", - {"username": "correct_username", "password": None, "activation_key": "correct_key", "org": None}, - "[subscription_manager]\nactivation_key = low_prior_key", - ), - ( - "[subscription_manager]\nactivation_key = correct_key\norg = correct_org", - {"username": None, "password": None, "activation_key": "correct_key", "org": "correct_org"}, - "[subscription_manager]\norg = low_prior_org", - ), - ( - "[subscription_manager]\nactivation_key = correct_key\nPassword = correct_password", - {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, - "[subscription_manager]\npassword = low_prior_pass", - ), - ( - "[subscription_manager]\nactivation_key = correct_key\nPassword = correct_password", - {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, - "[INVALID_HEADER]\npassword = low_prior_pass", - ), - ( - "[subscription_manager]\nactivation_key = correct_key\nPassword = correct_password", - {"username": None, "password": "correct_password", "activation_key": "correct_key", "org": None}, - "[subscription_manager]\nincorrect_option = incorrect_option", - ), - ), -) -def test_options_from_config_files_specified(content, output, content_lower_priority, monkeypatch, tmpdir, caplog): - """Test user specified path for config file.""" - path = os.path.join(str(tmpdir), "convert2rhel.ini") - with open(path, "w") as file: - file.write(content) - os.chmod(path, 0o600) - - path_lower_priority = os.path.join(str(tmpdir), "convert2rhel_lower.ini") - with open(path_lower_priority, "w") as file: - file.write(content_lower_priority) - os.chmod(path_lower_priority, 0o600) - - paths = [path_lower_priority] - monkeypatch.setattr(convert2rhel.toolopts, "CONFIG_PATHS", value=paths) - # user specified path - opts = convert2rhel.toolopts.options_from_config_files(path) - - assert opts["username"] == output["username"] - assert opts["password"] == output["password"] - assert opts["activation_key"] == output["activation_key"] - assert opts["org"] == output["org"] - - if "INVALID_HEADER" in content or "INVALID_HEADER" in content_lower_priority: - assert "Unsupported header" in caplog.text - if "incorrect_option" in content or "incorrect_option" in content_lower_priority: - assert "Unsupported option" in caplog.text - - -@pytest.mark.parametrize( - "supported_opts", - ( - { - "username": "correct_username", - "password": "correct_password", - "activation_key": "correct_key", - "org": "correct_org", - }, - { - "username": "correct_username", - "password": "correct_password", - "activation_key": "correct_key", - "org": "correct_org", - "invalid_key": "invalid_key", - }, - ), -) -def test_set_opts(supported_opts, global_tool_opts): - convert2rhel.toolopts.ToolOpts.set_opts(global_tool_opts, supported_opts) - - assert global_tool_opts.username == supported_opts["username"] - assert global_tool_opts.password == supported_opts["password"] - assert global_tool_opts.activation_key == supported_opts["activation_key"] - assert global_tool_opts.org == supported_opts["org"] - assert not hasattr(global_tool_opts, "invalid_key") - - -UrlParts = namedtuple("UrlParts", ("scheme", "hostname", "port")) - - -@pytest.mark.parametrize( - ("url_parts", "message"), - ( - ( - UrlParts("gopher", "localhost", None), - "Subscription manager must be accessed over http or https. gopher is not valid", - ), - (UrlParts("http", None, None), "A hostname must be specified in a subscription-manager serverurl"), - (UrlParts("http", "", None), "A hostname must be specified in a subscription-manager serverurl"), - ), -) -def test_validate_serverurl_parsing(url_parts, message): - with pytest.raises(ValueError, match=message): - convert2rhel.toolopts._validate_serverurl_parsing(url_parts) - - -def test_log_command_used(caplog, monkeypatch): - obfuscation_string = "*" * 5 - input_command = mock_cli_arguments( - ["--username", "uname", "--password", "123", "--activationkey", "456", "--org", "789"] - ) - expected_command = mock_cli_arguments( - [ - "--username", - obfuscation_string, - "--password", - obfuscation_string, - "--activationkey", - obfuscation_string, - "--org", - obfuscation_string, - ] - ) - monkeypatch.setattr(sys, "argv", input_command) - convert2rhel.toolopts._log_command_used() - - assert " ".join(expected_command) in caplog.records[-1].message - - -@pytest.mark.parametrize( - ("argv", "message"), - ( - # The message is a log of used command - (mock_cli_arguments(["-o", "org", "-k", "key"]), "-o ***** -k *****"), - ( - mock_cli_arguments(["-o", "org"]), - "Either the --org or the --activationkey option is missing. You can't use one without the other.", - ), - ( - mock_cli_arguments(["-k", "key"]), - "Either the --org or the --activationkey option is missing. You can't use one without the other.", - ), - ), -) -def test_org_activation_key_specified(argv, message, monkeypatch, caplog, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - - try: - convert2rhel.toolopts.CLI() - except SystemExit: - # Don't care about the exception, focus on output message - pass - assert message in caplog.text - - -@pytest.mark.parametrize( - ("argv", "expected"), - ( - (mock_cli_arguments(["convert"]), "conversion"), - (mock_cli_arguments(["analyze"]), "analysis"), - (mock_cli_arguments([]), "conversion"), - ), -) -def test_pre_assessment_set(argv, expected, monkeypatch, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - - convert2rhel.toolopts.CLI() - - assert global_tool_opts.activity == expected - - -@pytest.mark.parametrize( - ("argv", "expected"), - ( - ( - mock_cli_arguments(["--disablerepo", "*", "--enablerepo", "*"]), - "Duplicate repositories were found across disablerepo and enablerepo options", - ), - ( - mock_cli_arguments( - ["--disablerepo", "*", "--disablerepo", "rhel-7-extras-rpm", "--enablerepo", "rhel-7-extras-rpm"] - ), - "Duplicate repositories were found across disablerepo and enablerepo options", - ), - ( - mock_cli_arguments(["--disablerepo", "test", "--enablerepo", "test"]), - "Duplicate repositories were found across disablerepo and enablerepo options", - ), - ), -) -def test_disable_and_enable_repos_has_same_repo(argv, expected, monkeypatch, caplog, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - convert2rhel.toolopts.CLI() - - assert expected in caplog.records[-1].message - - -@pytest.mark.parametrize( - ("argv", "expected"), - ( - ( - mock_cli_arguments(["--disablerepo", "*", "--enablerepo", "test"]), - "Duplicate repositories were found across disablerepo and enablerepo options", - ), - ( - mock_cli_arguments(["--disablerepo", "test", "--enablerepo", "test1"]), - "Duplicate repositories were found across disablerepo and enablerepo options", - ), - ), -) -def test_disable_and_enable_repos_with_different_repos(argv, expected, monkeypatch, caplog, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - convert2rhel.toolopts.CLI() - - assert expected not in caplog.records[-1].message - - -@pytest.mark.parametrize( - ("argv", "expected"), - ( - ([], ["convert"]), - (["--debug"], ["convert", "--debug"]), - (["analyze", "--debug"], ["analyze", "--debug"]), - (["--password=convert", "--debug"], ["convert", "--password=convert", "--debug"]), - ), -) -def test_add_default_command(argv, expected, monkeypatch): - monkeypatch.setattr(sys, "argv", argv) - assert convert2rhel.toolopts._add_default_command(argv) == expected - - -@pytest.mark.parametrize( - ("username", "password", "organization", "activation_key", "no_rhsm", "expected"), - ( - ("User1", "Password1", None, None, False, True), - (None, None, "My Org", "12345ABC", False, True), - ("User1", "Password1", "My Org", "12345ABC", False, True), - (None, None, None, None, True, False), - ("User1", None, None, "12345ABC", False, False), - (None, None, None, None, False, False), - ("User1", "Password1", None, None, True, False), - ), -) -def test_should_subscribe(username, password, organization, activation_key, no_rhsm, expected): - t_opts = convert2rhel.toolopts.ToolOpts() - t_opts.username = username - t_opts.password = password - t_opts.org = organization - t_opts.activation_key = activation_key - t_opts.no_rhsm = no_rhsm - - assert convert2rhel.toolopts._should_subscribe(t_opts) is expected - - -@pytest.mark.parametrize( - ("argv", "env_var", "expected", "message"), - ( - ( - ["analyze", "--no-rpm-va"], - False, - False, - "We will proceed with ignoring the --no-rpm-va option as running rpm -Va in the analysis mode is essential for a complete rollback to the original system state at the end of the analysis.", - ), - ( - ["analyze", "--no-rpm-va"], - True, - False, - "We will proceed with ignoring the --no-rpm-va option as running rpm -Va in the analysis mode is essential for a complete rollback to the original system state at the end of the analysis.", - ), - ( - ["--no-rpm-va"], - False, - False, - "We need to run the 'rpm -Va' command to be able to perform a complete rollback of changes done to the system during the pre-conversion analysis. If you accept the risk of an incomplete rollback, set the CONVERT2RHEL_INCOMPLETE_ROLLBACK=1 environment variable. Otherwise, remove the --no-rpm-va option.", - ), - (["--no-rpm-va"], True, True, ""), - ), -) -def test_setting_no_rpm_va(argv, env_var, expected, message, monkeypatch, global_tool_opts, caplog): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(argv)) - if env_var: - monkeypatch.setenv("CONVERT2RHEL_INCOMPLETE_ROLLBACK", "1") - - try: - convert2rhel.toolopts.CLI() - except SystemExit: - pass - - assert global_tool_opts.no_rpm_va == expected - if message: - assert caplog.records[-1].message == message - - -@pytest.mark.parametrize( - ("argv", "message"), - ( - # The message is a log of used command - (mock_cli_arguments(["-u", "user", "-p", "pass"]), "-u ***** -p *****"), - ( - mock_cli_arguments(["-p", "pass"]), - "You have passed the RHSM password without an associated username. Please provide a username together with the password", - ), - ( - mock_cli_arguments(["-u", "user"]), - "You have passed the RHSM username without an associated password. Please provide a password together with the username", - ), - ), -) -def test_cli_userpass_specified(argv, message, monkeypatch, caplog, global_tool_opts): - monkeypatch.setattr(sys, "argv", argv) - - try: - convert2rhel.toolopts.CLI() - except SystemExit: - # Don't care about the exception, focus on output message - pass - assert message in caplog.text - - -@pytest.mark.parametrize( - ("activation_key", "organization", "argv"), - ( - ("activation_key", "org", []), - ("activation_key", "org", ["analyze", "-u name", "-p pass"]), - (None, None, ["analyze", "-u name", "-p pass"]), - ), -) -def test_cli_args_config_file_cornercase(activation_key, organization, argv, monkeypatch): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(argv)) - t_opts = convert2rhel.toolopts.ToolOpts() - t_opts.org = organization - t_opts.activation_key = activation_key - t_opts.no_rhsm = True - monkeypatch.setattr(convert2rhel.toolopts, "tool_opts", t_opts) - - # Make sure it doesn't raise an exception - convert2rhel.toolopts.CLI() diff --git a/convert2rhel/unit_tests/utils/__init__.py b/convert2rhel/unit_tests/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/convert2rhel/unit_tests/utils/subscription_test.py b/convert2rhel/unit_tests/utils/subscription_test.py new file mode 100644 index 0000000000..f51d685c51 --- /dev/null +++ b/convert2rhel/unit_tests/utils/subscription_test.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2018 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +from collections import namedtuple + +import pytest + +from convert2rhel.utils import subscription + + +UrlParts = namedtuple("UrlParts", ("scheme", "hostname", "port")) + + +@pytest.mark.parametrize( + ("url_parts", "message"), + ( + ( + UrlParts("gopher", "localhost", None), + "Subscription manager must be accessed over http or https. gopher is not valid", + ), + (UrlParts("http", None, None), "A hostname must be specified in a subscription-manager serverurl"), + (UrlParts("http", "", None), "A hostname must be specified in a subscription-manager serverurl"), + ), +) +def test_validate_serverurl_parsing(url_parts, message): + with pytest.raises(ValueError, match=message): + subscription._validate_serverurl_parsing(url_parts) + + +@pytest.mark.parametrize( + ("username", "password", "organization", "activation_key", "no_rhsm", "expected"), + ( + ("User1", "Password1", None, None, False, True), + (None, None, "My Org", "12345ABC", False, True), + ("User1", "Password1", "My Org", "12345ABC", False, True), + (None, None, None, None, True, False), + ("User1", None, None, "12345ABC", False, False), + (None, None, None, None, False, False), + ("User1", "Password1", None, None, True, False), + ), +) +def test_should_subscribe(username, password, organization, activation_key, no_rhsm, expected, global_tool_opts): + global_tool_opts.username = username + global_tool_opts.password = password + global_tool_opts.org = organization + global_tool_opts.activation_key = activation_key + global_tool_opts.no_rhsm = no_rhsm + + assert subscription._should_subscribe(global_tool_opts) is expected diff --git a/convert2rhel/unit_tests/utils_test.py b/convert2rhel/unit_tests/utils/utils_test.py similarity index 98% rename from convert2rhel/unit_tests/utils_test.py rename to convert2rhel/unit_tests/utils/utils_test.py index b84fd09105..2a61212a86 100644 --- a/convert2rhel/unit_tests/utils_test.py +++ b/convert2rhel/unit_tests/utils/utils_test.py @@ -328,7 +328,7 @@ def __call__(self, *args, **kwargs): return self.real_rmtree(*args, **kwargs) gpg_key = os.path.realpath( - os.path.join(os.path.dirname(__file__), "../data/version-independent/gpg-keys/RPM-GPG-KEY-redhat-release") + os.path.join(os.path.dirname(__file__), "../../data/version-independent/gpg-keys/RPM-GPG-KEY-redhat-release") ) def test_find_keyid(self): @@ -439,6 +439,10 @@ def test_remove_tmp_dir(monkeypatch, dir_name, caplog, tmpdir): class TestDownload_pkg: + @pytest.fixture(autouse=True) + def apply_cls_global_tool_opts(self, monkeypatch, global_tool_opts): + monkeypatch.setattr(toolopts, "tool_opts", global_tool_opts) + def test_download_pkgs(self, monkeypatch): monkeypatch.setattr( utils, @@ -548,7 +552,7 @@ def test_download_pkg_failed_download_overridden(self, monkeypatch): ("",), ), ) - def test_download_pkg_incorrect_output(self, output, monkeypatch): + def test_download_pkg_incorrect_output(self, output, monkeypatch, global_tool_opts): monkeypatch.setattr(system_info, "releasever", "7Server") monkeypatch.setattr(system_info, "version", systeminfo.Version(7, 0)) monkeypatch.setattr(utils, "run_cmd_in_pty", RunCmdInPtyMocked(return_string=output)) @@ -573,8 +577,8 @@ def test_get_rpm_path_from_yumdownloader_output(output): ("CONVERT2RHEL_INCOMPLETE_ROLLBACK", "analysis", True, "you can choose to disregard this check"), ), ) -def test_report_on_a_download_error(envvar, activity, should_raise, message, monkeypatch, caplog): - monkeypatch.setattr(toolopts.tool_opts, "activity", activity) +def test_report_on_a_download_error(envvar, activity, should_raise, message, monkeypatch, caplog, global_tool_opts): + global_tool_opts.activity = activity monkeypatch.setattr(os, "environ", {envvar: "1"}) if should_raise: diff --git a/convert2rhel/utils/__init__.py b/convert2rhel/utils/__init__.py index 18d8e2134d..8d5bd35040 100644 --- a/convert2rhel/utils/__init__.py +++ b/convert2rhel/utils/__init__.py @@ -40,6 +40,7 @@ from convert2rhel import exceptions, i18n from convert2rhel.logger import root_logger +from convert2rhel.toolopts import tool_opts logger = root_logger.getChild(__name__) @@ -320,8 +321,6 @@ def store_content_to_file(filename, content): def restart_system(): - from convert2rhel.toolopts import tool_opts - if tool_opts.restart: run_subprocess(["reboot"]) else: @@ -505,8 +504,6 @@ def ask_to_continue(): """Ask user whether to continue with the system conversion. If no, execution of the tool is stopped. """ - from convert2rhel.toolopts import tool_opts - if tool_opts.autoaccept: return while True: diff --git a/convert2rhel/utils/rpm.py b/convert2rhel/utils/rpm.py new file mode 100644 index 0000000000..e548b34340 --- /dev/null +++ b/convert2rhel/utils/rpm.py @@ -0,0 +1,22 @@ +# Copyright(C) 2024 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +# For a list of modified rpm files before the conversion starts +PRE_RPM_VA_LOG_FILENAME = "rpm_va.log" + +# For a list of modified rpm files after the conversion finishes for comparison purposes +POST_RPM_VA_LOG_FILENAME = "rpm_va_after_conversion.log" diff --git a/convert2rhel/utils/subscription.py b/convert2rhel/utils/subscription.py new file mode 100644 index 0000000000..d23563f0f6 --- /dev/null +++ b/convert2rhel/utils/subscription.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# +# Copyright(C) 2024 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__metaclass__ = type + +import logging +import re + +from six.moves import urllib + + +loggerinst = logging.getLogger(__name__) + + +def setup_rhsm_parts(opts): + # We initialize the values here as a measurement to give the complete dictionary back to the caller. + rhsm_parts = {"rhsm_hostname": None, "rhsm_port": None, "rhsm_prefix": None} + + if opts.serverurl: + if opts.no_rhsm: + loggerinst.warning("Ignoring the --serverurl option. It has no effect when --no-rhsm is used.") + + # WARNING: We cannot use the following helper until after no_rhsm, + # username, password, activation_key, and organization have been + # set. + elif not _should_subscribe(opts): + loggerinst.warning( + "Ignoring the --serverurl option. It has no effect when no credentials to subscribe the system were given." + ) + else: + # Parse the serverurl and save the components. + try: + url_parts = _parse_subscription_manager_serverurl(opts.serverurl) + url_parts = _validate_serverurl_parsing(url_parts) + except ValueError as e: + # If we fail to parse, fail the conversion. The reason for + # this harsh treatment is that we will be submitting + # credentials to the server parsed from the serverurl. If + # the user is specifying an internal subscription-manager + # server but typo the url, we would fallback to the + # public red hat subscription-manager server. That would + # mean the user thinks the credentials are being passed + # to their internal subscription-manager server but it + # would really be passed externally. That's not a good + # security practice. + loggerinst.critical( + "Failed to parse a valid subscription-manager server from the --serverurl option.\n" + "Please check for typos and run convert2rhel again with a corrected --serverurl.\n" + "Supplied serverurl: %s\nError: %s" % (opts.serverurl, e) + ) + + rhsm_parts["rhsm_hostname"] = url_parts.hostname + + if url_parts.port: + # urllib.parse.urlsplit() converts this into an int but we + # always use it as a str + rhsm_parts["rhsm_port"] = str(url_parts.port) + + if url_parts.path: + rhsm_parts["rhsm_prefix"] = url_parts.path + + return rhsm_parts + + +def _parse_subscription_manager_serverurl(serverurl): + """Parse a url string in a manner mostly compatible with subscription-manager --serverurl.""" + # This is an adaptation of what subscription-manager's cli enforces: + # https://github.com/candlepin/subscription-manager/blob/main/src/rhsm/utils.py#L112 + + # Don't modify http:// and https:// as they are fine + if not re.match("https?://[^/]+", serverurl): + # Anthing that looks like a malformed scheme is immediately discarded + if re.match("^[^:]+:/.+", serverurl): + raise ValueError("Unable to parse --serverurl. Make sure it starts with http://HOST or https://HOST") + + # If there isn't a scheme, add one now + serverurl = "https://%s" % serverurl + + url_parts = urllib.parse.urlsplit(serverurl, allow_fragments=False) + + return url_parts + + +def _validate_serverurl_parsing(url_parts): + """ + Perform some tests that we parsed the subscription-manager serverurl successfully. + + :arg url_parts: The parsed serverurl as returned by urllib.parse.urlsplit() + :raises ValueError: If any of the checks fail. + :returns: url_parts If the check was successful. + """ + if url_parts.scheme not in ("https", "http"): + raise ValueError( + "Subscription manager must be accessed over http or https. %s is not valid" % url_parts.scheme + ) + + if not url_parts.hostname: + raise ValueError("A hostname must be specified in a subscription-manager serverurl") + + return url_parts + + +def _should_subscribe(opts): + """ + Whether we should subscribe the system with subscription-manager. + + If there are no ways to authenticate to subscription-manager, then we will + attempt to convert without subscribing the system. The assumption is that + the user has already subscribed the system or that this machine does not + need to subscribe to rhsm in order to get the RHEL rpm packages. + """ + # No means to authenticate with rhsm. + if not (opts.username and opts.password) and not (opts.activation_key and opts.org): + return False + + # --no-rhsm means that there is no need to use any part of rhsm to + # convert this host. (Usually used when you configure + # your RHEL repos another way, like a local mirror and telling + # convert2rhel about it using --enablerepo) + if opts.no_rhsm: + return False + + return True diff --git a/man/__init__.py b/man/__init__.py index 6c229de033..69e9a7ac2f 100644 --- a/man/__init__.py +++ b/man/__init__.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from convert2rhel import toolopts +from convert2rhel import cli def get_parser(): @@ -27,7 +27,7 @@ def get_parser(): $ python -c 'from convert2rhel import toolopts; print("[synopsis]\n."+toolopts.CLI.usage())' > man/synopsis $ PYTHONPATH=. argparse-manpage --pyfile man/__init__.py --function get_parser --manual-title="General Commands Manual" --description="Automates the conversion of Red Hat Enterprise Linux derivative distributions to Red Hat Enterprise Linux." --project-name "convert2rhel " --prog="convert2rhel" --include man/distribution --include man/synopsis > man/convert2rhel.8 """ - parser = toolopts.CLI()._parser + parser = cli.CLI()._parser # Description taken out of our Confluence page. parser.description = ( diff --git a/scripts/whitelist.py b/scripts/whitelist.py index 806d549e19..a20785dfa5 100644 --- a/scripts/whitelist.py +++ b/scripts/whitelist.py @@ -12,3 +12,5 @@ z # unused variable (convert2rhel/unit_tests/utils_test.py:470) cols # unused variable (convert2rhel/utils.py:491) rows # unused variable (convert2rhel/utils.py:491) +x # unused variable (convert2rhel/unit_tests/actions/pre_ponr_changes/subscription_test.py:68) +y # unused variable (convert2rhel/unit_tests/utils/utils_test.py:473) diff --git a/tests/integration/tier0/non-destructive/config-file/test_config_file.py b/tests/integration/tier0/non-destructive/config-file/test_config_file.py index d12aed9200..cdc9ee9b1b 100644 --- a/tests/integration/tier0/non-destructive/config-file/test_config_file.py +++ b/tests/integration/tier0/non-destructive/config-file/test_config_file.py @@ -63,7 +63,7 @@ def test_std_path_std_filename(convert2rhel, c2r_config_setup): with the config file having standard filename. """ with convert2rhel("--debug") as c2r: - c2r.expect("DEBUG - Found password in /root/.convert2rhel.ini") + c2r.expect("DEBUG - Found password in subscription_manager") c2r.expect("Continue with the system conversion?") c2r.sendline("n") @@ -92,7 +92,8 @@ def test_user_path_custom_filename(convert2rhel, c2r_config_setup): filename and the path is passed to the utility command. """ with convert2rhel('--debug -c "~/.convert2rhel_custom.ini"') as c2r: - c2r.expect("DEBUG - Found activation_key in /root/.convert2rhel_custom.ini") + c2r.expect("DEBUG - Checking configuration file at /root/.convert2rhel_custom.ini") + c2r.expect("DEBUG - Found activation_key in subscription_manager") assert c2r.exitstatus == 1 @@ -122,10 +123,11 @@ def test_config_cli_priority(convert2rhel, c2r_config_setup): """ with convert2rhel("--username username --password password --debug") as c2r: # Found options in config file - c2r.expect("DEBUG - Found username in /root/.convert2rhel.ini", timeout=120) - c2r.expect("DEBUG - Found password in /root/.convert2rhel.ini", timeout=120) - c2r.expect("DEBUG - Found activation_key in /root/.convert2rhel.ini", timeout=120) - c2r.expect("DEBUG - Found org in /root/.convert2rhel.ini", timeout=120) + c2r.expect("DEBUG - Checking configuration file at /root/.convert2rhel.ini", timeout=120) + c2r.expect("DEBUG - Found username in subscription_manager", timeout=120) + c2r.expect("DEBUG - Found password in subscription_manager", timeout=120) + c2r.expect("DEBUG - Found activation_key in subscription_manager", timeout=120) + c2r.expect("DEBUG - Found org in subscription_manager", timeout=120) c2r.expect( "WARNING - You have passed either the RHSM password or activation key through both the command line and" " the configuration file. We're going to use the command line values.", @@ -171,14 +173,12 @@ def test_config_standard_paths_priority_diff_methods(convert2rhel, c2r_config_se (password and activation key) the activation key is preferred. """ with convert2rhel(f"analyze --serverurl {TEST_VARS['RHSM_SERVER_URL']} -y --debug") as c2r: - c2r.expect("DEBUG - Found password in /root/.convert2rhel.ini") - c2r.expect("DEBUG - Found activation_key in /etc/convert2rhel.ini") - c2r.expect( - "WARNING - Passing the RHSM password or activation key through the --activationkey or --password options" - " is insecure as it leaks the values through the list of running processes." - " We recommend using the safer --config-file option instead." - ) - + c2r.expect("DEBUG - Checking configuration file at /etc/convert2rhel.ini", timeout=120) + c2r.expect("DEBUG - Found activation_key in subscription_manager") + c2r.expect("DEBUG - Found org in subscription_manager") + c2r.expect("DEBUG - Checking configuration file at /root/.convert2rhel.ini", timeout=120) + c2r.expect("DEBUG - Found password in subscription_manager") + c2r.expect("DEBUG - Found username in subscription_manager") c2r.expect( "WARNING - Either a password or an activation key can be used for system registration." " We're going to use the activation key." @@ -222,8 +222,8 @@ def test_config_standard_paths_priority(convert2rhel, c2r_config_setup): Config file located in the home folder to be preferred. """ with convert2rhel(f"analyze --serverurl {TEST_VARS['RHSM_SERVER_URL']} -y --debug") as c2r: - c2r.expect("DEBUG - Found username in /root/.convert2rhel.ini") - c2r.expect("DEBUG - Found password in /root/.convert2rhel.ini") + c2r.expect("DEBUG - Found username in subscription_manager") + c2r.expect("DEBUG - Found password in subscription_manager") c2r.expect("SUBSCRIBE_SYSTEM has succeeded") assert c2r.exitstatus == 0