From 84a68ca6ed914074c9118df31be7a402c7c83cdb Mon Sep 17 00:00:00 2001 From: Renan Rodrigo Date: Tue, 2 Jul 2024 22:56:11 -0300 Subject: [PATCH] cli: refactor attach to the new approach Signed-off-by: Renan Rodrigo --- features/help.feature | 23 +++ uaclient/cli/__init__.py | 194 ++------------------- uaclient/cli/attach.py | 165 ++++++++++++++++++ uaclient/cli/cli_util.py | 36 +++- uaclient/cli/commands.py | 4 + uaclient/cli/tests/test_cli_attach.py | 188 ++++---------------- uaclient/cli/tests/test_cli_auto_attach.py | 6 +- uaclient/cli/tests/test_cli_util.py | 28 +++ 8 files changed, 300 insertions(+), 344 deletions(-) create mode 100644 uaclient/cli/attach.py diff --git a/features/help.feature b/features/help.feature index ff48c42136..3462d416f2 100644 --- a/features/help.feature +++ b/features/help.feature @@ -78,6 +78,29 @@ Feature: Pro Client help text --variant VARIANT The name of the variant to use when enabling the service """ + When I run `pro attach --help` as non-root + Then stdout matches regexp: + """ + usage: pro attach \[flags\] + + Attach this machine to an Ubuntu Pro subscription with a token obtained from: + https://ubuntu.com/pro/dashboard + + When running this command without a token, it will generate a short code + and prompt you to attach the machine to your Ubuntu Pro account using + a web browser. + + positional arguments: + token token obtained for Ubuntu Pro authentication + + (optional arguments|options): + -h, --help show this help message and exit + --no-auto-enable do not enable any recommended services automatically + --attach-config ATTACH_CONFIG + use the provided attach config file instead of passing + the token on the cli + --format \{cli,json\} output in the specified format \(default: cli\) + """ Examples: ubuntu release | release | machine_type | diff --git a/uaclient/cli/__init__.py b/uaclient/cli/__init__.py index 7988a4658d..e566ce7339 100644 --- a/uaclient/cli/__init__.py +++ b/uaclient/cli/__init__.py @@ -22,7 +22,6 @@ lock, log, messages, - secret_manager, security_status, status, timer, @@ -33,39 +32,23 @@ FullAutoAttachOptions, _full_auto_attach, ) -from uaclient.api.u.pro.attach.magic.initiate.v1 import _initiate -from uaclient.api.u.pro.attach.magic.revoke.v1 import ( - MagicAttachRevokeOptions, - _revoke, -) -from uaclient.api.u.pro.attach.magic.wait.v1 import ( - MagicAttachWaitOptions, - _wait, -) from uaclient.api.u.pro.security.status.reboot_required.v1 import ( _reboot_required, ) from uaclient.apt import AptProxyScope, setup_apt_proxy from uaclient.cli import cli_util, fix from uaclient.cli.api import api_command +from uaclient.cli.attach import attach_command from uaclient.cli.collect_logs import collect_logs_command from uaclient.cli.constants import NAME, USAGE_TMPL from uaclient.cli.disable import disable_command, perform_disable from uaclient.cli.enable import enable_command -from uaclient.data_types import AttachActionsConfigFile, IncorrectTypeError -from uaclient.entitlements import ( - create_enable_entitlements_not_found_error, - entitlements_disable_order, - get_valid_entitlement_names, -) -from uaclient.entitlements.entitlement_status import ( - ApplicationStatus, - CanEnableFailure, -) +from uaclient.entitlements import entitlements_disable_order +from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.files import machine_token, state_files from uaclient.log import get_user_or_root_log_file_path from uaclient.timer.update_messaging import refresh_motd, update_motd_messages -from uaclient.yaml import safe_dump, safe_load +from uaclient.yaml import safe_dump UA_AUTH_TOKEN_URL = "https://auth.contracts.canonical.com" @@ -74,7 +57,13 @@ event = event_logger.get_event_logger() LOG = logging.getLogger(util.replace_top_level_logger_name(__name__)) -COMMANDS = [api_command, collect_logs_command, disable_command, enable_command] +COMMANDS = [ + api_command, + attach_command, + collect_logs_command, + disable_command, + enable_command, +] class UAArgumentParser(argparse.ArgumentParser): @@ -190,35 +179,6 @@ def config_parser(parser): return parser -def attach_parser(parser): - """Build or extend an arg parser for attach subcommand.""" - parser.usage = USAGE_TMPL.format(name=NAME, command="attach ") - parser.formatter_class = argparse.RawDescriptionHelpFormatter - parser.prog = "attach" - parser.description = messages.CLI_ATTACH_DESC - parser._optionals.title = messages.CLI_FLAGS - parser.add_argument("token", nargs="?", help=messages.CLI_ATTACH_TOKEN) - parser.add_argument( - "--no-auto-enable", - action="store_false", - dest="auto_enable", - help=messages.CLI_ATTACH_NO_AUTO_ENABLE, - ) - parser.add_argument( - "--attach-config", - type=argparse.FileType("r"), - help=messages.CLI_ATTACH_ATTACH_CONFIG, - ) - parser.add_argument( - "--format", - action="store", - choices=["cli", "json"], - default="cli", - help=messages.CLI_FORMAT_DESC.format(default="cli"), - ) - return parser - - def security_status_parser(parser): """Build or extend an arg parser for security-status subcommand.""" parser.prog = "security-status" @@ -719,26 +679,6 @@ def _detach(cfg: config.UAConfig, assume_yes: bool, json_output: bool) -> int: return 0 -def _post_cli_attach(cfg: config.UAConfig) -> None: - machine_token_file = machine_token.get_machine_token_file(cfg) - contract_name = machine_token_file.contract_name - - if contract_name: - event.info( - messages.ATTACH_SUCCESS_TMPL.format(contract_name=contract_name) - ) - else: - event.info(messages.ATTACH_SUCCESS_NO_CONTRACT_NAME) - - daemon.stop() - daemon.cleanup(cfg) - - status_dict, _ret = actions.status(cfg) - output = status.format_tabular(status_dict) - event.info(util.handle_unicode_characters(output)) - event.process_events() - - @cli_util.assert_root def action_auto_attach(args, *, cfg: config.UAConfig, **kwargs) -> int: try: @@ -751,114 +691,10 @@ def action_auto_attach(args, *, cfg: config.UAConfig, **kwargs) -> int: event.info(messages.E_ATTACH_FAILURE.msg) return 1 else: - _post_cli_attach(cfg) + cli_util.post_cli_attach(cfg) return 0 -def _magic_attach(args, *, cfg, **kwargs): - if args.format == "json": - raise exceptions.MagicAttachInvalidParam( - param="--format", - value=args.format, - ) - - event.info(messages.CLI_MAGIC_ATTACH_INIT) - initiate_resp = _initiate(cfg=cfg) - event.info( - "\n" - + messages.CLI_MAGIC_ATTACH_SIGN_IN.format( - user_code=initiate_resp.user_code - ) - ) - - wait_options = MagicAttachWaitOptions(magic_token=initiate_resp.token) - - try: - wait_resp = _wait(options=wait_options, cfg=cfg) - except exceptions.MagicAttachTokenError as e: - event.info(messages.CLI_MAGIC_ATTACH_FAILED) - - revoke_options = MagicAttachRevokeOptions( - magic_token=initiate_resp.token - ) - _revoke(options=revoke_options, cfg=cfg) - raise e - - event.info("\n" + messages.CLI_MAGIC_ATTACH_PROCESSING) - return wait_resp.contract_token - - -@cli_util.assert_not_attached -@cli_util.assert_root -@cli_util.assert_lock_file("pro attach") -def action_attach(args, *, cfg, **kwargs): - if args.token and args.attach_config: - raise exceptions.CLIAttachTokenArgXORConfig() - elif not args.token and not args.attach_config: - token = _magic_attach(args, cfg=cfg) - enable_services_override = None - elif args.token: - token = args.token - secret_manager.secrets.add_secret(token) - enable_services_override = None - else: - try: - attach_config = AttachActionsConfigFile.from_dict( - safe_load(args.attach_config) - ) - except IncorrectTypeError as e: - raise exceptions.AttachInvalidConfigFileError( - config_name=args.attach_config.name, error=e.msg - ) - - token = attach_config.token - enable_services_override = attach_config.enable_services - - allow_enable = args.auto_enable and enable_services_override is None - - try: - actions.attach_with_token(cfg, token=token, allow_enable=allow_enable) - except exceptions.ConnectivityError: - raise exceptions.AttachError() - else: - ret = 0 - if enable_services_override is not None and args.auto_enable: - found, not_found = get_valid_entitlement_names( - enable_services_override, cfg - ) - for name in found: - ent_ret, reason = actions.enable_entitlement_by_name(cfg, name) - if not ent_ret: - ret = 1 - if ( - reason is not None - and isinstance(reason, CanEnableFailure) - and reason.message is not None - ): - event.info(reason.message.msg) - event.error( - error_msg=reason.message.msg, - error_code=reason.message.name, - service=name, - ) - else: - event.service_processed(name) - - if not_found: - error = create_enable_entitlements_not_found_error( - not_found, cfg=cfg - ) - event.info(error.msg, file_type=sys.stderr) - event.error(error_msg=error.msg, error_code=error.msg_code) - ret = 1 - - contract_client = contract.UAContractClient(cfg) - contract_client.update_activity_token() - - _post_cli_attach(cfg) - return ret - - def get_parser(cfg: config.UAConfig): parser = UAArgumentParser( prog=NAME, @@ -885,12 +721,6 @@ def get_parser(cfg: config.UAConfig): for command in COMMANDS: command.register(subparsers) - parser_attach = subparsers.add_parser( - "attach", help=messages.CLI_ROOT_ATTACH - ) - attach_parser(parser_attach) - parser_attach.set_defaults(action=action_attach) - parser_auto_attach = subparsers.add_parser( "auto-attach", help=messages.CLI_ROOT_AUTO_ATTACH ) diff --git a/uaclient/cli/attach.py b/uaclient/cli/attach.py new file mode 100644 index 0000000000..e98123aa3c --- /dev/null +++ b/uaclient/cli/attach.py @@ -0,0 +1,165 @@ +import argparse +import sys + +from uaclient import ( + actions, + contract, + event_logger, + exceptions, + messages, + secret_manager, +) +from uaclient.api.u.pro.attach.magic.initiate.v1 import _initiate +from uaclient.api.u.pro.attach.magic.revoke.v1 import ( + MagicAttachRevokeOptions, + _revoke, +) +from uaclient.api.u.pro.attach.magic.wait.v1 import ( + MagicAttachWaitOptions, + _wait, +) +from uaclient.cli import cli_util +from uaclient.cli.commands import ProArgument, ProCommand +from uaclient.data_types import AttachActionsConfigFile, IncorrectTypeError +from uaclient.entitlements import ( + create_enable_entitlements_not_found_error, + get_valid_entitlement_names, +) +from uaclient.entitlements.entitlement_status import CanEnableFailure +from uaclient.yaml import safe_load + +event = event_logger.get_event_logger() + + +def _magic_attach(args, *, cfg, **kwargs): + if args.format == "json": + raise exceptions.MagicAttachInvalidParam( + param="--format", + value=args.format, + ) + + event.info(messages.CLI_MAGIC_ATTACH_INIT) + initiate_resp = _initiate(cfg=cfg) + event.info( + "\n" + + messages.CLI_MAGIC_ATTACH_SIGN_IN.format( + user_code=initiate_resp.user_code + ) + ) + + wait_options = MagicAttachWaitOptions(magic_token=initiate_resp.token) + + try: + wait_resp = _wait(options=wait_options, cfg=cfg) + except exceptions.MagicAttachTokenError as e: + event.info(messages.CLI_MAGIC_ATTACH_FAILED) + + revoke_options = MagicAttachRevokeOptions( + magic_token=initiate_resp.token + ) + _revoke(options=revoke_options, cfg=cfg) + raise e + + event.info("\n" + messages.CLI_MAGIC_ATTACH_PROCESSING) + return wait_resp.contract_token + + +@cli_util.assert_not_attached +@cli_util.assert_root +@cli_util.assert_lock_file("pro attach") +def action_attach(args, *, cfg, **kwargs): + if args.token and args.attach_config: + raise exceptions.CLIAttachTokenArgXORConfig() + elif not args.token and not args.attach_config: + token = _magic_attach(args, cfg=cfg) + enable_services_override = None + elif args.token: + token = args.token + secret_manager.secrets.add_secret(token) + enable_services_override = None + else: + try: + attach_config = AttachActionsConfigFile.from_dict( + safe_load(args.attach_config) + ) + except IncorrectTypeError as e: + raise exceptions.AttachInvalidConfigFileError( + config_name=args.attach_config.name, error=e.msg + ) + + token = attach_config.token + enable_services_override = attach_config.enable_services + + allow_enable = args.auto_enable and enable_services_override is None + + try: + actions.attach_with_token(cfg, token=token, allow_enable=allow_enable) + except exceptions.ConnectivityError: + raise exceptions.AttachError() + else: + ret = 0 + if enable_services_override is not None and args.auto_enable: + found, not_found = get_valid_entitlement_names( + enable_services_override, cfg + ) + for name in found: + ent_ret, reason = actions.enable_entitlement_by_name(cfg, name) + if not ent_ret: + ret = 1 + if ( + reason is not None + and isinstance(reason, CanEnableFailure) + and reason.message is not None + ): + event.info(reason.message.msg) + event.error( + error_msg=reason.message.msg, + error_code=reason.message.name, + service=name, + ) + else: + event.service_processed(name) + + if not_found: + error = create_enable_entitlements_not_found_error( + not_found, cfg=cfg + ) + event.info(error.msg, file_type=sys.stderr) + event.error(error_msg=error.msg, error_code=error.msg_code) + ret = 1 + + contract_client = contract.UAContractClient(cfg) + contract_client.update_activity_token() + + cli_util.post_cli_attach(cfg) + return ret + + +attach_command = ProCommand( + "attach", + help=messages.CLI_ROOT_ATTACH, + description=messages.CLI_ATTACH_DESC, + action=action_attach, + preserve_description=True, + arguments=[ + ProArgument("token", help=messages.CLI_ATTACH_TOKEN, nargs="?"), + ProArgument( + "--no-auto-enable", + help=messages.CLI_ATTACH_NO_AUTO_ENABLE, + action="store_false", + dest="auto_enable", + ), + ProArgument( + "--attach-config", + help=messages.CLI_ATTACH_ATTACH_CONFIG, + type=argparse.FileType("r"), + ), + ProArgument( + "--format", + help=messages.CLI_FORMAT_DESC.format(default="cli"), + action="store", + choices=["cli", "json"], + default="cli", + ), + ], +) diff --git a/uaclient/cli/cli_util.py b/uaclient/cli/cli_util.py index 28e7b52d66..1db50e168a 100644 --- a/uaclient/cli/cli_util.py +++ b/uaclient/cli/cli_util.py @@ -1,10 +1,24 @@ from functools import wraps from typing import Optional -from uaclient import api, entitlements, exceptions, lock, util +from uaclient import ( + actions, + api, + daemon, + entitlements, + event_logger, + exceptions, + lock, + messages, + status, + util, +) from uaclient.api.u.pro.status.is_attached.v1 import _is_attached +from uaclient.config import UAConfig from uaclient.files import machine_token +event = event_logger.get_event_logger() + class CLIEnableDisableProgress(api.AbstractProgress): def __init__(self, *, assume_yes: bool): @@ -151,3 +165,23 @@ def _raise_enable_disable_unattached_error(command, service_names, cfg): invalid_service=", ".join(entitlements_not_found), service_msg="", ) + + +def post_cli_attach(cfg: UAConfig) -> None: + machine_token_file = machine_token.get_machine_token_file(cfg) + contract_name = machine_token_file.contract_name + + if contract_name: + event.info( + messages.ATTACH_SUCCESS_TMPL.format(contract_name=contract_name) + ) + else: + event.info(messages.ATTACH_SUCCESS_NO_CONTRACT_NAME) + + daemon.stop() + daemon.cleanup(cfg) + + status_dict, _ret = actions.status(cfg) + output = status.format_tabular(status_dict) + event.info(util.handle_unicode_characters(output)) + event.process_events() diff --git a/uaclient/cli/commands.py b/uaclient/cli/commands.py index 6468c16682..a732855d37 100644 --- a/uaclient/cli/commands.py +++ b/uaclient/cli/commands.py @@ -32,6 +32,7 @@ def __init__( description: str, usage: Optional[str] = None, action: Callable = lambda *args, **kwargs: None, + preserve_description: bool = False, arguments: List[ProArgument] = [], ): self.name = name @@ -39,6 +40,7 @@ def __init__( self.description = description self.usage = usage or USAGE_TMPL.format(name=NAME, command=name) self.action = action + self.preserve_description = preserve_description self.arguments = arguments def register(self, subparsers: argparse._SubParsersAction): @@ -48,6 +50,8 @@ def register(self, subparsers: argparse._SubParsersAction): description=self.description, usage=self.usage, ) + if self.preserve_description: + self.parser.formatter_class = argparse.RawDescriptionHelpFormatter for argument in self.arguments: argument.register(self.parser) self.parser.set_defaults(action=self.action) diff --git a/uaclient/cli/tests/test_cli_attach.py b/uaclient/cli/tests/test_cli_attach.py index 3b8638bb9c..8ad1e6bd99 100644 --- a/uaclient/cli/tests/test_cli_attach.py +++ b/uaclient/cli/tests/test_cli_attach.py @@ -2,20 +2,13 @@ import copy import io import json -import textwrap import mock import pytest from uaclient import event_logger, http, lock, messages, util -from uaclient.cli import ( - _post_cli_attach, - action_attach, - attach_parser, - get_parser, - main, - main_error_handler, -) +from uaclient.cli import main_error_handler +from uaclient.cli.attach import attach_command from uaclient.exceptions import ( AlreadyAttachedError, LockHeldError, @@ -28,31 +21,7 @@ from uaclient.testing.fakes import FakeFile, FakeUbuntuProError from uaclient.yaml import safe_dump -HELP_OUTPUT = textwrap.dedent( - """\ -usage: pro attach [flags] - -Attach this machine to an Ubuntu Pro subscription with a token obtained from: -https://ubuntu.com/pro/dashboard - -When running this command without a token, it will generate a short code -and prompt you to attach the machine to your Ubuntu Pro account using -a web browser. - -positional arguments: - token token obtained for Ubuntu Pro authentication - -Flags: - -h, --help show this help message and exit - --no-auto-enable do not enable any recommended services automatically - --attach-config ATTACH_CONFIG - use the provided attach config file instead of passing - the token on the cli - --format {cli,json} output in the specified format (default: cli) -""" -) - -M_PATH = "uaclient.cli." +M_PATH = "uaclient.cli.attach." # Also used in test_cli_auto_attach.py BASIC_MACHINE_TOKEN = { @@ -106,7 +75,9 @@ ] = [ENTITLED_EXAMPLE_ESM_RESOURCE] -@mock.patch(M_PATH + "util.we_are_currently_root", return_value=False) +@mock.patch( + "uaclient.cli.cli_util.util.we_are_currently_root", return_value=False +) def test_non_root_users_are_rejected( m_we_are_currently_root, FakeConfig, capsys, event ): @@ -114,13 +85,13 @@ def test_non_root_users_are_rejected( cfg = FakeConfig() with pytest.raises(NonRootUserError): - action_attach(mock.MagicMock(), cfg) + attach_command.action(mock.MagicMock(), cfg) with pytest.raises(SystemExit): with mock.patch.object( event, "_event_logger_mode", event_logger.EventLoggerMode.JSON ): - main_error_handler(action_attach)(mock.MagicMock(), cfg) + main_error_handler(attach_command.action)(mock.MagicMock(), cfg) expected = { "_schema_version": event_logger.JSON_SCHEMA_VERSION, @@ -147,13 +118,15 @@ def test_already_attached(self, capsys, fake_machine_token_file, event): fake_machine_token_file.attached = True with pytest.raises(AlreadyAttachedError): - action_attach(mock.MagicMock(), cfg=None) + attach_command.action(mock.MagicMock(), cfg=None) with pytest.raises(SystemExit): with mock.patch.object( event, "_event_logger_mode", event_logger.EventLoggerMode.JSON ): - main_error_handler(action_attach)(mock.MagicMock(), None) + main_error_handler(attach_command.action)( + mock.MagicMock(), None + ) msg = messages.E_ALREADY_ATTACHED.format(account_name="test") expected = { @@ -194,7 +167,7 @@ def test_lock_file_exists( ) """Check when an operation holds a lock file, attach cannot run.""" with pytest.raises(LockHeldError) as exc_info: - action_attach(mock.MagicMock(), cfg=cfg) + attach_command.action(mock.MagicMock(), cfg=cfg) assert 12 == m_check_lock_info.call_count assert expected_msg.msg == exc_info.value.msg @@ -202,7 +175,9 @@ def test_lock_file_exists( with mock.patch.object( event, "_event_logger_mode", event_logger.EventLoggerMode.JSON ): - main_error_handler(action_attach)(mock.MagicMock(), cfg) + main_error_handler(attach_command.action)( + mock.MagicMock(), cfg + ) expected = { "_schema_version": event_logger.JSON_SCHEMA_VERSION, @@ -227,31 +202,6 @@ def test_lock_file_exists( } assert expected == json.loads(capsys.readouterr()[0]) - @mock.patch( - "uaclient.status.format_tabular", return_value="mock_tabular_status" - ) - @mock.patch("uaclient.actions.status", return_value=("", 0)) - @mock.patch(M_PATH + "daemon") - def test_post_cli_attach( - self, - m_daemon, - m_status, - m_format_tabular, - capsys, - fake_machine_token_file, - ): - cfg = mock.MagicMock() - fake_machine_token_file.attached = True - _post_cli_attach(cfg) - - assert [mock.call()] == m_daemon.stop.call_args_list - assert [mock.call(cfg)] == m_daemon.cleanup.call_args_list - assert [mock.call(cfg)] == m_status.call_args_list - assert [mock.call("")] == m_format_tabular.call_args_list - out, _ = capsys.readouterr() - assert "This machine is now attached to 'test_contract'" in out - assert "mock_tabular_status" in out - @mock.patch("uaclient.lock.check_lock_info", return_value=(-1, "")) @mock.patch( "uaclient.entitlements.check_entitlement_apt_directives_are_unique", @@ -267,7 +217,7 @@ def test_post_cli_attach( @mock.patch("uaclient.files.notices.NoticesManager.remove") @mock.patch("uaclient.timer.update_messaging.update_motd_messages") @mock.patch(M_PATH + "contract.UAContractClient.add_contract_machine") - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") def test_happy_path_with_token_arg( self, m_post_cli, @@ -296,7 +246,7 @@ def fake_contract_attach(contract_token, attachment_dt): contract_machine_attach.side_effect = fake_contract_attach with mock.patch.object(lock, "lock_data_file"): - ret = action_attach(args, cfg) + ret = attach_command.action(args, cfg) assert 0 == ret expected_calls = [ @@ -317,7 +267,7 @@ def fake_contract_attach(contract_token, attachment_dt): @mock.patch("uaclient.files.notices.NoticesManager.remove") @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.timer.update_messaging.update_motd_messages") - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") @mock.patch(M_PATH + "actions.attach_with_token") def test_auto_enable_passed_through_to_attach_with_token( self, @@ -339,7 +289,7 @@ def test_auto_enable_passed_through_to_attach_with_token( cfg = FakeConfig() with mock.patch.object(lock, "lock_data_file"): - action_attach(args, cfg) + attach_command.action(args, cfg) assert [ mock.call(mock.ANY, token="token", allow_enable=auto_enable) @@ -357,7 +307,7 @@ def test_attach_config_and_token_mutually_exclusive( cfg = FakeConfig() with pytest.raises(UbuntuProError) as e: with mock.patch.object(lock, "lock_data_file"): - action_attach(args, cfg=cfg) + attach_command.action(args, cfg=cfg) assert e.value.msg == messages.E_ATTACH_TOKEN_ARG_XOR_CONFIG.msg @@ -365,7 +315,7 @@ def test_attach_config_and_token_mutually_exclusive( @mock.patch( M_PATH + "contract.UAContractClient.update_activity_token", ) - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") @mock.patch(M_PATH + "actions.attach_with_token") def test_token_from_attach_config( self, @@ -381,7 +331,7 @@ def test_token_from_attach_config( ) cfg = FakeConfig() with mock.patch.object(lock, "lock_data_file"): - action_attach(args, cfg=cfg) + attach_command.action(args, cfg=cfg) assert [ mock.call(mock.ANY, token="faketoken", allow_enable=True) @@ -406,7 +356,7 @@ def test_attach_config_invalid_config( cfg = FakeConfig() with pytest.raises(UbuntuProError) as e: with mock.patch.object(lock, "lock_data_file"): - action_attach(args, cfg=cfg) + attach_command.action(args, cfg=cfg) assert "Error while reading fakename:" in e.value.msg args.attach_config = FakeFile( @@ -418,7 +368,7 @@ def test_attach_config_invalid_config( event, "_event_logger_mode", event_logger.EventLoggerMode.JSON ): with mock.patch.object(lock, "lock_data_file"): - main_error_handler(action_attach)(args, cfg) + main_error_handler(attach_command.action)(args, cfg) expected_message = messages.E_ATTACH_CONFIG_READ_ERROR.format( config_name="fakename", @@ -498,7 +448,7 @@ def test_attach_config_enable_services( auto_enable=auto_enable, ) with mock.patch.object(lock, "lock_data_file"): - action_attach(args, cfg=cfg) + attach_command.action(args, cfg=cfg) assert [ mock.call(mock.ANY, token="faketoken", allow_enable=False) @@ -519,7 +469,7 @@ def test_attach_config_enable_services( event, "_event_logger_mode", event_logger.EventLoggerMode.JSON ): with mock.patch.object(lock, "lock_data_file"): - main_error_handler(action_attach)(args, cfg) + main_error_handler(attach_command.action)(args, cfg) expected = { "_schema_version": event_logger.JSON_SCHEMA_VERSION, @@ -652,7 +602,7 @@ def test_attach_when_one_service_fails_to_enable( event_logger.EventLoggerMode.JSON, ): with mock.patch.object(lock, "lock_data_file"): - main_error_handler(action_attach)(args, cfg) + main_error_handler(attach_command.action)(args, cfg) expected = { "_schema_version": event_logger.JSON_SCHEMA_VERSION, @@ -701,7 +651,7 @@ def test_magic_attach_revoke_token_if_wait_fails( with pytest.raises(MagicAttachTokenError): with mock.patch.object(lock, "lock_data_file"): - action_attach(args=m_args, cfg=FakeConfig()) + attach_command.action(args=m_args, cfg=FakeConfig()) assert 1 == m_initiate.call_count assert 1 == m_wait.call_count @@ -715,86 +665,8 @@ def test_magic_attach_fails_if_format_json_param_used( with pytest.raises(MagicAttachInvalidParam) as exc_info: with mock.patch.object(lock, "lock_data_file"): - action_attach(args=m_args, cfg=FakeConfig()) + attach_command.action(args=m_args, cfg=FakeConfig()) assert ( "This attach flow does not support --format with value: json" ) == exc_info.value.msg - - -@mock.patch(M_PATH + "contract.get_available_resources") -class TestParser: - @mock.patch("uaclient.log.setup_cli_logging") - def test_attach_help( - self, _m_resources, _m_setup_logging, capsys, FakeConfig - ): - with pytest.raises(SystemExit): - with mock.patch("sys.argv", ["/usr/bin/pro", "attach", "--help"]): - with mock.patch( - "uaclient.config.UAConfig", - return_value=FakeConfig(), - ): - main() - out, _err = capsys.readouterr() - assert HELP_OUTPUT in out - - def test_attach_parser_usage(self, _m_resources): - parser = attach_parser(mock.Mock()) - assert "pro attach [flags]" == parser.usage - - def test_attach_parser_prog(self, _m_resources): - parser = attach_parser(mock.Mock()) - assert "attach" == parser.prog - - def test_attach_parser_optionals_title(self, _m_resources): - parser = attach_parser(mock.Mock()) - assert "Flags" == parser._optionals.title - - def test_attach_parser_stores_token(self, _m_resources, FakeConfig): - full_parser = get_parser(FakeConfig()) - with mock.patch("sys.argv", ["pro", "attach", "token"]): - args = full_parser.parse_args() - assert "token" == args.token - - def test_attach_parser_allows_empty_required_token( - self, _m_resources, FakeConfig - ): - """Token required but parse_args allows none due to action_attach""" - full_parser = get_parser(FakeConfig()) - with mock.patch("sys.argv", ["pro", "attach"]): - args = full_parser.parse_args() - assert None is args.token - - def test_attach_parser_accepts_and_stores_no_auto_enable( - self, _m_resources, FakeConfig - ): - full_parser = get_parser(FakeConfig()) - with mock.patch( - "sys.argv", ["pro", "attach", "--no-auto-enable", "token"] - ): - args = full_parser.parse_args() - assert not args.auto_enable - - def test_attach_parser_defaults_to_auto_enable( - self, _m_resources, FakeConfig - ): - full_parser = get_parser(FakeConfig()) - with mock.patch("sys.argv", ["pro", "attach", "token"]): - args = full_parser.parse_args() - assert args.auto_enable - - def test_attach_parser_default_to_cli_format( - self, _m_resources, FakeConfig - ): - full_parser = get_parser(FakeConfig()) - with mock.patch("sys.argv", ["pro", "attach", "token"]): - args = full_parser.parse_args() - assert "cli" == args.format - - def test_attach_parser_accepts_format_flag(self, _m_resources, FakeConfig): - full_parser = get_parser(FakeConfig()) - with mock.patch( - "sys.argv", ["pro", "attach", "token", "--format", "json"] - ): - args = full_parser.parse_args() - assert "json" == args.format diff --git a/uaclient/cli/tests/test_cli_auto_attach.py b/uaclient/cli/tests/test_cli_auto_attach.py index 593c23bafe..4278445274 100644 --- a/uaclient/cli/tests/test_cli_auto_attach.py +++ b/uaclient/cli/tests/test_cli_auto_attach.py @@ -60,7 +60,7 @@ def test_auto_attach_help( out, _err = capsys.readouterr() assert HELP_OUTPUT == out - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") @mock.patch(M_PATH + "_full_auto_attach") def test_happy_path( self, @@ -94,7 +94,7 @@ def test_happy_path( ), ) @mock.patch(M_PATH + "event") - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") @mock.patch(M_PATH + "_full_auto_attach") def test_handle_full_auto_attach_errors( self, @@ -143,7 +143,7 @@ def test_handle_full_auto_attach_errors( ], ) @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) - @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "cli_util.post_cli_attach") @mock.patch(M_PATH + "_full_auto_attach") def test_uncaught_errors_are_handled( self, diff --git a/uaclient/cli/tests/test_cli_util.py b/uaclient/cli/tests/test_cli_util.py index 2ff2d35030..65faf481d6 100644 --- a/uaclient/cli/tests/test_cli_util.py +++ b/uaclient/cli/tests/test_cli_util.py @@ -7,6 +7,7 @@ assert_lock_file, assert_not_attached, assert_root, + post_cli_attach, ) from uaclient.exceptions import ( AlreadyAttachedError, @@ -149,3 +150,30 @@ def test_function(args, cfg): out, _err = capsys.readouterr() assert "" == out.strip() + + +class TestPostCliAttach: + @mock.patch( + "uaclient.status.format_tabular", return_value="mock_tabular_status" + ) + @mock.patch("uaclient.actions.status", return_value=("", 0)) + @mock.patch("uaclient.cli.cli_util.daemon") + def test_post_cli_attach( + self, + m_daemon, + m_status, + m_format_tabular, + capsys, + fake_machine_token_file, + ): + cfg = mock.MagicMock() + fake_machine_token_file.attached = True + post_cli_attach(cfg) + + assert [mock.call()] == m_daemon.stop.call_args_list + assert [mock.call(cfg)] == m_daemon.cleanup.call_args_list + assert [mock.call(cfg)] == m_status.call_args_list + assert [mock.call("")] == m_format_tabular.call_args_list + out, _ = capsys.readouterr() + assert "This machine is now attached to 'test_contract'" in out + assert "mock_tabular_status" in out