diff --git a/features/api_enable.feature b/features/api_enable.feature index 4021d303f3..0f6b582b83 100644 --- a/features/api_enable.feature +++ b/features/api_enable.feature @@ -211,3 +211,79 @@ Feature: u.pro.services.enable Examples: | release | machine_type | | jammy | lxd-vm | + + Scenario Outline: u.pro.services.enable.v1 with progress + Given a `` `` machine with ubuntu-advantage-tools installed + When I run `apt-get update` with sudo + And I attach `contract_token` with sudo and options `--no-auto-enable` + # Basic enable + And I run shell command `pro api u.pro.services.enable.v1 --show-progress --args service=esm-infra` with sudo + Then stdout contains substring: + """ + {"total_steps": 3, "done_steps": 0, "previous_step_message": null, "current_step_message": "Acquiring Ubuntu Pro lock"} + {"total_steps": 3, "done_steps": 1, "previous_step_message": "Acquiring Ubuntu Pro lock", "current_step_message": "Configuring APT access to Ubuntu Pro: ESM Infra"} + {"total_steps": 3, "done_steps": 2, "previous_step_message": "Configuring APT access to Ubuntu Pro: ESM Infra", "current_step_message": "Updating Ubuntu Pro: ESM Infra package lists"} + {"total_steps": 3, "done_steps": 3, "previous_step_message": "Updating Ubuntu Pro: ESM Infra package lists", "current_step_message": null} + {"_schema_version": "v1", "data": {"attributes": {"disabled": [], "enabled": ["esm-infra"], "messages": [], "reboot_required": false}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success" + """ + # Enabling multiple services shows steps correctly + When I run shell command `pro api u.pro.services.enable.v1 --show-progress --args service=ros` with sudo + Then I will see the following on stdout: + """ +<<<<<<< HEAD + {} +======= + {"total_steps": 7, "done_steps": 0, "previous_step_message": null, "current_step_message": "Acquiring Ubuntu Pro lock"} + {"total_steps": 7, "done_steps": 1, "previous_step_message": "Acquiring Ubuntu Pro lock", "current_step_message": "Configuring APT access to Ubuntu Pro: ESM Apps"} + {"total_steps": 7, "done_steps": 2, "previous_step_message": "Configuring APT access to Ubuntu Pro: ESM Apps", "current_step_message": "Updating Ubuntu Pro: ESM Apps package lists"} + {"total_steps": 7, "done_steps": 3, "previous_step_message": "Updating Ubuntu Pro: ESM Apps package lists", "current_step_message": "Configuring APT access to ROS ESM Security Updates"} + {"total_steps": 7, "done_steps": 4, "previous_step_message": "Configuring APT access to ROS ESM Security Updates", "current_step_message": "Updating ROS ESM Security Updates package lists"} + {"total_steps": 7, "done_steps": 5, "previous_step_message": "Updating ROS ESM Security Updates package lists", "current_step_message": "Configuring APT access to ROS ESM All Updates"} + {"total_steps": 7, "done_steps": 6, "previous_step_message": "Configuring APT access to ROS ESM All Updates", "current_step_message": "Updating ROS ESM All Updates package lists"} + {"total_steps": 7, "done_steps": 7, "previous_step_message": "Updating ROS ESM All Updates package lists", "current_step_message": null} + {"_schema_version": "v1", "data": {"attributes": {"disabled": [], "enabled": ["esm-apps", "ros", "ros-updates"], "messages": [], "reboot_required": false}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success" +>>>>>>> 0b13f113 (wip: cli api progress) + """ + + Examples: + | release | machine_type | + | xenial | lxd-container | + + Scenario Outline: u.pro.services.enable.v1 vm services with progress + Given a `` `` machine with ubuntu-advantage-tools installed + When I apt update + And I attach `contract_token` with sudo and options `--no-auto-enable` + And I run `pro api u.pro.services.enable.v1 --args service=livepatch --show-progress` with sudo + Then stdout contains substring: + """ + {"total_steps": 3, "done_steps": 0, "previous_step_message": null, "current_step_message": "Acquiring Ubuntu Pro lock"} + {"total_steps": 3, "done_steps": 1, "previous_step_message": "Acquiring Ubuntu Pro lock", "current_step_message": "Installing Livepatch"} + {"total_steps": 3, "done_steps": 2, "previous_step_message": "Installing Livepatch", "current_step_message": "Setting up Livepatch"} + {"total_steps": 3, "done_steps": 3, "previous_step_message": "Setting up Livepatch", "current_step_message": null} + {"_schema_version": "v1", "data": {"attributes": {"disabled": [], "enabled": ["livepatch"], "messages": [], "reboot_required": false}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success" + """ + # disables incompatible services and variant works + When I run `pro api u.pro.services.enable.v1 --show-progress --data '{"service": "realtime-kernel", "variant": "intel-iotg"}'` with sudo + Then stdout contains substring: + """ + {"total_steps": 5, "done_steps": 0, "previous_step_message": null, "current_step_message": "Acquiring Ubuntu Pro lock"} + {"total_steps": 5, "done_steps": 1, "previous_step_message": "Acquiring Ubuntu Pro lock", "current_step_message": "Disabling incompatible service: Livepatch"} + {"total_steps": 5, "done_steps": 2, "previous_step_message": "Disabling incompatible service: Livepatch", "current_step_message": "Configuring APT access to Real-time Intel IOTG Kernel"} + {"total_steps": 5, "done_steps": 3, "previous_step_message": "Configuring APT access to Real-time Intel IOTG Kernel", "current_step_message": "Updating Real-time Intel IOTG Kernel package lists"} + {"total_steps": 5, "done_steps": 4, "previous_step_message": "Updating Real-time Intel IOTG Kernel package lists", "current_step_message": "Installing Real-time Intel IOTG Kernel packages"} + {"total_steps": 5, "done_steps": 5, "previous_step_message": "Installing Real-time Intel IOTG Kernel packages", "current_step_message": null} + {"_schema_version": "v1", "data": {"attributes": {"disabled": ["livepatch"], "enabled": ["realtime-kernel"], "messages": [], "reboot_required": true}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success" + """ + When I run `pro api u.pro.status.enabled_services.v1` with sudo + Then API data field output matches regexp: + """ + \s*{ + \s* "name": "realtime-kernel", + \s* "variant_enabled": true, + \s* "variant_name": "intel-iotg" + \s*} + """ + + Examples: + | release | machine_type | + | jammy | lxd-vm | diff --git a/uaclient/api/api.py b/uaclient/api/api.py index 538a950e21..0624d53f7c 100644 --- a/uaclient/api/api.py +++ b/uaclient/api/api.py @@ -1,8 +1,8 @@ import json from importlib import import_module -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple -from uaclient.api import errors +from uaclient.api import AbstractProgress, errors from uaclient.api.data_types import APIData, APIResponse, ErrorWarningObject from uaclient.config import UAConfig from uaclient.data_types import IncorrectFieldTypeError @@ -91,7 +91,11 @@ def _process_data( def call_api( - endpoint_path: str, options: List[str], data: str, cfg: UAConfig + endpoint_path: str, + options: List[str], + data: str, + cfg: UAConfig, + progress_object: Optional[AbstractProgress] = None, ) -> APIResponse: if endpoint_path not in VALID_ENDPOINTS: @@ -125,7 +129,12 @@ def call_api( ) try: - result = endpoint.fn(options, cfg) + if endpoint.supports_progress: + result = endpoint.fn( + options, cfg, progress_object=progress_object + ) + else: + result = endpoint.fn(options, cfg) except Exception as e: return errors.error_out(e) @@ -135,7 +144,10 @@ def call_api( errors.APINoArgsForEndpoint(endpoint=endpoint_path) ) try: - result = endpoint.fn(cfg) + if endpoint.supports_progress: + result = endpoint.fn(cfg, progress_object=progress_object) + else: + result = endpoint.fn(cfg) except Exception as e: return errors.error_out(e) @@ -169,8 +181,10 @@ def __init__( name: str, fn: Callable, options_cls, + supports_progress: bool = False, ): self.version = version self.name = name self.fn = fn self.options_cls = options_cls + self.supports_progress = supports_progress diff --git a/uaclient/api/u/pro/services/enable/v1.py b/uaclient/api/u/pro/services/enable/v1.py index d0cc7e390d..6a29b919bd 100644 --- a/uaclient/api/u/pro/services/enable/v1.py +++ b/uaclient/api/u/pro/services/enable/v1.py @@ -171,4 +171,5 @@ def _enable( name="EnableService", fn=_enable, options_cls=EnableOptions, + supports_progress=True, ) diff --git a/uaclient/cli/cli_api.py b/uaclient/cli/cli_api.py index 5b52b1bd8d..162c0ab2fb 100644 --- a/uaclient/cli/cli_api.py +++ b/uaclient/cli/cli_api.py @@ -1,12 +1,41 @@ -from uaclient import exceptions, messages -from uaclient.api import api +import json +from collections import OrderedDict +from typing import Optional + +from uaclient import exceptions, messages, util +from uaclient.api import AbstractProgress +from uaclient.api.api import call_api + + +class CLIAPIProgress(AbstractProgress): + def progress( + self, + *, + total_steps: int, + done_steps: int, + previous_step_message: Optional[str], + current_step_message: Optional[str] + ): + d = OrderedDict() + d["total_steps"] = total_steps + d["done_steps"] = done_steps + d["previous_step_message"] = previous_step_message + d["current_step_message"] = current_step_message + print(json.dumps(d)) def action_api(args, *, cfg, **kwargs): if args.options and args.data: raise exceptions.CLIAPIOptionsXORData() - result = api.call_api(args.endpoint_path, args.options, args.data, cfg) + if args.show_progress: + progress = CLIAPIProgress() + else: + progress = None + + result = call_api( + args.endpoint_path, args.options, args.data, cfg, progress + ) print(result.to_json()) return 0 if result.result == "success" else 1 @@ -19,6 +48,11 @@ def add_parser(subparsers, cfg): parser.add_argument( "endpoint_path", metavar="endpoint", help=messages.CLI_API_ENDPOINT ) + parser.add_argument( + "--show-progress", + action="store_true", + help=messages.CLI_API_SHOW_PROGRESS, + ) parser.add_argument( "--args", dest="options", diff --git a/uaclient/messages/__init__.py b/uaclient/messages/__init__.py index fdef21a600..9b79aea25d 100644 --- a/uaclient/messages/__init__.py +++ b/uaclient/messages/__init__.py @@ -884,6 +884,10 @@ class TxtColor: CLI_API_DESC = t.gettext("Calls the Client API endpoints.") CLI_API_ENDPOINT = t.gettext("API endpoint to call") +CLI_API_SHOW_PROGRESS = t.gettext( + "For endpoints that support progress updates, show each progress update " + "on a new line in JSON format" +) CLI_API_ARGS = t.gettext( "Options to pass to the API endpoint, formatted as key=value" )