diff --git a/features/help.feature b/features/help.feature index 41827a61cd..c3d01dff50 100644 --- a/features/help.feature +++ b/features/help.feature @@ -279,6 +279,67 @@ Feature: Pro Client help text reboot, but you can assess if the reboot can be performed in the nearest maintenance window. """ + When I run `pro config --help` as non-root + Then stdout matches regexp: + """ + usage: pro config \[flags\] + + Manage Ubuntu Pro configuration + + (optional arguments|options): + -h, --help show this help message and exit + + Available Commands: + + show Show customizable configuration settings + set Set and apply Ubuntu Pro configuration settings + unset Unset Ubuntu Pro configuration setting + """ + When I run `pro config show --help` as non-root + Then stdout matches regexp: + """ + usage: pro config show \[flags\] + + Show customizable configuration settings + + positional arguments: + key Optional key or key\(s\) to show configuration settings. + """ + When I run `pro config set --help` as non-root + Then stdout matches regexp: + """ + usage: pro config set \[flags\] + + Set and apply Ubuntu Pro configuration settings + + positional arguments: + key_value_pair key=value pair to configure for Ubuntu Pro services. Key + must be one of: http_proxy, https_proxy, apt_http_proxy, + apt_https_proxy, ua_apt_http_proxy, ua_apt_https_proxy, + global_apt_http_proxy, global_apt_https_proxy, + update_messaging_timer, metering_timer, apt_news, + apt_news_url + + (optional arguments|options): + -h, --help show this help message and exit + """ + When I run `pro config unset --help` as non-root + Then stdout matches regexp: + """ + usage: pro config unset \[flags\] + + Unset Ubuntu Pro configuration setting + + positional arguments: + key configuration key to unset from Ubuntu Pro services. One of: + http_proxy, https_proxy, apt_http_proxy, apt_https_proxy, + ua_apt_http_proxy, ua_apt_https_proxy, global_apt_http_proxy, + global_apt_https_proxy, update_messaging_timer, metering_timer, + apt_news, apt_news_url + + (optional arguments|options): + -h, --help show this help message and exit + """ Examples: ubuntu release | release | machine_type | diff --git a/uaclient/cli/__init__.py b/uaclient/cli/__init__.py index 71fcfd329c..8eed3ccc81 100644 --- a/uaclient/cli/__init__.py +++ b/uaclient/cli/__init__.py @@ -3,14 +3,10 @@ import argparse import logging import sys -from typing import Optional from uaclient import ( apt, - apt_news, - config, defaults, - entitlements, event_logger, exceptions, http, @@ -20,12 +16,11 @@ util, version, ) -from uaclient.apt import AptProxyScope, setup_apt_proxy -from uaclient.cli import cli_util from uaclient.cli.api import api_command from uaclient.cli.attach import attach_command from uaclient.cli.auto_attach import auto_attach_command from uaclient.cli.collect_logs import collect_logs_command +from uaclient.cli.config import config_command from uaclient.cli.constants import NAME, USAGE_TMPL from uaclient.cli.detach import detach_command from uaclient.cli.disable import disable_command @@ -36,8 +31,7 @@ from uaclient.cli.security_status import security_status_command from uaclient.cli.status import status_command from uaclient.cli.system import system_command -from uaclient.entitlements.entitlement_status import ApplicationStatus -from uaclient.files import state_files +from uaclient.config import UAConfig from uaclient.log import get_user_or_root_log_file_path event = event_logger.get_event_logger() @@ -48,6 +42,7 @@ attach_command, auto_attach_command, collect_logs_command, + config_command, detach_command, disable_command, enable_command, @@ -78,318 +73,7 @@ def error(self, message): self.exit(2, message + "\n") -def config_show_parser(parser, parent_command: str): - """Build or extend an arg parser for 'config show' subcommand.""" - parser.usage = USAGE_TMPL.format( - name=NAME, command="{} show [key]".format(parent_command) - ) - parser.prog = "show" - parser.description = messages.CLI_CONFIG_SHOW_DESC - parser.add_argument( - "key", - nargs="?", # action_config_show handles this optional argument - help=messages.CLI_CONFIG_SHOW_KEY, - ) - return parser - - -def config_set_parser(parser, parent_command: str): - """Build or extend an arg parser for 'config set' subcommand.""" - parser.usage = USAGE_TMPL.format( - name=NAME, command="{} set =".format(parent_command) - ) - parser.prog = "aset" - parser.description = messages.CLI_CONFIG_SET_DESC - parser._optionals.title = messages.CLI_FLAGS - parser.add_argument( - "key_value_pair", - help=( - messages.CLI_CONFIG_SET_KEY_VALUE.format( - options=", ".join(config.UA_CONFIGURABLE_KEYS) - ) - ), - ) - return parser - - -def config_unset_parser(parser, parent_command: str): - """Build or extend an arg parser for 'config unset' subcommand.""" - parser.usage = USAGE_TMPL.format( - name=NAME, command="{} unset ".format(parent_command) - ) - parser.prog = "unset" - parser.description = messages.CLI_CONFIG_UNSET_DESC - parser.add_argument( - "key", - help=( - messages.CLI_CONFIG_UNSET_KEY.format( - options=", ".join(config.UA_CONFIGURABLE_KEYS) - ) - ), - metavar="key", - ) - parser._optionals.title = messages.CLI_FLAGS - return parser - - -def config_parser(parser): - """Build or extend an arg parser for config subcommand.""" - command = "config" - parser.usage = USAGE_TMPL.format( - name=NAME, command="{} ".format(command) - ) - parser.prog = command - parser.description = messages.CLI_CONFIG_DESC - parser._optionals.title = messages.CLI_FLAGS - subparsers = parser.add_subparsers( - title=messages.CLI_AVAILABLE_COMMANDS, dest="command", metavar="" - ) - parser_show = subparsers.add_parser( - "show", help=messages.CLI_CONFIG_SHOW_DESC - ) - parser_show.set_defaults(action=action_config_show) - config_show_parser(parser_show, parent_command=command) - - parser_set = subparsers.add_parser( - "set", help=messages.CLI_CONFIG_SET_DESC - ) - parser_set.set_defaults(action=action_config_set) - config_set_parser(parser_set, parent_command=command) - - parser_unset = subparsers.add_parser( - "unset", help=messages.CLI_CONFIG_UNSET_DESC - ) - parser_unset.set_defaults(action=action_config_unset) - config_unset_parser(parser_unset, parent_command=command) - return parser - - -def _print_help_for_subcommand( - cfg: config.UAConfig, cmd_name: str, subcmd_name: str -): - parser = get_parser(cfg=cfg) - subparser = parser._get_positional_actions()[0].choices[cmd_name] - valid_choices = subparser._get_positional_actions()[0].choices.keys() - if subcmd_name not in valid_choices: - parser._get_positional_actions()[0].choices[cmd_name].print_help() - raise exceptions.InvalidArgChoice( - arg="", choices=", ".join(valid_choices) - ) - - -def action_config(args, *, cfg, **kwargs): - """Perform the config action. - - :return: 0 on success, 1 otherwise - """ - _print_help_for_subcommand( - cfg, cmd_name="config", subcmd_name=args.command - ) - return 0 - - -def action_config_show(args, *, cfg, **kwargs): - """Perform the 'config show' action optionally limit output to a single key - - :return: 0 on success - :raise UbuntuProError: on invalid keys - """ - if args.key: # limit reporting config to a single config key - if args.key not in config.UA_CONFIGURABLE_KEYS: - raise exceptions.InvalidArgChoice( - arg="'{}'".format(args.key), - choices=", ".join(config.UA_CONFIGURABLE_KEYS), - ) - print( - "{key} {value}".format( - key=args.key, value=getattr(cfg, args.key, None) - ) - ) - return 0 - - col_width = str(max([len(x) for x in config.UA_CONFIGURABLE_KEYS]) + 1) - row_tmpl = "{key: <" + col_width + "} {value}" - - for key in config.UA_CONFIGURABLE_KEYS: - print(row_tmpl.format(key=key, value=getattr(cfg, key, None))) - - if (cfg.global_apt_http_proxy or cfg.global_apt_https_proxy) and ( - cfg.ua_apt_http_proxy or cfg.ua_apt_https_proxy - ): - print(messages.CLI_CONFIG_GLOBAL_XOR_UA_PROXY) - - -@cli_util.assert_root -def action_config_set(args, *, cfg, **kwargs): - """Perform the 'config set' action. - - @return: 0 on success, 1 otherwise - """ - from uaclient.livepatch import configure_livepatch_proxy - from uaclient.snap import configure_snap_proxy - - parser = get_parser(cfg=cfg) - config_parser = parser._get_positional_actions()[0].choices["config"] - subparser = config_parser._get_positional_actions()[0].choices["set"] - try: - set_key, set_value = args.key_value_pair.split("=") - except ValueError: - subparser.print_help() - raise exceptions.GenericInvalidFormat( - expected="=", actual=args.key_value_pair - ) - if set_key not in config.UA_CONFIGURABLE_KEYS: - subparser.print_help() - raise exceptions.InvalidArgChoice( - arg="", choices=", ".join(config.UA_CONFIGURABLE_KEYS) - ) - if not set_value.strip(): - subparser.print_help() - raise exceptions.EmptyConfigValue(arg=set_key) - if set_key in ("http_proxy", "https_proxy"): - protocol_type = set_key.split("_")[0] - if protocol_type == "http": - validate_url = http.PROXY_VALIDATION_SNAP_HTTP_URL - else: - validate_url = http.PROXY_VALIDATION_SNAP_HTTPS_URL - http.validate_proxy(protocol_type, set_value, validate_url) - - kwargs = {set_key: set_value} - configure_snap_proxy(**kwargs) - # Only set livepatch proxy if livepatch is enabled - entitlement = entitlements.livepatch.LivepatchEntitlement(cfg) - livepatch_status, _ = entitlement.application_status() - if livepatch_status == ApplicationStatus.ENABLED: - configure_livepatch_proxy(**kwargs) - elif set_key in cfg.ua_scoped_proxy_options: - protocol_type = set_key.split("_")[2] - if protocol_type == "http": - validate_url = http.PROXY_VALIDATION_APT_HTTP_URL - else: - validate_url = http.PROXY_VALIDATION_APT_HTTPS_URL - http.validate_proxy(protocol_type, set_value, validate_url) - unset_current = bool( - cfg.global_apt_http_proxy or cfg.global_apt_https_proxy - ) - if unset_current: - print( - messages.WARNING_APT_PROXY_OVERWRITE.format( - current_proxy="pro scoped apt", previous_proxy="global apt" - ) - ) - configure_apt_proxy(cfg, AptProxyScope.UACLIENT, set_key, set_value) - cfg.global_apt_http_proxy = None - cfg.global_apt_https_proxy = None - - elif set_key in ( - cfg.deprecated_global_scoped_proxy_options - + cfg.global_scoped_proxy_options - ): - # setup_apt_proxy is destructive for unprovided values. Source complete - # current config values from uaclient.conf before applying set_value. - - protocol_type = "https" if "https" in set_key else "http" - if protocol_type == "http": - validate_url = http.PROXY_VALIDATION_APT_HTTP_URL - else: - validate_url = http.PROXY_VALIDATION_APT_HTTPS_URL - - if set_key in cfg.deprecated_global_scoped_proxy_options: - print( - messages.WARNING_CONFIG_FIELD_RENAME.format( - old="apt_{}_proxy".format(protocol_type), - new="global_apt_{}_proxy".format(protocol_type), - ) - ) - set_key = "global_" + set_key - - http.validate_proxy(protocol_type, set_value, validate_url) - - unset_current = bool(cfg.ua_apt_http_proxy or cfg.ua_apt_https_proxy) - - if unset_current: - print( - messages.WARNING_APT_PROXY_OVERWRITE.format( - current_proxy="global apt", previous_proxy="pro scoped apt" - ) - ) - configure_apt_proxy(cfg, AptProxyScope.GLOBAL, set_key, set_value) - cfg.ua_apt_http_proxy = None - cfg.ua_apt_https_proxy = None - - elif set_key in ( - "update_messaging_timer", - "metering_timer", - ): - try: - set_value = int(set_value) - if set_value < 0: - raise ValueError("Invalid interval for {}".format(set_key)) - except ValueError: - subparser.print_help() - # More readable in the CLI, without breaking the line in the logs - print("") - raise exceptions.InvalidPosIntConfigValue( - key=set_key, value=set_value - ) - elif set_key == "apt_news": - set_value = set_value.lower() == "true" - if set_value: - apt_news.update_apt_news(cfg) - else: - state_files.apt_news_contents_file.delete() - - setattr(cfg, set_key, set_value) - - -@cli_util.assert_root -def action_config_unset(args, *, cfg, **kwargs): - """Perform the 'config unset' action. - - @return: 0 on success, 1 otherwise - """ - from uaclient.apt import AptProxyScope - from uaclient.livepatch import unconfigure_livepatch_proxy - from uaclient.snap import unconfigure_snap_proxy - - if args.key not in config.UA_CONFIGURABLE_KEYS: - parser = get_parser(cfg=cfg) - config_parser = parser._get_positional_actions()[0].choices["config"] - subparser = config_parser._get_positional_actions()[0].choices["unset"] - subparser.print_help() - raise exceptions.InvalidArgChoice( - arg="", choices=", ".join(config.UA_CONFIGURABLE_KEYS) - ) - if args.key in ("http_proxy", "https_proxy"): - protocol_type = args.key.split("_")[0] - unconfigure_snap_proxy(protocol_type=protocol_type) - # Only unset livepatch proxy if livepatch is enabled - entitlement = entitlements.livepatch.LivepatchEntitlement(cfg) - livepatch_status, _ = entitlement.application_status() - if livepatch_status == ApplicationStatus.ENABLED: - unconfigure_livepatch_proxy(protocol_type=protocol_type) - elif args.key in cfg.ua_scoped_proxy_options: - configure_apt_proxy(cfg, AptProxyScope.UACLIENT, args.key, None) - elif args.key in ( - cfg.deprecated_global_scoped_proxy_options - + cfg.global_scoped_proxy_options - ): - if args.key in cfg.deprecated_global_scoped_proxy_options: - protocol_type = "https" if "https" in args.key else "http" - event.info( - messages.WARNING_CONFIG_FIELD_RENAME.format( - old="apt_{}_proxy".format(protocol_type), - new="global_apt_{}_proxy".format(protocol_type), - ) - ) - args.key = "global_" + args.key - configure_apt_proxy(cfg, AptProxyScope.GLOBAL, args.key, None) - - setattr(cfg, args.key, None) - return 0 - - -def get_parser(cfg: config.UAConfig): +def get_parser(cfg: UAConfig): parser = UAArgumentParser( prog=NAME, formatter_class=argparse.RawDescriptionHelpFormatter, @@ -415,39 +99,9 @@ def get_parser(cfg: config.UAConfig): for command in COMMANDS: command.register(subparsers) - parser_config = subparsers.add_parser( - "config", help=messages.CLI_ROOT_CONFIG - ) - config_parser(parser_config) - parser_config.set_defaults(action=action_config) - return parser -def configure_apt_proxy( - cfg: config.UAConfig, - scope: AptProxyScope, - set_key: str, - set_value: Optional[str], -) -> None: - """ - Handles setting part the apt proxies - global and uaclient scoped proxies - """ - if scope == AptProxyScope.GLOBAL: - http_proxy = cfg.global_apt_http_proxy - https_proxy = cfg.global_apt_https_proxy - elif scope == AptProxyScope.UACLIENT: - http_proxy = cfg.ua_apt_http_proxy - https_proxy = cfg.ua_apt_https_proxy - if "https" in set_key: - https_proxy = set_value - else: - http_proxy = set_value - setup_apt_proxy( - http_proxy=http_proxy, https_proxy=https_proxy, proxy_scope=scope - ) - - def _warn_about_new_version(cmd_args=None) -> None: # If no args, then it was called from the main error handler. # We don't want to show this text for the "api" CLI output, @@ -591,7 +245,7 @@ def main(sys_argv=None): defaults.CONFIG_DEFAULTS["log_level"], defaults.CONFIG_DEFAULTS["log_file"], ) - cfg = config.UAConfig() + cfg = UAConfig() log.setup_cli_logging(cfg.log_level, cfg.log_file) if not sys_argv: diff --git a/uaclient/cli/cli_util.py b/uaclient/cli/cli_util.py index 1db50e168a..91d0ab417c 100644 --- a/uaclient/cli/cli_util.py +++ b/uaclient/cli/cli_util.py @@ -14,6 +14,7 @@ util, ) from uaclient.api.u.pro.status.is_attached.v1 import _is_attached +from uaclient.apt import AptProxyScope, setup_apt_proxy from uaclient.config import UAConfig from uaclient.files import machine_token @@ -185,3 +186,27 @@ def post_cli_attach(cfg: UAConfig) -> None: output = status.format_tabular(status_dict) event.info(util.handle_unicode_characters(output)) event.process_events() + + +def configure_apt_proxy( + cfg: UAConfig, + scope: AptProxyScope, + set_key: str, + set_value: Optional[str], +) -> None: + """ + Handles setting part the apt proxies - global and uaclient scoped proxies + """ + if scope == AptProxyScope.GLOBAL: + http_proxy = cfg.global_apt_http_proxy + https_proxy = cfg.global_apt_https_proxy + elif scope == AptProxyScope.UACLIENT: + http_proxy = cfg.ua_apt_http_proxy + https_proxy = cfg.ua_apt_https_proxy + if "https" in set_key: + https_proxy = set_value + else: + http_proxy = set_value + setup_apt_proxy( + http_proxy=http_proxy, https_proxy=https_proxy, proxy_scope=scope + ) diff --git a/uaclient/cli/config.py b/uaclient/cli/config.py new file mode 100644 index 0000000000..0c5a7af737 --- /dev/null +++ b/uaclient/cli/config.py @@ -0,0 +1,319 @@ +from uaclient import ( + apt_news, + config, + entitlements, + event_logger, + exceptions, + http, + messages, +) +from uaclient.apt import AptProxyScope +from uaclient.cli import cli_util +from uaclient.cli.commands import ProArgument, ProArgumentGroup, ProCommand +from uaclient.entitlements.entitlement_status import ApplicationStatus +from uaclient.files import state_files +from uaclient.livepatch import ( + configure_livepatch_proxy, + unconfigure_livepatch_proxy, +) +from uaclient.snap import configure_snap_proxy, unconfigure_snap_proxy + +event = event_logger.get_event_logger() + + +def action_config(args, *, cfg, **kwargs): + # Avoiding a circular import + from uaclient.cli import get_parser + + # Pretty sure there will be some method to do this soon + # so we don't repeat it all over the place + get_parser(cfg).parse_args(["config", "--help"]) + return 0 + + +def action_config_show(args, *, cfg, **kwargs): + """Perform the 'config show' action optionally limit output to a single key + + :return: 0 on success + :raise UbuntuProError: on invalid keys + """ + if args.key: # limit reporting config to a single config key + if args.key not in config.UA_CONFIGURABLE_KEYS: + raise exceptions.InvalidArgChoice( + arg="'{}'".format(args.key), + choices=", ".join(config.UA_CONFIGURABLE_KEYS), + ) + print( + "{key} {value}".format( + key=args.key, value=getattr(cfg, args.key, None) + ) + ) + return 0 + + col_width = str(max([len(x) for x in config.UA_CONFIGURABLE_KEYS]) + 1) + row_tmpl = "{key: <" + col_width + "} {value}" + + for key in config.UA_CONFIGURABLE_KEYS: + print(row_tmpl.format(key=key, value=getattr(cfg, key, None))) + + if (cfg.global_apt_http_proxy or cfg.global_apt_https_proxy) and ( + cfg.ua_apt_http_proxy or cfg.ua_apt_https_proxy + ): + print(messages.CLI_CONFIG_GLOBAL_XOR_UA_PROXY) + + +@cli_util.assert_root +def action_config_set(args, *, cfg, **kwargs): + """Perform the 'config set' action. + + @return: 0 on success, 1 otherwise + """ + from uaclient.cli import get_parser + + parser = get_parser(cfg=cfg) + try: + set_key, set_value = args.key_value_pair.split("=") + except ValueError: + try: + parser.parse_args(["config", "set", "--help"]) + # Excepting SystemExit so we can raise our own exception + except SystemExit: + pass + raise exceptions.GenericInvalidFormat( + expected="=", actual=args.key_value_pair + ) + if set_key not in config.UA_CONFIGURABLE_KEYS: + try: + parser.parse_args(["config", "set", "--help"]) + # Excepting SystemExit so we can raise our own exception + except SystemExit: + pass + raise exceptions.InvalidArgChoice( + arg="", choices=", ".join(config.UA_CONFIGURABLE_KEYS) + ) + if not set_value.strip(): + try: + parser.parse_args(["config", "set", "--help"]) + # Excepting SystemExit so we can raise our own exception + except SystemExit: + pass + raise exceptions.EmptyConfigValue(arg=set_key) + if set_key in ("http_proxy", "https_proxy"): + protocol_type = set_key.split("_")[0] + if protocol_type == "http": + validate_url = http.PROXY_VALIDATION_SNAP_HTTP_URL + else: + validate_url = http.PROXY_VALIDATION_SNAP_HTTPS_URL + http.validate_proxy(protocol_type, set_value, validate_url) + + kwargs = {set_key: set_value} + configure_snap_proxy(**kwargs) + # Only set livepatch proxy if livepatch is enabled + entitlement = entitlements.livepatch.LivepatchEntitlement(cfg) + livepatch_status, _ = entitlement.application_status() + if livepatch_status == ApplicationStatus.ENABLED: + configure_livepatch_proxy(**kwargs) + elif set_key in cfg.ua_scoped_proxy_options: + protocol_type = set_key.split("_")[2] + if protocol_type == "http": + validate_url = http.PROXY_VALIDATION_APT_HTTP_URL + else: + validate_url = http.PROXY_VALIDATION_APT_HTTPS_URL + http.validate_proxy(protocol_type, set_value, validate_url) + unset_current = bool( + cfg.global_apt_http_proxy or cfg.global_apt_https_proxy + ) + if unset_current: + print( + messages.WARNING_APT_PROXY_OVERWRITE.format( + current_proxy="pro scoped apt", previous_proxy="global apt" + ) + ) + cli_util.configure_apt_proxy( + cfg, AptProxyScope.UACLIENT, set_key, set_value + ) + cfg.global_apt_http_proxy = None + cfg.global_apt_https_proxy = None + + elif set_key in ( + cfg.deprecated_global_scoped_proxy_options + + cfg.global_scoped_proxy_options + ): + # setup_apt_proxy is destructive for unprovided values. Source complete + # current config values from uaclient.conf before applying set_value. + + protocol_type = "https" if "https" in set_key else "http" + if protocol_type == "http": + validate_url = http.PROXY_VALIDATION_APT_HTTP_URL + else: + validate_url = http.PROXY_VALIDATION_APT_HTTPS_URL + + if set_key in cfg.deprecated_global_scoped_proxy_options: + print( + messages.WARNING_CONFIG_FIELD_RENAME.format( + old="apt_{}_proxy".format(protocol_type), + new="global_apt_{}_proxy".format(protocol_type), + ) + ) + set_key = "global_" + set_key + + http.validate_proxy(protocol_type, set_value, validate_url) + + unset_current = bool(cfg.ua_apt_http_proxy or cfg.ua_apt_https_proxy) + + if unset_current: + print( + messages.WARNING_APT_PROXY_OVERWRITE.format( + current_proxy="global apt", previous_proxy="pro scoped apt" + ) + ) + cli_util.configure_apt_proxy( + cfg, AptProxyScope.GLOBAL, set_key, set_value + ) + cfg.ua_apt_http_proxy = None + cfg.ua_apt_https_proxy = None + + elif set_key in ( + "update_messaging_timer", + "metering_timer", + ): + try: + set_value = int(set_value) + if set_value < 0: + raise ValueError("Invalid interval for {}".format(set_key)) + except ValueError: + try: + parser.parse_args(["config", "set", "--help"]) + # Excepting SystemExit so we can raise our own exception + except SystemExit: + pass + # More readable in the CLI, without breaking the line in the logs + print("") + raise exceptions.InvalidPosIntConfigValue( + key=set_key, value=set_value + ) + elif set_key == "apt_news": + set_value = set_value.lower() == "true" + if set_value: + apt_news.update_apt_news(cfg) + else: + state_files.apt_news_contents_file.delete() + + setattr(cfg, set_key, set_value) + + +@cli_util.assert_root +def action_config_unset(args, *, cfg, **kwargs): + """Perform the 'config unset' action. + + @return: 0 on success, 1 otherwise + """ + from uaclient.cli import get_parser + + if args.key not in config.UA_CONFIGURABLE_KEYS: + parser = get_parser(cfg=cfg) + try: + parser.parse_args(["config", "unset", "--help"]) + # Excepting SystemExit so we can raise our own exception + except SystemExit: + pass + raise exceptions.InvalidArgChoice( + arg="", choices=", ".join(config.UA_CONFIGURABLE_KEYS) + ) + if args.key in ("http_proxy", "https_proxy"): + protocol_type = args.key.split("_")[0] + unconfigure_snap_proxy(protocol_type=protocol_type) + # Only unset livepatch proxy if livepatch is enabled + entitlement = entitlements.livepatch.LivepatchEntitlement(cfg) + livepatch_status, _ = entitlement.application_status() + if livepatch_status == ApplicationStatus.ENABLED: + unconfigure_livepatch_proxy(protocol_type=protocol_type) + elif args.key in cfg.ua_scoped_proxy_options: + cli_util.configure_apt_proxy( + cfg, AptProxyScope.UACLIENT, args.key, None + ) + elif args.key in ( + cfg.deprecated_global_scoped_proxy_options + + cfg.global_scoped_proxy_options + ): + if args.key in cfg.deprecated_global_scoped_proxy_options: + protocol_type = "https" if "https" in args.key else "http" + event.info( + messages.WARNING_CONFIG_FIELD_RENAME.format( + old="apt_{}_proxy".format(protocol_type), + new="global_apt_{}_proxy".format(protocol_type), + ) + ) + args.key = "global_" + args.key + cli_util.configure_apt_proxy(cfg, AptProxyScope.GLOBAL, args.key, None) + + setattr(cfg, args.key, None) + return 0 + + +show_subcommand = ProCommand( + "show", + help=messages.CLI_CONFIG_SHOW_DESC, + description=messages.CLI_CONFIG_SHOW_DESC, + action=action_config_show, + argument_groups=[ + ProArgumentGroup( + arguments=[ + ProArgument( + "key", help=messages.CLI_CONFIG_SHOW_KEY, nargs="?" + ) + ] + ) + ], +) + +set_subcommand = ProCommand( + "set", + help=messages.CLI_CONFIG_SET_DESC, + description=messages.CLI_CONFIG_SET_DESC, + action=action_config_set, + argument_groups=[ + ProArgumentGroup( + arguments=[ + ProArgument( + "key_value_pair", + help=( + messages.CLI_CONFIG_SET_KEY_VALUE.format( + options=", ".join(config.UA_CONFIGURABLE_KEYS) + ) + ), + ) + ] + ) + ], +) + +unset_subcommand = ProCommand( + "unset", + help=messages.CLI_CONFIG_UNSET_DESC, + description=messages.CLI_CONFIG_UNSET_DESC, + action=action_config_unset, + argument_groups=[ + ProArgumentGroup( + arguments=[ + ProArgument( + "key", + help=( + messages.CLI_CONFIG_UNSET_KEY.format( + options=", ".join(config.UA_CONFIGURABLE_KEYS) + ) + ), + metavar="key", + ) + ] + ) + ], +) + +config_command = ProCommand( + "config", + help=messages.CLI_ROOT_CONFIG, + description=messages.CLI_CONFIG_DESC, + action=action_config, + subcommands=[show_subcommand, set_subcommand, unset_subcommand], +) diff --git a/uaclient/cli/tests/test_cli.py b/uaclient/cli/tests/test_cli.py index 70b9854895..3aa5229666 100644 --- a/uaclient/cli/tests/test_cli.py +++ b/uaclient/cli/tests/test_cli.py @@ -7,7 +7,6 @@ from uaclient import defaults, exceptions, messages from uaclient.cli import _warn_about_output_redirection, main -from uaclient.entitlements import get_valid_entitlement_names from uaclient.exceptions import ( AlreadyAttachedError, LockHeldError, @@ -230,7 +229,7 @@ def test_environment_is_logged( @mock.patch("uaclient.log.setup_cli_logging") @mock.patch("uaclient.cli.get_parser") - @mock.patch("uaclient.cli.config.UAConfig") + @mock.patch("uaclient.cli.UAConfig") @pytest.mark.parametrize("config_error", [True, False]) def test_setup_logging_with_defaults( self, @@ -322,24 +321,6 @@ def test_status_human_readable_warning( ) -class TestGetValidEntitlementNames: - @mock.patch( - "uaclient.cli.entitlements.valid_services", - return_value=["ent1", "ent2", "ent3"], - ) - def test_get_valid_entitlements(self, _m_valid_services, FakeConfig): - service = ["ent1", "ent3", "ent4"] - expected_ents_found = ["ent1", "ent3"] - expected_ents_not_found = ["ent4"] - - actual_ents_found, actual_ents_not_found = get_valid_entitlement_names( - service, cfg=FakeConfig() - ) - - assert expected_ents_found == actual_ents_found - assert expected_ents_not_found == actual_ents_not_found - - # There is a fixture for this function to avoid leaking, as it is called in # the main CLI function. So, instead of importing it directly, we are using # the reference for the fixture to test it. diff --git a/uaclient/cli/tests/test_cli_config.py b/uaclient/cli/tests/test_cli_config.py deleted file mode 100644 index 4a611b205d..0000000000 --- a/uaclient/cli/tests/test_cli_config.py +++ /dev/null @@ -1,58 +0,0 @@ -import mock -import pytest - -from uaclient.cli import main - -M_PATH = "uaclient.cli." - -HELP_OUTPUT = """\ -usage: pro config [flags] - -Manage Ubuntu Pro configuration - -Flags: - -h, --help show this help message and exit - -Available Commands: - - show Show customizable configuration settings - set Set and apply Ubuntu Pro configuration settings - unset Unset Ubuntu Pro configuration setting -""" # noqa - - -@mock.patch("uaclient.cli.LOG.error") -@mock.patch("uaclient.log.setup_cli_logging") -@mock.patch("uaclient.contract.get_available_resources") -class TestMainConfig: - @pytest.mark.parametrize("additional_params", ([], ["--help"])) - def test_config_help( - self, - _m_resources, - _logging, - logging_error, - additional_params, - capsys, - FakeConfig, - event, - ): - """Show help for --help and absent positional param""" - with pytest.raises(SystemExit): - with mock.patch( - "sys.argv", ["/usr/bin/ua", "config"] + additional_params - ): - with mock.patch( - "uaclient.config.UAConfig", - return_value=FakeConfig(), - ): - main() - out, err = capsys.readouterr() - assert HELP_OUTPUT == out - if additional_params == ["--help"]: - assert "" == err - else: - # When lacking show, set or unset inform about valid values - assert "\n must be one of: show, set, unset\n" == err - assert [ - mock.call("\n must be one of: show, set, unset") - ] == logging_error.call_args_list diff --git a/uaclient/cli/tests/test_cli_config_set.py b/uaclient/cli/tests/test_cli_config_set.py index 964a874090..2026fc5e51 100644 --- a/uaclient/cli/tests/test_cli_config_set.py +++ b/uaclient/cli/tests/test_cli_config_set.py @@ -2,26 +2,12 @@ import pytest from uaclient import apt, http, messages -from uaclient.cli import action_config_set, configure_apt_proxy, main +from uaclient.cli import main +from uaclient.cli.cli_util import configure_apt_proxy +from uaclient.cli.config import set_subcommand from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.exceptions import NonRootUserError, UbuntuProError -HELP_OUTPUT = """\ -usage: pro config set = [flags] - -Set and apply Ubuntu Pro configuration settings - -positional arguments: - key_value_pair key=value pair to configure for Ubuntu Pro services. Key - must be one of: http_proxy, https_proxy, apt_http_proxy, - apt_https_proxy, ua_apt_http_proxy, ua_apt_https_proxy, - global_apt_http_proxy, global_apt_https_proxy, - update_messaging_timer, metering_timer, apt_news, - apt_news_url - -Flags: - -h, --help show this help message and exit -""" M_LIVEPATCH = "uaclient.entitlements.livepatch." @@ -87,7 +73,6 @@ def test_set_error_with_help_on_invalid_key_value_pair( ): main() out, err = capsys.readouterr() - assert HELP_OUTPUT == out assert err_msg in err @@ -106,7 +91,7 @@ def test_set_error_on_non_root_user( args = mock.MagicMock(key_value_pair="something=1") cfg = FakeConfig() with pytest.raises(NonRootUserError): - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) @pytest.mark.parametrize( "key,value,livepatch_enabled", @@ -117,9 +102,9 @@ def test_set_error_on_non_root_user( ("https_proxy", "https://proxy", True), ), ) - @mock.patch("uaclient.livepatch.configure_livepatch_proxy") + @mock.patch("uaclient.cli.config.configure_livepatch_proxy") @mock.patch(M_LIVEPATCH + "LivepatchEntitlement.application_status") - @mock.patch("uaclient.snap.configure_snap_proxy") + @mock.patch("uaclient.cli.config.configure_snap_proxy") @mock.patch("uaclient.http.validate_proxy") def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( self, @@ -150,7 +135,7 @@ def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( ) args = mock.MagicMock(key_value_pair="{}={}".format(key, value)) cfg = FakeConfig() - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) kwargs = {key: value} if key == "http_proxy": url = http.PROXY_VALIDATION_SNAP_HTTP_URL @@ -184,7 +169,7 @@ def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( ), ), ) - @mock.patch("uaclient.cli.configure_apt_proxy") + @mock.patch("uaclient.cli.cli_util.configure_apt_proxy") @mock.patch("uaclient.http.validate_proxy") def test_set_apt_http_proxy_and_apt_https_proxy_prints_warning( self, @@ -203,7 +188,7 @@ def test_set_apt_http_proxy_and_apt_https_proxy_prints_warning( and sets global_* and exits 0.""" args = mock.MagicMock(key_value_pair="{}={}".format(key, value)) cfg = FakeConfig() - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) out, err = capsys.readouterr() global_eq = "global_" + key assert [ @@ -284,7 +269,7 @@ def test_set_apt_http_proxy_and_apt_https_proxy_prints_warning( ), ), ) - @mock.patch("uaclient.cli.configure_apt_proxy") + @mock.patch("uaclient.cli.cli_util.configure_apt_proxy") @mock.patch("uaclient.http.validate_proxy") def test_set_global_apt_http_and_global_apt_https_proxy( self, @@ -305,7 +290,7 @@ def test_set_global_apt_http_and_global_apt_https_proxy( cfg = FakeConfig() cfg.ua_apt_https_proxy = ua_apt_equ cfg.ua_apt_http_proxy = ua_apt_equ - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) out, err = capsys.readouterr() # will need to check output if ua_apt_equ: assert [ @@ -387,7 +372,7 @@ def test_set_global_apt_http_and_global_apt_https_proxy( ), ), ) - @mock.patch("uaclient.cli.configure_apt_proxy") + @mock.patch("uaclient.cli.cli_util.configure_apt_proxy") @mock.patch("uaclient.http.validate_proxy") def test_set_ua_apt_http_and_ua_apt_https_proxy( self, @@ -408,7 +393,7 @@ def test_set_ua_apt_http_and_ua_apt_https_proxy( cfg = FakeConfig() cfg.global_apt_http_proxy = global_apt_equ cfg.global_apt_https_proxy = global_apt_equ - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) out, err = capsys.readouterr() # will need to check output if global_apt_equ: assert [ @@ -452,7 +437,7 @@ def test_set_ua_apt_http_and_ua_apt_https_proxy( ("global_apt_https_proxy", None, apt.AptProxyScope.GLOBAL), ), ) - @mock.patch("uaclient.cli.setup_apt_proxy") + @mock.patch("uaclient.cli.cli_util.setup_apt_proxy") def test_configure_global_apt_proxy( self, setup_apt_proxy, @@ -491,7 +476,7 @@ def test_configure_global_apt_proxy( ("global_apt_https_proxy", None, apt.AptProxyScope.UACLIENT), ), ) - @mock.patch("uaclient.cli.setup_apt_proxy") + @mock.patch("uaclient.cli.cli_util.setup_apt_proxy") def test_configure_uaclient_apt_proxy( self, setup_apt_proxy, @@ -517,7 +502,7 @@ def test_configure_uaclient_apt_proxy( def test_set_timer_interval(self, _m_resources, _write, FakeConfig): args = mock.MagicMock(key_value_pair="update_messaging_timer=28800") cfg = FakeConfig() - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) assert 28800 == cfg.update_messaging_timer @pytest.mark.parametrize("invalid_value", ("notanumber", -1)) @@ -533,5 +518,5 @@ def test_error_when_interval_is_not_valid( ) cfg = FakeConfig() with pytest.raises(UbuntuProError): - action_config_set(args, cfg=cfg) + set_subcommand.action(args, cfg=cfg) assert cfg.update_messaging_timer is None diff --git a/uaclient/cli/tests/test_cli_config_show.py b/uaclient/cli/tests/test_cli_config_show.py index 19e0de791a..f4bd4bc74e 100644 --- a/uaclient/cli/tests/test_cli_config_show.py +++ b/uaclient/cli/tests/test_cli_config_show.py @@ -1,47 +1,16 @@ import mock import pytest -from uaclient.cli import action_config_show, main +from uaclient.cli import main +from uaclient.cli.config import show_subcommand M_PATH = "uaclient.cli." -HELP_OUTPUT = """\ -usage: pro config show [key] [flags] - -Show customizable configuration settings - -positional arguments: - key Optional key or key(s) to show configuration settings. - -""" - @mock.patch("uaclient.cli.logging.error") @mock.patch("uaclient.log.setup_cli_logging") @mock.patch("uaclient.contract.get_available_resources") class TestMainConfigShow: - def test_config_show_help( - self, - _m_resources, - _logging, - logging_error, - capsys, - FakeConfig, - ): - """Show help for --help and absent positional param""" - with pytest.raises(SystemExit): - with mock.patch( - "sys.argv", ["/usr/bin/ua", "config", "show", "--help"] - ): - with mock.patch( - "uaclient.config.UAConfig", - return_value=FakeConfig(), - ): - main() - out, err = capsys.readouterr() - assert out.startswith(HELP_OUTPUT) - assert "" == err - def test_config_show_error_on_invalid_subcommand( self, _m_resources, _logging, _logging_error, capsys, FakeConfig ): @@ -56,7 +25,7 @@ def test_config_show_error_on_invalid_subcommand( out, err = capsys.readouterr() assert "" == out expected_logs = [ - "usage: pro config [flags]", + "usage: pro config [flags]", "argument : invalid choice: 'invalid' (choose from 'show', 'set'," " 'unset')", ] @@ -86,7 +55,7 @@ def test_show_values_and_limit_when_optional_key_provided( "http://global_apt_https_proxy" ) args = mock.MagicMock(key=optional_key) - action_config_show(args, cfg=cfg) + show_subcommand.action(args, cfg=cfg) out, err = capsys.readouterr() if optional_key: assert "{key} http://{key}\n".format(key=optional_key) == out diff --git a/uaclient/cli/tests/test_cli_config_unset.py b/uaclient/cli/tests/test_cli_config_unset.py index 5a51aa2103..831db11dd3 100644 --- a/uaclient/cli/tests/test_cli_config_unset.py +++ b/uaclient/cli/tests/test_cli_config_unset.py @@ -1,26 +1,11 @@ import mock import pytest -from uaclient.cli import action_config_unset, main +from uaclient.cli import main +from uaclient.cli.config import unset_subcommand from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.exceptions import NonRootUserError -HELP_OUTPUT = """\ -usage: pro config unset [flags] - -Unset Ubuntu Pro configuration setting - -positional arguments: - key configuration key to unset from Ubuntu Pro services. One of: - http_proxy, https_proxy, apt_http_proxy, apt_https_proxy, - ua_apt_http_proxy, ua_apt_https_proxy, global_apt_http_proxy, - global_apt_https_proxy, update_messaging_timer, metering_timer, - apt_news, apt_news_url - -Flags: - -h, --help show this help message and exit -""" - M_LIVEPATCH = "uaclient.entitlements.livepatch." @@ -69,7 +54,6 @@ def test_set_error_with_help_on_invalid_key_value_pair( ): main() out, err = capsys.readouterr() - assert HELP_OUTPUT == out assert err_msg in err @@ -83,7 +67,7 @@ def test_set_error_on_non_root_user( args = mock.MagicMock(key="https_proxy") cfg = FakeConfig() with pytest.raises(NonRootUserError): - action_config_unset(args, cfg=cfg) + unset_subcommand.action(args, cfg=cfg) @pytest.mark.parametrize( "key,livepatch_enabled", @@ -94,9 +78,9 @@ def test_set_error_on_non_root_user( ("https_proxy", True), ), ) - @mock.patch("uaclient.livepatch.unconfigure_livepatch_proxy") + @mock.patch("uaclient.cli.config.unconfigure_livepatch_proxy") @mock.patch(M_LIVEPATCH + "LivepatchEntitlement.application_status") - @mock.patch("uaclient.snap.unconfigure_snap_proxy") + @mock.patch("uaclient.cli.config.unconfigure_snap_proxy") def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( self, unconfigure_snap_proxy, @@ -123,7 +107,7 @@ def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( ) args = mock.MagicMock(key=key) cfg = FakeConfig() - action_config_unset(args, cfg=cfg) + unset_subcommand.action(args, cfg=cfg) assert [ mock.call(protocol_type=key.split("_")[0]) ] == unconfigure_snap_proxy.call_args_list diff --git a/uaclient/cli/tests/test_cli_disable.py b/uaclient/cli/tests/test_cli_disable.py index 35c25ef7c4..579f9d2b52 100644 --- a/uaclient/cli/tests/test_cli_disable.py +++ b/uaclient/cli/tests/test_cli_disable.py @@ -49,8 +49,8 @@ class TestDisable: @mock.patch( "uaclient.cli.disable.contract.UAContractClient.update_activity_token", ) - @mock.patch("uaclient.cli.entitlements.entitlement_factory") - @mock.patch("uaclient.cli.entitlements.valid_services") + @mock.patch("uaclient.entitlements.entitlement_factory") + @mock.patch("uaclient.entitlements.valid_services") @mock.patch("uaclient.status.status") def test_entitlement_instantiated_and_disabled( self, diff --git a/uaclient/entitlements/tests/test_entitlements.py b/uaclient/entitlements/tests/test_entitlements.py index bfa97fe665..802b55ddd1 100644 --- a/uaclient/entitlements/tests/test_entitlements.py +++ b/uaclient/entitlements/tests/test_entitlements.py @@ -393,3 +393,21 @@ def test_check_entitlement_definitions_are_unique( entitlements.check_entitlement_apt_directives_are_unique( mock.MagicMock(url="test_url") ) + + +class TestGetValidEntitlementNames: + @mock.patch( + "uaclient.entitlements.valid_services", + return_value=["ent1", "ent2", "ent3"], + ) + def test_get_valid_entitlements(self, _m_valid_services, FakeConfig): + service = ["ent1", "ent3", "ent4"] + expected_ents_found = ["ent1", "ent3"] + expected_ents_not_found = ["ent4"] + + actual_ents_found, actual_ents_not_found = ( + entitlements.get_valid_entitlement_names(service, cfg=FakeConfig()) + ) + + assert expected_ents_found == actual_ents_found + assert expected_ents_not_found == actual_ents_not_found