diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/CONTRIBUTING.md ubuntu-advantage-tools-27.12~22.04.1/CONTRIBUTING.md --- ubuntu-advantage-tools-27.11.3~22.04.1/CONTRIBUTING.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/CONTRIBUTING.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,16 +1,16 @@ -# Contributing to Ubuntu Advantage Client +# Contributing to Ubuntu Pro Client ## Developer Documentation -Developer documentation for the Ubuntu Advantage Client project. The topics cover +Developer documentation for the Ubuntu Pro Client project. The topics cover from the architecture of the project to how you should test any code changes. ### How to Guides -* [How to build UA](./dev-docs/howtoguides/building.md) +* [How to build](./dev-docs/howtoguides/building.md) * [How to run the code formatting tools](./dev-docs/howtoguides/code_formatting.md) * [How to run the tests](./dev-docs/howtoguides/testing.md) -* [How to release a new version of UA](./dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md) +* [How to release a new version](./dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md) * [How to use the contract staging environment](./dev-docs/howtoguides/use_staging_environment.md) * [How to use the magic attach endpoints](./dev-docs/howtoguides/how_to_use_magic_attach_endpoints.md) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/debian/changelog ubuntu-advantage-tools-27.12~22.04.1/debian/changelog --- ubuntu-advantage-tools-27.11.3~22.04.1/debian/changelog 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/debian/changelog 2022-11-22 13:06:26.000000000 +0000 @@ -1,8 +1,34 @@ -ubuntu-advantage-tools (27.11.3~22.04.1) jammy; urgency=medium +ubuntu-advantage-tools (27.12~22.04.1) jammy; urgency=medium - * Backport new upstream release: (LP: #1993006) to jammy + * Backport new upstream release: (LP: #1996424) to jammy - -- Grant Orndorff Tue, 25 Oct 2022 12:46:23 -0400 + -- Lucas Moura Tue, 22 Nov 2022 10:06:26 -0300 + +ubuntu-advantage-tools (27.12~23.04.1) lunar; urgency=medium + + * New upstream release 27.12 (LP: #1996424): + - auto-attach: + + retry auto-attach for up to one month on Ubuntu Pro cloud instances + + make a best effort to auto-attach when using the API + - enable: show deduplicated list of supported arches (GH: #917) + - fips: remove cloud package override logic from the client + - messaging: verify contract expiration date on contract server before + outputting expired message on MOTD + - realtime-kernel: make service non-beta + - reboot-required: + + add API support to show if the system requires a reboot + (u.pro.security.status.reboot_required.v1) + + add cli command for the functionality (pro system reboot-required) + - security-status: + + add API support to report standard updates (u.pro.packages.updates.v1) + + add API support to show CVEs patched by Livepatch + (u.pro.security.status.livepatch_cves.v1) + + add API support to show packages summary information + (u.pro.packages.summary.v1) + + list packages in oci manifest format (u.security.package_manifest.v1) + - systemd: do not attempt to auto-attach if a machine-token is present + + -- Lucas Moura Fri, 11 Nov 2022 14:27:00 -0300 ubuntu-advantage-tools (27.11.3~22.10.1) kinetic; urgency=medium diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/debian/control ubuntu-advantage-tools-27.12~22.04.1/debian/control --- ubuntu-advantage-tools-27.11.3~22.04.1/debian/control 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/debian/control 2022-11-22 13:06:26.000000000 +0000 @@ -38,12 +38,12 @@ python3-apt, python3-pkg-resources, ${extra:Depends} -Description: management tools for Ubuntu Advantage - Ubuntu Advantage is the professional package of tooling, technology +Description: management tools for Ubuntu Pro + Ubuntu Pro is the professional package of tooling, technology and expertise from Canonical, helping organisations around the world manage their Ubuntu deployments. . - Subscribers to Ubuntu Advantage will find helpful tools for accessing + Subscribers to Ubuntu Pro will find helpful tools for accessing services in this package. Package: ubuntu-advantage-pro @@ -51,6 +51,7 @@ Depends: ${misc:Depends}, ubuntu-advantage-tools (>=20.2) Replaces: ubuntu-advantage-tools (<<20.2) Breaks: ubuntu-advantage-tools (<<20.2) -Description: utilities and services for Ubuntu Pro images - The Ubuntu Pro package delivers additional utilities for use on authorised - Ubuntu Pro machines. +Description: Additional services for Ubuntu Pro images + This package delivers an additional service that performs an auto-attach + operation for Ubuntu Pro cloud instances. This package should not be manually + installed, as it is already present on the cloud instances that require it. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/explanations/how_auto_attach_works.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/explanations/how_auto_attach_works.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/explanations/how_auto_attach_works.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/explanations/how_auto_attach_works.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ # How auto-attach works -The `pro auto-attach` command follows a specific flow on every **Ubuntu Pro** image: +The `pro auto-attach` command follows a specific flow on every **Public Cloud Ubuntu Pro** image: 1. Identify which cloud the command is running on. This is achieved by running the `cloud-id` command provided by the [cloud-init](https://cloudinit.readthedocs.io/en/latest/) @@ -44,7 +44,7 @@ contains all the directives the pro client needs to setup the machine and enable the necessary services the token is associated with. -6. Disable the ubuntu-advantage [daemon](../explanations/what_is_the_daemon.md). +6. Disable the `ubuntu-advantage.service` [daemon](../explanations/what_is_the_daemon.md), if running. If the machine is detached, this daemon will be started again. Additionally, you can disable the `pro auto-attach` command by adding diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/explanations/systemd_units.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/explanations/systemd_units.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/explanations/systemd_units.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/explanations/systemd_units.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,89 @@ +# Mechanisms for auto-attaching Ubuntu Pro Cloud instances + +> **Note** +> This document explains the systemd units that attempt to auto-attach in various scenarios. If you're interested in how auto-attach itself works, see [How auto-attach works](./how_auto_attach_works.md). + +There are three methods by which a cloud instance may auto-attach to become Ubuntu Pro. + +1. On boot auto-attach for known Pro cloud instances. +2. Upgrade-in-place for non-Pro instances that get modified via the Cloud platform to entitle them to become Ubuntu Pro (only on GCP for now) +3. Retry auto-attach in case of failures + +(1) is handled by a systemd unit (`ua-auto-attach.service`) delivered by a separate package called `ubuntu-advantage-pro`. This package is only installed on Ubuntu Pro Cloud images. In this way, an instance launched from an Ubuntu Pro Cloud image knows that it needs to auto-attach. + +(2) and (3) are both handled in a systemd unit (`ubuntu-advantage.service`) that is present on all Ubuntu machines (including non-Pro). + +Below is a flow chart intended to describe how all of these methods and systemd units interact. + +```mermaid +graph TD; + %% nodes + %%%% decisions + is_pro{Is -pro installed?} + auto_outcome{Success?} + is_attached{Attached?} + should_run_daemon{on GCP? or retry flag set?} + is_gcp{GCP?} + is_retry{retry flag set?} + is_gcp_pro{Pro license detected?} + daemon_attach_outcome{Success?} + daemon_attach_outcome2{Success?} + + %%%% actions + auto_attach[/Try to Attach/] + trigger_retry[/Create Retry Flag File/] + trigger_retry2[/Create Retry Flag File/] + poll_gcp[/Poll for GCP Pro license/] + daemon_attach[/Try to Attach/] + daemon_attach2[/Try to Attach/] + wait[/Wait a while/] + + %%%% systemd units + auto(ua-auto-attach.service) + daemon(ubuntu-advantage.service) + + %%%% states + done([End]) + + + %% arrows + is_pro--Yes-->auto + auto-->auto_attach + subgraph ua-auto-attach.service blocks boot + auto_attach-->auto_outcome + auto_outcome--No-->trigger_retry + end + + is_pro--No-->is_attached + trigger_retry-->is_attached + auto_outcome--Yes-->is_attached + is_attached--No-->should_run_daemon + is_attached--Yes-->done + should_run_daemon--No-->done + should_run_daemon--Yes-->daemon + + daemon-->is_gcp + subgraph ubuntu-advantage.service + is_gcp--Yes-->poll_gcp + subgraph poll for pro license + poll_gcp-->is_gcp_pro + is_gcp_pro--No-->poll_gcp + is_gcp_pro--Yes-->daemon_attach + daemon_attach-->daemon_attach_outcome + daemon_attach_outcome--No-->trigger_retry2 + end + trigger_retry2-->is_retry + is_gcp--No-->is_retry + is_retry--Yes-->daemon_attach2 + subgraph retry auto-attach + daemon_attach2-->daemon_attach_outcome2 + daemon_attach_outcome2--No-->wait + end + wait-->daemon_attach2 + end + + daemon_attach_outcome--Yes-->done + is_retry--No-->done + daemon_attach_outcome2--Yes-->done + daemon_attach_outcome2--Failed for a month-->done +``` \ No newline at end of file diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/how_to_release_a_new_version_of_ua.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,4 +1,4 @@ -# Ubuntu Advantage Client Releases +# Ubuntu Pro Client Releases ## Background @@ -38,7 +38,7 @@ b Create a new entry in the `debian/changelog` file: * You can do that by running `dch --newversion ` - * Remember to update the release from `UNRELEASED` to the ubuntu/devel release. Edit the version to look like: `27.2~21.10.1`, with the appropriate ua and ubuntu/devel version numbers. + * Remember to update the release from `UNRELEASED` to the ubuntu/devel release. Edit the version to look like: `27.2~21.10.1`, with the appropriate pro-client and ubuntu/devel version numbers. * Populate `debian/changelog` with the commits you have cherry-picked * You can do that by running `git log .. | log2dch` * This will generate a list of commits that could be included in the changelog. @@ -49,21 +49,21 @@ * To structure the changelog you can use the other entries as example. But we basically try to keep this order: debian changes, new features/modifications, testing. Within each section, bullet points should be alphabetized. - c. Create a PR on github into the release branch. Ask in the UA channel on mattermost for review. + c. Create a PR on github into the release branch. Ask in the ~UA channel on mattermost for review. d. When reviewing the release PR, please use the following guidelines when reviewing the new changelog entry: - * Is the version correctly updated ? We must ensure that the new version on the changelog is + * Is the version correctly updated? We must ensure that the new version on the changelog is correct and it also targets the latest Ubuntu release at the moment. - * Is the entry useful for the user ? The changelog entries should be user focused, meaning + * Is the entry useful for the user? The changelog entries should be user focused, meaning that we should only add entries that we think users will care about (i.e. we don't need entries when fixing a test, as this doesn't provide meaningful information to the user) - * Is this entry redundant ? Sometimes we may have changes that affect separate modules of the + * Is this entry redundant? Sometimes we may have changes that affect separate modules of the code. We should have an entry only for the module that was most affected by it - * Is the changelog entry unique ? We need to verify that the changelog entry is not already + * Is the changelog entry unique? We need to verify that the changelog entry is not already reflected in an earlier version of the changelog. If it is, we need not only to remove but double check the process we are using to cherry-pick the commits - * Is this entry actually reflected on the code ? Sometimes, we can have changelog entries + * Is this entry actually reflected on the code? Sometimes, we can have changelog entries that are not reflected in the code anymore. This can happen during development when we are still unsure about the behavior of a feature or when we fix a bug that removes the code that was added. We must verify each changelog entry that is added to be sure of their @@ -143,7 +143,7 @@ a. Ask the assigned ubuntu-advantage-tools reviewer/sponsor from Server team for a review of your MPs (If you don't know who that is, ask in ~Server). Include a link to the MP into ubuntu/devel and to the SRU bug. - b. If they request changes, create a PR into the release branch on github and ask UAClient team for review. After that is merged, cherry-pick the commit into your `upload--` branch and push to launchpad. Then notify the Server Team member that you have addressed their requests. + b. If they request changes, create a PR into the release branch on github and ask Pro Client team for review. After that is merged, cherry-pick the commit into your `upload--` branch and push to launchpad. Then notify the Server Team member that you have addressed their requests. * Some issues may just be filed for addressing in the future if they are not urgent or pertinent to this release. * Unless the changes are very minor, or only testing related, you should upload a new release candidate version to `ppa:ua-client/staging` as descibed in I.3. * After the release is finished, any commits that were merged directly into the release branch in this way should be brought back into `main` via a single PR. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/how_to_use_magic_attach_endpoints.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/how_to_use_magic_attach_endpoints.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/how_to_use_magic_attach_endpoints.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/how_to_use_magic_attach_endpoints.md 2022-11-22 13:06:26.000000000 +0000 @@ -3,7 +3,7 @@ > **Notice:** > Minimum version: 27.11 -Th UA Client provides three distinct endpoints to make it easier to perform +Th Ubuntu Pro Client provides three distinct endpoints to make it easier to perform the magic attach flow. They are: * u.pro.attach.magic.initiate.v1 @@ -18,7 +18,7 @@ will perform exactly that. When you run: ```console -$ ua api u.pro.attach.magic.initiate.v1 +$ pro api u.pro.attach.magic.initiate.v1 ``` It is expected for you to see the following json response: @@ -35,36 +35,36 @@ "expires_in": 10000, "token": "MAGIC_ATTACH_TOKEN", "user_code": "USER_CODE" - } + }, "type": "MagicAttachInitiate" }, "errors": [], "result": "success", - "version": "UA CLIENT VERSION", + "version": "UBUNTU PRO CLIENT VERSION", "warnings": [] } ``` It is noteworthy here that the `attributes` contain both the `user_code` and `token`. The `user_code` is the information that will be presented to the user, which it will make possible for the user -to validate the magic attach on the advantage portal. Additionally, the `token` information is required +to validate the magic attach on the Ubuntu Pro portal. Additionally, the `token` information is required for the other two API endpoints which will be described next. ## Wait endpoint -After we initiate the magic attach procedure, the user must go to the advantage portal and validate -the `user_code` it received. Once that is done, a ua token will be generated for the user, allowing +After we initiate the magic attach procedure, the user must go to the Ubuntu Pro portal and validate +the `user_code` it received. Once that is done, a contract token will be generated for the user, allowing the attach procedure to begin. The wait endpoint will wait for the user to perform all of those -steps on the advantage portal. To call it, use: +steps on the Ubuntu Pro portal. To call it, use: ```console -$ ua api u.pro.attach.magic.wait.v1 --args magic_token=MAGIC_ATTACH_TOKEN +$ pro api u.pro.attach.magic.wait.v1 --args magic_token=MAGIC_ATTACH_TOKEN ``` Note here that the command requires the `token` that was generated in the initiate step. This command will block and poll the server until there are any updates for that token. If the -user successfully performed the necessary steps on the advantage portal, we should see the following +user successfully performed the necessary steps on the Ubuntu Pro portal, we should see the following response: ```json @@ -83,7 +83,7 @@ }, "errors": [], "result": "success", - "version": "UA CLIENT VERSION", + "version": "UBUNTU PRO CLIENT VERSION", "warnings": [] } ``` @@ -108,7 +108,7 @@ } ], "result": "failure", - "version": "UA CLIENT VERSION", + "version": "UBUNTU PRO CLIENT VERSION", "warnings": [] } ``` @@ -122,7 +122,7 @@ If we want to revoke the token created during the initiate call, we can use the revoke command: ```console -$ ua api u.pro.attach.magic.revoke.v1 --args magic_token=MAGIC_ATTACH_TOKEN +$ pro api u.pro.attach.magic.revoke.v1 --args magic_token=MAGIC_ATTACH_TOKEN ``` If the token is valid, we should see the following output: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/testing.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/testing.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/howtoguides/testing.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/howtoguides/testing.md 2022-11-22 13:06:26.000000000 +0000 @@ -24,7 +24,7 @@ ## Integration Tests -ubuntu-advantage-client uses [behave](https://behave.readthedocs.io) +Ubuntu Pro Client uses [behave](https://behave.readthedocs.io) for its integration testing. The integration test definitions are stored in the `features/` @@ -34,7 +34,7 @@ By default, integration tests will do the following on a given cloud platform: * Launch an instance running latest daily image of the target Ubuntu release - * Add the Ubuntu advantage client daily build PPA: [ppa:ua-client/daily](https://code.launchpad.net/~ua-client/+archive/ubuntu/daily) + * Add the Ubuntu Pro client daily build PPA: [ppa:ua-client/daily](https://code.launchpad.net/~ua-client/+archive/ubuntu/daily) * Install the appropriate ubuntu-advantage-tools and ubuntu-advantage-pro deb * Run the integration tests on that instance. @@ -174,7 +174,7 @@ the required EC2 credentials. To specifically run non-ubuntu pro tests using canonical cloud-images an -additional token obtained from https://ubuntu.com/advantage needs to be set: +additional token obtained from https://ubuntu.com/pro needs to be set: - UACLIENT_BEHAVE_CONTRACT_TOKEN= By default, the public AMIs for Ubuntu Pro testing used for each Ubuntu @@ -217,7 +217,7 @@ the required Azure credentials. To specifically run non-ubuntu pro tests using canonical cloud-images an -additional token obtained from https://ubuntu.com/advantage needs to be set: +additional token obtained from https://ubuntu.com/pro needs to be set: - UACLIENT_BEHAVE_CONTRACT_TOKEN= * To manually run Azure integration tests with a specific Image Id provide the @@ -249,7 +249,7 @@ the required GCP credentials. To specifically run non-ubuntu pro tests using canonical cloud-images an -additional token obtained from https://ubuntu.com/advantage needs to be set: +additional token obtained from https://ubuntu.com/pro needs to be set: - UACLIENT_BEHAVE_CONTRACT_TOKEN= * To manually run GCP integration tests with a specific Image Id provide the diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/architecture.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/architecture.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/architecture.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/architecture.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ # Architecture -Ubuntu Advantage client, hereafter "UA client", is a python3-based command line +Ubuntu Pro Client is a python3-based command line utility. It provides a CLI to attach, detach, enable, disable and check status of support related services. @@ -8,8 +8,8 @@ advertise ESM service and available packages in MOTD and during various apt commands. -The `ubuntu-advantage-pro` package delivers auto-attach functionality via init -scripts and systemd services for various cloud platforms. +The `ubuntu-advantage-pro` package delivers auto-attach functionality via a +systemd service for various cloud platforms. By default, Ubuntu machines are deployed in an unattached state. A machine can get manually or automatically attached to a specific contract by interacting diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/directory_layout.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/directory_layout.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/directory_layout.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/directory_layout.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,21 +1,21 @@ # Directory layout -The following describes the intent of UA client related directories: +The following describes the intent of Ubuntu Pro Client related directories: | File/Directory | Intent | | -------- | -------- | -| ./tools | Helpful scripts used to publish, release or test various aspects of UA client | -| ./features/ | Behave BDD integration tests for UA Client -| ./uaclient/ | collection of python modules which will be packaged into ubuntu-advantage-tools package to deliver the UA Client CLI | +| ./tools | Helpful scripts used to publish, release or test various aspects of Ubuntu Pro Client | +| ./features/ | Behave BDD integration tests for Ubuntu Pro Client +| ./uaclient/ | collection of python modules which will be packaged into ubuntu-advantage-tools package to deliver the Ubuntu Pro Client CLI | | uaclient.entitlements | Service-specific \*Entitlement class definitions which perform enable, disable, status, and entitlement operations etc. All classes derive from base.py:UAEntitlement and many derive from repo.py:RepoEntitlement | | ./uaclient/cli.py | The entry-point for the command-line client | ./uaclient/clouds/ | Cloud-platform detection logic used in Ubuntu Pro to determine if a given should be auto-attached to a contract | | uaclient.contract | Module for interacting with the Contract Server API | -| uaclient.messages | Module that contains the messages delivered by UA to the user | -| uaclient.security | Module that hold the logic used to run `ua fix` commands | -| ./apt-hook/ | the C++ apt-hook delivering MOTD and apt command notifications about UA support services | +| uaclient.messages | Module that contains the messages delivered by `pro` to the user | +| uaclient.security | Module that hold the logic used to run `pro fix` commands | +| ./apt-hook/ | the C++ apt-hook delivering MOTD and apt command notifications about Ubuntu Pro support services | | ./apt-conf.d/ | apt config files delivered to /etc/apt/apt-conf.d to automatically allow unattended upgrades of ESM security-related components. If apt proxy settings are configured, an additional apt config file will be placed here to configure the apt proxy. | -| /etc/ubuntu-advantage/uaclient.conf | Configuration file for the UA client.| +| /etc/ubuntu-advantage/uaclient.conf | Configuration file for the Ubuntu Pro Client.| | /var/lib/ubuntu-advantage/private | `root` read-only directory containing Contract API responses, machine-tokens and service credentials | | /var/lib/ubuntu-advantage/machine-token.json | `world` readable file containing redacted Contract API responses, machine-tokens and service credentials | | /var/log/ubuntu-advantage.log | `root` read-only log of ubuntu-advantage operations | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/enabling_a_service.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/enabling_a_service.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/enabling_a_service.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/enabling_a_service.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,19 +1,19 @@ # Enabling a service -Each service controlled by UA client will have a python module in +Each service controlled by Ubuntu Pro Client will have a python module in uaclient/entitlements/\*.py which handles setup and teardown of services when enabled or disabled. If a contract entitles a machine to a service, `root` user can enable the -service with `ua enable `. If a service can be disabled -`ua disable ` will be permitted. +service with `pro enable `. If a service can be disabled +`pro disable ` will be permitted. -The goal of the UA client is to remain simple and flexible and let the +The goal of the Ubuntu Pro Client is to remain simple and flexible and let the contracts backend drive dynamic changes in contract offerings and constraints. -In pursuit of that goal, the UA client obtains most of it's service constraints +In pursuit of that goal, the Ubuntu Pro Client obtains most of it's service constraints from a machine token that it obtains from the Contract Server API. -The UA Client is simple in that it relies on the machine token on the attached +The Ubuntu Pro Client is simple in that it relies on the machine token on the attached machine to describe whether a service is applicable for an environment and what configuration is required to properly enable that service. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/terminology.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/terminology.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/terminology.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/terminology.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,13 +1,13 @@ # Terminology The following vocabulary is used to describe different aspects of the work -Ubuntu Advantage Client performs: +Ubuntu Pro Client performs: | Term | Meaning | | -------- | -------- | -| UA Client | The python command line client represented in this ubuntu-advantage-client repository. It is installed on each Ubuntu machine and is the entry-point to enable any Ubuntu Advantage commercial service on an Ubuntu machine. | -| Contract Server | The backend service exposing a REST API to which UA Client authenticates in order to obtain contract and commercial service information and manage which support services are active on a machine.| -| Entitlement/Service | An Ubuntu Advantage commercial support service such as FIPS, ESM, Livepatch, CIS-Audit to which a contract may be entitled | +| Ubuntu Pro Client | The python command line client represented in this ubuntu-advantage-client repository. It is installed on each Ubuntu machine and is the entry-point to enable any Ubuntu Pro commercial service on an Ubuntu machine. | +| Contract Server | The backend service exposing a REST API to which Ubuntu Pro Client authenticates in order to obtain contract and commercial service information and manage which support services are active on a machine.| +| Entitlement/Service | An Ubuntu Pro commercial support service such as FIPS, ESM, Livepatch, CIS-Audit to which a contract may be entitled | | Affordance | Service-specific list of applicable architectures and Ubuntu series on which a service can run | | Directives | Service-specific configuration values which are applied to a service when enabling that service | | Obligations | Service-specific policies that must be instrumented for support of a service. Example: `enableByDefault: true` means that any attached machine **MUST** enable a service on attach | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/what_happens_during_attach.md ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/what_happens_during_attach.md --- ubuntu-advantage-tools-27.11.3~22.04.1/dev-docs/references/what_happens_during_attach.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/dev-docs/references/what_happens_during_attach.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,5 +1,5 @@ ### What happens during attach -After running the command `ua attach TOKEN`, UA will perform the following steps: +After running the command `pro attach TOKEN`, Ubuntu Pro Client will perform the following steps: * read the config from /etc/ubuntu-advantage/uaclient.conf to obtain the contract\_url (default: https://contracts.canonical.com) @@ -7,9 +7,9 @@ /api/v1/context/machines/token providing the \ * The Contract Server responds with a JSON blob containing an unique machine token, service credentials, affordances, directives and obligations to allow - enabling and disabling Ubuntu Advantage services -* UA client writes the machine token API response to the root-readonly + enabling and disabling Ubuntu Pro services +* Ubuntu Pro Client writes the machine token API response to the root-readonly /var/lib/ubuntu-advantage/private/machine-token.json and a version with secrets redacted to the world-readable file /var/lib/ubuntu-advantage/machine-token.json. -* UA client auto-enables any services defined with +* Ubuntu Pro Client auto-enables any services defined with `obligations:{enableByDefault: true}` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/conf.py ubuntu-advantage-tools-27.12~22.04.1/docs/conf.py --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/conf.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/conf.py 2022-11-22 13:06:26.000000000 +0000 @@ -17,7 +17,7 @@ # -- Project information ----------------------------------------------------- -project = "Ubuntu Advantage Client" +project = "Ubuntu Pro Client" copyright = "2022, Canonical Ltd." @@ -38,6 +38,11 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] +# It seems we need to request creation of automatic anchors for our headings. +# Setting to 2 because that's what we need now. +# If referencing any heading of lesser importance, adjust here. +myst_heading_anchors = 2 + # -- Options for HTML output ------------------------------------------------- diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/apt_messages.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/apt_messages.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/apt_messages.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/apt_messages.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ -# UA related APT messages +# Ubuntu Pro related APT messages -When running some APT commands, you might see Ubuntu Advantage (UA) related messages on +When running some APT commands, you might see Ubuntu Pro related messages on the output of those commands. Currently, we deliver those messages when running either `apt-get upgrade` or `apt-get dist-upgrade` commands. The scenarios where we deliver those messages are: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/how_to_interpret_the_security_status_command.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/how_to_interpret_the_security_status_command.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/how_to_interpret_the_security_status_command.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/how_to_interpret_the_security_status_command.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,10 +1,9 @@ # How to interpret the security-status command output The `security-status` command is used to get an overview -of the packages installed in your machine. Currently, -the command only support machine readable output: `json` or `yaml`. +of the packages installed in your machine. -If you run the `ua security-status --format yaml` command on your +If you run the `pro security-status --format yaml` command on your machine, you are expected to see the output following this structure: ``` @@ -42,9 +41,9 @@ Patched: true ``` -Let's understand what each key mean on the output of the `ua security-status` command: +Let's understand what each key mean on the output of the `pro security-status` command: -* **`summary`**: The summary of the system related to Ubuntu Advantage (UA) and +* **`summary`**: The summary of the system related to Ubuntu Pro and the different package sources in the system: * **`num_installed_packages`**: The total number of installed packages in the system. @@ -82,11 +81,11 @@ num_universe_packages: 0 ``` - * **`ua`**: An object representing the state of UA on the system: - * **`attached`**: If the system is attached to a UA subscription. + * **`ua`**: An object representing the state of Ubuntu Pro on the system: + * **`attached`**: If the system is attached to an Ubuntu Pro subscription. * **`enabled_services`**: A list of services that are enabled on the system. If unattached, this will always be an empty list. - * **`entitled_services`**: A list of services that are entitled on your UA subscription. If + * **`entitled_services`**: A list of services that are entitled on your Ubuntu Pro subscription. If unattached, this will always be an empty list. * **`packages`**: A list of security updates for packages installed in the system. @@ -98,8 +97,8 @@ `esm-apps` or `standard-security`. * **`status`**: The status for this update. It will be one of: * **"upgrade_available"**: The package can be upgraded right now. - * **"pending_attach"**: The package needs a UA subscription attached to be upgraded. - * **"pending_enable"**: The machine is attached to a UA subscription, but the service required to + * **"pending_attach"**: The package needs an Ubuntu Pro subscription attached to be upgraded. + * **"pending_enable"**: The machine is attached to an Ubuntu Pro subscription, but the service required to provide the upgrade is not enabled. * **"upgrade_unavailable"**: The machine is attached, but the contract is not entitled to the service which provides the upgrade. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/motd_messages.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/motd_messages.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/motd_messages.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/motd_messages.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,10 +1,10 @@ -# UA related messages on MOTD +# Ubuntu Pro related messages on MOTD -When UA is installed on the system, it delivers custom messages on [MOTD](https://wiki.debian.org/motd). +When Ubuntu Pro Client (`pro`) is installed on the system, it delivers custom messages on [MOTD](https://wiki.debian.org/motd). Those messages are generated directly by two different sources: * **python script**: The [update-notifier](https://wiki.ubuntu.com/UpdateNotifier) deliver a script - called `apt_check.py`. Considering UA related information, this script is responsible for: + called `apt_check.py`. Considering Ubuntu Pro related information, this script is responsible for: * inform the user about the status of one of the ESM services, `esm-apps` if the machine is a LTS series or `esm-infra` if the series is on ESM mode. @@ -14,11 +14,11 @@ those services are enabled: ``` - UA Apps: Expanded Security Maintenance (ESM) is enabled. + Expanded Security Maintenance for Applications is enabled. 11 updates can be applied immediately. - 5 of these updates are UA Apps: ESM security updates. - 1 of these updates is a UA Infra: ESM security update. + 5 of these updates are ESM Apps security updates. + 1 of these updates is a ESM Infra security update. 5 of these updates are standard security updates. To see these additional updates run: apt list --upgradable ``` @@ -27,11 +27,11 @@ advertised: ``` - UA Infra: Expanded Security Maintenance (ESM) is enabled. + Expanded Security Maintenance Infrastructure is enabled. 11 updates can be applied immediately. - 5 of these updates are UA Apps: ESM security updates. - 1 of these updates is a UA Infra: ESM security update. + 5 of these updates are ESM Apps security updates. + 1 of these updates is a ESM Infra security update. 5 of these updates are standard security updates. To see these additional updates run: apt list --upgradable ``` @@ -40,15 +40,15 @@ `esm-apps` was not enabled, the output will be: ``` - UA Apps: Expanded Security Maintenance (ESM) is not enabled. + Expanded Security Maintenance for Applications is not enabled. 6 updates can be applied immediately. - 1 of these updates is a UA Infra: ESM security update. + 1 of these updates is a ESM Infra security update. 5 of these updates are standard security updates. To see these additional updates run: apt list --upgradable - 5 additional security updates can be applied with UA Apps: ESM - Learn more about enabling UA Infra: ESM service for Ubuntu 16.04 at + 5 additional security updates can be applied with ESM Apps + Learn more about enabling ESM Apps for Ubuntu 16.04 at https://ubuntu.com/16-04 ``` @@ -57,12 +57,12 @@ for `esm-infra` if the service was disabled and the series running on the machine is on ESM state. -* **UA timer jobs**: One of the timer jobs UA has is used to insert additional messages into MOTD. +* **Ubuntu Pro timer jobs**: One of the timer jobs Ubuntu Pro has is used to insert additional messages into MOTD. Those messages will be always delivered before or after the content created by the python - script delivered by `update-notifier`. Those additional messages are generated when UA detect + script delivered by `update-notifier`. Those additional messages are generated when `pro` detects some conditions on the machine. They are: - * **subscription expired**: When the UA subscription is expired, UA will deliver the following + * **subscription expired**: When the Ubuntu Pro subscription is expired, `pro` will deliver the following message after the `update-notifier` message: ``` @@ -71,7 +71,7 @@ Renew your service at https://ubuntu.com/pro ``` - * **subscription about to expire**: When the UA subscription is about to expire, we deliver the + * **subscription about to expire**: When the Ubuntu Pro subscription is about to expire, we deliver the following message after the `update-notifier` message: ``` @@ -80,7 +80,7 @@ coverage for your applications. ``` - * **subscription expired but within grace period**: When the UA subscription is expired, but is + * **subscription expired but within grace period**: When the Ubuntu Pro subscription is expired, but is still within the grace period, we deliver the following message after the `update-notifier` script: @@ -104,9 +104,9 @@ ``` Note that we could also advertise the `esm-infra` service instead. This will happen - if you use an ESM release. Additionally, the same can for the url we use to advertise the - esm service, we adapt it based on the series that is running on the machine. + if you use an ESM release. Additionally, the the url we use to advertise the service is different + based on the series that is running on the machine. - Additionally, all of those UA custom messages are delivered into + Additionally, all of those Ubuntu Pro custom messages are delivered into `/var/lib/ubuntu-advantage/messages`. We also add custom scripts into `/etc/update-motd.d` to check if those messages exist and if they do, insert them on the full MOTD message. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/status_columns.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/status_columns.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/status_columns.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/status_columns.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ # Status output explanation -When running `ua status` we can observe two different types of outputs, attached vs unattached. +When running `pro status` we can observe two different types of outputs, attached vs unattached. When unattached, users will see the status table containing only three columns: ``` @@ -38,7 +38,7 @@ It is possible that a service appears as available when running status unattached, but turns out as not entitled. This happens because even if the service can be enabled on the machine, -if your UA subscription doesn't allow you to do that, UA cannot enable it. +if your Ubuntu Pro subscription doesn't allow you to do that, `pro` cannot enable it. Additionally, the **STATUS** column allows for three possible states: @@ -48,10 +48,10 @@ due to a non-contract restriction. For example, we cannot enable `livepatch` on a container. ### Notices -Notices are information regarding the UA status which either require some kind of action from the user, or may impact the experience with UA. +Notices are information regarding the Ubuntu Pro status which either require some kind of action from the user, or may impact the experience with Ubuntu Pro. -For example, let's say FIPS was just enabled, but the system wasn't rebooted yet (which is needed for booting into the FIPS Kernel), the output of `ua status` will contain: -```bash +For example, let's say FIPS was just enabled, but the system wasn't rebooted yet (which is needed for booting into the FIPS Kernel), the output of `pro status` will contain: +``` NOTICES FIPS support requires system reboot to complete configuration. ``` @@ -60,15 +60,15 @@ Notices can always be resolved, and the way to resolve it should be explicit in the notice itself. ### Features -Features are extra configuration values that can be set/unset in `uaclient.conf`. Most of those are meant for development/testing purposes, but some can be used in fome application flows. For example, to always have beta services with the same flow as the non-beta (for enable, status, etc), `uaclient.conf` may have: +Features are extra configuration values that can be set/unset in `uaclient.conf`. Most of those are meant for development/testing purposes, but some can be used in application flows. For example, to always have beta services with the same flow as the non-beta (for enable, status, etc), `uaclient.conf` may have: ``` features: - allow_beta: True + allow_beta: true +``` +In this case, the output of `pro status` will contain: ``` -In this case, the output of `ua status` will contain: -```bash FEATURES -+allow_beta +allow_beta: True ``` Keep in mind that any feature defined like this will be listed, even if it is invalid or typed the wrong way. Those appear on status for information/debugging purposes. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_are_the_timer_jobs.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_are_the_timer_jobs.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_are_the_timer_jobs.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_are_the_timer_jobs.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,23 +1,23 @@ # Timer jobs -UA client sets up a systemd timer to run jobs that need to be executed recurrently. Everytime the +Ubuntu Pro Client (`pro`) sets up a systemd timer to run jobs that need to be executed recurrently. Everytime the timer runs, it decides which jobs need to be executed based on their intervals. When a job runs successfully, its next run is determined by the interval defined for that job. ## Current jobs -The jobs that UA client runs periodically are: +The jobs that `pro` runs periodically are: | Job | Description | Interval | | --- | ----------- | -------- | | update_messaging | Update MOTD and APT messages | 6 hours | -| update_status | Update UA status | 12 hours | -| metering | (Only when attached to UA services) Pings Canonical servers for contract metering | 4 hours | +| update_status | Update Ubuntu Pro status | 12 hours | +| metering | (Only when attached to Ubuntu Pro services) Pings Canonical servers for contract metering | 4 hours | - The `update_messaging` job makes sure that the MOTD and APT messages match the available/enabled services on the system, showing information about available packages or security updates. -- The `update_status` job makes sure the `ua status` command will have the latest +- The `update_status` job makes sure the `pro status` command will have the latest information even when executed by a non-root user, updating the `/var/lib/ubuntu-advantage/status.json` file. - The `metering` will inform Canonical on which services are enabled on the machine. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,15 @@ +# What is a Public Cloud Ubuntu Pro machine? + +Ubuntu Pro images are published to [AWS](https://ubuntu.com/aws/pro), [Azure](https://ubuntu.com/azure/pro) and [GCP](https://ubuntu.com/gcp/pro) which come with Ubuntu +Pro support and services built in. On first boot, Ubuntu Pro images will automatically attach +to an Ubuntu Pro support contract and enable necessary Ubuntu Pro services so +that no extra setup is required to ensure a secure and supported Ubuntu machine. + +There are two primary flavors of Ubuntu Pro images in clouds: + +* Ubuntu Pro: Ubuntu LTS images with attached Ubuntu Pro support with kernel Livepatch and +ESM security access already enabled. Ubuntu Pro images are entitled to enable any additional Ubuntu Pro +services (like [`fips`](../howtoguides/enable_fips.md) or [`usg`](../howtoguides/enable_cis.md)). +* Ubuntu Pro FIPS: Specialized Ubuntu Pro images for 16.04, 18.04 and 20.04 which come pre-enabled +with the cloud-optimized FIPS-certified kernel and all additional SSL and security hardening +enabled. These images are available as [AWS Ubuntu Pro FIPS](https://ubuntu.com/aws/fips), [Azure Ubuntu Pro FIPS](https://ubuntu.com/azure/fips) and GCP Ubuntu Pro FIPS. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_the_daemon.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_the_daemon.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_the_daemon.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_the_daemon.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,8 +1,8 @@ # What is the Pro Upgrade Daemon? -UA client sets up a daemon on supported platforms (currently GCP only) to detect if an Ubuntu Pro license is purchased for the machine. If a Pro license is detected, then the machine is automatically attached. +Ubuntu Pro Client sets up a daemon on supported platforms (currently GCP only) to detect if an Ubuntu Pro license is purchased for the machine. If a Pro license is detected, then the machine is automatically attached. -If you are uninterested in UA services, you can safely stop and disable the daemon using systemctl: +If you are uninterested in Ubuntu Pro services, you can safely stop and disable the daemon using systemctl: ``` sudo systemctl stop ubuntu-advantage.service diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,4 @@ # What is the ubuntu-advantage-pro package? -The ubuntu-advantage-pro package is used by [Ubuntu PRO](what_is_ubuntu_pro.md) machine to automate machine attach on boot. -Therefore, the only main difference between the ubuntu-advantage-pro and ubuntu-advantage-tools -package is that the pro package ships a systemd unit that runs the auto-attach command on first -boot. +The ubuntu-advantage-pro package is used by [Public Cloud Ubuntu Pro](what_are_ubuntu_pro_cloud_instances.md) machines to automate machine attach on boot. +Therefore, the only thing that `ubuntu-advantage-pro` does is ship a systemd unit that runs an auto-attach command on first boot. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_ubuntu_pro.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_ubuntu_pro.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_is_ubuntu_pro.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_is_ubuntu_pro.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -# What is an Ubuntu PRO machine? - -Ubuntu PRO premium images are published to [AWS](https://ubuntu.com/aws/pro), [Azure](https://ubuntu.com/azure/pro) and [GCP](https://ubuntu.com/gcp/pro) which come with Ubuntu -Advantage support and services built in. On first boot, Ubuntu PRO images will automatically attach -to an Ubuntu Advantage support contract and enable necessary security and support out of the box so -that no extra setup is required to ensure a secure and supported Ubuntu machine. - -There are two primary flavors of Ubuntu PRO images in clouds: - -* Ubuntu PRO: Ubuntu LTS images with attached Ubuntu Advantage support with kernel Livepatch and -ESM security access already enabled. Ubuntu PRO images are entitled to enable any additional UA -services. -* Ubuntu PRO FIPS: Specialized Ubuntu PRO images for 16.04, 18.04 and 20.04 which come pre-enabled -with the cloud-optimized FIPS-certified kernel and all additional SSL and security hardening -enabled are available in [AWS Ubuntu PRO FIPS](https://ubuntu.com/aws/fips), [Azure Ubuntu PRO FIPS](https://ubuntu.com/azure/fips) and GCP Ubuntu PRO FIPS diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_refresh_does.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_refresh_does.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/what_refresh_does.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/what_refresh_does.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,13 +1,13 @@ # What refresh does -When you run the `ua refresh` command on your machine, three distinct stages are performed: +When you run the `pro refresh` command on your machine, three distinct stages are performed: * **contract**: The contract information on the machine is refreshed. If we find any deltas between the old contract and the new one, we process that delta and apply the changes - on the machine. If you need only this stage during refresh, run `ua refresh contract`. + on the machine. If you need only this stage during refresh, run `pro refresh contract`. * **config**: If there is any config change made on `/etc/ubuntu-advantage/uaclient.conf`, those - changes will now be applied to the machine. If you need only this stage during refresh, run `ua refresh config`. + changes will now be applied to the machine. If you need only this stage during refresh, run `pro refresh config`. -* **MOTD and APT messages**: UA process new MOTD and APT messages and refresh the machine to use - them. If you need only this stage during refresh, run `ua refresh messages`. +* **MOTD and APT messages**: Process new MOTD and APT messages and refresh the machine to use + them. If you need only this stage during refresh, run `pro refresh messages`. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/why_trusty_is_no_longer_supported.md ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/why_trusty_is_no_longer_supported.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/explanations/why_trusty_is_no_longer_supported.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/explanations/why_trusty_is_no_longer_supported.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,7 +1,7 @@ # Why trusty is no longer supported -On 14.04 (Trusty), the Ubuntu Advantange Client (UA) package is not receiving upstream updates +On 14.04 (Trusty), the Ubuntu Pro Client package is not receiving upstream updates beyond version 19.6 plus any critical CVE maintenance related to this version. Version 19.6 already -has full-featured support of the applicable UA service offerings `esm-infra` and `livepatch`. In the -event that a CVE is discovered that affects the UA version on Trusty, a fix and backport will be +has full-featured support of the applicable Ubuntu Pro service offerings `esm-infra` and `livepatch`. In the +event that a CVE is discovered that affects the Ubuntu Pro Client version on Trusty, a fix and backport will be provided for those specific issues. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/configure_proxies.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/configure_proxies.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/configure_proxies.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/configure_proxies.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,49 +1,49 @@ # How to configure proxies -The UA client can be configured to use an HTTP/HTTPS proxy as needed for network requests. It will -also honor the no\_proxy environment variable if set to avoid using local proxies for certain -outbound traffic. In addition, the UA client will automatically set up proxies for all programs -required for enabling Ubuntu Advantage services. This includes APT, Snaps, and Livepatch. +The Ubuntu Pro Client can be configured to use an HTTP/HTTPS proxy as needed for network requests. It will +also honor the `no_proxy` environment variable if set to avoid using local proxies for certain +outbound traffic. In addition, the Ubuntu Pro Client will automatically set up proxies for all programs +required for enabling Ubuntu Pro services. This includes APT, Snaps, and Livepatch. ## HTTP/HTTPS Proxies To configure standard HTTP and/or HTTPS proxies, run the following commands: ```console -$ sudo ua config set http\_proxy=http://host:port -$ sudo ua config set https\_proxy=https://host:port +$ sudo pro config set http\_proxy=http://host:port +$ sudo pro config set https\_proxy=https://host:port ``` -After running the above commands, UA client: +After running the above commands, Ubuntu Pro Client: 1. Verifies that the proxy is working by using it to reach `api.snapcraft.io` 2. Configures itself to use the given proxy for all future network requests 3. If snapd is installed, configures snapd to use the given proxy 4. If Livepatch has already been enabled, configures Livepatch to use the given proxy - 1. If Livepatch is enabled after this command, UA client will configure + 1. If Livepatch is enabled after this command, Ubuntu Pro Client will configure Livepatch to use the given proxy at that time. To remove HTTP/HTTPS proxy configuration, run the following: ```console -$ sudo ua config unset http\_proxy -$ sudo ua config unset https\_proxy +$ sudo pro config unset http\_proxy +$ sudo pro config unset https\_proxy ``` -After running the above commands, UA client will also remove proxy +After running the above commands, Ubuntu Pro Client will also remove proxy configuration from snapd (if installed) and Livepatch (if enabled). ## APT Proxies -APT proxy settings are configured separately. To have UA client manage your +APT proxy settings are configured separately. To have Ubuntu Pro Client manage your APT proxy configuration, run the following commands: ```console -$ sudo ua config set apt\_http\_proxy=http://host:port -$ sudo ua config set apt\_https\_proxy=https://host:port +$ sudo pro config set apt\_http\_proxy=http://host:port +$ sudo pro config set apt\_https\_proxy=https://host:port ``` -After running the above commands, UA client: +After running the above commands, Ubuntu Pro Client: 1. Verifies that the proxy works by using it to reach `archive.ubuntu.com` or `esm.ubuntu.com`. 2. Configures APT to use the given proxy by writing an apt configuration file to @@ -51,18 +51,18 @@ > **Note** > Any configuration file that comes later in the apt.conf.d -> directory could override the proxy configured by the UA client. +> directory could override the proxy configured by the Ubuntu Pro Client. To remove the APT proxy configuration, run the following: -$ sudo ua config unset apt\_http\_proxy -$ sudo ua config unset apt\_https\_proxy +$ sudo pro config unset apt\_http\_proxy +$ sudo pro config unset apt\_https\_proxy > **Note** -> Starting in to-be-released Version 27.9, APT proxies config options will -> change. You will be able to set global apt proxies that affect the whole system +> Starting in version 27.9, APT proxy config options changed. +> You will be able to set global apt proxies that affect the whole system > using the fields `global_apt_http_proxy` and `global_apt_https_proxy`. -> Alternatively, you could set apt proxies only for UA related services with the +> Alternatively, you could set apt proxies only for Ubuntu Pro related services with the > fields `ua_apt_http_proxy` and `ua_apt_https_proxy`. ## Authenticating @@ -71,14 +71,14 @@ the credentials directly in the URL when setting the configuration, as in: -$ sudo ua config set https\_proxy=https://username:password@host:port +$ sudo pro config set https\_proxy=https://username:password@host:port ## Checking the configuration -To see what proxies UA client is currently configured to use, you can use the show command. +To see what proxies Ubuntu Pro Client is currently configured to use, you can use the show command. ```console -$ sudo ua config show +$ sudo pro config show ``` The above will output something that looks like the following if there are proxies set: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/configuring_timer_jobs.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/configuring_timer_jobs.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/configuring_timer_jobs.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/configuring_timer_jobs.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ # How to configure a timer job -All timer jobs can be configured through the `ua config set` command. +All timer jobs can be configured through the `pro config set` command. We will show how we can properly use that command to interact with those jobs. @@ -9,7 +9,7 @@ To see each job’s running interval, use the show command: ```console -$ sudo ua config show +$ sudo pro config show ``` You should see output which include the timer jobs: @@ -24,12 +24,12 @@ ## Changing a timer job interval Each job has a configuration option of the form `_timer`, -which can be set with `ua config`. The expected value is a positive +which can be set with `pro config`. The expected value is a positive integer for the number of seconds in the interval. For example, to change the `update_status job` timer interval to run every 24 hours, run: ```console -$ sudo ua config set update_status_timer=86400 +$ sudo pro config set update_status_timer=86400 ``` @@ -39,5 +39,5 @@ the `update_messaging` job, run: ```console -$ sudo ua config set update_messaging_timer=0 +$ sudo pro config set update_messaging_timer=0 ``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/create_pro_golden_image.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/create_pro_golden_image.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/create_pro_golden_image.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/create_pro_golden_image.md 2022-11-22 13:06:26.000000000 +0000 @@ -6,4 +6,4 @@ * Use your cloud platform to clone or snapshot this VM as a golden image > **Note** -> Prior to version 27.11 - when launching instances based on this instance, you will need to re-enable any non-standard UA services that you enabled on the image. This will be faster on the new instance because it was already enabled on the image. You will not need to reboot for e.g. `fips` or `fips-updates`. +> Prior to version 27.11 - when launching instances based on this instance, you will need to re-enable any non-standard Ubuntu Pro services that you enabled on the image. This will be faster on the new instance because it was already enabled on the image. You will not need to reboot for e.g. `fips` or `fips-updates`. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_cc.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_cc.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_cc.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_cc.md 2022-11-22 13:06:26.000000000 +0000 @@ -12,7 +12,7 @@ To enable it through UA, please run: ```console -$ sudo ua enable cc-eal +$ sudo pro enable cc-eal ``` You should see output like the following, indicating that the CC EAL packages has @@ -33,7 +33,7 @@ If you would like to enable access to the CC EAL apt repository but not install the packages right away, use the `--access-only` flag while enabling. ```console -$ sudo ua enable cc-eal --access-only +$ sudo pro enable cc-eal --access-only ``` With that extra flag you'll see output like the following: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_cis.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_cis.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_cis.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_cis.md 2022-11-22 13:06:26.000000000 +0000 @@ -6,7 +6,7 @@ To access the CIS tooling first enable the software repository. ```console -$ sudo ua enable cis +$ sudo pro enable cis ``` You should see output like the following, indicating that the CIS package has been installed. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_esm_infra.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_esm_infra.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_esm_infra.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_esm_infra.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,23 +1,23 @@ -# How to enable ESM Infra +# How to enable Expanded Security Maintenance for Infrastructure (`esm-infra`) -For Ubuntu LTS releases, ESM Infra will be automatically enabled after attaching -the UA client to your account. After ubuntu-advantage-tools is installed and your machine is -attached, ESM Infra should be enabled. If ESM Infra is not enabled, you can enable it +For Ubuntu LTS releases, `esm-infra` will be automatically enabled after attaching +the Ubuntu Pro Client to your account. After ubuntu-advantage-tools is installed and your machine is +attached, `esm-infra` should be enabled. If `esm-infra` is not enabled, you can enable it with the following command: ```console -$ sudo ua enable esm-infra +$ sudo pro enable esm-infra ``` -With the ESM Infra repository enabled, specially on Ubuntu 14.04 and 16.04, you may see +With the `esm-infra` repository enabled, specially on Ubuntu 14.04 and 16.04, you may see a number of additional package updates available that were not available previously. Even if your system had indicated that it was up to date before installing the ubuntu-advantage-tools and attaching, make sure to check for new package updates after -ESM Infra is enabled using apt upgrade. If you have cron jobs set to install updates, or other +`esm-infra` is enabled using `apt upgrade`. If you have cron jobs set to install updates, or other unattended upgrades configured, be aware that this will likely result in a number of package updates -with the ESM content. +with the `esm-infra` content. -Running apt upgrade will now apply all of package updates available, including the ones in ESM. +Running apt upgrade will now apply all of package updates available, including the ones in `esm-infra`. ```console $ sudo apt upgrade diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_fips.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_fips.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_fips.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_fips.md 2022-11-22 13:06:26.000000000 +0000 @@ -3,9 +3,9 @@ [FIPS is supported on 16.04, 18.04 and 20.04 releases](https://ubuntu.com/security/certifications/docs/fips). To use FIPS, one can either launch existing Ubuntu premium support images which already have FIPS -kernel and security pre-enabled on first boot at [AWS Ubuntu PRO FIPS images](https://ubuntu.com/aws/fips), [Azure PRO FIPS images](https://ubuntu.com/azure/fips) and GCP PRO FIPS Images. +kernel and security pre-enabled on first boot at [AWS Ubuntu Pro FIPS images](https://ubuntu.com/aws/fips), [Azure Pro FIPS images](https://ubuntu.com/azure/fips) and GCP Pro FIPS Images. -Alternatively, enable FIPS using the UA client will install a FIPS-certified kernel and core security-related +Alternatively, enable FIPS using the Ubuntu Pro Client will install a FIPS-certified kernel and core security-related packages such as openssh-server/client and libssl. Note: disabling FIPS on an image is not yet supported @@ -21,7 +21,7 @@ To enable, run: ```console -$ sudo ua enable fips +$ sudo pro enable fips ``` You should see output like the following, indicating that the FIPS packages has been installed. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_in_dockerfile.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_in_dockerfile.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_in_dockerfile.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_in_dockerfile.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,131 @@ +# How to Enable Ubuntu Pro Services in a Dockerfile + +> Requires at least Ubuntu Pro Client version 27.7 + +Ubuntu Pro comes with several services, some of which can be useful in docker. For example, Expanded Security Maintenance of packages and FIPS certified packages may be desirable in a docker image. In this how-to-guide, we show how you can use the `pro` tool to take advantage of these services in your Dockerfile. + + +## Step 1: Create an Ubuntu Pro Attach Config file + +> **Note** +> The Ubuntu Pro Attach Config file will contain your Ubuntu Pro Contract token and should be treated as a secret file. + +An attach config file for `pro` is a yaml file that specifies some options when running `pro attach`. The file has two fields, `token` and `enable_services` and looks something like this: + +```yaml +token: TOKEN +enable_services: + - service1 + - service2 + - service3 +``` + +The `token` field is required and must be set to your Ubuntu Pro token that you can get from signing into [ubuntu.com/pro](https://ubuntu.com/pro). + +The `enable_services` field value is a list of Ubuntu Pro service names. When it is set, then the services specified will be automatically enabled after attaching with your Ubuntu Pro token. + +Service names that you may be interested in enabling in your docker builds include: +- `esm-infra` +- `esm-apps` +- `fips` +- `fips-updates` + +You can find out more about these services by running `pro help service-name` on any Ubuntu machine. + + +## Step 2: Create a Dockerfile to use `pro` and your attach config file + +Your Dockerfile is going to look something like this. + +There are comments inline explaining each line. + +```dockerfile +# Base off of the LTS of your choice +FROM ubuntu:focal + +# We mount a BuildKit secret here to access the attach config file which should +# be kept separate from the Dockerfile and managed in a secure fashion since it +# needs to contain your Ubuntu Pro token. +# In the next step, we demonstrate how to pass the file as a secret when +# running docker build. +RUN --mount=type=secret,id=pro-attach-config \ + + # First we update apt so we install the correct versions of packages in + # the next step + apt-get update \ + + # Here we install `pro` (ubuntu-advantage-tools) as well as ca-certificates, + # which is required to talk to the Ubuntu Pro authentication server securely. + && apt-get install --no-install-recommends -y ubuntu-advantage-tools ca-certificates \ + + # With pro installed, we attach using our attach config file from the + # previous step + && pro attach --attach-config /run/secrets/pro-attach-config \ + + ########################################################################### + # At this point, the container has access to all Ubuntu Pro services + # specified in the attach config file. + ########################################################################### + + # Always upgrade all packages to the latest available version with the Ubuntu Pro + # services enabled. + && apt-get upgrade -y \ + + # Then, you can install any specific packages you need for your docker + # container. + # Install them here, while Ubuntu Pro is enabled, so that you get the appropriate + # versions. + # Any `apt-get install ...` commands you have in an existing Dockerfile + # that you may be migrating to use Ubuntu Pro should probably be moved here. + && apt-get install -y openssl \ + + ########################################################################### + # Now that we've upgraded and installed any packages from the Ubuntu Pro + # services, we can clean up. + ########################################################################### + + # This purges ubuntu-advantage-tools, including all Ubuntu Pro related + # secrets from the system. + ########################################################################### + # IMPORTANT: As written here, this command assumes your container does not + # need ca-certificates so it is purged as well. + # If your container needs ca-certificates, then do not purge it from the + # system here. + ########################################################################### + && apt-get purge --auto-remove -y ubuntu-advantage-tools ca-certificates \ + + # Finally, we clean up the apt lists which shouldn't be needed anymore + # because any `apt-get install`s should've happened above. Cleaning these + # lists keeps your image smaller. + && rm -rf /var/lib/apt/lists/* + + +# Now, with all of your ubuntu apt packages installed, including all those +# from Ubuntu Pro services, you can continue the rest of your app-specific Dockerfile. +``` + +An important point to note about the above Dockerfile is that all of the `apt` and `pro` commands happen inside of one Dockerfile `RUN` instruction. This is critical and must not be changed. Keeping everything as written inside of one `RUN` instruction has two key benefits: + +1. Prevents any Ubuntu Pro Subscription-related tokens and secrets from being leaked in an image layer +2. Keeps the image as small as possible by cleaning up extra packages and files before the layer is finished. + +> **Note** +> These benefits could also be attained by squashing the image. + +## Step 3: Build the Docker image + + +Now, with our attach config file and Dockerfile created, we can build the image with a command like the following + +```bash +DOCKER_BUILDKIT=1 docker build . --secret id=pro-attach-config,src=pro-attach-config.yaml -t ubuntu-focal-pro +``` + +There are two important pieces of this command. + +1. We enable BuildKit with `DOCKER_BUILDKIT=1`. This is necessary to support the secret mount feature. +2. We use the secret mount feature of BuildKit with `--secret id=pro-attach-config,src=pro-attach-config.yaml`. This is what passes our attach config file in to be securely used by the `RUN --mount=type=secret,id=pro-attach-config` command in the Dockerfile. + +## Success + +Congratulations! At this point, you should have a docker image that has been built with Ubuntu Pro packages installed from whichever Ubuntu Pro service you required. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_livepatch.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_livepatch.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_livepatch.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_livepatch.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,13 +1,11 @@ # How to enable Livepatch -Livepatch requires: - -* kernel version 4.4 or above (16.04+ delivered via the HWE Kernel https://wiki.ubuntu.com/Kernel/LTSEnablementStack) +Check if your kernel is supported by Livepatch here: https://ubuntu.com/security/livepatch/docs/kernels To enable, run: ```console -$ sudo ua enable livepatch +$ sudo pro enable livepatch ``` You should see output like the following, indicating that the Livepatch snap package has diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_realtime_kernel.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_realtime_kernel.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_realtime_kernel.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_realtime_kernel.md 2022-11-22 13:06:26.000000000 +0000 @@ -11,7 +11,7 @@ To enable it through UA, please run: ```console -$ sudo ua enable realtime-kernel --beta +$ sudo pro enable realtime-kernel --beta ``` You'll need to acknowledge a warning and then you should see output like the following, indicating that the Real-Time Kernel package has been installed. @@ -41,7 +41,7 @@ If you would like to enable access to the Real-Time Kernel apt repository but not install the kernel right away, use the `--access-only` flag while enabling. ```console -$ sudo ua enable realtime-kernel --beta --access-only +$ sudo pro enable realtime-kernel --beta --access-only ``` With that extra flag you'll see output like the following: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_ua_in_dockerfile.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_ua_in_dockerfile.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/enable_ua_in_dockerfile.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/enable_ua_in_dockerfile.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -# How to Enable Ubuntu Advantage Services in a Dockerfile - -> Requires at least UA Client version 27.7 - -Ubuntu Advantage (UA) comes with several services, some of which can be useful in docker. For example, Expanded Security Maintenance of packages and FIPS certified packages may be desirable in a docker image. In this how-to-guide, we show how you can use the `ua` tool to take advantage of these services in your Dockerfile. - - -## Step 1: Create a UA Attach Config file - -> **Note** -> The UA Attach Config file will contain your UA Contract token and should be treated as a secret file. - -An attach config file for ua is a yaml file that specifies some options when running `ua attach`. The file has two fields, `token` and `enable_services` and looks something like this: - -```yaml -token: TOKEN -enable_services: - - service1 - - service2 - - service3 -``` - -The `token` field is required and must be set to your UA token that you can get from signing into [ubuntu.com/advantage](https://ubuntu.com/advantage). - -The `enable_services` field value is a list of UA service names. When it is set, then the services specified will be automatically enabled after attaching with your UA token. - -Service names that you may be interested in enabling in your docker builds include: -- `esm-infra` -- `esm-apps` -- `fips` -- `fips-updates` - -You can find out more about these services by running `ua help service-name` on any Ubuntu machine. - - -## Step 2: Create a Dockerfile to use `ua` and your attach config file - -Your Dockerfile is going to look something like this. - -There are comments inline explaining each line. - -```dockerfile -# Base off of the LTS of your choice -FROM ubuntu:focal - -# We mount a BuildKit secret here to access the attach config file which should -# be kept separate from the Dockerfile and managed in a secure fashion since it -# needs to contain your UA token. -# In the next step, we demonstrate how to pass the file as a secret when -# running docker build. -RUN --mount=type=secret,id=ua-attach-config \ - - # First we update apt so we install the correct versions of packages in - # the next step - apt-get update \ - - # Here we install `ua` (ubuntu-advantage-tools) as well as ca-certificates, - # which is required to talk to the UA authentication server securely. - && apt-get install --no-install-recommends -y ubuntu-advantage-tools ca-certificates \ - - # With ua installed, we attach using our attach config file from the - # previous step - && ua attach --attach-config /run/secrets/ua-attach-config \ - - ########################################################################### - # At this point, the container has access to all UA services specified in - # the attach config file. - ########################################################################### - - # Always upgrade all packages to the latest available version with the UA services - # enabled. - && apt-get upgrade -y \ - - # Then, you can install any specific packages you need for your docker - # container. - # Install them here, while UA is enabled, so that you get the appropriate - # versions. - # Any `apt-get install ...` commands you have in an existing Dockerfile - # that you may be migrating to use UA should probably be moved here. - && apt-get install -y openssl \ - - ########################################################################### - # Now that we've upgraded and installed any packages from the UA services, - # we can clean up. - ########################################################################### - - # This purges ubuntu-advantage-tools, including all UA related secrets from - # the system. - ########################################################################### - # IMPORTANT: As written here, this command assumes your container does not - # need ca-certificates so it is purged as well. - # If your container needs ca-certificates, then do not purge it from the - # system here. - ########################################################################### - && apt-get purge --auto-remove -y ubuntu-advantage-tools ca-certificates \ - - # Finally, we clean up the apt lists which shouldn't be needed anymore - # because any `apt-get install`s should've happened above. Cleaning these - # lists keeps your image smaller. - && rm -rf /var/lib/apt/lists/* - - -# Now, with all of your ubuntu apt packages installed, including all those -# from UA services, you can continue the rest of your app-specific Dockerfile. -``` - -An important point to note about the above Dockerfile is that all of the `apt` and `ua` commands happen inside of one Dockerfile `RUN` instruction. This is critical and must not be changed. Keeping everything as written inside of one `RUN` instruction has two key benefits: - -1. Prevents any UA Subscription-related tokens and secrets from being leaked in an image layer -2. Keeps the image as small as possible by cleaning up extra packages and files before the layer is finished. - -> **Note** -> These benefits could also be attained by squashing the image. - -## Step 3: Build the Docker image - - -Now, with our attach config file and Dockerfile created, we can build the image with a command like the following - -```bash -DOCKER_BUILDKIT=1 docker build . --secret id=ua-attach-config,src=ua-attach-config.yaml -t ubuntu-focal-ua -``` - -There are two important pieces of this command. - -1. We enable BuildKit with `DOCKER_BUILDKIT=1`. This is necessary to support the secret mount feature. -2. We use the secret mount feature of BuildKit with `--secret id=ua-attach-config,src=ua-attach-config.yaml`. This is what passes our attach config file in to be securely used by the `RUN --mount=type=secret,id=ua-attach-config` command in the Dockerfile. - -## Success - -Congratulations! At this point, you should have a docker image that has been built with UA packages installed from whichever UA service you required. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/get_token_and_attach.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/get_token_and_attach.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/get_token_and_attach.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/get_token_and_attach.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,13 +1,14 @@ -# How to get UA token and attach to a subscription +# How to get an Ubuntu Pro token and attach to a subscription -Retrieve your UA token from the [advantage](https://ubuntu.com/advantage/) portal. You will log in with your SSO credentials, the same credentials you use for https://login.ubuntu.com. Note that you -can obtain a free personal token, which already provide you with access to several of the UA +Retrieve your Ubuntu Pro token from the [Ubuntu Pro portal](https://ubuntu.com/pro/). You will +log in with your SSO credentials, the same credentials you use for https://login.ubuntu.com. Note that you +can obtain a free personal token, which provides you with access to several of the Ubuntu Pro services. Once that token is obtained, to attach your machine to a subscription, just run: ``` -$ sudo ua attach YOUR_TOKEN +$ sudo pro attach YOUR_TOKEN ``` You should see output like the following, indicating that you have successfully associated this @@ -17,21 +18,19 @@ Enabling default service esm-infra Updating package lists ESM Infra enabled -This machine is now attached to 'UA Infra - Essential (Virtual)' +This machine is now attached to 'Ubuntu Pro' SERVICE ENTITLED STATUS DESCRIPTION -cis yes disabled Center for Internet Security Audit Tools +esm-apps yes enabled Expanded Security Maintenance for Applications esm-infra yes enabled Expanded Security Maintenance for Infrastructure -fips yes n/a NIST-certified FIPS modules -fips-updates yes n/a Uncertified security updates to FIPS modules -livepatch yes n/a Canonical Livepatch service +livepatch yes enabled Canonical Livepatch service NOTICES -Operation in progress: ua attach +Operation in progress: pro attach -Enable services with: ua enable +Enable services with: pro enable ``` -Once the UA client is attached to your UA account, you can use it to activate various services, +Once the Ubuntu Pro Client is attached to your Ubuntu Pro account, you can use it to activate various services, including: access to ESM packages, Livepatch, FIPS, and CIS. Some features are specific to certain LTS releases diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_attach_with_config_file.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_attach_with_config_file.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_attach_with_config_file.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_attach_with_config_file.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ # How to attach with a configuration file -To attach with a configuration file, you must run `ua attach` with the `--attach-config` flag, +To attach with a configuration file, you must run `pro attach` with the `--attach-config` flag, passing the path of the configuration file you intend to use. When using `--attach-config` the token must be passed in the file rather than on the command line. This is useful in situations where it is preferred to keep the secret token in a file. @@ -18,5 +18,5 @@ And can be passed on the cli like this: ```shell -sudo ua attach --attach-config /path/to/file.yaml +sudo pro attach --attach-config /path/to/file.yaml ``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_collect_logs.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_collect_logs.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_collect_logs.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_collect_logs.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,21 @@ +# How to collect Ubuntu Pro Client logs + +To collect all of the necessary logs for Ubuntu Pro Client (`pro`), please run the command: + +```console +$ sudo pro collect-logs +``` + +This command creates a tarball with all relevant data for debugging possible problems with `pro`. +It puts together: +* The Ubuntu Pro Client configuration file (the default is `/etc/ubuntu-advantage/uaclient.conf`) +* The Ubuntu Pro Client log files (the default is `/var/log/ubuntu-advantage*`) +* The files in `/etc/apt/sources.list.d/*` related to UA +* Output of `systemctl status` for the Ubuntu Pro Client related services +* Status of the timer jobs, `canonical-livepatch`, and the systemd timers +* Output of `cloud-id`, `dmesg` and `journalctl` + +Sensitive data is redacted from all files included in the tarball. As of now, the command must be run as root. + +Running the command creates a `ua_logs.tar.gz` file in the current directory. +The output file path/name can be changed using the `-o` option. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_collect_ua_logs.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_collect_ua_logs.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_collect_ua_logs.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_collect_ua_logs.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -# How to collect UA logs - -To collect all of the necessary logs for UA, please run the command: - -```console -$ sudo ua collect-logs -``` - -This command creates a tarball with all relevant data for debugging possible problems with UA. -It puts together: -* The UA Client configuration file (the default is `/etc/ubuntu-advantage/uaclient.conf`) -* The UA Client log files (the default is `/var/log/ubuntu-advantage*`) -* The files in `/etc/apt/sources.list.d/*` related to UA -* Output of `systemctl status` for the UA Client related services -* Status of the timer jobs, `canonical-livepatch`, and the systemd timers -* Output of `cloud-id`, `dmesg` and `journalctl` - -Sensitive data is redacted from all files included in the tarball. As of now, the command must be run as root. - -Running the command creates a `ua_logs.tar.gz` file in the current directory. -The output file path/name can be changed using the `-o` option. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_run_fix_in_dry_run_mode.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_run_fix_in_dry_run_mode.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_run_fix_in_dry_run_mode.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_run_fix_in_dry_run_mode.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,50 @@ +# How to run fix command in dry run mode + +If you are unsure on what changes will happen on your system after you run `pro fix` to address a +CVE/USN, you can use the `--dry-run` flag to see that packages will be installed in the system if +the command was actually run. For example, this is the output of running `pro fix USN-5079-2 --dry-run`: + +``` +WARNING: The option --dry-run is being used. +No packages will be installed when running this command. +USN-5079-2: curl vulnerabilities +Found CVEs: +https://ubuntu.com/security/CVE-2021-22946 +https://ubuntu.com/security/CVE-2021-22947 +1 affected source package is installed: curl +(1/1) curl: +A fix is available in Ubuntu Pro: ESM Infra. +The machine is not attached to an Ubuntu Pro subscription. +To proceed with the fix, a prompt would ask for a valid Ubuntu Pro token. +{ pro attach TOKEN } +Ubuntu Pro service: esm-infra is not enabled. +To proceed with the fix, a prompt would ask permission to automatically enable +this service. +{ pro enable esm-infra } +{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls } +✔ USN-5079-2 is resolved. +``` + +You can see that using `--dry-run` will also indicate which actions would need to happen +to completely address the USN/CVE. Here we can see that the package fix can only be accessed +through the `esm-infra` service. Therefore, we need an Ubuntu Pro subscription, as can be seen on this +part of the output: + +``` +The machine is not attached to an Ubuntu Pro subscription. +To proceed with the fix, a prompt would ask for a valid Ubuntu Pro token. +{ pro attach TOKEN } +``` + +Additionally, we also inform you that even with a subscription, we need the specific +`esm-infra` service to be enabled: + +``` +Ubuntu Pro service: esm-infra is not enabled. +To proceed with the fix, a prompt would ask permission to automatically enable +this service. +{ pro enable esm-infra } +``` + +After performing these steps during a fix command without `--dry-run`, your machine should +no longer be affected by that USN we used as an example. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_run_ua_fix_in_dry_run_mode.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_run_ua_fix_in_dry_run_mode.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_run_ua_fix_in_dry_run_mode.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_run_ua_fix_in_dry_run_mode.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -# How to run fix command in dry run mode - -If you are unsure on what changes will happen on your system after you run `ua fix` to address a -CVE/USN, you can use the `--dry-run` flag to see that packages will be installed in the system if -the command was actually run. For example, this is the output of running `ua fix USN-5079-2 --dry-run`: - -``` -WARNING: The option --dry-run is being used. -No packages will be installed when running this command. -USN-5079-2: curl vulnerabilities -Found CVEs: -https://ubuntu.com/security/CVE-2021-22946 -https://ubuntu.com/security/CVE-2021-22947 -1 affected source package is installed: curl -(1/1) curl: -A fix is available in Ubuntu Pro: ESM Infra. -The machine is not attached to an Ubuntu Advantage (UA) subscription. -To proceed with the fix, a prompt would ask for a valid UA token. -{ ua attach TOKEN } -UA service: esm-infra is not enabled. -To proceed with the fix, a prompt would ask permission to automatically enable -this service. -{ ua enable esm-infra } -{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls } -✔ USN-5079-2 is resolved. -``` - -You can see that using `--dry-run` will also indicate which actions would need to happen -to completely address the USN/CVE. Here we can see that the package fix can only be accessed -through the `esm-infra` service. Therefore, we need a UA subscription, as can be seen on this -part of the output: - -``` -The machine is not attached to an Ubuntu Advantage (UA) subscription. -To proceed with the fix, a prompt would ask for a valid UA token. -{ ua attach TOKEN } -``` - -Additionally, we also inform you that even with a subscription, we need the specific -`esm-infra` service to be enabled: - -``` -UA service: esm-infra is not enabled. -To proceed with the fix, a prompt would ask permission to automatically enable -this service. -{ ua enable esm-infra } -``` - -After performing these steps during a fix command without `--dry-run`, your machine should -no longer be affected by that USN we used as an example. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_simulate_attach.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_simulate_attach.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/how_to_simulate_attach.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/how_to_simulate_attach.md 2022-11-22 13:06:26.000000000 +0000 @@ -4,7 +4,7 @@ with a specific token, you can simulate running the attach operation by running: ```console -$ ua status --simulate-with-token YOUR_TOKEN +$ pro status --simulate-with-token YOUR_TOKEN ``` After running the command, you should see a modified status table, similar to this diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/update_motd_messages.md ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/update_motd_messages.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/howtoguides/update_motd_messages.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/howtoguides/update_motd_messages.md 2022-11-22 13:06:26.000000000 +0000 @@ -8,5 +8,5 @@ update the state of MOTD and APT messages: ```sh -ua refresh messages +pro refresh messages ``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/index.rst ubuntu-advantage-tools-27.12~22.04.1/docs/index.rst --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/index.rst 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/index.rst 2022-11-22 13:06:26.000000000 +0000 @@ -1,10 +1,10 @@ -Ubuntu Advantage Client -=================================== +Ubuntu Pro Client +================= -The Ubuntu Advantage (UA) Client is the offical tool to enable Canonical offerings on your +The Ubuntu Pro Client (``pro``) is the offical tool to enable Canonical offerings on your system. -UA provides support to view, enable, and disable the following Canonical services: +``pro`` provides support to view, enable, and disable the following Canonical services: - `Common Criteria EAL2 Certification Tooling `_ - `CIS Benchmark Audit Tooling `_ @@ -14,8 +14,9 @@ - `FIPS 140-2 Certified Modules (and optional non-certified patches `_ - `Livepatch `_ -If you need any of those services for your machine, UA is the right tool for you. -Furthermore, UA is already installed on every Ubuntu system. Try it out by running ``ua help``! +If you need any of those services for your machine, ``pro`` is the right tool for you. + +``pro`` is already installed on every Ubuntu system. Try it out by running ``pro help``! Getting help ************ diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/README.md ubuntu-advantage-tools-27.12~22.04.1/docs/README.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/README.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/README.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,6 @@ -# How to generate Ubuntu Advantage user documentation +# How to generate Ubuntu Pro Client user documentation -To build the docs for Ubuntu Advantage, you can use a dedicated `tox` command for it. +To build the docs for Ubuntu Pro Client, you can use a dedicated `tox` command for it. You can install `tox` on your machine by running the `make test` command. Once tox is installed just run the command: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/api.md ubuntu-advantage-tools-27.12~22.04.1/docs/references/api.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/api.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/references/api.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,336 @@ +# Pro API Reference Guide + + +The Pro Client has a Python-based API to be consumed by users who want to integrate the client's functionality to their software. +The functions and objects are available through the `uaclient.api` module, and all of the available endpoints return an object with specific data for the calls. + +Besides importing the Python code directly, consumers who are not writing Python may use the CLI to call the same functionality, using the `pro api` command. This command will always return a JSON with a standard structure, as can be seen below: + +```json +{ + "_schema_version": "v1", + "data": { + "attributes": { + // + }, + "meta": { + "environment_vars": [] + }, + "type": "" + }, + "errors": [], + "result": "", + "version": "", + "warnings": [] +} +``` + +The currently available endpoints are: +- [u.pro.version.v1](#uproversionv1) +- [u.pro.attach.magic.initiate.v1](#uproattachmagicinitiatev1) +- [u.pro.attach.magic.wait.v1](#uproattachmagicwaitv1) +- [u.pro.attach.magic.revoke.v1](#uproattachmagicrevokev1) +- [u.pro.attach.auto.should_auto_attach.v1](#uproattachautoshould_auto_attachv1) +- [u.pro.attach.auto.full_auto_attach.v1](#uproattachautofull_auto_attachv1) + +## u.pro.version.v1 +Shows the installed Client version. + +### Args +This endpoint takes no arguments. + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.version.v1 import version + +result = version() +``` + +#### Expected return object: +`uaclient.api.u.pro.version.v1.VersionResult` + +|Field Name|Type|Description| +|-|-|-| +|installed_version|str|The current installed version| + +### Raised Exceptions +- `VersionError`: raised if the client cannot determine the version + + +### CLI interaction +#### Calling from the CLI: +```bash +pro api u.pro.version.v1 +``` + +#### Expected attributes in JSON structure +```json +{ + "installed_version":"" +} +``` + + +## u.pro.attach.magic.initiate.v1 +Initiates the Magic Attach flow, retrieving the User Code to confirm the +operation and the Token used to proceed. + +### Args +This endpoint takes no arguments. + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.magic.initiate.v1 import initiate + +result = initiate() +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.magic.initiate.v1.MagicAttachInitiateResult` + +|Field Name|Type|Description| +|-|-|-| +|user_code|str|Code the user will see in the UI when confirming the Magic Attach| +|token|str|Magic token used by the tooling to continue the operation| +|expires|str|Timestamp of the Magic Attach process expiration| +|expires_in|int|Seconds before the Magic Attach process expires| + + +### Raised Exceptions + +- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server +- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction +- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment + +### CLI interaction +#### Calling from the CLI: +```bash +pro api u.pro.attach.magic.initiate.v1 +``` + +#### Expected attributes in JSON structure +```json +{ + "user_code":"", + "token":"", + "expires": "T.", + "expires_in": 600 +} +``` + + + +## u.pro.attach.magic.wait.v1 +Polls the contract server waiting for the user to confirm the Magic Attach. + +### Args +- `magic_token`: The token provided by the initiate endpoint + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.magic.wait.v1 import MagicAttachWaitOptions, wait + +options = MagicAttachWaitOptions(magic_token="") +result = wait(options) +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.magic.wait.v1.MagicAttachWaitResult` + +|Field Name|Type|Description| +|-|-|-| +|user_code|str|Code the user will see in the UI when confirming the Magic Attach| +|token|str|Magic token used by the tooling to continue the operation| +|expires|str|Timestamp of the Magic Attach process expiration| +|expires_in|int|Seconds before the Magic Attach process expires| +|contract_id|str|ID of the contract the machine will be attached to| +|contract_token|str|The contract token to attach the machine| + + +### Raised Exceptions + +- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server +- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction +- `MagicAttachTokenError`: raised when an invalid/expired token is sent +- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment + + +### CLI interaction +#### Calling from the CLI: +```bash +pro api u.pro.attach.magic.wait.v1 --args magic_token= +``` + +#### Expected attributes in JSON structure +```json +{ + "user_code":"", + "token":"", + "expires": "T.", + "expires_in": 500, + "contract_id": "", + "contract_token": "", +} +``` + + +## u.pro.attach.magic.revoke.v1 +Revokes a magic attach token. + +### Args +- `magic_token`: The token provided by the initiate endpoint + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.magic.revoke.v1 import MagicAttachRevokeOptions, revoke + +options = MagicAttachWaitOptions(magic_token="") +result = revoke(options) +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.magic.wait.v1.MagicAttachRevokeResult` + +No data present in the result. + + +### Raised Exceptions + +- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server +- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction +- `MagicAttachTokenAlreadyActivated`: raised when trying to revoke a token which was already activated through the UI +- `MagicAttachTokenError`: raised when an invalid/expired token is sent +- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment + + +### CLI interaction +#### Calling from the CLI: +```bash +pro api u.pro.attach.magic.revoke.v1 --args magic_token= +``` + +#### Expected attributes in JSON structure +```json +{} +``` + + +## u.pro.attach.auto.should_auto_attach.v1 +Checks if a given system should run auto-attach on boot. + +### Args +This endpoint takes no arguments. + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.auto.should_auto_attach.v1 import should_auto_attach + +result = should_auto_attach() +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.auto.should_auto_attach.v1.ShouldAutoAttachResult` + +|Field Name|Type|Description| +|-|-|-| +|should_auto_attach|bool|True if the system should run auto-attach on boot| + +### Raised Exceptions +No exceptions raised by this endpoint. + +### CLI interaction +#### Calling from the CLI: +```bash +pro api u.pro.attach.auto.should_auto_attach.v1 +``` + +#### Expected attributes in JSON structure +```json +{ + "should_auto_attach": false +} +``` + + +## u.pro.attach.auto.full_auto_attach.v1 +Runs the whole auto-attach process on the system. + +### Args +- `enable`: optional list of services to enable after auto-attaching +- `enable_beta`: optional list of beta services to enable after auto-attaching + +> If none of the lists are set, the services will be enabled based on the contract definitions. + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import full_auto_attach, FullAutoAttachOptions + +options = FullAutoAttachOptions(enable=["", ""], enable_beta=[""]) +result = full_auto_attach(options) +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.auto.full_auto_attach.v1.FullAutoAttachResult` + +No data present in the result. + +### Raised Exceptions + +- `AlreadyAttachedError`: raised if running on a machine which is already attached to a Pro subscription +- `AutoAttachDisabledError`: raised if `disable_auto_attach: true` in uaclient.conf +- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server +- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction +- `EntitlementsNotEnabledError`: raised if the client fails to enable any of the entitlements + (whether present in any of the lists or listed in the contract) +- `LockHeldError`: raised if another Client process is holding the lock on the machine +- `NonAutoAttachImageError`: raised if the cloud where the system is running does not support auto-attach +- `UserFacingError`: raised if: + - the client is unable to determine on which cloud the system is running + - the image where the client is running does not support auto-attach + + +### CLI interaction +#### Calling from the CLI: +This endpoint currently has no CLI support. Only the Python-based version is available. + + +## u.pro.attach.auto.configure_retry_service.v1 +Configures options for the retry auto attach functionality and create file that will activate the retry auto attach functionality if `ubuntu-advantage.service` runs. + +Note that this does not start `ubuntu-advantage.service`. This makes it useful for calling during the boot process `Before: ubuntu-advantage.service` so that when `ubuntu-advantage.service` starts, its ConditionPathExists check passes and executes the retry auto attach function. + +If you call this function outside of the boot process and would like the retry auto attach functionality to actually start, you'll need to call something like `systemctl start ubuntu-advantage.service`. + + +### Args +- `enable`: optional list of services to enable after auto-attaching +- `enable_beta`: optional list of beta services to enable after auto-attaching + +> If none of the lists are set, the services will be enabled based on the contract definitions. + +### Python API interaction +#### Calling from Python code +```python +from uaclient.api.u.pro.attach.auto.configure_retry_service.v1 import configure_retry_service, ConfigureRetryServiceOptions + +options = ConfigureRetryServiceOptions(enable=["", ""], enable_beta=[""]) +result = configure_retry_service(options) +``` + +#### Expected return object: +`uaclient.api.u.pro.attach.auto.configure_retry_service.v1.ConfigureRetryServiceResult` + +No data present in the result. + +### Raised Exceptions +No exceptions raised by this endpoint. + +### CLI interaction +#### Calling from the CLI: +This endpoint currently has no CLI support. Only the Python-based version is available. diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/network_requirements.md ubuntu-advantage-tools-27.12~22.04.1/docs/references/network_requirements.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/network_requirements.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/references/network_requirements.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,12 +1,12 @@ -# UA Network Requirements +# Ubuntu Pro Client Network Requirements -Using the UA client to enable support services will rely on network access to obtain updated service +Using the Ubuntu Pro Client to enable support services will rely on network access to obtain updated service credentials, add APT repositories to install deb packages and install [snap packages](https://snapcraft.io/about) when -Livepatch is enabled. Also see the Proxy Configuration explanation to inform UA client of HTTP(S)/APT proxies. +Livepatch is enabled. Also see the Proxy Configuration explanation to inform Ubuntu Pro Client of HTTP(S)/APT proxies. Ensure the managed system has access to the following port:urls if in a network-limited environment: -* 443:https://contracts.canonical.com/ - HTTP PUTs, GETs and POSTs for UAClient interaction +* 443:https://contracts.canonical.com/ - HTTP PUTs, GETs and POSTs for Ubuntu Pro Client interaction * 443:https://esm.ubuntu.com/\* - APT repository access for most services Enabling kernel Livepatch require additional network egress: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/ppas.md ubuntu-advantage-tools-27.12~22.04.1/docs/references/ppas.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/ppas.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/references/ppas.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,9 +1,9 @@ -# Available PPAs with different version of Ubuntu Advantage Client -There are 3 PPAs with different release channels of the Ubuntu Advantage Client: +# Available PPAs with different version of Ubuntu Pro Client +There are 3 PPAs with different release channels of the Ubuntu Pro Client: -1. Stable: This contains stable builds only which have been verified for release into Ubuntu stable releases or Ubuntu PRO images. +1. Stable: This contains stable builds only which have been verified for release into Ubuntu stable releases or Ubuntu Pro images. - add with `sudo add-apt-repository ppa:ua-client/stable` 2. Staging: This contains builds under validation for release to stable Ubuntu releases and images - add with `sudo add-apt-repository ppa:ua-client/staging` 3. Daily: This PPA is updated every day with the latest changes. - - add with `sudo add-apt-repository ppa:ua-client/daily` \ No newline at end of file + - add with `sudo add-apt-repository ppa:ua-client/daily` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/support_matrix.md ubuntu-advantage-tools-27.12~22.04.1/docs/references/support_matrix.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/references/support_matrix.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/references/support_matrix.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,8 +1,8 @@ # Support Matrix for the client -Ubuntu Advantage services are only available on Ubuntu Long Term Support (LTS) releases. +Ubuntu Pro services are only available on Ubuntu Long Term Support (LTS) releases. -On interim Ubuntu releases, `ua status` will report most of the services as 'n/a' and disallow enabling those services. +On interim Ubuntu releases, `pro status` will report most of the services as 'n/a' and disallow enabling those services. Below is a list of platforms and releases ubuntu-advantage-tools supports @@ -17,5 +17,3 @@ | Impish | amd64, arm64, armhf, ppc64el, riscv64, s390x | Last release 27.9 | | Jammy | amd64, arm64, armhf, ppc64el, riscv64, s390x | Active SRU of all features | | Kinetic | amd64, arm64, armhf, ppc64el, riscv64, s390x | Active SRU of all features | - -Note: ppc64el will not have all APT messaging due to insufficient golang support diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/basic_commands.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/basic_commands.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/basic_commands.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/basic_commands.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,257 @@ +# Getting Started with Ubuntu Pro Client + +The Ubuntu Pro Client (`pro`) provides users with a simple mechanism to +view, enable, and disable offerings from Canonical on their system. In this tutorial +we will cover the base `pro` commands that allow a user to successfully manage the offering +on their machine. + +## Prerequisites + +On this tutorial, you will use [LXD](https://linuxcontainers.org/lxd/) containers. +To set up LXD on your computer, please follow this [guide](https://linuxcontainers.org/lxd/getting-started-cli/). + +## Cover base `pro` commands + +When dealing with `pro` through the CLI, there are six main `pro` commands that cover the main +functionalites of the tool. They are: + +* **status** +* **attach** +* **refresh** +* **detach** +* **enable** +* **disable** + +In this tutorial, we will go through all those commands to show how to properly use them. +To achieve that, we will use a Xenial LXD container. + +## Creating the Xenial LXD container + +To test all of those commands, let's create a Xenial LXD container. Remember to set up LXD as +mentioned on the [Prerequisites](#prerequisites) section. After that, just run the command: + +```console +$ lxc launch ubuntu-daily:xenial dev-x +``` + +After running that, let's access the container by running: + +```console +$ lxc shell dev-x +``` + +## Base `pro` commands + +### Status + +The status command of `pro` allows you to see the status of any Ubuntu Pro service on your machine. +It also easily allows you to verify if your machine is attached to an Ubuntu Pro subscription +or not. + +Let's run it on the LXD container: + +```console +$ pro status +``` + +It is expected for you to see an output similar to this one: + +``` +SERVICE AVAILABLE DESCRIPTION +cis yes Center for Internet Security Audit Tools +esm-infra yes Expanded Security Maintenance for Infrastructure +fips yes NIST-certified core packages +fips-updates yes NIST-certified core packages with priority security updates +livepatch yes Canonical Livepatch service + +This machine is not attached to an Ubuntu Pro subscription. +See https://ubuntu.com/pro +``` + +You can see that the status command shows the services that are available for that given machine, +while also presenting a short description for each of them. + +Additionally, if you look at the last lines of the output, you can identify that this machine is not +attached to an Ubuntu Pro subscription. +``` +This machine is not attached to an Ubuntu Pro subscription. +See https://ubuntu.com/pro +``` + +### Attach + +To access any of those service offerings, you need to attach to an Ubuntu Pro subscription. This is +achieved by running the attach command. Before you run it, you need to get an Ubuntu Pro token. +Any user with a Ubuntu One account is entitled to a free personal token to use with Ubuntu Pro. +You can retrieve your Ubuntu Pro token from the [Ubuntu Pro portal](https://ubuntu.com/pro/). +You will log in with your SSO credentials, the same credentials you use for https://login.ubuntu.com. +After getting your Ubuntu Pro token, go to the LXD container and run: + +```console +$ sudo pro attach YOUR_TOKEN +``` + +It is expected for you to see an output similar to this one: + +``` +Enabling default service esm-infra +Updating package lists +Ubuntu Pro: ESM Infra enabled +This machine is now attached to 'USER ACCOUNT' + +SERVICE ENTITLED STATUS DESCRIPTION +cis yes disabled Center for Internet Security Audit Tools +esm-infra yes enabled Expanded Security Maintenance for Infrastructure +fips yes n/a NIST-certified core packages +fips-updates yes n/a NIST-certified core packages with priority security updates +livepatch yes n/a Canonical Livepatch service + +NOTICES +Operation in progress: pro attach + +Enable services with: pro enable + + Account: USER ACCOUNT + Subscription: USER SUBSCRIPTION + Valid until: 9999-12-31 00:00:00+00:00 +Technical support level: essential +``` + +From this output, you can see that the attach command enables all of the services specified by the user subscription. +After the command ends, `pro` displays the new state of the machine. +That status output is exactly what you will see if you run the status command again. +You can confirm this by running: + +```console +$ pro status +``` + +One question that may arise is that the output of `pro status` while attached is different from +the output of `pro status` when unattached. When attached, status presents two new columns, +**ENTITLED** and **STATUS**, while also dropping the **AVAILABLE** column. For more information +of why the output is different, please refer to this [explanation](../explanations/status_columns.md). + +Finally, another useful bit at the end of both attach and status is the contract expiration date: + +``` + Account: USER ACCOUNT +Subscription: USER SUBSCRIPTION +Valid until: 9999-12-31 00:00:00+00:00 +``` + +The `Valid until` field describes when your contract will be expired, so you can be aware of when it +needs to be renewed. + + +### Refresh + +In the last section, we mentioned that your contract can expire. Although free tokens never expire, if +you buy an Ubuntu Pro subscription, and later need to renew the contract, how you can make your machine aware of +it? You can do this through the `refresh` command: + +```console +$ sudo pro refresh +``` + +This command will refresh the contract on your machine. This command is also really useful if you +want to change any definitions on your subscription. For example, let's assume that you now want +`cis` to be enabled by default when attaching. After you modify your subscription for that, running +the refresh command will process any changes that were performed in the subscription, enabling +`cis` because of this. + +> Note: the refresh command does more than just update the contract in the machine. If you want +more information about the command, please take a look at this [explanation](../explanations/what_refresh_does.md). + +### Enable + +There is another way to enable a service that wasn't activated during attach or refresh. +Suppose that you want to enable `cis` on this machine manually. To achieve that, You can use the +enable command. + +Let's enable `cis` on our LXD container by running: + +```console +$ sudo pro enable cis +``` + +After running it, you should see an output similar to this one: + +``` +One moment, checking your subscription first +Updating package lists +Installing CIS Audit packages +CIS Audit enabled +Visit https://security-certs.docs.ubuntu.com/en/cis to learn how to use CIS +``` + +You can confirm that `cis` is enabled now by running: + +```console +$ pro status +``` + +And you should see: +``` +SERVICE ENTITLED STATUS DESCRIPTION +cis yes enabled Center for Internet Security Audit Tools +esm-infra yes enabled Expanded Security Maintenance for Infrastructure +fips yes n/a NIST-certified core packages +fips-updates yes n/a NIST-certified core packages with priority security updates +livepatch yes n/a Canonical Livepatch service +``` + +You can see now that `cis` is marked as `enabled` on status. + + +### Disable + +Let's suppose that you don't want a service anymore, you can also disable any service offering +through `pro`. For example, let's disable the `cis` service you just enabled by running on the LXD +container: + +```console +$ sudo pro disable cis +``` + +After running that command, let's now run `pro status` to see what happened to `cis`: +``` +SERVICE ENTITLED STATUS DESCRIPTION +cis yes disabled Center for Internet Security Audit Tools +esm-infra yes enabled Expanded Security Maintenance for Infrastructure +fips yes n/a NIST-certified core packages +fips-updates yes n/a NIST-certified core packages with priority security updates +livepatch yes n/a Canonical Livepatch service +``` + +You can see that `cis` status is back to disabled. + +> Note: the disable command doesn't uninstall any package that was installed by +the service. The command only removes the access you have to the service, but it +doesn't undo any configuration that was applied on the machine. + +### Detach + +Finally, what if you don't want this machine to be attached to an Ubuntu Pro subscription any longer? +You can achieve that using the `detach` command: + +```console +$ sudo pro detach +``` + +This command will disable all of the Ubuntu Pro services on the machine for you and +get rid of the subscription stored on your machine during attach. + +> Note: the detach command will also not uninstall any packages that were installed by +any service enabled through `pro`. + +### Final thoughts + +This tutorial has covered the 6 main commands of `pro`. If you need more advanced options to configure +the tool, please take a look in [How to guides](../howtoguides). If that still doesn't cover +your needs, feel free to reach the `pro` team on `#ubuntu-server` on Libera IRC. + +Before you finish this tutorial, exit the container by running `CTRL-D` and delete it by running +this command on the machine: +```console +$ lxc delete --force dev-x +``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/basic_ua_commands.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/basic_ua_commands.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/basic_ua_commands.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/basic_ua_commands.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,257 +0,0 @@ -# Getting Started with UA - -The Ubuntu Advantage (UA) Tools provides users with a simple mechanism to -view, enable, and disable offerings from Canonical on their system. In this tutorial -we will cover the base UA commands that allow a user to successfully manage the offering -on their machine. - -## Prerequisites - -On this tutorial, you will use [LXD](https://linuxcontainers.org/lxd/) containers. -To set up LXD on your computer, please follow this [guide](https://linuxcontainers.org/lxd/getting-started-cli/). - -## Cover base UA commands - -When dealing with UA through the CLI, there are six main UA commands that cover the main -functionalites of the tool. They are: - -* **status** -* **attach** -* **refresh** -* **detach** -* **enable** -* **disable** - -In this tutorial, we will go through all those commands to show how to properly use them. -To achieve that, we will use a Xenial LXD container. - -## Creating the Xenial LXD container - -To test all of those commands, let's create a Xenial LXD container. Remember to set up LXD as -mentioned on the [Prerequisites](#prerequisites) section. After that, just run the command: - -```console -$ lxc launch ubuntu-daily:xenial dev-x -``` - -After running that, let's access the container by running: - -```console -$ lxc shell dev-x -``` - -## Base UA commands - -### Status - -The status command of UA allows you to see the status of any UA service on your machine. -It also easily allows you to verify if your machine is attached to a UA subscription -or not. - -Let's run it on the LXD container: - -```console -$ ua status -``` - -It is expected for you to see an output similar to this one: - -``` -SERVICE AVAILABLE DESCRIPTION -cis yes Center for Internet Security Audit Tools -esm-infra yes Expanded Security Maintenance for Infrastructure -fips yes NIST-certified core packages -fips-updates yes NIST-certified core packages with priority security updates -livepatch yes Canonical Livepatch service - -This machine is not attached to a UA subscription. -See https://ubuntu.com/advantage -``` - -You can see that the status command shows the services that are available for that given machine, -while also presenting a short description for each of them. - -Additionally, if you look at the last lines of the output, you can identify that this machine is not -attached to a UA subscription. -``` -This machine is not attached to a UA subscription. -See https://ubuntu.com/advantage -``` - -### Attach - -To access any of those service offerings, you need to attach to a UA subscription. This is -achieved by running the attach command. Before you run it, you need to get a UA token. -Any user with a Ubuntu One account is entitled to a free personal token to use with UA. -You can retrieve your UA token from the [advantage](https://ubuntu.com/advantage/) portal. -You will log in with your SSO credentials, the same credentials you use for https://login.ubuntu.com. -After getting your UA token, go to the LXD container and run: - -```console -$ sudo ua attach YOUR_TOKEN -``` - -It is expected for you to see an output similar to this one: - -``` -Enabling default service esm-infra -Updating package lists -Ubuntu Pro: ESM Infra enabled -This machine is now attached to 'USER ACCOUNT' - -SERVICE ENTITLED STATUS DESCRIPTION -cis yes disabled Center for Internet Security Audit Tools -esm-infra yes enabled Expanded Security Maintenance for Infrastructure -fips yes n/a NIST-certified core packages -fips-updates yes n/a NIST-certified core packages with priority security updates -livepatch yes n/a Canonical Livepatch service - -NOTICES -Operation in progress: ua attach - -Enable services with: ua enable - - Account: USER ACCOUNT - Subscription: USER SUBSCRIPTION - Valid until: 9999-12-31 00:00:00+00:00 -Technical support level: essential -``` - -From this output, you can see that the attach command enables all of the services specified by the user subscription. -After the command ends, `ua` displays the new state of the machine. -That status output is exactly what you will see if you run the status command again. -You can confirm this by running: - -```console -$ ua status -``` - -One question that may arise is that the output of `ua status` while attached is different from -the output of `ua status` when unattached. When attached, status presents two new columns, -**ENTITLED** and **STATUS**, while also dropping the **AVAILABLE** column. For more information -of why the output is different, please refer to this [explanation](../explanations/status_columns.md). - -Finally, another useful bit at the end of both attach and status is the contract expiration date: - -``` - Account: USER ACCOUNT -Subscription: USER SUBSCRIPTION -Valid until: 9999-12-31 00:00:00+00:00 -``` - -The `Valid until` field describes when your contract will be expired, so you can be aware of when it -needs to be renewed. - - -### Refresh - -In the last section, we mentioned that your contract can expire. Although free tokens never expire, if -you buy a UA subscription, and later need to renew the contract, how you can make your machine aware of -it ? You can do this through the `refresh` command: - -```console -$ sudo ua refresh -``` - -This command will refresh the contract on your machine. This command is also really useful if you -want to change any definitions on your subscription. For example, let's assume that you now want -`cis` to be enabled by default when attaching. After you modify your subscription for that, running -the refresh command will process any changes that were performed in the subscription, enabling -`cis` because of this. - -> Note: the refresh command does more than just update the contract in the machine. If you want -more information about the command, please take a look at this [explanation](../explanations/what_refresh_does.md). - -### Enable - -There is another way to enable a service that wasn't activated during attach or refresh. -Suppose that you want to enable `cis` on this machine manually. To achieve that, You can use the -enable command. - -Let's enable `cis` on our LXD container by running: - -```console -$ sudo ua enable cis -``` - -After running it, you should see an output similar to this one: - -``` -One moment, checking your subscription first -Updating package lists -Installing CIS Audit packages -CIS Audit enabled -Visit https://security-certs.docs.ubuntu.com/en/cis to learn how to use CIS -``` - -You can confirm that `cis` is enabled now by running: - -```console -$ ua status -``` - -And you should see: -``` -SERVICE ENTITLED STATUS DESCRIPTION -cis yes enabled Center for Internet Security Audit Tools -esm-infra yes enabled Expanded Security Maintenance for Infrastructure -fips yes n/a NIST-certified core packages -fips-updates yes n/a NIST-certified core packages with priority security updates -livepatch yes n/a Canonical Livepatch service -``` - -You can see now that `cis` is marked as `enabled` on status. - - -### Disable - -Let's suppose that you don't want a service anymore, you can also disable any service offering -through UA. For example, let's disable the `cis` service you just enabled by running on the LXD -container: - -```console -$ sudo ua disable cis -``` - -After running that command, let's now run `ua status` to see what happened to `cis`: -``` -SERVICE ENTITLED STATUS DESCRIPTION -cis yes disabled Center for Internet Security Audit Tools -esm-infra yes enabled Expanded Security Maintenance for Infrastructure -fips yes n/a NIST-certified core packages -fips-updates yes n/a NIST-certified core packages with priority security updates -livepatch yes n/a Canonical Livepatch service -``` - -You can see that `cis` status is back to disabled. - -> Note: the disable command doesn't uninstall any package that was installed by -the service. The command only removes the access you have to the service, but it -doesn't undo any configuration that was applied on the machine. - -### Detach - -Finally, what if you don't want this machine to be attached to a UA subscription any longer ? -You can achieve that using the `detach` command: - -```console -$ sudo ua detach -``` - -This command will disable all of the UA services on the machine for you and -get rid of the subscription stored on your machine during attach. - -> Note: the detach command will also not uninstall any packages that were installed by -any service enabled through UA. - -### Final thoughts - -This tutorial has covered the 6 main commands of UA. If you need more advanced options to configure -the tool, please take a look in [How to guides](./docs/howtoguides). If that still doesn't cover -your needs, feel free to reach the UA team on `#ubuntu-server` on Libera IRC. - -Before you finish this tutorial, exit the container by running `CTRL-D` and delete it by running -this command on the machine: -```console -$ lxc delete --force dev-x -``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/create_a_fips_docker_image.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/create_a_fips_docker_image.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/create_a_fips_docker_image.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/create_a_fips_docker_image.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,29 +1,29 @@ # Create an Ubuntu FIPS Docker image -> Requires at least UA Client version 27.7 +> Requires at least Ubuntu Pro Client version 27.7 -## Step 1: Acquire your Ubuntu Advantage (UA) token +## Step 1: Acquire your Ubuntu Pro token -Your UA token can be found on your Ubuntu Advantage dashboard. To access your dashboard, you need an [Ubuntu One](https://login.ubuntu.com/) account. If you purchased a UA subscription and don't yet have an Ubuntu One account, be sure to use the same email address used to purchase your subscription. If you haven't purchased a UA subscription, don't worry! You get a free token for personal use with your Ubuntu One account, no purchase necessary. +Your Ubuntu Pro token can be found on your Ubuntu Pro dashboard. To access your dashboard, you need an [Ubuntu One](https://login.ubuntu.com/) account. If you purchased an Ubuntu Pro subscription and don't yet have an Ubuntu One account, be sure to use the same email address used to purchase your subscription. If you haven't purchased an Ubuntu Pro subscription, don't worry! You get a free token for personal use with your Ubuntu One account, no purchase necessary. -The Ubuntu One account functions as a Single Sign On, so once logged in we can go straight to the Ubuntu Advantage dashboard at [ubuntu.com/advantage](https://ubuntu.com/advantage). Then we should see a list of our subscriptions (including the free for personal use subscription) in the left-hand column. Click on the subscription that you wish to use for this tutorial if it is not already selected. On the right we will now see the details of our subscription including our secret token under the "Subscription" header next to the "🔗" symbol. +The Ubuntu One account functions as a Single Sign On, so once logged in we can go straight to the Ubuntu Pro dashboard at [ubuntu.com/pro](https://ubuntu.com/pro). Then we should see a list of our subscriptions (including the free for personal use subscription) in the left-hand column. Click on the subscription that you wish to use for this tutorial if it is not already selected. On the right we will now see the details of our subscription including our secret token under the "Subscription" header next to the "🔗" symbol. > **Note** -> The UA token should be kept secret. It is used to uniquely identify your Ubuntu Advantage subscription. +> The Ubuntu Pro token should be kept secret. It is used to uniquely identify your Ubuntu Pro subscription. -## Step 2: Create a UA Attach Config file +## Step 2: Create an Ubuntu Pro Client Attach Config file First create a directory for this tutorial. ```bash -mkdir ua_fips_tutorial -cd ua_fips_tutorial +mkdir pro_fips_tutorial +cd pro_fips_tutorial ``` -Create a file named `ua-attach-config.yaml`. +Create a file named `pro-attach-config.yaml`. ```bash -touch ua-attach-config.yaml +touch pro-attach-config.yaml ``` Edit the file and add the following contents: @@ -34,7 +34,7 @@ - fips ``` -Replace `YOUR_TOKEN` with the UA token we got from [ubuntu.com/advantage](https://ubuntu.com/advantage) in Step 1. +Replace `YOUR_TOKEN` with the Ubuntu Pro token we got from [ubuntu.com/pro](https://ubuntu.com/pro) in Step 1. ## Step 3: Create a Dockerfile @@ -49,10 +49,10 @@ ```dockerfile FROM ubuntu:focal -RUN --mount=type=secret,id=ua-attach-config \ +RUN --mount=type=secret,id=pro-attach-config \ apt-get update \ && apt-get install --no-install-recommends -y ubuntu-advantage-tools ca-certificates \ - && ua attach --attach-config /run/secrets/ua-attach-config \ + && pro attach --attach-config /run/secrets/pro-attach-config \ && apt-get upgrade -y \ && apt-get install -y openssl libssl1.1 libssl1.1-hmac libgcrypt20 libgcrypt20-hmac strongswan strongswan-hmac openssh-client openssh-server \ @@ -61,17 +61,17 @@ && rm -rf /var/lib/apt/lists/* ``` -This Dockerfile will enable FIPS in the container, upgrade all packages and install the FIPS version of `openssl`. For more details on how this works, see [How to Enable UA Services in a Dockerfile](../howtoguides/enable_ua_in_dockerfile.md) +This Dockerfile will enable FIPS in the container, upgrade all packages and install the FIPS version of `openssl`. For more details on how this works, see [How to Enable Ubuntu Pro Services in a Dockerfile](../howtoguides/enable_in_dockerfile.md) ## Step 4: Build the Docker image Build the docker image with the following command: ```bash -DOCKER_BUILDKIT=1 docker build . --secret id=ua-attach-config,src=ua-attach-config.yaml -t ubuntu-bionic-fips +DOCKER_BUILDKIT=1 docker build . --secret id=pro-attach-config,src=pro-attach-config.yaml -t ubuntu-bionic-fips ``` -This will pass the attach-config as a [BuildKit Secret](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information) so that the finished docker image will not contain your UA token. +This will pass the attach-config as a [BuildKit Secret](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information) so that the finished docker image will not contain your Ubuntu Pro token. ## Step 5: Test the Docker image @@ -98,4 +98,4 @@ That's it! You could now push this image to a private registry and use it as the base of other docker images using `FROM`. -If you want to learn more about how the steps in this tutorial work, take a look at the more generic [How to Enable UA Services in a Dockerfile](../howtoguides/enable_ua_in_dockerfile.md). +If you want to learn more about how the steps in this tutorial work, take a look at the more generic [How to Enable Ubuntu Pro Services in a Dockerfile](../howtoguides/enable_in_dockerfile.md). diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/create_a_fips_updates_pro_cloud_image.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/create_a_fips_updates_pro_cloud_image.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/create_a_fips_updates_pro_cloud_image.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/create_a_fips_updates_pro_cloud_image.md 2022-11-22 13:06:26.000000000 +0000 @@ -12,12 +12,12 @@ First wait for the standard Ubuntu Pro services to be set up. ```bash -sudo ua status --wait +sudo pro status --wait ``` Then use [the enable command](../howtoguides/enable_fips.md) to setup FIPS Updates. ```bash -sudo ua enable fips-updates --assume-yes +sudo pro enable fips-updates --assume-yes ``` Now reboot the instance @@ -25,9 +25,9 @@ sudo reboot ``` -And verify that `fips-updates` is enabled in the output of `ua status` +And verify that `fips-updates` is enabled in the output of `pro status` ```bash -sudo ua status +sudo pro status ``` Also remove the machine-id so that it is regenerated for each instance launch from the snapshot. @@ -52,7 +52,7 @@ > This won't require a reboot and is only necessary to ensure the instance gets updates to fips packages when they become available. > > ```bash -> sudo ua enable fips-updates --assume-yes +> sudo pro enable fips-updates --assume-yes > ``` > > You can easily script this using [cloud-init user-data](https://cloudinit.readthedocs.io/en/latest/topics/modules.html#runcmd) at launch time @@ -60,6 +60,6 @@ > #cloud-config > # Enable fips-updates after pro auto-attach and reboot after cloud-init completes > runcmd: -> - 'ua status --wait' -> - 'ua enable fips-updates --assume-yes' +> - 'pro status --wait' +> - 'pro enable fips-updates --assume-yes' > ``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/fix_scenarios.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/fix_scenarios.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/fix_scenarios.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/fix_scenarios.md 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,374 @@ +# Using fix command to solve CVE/USNs on the machine + +The Ubuntu Pro Client (`pro`) can be used to inspect and resolve +[CVEs](https://ubuntu.com/security/cves) and [USNs](https://ubuntu.com/security/notices) (Ubuntu +Security Notices) on this machine. + +Every CVE/USN is fixed by trying to upgrade all of the affected packages described by the CVE or +USN. Sometimes, the packages fixes can only be applied if an Ubuntu Pro service is already enabled on the +machine. + +On this tutorial, we will cover the main scenarios that can happen when running the `pro fix` command. + +## Prerequisites + +On this tutorial, we will use [LXD](https://linuxcontainers.org/lxd/) containers. +To set up LXD on your computer, please follow this +[guide](https://linuxcontainers.org/lxd/getting-started-cli/). + +## Creating the Xenial LXD container + +To test the `pro fix` command, let's create a Xenial LXD container. Remember to set up LXD as +mentioned on the [Prerequisites](#prerequisites) section. After that, just run the command: + +```console +$ lxc launch ubuntu-daily:xenial dev-x +``` + +After running that, let's access the container by running: + +```console +$ lxc shell dev-x +``` + +Every time we say: "run the command" our intention will be for +you to run that command on your Xenial LXD container. + +## Using `pro fix` + +First, let's see what happens on your system when `pro fix` runs. We will choose +to fix a CVE that does not affect the Xenial container, +[CVE-2020-15180](https://ubuntu.com/security/CVE-2020-15180). This CVE address security +issues for the `MariaDB` package, which is not installed on the system. Let's confirm that +it doesn't affect the system by running this command: + +```console +$ pro fix CVE-2020-15180 +``` + +You should see an output like this one: + +``` +CVE-2020-15180: MariaDB vulnerabilities +https://ubuntu.com/security/CVE-2020-15180 +No affected source packages are installed. +✔ CVE-2020-15180 does not affect your system. +``` + +Every `pro fix` output will have a similar output structure where we describe the CVE/USN, +display the affected packages, fix the affected packages and at the end, show if the +CVE/USN is fully fixed in the machine. + +You can better see this on an `pro fix` call that does fix a package. Let's install a package +on the container that we know are associated with [CVE-2020-25686](https://ubuntu.com/security/CVE-2020-25686). +You can install that package by running these commands: + +```console +$ sudo apt update +$ sudo apt install dnsmasq=2.75-1 +``` + +Now you can run the following command: + +```console +$ sudo pro fix CVE-2020-25686 +``` + +You will then see the following output: + +``` +CVE-2020-25686: Dnsmasq vulnerabilities +https://ubuntu.com/security/CVE-2020-25686 +1 affected package is installed: dnsmasq +(1/1) dnsmasq: +A fix is available in Ubuntu standard updates. +{ apt update && apt install --only-upgrade -y dnsmasq } +✔ CVE-2020-25686 is resolved. +``` + +> **Note** +> We need to run the command with sudo because we are now installing a package on the system. + +Whenever `pro fix` has a package to upgrade, it follows a consistent structure and displays the +following in order + +1. The affected package +2. The availability of a fix +3. The location of the fix, if one is available +4. The command that will fix the issue + +Also, in the end of the output is the confirmation that the CVE was fixed by the command. +You can confirm that fix was successfully applied by running the same `pro fix` command again: + +``` +CVE-2020-25686: Dnsmasq vulnerabilities +https://ubuntu.com/security/CVE-2020-25686 +1 affected package is installed: dnsmasq +(1/1) dnsmasq: +A fix is available in Ubuntu standard updates. +The update is already installed. +✔ CVE-2020-25686 is resolved. +``` + +## CVE/USN without released fix + +Some CVE/USNs do not have a fix released yet. When that happens, `pro fix` will let you know +about this situation. Before we reproduce that scenario, you will first install a package by running: + +```console +$ sudo apt install -y libawl-php +``` + +Now, you can confirm that scenario by running the following command: + +```console +$ pro fix USN-4539-1 +``` + +You will see the following output: + +``` +USN-4539-1: AWL vulnerability +Found CVEs: +https://ubuntu.com/security/CVE-2020-11728 +1 affected source package is installed: awl +(1/1) awl: +Sorry, no fix is available. +1 package is still affected: awl +✘ USN-4539-1 is not resolved. +``` + +Notice that we inform that the is no fix available and in the last line the commands also +mentions that the USN is not resolved. + +## CVE/USN that require an Ubuntu Pro subscription + +Some package fixes can only be installed when the machine is attached to an Ubuntu Pro subscription. +When that happens, `pro fix` will let you know about that. To see an example of this scenario, +you can run the following fix command: + + +```console +$ sudo pro fix USN-5079-2 +``` + +You will see that the command will prompt you like this: + +``` +USN-5079-2: curl vulnerabilities +Found CVEs: +https://ubuntu.com/security/CVE-2021-22946 +https://ubuntu.com/security/CVE-2021-22947 +1 affected package is installed: curl +(1/1) curl: +A fix is available in Ubuntu Pro: ESM Infra. +The update is not installed because this system is not attached to a +subscription. + +Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel +> +``` + +You can see that the prompt is asking for an Ubuntu Pro subscription token. Any user with +a Ubuntu One account is entitled to a free personal token to use with Ubuntu Pro. +If you choose the `Subscribe` option on the prompt, the command will ask you to go +to the [Ubuntu Pro portal](https://ubuntu.com/pro/). You can go into that portal +and get yourself a free subscription token by logging in with your +SSO credentials, the same credentials you use for https://login.ubuntu.com. + +After getting your Ubuntu Pro token, you can hit `Enter` on the prompt and it will ask you +to provide the token you just obtained. After entering the token you are expected to +see now the following output: + +``` +USN-5079-2: curl vulnerabilities +Found CVEs: +https://ubuntu.com/security/CVE-2021-22946 +https://ubuntu.com/security/CVE-2021-22947 +1 affected package is installed: curl +(1/1) curl: +A fix is available in Ubuntu Pro: ESM Infra. +The update is not installed because this system is not attached to a +subscription. + +Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel +>S +Open a browser to: https://ubuntu.com/pro +Hit [Enter] when subscription is complete. +Enter your token (from https://ubuntu.com/pro) to attach this system: +> TOKEN +{ pro attach TOKEN } +Enabling default service esm-infra +Updating package lists +Ubuntu Pro: ESM Infra enabled +This machine is now attached to 'SUBSCRIPTION' + +SERVICE ENTITLED STATUS DESCRIPTION +cis yes disabled Center for Internet Security Audit Tools +esm-infra yes enabled Expanded Security Maintenance for Infrastructure +fips yes n/a NIST-certified core packages +fips-updates yes n/a NIST-certified core packages with priority security updates +livepatch yes n/a Canonical Livepatch service + +NOTICES +Operation in progress: pro attach + +Enable services with: pro enable + + Account: Ubuntu Pro Client Test + Subscription: SUBSCRIPTION + Valid until: 9999-12-31 00:00:00+00:00 +Technical support level: essential +{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls } +✔ USN-5079-2 is resolved. +``` + +We can see that that the attach command was successful, which can be verified by +the status output we see when executing the command. Additionally, we can also observe +that the USN is indeed fixed, which you can confirm by running the command again: + +``` +N-5079-2: curl vulnerabilities +Found CVEs: +https://ubuntu.com/security/CVE-2021-22946 +https://ubuntu.com/security/CVE-2021-22947 +1 affected package is installed: curl +(1/1) curl: +A fix is available in Ubuntu Pro: ESM Infra. +The update is already installed. +✔ USN-5079-2 is resolved. +``` + +> **Note** +> Even though we are not covering this scenario here, if you have an expired contract, +> `pro fix` will detect that and prompt you to attach a new token for your machine. + +## CVE/USN that requires an Ubuntu Pro service + +Now, let's assume that you have attached to an Ubuntu Pro subscription, but when running `pro fix`, +the required service that fixes the issue is not enabled. In that situation, `pro fix` will +also prompt you to enable that service. + +To confirm that, run the following command to disable `esm-infra`: + +```console +$ sudo pro disable esm-infra +``` + +Now, you can run the following command: + +```console +$ sudo pro fix CVE-2021-44731 +``` + +And you should see the following output (if you type `E` when prompted): + +``` +CVE-2021-44731: snapd vulnerabilities +https://ubuntu.com/security/CVE-2021-44731 +1 affected package is installed: snapd +(1/1) snapd: +A fix is available in Ubuntu Pro: ESM Infra. +The update is not installed because this system does not have +esm-infra enabled. + +Choose: [E]nable esm-infra [C]ancel +> E +{ pro enable esm-infra } +One moment, checking your subscription first +Updating package lists +Ubuntu Pro: ESM Infra enabled +{ apt update && apt install --only-upgrade -y ubuntu-core-launcher snapd } +✔ CVE-2021-44731 is resolved. +``` + +You can observe that the required service was enabled and `pro fix` was able to successfully upgrade +the affected package. + +## CVE/USN that requires reboot + +When running an `pro fix` command, sometimes we can install a package that requires +a system reboot to complete. The `pro fix` command can detect that and will inform you +about it. + +You can confirm this by running the following fix command: + +```console +$ sudo pro fix CVE-2022-0778 +``` + +Then you will see the following output: + +``` +VE-2022-0778: OpenSSL vulnerability +https://ubuntu.com/security/CVE-2022-0778 +1 affected package is installed: openssl +(1/1) openssl: +A fix is available in Ubuntu Pro: ESM Infra. +{ apt update && apt install --only-upgrade -y libssl1.0.0 openssl } +A reboot is required to complete fix operation. +✘ CVE-2022-0778 is not resolved. +``` + +If we reboot the machine and run the command again, you will see that it is indeed fixed: + +``` +CVE-2022-0778: OpenSSL vulnerability +https://ubuntu.com/security/CVE-2022-0778 +1 affected package is installed: openssl +(1/1) openssl: +A fix is available in Ubuntu Pro: ESM Infra. +The update is already installed. +✔ CVE-2022-0778 is resolved. +``` + +## Partially resolved CVE/USNs + +Finally, you might run a `pro fix` command that only partially fixes some of the packages affected. +This happens when only a subset of the packages have available updates to fix for that CVE/USN. +In this case, `pro fix` will inform of which package it can and cannot fix. + +But first, let's install some package so we can run `pro fix` to exercise that scenario. + +```console +$ sudo apt-get install expat=2.1.0-7 swish-e matanza ghostscript +``` + +Now, you can run the following command: + +```console +$ sudo pro fix CVE-2017-9233 +``` + +And you will see the following command: + +``` +CVE-2017-9233: Expat vulnerability +https://ubuntu.com/security/CVE-2017-9233 +3 affected packages are installed: expat, matanza, swish-e +(1/3, 2/3) matanza, swish-e: +Sorry, no fix is available. +(3/3) expat: +A fix is available in Ubuntu standard updates. +{ apt update && apt install --only-upgrade -y expat } +2 packages are still affected: matanza, swish-e +✘ CVE-2017-9233 is not resolved. +``` + +We can see that two packages, `matanza` and `swish-e`, don't have any fixes available, but there +is one for `expat`. In that scenario, we install the fix for `expat` and report at the end that +some packages are still affected. Also, observe that in this scenario we mark the CVE/USN as not +resolved. + +### Final thoughts + +This tutorial has covered the main scenario that can happen to you when running `pro fix`. +If you need more information about the command please feel free to reach the Ubuntu Pro Client team on +`#ubuntu-server` on Libera IRC. + +Before you finish this tutorial, exit the container by running `CTRL-D` and delete it by running +this command on the host machine: + +```console +$ lxc delete --force dev-x +``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/ua_fix_scenarios.md ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/ua_fix_scenarios.md --- ubuntu-advantage-tools-27.11.3~22.04.1/docs/tutorials/ua_fix_scenarios.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs/tutorials/ua_fix_scenarios.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,374 +0,0 @@ -# Using fix command to solve CVE/USNs on the machine - -The Ubuntu Advantage Tools (UA) program can be used to inspect and resolve -[CVEs](https://ubuntu.com/security/cves) and [USNs](https://ubuntu.com/security/notices) (Ubuntu -Security Notices) on this machine. - -Every CVE/USN is fixed by trying to upgrade all of the affected packages described by the CVE or -USN. Sometimes, the packages fixes can only be applied if an UA service is already enabled on the -machine. - -On this tutorial, we will cover the main scenarios that can happen when running the UA fix command. - -## Prerequisites - -On this tutorial, we will use [LXD](https://linuxcontainers.org/lxd/) containers. -To set up LXD on your computer, please follow this -[guide](https://linuxcontainers.org/lxd/getting-started-cli/). - -## Creating the Xenial LXD container - -To test the `ua fix` command, let's create a Xenial LXD container. Remember to set up LXD as -mentioned on the [Prerequisites](#prerequisites) section. After that, just run the command: - -```console -$ lxc launch ubuntu-daily:xenial dev-x -``` - -After running that, let's access the container by running: - -```console -$ lxc shell dev-x -``` - -Every time we say: "run the command" our intention will be for -you to run that command on your Xenial LXD container. - -## Using UA fix - -First, let's see what happens on your system when `ua fix` runs. We will choose -to fix a CVE that does not affect the Xenial container, -[CVE-2020-15180](https://ubuntu.com/security/CVE-2020-15180). This CVE address security -issues for the `MariaDB` package, which is not installed on the system. Let's confirm that -it doesn't affect the system by running this command: - -```console -$ ua fix CVE-2020-15180 -``` - -You should see an output like this one: - -``` -CVE-2020-15180: MariaDB vulnerabilities -https://ubuntu.com/security/CVE-2020-15180 -No affected source packages are installed. -✔ CVE-2020-15180 does not affect your system. -``` - -Every `ua fix` output will have a similar output structure where we describe the CVE/USN, -display the affected packages, fix the affected packages and at the end, show if the -CVE/USN is fully fixed in the machine. - -You can better see this on an `ua fix` call that does fix a package. Let's install a package -on the container that we know are associated with [CVE-2020-25686](https://ubuntu.com/security/CVE-2020-25686). -You can install that package by running these commands: - -```console -$ sudo apt update -$ sudo apt install dnsmasq=2.75-1 -``` - -Now you can run the following command: - -```console -$ sudo ua fix CVE-2020-25686 -``` - -You will then see the following output: - -``` -CVE-2020-25686: Dnsmasq vulnerabilities -https://ubuntu.com/security/CVE-2020-25686 -1 affected package is installed: dnsmasq -(1/1) dnsmasq: -A fix is available in Ubuntu standard updates. -{ apt update && apt install --only-upgrade -y dnsmasq } -✔ CVE-2020-25686 is resolved. -``` - -> **Note** -> We need to run the command with sudo because we are now installing a package on the system. - -Whenever `ua fix` has a package to upgrade, it follows a consistent structure and displays the -following in order - -1. The affected package -2. The availability of a fix -3. The location of the fix, if one is available -4. The command that will fix the issue - -Also, in the end of the output is the confirmation that the CVE was fixed by the command. -You can confirm that fix was successfully applied by running the same `ua fix` command again: - -``` -CVE-2020-25686: Dnsmasq vulnerabilities -https://ubuntu.com/security/CVE-2020-25686 -1 affected package is installed: dnsmasq -(1/1) dnsmasq: -A fix is available in Ubuntu standard updates. -The update is already installed. -✔ CVE-2020-25686 is resolved. -``` - -## CVE/USN without released fix - -Some CVE/USNs do not have a fix released yet. When that happens, `ua fix` will let you know -about this situation. Before we reproduce that scenario, you will first install a package by running: - -```console -$ sudo apt install -y libawl-php -``` - -Now, you can confirm that scenario by running the following command: - -```console -$ ua fix USN-4539-1 -``` - -You will see the following output: - -``` -USN-4539-1: AWL vulnerability -Found CVEs: -https://ubuntu.com/security/CVE-2020-11728 -1 affected source package is installed: awl -(1/1) awl: -Sorry, no fix is available. -1 package is still affected: awl -✘ USN-4539-1 is not resolved. -``` - -Notice that we inform that the is no fix available and in the last line the commands also -mentions that the USN is not resolved. - -## CVE/USN that require an UA subscription - -Some package fixes can only be installed when the machine is attached to an UA subscription. -When that happens, `ua fix` will let you know about that. To see an example of this scenario, -you can run the following fix command: - - -```console -$ sudo ua fix USN-5079-2 -``` - -You will see that the command will prompt you like this: - -``` -USN-5079-2: curl vulnerabilities -Found CVEs: -https://ubuntu.com/security/CVE-2021-22946 -https://ubuntu.com/security/CVE-2021-22947 -1 affected package is installed: curl -(1/1) curl: -A fix is available in Ubuntu Pro: ESM Infra. -The update is not installed because this system is not attached to a -subscription. - -Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel -> -``` - -You can see that the prompt is asking for a UA subscription token. Any user with -a Ubuntu One account is entitled to a free personal token to use with UA. -If you choose the `Subscribe` option on the prompt, the command will ask you to go -to the [advantage](https://ubuntu.com/advantage/) portal. You can go into that portal -and get yourself a free subscription token by logging in with your -SSO credentials, the same credentials you use for https://login.ubuntu.com. - -After getting your UA token, you can hit `Enter` on the prompt and it will ask you -to provide the token you just obtained. After entering the token you are expected to -see now the following output: - -``` -USN-5079-2: curl vulnerabilities -Found CVEs: -https://ubuntu.com/security/CVE-2021-22946 -https://ubuntu.com/security/CVE-2021-22947 -1 affected package is installed: curl -(1/1) curl: -A fix is available in Ubuntu Pro: ESM Infra. -The update is not installed because this system is not attached to a -subscription. - -Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel ->S -Open a browser to: https://ubuntu.com/advantage -Hit [Enter] when subscription is complete. -Enter your token (from https://ubuntu.com/advantage) to attach this system: -> TOKEN -{ ua attach TOKEN } -Enabling default service esm-infra -Updating package lists -Ubuntu Pro: ESM Infra enabled -This machine is now attached to 'SUBSCRIPTION' - -SERVICE ENTITLED STATUS DESCRIPTION -cis yes disabled Center for Internet Security Audit Tools -esm-infra yes enabled Expanded Security Maintenance for Infrastructure -fips yes n/a NIST-certified core packages -fips-updates yes n/a NIST-certified core packages with priority security updates -livepatch yes n/a Canonical Livepatch service - -NOTICES -Operation in progress: ua attach - -Enable services with: ua enable - - Account: UA Client Test - Subscription: SUBSCRIPTION - Valid until: 9999-12-31 00:00:00+00:00 -Technical support level: essential -{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls } -✔ USN-5079-2 is resolved. -``` - -We can see that that the attach command was successful, which can be verified by -the status output we see when executing the command. Additionally, we can also observe -that the USN is indeed fixed, which you can confirm by running the command again: - -``` -N-5079-2: curl vulnerabilities -Found CVEs: -https://ubuntu.com/security/CVE-2021-22946 -https://ubuntu.com/security/CVE-2021-22947 -1 affected package is installed: curl -(1/1) curl: -A fix is available in Ubuntu Pro: ESM Infra. -The update is already installed. -✔ USN-5079-2 is resolved. -``` - -> **Note** -> Even though we are not covering this scenario here, if you have an expired contract, -> `ua fix` will detect that and prompt you to attach a new token for your machine. - -## CVE/USN that requires an UA service - -Now, let's assume that you have attached to an UA subscription, but when running `ua fix`, -the required service that fixes the issue is not enabled. In that situation, `ua fix` will -also prompt you to enable that service. - -To confirm that, run the following command to disable `esm-infra`: - -```console -$ sudo ua disable esm-infra -``` - -Now, you can run the following command: - -```console -$ sudo ua fix CVE-2021-44731 -``` - -And you should see the following output (if you type `E` when prompted): - -``` -CVE-2021-44731: snapd vulnerabilities -https://ubuntu.com/security/CVE-2021-44731 -1 affected package is installed: snapd -(1/1) snapd: -A fix is available in Ubuntu Pro: ESM Infra. -The update is not installed because this system does not have -esm-infra enabled. - -Choose: [E]nable esm-infra [C]ancel -> E -{ ua enable esm-infra } -One moment, checking your subscription first -Updating package lists -Ubuntu Pro: ESM Infra enabled -{ apt update && apt install --only-upgrade -y ubuntu-core-launcher snapd } -✔ CVE-2021-44731 is resolved. -``` - -You can observe that the required service was enabled and `ua fix` was able to successfully upgrade -the affected package. - -## CVE/USN that requires reboot - -When running an `ua fix` command, sometimes we can install a package that requires -a system reboot to complete. The `ua fix` command can detect that and will inform you -about it. - -You can confirm this by running the following fix command: - -```console -$ sudo ua fix CVE-2022-0778 -``` - -Then you will see the following output: - -``` -VE-2022-0778: OpenSSL vulnerability -https://ubuntu.com/security/CVE-2022-0778 -1 affected package is installed: openssl -(1/1) openssl: -A fix is available in Ubuntu Pro: ESM Infra. -{ apt update && apt install --only-upgrade -y libssl1.0.0 openssl } -A reboot is required to complete fix operation. -✘ CVE-2022-0778 is not resolved. -``` - -If we reboot the machine and run the command again, you will see that it is indeed fixed: - -``` -CVE-2022-0778: OpenSSL vulnerability -https://ubuntu.com/security/CVE-2022-0778 -1 affected package is installed: openssl -(1/1) openssl: -A fix is available in Ubuntu Pro: ESM Infra. -The update is already installed. -✔ CVE-2022-0778 is resolved. -``` - -## Partially resolved CVE/USNs - -Finally, you might run a `ua fix` command that only partially fixes some of the packages affected. -This happens when only a subset of the packages have available updates to fix for that CVE/USN. -In this case, `ua fix` will inform of which package it can and cannot fix. - -But first, let's install some package so we can run `ua fix` to exercise that scenario. - -```console -$ sudo apt-get install expat=2.1.0-7 swish-e matanza ghostscript -``` - -Now, you can run the following command: - -```console -$ sudo ua fix CVE-2017-9233 -``` - -And you will see the following command: - -``` -CVE-2017-9233: Expat vulnerability -https://ubuntu.com/security/CVE-2017-9233 -3 affected packages are installed: expat, matanza, swish-e -(1/3, 2/3) matanza, swish-e: -Sorry, no fix is available. -(3/3) expat: -A fix is available in Ubuntu standard updates. -{ apt update && apt install --only-upgrade -y expat } -2 packages are still affected: matanza, swish-e -✘ CVE-2017-9233 is not resolved. -``` - -We can see that two packages, `matanza` and `swish-e`, don't have any fixes available, but there -is one for `expat`. In that scenario, we install the fix for `expat` and report at the end that -some packages are still affected. Also, observe that in this scenario we mark the CVE/USN as not -resolved. - -### Final thoughts - -This tutorial has covered the main scenario that can happen to you when running `ua fix`. -If you need more information about the command please feel free to reach the UA team on -`#ubuntu-server` on Libera IRC. - -Before you finish this tutorial, exit the container by running `CTRL-D` and delete it by running -this command on the host machine: - -```console -$ lxc delete --force dev-x -``` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/docs-requirements.txt ubuntu-advantage-tools-27.12~22.04.1/docs-requirements.txt --- ubuntu-advantage-tools-27.11.3~22.04.1/docs-requirements.txt 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/docs-requirements.txt 2022-11-22 13:06:26.000000000 +0000 @@ -1,4 +1,5 @@ sphinx==5.1.1 m2r2 myst-parser -sphinx_rtd_theme +sphinx_rtd_theme>=1.0.0 +docutils<0.17 diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/airgapped.feature ubuntu-advantage-tools-27.12~22.04.1/features/airgapped.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/airgapped.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/airgapped.feature 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,53 @@ +@uses.config.contract_token +Feature: Performing attach using ua-airgapped + + @series.jammy + @uses.config.machine_type.lxd.container + Scenario Outline: Attached enable Common Criteria service in an ubuntu lxd container + Given a `` machine with ubuntu-advantage-tools installed + # set up the apt mirror configuration + When I launch a `jammy` `mirror` machine + And I run `add-apt-repository ppa:yellow/ua-airgapped -y` `with sudo` on the `mirror` machine + And I run `apt-get update` `with sudo` on the `mirror` machine + And I run `apt-get install apt-mirror get-resource-tokens ua-airgapped -yq` `with sudo` on the `mirror` machine + And I download the service credentials on the `mirror` machine + And I extract the `esm-infra` credentials from the `mirror` machine + And I extract the `esm-apps` credentials from the `mirror` machine + And I set the apt-mirror file for `` with the `esm-infra,esm-apps` credentials on the `mirror` machine + And I run `apt-mirror` `with sudo` on the `mirror` machine + And I serve the `esm-infra` mirror using port `8000` on the `mirror` machine + And I serve the `esm-apps` mirror using port `9000` on the `mirror` machine + # set up the ua-airgapped configuration + And I create the contract config overrides file for `esm-infra,esm-apps` on the `mirror` machine + And I generate the contracts-airgapped configuration on the `mirror` machine + # set up the contracts-airgapped configuration + When I launch a `jammy` `contracts` machine + And I run `add-apt-repository ppa:yellow/ua-airgapped -y` `with sudo` on the `contracts` machine + And I run `apt-get update` `with sudo` on the `contracts` machine + And I run `apt-get install contracts-airgapped -yq` `with sudo` on the `contracts` machine + And I run `apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4067E40313CB4B13` `with sudo` on the `contracts` machine + And I disable any internet connection on the `contracts` machine + And I send the contracts-airgapped config from the `mirror` machine to the `contracts` machine + And I start the contracts-airgapped service on the `contracts` machine + # attach an airgapped machine to the contracts-airgapped server + And I disable any internet connection on the machine + And I change config key `contract_url` to use value `contracts:ip-address:8484` + And I attach `contract_token` with sudo + Then stdout matches regexp: + """ + esm-apps +yes +enabled .* + esm-infra +yes +enabled .* + """ + When I run `apt-cache policy hello` with sudo + Then stdout matches regexp: + """ + 500 .*:9000/ubuntu jammy-apps-security/main + """ + And stdout matches regexp: + """ + 500 .*:8000/ubuntu jammy-infra-security/main + """ + + Examples: ubuntu release + | release | + | jammy | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/api_configure_retry_service.feature ubuntu-advantage-tools-27.12~22.04.1/features/api_configure_retry_service.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/api_configure_retry_service.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/api_configure_retry_service.feature 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,59 @@ +Feature: api.u.pro.attach.auto.configure_retry_service + + @series.lts + @uses.config.machine_type.lxd.container + Scenario Outline: v1 successfully triggers retry service when run during startup + Given a `` machine with ubuntu-advantage-tools installed + When I change contract to staging with sudo + When I create the file `/lib/systemd/system/apitest.service` with the following + """ + [Unit] + Description=test + Before=cloud-config.service + After=cloud-config.target + + [Service] + Type=oneshot + ExecStart=/usr/bin/pro api u.pro.attach.auto.configure_retry_service.v1 + + [Install] + WantedBy=cloud-config.service multi-user.target + """ + When I run `systemctl enable apitest.service` with sudo + When I reboot the machine + Then I verify that running `systemctl status ubuntu-advantage.service` `with sudo` exits `0` + Then stdout matches regexp: + """ + Active: active \(running\) + """ + Then stdout matches regexp: + """ + mode: retry auto attach + """ + Then stdout does not match regexp: + """ + mode: poll for pro license + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services 1 time\(s\). + The failure was due to: an unknown error. + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services 1 time\(s\). + The failure was due to: an unknown error. + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/api_packages.feature ubuntu-advantage-tools-27.12~22.04.1/features/api_packages.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/api_packages.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/api_packages.feature 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,36 @@ +Feature: Package related API endpoints + + @series.all + @uses.config.machine_type.lxd.container + @uses.config.contract_token + Scenario Outline: Call packages API endpoints to see information in a Ubuntu machine + Given a `` machine with ubuntu-advantage-tools installed + When I run `pro api u.pro.packages.summary.v1` as non-root + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"summary": {"num_esm_apps_packages": \d+, "num_esm_infra_packages": \d+, "num_installed_packages": \d+, "num_main_packages": \d+, "num_multiverse_packages": \d+, "num_restricted_packages": \d+, "num_third_party_packages": \d+, "num_universe_packages": \d+, "num_unknown_packages": \d+}}, "meta": {"environment_vars": \[\]}, "type": "PackageSummary"}, "errors": \[\], "result": "success", "version": ".+", "warnings": \[\]} + """ + When I run `pro api u.pro.packages.updates.v1` as non-root + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"summary": {"num_esm_apps_updates": \d+, "num_esm_infra_updates": \d+, "num_standard_security_updates": \d+, "num_standard_updates": \d+, "num_updates": \d+}, "updates": \[.*\]}, "meta": {"environment_vars": \[\]}, "type": "PackageUpdates"}, "errors": \[\], "result": "success", "version": ".+", "warnings": \[\]} + """ + # Make sure we have an updated system + When I attach `contract_token` with sudo + And I run `apt upgrade -y` with sudo + # Install some outdated package + And I run `apt install = -y --allow-downgrades` with sudo + # See the update there + When I run `pro api u.pro.packages.updates.v1` as non-root + Then stdout matches regexp: + """ + {"download_size": \d+, "origin": ".+", "package": "", "provided_by": "", "status": "upgrade_available", "version": ""} + """ + + Examples: ubuntu release + | release | package | outdated_version | candidate_version | provided_by | + | xenial | libcurl3-gnutls | 7.47.0-1ubuntu2 | 7.47.0-1ubuntu2.19\+esm6 | esm-infra | + | bionic | libcurl4 | 7.58.0-2ubuntu3 | 7.58.0-2ubuntu3.21 | standard-security | + | focal | libcurl4 | 7.68.0-1ubuntu2 | 7.68.0-1ubuntu2.14 | standard-security | + | jammy | libcurl4 | 7.81.0-1 | 7.81.0-1ubuntu1.6 | standard-security | + | kinetic | libcurl4 | 7.85.0-1 | 7.85.0-1ubuntu0.1 | standard-security | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/api_security.feature ubuntu-advantage-tools-27.12~22.04.1/features/api_security.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/api_security.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/api_security.feature 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,71 @@ +Feature: API security/security status tests + + @series.xenial + @uses.config.machine_type.lxd.vm + @uses.config.contract_token + Scenario: Call Livepatched CVEs endpoint + Given a `xenial` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + And I run `pro api u.pro.security.status.livepatch_cves.v1` as non-root + Then stdout matches regexp: + """ + {"name": "cve-2013-1798", "patched": true} + """ + And stdout matches regexp: + """ + "type": "LivepatchCVEs" + """ + + @series.lts + @uses.config.machine_type.lxd.container + @uses.config.contract_token + Scenario Outline: Call package manifest endpoint for machine + Given a `` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + And I run `pro status` as non-root + Then stdout matches regexp: + """ + esm-infra +yes +enabled +Expanded Security Maintenance for Infrastructure + """ + When I run `apt update` with sudo + And I run `apt upgrade -y` with sudo + And I run `apt install jq bzip2 libopenscap8 -y` with sudo + And I run shell command `pro api u.security.package_manifest.v1 | jq -r '.data.attributes.manifest_data' > manifest` as non-root + And I run shell command `wget https://security-metadata.canonical.com/oval/oci.com.ubuntu..usn.oval.xml.bz2` as non-root + And I run `bunzip2 oci.com.ubuntu..usn.oval.xml.bz2` as non-root + And I run shell command `oscap oval eval --report report.html oci.com.ubuntu..usn.oval.xml` as non-root + Then stdout matches regexp: + """ + oval:com.ubuntu.:def::\s+false + """ + # Trigger CVE https://ubuntu.com/security/CVE-2018-10846 with ID 39991000000 in OVAL data ( == Xenial $ Bionic) + # Trigger CVE https://ubuntu.com/security/CVE-2022-2509 with ID 55501000000 in OVAL data ( > Xenial) + When I run shell command `sed -i -E 's/libgnutls30:amd64\s+.*/libgnutls30:amd64 /' manifest` as non-root + And I run shell command `oscap oval eval --report report.html oci.com.ubuntu..usn.oval.xml` as non-root + Then stdout matches regexp: + """ + oval:com.ubuntu.:def::\s+true + """ + # Update the manifest + When I run shell command `pro api u.security.package_manifest.v1 | jq -r '.data.attributes.manifest_data' > manifest` as non-root + And I run shell command `oscap oval eval --report report.html oci.com.ubuntu..usn.oval.xml` as non-root + Then stdout matches regexp: + """ + oval:com.ubuntu.:def::\s+false + """ + # Downgrade the package + When I run shell command `apt install libgnutls30= -y --allow-downgrades` with sudo + And I run shell command `pro api u.security.package_manifest.v1 | jq -r '.data.attributes.manifest_data' > manifest` as non-root + And I run shell command `oscap oval eval --report report.html oci.com.ubuntu..usn.oval.xml` as non-root + Then stdout matches regexp: + """ + oval:com.ubuntu.:def::\s+true + """ + + + Examples: ubuntu release + | release | base_version | CVE_ID | + | xenial | 3.4.10-4ubuntu1 | 39991000000 | + | bionic | 3.5.18-1ubuntu1 | 55501000000 | + | focal | 3.6.13-2ubuntu1 | 55501000000 | + | jammy | 3.7.3-4ubuntu1 | 55501000000 | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/apt_messages.feature ubuntu-advantage-tools-27.12~22.04.1/features/apt_messages.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/apt_messages.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/apt_messages.feature 2022-11-22 13:06:26.000000000 +0000 @@ -138,7 +138,21 @@ # The following packages will be upgraded: """ - When I update contract to use `effectiveTo` as `days=+2` + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ + And I append the following on uaclient config: + """ + features: + machine_token_overlay: "/tmp/machine-token-overlay.json" + """ When I run `pro refresh messages` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: @@ -159,7 +173,16 @@ The following packages will be upgraded: """ - When I update contract to use `effectiveTo` as `days=-3` + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ When I run `pro refresh messages` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: @@ -181,7 +204,16 @@ The following packages will be upgraded: """ - When I update contract to use `effectiveTo` as `days=-20` + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ When I run `pro refresh messages` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: @@ -292,23 +324,37 @@ The following packages will be upgraded: hello """ -# When I update contract to use `effectiveTo` as `days=-20` -# When I run `pro refresh messages` with sudo -# When I run `apt upgrade --dry-run` with sudo -# Then stdout matches regexp: -# """ -# Reading package lists... -# Building dependency tree... -# Reading state information... -# Calculating upgrade... -# -# \*Your Ubuntu Pro subscription has EXPIRED\* -# The following security updates require Ubuntu Pro with 'esm-apps' enabled: -# hello -# Renew your service at https:\/\/ubuntu.com\/pro -# -# The following packages will be upgraded: -# """ + # When I create the file `/tmp/machine-token-overlay.json` with the following: + # """ + # { + # "machineTokenInfo": { + # "contractInfo": { + # "effectiveTo": + # } + # } + # } + # """ + # And I append the following on uaclient config: + # """ + # features: + # machine_token_overlay: "/tmp/machine-token-overlay.json" + # """ + # When I run `pro refresh messages` with sudo + # When I run `apt-get upgrade --dry-run` with sudo + # Then stdout matches regexp: + # """ + # Reading package lists... + # Building dependency tree... + # Reading state information... + # Calculating upgrade... + + # \*Your Ubuntu Pro subscription has EXPIRED\* + # The following security updates require Ubuntu Pro with 'esm-apps' enabled: + # hello + # Renew your service at https:\/\/ubuntu.com\/pro + + # The following packages will be upgraded: + # """ When I run `apt-get upgrade -y` with sudo When I run `pro detach --assume-yes` with sudo When I run `pro refresh messages` with sudo diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_commands.feature ubuntu-advantage-tools-27.12~22.04.1/features/attached_commands.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_commands.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/attached_commands.feature 2022-11-22 13:06:26.000000000 +0000 @@ -248,7 +248,7 @@ fips + +NIST-certified core packages fips-updates + +NIST-certified core packages with priority security updates livepatch +(yes|no) +Canonical Livepatch service - realtime-kernel + +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel + +Ubuntu kernel with PREEMPT_RT patches integrated ros + +Security Updates for the Robot Operating System ros-updates + +All Updates for the Robot Operating System """ @@ -304,10 +304,11 @@ """ This command must be run as root \(try using sudo\). """ - When I verify that running `pro auto-attach` `with sudo` exits `0` + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to 'UA Client Test' + To use a different subscription first run: sudo pro detach. """ Examples: ubuntu release @@ -394,10 +395,10 @@ other: False """ When I run `pro detach --assume-yes` with sudo - When I run `pro auto-attach` with sudo - Then stdout matches regexp: + Then I verify that running `pro auto-attach` `with sudo` exits `1` + Then stderr matches regexp: """ - Skipping auto-attach. Config disable_auto_attach is set. + features.disable_auto_attach set in config """ Examples: ubuntu release @@ -584,7 +585,7 @@ \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - - realtime-kernel: Beta-version Ubuntu Kernel with PREEMPT_RT patches + - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated \(https://ubuntu.com/realtime-kernel\) - ros-updates: All Updates for the Robot Operating System \(https://ubuntu.com/robotics/ros-esm\) @@ -648,6 +649,8 @@ \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) + - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated + \(https://ubuntu.com/realtime-kernel\) - usg: Security compliance and audit tools \(https://ubuntu.com/security/certifications/docs/usg\) """ @@ -665,6 +668,8 @@ \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) + - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated + \(https://ubuntu.com/realtime-kernel\) - usg: Security compliance and audit tools \(https://ubuntu.com/security/certifications/docs/usg\) """ @@ -684,7 +689,7 @@ \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - - realtime-kernel: Beta-version Ubuntu Kernel with PREEMPT_RT patches + - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated \(https://ubuntu.com/realtime-kernel\) - ros-updates: All Updates for the Robot Operating System \(https://ubuntu.com/robotics/ros-esm\) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_enable.feature ubuntu-advantage-tools-27.12~22.04.1/features/attached_enable.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_enable.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/attached_enable.feature 2022-11-22 13:06:26.000000000 +0000 @@ -200,11 +200,11 @@ """ Examples: ubuntu release - | release | valid_services | - | xenial | cc-eal, cis, esm-infra, fips, fips-updates, livepatch. | - | bionic | cc-eal, cis, esm-infra, fips, fips-updates, livepatch. | - | focal | cc-eal, esm-infra, fips, fips-updates, livepatch, usg. | - | jammy | cc-eal, esm-infra, fips, fips-updates, livepatch, usg. | + | release | valid_services | + | xenial | cc-eal, cis, esm-infra, fips, fips-updates, livepatch, realtime-kernel. | + | bionic | cc-eal, cis, esm-infra, fips, fips-updates, livepatch, realtime-kernel. | + | focal | cc-eal, esm-infra, fips, fips-updates, livepatch, realtime-kernel, usg. | + | jammy | cc-eal, esm-infra, fips, fips-updates, livepatch, realtime-kernel, usg. | @series.lts @uses.config.machine_type.lxd.container @@ -224,7 +224,7 @@ And stderr matches regexp: """ Cannot enable unknown service 'foobar'. - Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch. + Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch, realtime-kernel. """ And I verify that running `pro enable ros foobar` `with sudo` exits `1` And I will see the following on stdout: @@ -234,7 +234,7 @@ And stderr matches regexp: """ Cannot enable unknown service 'foobar, ros'. - Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch. + Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch, realtime-kernel. """ And I verify that running `pro enable esm-infra` `with sudo` exits `1` And I will see the following on stdout: @@ -269,36 +269,36 @@ When I attach `contract_token` with sudo Then I verify that running `pro enable foobar` `as non-root` exits `1` And I will see the following on stderr: - """ - This command must be run as root (try using sudo). - """ + """ + This command must be run as root (try using sudo). + """ And I verify that running `pro enable foobar` `with sudo` exits `1` And I will see the following on stdout: - """ - One moment, checking your subscription first - """ + """ + One moment, checking your subscription first + """ And stderr matches regexp: """ Cannot enable unknown service 'foobar'. - Try cc-eal, esm-infra, fips, fips-updates, livepatch, usg. + Try cc-eal, esm-infra, fips, fips-updates, livepatch, realtime-kernel, usg. """ And I verify that running `pro enable ros foobar` `with sudo` exits `1` And I will see the following on stdout: - """ - One moment, checking your subscription first - """ + """ + One moment, checking your subscription first + """ And stderr matches regexp: """ Cannot enable unknown service 'foobar, ros'. - Try cc-eal, esm-infra, fips, fips-updates, livepatch, usg. + Try cc-eal, esm-infra, fips, fips-updates, livepatch, realtime-kernel, usg. """ And I verify that running `pro enable esm-infra` `with sudo` exits `1` Then I will see the following on stdout: - """ - One moment, checking your subscription first - Ubuntu Pro: ESM Infra is already enabled. - See: sudo pro status - """ + """ + One moment, checking your subscription first + Ubuntu Pro: ESM Infra is already enabled. + See: sudo pro status + """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` """ @@ -561,7 +561,7 @@ Then I will see the following on stderr: """ Cannot enable unknown service 'usg'. - Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch. + Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch, realtime-kernel. """ Examples: cis service @@ -706,6 +706,75 @@ | focal | | jammy | + @series.xenial + @uses.config.machine_type.lxd.vm + Scenario Outline: Attached enable livepatch + Given a `` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + Then stdout matches regexp: + """ + Installing canonical-livepatch snap + Canonical livepatch enabled + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +enabled + """ + When I run `pro api u.pro.security.status.reboot_required.v1` with sudo + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"reboot_required": "no"}, "meta": {"environment_vars": \[\]}, "type": "RebootRequired"}, "errors": \[\], "result": "success", "version": ".*", "warnings": \[\]} + """ + When I run `pro system reboot-required` as non-root + Then I will see the following on stdout: + """ + no + """ + When I run `apt-get install libc6 -y` with sudo + And I run `pro api u.pro.security.status.reboot_required.v1` as non-root + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"reboot_required": "yes"}, "meta": {"environment_vars": \[\]}, "type": "RebootRequired"}, "errors": \[\], "result": "success", "version": ".*", "warnings": \[\]} + """ + When I run `pro system reboot-required` as non-root + Then I will see the following on stdout: + """ + yes + """ + When I reboot the machine + And I run `pro system reboot-required` as non-root + Then I will see the following on stdout: + """ + no + """ + When I run `apt-get install linux-image-generic -y` with sudo + And I run `pro api u.pro.security.status.reboot_required.v1` as non-root + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"reboot_required": "yes-kernel-livepatches-applied"}, "meta": {"environment_vars": \[\]}, "type": "RebootRequired"}, "errors": \[\], "result": "success", "version": ".*", "warnings": \[\]} + """ + When I run `pro system reboot-required` as non-root + Then I will see the following on stdout: + """ + yes-kernel-livepatches-applied + """ + When I run `apt-get install dbus -y` with sudo + And I run `pro api u.pro.security.status.reboot_required.v1` with sudo + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"reboot_required": "yes"}, "meta": {"environment_vars": \[\]}, "type": "RebootRequired"}, "errors": \[\], "result": "success", "version": ".*", "warnings": \[\]} + """ + When I run `pro system reboot-required` as non-root + Then I will see the following on stdout: + """ + yes + """ + + Examples: ubuntu release + | release | + | xenial | + @slow @series.bionic @uses.config.machine_type.lxd.vm diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_status.feature ubuntu-advantage-tools-27.12~22.04.1/features/attached_status.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/attached_status.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/attached_status.feature 2022-11-22 13:06:26.000000000 +0000 @@ -91,7 +91,7 @@ fips +yes +disabled +NIST-certified core packages fips-updates +yes +disabled +NIST-certified core packages with priority security updates livepatch +yes +n/a +Canonical Livepatch service - realtime-kernel +yes +n/a +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +n/a +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +disabled +Security Updates for the Robot Operating System ros-updates +yes +disabled +All Updates for the Robot Operating System @@ -132,7 +132,7 @@ fips +yes +disabled +NIST-certified core packages fips-updates +yes +disabled +NIST-certified core packages with priority security updates livepatch +yes +n/a +Canonical Livepatch service - realtime-kernel +yes +n/a +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +n/a +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +n/a +Security Updates for the Robot Operating System ros-updates +yes +n/a +All Updates for the Robot Operating System usg +yes +disabled +Security compliance and audit tools @@ -170,7 +170,7 @@ fips +yes +n/a +NIST-certified core packages fips-updates +yes +n/a +NIST-certified core packages with priority security updates livepatch +yes +n/a +Canonical Livepatch service - realtime-kernel +yes +n/a +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +n/a +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +n/a +Security Updates for the Robot Operating System ros-updates +yes +n/a +All Updates for the Robot Operating System usg +yes +n/a +Security compliance and audit tools diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/daemon.feature ubuntu-advantage-tools-27.12~22.04.1/features/daemon.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/daemon.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/daemon.feature 2022-11-22 13:06:26.000000000 +0000 @@ -244,7 +244,6 @@ """ Active: inactive \(dead\) \s*Condition: start condition failed.* - .*ConditionPathExists=/run/cloud-init/cloud-id-gce was not met """ Then I verify that running `cat /var/log/ubuntu-advantage-daemon.log` `with sudo` exits `1` When I attach `contract_token` with sudo @@ -255,7 +254,6 @@ """ Active: inactive \(dead\) \s*Condition: start condition failed.* - .*ConditionPathExists=/run/cloud-init/cloud-id-gce was not met """ Then I verify that running `cat /var/log/ubuntu-advantage-daemon.log` `with sudo` exits `1` Examples: version @@ -285,7 +283,6 @@ """ Active: inactive \(dead\) \s*Condition: start condition failed.* - .*ConditionPathExists=/run/cloud-init/cloud-id-gce was not met """ Then I verify that running `cat /var/log/ubuntu-advantage-daemon.log` `with sudo` exits `1` When I reboot the machine @@ -294,7 +291,6 @@ """ Active: inactive \(dead\) \s*Condition: start condition failed.* - .*ConditionPathExists=/run/cloud-init/cloud-id-gce was not met """ Then I verify that running `cat /var/log/ubuntu-advantage-daemon.log` `with sudo` exits `1` Examples: version diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/detached_auto_attach.feature ubuntu-advantage-tools-27.12~22.04.1/features/detached_auto_attach.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/detached_auto_attach.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/detached_auto_attach.feature 2022-11-22 13:06:26.000000000 +0000 @@ -15,10 +15,11 @@ Successfully refreshed your subscription. Successfully updated Ubuntu Pro related APT and MOTD messages. """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to 'UA Client Test' + To use a different subscription first run: sudo pro detach. """ When I run `pro status` with sudo Then stdout matches regexp: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/environment.py ubuntu-advantage-tools-27.12~22.04.1/features/environment.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/environment.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/environment.py 2022-11-22 13:06:26.000000000 +0000 @@ -150,26 +150,26 @@ def __init__( self, *, - cloud_credentials_path: str = None, + cloud_credentials_path: Optional[str] = None, image_clean: bool = True, destroy_instances: bool = True, ephemeral_instance: bool = False, snapshot_strategy: bool = False, machine_type: str = "lxd.container", - private_key_file: str = None, + private_key_file: Optional[str] = None, private_key_name: str = "uaclient-integration", - reuse_image: str = None, - contract_token: str = None, - contract_token_staging: str = None, - contract_token_staging_expired: str = None, - debs_path: str = None, - artifact_dir: str = None, + reuse_image: Optional[str] = None, + contract_token: Optional[str] = None, + contract_token_staging: Optional[str] = None, + contract_token_staging_expired: Optional[str] = None, + debs_path: Optional[str] = None, + artifact_dir: Optional[str] = None, install_from: InstallationSource = InstallationSource.DAILY, - custom_ppa: str = None, - custom_ppa_keyid: str = None, - userdata_file: str = None, - check_version: str = None, - sbuild_chroot: str = None, + custom_ppa: Optional[str] = None, + custom_ppa_keyid: Optional[str] = None, + userdata_file: Optional[str] = None, + check_version: Optional[str] = None, + sbuild_chroot: Optional[str] = None, cmdline_tags: List = [] ) -> None: # First, store the values we've detected @@ -252,6 +252,7 @@ tag=timed_job_tag, timestamp_suffix=False, ) + self.cloud = "aws" elif "azure" in self.machine_type: self.cloud_manager = cloud.Azure( machine_type=self.machine_type, @@ -259,6 +260,7 @@ tag=timed_job_tag, timestamp_suffix=False, ) + self.cloud = "azure" elif "gcp" in self.machine_type: self.cloud_manager = cloud.GCP( machine_type=self.machine_type, @@ -266,16 +268,19 @@ tag=timed_job_tag, timestamp_suffix=False, ) + self.cloud = "gcp" elif "lxd.vm" in self.machine_type: self.cloud_manager = cloud.LXDVirtualMachine( machine_type=self.machine_type, cloud_credentials_path=self.cloud_credentials_path, ) + self.cloud = "lxd.vm" else: self.cloud_manager = cloud.LXDContainer( machine_type=self.machine_type, cloud_credentials_path=self.cloud_credentials_path, ) + self.cloud = "lxd" self.cloud_api = self.cloud_manager.api @@ -331,8 +336,18 @@ def before_all(context: Context) -> None: """behave will invoke this before anything else happens.""" context.config.setup_logging() - logging.getLogger("botocore").setLevel(logging.INFO) - logging.getLogger("boto3").setLevel(logging.INFO) + if logging.getLogger().level == logging.DEBUG: + # The AWS boto libraries are very very very verbose + # We pretty much never want their debug level messages, + # but we do want to be able to use the debug log level + # in our code. So we bump their loggers up to info + # when we set debug at the cli with --logging-level=debug + logging.warn( + "Setting AWS botocore and boto3 loggers to INFO to avoid" + " extra verbose logs" + ) + logging.getLogger("botocore").setLevel(logging.INFO) + logging.getLogger("boto3").setLevel(logging.INFO) userdata = context.config.userdata if userdata: logging.debug("Userdata key / value pairs:") @@ -465,6 +480,8 @@ "/etc/ubuntu-advantage/uaclient.log", "/var/log/cloud-init.log", "/var/log/ubuntu-advantage.log", + "/var/log/ubuntu-advantage-daemon.log", + "/var/log/ubuntu-advantage-timer.log", "/var/lib/cloud/instance/user-data.txt", "/var/lib/cloud/instance/vendor-data.txt", ) @@ -472,7 +489,7 @@ "ua-version": ["pro", "version"], "cloud-init-analyze": ["cloud-init", "analyze", "show"], "cloud-init.status": ["cloud-init", "status", "--long"], - "status.json": ["pro", "status", "--all", "--format=json"], + "status.yaml": ["pro", "status", "--all", "--format=yaml"], "journal.log": ["journalctl", "-b", "0"], "systemd-analyze-blame": ["systemd-analyze", "blame"], "systemctl-status": ["systemctl", "status"], @@ -486,6 +503,11 @@ "status", "ua-reboot-cmds.service", ], + "systemctl-status-ubuntu-advantage": [ + "systemctl", + "status", + "ubuntu-advantage.service", + ], } @@ -514,7 +536,7 @@ ) ) - if hasattr(context, "instance"): + if hasattr(context, "instances"): if not os.path.exists(artifacts_dir): os.makedirs(artifacts_dir) for log_file in FAILURE_FILES: @@ -553,6 +575,16 @@ else: context.config.cloud_api.delete_image(image) + if context.config.destroy_instances: + try: + key_pair = context.config.cloud_manager.api.key_pair + os.remove(key_pair.private_key_path) + os.remove(key_pair.public_key_path) + except Exception as e: + logging.error( + "Failed to delete instance ssh keys:\n{}".format(str(e)) + ) + def capture_container_as_image( container_id: str, image_name: str, cloud_api: pycloudlib.cloud.BaseCloud @@ -611,8 +643,7 @@ if "pro" in context.config.machine_type: return deb_paths - # Redact ubuntu-advantage-pro deb as inapplicable - return [deb_path for deb_path in deb_paths if "pro" not in deb_path] + return deb_paths def create_instance_with_uat_installed( diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/motd_messages.feature ubuntu-advantage-tools-27.12~22.04.1/features/motd_messages.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/motd_messages.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/motd_messages.feature 2022-11-22 13:06:26.000000000 +0000 @@ -101,14 +101,14 @@ @series.xenial @series.bionic @uses.config.machine_type.lxd.container - Scenario Outline: MOTD Contract Expiration Notices + Scenario Outline: MOTD Contract Expiration Notices After Contract Update Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo When I attach `contract_token` with sudo When I update contract to use `effectiveTo` as `days=+2` When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: + Then stdout does not match regexp: """ [\w\d.]+ @@ -121,7 +121,7 @@ When I update contract to use `effectiveTo` as `days=-3` When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: + Then stdout does not match regexp: """ [\w\d.]+ @@ -135,6 +135,103 @@ When I update contract to use `effectiveTo` as `days=-20` When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo + Then stdout does not match regexp: + """ + [\w\d.]+ + + \*Your Ubuntu Pro subscription has EXPIRED\* + \d+ additional security update\(s\) require Ubuntu Pro with '' enabled. + Renew your service at https:\/\/ubuntu.com\/pro + + [\w\d.]+ + """ + When I run `apt-get upgrade -y` with sudo + When I run `pro refresh messages` with sudo + And I run `run-parts /etc/update-motd.d/` with sudo + Then stdout does not match regexp: + """ + [\w\d.]+ + + \*Your Ubuntu Pro subscription has EXPIRED\* + Renew your service at https:\/\/ubuntu.com\/pro + + [\w\d.]+ + """ + Examples: ubuntu release + | release | service | + | xenial | esm-infra | + | bionic | esm-apps | + + + @series.xenial + @series.bionic + @uses.config.machine_type.lxd.container + Scenario Outline: MOTD Contract Expiration Notices with contract not updated + Given a `` machine with ubuntu-advantage-tools installed + When I run `apt-get update` with sudo + When I attach `contract_token` with sudo + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ + And I append the following on uaclient config: + """ + features: + machine_token_overlay: "/tmp/machine-token-overlay.json" + """ + When I run `pro refresh messages` with sudo + And I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + [\w\d.]+ + + CAUTION: Your Ubuntu Pro subscription will expire in 2 days. + Renew your subscription at https:\/\/ubuntu.com\/pro to ensure continued security + coverage for your applications. + + [\w\d.]+ + """ + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ + When I run `pro refresh messages` with sudo + And I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + [\w\d.]+ + + CAUTION: Your Ubuntu Pro subscription expired on \d+ \w+ \d+. + Renew your subscription at https:\/\/ubuntu.com\/pro to ensure continued security + coverage for your applications. + Your grace period will expire in 11 days. + + [\w\d.]+ + """ + When I create the file `/tmp/machine-token-overlay.json` with the following: + """ + { + "machineTokenInfo": { + "contractInfo": { + "effectiveTo": + } + } + } + """ + When I run `pro refresh messages` with sudo + And I run `run-parts /etc/update-motd.d/` with sudo Then stdout matches regexp: """ [\w\d.]+ diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/proxy_config.feature ubuntu-advantage-tools-27.12~22.04.1/features/proxy_config.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/proxy_config.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/proxy_config.feature 2022-11-22 13:06:26.000000000 +0000 @@ -1183,3 +1183,34 @@ | bionic | | focal | | jammy | + + @slow + @series.jammy + @uses.config.machine_type.lxd.vm + Scenario: Enable realtime kernel through proxy on a machine with no internet + Given a `jammy` machine with ubuntu-advantage-tools installed + When I disable any internet connection on the machine + And I launch a `focal` `proxy` machine + And I run `apt install squid -y` `with sudo` on the `proxy` machine + And I add this text on `/etc/squid/squid.conf` on `proxy` above `http_access deny all`: + """ + dns_v4_first on\nacl all src 0.0.0.0\/0\nhttp_access allow all + """ + And I run `systemctl restart squid.service` `with sudo` on the `proxy` machine + And I run `pro config set https_proxy=http://:3128` with sudo + And I run `pro config set http_proxy=http://:3128` with sudo + And I run `pro config set global_apt_http_proxy=http://:3128` with sudo + And I run `pro config set global_apt_https_proxy=http://:3128` with sudo + And I attach `contract_token` with sudo + Then stdout matches regexp: + """ + esm-apps +yes +enabled +Expanded Security Maintenance for Applications + esm-infra +yes +enabled +Expanded Security Maintenance for Infrastructure + """ + When I run `pro enable realtime-kernel --beta` `with sudo` and stdin `y` + Then stdout matches regexp: + """ + Installing Real-Time Kernel packages + Real-Time Kernel enabled + A reboot is required to complete install. + """ diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/realtime_kernel.feature ubuntu-advantage-tools-27.12~22.04.1/features/realtime_kernel.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/realtime_kernel.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/realtime_kernel.feature 2022-11-22 13:06:26.000000000 +0000 @@ -53,24 +53,18 @@ """ This command must be run as root (try using sudo). """ - Then I verify that running `pro enable realtime-kernel` `with sudo` exits `1` - And stderr matches regexp: - """ - Cannot enable unknown service 'realtime-kernel'. - """ - When I run `pro enable realtime-kernel --beta` `with sudo` and stdin `y` + When I run `pro enable realtime-kernel` `with sudo` and stdin `y` Then stdout matches regexp: """ One moment, checking your subscription first - The real-time kernel is a beta version of the 22.04 Ubuntu kernel with the - PREEMPT_RT patchset integrated for x86_64 and ARM64. + The real-time kernel is an Ubuntu kernel with PREEMPT_RT patches integrated. - .*This will change your kernel. You will need to manually configure grub to - revert back to your original kernel after enabling real-time..* + .*This will change your kernel. To revert to your original kernel, you will need + to make the change manually..* Do you want to continue\? \[ default = Yes \]: \(Y/n\) Updating package lists - Installing Real-Time Kernel packages - Real-Time Kernel enabled + Installing Real-time kernel packages + Real-time kernel enabled A reboot is required to complete install. """ When I run `apt-cache policy ubuntu-realtime` as non-root @@ -82,11 +76,11 @@ """ \s* 500 https://esm.ubuntu.com/realtime/ubuntu /main amd64 Packages """ - When I verify that running `pro enable realtime-kernel --beta` `with sudo` exits `1` + When I verify that running `pro enable realtime-kernel` `with sudo` exits `1` Then stdout matches regexp """ One moment, checking your subscription first - Real-Time Kernel is already enabled. + Real-time kernel is already enabled. See: sudo pro status """ When I reboot the machine @@ -98,8 +92,10 @@ When I run `pro disable realtime-kernel` `with sudo` and stdin `y` Then stdout matches regexp: """ - This will disable the Real-Time Kernel entitlement but the Real-Time Kernel will remain installed. + This will disable Ubuntu Pro updates to the real-time kernel on this machine. + The real-time kernel will remain installed. """ + Examples: ubuntu release | release | | jammy | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/retry_auto_attach.feature ubuntu-advantage-tools-27.12~22.04.1/features/retry_auto_attach.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/retry_auto_attach.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/retry_auto_attach.feature 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,364 @@ +Feature: auto-attach retries periodically on failures + + @series.lts + @uses.config.machine_type.aws.generic + @uses.config.machine_type.azure.generic + @uses.config.machine_type.gcp.generic + Scenario Outline: auto-attach retries for a month and updates status + Given a `` machine with ubuntu-advantage-tools installed + When I change contract to staging with sudo + When I install ubuntu-advantage-pro + When I reboot the machine + When I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `3` + Then stdout matches regexp: + """ + Active: failed + """ + Then stdout matches regexp: + """ + creating flag file to trigger retries + """ + Then I verify that running `systemctl status ubuntu-advantage.service` `with sudo` exits `0` + Then stdout matches regexp: + """ + Active: active \(running\) + """ + Then stdout matches regexp: + """ + mode: retry auto attach + """ + Then stdout does not match regexp: + """ + mode: poll for pro license + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services 1 time\(s\). + The failure was due to: Canonical servers did not recognize this machine as Ubuntu Pro: ".*". + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services 1 time\(s\). + The failure was due to: Canonical servers did not recognize this machine as Ubuntu Pro: ".*". + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + + # simulate a middle attempt with different reason + When I set `interval_index` = `10` in json file `/var/lib/ubuntu-advantage/retry-auto-attach-state.json` + When I set `failure_reason` = `"an unknown error"` in json file `/var/lib/ubuntu-advantage/retry-auto-attach-state.json` + When I run `systemctl restart ubuntu-advantage.service` with sudo + Then I verify that running `systemctl status ubuntu-advantage.service` `with sudo` exits `0` + Then stdout matches regexp: + """ + Active: active \(running\) + """ + Then stdout matches regexp: + """ + mode: retry auto attach + """ + Then stdout does not match regexp: + """ + mode: poll for pro license + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services 11 time\(s\). + The failure was due to: an unknown error. + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services 11 time\(s\). + The failure was due to: an unknown error. + The next attempt is scheduled for \d+-\d+-\d+T\d+:\d+:00.*. + You can try manually with `sudo pro auto-attach`. + """ + + # simulate all attempts failing + When I set `interval_index` = `18` in json file `/var/lib/ubuntu-advantage/retry-auto-attach-state.json` + When I run `systemctl restart ubuntu-advantage.service` with sudo + Then I verify that running `systemctl status ubuntu-advantage.service` `with sudo` exits `3` + Then stdout contains substring + """ + Active: inactive (dead) + """ + Then stdout matches regexp: + """ + mode: retry auto attach + """ + Then stdout does not match regexp: + """ + mode: poll for pro license + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services 19 times. + The most recent failure was due to: an unknown error. + Try re-launching the instance or report this issue by running `ubuntu-bug ubuntu-advantage-tools` + You can try manually with `sudo pro auto-attach`. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services 19 times. + The most recent failure was due to: an unknown error. + Try re-launching the instance or report this issue by running `ubuntu-bug ubuntu-advantage-tools` + You can try manually with `sudo pro auto-attach`. + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | + + + @series.lts + @uses.config.machine_type.aws.pro + @uses.config.machine_type.azure.pro + @uses.config.machine_type.gcp.pro + Scenario Outline: auto-attach retries stop if manual auto-attach succeeds + Given a `` machine with ubuntu-advantage-tools installed + When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + """ + contract_url: 'https://contracts.canonical.com' + data_dir: /var/lib/ubuntu-advantage + log_level: debug + log_file: /var/log/ubuntu-advantage.log + + """ + When I create the file `/var/lib/ubuntu-advantage/response-overlay.json` with the following: + """ + { + "https://contracts.canonical.com/v1/clouds//token": [{ + "type": "contract", + "code": 400, + "response": { + "message": "error" + } + }] + } + """ + And I append the following on uaclient config: + """ + features: + serviceclient_url_responses: "/var/lib/ubuntu-advantage/response-overlay.json" + """ + When I reboot the machine + When I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `3` + Then stdout matches regexp: + """ + Active: failed + """ + Then I verify that running `systemctl status ubuntu-advantage.service` `with sudo` exits `0` + Then stdout matches regexp: + """ + Active: active \(running\) + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services + """ + When I append the following on uaclient config: + """ + features: {} + """ + # The retry service waits 15 minutes before trying again, so this + # _should_ run and finish before the retry service has done anything + When I run `pro auto-attach` with sudo + When I verify that running `systemctl status ubuntu-advantage.service` `as non-root` exits `3` + Then stdout contains substring + """ + Active: inactive (dead) + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout does not match regexp: + """ + Failed to automatically attach to Ubuntu Pro services + """ + When I run `pro status` with sudo + Then stdout does not match regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | + + @series.lts + @uses.config.machine_type.gcp.pro + Scenario Outline: gcp auto-detect triggers retries on fail + Given a `` machine with ubuntu-advantage-tools installed + When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + """ + contract_url: 'https://contracts.canonical.com' + data_dir: /var/lib/ubuntu-advantage + log_level: debug + log_file: /var/log/ubuntu-advantage.log + + """ + When I create the file `/var/lib/ubuntu-advantage/response-overlay.json` with the following: + """ + { + "https://contracts.canonical.com/v1/clouds/gcp/token": [{ + "type": "contract", + "code": 400, + "response": { + "message": "error" + } + }] + } + """ + And I append the following on uaclient config: + """ + features: + serviceclient_url_responses: "/var/lib/ubuntu-advantage/response-overlay.json" + """ + When I run `systemctl start ubuntu-advantage.service` with sudo + When I wait `1` seconds + When I verify that running `systemctl status ubuntu-advantage.service` `as non-root` exits `0` + Then stdout contains substring + """ + Active: active (running) + """ + Then stdout matches regexp: + """ + mode: poll for pro license + """ + Then stdout matches regexp: + """ + creating flag file to trigger retries + """ + Then stdout matches regexp: + """ + mode: retry auto attach + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | + + + @series.lts + @uses.config.machine_type.aws.pro + @uses.config.machine_type.azure.pro + @uses.config.machine_type.gcp.pro + Scenario Outline: auto-attach retries eventually succeed and clean up + Given a `` machine with ubuntu-advantage-tools installed + # modify the wait time to be shorter so we don't have to wait 15m + When I replace `900, # 15m (T+15m)` in `/usr/lib/python3/dist-packages/uaclient/daemon/retry_auto_attach.py` with `60,` + When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + """ + contract_url: 'https://contracts.canonical.com' + data_dir: /var/lib/ubuntu-advantage + log_level: debug + log_file: /var/log/ubuntu-advantage.log + + """ + When I create the file `/var/lib/ubuntu-advantage/response-overlay.json` with the following: + """ + { + "https://contracts.canonical.com/v1/clouds//token": [{ + "type": "contract", + "code": 400, + "response": { + "message": "error" + } + }] + } + """ + And I append the following on uaclient config: + """ + features: + serviceclient_url_responses: "/var/lib/ubuntu-advantage/response-overlay.json" + """ + When I reboot the machine + When I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `3` + Then stdout matches regexp: + """ + Active: failed + """ + When I verify that running `systemctl status ubuntu-advantage.service` `as non-root` exits `0` + Then stdout matches regexp: + """ + Active: active \(running\) + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout matches regexp: + """ + Failed to automatically attach to Ubuntu Pro services + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services + """ + When I append the following on uaclient config: + """ + features: {} + """ + When I wait `60` seconds + When I run `ua status --wait --format yaml` with sudo + Then stdout contains substring + """ + attached: true + """ + When I verify that running `systemctl status ubuntu-advantage.service` `as non-root` exits `3` + Then stdout contains substring + """ + Active: inactive (dead) + """ + When I run `run-parts /etc/update-motd.d/` with sudo + Then stdout does not match regexp: + """ + Failed to automatically attach to Ubuntu Pro services + """ + When I run `pro status` with sudo + Then stdout does not match regexp: + """ + NOTICES + Failed to automatically attach to Ubuntu Pro services + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/schemas/ua_security_status.json ubuntu-advantage-tools-27.12~22.04.1/features/schemas/ua_security_status.json --- ubuntu-advantage-tools-27.11.3~22.04.1/features/schemas/ua_security_status.json 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/schemas/ua_security_status.json 2022-11-22 13:06:26.000000000 +0000 @@ -63,6 +63,9 @@ }, "num_standard_security_updates": { "type": "integer" + }, + "reboot_required": { + "type": "string" } } }, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/airgap.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/airgap.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/airgap.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/airgap.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,185 @@ +import os + +import yaml +from behave import when + +from features.steps.files import when_i_create_file_with_content +from features.steps.shell import when_i_run_command_on_machine + + +@when("I download the service credentials on the `{machine}` machine") +def download_service_credentials(context, machine): + token = context.config.contract_token + when_i_run_command_on_machine( + context, + "get-resource-tokens {}".format(token), + "as non-root", + "mirror", + ) + + context.service_credentials = context.process.stdout + + +@when("I extract the `{service}` credentials from the `{machine}` machine") +def extract_service_credentials(context, service, machine): + if getattr(context, "service_mirror_cfg", None) is None: + context.service_mirror_cfg = {} + + cmd = "sh -c 'echo \"{}\" | grep -A1 {} | grep -v {}'".format( + context.service_credentials, service, service + ) + when_i_run_command_on_machine( + context, + cmd, + "as non-root", + "mirror", + ) + + context.service_mirror_cfg[service.replace("-", "_")] = { + "credentials": context.process.stdout + } + + +@when( + "I set the apt-mirror file for `{release}` with the `{service_list}` credentials on the `{machine}` machine" # noqa +) +def set_apt_mirror_file_with_credentials( + context, release, service_list, machine +): + service_list = service_list.split(",") + + apt_mirror_file = """ + set nthreads 20 + set _tilde 0 + """ + + for service in service_list: + token = context.service_mirror_cfg[service.replace("-", "_")][ + "credentials" + ] + service_type = service.split("-")[1] + + apt_mirror_cfg = """ + deb https://bearer:{}@esm.ubuntu.com/{}/ubuntu/ jammy-{}-updates main + deb https://bearer:{}@esm.ubuntu.com/{}/ubuntu/ jammy-{}-security main + """.format( + token, + service_type, + service_type, + token, + service_type, + service_type, + ) + + apt_mirror_file += "\n" + apt_mirror_cfg + "\n" + + apt_mirror_file += "clean http://archive.ubuntu.com/ubuntu" + + context.text = apt_mirror_file.strip() + when_i_create_file_with_content( + context, + "/etc/apt/mirror.list", + machine=machine, + ) + + +@when( + "I serve the `{service}` mirror using port `{port}` on the `{machine}` machine" # noqa +) +def serve_apt_mirror(context, service, port, machine): + service_type = service.split("-")[1] + path = "/var/spool/apt-mirror/mirror/esm.ubuntu.com/{}/".format( + service_type + ) + cmd = "nohup sh -c 'python3 -m http.server --directory {} {} > /dev/null 2>&1 &'".format( # noqa + path, port + ) + + when_i_run_command_on_machine( + context, + cmd, + "with sudo", + "mirror", + ) + + context.service_mirror_cfg[service.replace("-", "_")]["port"] = port + + +@when( + "I create the contract config overrides file for `{service_list}` on the `{machine}` machine" # noqa +) +def create_contract_overrides(context, service_list, machine): + token = context.config.contract_token + config_override = {token: {}} + + for service in service_list.split(","): + config_override[token][service] = { + "directives": { + "aptURL": "http://{}:{}".format( + context.instances[machine].ip, + context.service_mirror_cfg[service.replace("-", "_")][ + "port" + ], + ) + } + } + + context.text = yaml.dump(config_override) + contract_override_path = "/tmp/contract-override" + when_i_create_file_with_content( + context, + contract_override_path, + machine, + ) + + context.service_mirror_cfg["contract_override"] = contract_override_path + + +@when( + "I generate the contracts-airgapped configuration on the `{machine}` machine" # noqa +) +def i_configure_the_ua_airgapped_service(context, machine): + contract_override_path = context.service_mirror_cfg["contract_override"] + contract_final_cfg_path = "contract-server-ready.yml" + cmd = "sh -c 'cat {} | ua-airgapped > {}'".format( + contract_override_path, + contract_final_cfg_path, + ) + + when_i_run_command_on_machine( + context, + cmd, + "with sudo", + machine, + ) + + context.service_mirror_cfg["contract_final_cfg"] = contract_final_cfg_path + + +@when( + "I send the contracts-airgapped config from the `{base_machine}` machine to the `{target_machine}` machine" # noqa +) +def i_fetch_contracts_airgapped_config(context, base_machine, target_machine): + local_file_path = "/tmp/contracts-airgapped-cfg" + + context.instances[base_machine].pull_file( + context.service_mirror_cfg["contract_final_cfg"], + local_file_path, + ) + + context.instances[target_machine].push_file( + local_file_path, + context.service_mirror_cfg["contract_final_cfg"], + ) + + os.unlink(local_file_path) + + +@when("I start the contracts-airgapped service on the `{machine}` machine") +def i_start_the_contracts_airgapped_service(context, machine): + path = context.service_mirror_cfg["contract_final_cfg"] + cmd = "nohup sh -c 'contracts-airgapped --input=./{} > /dev/null 2>&1 &'".format( # noqa + path + ) + + when_i_run_command_on_machine(context, cmd, "with sudo", machine) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/attach.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/attach.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,80 @@ +import logging +import time + +from behave import when + +from features.steps.contract import change_contract_endpoint_to_staging +from features.steps.shell import ( + then_i_verify_that_running_cmd_with_spec_exits_with_codes, + when_i_run_command, +) + +ERROR_CODE = "1" + + +@when("I attach `{token_type}` {user_spec} and options `{options}`") +def when_i_attach_staging_token_with_options( + context, token_type, user_spec, options +): + when_i_attach_staging_token( + context, token_type, user_spec, options=options + ) + + +@when("I attach `{token_type}` {user_spec}") +def when_i_attach_staging_token( + context, token_type, user_spec, verify_return=True, options="" +): + token = getattr(context.config, token_type) + if ( + token_type == "contract_token_staging" + or token_type == "contract_token_staging_expired" + ): + change_contract_endpoint_to_staging(context, user_spec) + cmd = "pro attach {} {}".format(token, options).strip() + when_i_run_command(context, cmd, user_spec, verify_return=False) + + if verify_return: + retries = [5, 5, 10] # Sleep times to wait between retries + while context.process.returncode != 0: + try: + time.sleep(retries.pop(0)) + except IndexError: # no more timeouts + logging.warning("Exhausted retries waiting for exit code: 0") + break + logging.info( + "--- Retrying on exit {exit_code}: {cmd}".format( + exit_code=context.process.returncode, cmd=cmd + ) + ) + when_i_run_command(context, cmd, user_spec, verify_return=False) + + +@when("I attempt to attach `{token_type}` {user_spec}") +def when_i_attempt_to_attach_staging_token(context, token_type, user_spec): + when_i_attach_staging_token( + context, token_type, user_spec, verify_return=False + ) + + +@when( + "I verify that running attach `{spec}` with json response exits `{exit_codes}`" # noqa +) +def when_i_verify_attach_with_json_response(context, spec, exit_codes): + cmd = "pro attach {} --format json".format(context.config.contract_token) + then_i_verify_that_running_cmd_with_spec_exits_with_codes( + context=context, cmd_name=cmd, spec=spec, exit_codes=exit_codes + ) + + +@when( + "I verify that running attach `{spec}` using expired token with json response fails" # noqa +) +def when_i_verify_attach_expired_token_with_json_response(context, spec): + change_contract_endpoint_to_staging(context, user_spec="with sudo") + cmd = "pro attach {} --format json".format( + context.config.contract_token_staging_expired + ) + then_i_verify_that_running_cmd_with_spec_exits_with_codes( + context=context, cmd_name=cmd, spec=spec, exit_codes=ERROR_CODE + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/contract.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/contract.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/contract.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/contract.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,122 @@ +import datetime + +from behave import then, when +from hamcrest import assert_that, equal_to, not_ + +from features.steps.shell import ( + when_i_run_command, + when_i_run_command_on_machine, +) +from uaclient.defaults import ( + DEFAULT_CONFIG_FILE, + DEFAULT_PRIVATE_MACHINE_TOKEN_PATH, +) + + +@when("I update contract to use `{contract_field}` as `{new_value}`") +def when_i_update_contract_field_to_new_value( + context, contract_field, new_value +): + if contract_field == "effectiveTo": + if "days=" in new_value: # Set timedelta offset from current day + now = datetime.datetime.utcnow() + contract_expiry = now + datetime.timedelta(days=int(new_value[5:])) + new_value = contract_expiry.strftime("%Y-%m-%dT00:00:00Z") + when_i_run_command( + context, + 'sed -i \'s/"{}": "[^"]*"/"{}": "{}"/g\' {}'.format( + contract_field, + contract_field, + new_value, + DEFAULT_PRIVATE_MACHINE_TOKEN_PATH, + ), + user_spec="with sudo", + ) + + +@when("I change contract to staging {user_spec}") +def change_contract_endpoint_to_staging(context, user_spec): + when_i_run_command( + context, + "sed -i 's/contracts.can/contracts.staging.can/' {}".format( + DEFAULT_CONFIG_FILE + ), + user_spec, + ) + + +def change_contract_endpoint_to_production(context, user_spec): + when_i_run_command( + context, + "sed -i 's/contracts.staging.can/contracts.can/' {}".format( + DEFAULT_CONFIG_FILE + ), + user_spec, + ) + + +@when("I save the `{key}` value from the contract") +def i_save_the_key_value_from_contract(context, key): + when_i_run_command( + context, + "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), + "with sudo", + ) + output = context.process.stdout.strip() + + if output: + if not hasattr(context, "saved_values"): + setattr(context, "saved_values", {}) + + context.saved_values[key] = output + + +def _get_saved_attr(context, key): + saved_value = getattr(context, "saved_values", {}).get(key) + + if saved_value is None: + raise AssertionError( + "Value for key {} was not previously saved\n".format(key) + ) + + return saved_value + + +@then( + "I verify that `{key}` value has been updated on the contract on the `{machine}` machine" # noqa: E501 +) +def i_verify_that_key_value_has_been_updated_on_machine(context, key, machine): + i_verify_that_key_value_has_been_updated(context, key, machine) + + +@then("I verify that `{key}` value has been updated on the contract") +def i_verify_that_key_value_has_been_updated(context, key, machine="uaclient"): + saved_value = _get_saved_attr(context, key) + when_i_run_command_on_machine( + context, + "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), + "with sudo", + instance_name=machine, + ) + assert_that(context.process.stdout.strip(), not_(equal_to(saved_value))) + + +@then("I verify that `{key}` value has not been updated on the contract") +def i_verify_that_key_value_has_not_been_updated(context, key): + saved_value = _get_saved_attr(context, key) + when_i_run_command( + context, + "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), + "with sudo", + ) + assert_that(context.process.stdout.strip(), equal_to(saved_value)) + + +@when("I restore the saved `{key}` value on contract") +def i_restore_the_saved_key_value_on_contract(context, key): + saved_value = _get_saved_attr(context, key) + when_i_update_contract_field_to_new_value( + context=context, + contract_field=key.split(".")[-1], + new_value=saved_value, + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/docker.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/docker.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/docker.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/docker.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,86 @@ +import logging + +from behave import then + +from features.steps.files import when_i_create_file_with_content +from features.steps.shell import when_i_run_command, when_i_run_shell_command + + +@then("`{file_name}` is not present in any docker image layer") +def file_is_not_present_in_any_docker_image_layer(context, file_name): + when_i_run_command( + context, + "find /var/lib/docker/overlay2 -name {}".format(file_name), + "with sudo", + ) + results = context.process.stdout.strip() + if results: + raise AssertionError( + 'found "{}"'.format(", ".join(results.split("\n"))) + ) + + +# This defines "not significantly larger" as "less than 2MB larger" +@then( + "docker image `{name}` is not significantly larger than `ubuntu:{series}` with `{package}` installed" # noqa: E501 +) +def docker_image_is_not_larger(context, name, series, package): + base_image_name = "ubuntu:{}".format(series) + base_upgraded_image_name = "{}-with-test-package".format(series) + + # We need to compare against the base image after apt upgrade + # and package install + dockerfile = """\ + FROM {} + RUN apt-get update \\ + && apt-get install -y {} \\ + && rm -rf /var/lib/apt/lists/* + """.format( + base_image_name, package + ) + context.text = dockerfile + when_i_create_file_with_content(context, "Dockerfile.base") + when_i_run_command( + context, + "docker build . -f Dockerfile.base -t {}".format( + base_upgraded_image_name + ), + "with sudo", + ) + + # find image sizes + when_i_run_shell_command( + context, "docker inspect {} | jq .[0].Size".format(name), "with sudo" + ) + custom_image_size = int(context.process.stdout.strip()) + when_i_run_shell_command( + context, + "docker inspect {} | jq .[0].Size".format(base_upgraded_image_name), + "with sudo", + ) + base_image_size = int(context.process.stdout.strip()) + + # Get pro test deb size + when_i_run_command(context, "du ubuntu-advantage-tools.deb", "with sudo") + # Example out: "1234\tubuntu-advantage-tools.deb" + ua_test_deb_size = ( + int(context.process.stdout.strip().split("\t")[0]) * 1024 + ) # KB -> B + + # Give us some space for bloat we don't control: 2MB -> B + extra_space = 2 * 1024 * 1024 + + if custom_image_size > (base_image_size + ua_test_deb_size + extra_space): + raise AssertionError( + "Custom image size ({}) is over 2MB greater than the base image" + " size ({}) + pro test deb size ({})".format( + custom_image_size, base_image_size, ua_test_deb_size + ) + ) + logging.debug( + "custom image size ({})\n" + "base image size ({})\n" + "pro test deb size ({})".format( + custom_image_size, base_image_size, ua_test_deb_size + ) + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/files.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/files.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/files.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,147 @@ +import datetime +import json +import os +import re +import tempfile + +from behave import then, when +from hamcrest import assert_that, matches_regexp + +from features.steps.shell import ( + when_i_run_command, + when_i_run_command_on_machine, +) +from uaclient.defaults import DEFAULT_CONFIG_FILE + + +@when("I add this text on `{file_name}` on `{instance_name}` above `{line}`") +def when_i_add_this_text_on_file_above_line( + context, file_name, instance_name, line +): + command = 'sed -i "s/{}/{}\\n{}/" {}'.format( + line, context.text, line, file_name + ) + when_i_run_command( + context, command, "with sudo", instance_name=instance_name + ) + + +@when("I verify `{file_name}` is empty on `{instance_name}` machine") +def when_i_verify_file_is_empty_on_machine(context, file_name, instance_name): + command = 'sh -c "cat {} | wc -l"'.format(file_name) + when_i_run_command( + context, command, user_spec="with sudo", instance_name=instance_name + ) + + assert_that(context.process.stdout.strip(), matches_regexp("0")) + + +@when("I create the file `{file_path}` with the following") +def when_i_create_file_with_content(context, file_path, machine="uaclient"): + text = context.text + if "" in text and "proxy" in context.instances: + text = text.replace("", context.instances["proxy"].ip) + if "" in text: + text = text.replace("", context.config.cloud) + + date_match = re.search(r".*)>", text) + if date_match: + day_offset = date_match.group("offset") + offset = 0 if day_offset == "" else int(day_offset) + now = datetime.datetime.utcnow() + contract_expiry = now + datetime.timedelta(days=offset) + new_value = '"' + contract_expiry.strftime("%Y-%m-%dT00:00:00Z") + '"' + orig_str_value = "" + text = text.replace(orig_str_value, new_value) + + with tempfile.TemporaryDirectory() as tmpd: + tmpf_path = os.path.join(tmpd, "tmpfile") + with open(tmpf_path, mode="w") as tmpf: + tmpf.write(text) + context.instances[machine].push_file(tmpf_path, "/tmp/behave_tmpfile") + + when_i_run_command_on_machine( + context, + "cp /tmp/behave_tmpfile {}".format(file_path), + "with sudo", + machine, + ) + + +@when("I delete the file `{file_path}`") +def when_i_delete_file(context, file_path): + cmd = "rm -rf {}".format(file_path) + cmd = 'sh -c "{}"'.format(cmd) + when_i_run_command(context, cmd, "with sudo") + + +@when("I replace `{original}` in `{filename}` with `{new}`") +def when_i_replace_string_in_file(context, original, filename, new): + new = new.replace("\\", r"\\") + new = new.replace("/", r"\/") + new = new.replace("&", r"\&") + when_i_run_command( + context, + "sed -i 's/{original}/{new}/' {filename}".format( + original=original, new=new, filename=filename + ), + "with sudo", + ) + + +@when("I replace `{original}` in `{filename}` with token `{token_name}`") +def when_i_replace_string_in_file_with_token( + context, original, filename, token_name +): + token = getattr(context.config, token_name) + when_i_replace_string_in_file(context, original, filename, token) + + +@then("I verify that files exist matching `{path_regex}`") +def there_should_be_files_matching_regex(context, path_regex): + when_i_run_command( + context, "ls {}".format(path_regex), "with sudo", verify_return=False + ) + if context.process.returncode != 0: + raise AssertionError("Missing expected files: {}".format(path_regex)) + + +@then("I verify that no files exist matching `{path_regex}`") +def there_should_be_no_files_matching_regex(context, path_regex): + when_i_run_command( + context, "ls {}".format(path_regex), "with sudo", verify_return=False + ) + if context.process.returncode == 0: + raise AssertionError( + "Unexpected files found: {}".format(context.process.stdout.strip()) + ) + + +@when("I change config key `{key}` to use value `{value}`") +def change_contract_key_to_use_value(context, key, value): + if "ip-address" in value: + machine, _, port = value.split(":") + ip_value = context.instances[machine].ip + value = "http:\/\/{}:{}".format(ip_value, port) # noqa: W605 + + when_i_run_command( + context, + "sed -i 's/{}: .*/{}: {}/g' {}".format( + key, key, value, DEFAULT_CONFIG_FILE + ), + "with sudo", + ) + + +@when("I set `{key}` = `{json_value}` in json file `{filename}`") +def when_i_set_key_val_json_file(context, key, json_value, filename): + when_i_run_command( + context, + "cat {}".format(filename), + "with sudo", + ) + val = json.loads(json_value) + content = json.loads(context.process.stdout) + content[key] = val + context.text = json.dumps(content) + when_i_create_file_with_content(context, filename) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/fix.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/fix.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/fix.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/fix.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,49 @@ +from behave import when + +from features.steps.contract import ( + change_contract_endpoint_to_production, + change_contract_endpoint_to_staging, +) +from features.steps.shell import when_i_run_command + + +@when("I fix `{issue}` by attaching to a subscription with `{token_type}`") +def when_i_fix_a_issue_by_attaching(context, issue, token_type): + token = getattr(context.config, token_type) + + if ( + token_type == "contract_token_staging" + or token_type == "contract_token_staging_expired" + ): + change_contract_endpoint_to_staging(context, user_spec="with sudo") + else: + change_contract_endpoint_to_production(context, user_spec="with sudo") + + when_i_run_command( + context=context, + command="pro fix {}".format(issue), + user_spec="with sudo", + stdin="a\n{}\n".format(token), + verify_return=False, + ) + + +@when("I fix `{issue}` by enabling required service") +def when_i_fix_a_issue_by_enabling_service(context, issue): + when_i_run_command( + context=context, + command="pro fix {}".format(issue), + user_spec="with sudo", + stdin="e\n", + ) + + +@when("I fix `{issue}` by updating expired token") +def when_i_fix_a_issue_by_updating_expired_token(context, issue): + token = getattr(context.config, "contract_token") + when_i_run_command( + context=context, + command="pro fix {}".format(issue), + user_spec="with sudo", + stdin="r\n{}\n".format(token), + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/machines.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/machines.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/machines.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/machines.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,178 @@ +import datetime +import logging +import os + +from behave import given, when + +from features.environment import ( + capture_container_as_image, + create_instance_with_uat_installed, +) +from features.util import cleanup_instance + +CONTAINER_PREFIX = "ubuntu-behave-test" +IMAGE_BUILD_PREFIX = "ubuntu-behave-image-build" +IMAGE_PREFIX = "ubuntu-behave-image" + + +def add_test_name_suffix(context, series, prefix): + pr_number = os.environ.get("UACLIENT_BEHAVE_JENKINS_CHANGE_ID") + pr_suffix = "-" + str(pr_number) if pr_number else "" + is_vm = bool(context.config.machine_type == "lxd.vm") + vm_suffix = "-vm" if is_vm else "" + time_suffix = datetime.datetime.now().strftime("-%s%f") + + return "{prefix}{pr_suffix}{vm_suffix}-{series}{time_suffix}".format( + prefix=prefix, + pr_suffix=pr_suffix, + vm_suffix=vm_suffix, + series=series, + time_suffix=time_suffix, + ) + + +@given("a `{series}` machine with ubuntu-advantage-tools installed") +def given_a_machine(context, series, custom_user_data=None): + if series in context.reuse_container: + context.instances = {} + context.container_name = context.reuse_container[series] + context.instances["uaclient"] = context.config.cloud_api.get_instance( + context.container_name + ) + if "pro" in context.config.machine_type: + context.instances[ + "uaclient" + ] = context.config.cloud_api.get_instance(context.container_name) + return + + instance_name = add_test_name_suffix(context, series, CONTAINER_PREFIX) + + if context.config.snapshot_strategy and not custom_user_data: + if series not in context.series_image_name: + build_container_name = add_test_name_suffix( + context, series, IMAGE_BUILD_PREFIX + ) + image_inst = create_instance_with_uat_installed( + context, series, build_container_name, custom_user_data + ) + + image_name = add_test_name_suffix(context, series, IMAGE_PREFIX) + image_inst_id = context.config.cloud_manager.get_instance_id( + image_inst + ) + image_id = capture_container_as_image( + image_inst_id, + image_name=image_name, + cloud_api=context.config.cloud_api, + ) + + context.series_image_name[series] = image_id + image_inst.delete(wait=False) + + inst = context.config.cloud_manager.launch( + series=series, + instance_name=instance_name, + image_name=context.series_image_name[series], + ephemeral=context.config.ephemeral_instance, + ) + else: + inst = create_instance_with_uat_installed( + context, series, instance_name, custom_user_data + ) + + context.series = series + context.instances = {"uaclient": inst} + + context.container_name = context.config.cloud_manager.get_instance_id( + context.instances["uaclient"] + ) + + context.add_cleanup(cleanup_instance(context, "uaclient")) + logging.info( + "--- instance ip: {}".format(context.instances["uaclient"].ip) + ) + + +@when("I take a snapshot of the machine") +def when_i_take_a_snapshot(context): + cloud = context.config.cloud_manager + inst = context.instances["uaclient"] + + snapshot = cloud.api.snapshot(inst) + + context.instance_snapshot = snapshot + + def cleanup_image() -> None: + try: + context.config.cloud_manager.api.delete_image( + context.instance_snapshot + ) + except RuntimeError as e: + logging.error( + "Failed to delete image: {}\n{}".format( + context.instance_snapshot, str(e) + ) + ) + + context.add_cleanup(cleanup_image) + + +@given( + "a `{series}` machine with ubuntu-advantage-tools installed adding this cloud-init user_data" # noqa +) +def given_a_machine_with_user_data(context, series): + custom_user_data = context.text + given_a_machine(context, series, custom_user_data) + + +@when( + "I launch a `{series}` `{instance_name}` machine with ingress ports `{ports}`" # noqa +) +def launch_machine_with_ingress_ports(context, series, instance_name, ports): + launch_machine( + context=context, + series=series, + instance_name=instance_name, + ports=ports, + ) + + +@when("I launch a `{series}` `{instance_name}` machine") +def launch_machine(context, series, instance_name, ports=None): + now = datetime.datetime.now() + date_prefix = now.strftime("-%s%f") + name = CONTAINER_PREFIX + series + date_prefix + "-" + instance_name + + kwargs = {"series": series, "instance_name": name} + if ports: + kwargs["inbound_ports"] = ports.split(",") + context.instances[instance_name] = context.config.cloud_manager.launch( + **kwargs + ) + + context.add_cleanup(cleanup_instance(context, instance_name)) + + +@when("I launch a `{instance_name}` machine from the snapshot") +def launch_machine_from_snapshot(context, instance_name): + now = datetime.datetime.now() + date_prefix = now.strftime("-%s%f") + name = CONTAINER_PREFIX + date_prefix + "-" + instance_name + + context.instances[instance_name] = context.config.cloud_manager.launch( + context.series, + instance_name=name, + image_name=context.instance_snapshot, + ) + + context.add_cleanup(cleanup_instance(context, instance_name)) + + +@when("I reboot the machine") +def when_i_reboot_the_machine(context): + context.instances["uaclient"].restart(wait=True) + + +@when("I reboot the `{machine}` machine") +def when_i_reboot_the_machine_name(context, machine): + context.instances[machine].restart(wait=True) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/magic_attach.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/magic_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/magic_attach.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/magic_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,43 @@ +import json + +from behave import when + +from features.steps.shell import when_i_run_command +from uaclient.util import DatetimeAwareJSONDecoder + + +@when("I initiate the magic attach flow") +def when_i_initiate_magic_attach(context): + when_i_run_command( + context=context, + command="pro api u.pro.attach.magic.initiate.v1", + user_spec="as non-root", + ) + + magic_attach_resp = json.loads( + context.process.stdout.strip(), cls=DatetimeAwareJSONDecoder + ) + + context.magic_token = magic_attach_resp["data"]["attributes"]["token"] + + +@when("I revoke the magic attach token") +def when_i_revoke_the_magic_attach_token(context): + when_i_run_command( + context=context, + command="pro api u.pro.attach.magic.revoke.v1 --args magic_token={}".format( # noqa + context.magic_token + ), + user_spec="as non-root", + ) + + +@when("I wait for the magic attach token to be activated") +def when_i_wait_for_magic_attach_token(context): + when_i_run_command( + context=context, + command="pro api u.pro.attach.magic.wait.v1 --args magic_token={}".format( # noqa + context.magic_token + ), + user_spec="as non-root", + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/misc.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/misc.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/misc.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/misc.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,40 @@ +import json +import time + +from behave import then, when +from hamcrest import assert_that, equal_to + +from features.steps.shell import when_i_run_command +from uaclient.defaults import DEFAULT_CONFIG_FILE +from uaclient.util import DatetimeAwareJSONDecoder + + +@when("I append the following on uaclient config") +def when_i_append_to_uaclient_config(context): + cmd = "printf '{}\n' > /tmp/uaclient.conf".format(context.text) + cmd = 'sh -c "{}"'.format(cmd) + when_i_run_command(context, cmd, "as non-root") + + cmd = "cat /tmp/uaclient.conf >> {}".format(DEFAULT_CONFIG_FILE) + cmd = 'sh -c "{}"'.format(cmd) + when_i_run_command(context, cmd, "with sudo") + + +@when("I wait `{seconds}` seconds") +def when_i_wait(context, seconds): + time.sleep(int(seconds)) + + +@then("I verify that the timer interval for `{job}` is `{interval}`") +def verify_timer_interval_for_job(context, job, interval): + when_i_run_command( + context, "cat /var/lib/ubuntu-advantage/jobs-status.json", "with sudo" + ) + jobs_status = json.loads( + context.process.stdout.strip(), cls=DatetimeAwareJSONDecoder + ) + last_run = jobs_status[job]["last_run"] + next_run = jobs_status[job]["next_run"] + run_diff = next_run - last_run + + assert_that(run_diff.seconds, equal_to(int(interval))) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/network.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/network.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/network.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/network.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,74 @@ +from behave import when + +from features.steps.shell import when_i_run_command + + +@when("I disable any internet connection on the machine") +def disable_internet_connection(context, instance_name="uaclient"): + when_i_run_command( + context, + "ufw default deny incoming", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw default deny outgoing", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow from 10.0.0.0/8", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow from 172.16.0.0/12", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow from 192.168.0.0/16", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow out to 10.0.0.0/8", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow out to 172.16.0.0/12", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, + "ufw allow out to 192.168.0.0/16", + "with sudo", + instance_name=instance_name, + ) + when_i_run_command( + context, "ufw allow ssh", "with sudo", instance_name=instance_name + ) + # We expect DNS to be working, but don't really want to set a server up... + when_i_run_command( + context, "ufw allow out 53", "with sudo", instance_name=instance_name + ) + when_i_run_command( + context, + "ufw enable", + "with sudo", + instance_name=instance_name, + stdin="y\n", + ) + + +@when("I disable any internet connection on the `{machine}` machine") +def disable_internet_connection_on_machine(context, machine): + disable_internet_connection(context, instance_name=machine) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/output.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/output.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/output.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/output.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,114 @@ +import json + +import jsonschema # type: ignore +import yaml +from behave import then, when +from hamcrest import ( + assert_that, + contains_string, + equal_to, + matches_regexp, + not_, +) + +from features.steps.shell import when_i_run_command +from features.util import SafeLoaderWithoutDatetime + + +@then("I will see the following on stdout") +def then_i_will_see_on_stdout(context): + assert_that(context.process.stdout.strip(), equal_to(context.text)) + + +@then("if `{value1}` in `{value2}` and stdout matches regexp") +def then_conditional_stdout_matches_regexp(context, value1, value2): + """Only apply regex assertion if value1 in value2.""" + if value1 in value2.split(" or "): + then_stream_matches_regexp(context, "stdout") + + +@then("if `{value1}` in `{value2}` and stdout does not match regexp") +def then_conditional_stdout_does_not_match_regexp(context, value1, value2): + """Only apply regex assertion if value1 in value2.""" + if value1 in value2.split(" or "): + then_stream_does_not_match_regexp(context, "stdout") + + +@then("{stream} does not match regexp") +def then_stream_does_not_match_regexp(context, stream): + content = getattr(context.process, stream).strip() + assert_that(content, not_(matches_regexp(context.text))) + + +@then("{stream} matches regexp") +def then_stream_matches_regexp(context, stream): + content = getattr(context.process, stream).strip() + text = context.text + if "" in text and "proxy" in context.instances: + text = text.replace("", context.instances["proxy"].ip) + assert_that(content, matches_regexp(text)) + + +@then("{stream} contains substring") +def then_stream_contains_substring(context, stream): + content = getattr(context.process, stream).strip() + assert_that(content, contains_string(context.text)) + + +@then("I will see the following on stderr") +def then_i_will_see_on_stderr(context): + assert_that(context.process.stderr.strip(), equal_to(context.text)) + + +@then("I will see the uaclient version on stdout") +def then_i_will_see_the_uaclient_version_on_stdout(context): + python_import = "from uaclient.version import get_version" + + cmd = "python3 -c '{}; print(get_version())'".format(python_import) + + actual_version = context.process.stdout.strip() + when_i_run_command(context, cmd, "as non-root") + expected_version = context.process.stdout.strip() + + assert_that(expected_version, equal_to(actual_version)) + + +@then("stdout is a {output_format} matching the `{schema}` schema") +def stdout_matches_the_json_schema(context, output_format, schema): + if output_format == "json": + instance = json.loads(context.process.stdout.strip()) + elif output_format == "yaml": + instance = yaml.load( + context.process.stdout.strip(), SafeLoaderWithoutDatetime + ) + with open("features/schemas/{}.json".format(schema), "r") as schema_file: + jsonschema.validate(instance=instance, schema=json.load(schema_file)) + + +@then("the {output_format} API response data matches the `{schema}` schema") +def api_response_matches_schema(context, output_format, schema): + if output_format == "json": + instance = json.loads(context.process.stdout.strip()) + elif output_format == "yaml": + instance = yaml.load( + context.process.stdout.strip(), SafeLoaderWithoutDatetime + ) + with open("features/schemas/{}.json".format(schema), "r") as schema_file: + jsonschema.validate( + instance=instance.get("data", {}).get("attributes"), + schema=json.load(schema_file), + ) + + +@when("I verify root and non-root `{cmd}` calls have the same output") +def root_vs_nonroot_cmd_comparison(context, cmd): + when_i_run_command(context, cmd, "with sudo") + root_status_stdout = context.process.stdout.strip() + root_status_stderr = context.process.stderr.strip() + + when_i_run_command(context, cmd, "as non-root") + nonroot_status_stdout = context.process.stdout.strip() + nonroot_status_stderr = context.process.stderr.strip() + + assert_that(root_status_stdout, nonroot_status_stdout) + assert root_status_stderr == nonroot_status_stderr diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/packages.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/packages.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/packages.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/packages.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,111 @@ +import re + +from behave import then, when +from hamcrest import assert_that, contains_string, matches_regexp + +from features.steps.shell import when_i_run_command + + +@then("apt-cache policy for the following url has permission `{perm_id}`") +def then_apt_cache_policy_for_the_following_url_has_permission_perm_id( + context, perm_id +): + full_url = "{} {}".format(perm_id, context.text) + assert_that(context.process.stdout.strip(), matches_regexp(full_url)) + + +@then("I verify that `{package}` installed version matches regexp `{regex}`") +def verify_installed_package_matches_version_regexp(context, package, regex): + when_i_run_command( + context, + "dpkg-query --showformat='${{Version}}' --show {}".format(package), + "as non-root", + ) + assert_that(context.process.stdout.strip(), matches_regexp(regex)) + + +@then( + "I verify that packages `{packages}` installed versions match regexp `{regex}`" # noqa: E501 +) +def verify_installed_packages_match_version_regexp(context, packages, regex): + for package in packages.split(" "): + verify_installed_package_matches_version_regexp( + context, package, regex + ) + + +@then("I verify that `{package}` is installed from apt source `{apt_source}`") +def verify_package_is_installed_from_apt_source(context, package, apt_source): + when_i_run_command( + context, "apt-cache policy {}".format(package), "as non-root" + ) + policy = context.process.stdout.strip() + RE_APT_SOURCE = r"\s+\d+\s+(?P.*)" + lines = policy.splitlines() + for index, line in enumerate(lines): + if re.match(r"\s+\*\*\*", line): # apt-policy installed prefix *** + # Next line is the apt repo from which deb is installed + installed_apt_source = re.match(RE_APT_SOURCE, lines[index + 1]) + if installed_apt_source is None: + raise RuntimeError( + "Unable to process apt-policy line {}".format( + lines[index + 1] + ) + ) + assert_that( + installed_apt_source.groupdict()["source"], + contains_string(apt_source), + ) + return + raise AssertionError( + "Package {package} is not installed".format(package=package) + ) + + +@then( + "I verify that `{packages}` are installed from apt source `{apt_source}`" +) +def verify_packages_are_installed_from_apt_source( + context, packages, apt_source +): + for package in packages.split(" "): + verify_package_is_installed_from_apt_source( + context, package, apt_source + ) + + +@when("I install third-party / unknown packages in the machine") +def when_i_install_packages(context): + # The `code` deb package sets up an apt remote for updates, + # and is then listed as third-party. + # https://code.visualstudio.com/download + + # The `gh` deb package is just installed locally, + # and is then listed as unknown + # https://github.com/cli/cli/releases + when_i_run_command( + context, + ( + "curl -L " + "https://az764295.vo.msecnd.net/stable/" + "e4503b30fc78200f846c62cf8091b76ff5547662/" + "code_1.70.2-1660629410_amd64.deb " + "-o /tmp/code.deb" + ), + "with sudo", + ) + when_i_run_command( + context, + ( + "curl -L " + "https://github.com/cli/cli/releases/download/" + "v2.14.4/gh_2.14.4_linux_amd64.deb " + "-o /tmp/gh.deb" + ), + "with sudo", + ) + when_i_run_command( + context, "apt-get install -y /tmp/code.deb", "with sudo" + ) + when_i_run_command(context, "apt-get install -y /tmp/gh.deb", "with sudo") + when_i_run_command(context, "apt-get update", "with sudo") diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/shell.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/shell.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/shell.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/shell.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,144 @@ +import logging +import shlex +import subprocess +import time + +from behave import then, when +from hamcrest import assert_that, equal_to + + +@when("I run `{command}` `{user_spec}` on the `{instance_name}` machine") +def when_i_run_command_on_machine(context, command, user_spec, instance_name): + when_i_run_command( + context, command, user_spec, instance_name=instance_name + ) + + +@when("I run `{command}` {user_spec}, retrying exit [{exit_codes}]") +def when_i_retry_run_command(context, command, user_spec, exit_codes): + when_i_run_command(context, command, user_spec, verify_return=False) + retries = [5, 5, 10] # Sleep times to wait between retries + while str(context.process.returncode) in exit_codes.split(","): + try: + time.sleep(retries.pop(0)) + except IndexError: # no more timeouts + logging.warning( + "Exhausted retries waiting for exit codes: %s", exit_codes + ) + break + logging.info( + "--- Retrying on exit {exit_code}: {command}".format( + exit_code=context.process.returncode, command=command + ) + ) + when_i_run_command(context, command, user_spec, verify_return=False) + assert_that(context.process.returncode, equal_to(0)) + + +@when("I run `{command}` `{user_spec}` and stdin `{stdin}`") +def when_i_run_command_with_stdin( + context, command, user_spec, stdin, instancedebug_name="uaclient" +): + when_i_run_command( + context=context, command=command, user_spec=user_spec, stdin=stdin + ) + + +@when("I run `{command}` {user_spec}") +def when_i_run_command( + context, + command, + user_spec, + verify_return=True, + stdin=None, + instance_name="uaclient", +): + if "" in command and "proxy" in context.instances: + command = command.replace( + "", context.instances["proxy"].ip + ) + prefix = get_command_prefix_for_user_spec(user_spec) + + full_cmd = prefix + shlex.split(command) + result = context.instances[instance_name].execute(full_cmd, stdin=stdin) + + process = subprocess.CompletedProcess( + args=full_cmd, + stdout=result.stdout, + stderr=result.stderr, + returncode=result.return_code, + ) + + if verify_return and result.return_code != 0: + logging.error("Error executing command: {}".format(command)) + logging.error("stdout: {}".format(result.stdout)) + logging.error("stderr: {}".format(result.stderr)) + else: + logging.debug("stdout: {}".format(result.stdout)) + logging.debug("stderr: {}".format(result.stderr)) + + if verify_return: + assert_that(process.returncode, equal_to(0)) + + context.process = process + + +@when("I run shell command `{command}` {user_spec}") +def when_i_run_shell_command(context, command, user_spec): + when_i_run_command(context, 'sh -c "{}"'.format(command), user_spec) + + +@then("I verify that the `{cmd_name}` command is not found") +def then_i_should_see_that_the_command_is_not_found(context, cmd_name): + cmd = "which {} || echo FAILURE".format(cmd_name) + cmd = 'sh -c "{}"'.format(cmd) + when_i_run_command(context, cmd, "as non-root") + + expected_return = "FAILURE" + actual_return = context.process.stdout.strip() + assert_that(expected_return, equal_to(actual_return)) + + +@then("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") +def then_i_verify_that_running_cmd_with_spec_exits_with_codes( + context, cmd_name, spec, exit_codes +): + when_i_run_command(context, cmd_name, spec, verify_return=False) + logging.debug("got return code: %d", context.process.returncode) + expected_codes = exit_codes.split(",") + assert str(context.process.returncode) in expected_codes + + +@when( + "I verify that running `{cmd_name}` `{spec}` and stdin `{stdin}` exits `{exit_codes}`" # noqa +) +def then_i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( + context, cmd_name, spec, stdin, exit_codes +): + when_i_run_command( + context, cmd_name, spec, stdin=stdin, verify_return=False + ) + + expected_codes = exit_codes.split(",") + assert str(context.process.returncode) in expected_codes + + +@when("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") +def when_i_verify_that_running_cmd_with_spec_exits_with_codes( + context, cmd_name, spec, exit_codes +): + then_i_verify_that_running_cmd_with_spec_exits_with_codes( + context, cmd_name, spec, exit_codes + ) + + +def get_command_prefix_for_user_spec(user_spec): + prefix = [] + if user_spec == "with sudo": + prefix = ["sudo"] + elif user_spec != "as non-root": + raise Exception( + "The two acceptable values for user_spec are: 'with sudo'," + " 'as non-root'" + ) + return prefix diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/status.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/status.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/status.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,30 @@ +from behave import when + +from features.steps.shell import when_i_run_command + + +@when("I do a preflight check for `{contract_token}` {user_spec}") +def when_i_preflight(context, contract_token, user_spec, verify_return=True): + token = getattr(context.config, contract_token, "invalid_token") + command = "pro status --simulate-with-token {}".format(token) + if user_spec == "with the all flag": + command += " --all" + if "formatted as" in user_spec: + output_format = user_spec.split()[2] + command += " --format {}".format(output_format) + when_i_run_command( + context=context, + command=command, + user_spec="as non-root", + verify_return=verify_return, + ) + + +@when( + "I verify that a preflight check for `{contract_token}` {user_spec} exits {exit_codes}" # noqa +) +def when_i_attempt_preflight(context, contract_token, user_spec, exit_codes): + when_i_preflight(context, contract_token, user_spec, verify_return=False) + + expected_codes = exit_codes.split(",") + assert str(context.process.returncode) in expected_codes diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/steps.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/steps.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/steps.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/steps.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,1248 +0,0 @@ -import datetime -import logging -import os -import re -import shlex -import subprocess -import time - -import jsonschema # type: ignore -import yaml -from behave import given, then, when -from hamcrest import ( - assert_that, - contains_string, - equal_to, - matches_regexp, - not_, -) - -from features.environment import ( - UA_PPA_TEMPLATE, - build_debs_from_sbuild, - capture_container_as_image, - create_instance_with_uat_installed, -) -from features.util import ( - InstallationSource, - SafeLoaderWithoutDatetime, - cleanup_instance, -) -from uaclient.defaults import ( - DEFAULT_CONFIG_FILE, - DEFAULT_PRIVATE_MACHINE_TOKEN_PATH, -) -from uaclient.util import DatetimeAwareJSONDecoder - -CONTAINER_PREFIX = "ubuntu-behave-test" -IMAGE_BUILD_PREFIX = "ubuntu-behave-image-build" -IMAGE_PREFIX = "ubuntu-behave-image" - -ERROR_CODE = "1" - - -def add_test_name_suffix(context, series, prefix): - pr_number = os.environ.get("UACLIENT_BEHAVE_JENKINS_CHANGE_ID") - pr_suffix = "-" + str(pr_number) if pr_number else "" - is_vm = bool(context.config.machine_type == "lxd.vm") - vm_suffix = "-vm" if is_vm else "" - time_suffix = datetime.datetime.now().strftime("-%s%f") - - return "{prefix}{pr_suffix}{vm_suffix}-{series}{time_suffix}".format( - prefix=prefix, - pr_suffix=pr_suffix, - vm_suffix=vm_suffix, - series=series, - time_suffix=time_suffix, - ) - - -@given("a `{series}` machine with ubuntu-advantage-tools installed") -def given_a_machine(context, series, custom_user_data=None): - if series in context.reuse_container: - context.instances = {} - context.container_name = context.reuse_container[series] - context.instances["uaclient"] = context.config.cloud_api.get_instance( - context.container_name - ) - if "pro" in context.config.machine_type: - context.instances[ - "uaclient" - ] = context.config.cloud_api.get_instance(context.container_name) - return - - instance_name = add_test_name_suffix(context, series, CONTAINER_PREFIX) - - if context.config.snapshot_strategy and not custom_user_data: - if series not in context.series_image_name: - build_container_name = add_test_name_suffix( - context, series, IMAGE_BUILD_PREFIX - ) - image_inst = create_instance_with_uat_installed( - context, series, build_container_name, custom_user_data - ) - - image_name = add_test_name_suffix(context, series, IMAGE_PREFIX) - image_inst_id = context.config.cloud_manager.get_instance_id( - image_inst - ) - image_id = capture_container_as_image( - image_inst_id, - image_name=image_name, - cloud_api=context.config.cloud_api, - ) - - context.series_image_name[series] = image_id - image_inst.delete(wait=False) - - inst = context.config.cloud_manager.launch( - series=series, - instance_name=instance_name, - image_name=context.series_image_name[series], - ephemeral=context.config.ephemeral_instance, - ) - else: - inst = create_instance_with_uat_installed( - context, series, instance_name, custom_user_data - ) - - context.series = series - context.instances = {"uaclient": inst} - - context.container_name = context.config.cloud_manager.get_instance_id( - context.instances["uaclient"] - ) - - context.add_cleanup(cleanup_instance(context, "uaclient")) - logging.info( - "--- instance ip: {}".format(context.instances["uaclient"].ip) - ) - - -@when("I take a snapshot of the machine") -def when_i_take_a_snapshot(context): - cloud = context.config.cloud_manager - inst = context.instances["uaclient"] - - snapshot = cloud.api.snapshot(inst) - - context.instance_snapshot = snapshot - - def cleanup_image() -> None: - try: - context.config.cloud_manager.api.delete_image( - context.instance_snapshot - ) - except RuntimeError as e: - logging.error( - "Failed to delete image: {}\n{}".format( - context.instance_snapshot, str(e) - ) - ) - - context.add_cleanup(cleanup_image) - - -@given( - "a `{series}` machine with ubuntu-advantage-tools installed adding this cloud-init user_data" # noqa -) -def given_a_machine_with_user_data(context, series): - custom_user_data = context.text - given_a_machine(context, series, custom_user_data) - - -@when("I have the `{series}` debs under test in `{dest}`") -def when_i_have_the_debs_under_test(context, series, dest): - if context.config.install_from is InstallationSource.LOCAL: - deb_paths = build_debs_from_sbuild(context, series) - - for deb_path in deb_paths: - tools_or_pro = "tools" if "tools" in deb_path else "pro" - dest_path = "{}/ubuntu-advantage-{}.deb".format(dest, tools_or_pro) - context.instances["uaclient"].push_file(deb_path, dest_path) - else: - if context.config.install_from is InstallationSource.PROPOSED: - ppa_opts = "" - else: - if context.config.install_from is InstallationSource.DAILY: - ppa = UA_PPA_TEMPLATE.format("daily") - elif context.config.install_from is InstallationSource.STAGING: - ppa = UA_PPA_TEMPLATE.format("staging") - elif context.config.install_from is InstallationSource.STABLE: - ppa = UA_PPA_TEMPLATE.format("stable") - elif context.config.install_from is InstallationSource.CUSTOM: - ppa = context.config.custom_ppa - if not ppa.startswith("ppa"): - # assumes format "http://domain.name/user/ppa/ubuntu" - match = re.match(r"https?://[\w.]+/([^/]+/[^/]+)", ppa) - if not match: - raise AssertionError( - "ppa is in unsupported format: {}".format(ppa) - ) - ppa = "ppa:{}".format(match.group(1)) - ppa_opts = "--distro ppa --ppa {}".format(ppa) - download_cmd = "pull-lp-debs {} ubuntu-advantage-tools {}".format( - ppa_opts, series - ) - when_i_run_command( - context, "apt-get install -y ubuntu-dev-tools", "with sudo" - ) - when_i_run_command(context, download_cmd, "with sudo") - logging.info("Download command `{}`".format(download_cmd)) - logging.info("stdout: {}".format(context.process.stdout)) - logging.info("stderr: {}".format(context.process.stderr)) - when_i_run_shell_command( - context, - "cp ubuntu-advantage-tools*.deb ubuntu-advantage-tools.deb", - "with sudo", - ) - when_i_run_shell_command( - context, - "cp ubuntu-advantage-pro*.deb ubuntu-advantage-pro.deb", - "with sudo", - ) - - -@when( - "I launch a `{series}` `{instance_name}` machine with ingress ports `{ports}`" # noqa -) -def launch_machine_with_ingress_ports(context, series, instance_name, ports): - launch_machine( - context=context, - series=series, - instance_name=instance_name, - ports=ports, - ) - - -@when("I launch a `{series}` `{instance_name}` machine") -def launch_machine(context, series, instance_name, ports=None): - now = datetime.datetime.now() - date_prefix = now.strftime("-%s%f") - name = CONTAINER_PREFIX + series + date_prefix + "-" + instance_name - - kwargs = {"series": series, "instance_name": name} - if ports: - kwargs["inbound_ports"] = ports.split(",") - context.instances[instance_name] = context.config.cloud_manager.launch( - **kwargs - ) - - context.add_cleanup(cleanup_instance(context, instance_name)) - - -@when("I launch a `{instance_name}` machine from the snapshot") -def launch_machine_from_snapshot(context, instance_name): - now = datetime.datetime.now() - date_prefix = now.strftime("-%s%f") - name = CONTAINER_PREFIX + date_prefix + "-" + instance_name - - context.instances[instance_name] = context.config.cloud_manager.launch( - context.series, - instance_name=name, - image_name=context.instance_snapshot, - ) - - context.add_cleanup(cleanup_instance(context, instance_name)) - - -@when("I add this text on `{file_name}` on `{instance_name}` above `{line}`") -def when_i_add_this_text_on_file_above_line( - context, file_name, instance_name, line -): - command = 'sed -i "s/{}/{}\\n{}/" {}'.format( - line, context.text, line, file_name - ) - when_i_run_command( - context, command, "with sudo", instance_name=instance_name - ) - - -@when("I run `{command}` `{user_spec}` on the `{instance_name}` machine") -def when_i_run_command_on_machine(context, command, user_spec, instance_name): - when_i_run_command( - context, command, user_spec, instance_name=instance_name - ) - - -@when("I verify `{file_name}` is empty on `{instance_name}` machine") -def when_i_verify_file_is_empty_on_machine(context, file_name, instance_name): - command = 'sh -c "cat {} | wc -l"'.format(file_name) - when_i_run_command( - context, command, user_spec="with sudo", instance_name=instance_name - ) - - assert_that(context.process.stdout.strip(), matches_regexp("0")) - - -@when("I run `{command}` {user_spec}, retrying exit [{exit_codes}]") -def when_i_retry_run_command(context, command, user_spec, exit_codes): - when_i_run_command(context, command, user_spec, verify_return=False) - retries = [5, 5, 10] # Sleep times to wait between retries - while str(context.process.returncode) in exit_codes.split(","): - try: - time.sleep(retries.pop(0)) - except IndexError: # no more timeouts - logging.warning( - "Exhausted retries waiting for exit codes: %s", exit_codes - ) - break - logging.info( - "--- Retrying on exit {exit_code}: {command}".format( - exit_code=context.process.returncode, command=command - ) - ) - when_i_run_command(context, command, user_spec, verify_return=False) - assert_that(context.process.returncode, equal_to(0)) - - -@when("I run `{command}` `{user_spec}` and stdin `{stdin}`") -def when_i_run_command_with_stdin( - context, command, user_spec, stdin, instancedebug_name="uaclient" -): - when_i_run_command( - context=context, command=command, user_spec=user_spec, stdin=stdin - ) - - -@when("I do a preflight check for `{contract_token}` {user_spec}") -def when_i_preflight(context, contract_token, user_spec, verify_return=True): - token = getattr(context.config, contract_token, "invalid_token") - command = "pro status --simulate-with-token {}".format(token) - if user_spec == "with the all flag": - command += " --all" - if "formatted as" in user_spec: - output_format = user_spec.split()[2] - command += " --format {}".format(output_format) - when_i_run_command( - context=context, - command=command, - user_spec="as non-root", - verify_return=verify_return, - ) - - -@when("I initiate the magic attach flow") -def when_i_initiate_magic_attach(context): - when_i_run_command( - context=context, - command="pro api u.pro.attach.magic.initiate.v1", - user_spec="as non-root", - ) - - magic_attach_resp = json.loads( - context.process.stdout.strip(), cls=DatetimeAwareJSONDecoder - ) - - context.magic_token = magic_attach_resp["data"]["attributes"]["token"] - - -@when("I revoke the magic attach token") -def when_i_revoke_the_magic_attach_token(context): - when_i_run_command( - context=context, - command="pro api u.pro.attach.magic.revoke.v1 --args magic_token={}".format( # noqa - context.magic_token - ), - user_spec="as non-root", - ) - - -@when("I wait for the magic attach token to be activated") -def when_i_wait_for_magic_attach_token(context): - when_i_run_command( - context=context, - command="pro api u.pro.attach.magic.wait.v1 --args magic_token={}".format( # noqa - context.magic_token - ), - user_spec="as non-root", - ) - - -@when( - "I verify that a preflight check for `{contract_token}` {user_spec} exits {exit_codes}" # noqa -) -def when_i_attempt_preflight(context, contract_token, user_spec, exit_codes): - when_i_preflight(context, contract_token, user_spec, verify_return=False) - - expected_codes = exit_codes.split(",") - assert str(context.process.returncode) in expected_codes - - -@when("I run `{command}` {user_spec}") -def when_i_run_command( - context, - command, - user_spec, - verify_return=True, - stdin=None, - instance_name="uaclient", -): - if "" in command and "proxy" in context.instances: - command = command.replace( - "", context.instances["proxy"].ip - ) - prefix = get_command_prefix_for_user_spec(user_spec) - - full_cmd = prefix + shlex.split(command) - result = context.instances[instance_name].execute(full_cmd, stdin=stdin) - - process = subprocess.CompletedProcess( - args=full_cmd, - stdout=result.stdout, - stderr=result.stderr, - returncode=result.return_code, - ) - - if verify_return and result.return_code != 0: - logging.error("Error executing command: {}".format(command)) - logging.error("stdout: {}".format(result.stdout)) - logging.error("stderr: {}".format(result.stderr)) - else: - logging.debug("stdout: {}".format(result.stdout)) - logging.debug("stderr: {}".format(result.stderr)) - - if verify_return: - assert_that(process.returncode, equal_to(0)) - - context.process = process - - -@when("I run shell command `{command}` {user_spec}") -def when_i_run_shell_command(context, command, user_spec): - when_i_run_command(context, 'sh -c "{}"'.format(command), user_spec) - - -@when("I fix `{issue}` by attaching to a subscription with `{token_type}`") -def when_i_fix_a_issue_by_attaching(context, issue, token_type): - token = getattr(context.config, token_type) - - if ( - token_type == "contract_token_staging" - or token_type == "contract_token_staging_expired" - ): - change_contract_endpoint_to_staging(context, user_spec="with sudo") - else: - change_contract_endpoint_to_production(context, user_spec="with sudo") - - when_i_run_command( - context=context, - command="pro fix {}".format(issue), - user_spec="with sudo", - stdin="a\n{}\n".format(token), - verify_return=False, - ) - - -@when("I fix `{issue}` by enabling required service") -def when_i_fix_a_issue_by_enabling_service(context, issue): - when_i_run_command( - context=context, - command="pro fix {}".format(issue), - user_spec="with sudo", - stdin="e\n", - ) - - -@when("I fix `{issue}` by updating expired token") -def when_i_fix_a_issue_by_updating_expired_token(context, issue): - token = getattr(context.config, "contract_token") - when_i_run_command( - context=context, - command="pro fix {}".format(issue), - user_spec="with sudo", - stdin="r\n{}\n".format(token), - ) - - -@when("I update contract to use `{contract_field}` as `{new_value}`") -def when_i_update_contract_field_to_new_value( - context, contract_field, new_value -): - if contract_field == "effectiveTo": - if "days=" in new_value: # Set timedelta offset from current day - now = datetime.datetime.utcnow() - contract_expiry = now + datetime.timedelta(days=int(new_value[5:])) - new_value = contract_expiry.strftime("%Y-%m-%dT00:00:00Z") - when_i_run_command( - context, - 'sed -i \'s/"{}": "[^"]*"/"{}": "{}"/g\' {}'.format( - contract_field, - contract_field, - new_value, - DEFAULT_PRIVATE_MACHINE_TOKEN_PATH, - ), - user_spec="with sudo", - ) - - -@when("I change contract to staging {user_spec}") -def change_contract_endpoint_to_staging(context, user_spec): - when_i_run_command( - context, - "sed -i 's/contracts.can/contracts.staging.can/' {}".format( - DEFAULT_CONFIG_FILE - ), - user_spec, - ) - - -def change_contract_endpoint_to_production(context, user_spec): - when_i_run_command( - context, - "sed -i 's/contracts.staging.can/contracts.can/' {}".format( - DEFAULT_CONFIG_FILE - ), - user_spec, - ) - - -@when("I attach `{token_type}` {user_spec} and options `{options}`") -def when_i_attach_staging_token_with_options( - context, token_type, user_spec, options -): - when_i_attach_staging_token( - context, token_type, user_spec, options=options - ) - - -@when("I attach `{token_type}` {user_spec}") -def when_i_attach_staging_token( - context, token_type, user_spec, verify_return=True, options="" -): - token = getattr(context.config, token_type) - if ( - token_type == "contract_token_staging" - or token_type == "contract_token_staging_expired" - ): - change_contract_endpoint_to_staging(context, user_spec) - cmd = "pro attach {} {}".format(token, options).strip() - when_i_run_command(context, cmd, user_spec, verify_return=False) - - if verify_return: - retries = [5, 5, 10] # Sleep times to wait between retries - while context.process.returncode != 0: - try: - time.sleep(retries.pop(0)) - except IndexError: # no more timeouts - logging.warning("Exhausted retries waiting for exit code: 0") - break - logging.info( - "--- Retrying on exit {exit_code}: {cmd}".format( - exit_code=context.process.returncode, cmd=cmd - ) - ) - when_i_run_command(context, cmd, user_spec, verify_return=False) - - -@when("I attempt to attach `{token_type}` {user_spec}") -def when_i_attempt_to_attach_staging_token(context, token_type, user_spec): - when_i_attach_staging_token( - context, token_type, user_spec, verify_return=False - ) - - -@when("I append the following on uaclient config") -def when_i_append_to_uaclient_config(context): - cmd = "printf '{}\n' > /tmp/uaclient.conf".format(context.text) - cmd = 'sh -c "{}"'.format(cmd) - when_i_run_command(context, cmd, "as non-root") - - cmd = "cat /tmp/uaclient.conf >> {}".format(DEFAULT_CONFIG_FILE) - cmd = 'sh -c "{}"'.format(cmd) - when_i_run_command(context, cmd, "with sudo") - - -@when("I create the file `{file_path}` with the following") -def when_i_create_file_with_content(context, file_path): - text = context.text.replace('"', '\\"') - if "" in text and "proxy" in context.instances: - text = text.replace("", context.instances["proxy"].ip) - cmd = "printf '{}' > {}".format(text, file_path) - cmd = 'sh -c "{}"'.format(cmd) - when_i_run_command(context, cmd, "with sudo") - - -@when("I delete the file `{file_path}`") -def when_i_delete_file(context, file_path): - cmd = "rm -rf {}".format(file_path) - cmd = 'sh -c "{}"'.format(cmd) - when_i_run_command(context, cmd, "with sudo") - - -@when("I reboot the machine") -def when_i_reboot_the_machine(context): - context.instances["uaclient"].restart(wait=True) - - -@when("I reboot the `{machine}` machine") -def when_i_reboot_the_machine_name(context, machine): - context.instances[machine].restart(wait=True) - - -@when("I wait `{seconds}` seconds") -def when_i_wait(context, seconds): - time.sleep(int(seconds)) - - -@when("I replace `{original}` in `{filename}` with `{new}`") -def when_i_replace_string_in_file(context, original, filename, new): - new = new.replace("\\", r"\\") - new = new.replace("/", r"\/") - new = new.replace("&", r"\&") - when_i_run_command( - context, - "sed -i 's/{original}/{new}/' {filename}".format( - original=original, new=new, filename=filename - ), - "with sudo", - ) - - -@when("I replace `{original}` in `{filename}` with token `{token_name}`") -def when_i_replace_string_in_file_with_token( - context, original, filename, token_name -): - token = getattr(context.config, token_name) - when_i_replace_string_in_file(context, original, filename, token) - - -@then("I will see the following on stdout") -def then_i_will_see_on_stdout(context): - assert_that(context.process.stdout.strip(), equal_to(context.text)) - - -@then("if `{value1}` in `{value2}` and stdout matches regexp") -def then_conditional_stdout_matches_regexp(context, value1, value2): - """Only apply regex assertion if value1 in value2.""" - if value1 in value2.split(" or "): - then_stream_matches_regexp(context, "stdout") - - -@then("if `{value1}` in `{value2}` and stdout does not match regexp") -def then_conditional_stdout_does_not_match_regexp(context, value1, value2): - """Only apply regex assertion if value1 in value2.""" - if value1 in value2.split(" or "): - then_stream_does_not_match_regexp(context, "stdout") - - -@then("{stream} does not match regexp") -def then_stream_does_not_match_regexp(context, stream): - content = getattr(context.process, stream).strip() - assert_that(content, not_(matches_regexp(context.text))) - - -@then("{stream} matches regexp") -def then_stream_matches_regexp(context, stream): - content = getattr(context.process, stream).strip() - text = context.text - if "" in text and "proxy" in context.instances: - text = text.replace("", context.instances["proxy"].ip) - assert_that(content, matches_regexp(text)) - - -@then("{stream} contains substring") -def then_stream_contains_substring(context, stream): - content = getattr(context.process, stream).strip() - assert_that(content, contains_string(context.text)) - - -@then("I will see the following on stderr") -def then_i_will_see_on_stderr(context): - assert_that(context.process.stderr.strip(), equal_to(context.text)) - - -@then("I will see the uaclient version on stdout") -def then_i_will_see_the_uaclient_version_on_stdout(context): - python_import = "from uaclient.version import get_version" - - cmd = "python3 -c '{}; print(get_version())'".format(python_import) - - actual_version = context.process.stdout.strip() - when_i_run_command(context, cmd, "as non-root") - expected_version = context.process.stdout.strip() - - assert_that(expected_version, equal_to(actual_version)) - - -@then("I verify that the `{cmd_name}` command is not found") -def then_i_should_see_that_the_command_is_not_found(context, cmd_name): - cmd = "which {} || echo FAILURE".format(cmd_name) - cmd = 'sh -c "{}"'.format(cmd) - when_i_run_command(context, cmd, "as non-root") - - expected_return = "FAILURE" - actual_return = context.process.stdout.strip() - assert_that(expected_return, equal_to(actual_return)) - - -@then("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") -def then_i_verify_that_running_cmd_with_spec_exits_with_codes( - context, cmd_name, spec, exit_codes -): - when_i_run_command(context, cmd_name, spec, verify_return=False) - - expected_codes = exit_codes.split(",") - assert str(context.process.returncode) in expected_codes - - -@when( - "I verify that running attach `{spec}` with json response exits `{exit_codes}`" # noqa -) -def when_i_verify_attach_with_json_response(context, spec, exit_codes): - cmd = "pro attach {} --format json".format(context.config.contract_token) - then_i_verify_that_running_cmd_with_spec_exits_with_codes( - context=context, cmd_name=cmd, spec=spec, exit_codes=exit_codes - ) - - -@when( - "I verify that running attach `{spec}` using expired token with json response fails" # noqa -) -def when_i_verify_attach_expired_token_with_json_response(context, spec): - change_contract_endpoint_to_staging(context, user_spec="with sudo") - cmd = "pro attach {} --format json".format( - context.config.contract_token_staging_expired - ) - then_i_verify_that_running_cmd_with_spec_exits_with_codes( - context=context, cmd_name=cmd, spec=spec, exit_codes=ERROR_CODE - ) - - -@when( - "I verify that running `{cmd_name}` `{spec}` and stdin `{stdin}` exits `{exit_codes}`" # noqa -) -def then_i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( - context, cmd_name, spec, stdin, exit_codes -): - when_i_run_command( - context, cmd_name, spec, stdin=stdin, verify_return=False - ) - - expected_codes = exit_codes.split(",") - assert str(context.process.returncode) in expected_codes - - -@when("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") -def when_i_verify_that_running_cmd_with_spec_exits_with_codes( - context, cmd_name, spec, exit_codes -): - then_i_verify_that_running_cmd_with_spec_exits_with_codes( - context, cmd_name, spec, exit_codes - ) - - -@then("apt-cache policy for the following url has permission `{perm_id}`") -def then_apt_cache_policy_for_the_following_url_has_permission_perm_id( - context, perm_id -): - full_url = "{} {}".format(perm_id, context.text) - assert_that(context.process.stdout.strip(), matches_regexp(full_url)) - - -@then("I verify that files exist matching `{path_regex}`") -def there_should_be_files_matching_regex(context, path_regex): - when_i_run_command( - context, "ls {}".format(path_regex), "with sudo", verify_return=False - ) - if context.process.returncode != 0: - raise AssertionError("Missing expected files: {}".format(path_regex)) - - -@then("I verify that no files exist matching `{path_regex}`") -def there_should_be_no_files_matching_regex(context, path_regex): - when_i_run_command( - context, "ls {}".format(path_regex), "with sudo", verify_return=False - ) - if context.process.returncode == 0: - raise AssertionError( - "Unexpected files found: {}".format(context.process.stdout.strip()) - ) - - -@then("I verify that `{package}` installed version matches regexp `{regex}`") -def verify_installed_package_matches_version_regexp(context, package, regex): - when_i_run_command( - context, - "dpkg-query --showformat='${{Version}}' --show {}".format(package), - "as non-root", - ) - assert_that(context.process.stdout.strip(), matches_regexp(regex)) - - -@then( - "I verify that packages `{packages}` installed versions match regexp `{regex}`" # noqa: E501 -) -def verify_installed_packages_match_version_regexp(context, packages, regex): - for package in packages.split(" "): - verify_installed_package_matches_version_regexp( - context, package, regex - ) - - -@then("I verify that `{package}` is installed from apt source `{apt_source}`") -def verify_package_is_installed_from_apt_source(context, package, apt_source): - when_i_run_command( - context, "apt-cache policy {}".format(package), "as non-root" - ) - policy = context.process.stdout.strip() - RE_APT_SOURCE = r"\s+\d+\s+(?P.*)" - lines = policy.splitlines() - for index, line in enumerate(lines): - if re.match(r"\s+\*\*\*", line): # apt-policy installed prefix *** - # Next line is the apt repo from which deb is installed - installed_apt_source = re.match(RE_APT_SOURCE, lines[index + 1]) - if installed_apt_source is None: - raise RuntimeError( - "Unable to process apt-policy line {}".format( - lines[index + 1] - ) - ) - assert_that( - installed_apt_source.groupdict()["source"], - contains_string(apt_source), - ) - return - raise AssertionError( - "Package {package} is not installed".format(package=package) - ) - - -@then( - "I verify that `{packages}` are installed from apt source `{apt_source}`" -) -def verify_packages_are_installed_from_apt_source( - context, packages, apt_source -): - for package in packages.split(" "): - verify_package_is_installed_from_apt_source( - context, package, apt_source - ) - - -@then("I verify that the timer interval for `{job}` is `{interval}`") -def verify_timer_interval_for_job(context, job, interval): - when_i_run_command( - context, "cat /var/lib/ubuntu-advantage/jobs-status.json", "with sudo" - ) - jobs_status = json.loads( - context.process.stdout.strip(), cls=DatetimeAwareJSONDecoder - ) - last_run = jobs_status[job]["last_run"] - next_run = jobs_status[job]["next_run"] - run_diff = next_run - last_run - - assert_that(run_diff.seconds, equal_to(int(interval))) - - -def systemd_timer_info(context, timer_name, all=False): - all_flag = " --all" if all else "" - when_i_run_command( - context, - "systemctl list-timers {}.timer{}".format(timer_name, all_flag), - "with sudo", - ) - output = context.process.stdout.strip() - lines = output.split("\n") - return next((line for line in lines if timer_name in line), None) - - -@then("I verify the `{timer_name}` systemd timer is disabled") -def verify_systemd_timer_disabled(context, timer_name): - timer_info_str = systemd_timer_info(context, timer_name) - if timer_info_str is not None: - raise AssertionError( - "timer {} is not disabled:\n{}".format(timer_name, timer_info_str) - ) - - -@then( - "I verify the `{timer_name}` systemd timer ran within the last `{seconds}` seconds" # noqa: E501 -) -def verify_systemd_timer_ran(context, timer_name, seconds): - timer_info_str = systemd_timer_info(context, timer_name, all=True) - if timer_info_str is None: - raise AssertionError( - "timer {} is not enabled or does not exist".format(timer_name) - ) - match = re.match(r".*(left|n/a|ago)\s+(.+) UTC\s+(.+) ago", timer_info_str) - if match is None: - raise AssertionError( - "timer {} has never run:\n{}".format(timer_name, timer_info_str) - ) - datestr = match.group(2) - last_ran = datetime.datetime.strptime(datestr, "%a %Y-%m-%d %H:%M:%S") - if (datetime.datetime.utcnow() - last_ran) > datetime.timedelta( - seconds=int(seconds) - ): - raise AssertionError( - "timer {} has not run within {} seconds:\n{}".format( - timer_name, seconds, timer_info_str - ) - ) - - -@then( - "I verify the `{timer_name}` systemd timer is scheduled to run within `{minutes}` minutes" # noqa: E501 -) -def verify_systemd_timer_scheduled(context, timer_name, minutes): - timer_info_str = systemd_timer_info(context, timer_name) - if timer_info_str is None: - raise AssertionError( - "timer {} is not enabled or does not exist".format(timer_name) - ) - match = re.match(r"^(.+) UTC\s+(.+) left", timer_info_str) - if match is None: - raise AssertionError( - "timer {} is not scheduled to run:\n{}".format( - timer_name, timer_info_str - ) - ) - datestr = match.group(1) - next_run = datetime.datetime.strptime(datestr, "%a %Y-%m-%d %H:%M:%S") - if (next_run - datetime.datetime.utcnow()) > datetime.timedelta( - minutes=int(minutes) - ): - raise AssertionError( - "timer {} is not scheduled to run within {} minutes:\n{}".format( - timer_name, minutes, timer_info_str - ) - ) - - -@then( - "I verify the `{timer_name}` systemd timer either ran within the past `{seconds}` seconds OR is scheduled to run within `{minutes}` minutes" # noqa: E501 -) -def verify_systemd_timer_ran_or_scheduled( - context, timer_name, seconds, minutes -): - ran_error = None - going_to_run_error = None - try: - verify_systemd_timer_ran(context, timer_name, seconds) - except AssertionError as e: - ran_error = e - try: - verify_systemd_timer_scheduled(context, timer_name, minutes) - except AssertionError as e: - going_to_run_error = e - - if ran_error and going_to_run_error: - raise AssertionError("{}\n{}".format(ran_error, going_to_run_error)) - - -@when("I save the `{key}` value from the contract") -def i_save_the_key_value_from_contract(context, key): - when_i_run_command( - context, - "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), - "with sudo", - ) - output = context.process.stdout.strip() - - if output: - if not hasattr(context, "saved_values"): - setattr(context, "saved_values", {}) - - context.saved_values[key] = output - - -def _get_saved_attr(context, key): - saved_value = getattr(context, "saved_values", {}).get(key) - - if saved_value is None: - raise AssertionError( - "Value for key {} was not previously saved\n".format(key) - ) - - return saved_value - - -@then( - "I verify that `{key}` value has been updated on the contract on the `{machine}` machine" # noqa: E501 -) -def i_verify_that_key_value_has_been_updated_on_machine(context, key, machine): - i_verify_that_key_value_has_been_updated(context, key, machine) - - -@then("I verify that `{key}` value has been updated on the contract") -def i_verify_that_key_value_has_been_updated(context, key, machine="uaclient"): - saved_value = _get_saved_attr(context, key) - when_i_run_command_on_machine( - context, - "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), - "with sudo", - instance_name=machine, - ) - assert_that(context.process.stdout.strip(), not_(equal_to(saved_value))) - - -@then("I verify that `{key}` value has not been updated on the contract") -def i_verify_that_key_value_has_not_been_updated(context, key): - saved_value = _get_saved_attr(context, key) - when_i_run_command( - context, - "jq -r '.{}' {}".format(key, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH), - "with sudo", - ) - assert_that(context.process.stdout.strip(), equal_to(saved_value)) - - -@when("I restore the saved `{key}` value on contract") -def i_restore_the_saved_key_value_on_contract(context, key): - saved_value = _get_saved_attr(context, key) - when_i_update_contract_field_to_new_value( - context=context, - contract_field=key.split(".")[-1], - new_value=saved_value, - ) - - -@then("stdout is a {output_format} matching the `{schema}` schema") -def stdout_matches_the_json_schema(context, output_format, schema): - if output_format == "json": - instance = json.loads(context.process.stdout.strip()) - elif output_format == "yaml": - instance = yaml.load( - context.process.stdout.strip(), SafeLoaderWithoutDatetime - ) - with open("features/schemas/{}.json".format(schema), "r") as schema_file: - jsonschema.validate(instance=instance, schema=json.load(schema_file)) - - -@then("the {output_format} API response data matches the `{schema}` schema") -def api_response_matches_schema(context, output_format, schema): - if output_format == "json": - instance = json.loads(context.process.stdout.strip()) - elif output_format == "yaml": - instance = yaml.load( - context.process.stdout.strip(), SafeLoaderWithoutDatetime - ) - with open("features/schemas/{}.json".format(schema), "r") as schema_file: - jsonschema.validate( - instance=instance.get("data", {}).get("attributes"), - schema=json.load(schema_file), - ) - - -@then("`{file_name}` is not present in any docker image layer") -def file_is_not_present_in_any_docker_image_layer(context, file_name): - when_i_run_command( - context, - "find /var/lib/docker/overlay2 -name {}".format(file_name), - "with sudo", - ) - results = context.process.stdout.strip() - if results: - raise AssertionError( - 'found "{}"'.format(", ".join(results.split("\n"))) - ) - - -# This defines "not significantly larger" as "less than 2MB larger" -@then( - "docker image `{name}` is not significantly larger than `ubuntu:{series}` with `{package}` installed" # noqa: E501 -) -def docker_image_is_not_larger(context, name, series, package): - base_image_name = "ubuntu:{}".format(series) - base_upgraded_image_name = "{}-with-test-package".format(series) - - # We need to compare against the base image after apt upgrade - # and package install - dockerfile = """\ - FROM {} - RUN apt-get update \\ - && apt-get install -y {} \\ - && rm -rf /var/lib/apt/lists/* - """.format( - base_image_name, package - ) - context.text = dockerfile - when_i_create_file_with_content(context, "Dockerfile.base") - when_i_run_command( - context, - "docker build . -f Dockerfile.base -t {}".format( - base_upgraded_image_name - ), - "with sudo", - ) - - # find image sizes - when_i_run_shell_command( - context, "docker inspect {} | jq .[0].Size".format(name), "with sudo" - ) - custom_image_size = int(context.process.stdout.strip()) - when_i_run_shell_command( - context, - "docker inspect {} | jq .[0].Size".format(base_upgraded_image_name), - "with sudo", - ) - base_image_size = int(context.process.stdout.strip()) - - # Get pro test deb size - when_i_run_command(context, "du ubuntu-advantage-tools.deb", "with sudo") - # Example out: "1234\tubuntu-advantage-tools.deb" - ua_test_deb_size = ( - int(context.process.stdout.strip().split("\t")[0]) * 1024 - ) # KB -> B - - # Give us some space for bloat we don't control: 2MB -> B - extra_space = 2 * 1024 * 1024 - - if custom_image_size > (base_image_size + ua_test_deb_size + extra_space): - raise AssertionError( - "Custom image size ({}) is over 2MB greater than the base image" - " size ({}) + pro test deb size ({})".format( - custom_image_size, base_image_size, ua_test_deb_size - ) - ) - logging.debug( - "custom image size ({})\n" - "base image size ({})\n" - "pro test deb size ({})".format( - custom_image_size, base_image_size, ua_test_deb_size - ) - ) - - -@then( - "on `{release}`, systemd status output says memory usage is less than `{mb_limit}` MB" # noqa -) -def systemd_memory_usage_less_than(context, release, mb_limit): - curr_release = context.active_outline["release"] - if release != curr_release: - logging.debug("Skipping for {}".format(curr_release)) - return - match = re.search(r"Memory: (.*)M", context.process.stdout.strip()) - if match is None: - raise AssertionError( - "Memory usage not present in current process stdout" - ) - mb_used = float(match.group(1)) - logging.debug("Found {}M".format(mb_used)) - - mb_limit_float = float(mb_limit) - if mb_used > mb_limit_float: - raise AssertionError( - "Using more memory than expected ({}M)".format(mb_used) - ) - - -@when( - "I prepare the local PPAs to upgrade from `{release}` to `{next_release}`" -) -def when_i_create_local_ppas(context, release, next_release): - if context.config.install_from is not InstallationSource.LOCAL: - return - - # We need Kinetic or greater to support zstd when creating the PPAs - launch_machine(context, "kinetic", "ppa") - when_i_run_command_on_machine( - context, "apt-get update", "with sudo", "ppa" - ) - when_i_run_command_on_machine( - context, "apt-get install -y aptly", "with sudo", "ppa" - ) - create_local_ppa(context, release) - create_local_ppa(context, next_release) - repo_line = "deb [trusted=yes] http://{}:8080 {} main".format( - context.instances["ppa"].ip, release - ) - repo_file = "/etc/apt/sources.list.d/local-ua.list" - when_i_run_shell_command( - context, "printf '{}\n' > {}".format(repo_line, repo_file), "with sudo" - ) - when_i_run_command_on_machine( - context, - "sh -c 'nohup aptly serve > /dev/null 2>&1 &'", - "with sudo", - "ppa", - ) - - -@when("I verify root and non-root `{cmd}` calls have the same output") -def root_vs_nonroot_cmd_comparison(context, cmd): - when_i_run_command(context, cmd, "with sudo") - root_status_stdout = context.process.stdout.strip() - root_status_stderr = context.process.stderr.strip() - - when_i_run_command(context, cmd, "as non-root") - nonroot_status_stdout = context.process.stdout.strip() - nonroot_status_stderr = context.process.stderr.strip() - - assert_that(root_status_stdout, nonroot_status_stdout) - assert root_status_stderr == nonroot_status_stderr - - -@when("I install third-party / unknown packages in the machine") -def when_i_install_packages(context): - # The `code` deb package sets up an apt remote for updates, - # and is then listed as third-party. - # https://code.visualstudio.com/download - - # The `gh` deb package is just installed locally, - # and is then listed as unknown - # https://github.com/cli/cli/releases - when_i_run_command( - context, - ( - "curl -L " - "https://az764295.vo.msecnd.net/stable/" - "e4503b30fc78200f846c62cf8091b76ff5547662/" - "code_1.70.2-1660629410_amd64.deb " - "-o /tmp/code.deb" - ), - "with sudo", - ) - when_i_run_command( - context, - ( - "curl -L " - "https://github.com/cli/cli/releases/download/" - "v2.14.4/gh_2.14.4_linux_amd64.deb " - "-o /tmp/gh.deb" - ), - "with sudo", - ) - when_i_run_command( - context, "apt-get install -y /tmp/code.deb", "with sudo" - ) - when_i_run_command(context, "apt-get install -y /tmp/gh.deb", "with sudo") - when_i_run_command(context, "apt-get update", "with sudo") - - -def create_local_ppa(context, release): - when_i_run_command_on_machine( - context, - "aptly repo create -distribution {} repo-{}".format(release, release), - "with sudo", - "ppa", - ) - debs = build_debs_from_sbuild(context, release) - for deb in debs: - deb_destination = "/tmp/" + deb.split("/")[-1] - context.instances["ppa"].push_file(deb, deb_destination) - when_i_run_command_on_machine( - context, - "aptly repo add repo-{} {}".format(release, deb_destination), - "with sudo", - "ppa", - ) - when_i_run_command_on_machine( - context, - "aptly publish repo -skip-signing repo-{}".format(release), - "with sudo", - "ppa", - ) - - -def get_command_prefix_for_user_spec(user_spec): - prefix = [] - if user_spec == "with sudo": - prefix = ["sudo"] - elif user_spec != "as non-root": - raise Exception( - "The two acceptable values for user_spec are: 'with sudo'," - " 'as non-root'" - ) - return prefix diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/systemd.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/systemd.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/systemd.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/systemd.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,126 @@ +import datetime +import logging +import re + +from behave import then + +from features.steps.shell import when_i_run_command + + +def systemd_timer_info(context, timer_name, all=False): + all_flag = " --all" if all else "" + when_i_run_command( + context, + "systemctl list-timers {}.timer{}".format(timer_name, all_flag), + "with sudo", + ) + output = context.process.stdout.strip() + lines = output.split("\n") + return next((line for line in lines if timer_name in line), None) + + +@then("I verify the `{timer_name}` systemd timer is disabled") +def verify_systemd_timer_disabled(context, timer_name): + timer_info_str = systemd_timer_info(context, timer_name) + if timer_info_str is not None: + raise AssertionError( + "timer {} is not disabled:\n{}".format(timer_name, timer_info_str) + ) + + +@then( + "I verify the `{timer_name}` systemd timer ran within the last `{seconds}` seconds" # noqa: E501 +) +def verify_systemd_timer_ran(context, timer_name, seconds): + timer_info_str = systemd_timer_info(context, timer_name, all=True) + if timer_info_str is None: + raise AssertionError( + "timer {} is not enabled or does not exist".format(timer_name) + ) + match = re.match(r".*(left|n/a|ago)\s+(.+) UTC\s+(.+) ago", timer_info_str) + if match is None: + raise AssertionError( + "timer {} has never run:\n{}".format(timer_name, timer_info_str) + ) + datestr = match.group(2) + last_ran = datetime.datetime.strptime(datestr, "%a %Y-%m-%d %H:%M:%S") + if (datetime.datetime.utcnow() - last_ran) > datetime.timedelta( + seconds=int(seconds) + ): + raise AssertionError( + "timer {} has not run within {} seconds:\n{}".format( + timer_name, seconds, timer_info_str + ) + ) + + +@then( + "I verify the `{timer_name}` systemd timer is scheduled to run within `{minutes}` minutes" # noqa: E501 +) +def verify_systemd_timer_scheduled(context, timer_name, minutes): + timer_info_str = systemd_timer_info(context, timer_name) + if timer_info_str is None: + raise AssertionError( + "timer {} is not enabled or does not exist".format(timer_name) + ) + match = re.match(r"^(.+) UTC\s+(.+) left", timer_info_str) + if match is None: + raise AssertionError( + "timer {} is not scheduled to run:\n{}".format( + timer_name, timer_info_str + ) + ) + datestr = match.group(1) + next_run = datetime.datetime.strptime(datestr, "%a %Y-%m-%d %H:%M:%S") + if (next_run - datetime.datetime.utcnow()) > datetime.timedelta( + minutes=int(minutes) + ): + raise AssertionError( + "timer {} is not scheduled to run within {} minutes:\n{}".format( + timer_name, minutes, timer_info_str + ) + ) + + +@then( + "I verify the `{timer_name}` systemd timer either ran within the past `{seconds}` seconds OR is scheduled to run within `{minutes}` minutes" # noqa: E501 +) +def verify_systemd_timer_ran_or_scheduled( + context, timer_name, seconds, minutes +): + ran_error = None + going_to_run_error = None + try: + verify_systemd_timer_ran(context, timer_name, seconds) + except AssertionError as e: + ran_error = e + try: + verify_systemd_timer_scheduled(context, timer_name, minutes) + except AssertionError as e: + going_to_run_error = e + + if ran_error and going_to_run_error: + raise AssertionError("{}\n{}".format(ran_error, going_to_run_error)) + + +@then( + "on `{release}`, systemd status output says memory usage is less than `{mb_limit}` MB" # noqa +) +def systemd_memory_usage_less_than(context, release, mb_limit): + curr_release = context.active_outline["release"] + if release != curr_release: + logging.debug("Skipping for {}".format(curr_release)) + return + match = re.search(r"Memory: (.*)M", context.process.stdout.strip()) + if match is None: + raise AssertionError( + "Memory usage not present in current process stdout" + ) + mb_used = float(match.group(1)) + logging.debug("Found {}M".format(mb_used)) + + mb_limit_float = float(mb_limit) + if mb_used > mb_limit_float: + raise AssertionError( + "Using more memory than expected ({}M)".format(mb_used) + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/ubuntu_advantage_tools.py ubuntu-advantage-tools-27.12~22.04.1/features/steps/ubuntu_advantage_tools.py --- ubuntu-advantage-tools-27.11.3~22.04.1/features/steps/ubuntu_advantage_tools.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/steps/ubuntu_advantage_tools.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,141 @@ +import logging +import re + +from behave import when + +from features.environment import UA_PPA_TEMPLATE, build_debs_from_sbuild +from features.steps.machines import launch_machine +from features.steps.shell import ( + when_i_run_command, + when_i_run_command_on_machine, + when_i_run_shell_command, +) +from features.util import InstallationSource + + +@when("I have the `{series}` debs under test in `{dest}`") +def when_i_have_the_debs_under_test(context, series, dest): + if context.config.install_from is InstallationSource.LOCAL: + deb_paths = build_debs_from_sbuild(context, series) + + for deb_path in deb_paths: + tools_or_pro = "tools" if "tools" in deb_path else "pro" + dest_path = "{}/ubuntu-advantage-{}.deb".format(dest, tools_or_pro) + context.instances["uaclient"].push_file(deb_path, dest_path) + else: + if context.config.install_from is InstallationSource.PROPOSED: + ppa_opts = "" + else: + if context.config.install_from is InstallationSource.DAILY: + ppa = UA_PPA_TEMPLATE.format("daily") + elif context.config.install_from is InstallationSource.STAGING: + ppa = UA_PPA_TEMPLATE.format("staging") + elif context.config.install_from is InstallationSource.STABLE: + ppa = UA_PPA_TEMPLATE.format("stable") + elif context.config.install_from is InstallationSource.CUSTOM: + ppa = context.config.custom_ppa + if not ppa.startswith("ppa"): + # assumes format "http://domain.name/user/ppa/ubuntu" + match = re.match(r"https?://[\w.]+/([^/]+/[^/]+)", ppa) + if not match: + raise AssertionError( + "ppa is in unsupported format: {}".format(ppa) + ) + ppa = "ppa:{}".format(match.group(1)) + ppa_opts = "--distro ppa --ppa {}".format(ppa) + download_cmd = "pull-lp-debs {} ubuntu-advantage-tools {}".format( + ppa_opts, series + ) + when_i_run_command( + context, "apt-get install -y ubuntu-dev-tools", "with sudo" + ) + when_i_run_command(context, download_cmd, "with sudo") + logging.info("Download command `{}`".format(download_cmd)) + logging.info("stdout: {}".format(context.process.stdout)) + logging.info("stderr: {}".format(context.process.stderr)) + when_i_run_shell_command( + context, + "cp ubuntu-advantage-tools*.deb ubuntu-advantage-tools.deb", + "with sudo", + ) + when_i_run_shell_command( + context, + "cp ubuntu-advantage-pro*.deb ubuntu-advantage-pro.deb", + "with sudo", + ) + + +@when( + "I prepare the local PPAs to upgrade from `{release}` to `{next_release}`" +) +def when_i_create_local_ppas(context, release, next_release): + if context.config.install_from is not InstallationSource.LOCAL: + return + + # We need Kinetic or greater to support zstd when creating the PPAs + launch_machine(context, "kinetic", "ppa") + when_i_run_command_on_machine( + context, "apt-get update", "with sudo", "ppa" + ) + when_i_run_command_on_machine( + context, "apt-get install -y aptly", "with sudo", "ppa" + ) + create_local_ppa(context, release) + create_local_ppa(context, next_release) + repo_line = "deb [trusted=yes] http://{}:8080 {} main".format( + context.instances["ppa"].ip, release + ) + repo_file = "/etc/apt/sources.list.d/local-ua.list" + when_i_run_shell_command( + context, "printf '{}\n' > {}".format(repo_line, repo_file), "with sudo" + ) + when_i_run_command_on_machine( + context, + "sh -c 'nohup aptly serve > /dev/null 2>&1 &'", + "with sudo", + "ppa", + ) + + +def create_local_ppa(context, release): + when_i_run_command_on_machine( + context, + "aptly repo create -distribution {} repo-{}".format(release, release), + "with sudo", + "ppa", + ) + debs = build_debs_from_sbuild(context, release) + for deb in debs: + deb_destination = "/tmp/" + deb.split("/")[-1] + context.instances["ppa"].push_file(deb, deb_destination) + when_i_run_command_on_machine( + context, + "aptly repo add repo-{} {}".format(release, deb_destination), + "with sudo", + "ppa", + ) + when_i_run_command_on_machine( + context, + "aptly publish repo -skip-signing repo-{}".format(release), + "with sudo", + "ppa", + ) + + +@when("I install ubuntu-advantage-pro") +def when_i_install_pro(context): + if context.config.install_from is InstallationSource.LOCAL: + deb_paths = build_debs_from_sbuild(context, context.series) + + for deb_path in deb_paths: + if "pro" in deb_path: + context.instances["uaclient"].push_file( + deb_path, "/tmp/pro.deb" + ) + when_i_run_command( + context, "dpkg -i /tmp/pro.deb", "with sudo" + ) + else: + when_i_run_command( + context, "apt-get install ubuntu-advantage-pro", "with sudo" + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/ubuntu_pro.feature ubuntu-advantage-tools-27.12~22.04.1/features/ubuntu_pro.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/ubuntu_pro.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/ubuntu_pro.feature 2022-11-22 13:06:26.000000000 +0000 @@ -272,12 +272,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` @@ -395,12 +398,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` @@ -517,12 +523,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/ubuntu_pro_fips.feature ubuntu-advantage-tools-27.12~22.04.1/features/ubuntu_pro_fips.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/ubuntu_pro_fips.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/ubuntu_pro_fips.feature 2022-11-22 13:06:26.000000000 +0000 @@ -48,12 +48,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` @@ -261,12 +264,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` @@ -527,12 +533,15 @@ """ And stdout matches regexp: """ - Skipping auto-attach: Instance is already attached. + Active: inactive \(dead\).* + \s*Condition: start condition failed.* + .*ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json was not met """ - When I run `pro auto-attach` with sudo + When I verify that running `pro auto-attach` `with sudo` exits `2` Then stderr matches regexp: """ - Skipping auto-attach: Instance is already attached. + This machine is already attached to '.*' + To use a different subscription first run: sudo pro detach. """ When I run `apt-cache policy` with sudo Then apt-cache policy for the following url has permission `500` diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/features/unattached_status.feature ubuntu-advantage-tools-27.12~22.04.1/features/unattached_status.feature --- ubuntu-advantage-tools-27.11.3~22.04.1/features/unattached_status.feature 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/features/unattached_status.feature 2022-11-22 13:06:26.000000000 +0000 @@ -72,7 +72,7 @@ fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates livepatch +yes +Canonical Livepatch service - realtime-kernel +no +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System @@ -140,7 +140,7 @@ fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates livepatch +yes +Canonical Livepatch service - realtime-kernel +no +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +Security Updates for the Robot Operating System ros-updates +no +All Updates for the Robot Operating System usg +yes +Security compliance and audit tools @@ -187,6 +187,7 @@ SERVICE +AVAILABLE +DESCRIPTION esm-infra +yes +Expanded Security Maintenance for Infrastructure livepatch +yes +Canonical Livepatch service + realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated This machine is not attached to an Ubuntu Pro subscription. See https://ubuntu.com/pro @@ -202,7 +203,7 @@ fips +no +NIST-certified core packages fips-updates +no +NIST-certified core packages with priority security updates livepatch +yes +Canonical Livepatch service - realtime-kernel +yes +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +Security Updates for the Robot Operating System ros-updates +no +All Updates for the Robot Operating System usg +no +Security compliance and audit tools @@ -223,7 +224,7 @@ esm-apps +yes +Expanded Security Maintenance for Applications esm-infra +yes +Expanded Security Maintenance for Infrastructure livepatch +yes +Canonical Livepatch service - realtime-kernel +yes +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated FEATURES allow_beta: True @@ -264,7 +265,7 @@ fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates livepatch +yes +yes +yes +Canonical Livepatch service - realtime-kernel +no +yes +no +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +no +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +yes +no +Security Updates for the Robot Operating System ros-updates +yes +yes +no +All Updates for the Robot Operating System """ @@ -322,7 +323,7 @@ fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates livepatch +yes +yes +yes +Canonical Livepatch service - realtime-kernel +no +yes +no +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +no +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +yes +no +Security Updates for the Robot Operating System ros-updates +no +yes +no +All Updates for the Robot Operating System usg +yes +yes +no +Security compliance and audit tools @@ -378,7 +379,7 @@ fips +no +yes +no +NIST-certified core packages fips-updates +no +yes +no +NIST-certified core packages with priority security updates livepatch +yes +yes +yes +Canonical Livepatch service - realtime-kernel +yes +yes +no +Beta-version Ubuntu Kernel with PREEMPT_RT patches + realtime-kernel +yes +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +yes +no +Security Updates for the Robot Operating System ros-updates +no +yes +no +All Updates for the Robot Operating System usg +no +yes +no +Security compliance and audit tools diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/help_data.yaml ubuntu-advantage-tools-27.12~22.04.1/help_data.yaml --- ubuntu-advantage-tools-27.11.3~22.04.1/help_data.yaml 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/help_data.yaml 2022-11-22 13:06:26.000000000 +0000 @@ -62,13 +62,11 @@ realtime-kernel: help: | - The real-time kernel is a beta version of the 22.04 Ubuntu kernel with the - PREEMPT_RT patchset integrated for x86_64 and ARM64. It services extreme - latency-dependent use cases and provides deterministic response times to - service events. By meeting stringent preemption specifications, the - real-time kernel is suitable for telco applications and embedded devices - in industrial automation and robotics. To enroll in the beta program, visit - https://ubuntu.com/realtime-kernel + The real-time kernel is an Ubuntu kernel with PREEMPT_RT patches integrated. + It services latency-dependent use cases by providing deterministic response times. + The real-time kernel meets stringent preemption specifications and is suitable for + telco applications and dedicated devices in industrial automation and robotics. + The real-time kernel is currently incompatible with FIPS and Livepatch. ros: help: | diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/lib/auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/lib/auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/lib/auto_attach.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/lib/auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -13,9 +13,23 @@ import logging import sys -from uaclient.cli import action_auto_attach, setup_logging +from uaclient import messages, system +from uaclient.api.exceptions import ( + AlreadyAttachedError, + AutoAttachDisabledError, + EntitlementsNotEnabledError, +) +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, + full_auto_attach, +) from uaclient.config import UAConfig -from uaclient.exceptions import AlreadyAttachedOnPROError +from uaclient.daemon import ( + AUTO_ATTACH_STATUS_MOTD_FILE, + retry_auto_attach, + setup_logging, +) +from uaclient.files import state_files try: import cloudinit.stages as ci_stages # type: ignore @@ -38,32 +52,63 @@ if init is None: return False - if init.cfg and "ubuntu_advantage" in init.cfg.keys(): + if init.cfg and ( + "ubuntu_advantage" in init.cfg.keys() + or "ubuntu-advantage" in init.cfg.keys() + ): return True return False def main(cfg: UAConfig): - if not check_cloudinit_userdata_for_ua_info(): - # Once we have the api functions ready, we should - # update this part of the code to not call the cli - # function directly - try: - action_auto_attach(args=None, cfg=cfg) - except AlreadyAttachedOnPROError as e: - logging.info(e.msg) - else: - auto_attach_msg = ( + if check_cloudinit_userdata_for_ua_info(): + logging.info("cloud-init userdata has ubuntu-advantage key.") + logging.info( "Skipping auto-attach and deferring to cloud-init " "to setup and configure auto-attach" ) + return - logging.info("cloud-init userdata has ubuntu-advantage key.") - logging.info(auto_attach_msg) + system.write_file( + AUTO_ATTACH_STATUS_MOTD_FILE, messages.AUTO_ATTACH_RUNNING + ) + try: + full_auto_attach(FullAutoAttachOptions()) + except AlreadyAttachedError as e: + logging.info(e.msg) + except AutoAttachDisabledError: + logging.debug( + "Skipping auto-attach. Config disable_auto_attach is set." + ) + return + except EntitlementsNotEnabledError as e: + logging.warning(e.msg) + except Exception as e: + logging.error(e) + system.remove_file(AUTO_ATTACH_STATUS_MOTD_FILE) + logging.info("creating flag file to trigger retries") + system.create_file(retry_auto_attach.FLAG_FILE_PATH) + failure_reason = ( + retry_auto_attach.full_auto_attach_exception_to_failure_reason(e) + ) + state_files.retry_auto_attach_state_file.write( + state_files.RetryAutoAttachState( + interval_index=0, failure_reason=failure_reason + ) + ) + return 1 + + system.remove_file(AUTO_ATTACH_STATUS_MOTD_FILE) + return 0 if __name__ == "__main__": cfg = UAConfig(root_mode=True) - setup_logging(logging.INFO, logging.DEBUG, log_file=cfg.log_file) - main(cfg=cfg) + setup_logging( + logging.INFO, + logging.DEBUG, + log_file=cfg.log_file, + logger=logging.getLogger(), + ) + sys.exit(main(cfg)) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/lib/daemon.py ubuntu-advantage-tools-27.12~22.04.1/lib/daemon.py --- ubuntu-advantage-tools-27.11.3~22.04.1/lib/daemon.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/lib/daemon.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,35 +1,20 @@ import logging +import os import sys from systemd.daemon import notify # type: ignore -from uaclient import daemon from uaclient.config import UAConfig -from uaclient.defaults import DEFAULT_LOG_FORMAT +from uaclient.daemon import ( + poll_for_pro_license, + retry_auto_attach, + setup_logging, +) LOG = logging.getLogger("pro") -def setup_logging(console_level, log_level, log_file, logger): - logger.setLevel(log_level) - - logger.handlers = [] - - console_handler = logging.StreamHandler(sys.stderr) - console_handler.setFormatter(logging.Formatter("%(message)s")) - console_handler.setLevel(console_level) - console_handler.set_name("ua-console") - logger.addHandler(console_handler) - - file_handler = logging.FileHandler(log_file) - file_handler.setLevel(log_level) - file_handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) - file_handler.set_name("ua-file") - logger.addHandler(file_handler) - - def main() -> int: - cfg = UAConfig(root_mode=True) setup_logging( logging.INFO, logging.DEBUG, log_file=cfg.daemon_log_file, logger=LOG @@ -50,7 +35,17 @@ notify("READY=1") - daemon.poll_for_pro_license(cfg) + if os.path.exists("/run/cloud-init/cloud-id-gce") and not os.path.exists( + retry_auto_attach.FLAG_FILE_PATH + ): + LOG.info("mode: poll for pro license") + poll_for_pro_license.poll_for_pro_license(cfg) + + # not using elif because `poll_for_pro_license` may create the flag file + + if os.path.exists(retry_auto_attach.FLAG_FILE_PATH): + LOG.info("mode: retry auto attach") + retry_auto_attach.retry_auto_attach(cfg) LOG.debug("daemon ending") return 0 diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/README.md ubuntu-advantage-tools-27.12~22.04.1/README.md --- ubuntu-advantage-tools-27.11.3~22.04.1/README.md 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/README.md 2022-11-22 13:06:26.000000000 +0000 @@ -1,12 +1,12 @@

- +
- Ubuntu Advantage Client + Ubuntu Pro Client

-###### Clean and Consistent CLI for your Ubuntu Advantage Systems +###### Clean and Consistent CLI for your Ubuntu Pro Systems ![Latest Upstream Version](https://img.shields.io/github/v/tag/canonical/ubuntu-advantage-client.svg?label=Latest%20Upstream%20Version&logo=github&logoColor=white&color=33ce57) ![CI](https://github.com/canonical/ubuntu-advantage-client/actions/workflows/ci-base.yaml/badge.svg?branch=main)
@@ -15,10 +15,10 @@ ![Released Focal Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/focal?label=Focal&logo=ubuntu&logoColor=white) ![Released Jammy Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/jammy?label=Jammy&logo=ubuntu&logoColor=white) -The Ubuntu Advantage (UA) Client is the official tool to enable Canonical offerings on your +The Ubuntu Pro Client (`pro`) is the official tool to enable Canonical offerings on your system. -UA provides support to view, enable, and disable the following Canonical services: +`pro` provides support to view, enable, and disable the following Canonical services: - [Common Criteria EAL2 Certification Tooling](https://ubuntu.com/security/cc) - [CIS Benchmark Audit Tooling](https://ubuntu.com/security/cis) @@ -29,56 +29,58 @@ - [Livepatch Service](https://ubuntu.com/security/livepatch) -If you need any of those services for your machine, UA is the right tool for you. -Furthermore, UA is already installed on every Ubuntu system. Try it out by running `ua help`! +If you need any of those services for your machine, `pro` is the right tool for you. + +`pro` is already installed on every Ubuntu system. Try it out by running `pro help`! ## Documentation ### Tutorials -* [Getting started with UA](./docs/source/tutorials/basic_ua_commands.md) -* [Create a FIPS compliant Ubuntu Docker image](./docs/source/tutorials/create_a_fips_docker_image.md) -* [Fixing vulnerabilities by CVE or USN using `ua fix`](./docs/source/tutorials/ua_fix_scenarios.md) -* [Create a Custom Ubuntu Pro Cloud Image with FIPS Updates](./docs/source/tutorials/create_a_fips_updates_pro_cloud_image.md) +* [Getting started with Ubuntu Pro Client](./docs/tutorials/basic_commands.md) +* [Create a FIPS compliant Ubuntu Docker image](./docs/tutorials/create_a_fips_docker_image.md) +* [Fix vulnerabilities by CVE or USN using `pro fix`](./docs/tutorials/fix_scenarios.md) +* [Create a Custom Ubuntu Pro Cloud Image with FIPS Updates](./docs/tutorials/create_a_fips_updates_pro_cloud_image.md) ### How To Guides -* [How to get an UA token and attach to a subscription](./docs/source/howtoguides/get_token_and_attach.md) -* [How to Configure Proxies](./docs/howtoguides/source/configure_proxies.md) -* [How to Enable Ubuntu Advantage Services in a Dockerfile](./docs/source/howtoguides/enable_ua_in_dockerfile.md) -* [How to Create a custom Golden Image based on Ubuntu Pro](./docs/source/howtoguides/create_pro_golden_image.md) -* [How to Manually update MOTD and APT messages](./docs/source/howtoguides/update_motd_messages.md) -* [How to enable CIS](./docs/source/howtoguides/enable_cis.md) -* [How to enable CC EAL](./docs/source/howtoguides/enable_cc.md) -* [How to enable ESM Infra](./docs/source/howtoguides/enable_esm_infra.md) -* [How to enable FIPS](./docs/source/howtoguides/enable_fips.md) -* [How to enable Livepatch](./docs/source/howtoguides/enable_livepatch.md) -* [How to configure a timer job](./docs/source/howtoguides/configuring_timer_jobs.md) -* [How to attach with a configuration file](./docs/source/howtoguides/how_to_attach_with_config_file.md) -* [How to collect UA logs](./docs/source/howtoguides/how_to_collect_ua_logs.md) -* [How to Simulate attach operation](./docs/source/howtoguides/how_to_simulate_attach.md) -* [How to run ua fix in dry-run mode](./docs/source/howtoguides/how_to_run_ua_fix_in_dry_run_mode.md) +* [How to get an Ubuntu Pro token and attach to a subscription](./docs/howtoguides/get_token_and_attach.md) +* [How to Configure Proxies](./docs/howtoguides/configure_proxies.md) +* [How to Enable Ubuntu Pro Services in a Dockerfile](./docs/howtoguides/enable_in_dockerfile.md) +* [How to Create a custom Golden Image based on Public Cloud Ubuntu Pro images](./docs/howtoguides/create_pro_golden_image.md) +* [How to Manually update MOTD and APT messages](./docs/howtoguides/update_motd_messages.md) +* [How to enable CIS](./docs/howtoguides/enable_cis.md) +* [How to enable CC EAL](./docs/howtoguides/enable_cc.md) +* [How to enable ESM Infra](./docs/howtoguides/enable_esm_infra.md) +* [How to enable FIPS](./docs/howtoguides/enable_fips.md) +* [How to enable Livepatch](./docs/howtoguides/enable_livepatch.md) +* [How to configure a timer job](./docs/howtoguides/configuring_timer_jobs.md) +* [How to attach with a configuration file](./docs/howtoguides/how_to_attach_with_config_file.md) +* [How to collect Ubuntu Pro Client logs](./docs/howtoguides/how_to_collect_logs.md) +* [How to simulate attach operation](./docs/howtoguides/how_to_simulate_attach.md) +* [How to run `pro fix` in dry-run mode](./docs/howtoguides/how_to_run_fix_in_dry_run_mode.md) ### Reference -* [Ubuntu Release and Architecture Support Matrix](./docs/source/references/support_matrix.md) -* [UA Network Requirements](./docs/source/references/network_requirements.md) -* [PPAs with different versions of `ua`](./docs/source/references/ppas.md) +* [Ubuntu Release and Architecture Support Matrix](./docs/references/support_matrix.md) +* [Network Requirements](./docs/references/network_requirements.md) +* [API Reference Guide](./docs/references/api.md) +* [PPAs with different versions of `pro`](./docs/references/ppas.md) ### Explanation -* [What is the daemon for? (And how to disable it)](./docs/source/explanations/what_is_the_daemon.md) -* [What is Ubuntu PRO?](./docs/source/explanations/what_is_ubuntu_pro.md) -* [What is the ubuntu-advantage-pro package?](./docs/source/explanations/what_is_the_ubuntu_advantage_pro_package.md) -* [What are the timer jobs?](./docs/source/explanations/what_are_the_timer_jobs.md) -* [What are the UA related MOTD messages?](./docs/source/explanations/motd_messages.md) -* [What are the UA related APT messages?](./docs/source/explanations/apt_messages.md) -* [How to interpret the security-status command](./docs/source/explanations/how_to_interpret_the_security_status_command.md) -* [Why Trusty (14.04) is no longer supported](./docs/source/explanations/why_trusty_is_no_longer_supported.md) +* [What is the daemon for? (And how to disable it)](./docs/explanations/what_is_the_daemon.md) +* [What are Public Cloud Ubuntu Pro instances?](./docs/explanations/what_are_ubuntu_pro_cloud_instances.md) +* [What is the ubuntu-advantage-pro package?](./docs/explanations/what_is_the_ubuntu_advantage_pro_package.md) +* [What are the timer jobs?](./docs/explanations/what_are_the_timer_jobs.md) +* [What are the Ubuntu Pro related MOTD messages?](./docs/explanations/motd_messages.md) +* [What are the Ubuntu Pro related APT messages?](./docs/explanations/apt_messages.md) +* [How to interpret the security-status command](./docs/explanations/how_to_interpret_the_security_status_command.md) +* [Why Trusty (14.04) is no longer supported](./docs/explanations/why_trusty_is_no_longer_supported.md) ## Project and community -UA is a member of the Ubuntu family. It’s an open source project that warmly welcomes +Ubuntu Pro Client is a member of the Ubuntu family. It’s an open source project that warmly welcomes community projects, contributions, suggestions, fixes and constructive feedback. * [Contribute](CONTRIBUTING.md) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/sru/_archive/release-27.10/daemon_stall_cloned_machine.py ubuntu-advantage-tools-27.12~22.04.1/sru/_archive/release-27.10/daemon_stall_cloned_machine.py --- ubuntu-advantage-tools-27.11.3~22.04.1/sru/_archive/release-27.10/daemon_stall_cloned_machine.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/sru/_archive/release-27.10/daemon_stall_cloned_machine.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,65 @@ +import logging +import os + +import pycloudlib +from pycloudlib.cloud import ImageType + + +def handle_ssh_key(ec2, key_name): + """Manage ssh keys to be used in the instances.""" + if key_name in ec2.list_keys(): + ec2.delete_key(key_name) + + key_pair = ec2.client.create_key_pair(KeyName=key_name) + private_key_path = "ec2-test.pem" + with open(private_key_path, "w", encoding="utf-8") as stream: + stream.write(key_pair["KeyMaterial"]) + os.chmod(private_key_path, 0o600) + + # Since we are using a pem file, we don't have distinct public and + # private key paths + ec2.use_key( + public_key_path=private_key_path, + private_key_path=private_key_path, + name=key_name, + ) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + ec2 = pycloudlib.EC2(tag="examples") + key_name = "test-ec2" + handle_ssh_key(ec2, key_name) + + daily_pro = ec2.daily_image(release="focal", image_type=ImageType.PRO) + + print("Launching Pro instance...") + instance = ec2.launch(daily_pro, instance_type="m5.large") + instance.execute("touch custom_config_file") + print(instance.execute("sudo ua status --wait")) + print(instance.execute("sudo apt update")) + print(instance.execute("sudo apt install ubuntu-advantage-tools")) + + print("") + print("install ua version with the fix (must be at ./ua.deb)") + print("") + instance.push_file("./ua.deb", "/tmp/ua.deb") + print(instance.execute("sudo dpkg -i /tmp/ua.deb")) + + print("") + print("snapshotting") + print("") + image = ec2.snapshot(instance) + + print("") + print("launching clone - if this finishes, then success!") + print("") + new_instance = ec2.launch(image, instance_type="m5.large") + print(new_instance.execute("sudo ua status --wait")) + + print("") + print("Deleting Pro instances and image") + print("") + new_instance.delete() + ec2.delete_image(image) + instance.delete() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/sru/daemon_stall_cloned_machine.py ubuntu-advantage-tools-27.12~22.04.1/sru/daemon_stall_cloned_machine.py --- ubuntu-advantage-tools-27.11.3~22.04.1/sru/daemon_stall_cloned_machine.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/sru/daemon_stall_cloned_machine.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -import logging -import os - -import pycloudlib -from pycloudlib.cloud import ImageType - - -def handle_ssh_key(ec2, key_name): - """Manage ssh keys to be used in the instances.""" - if key_name in ec2.list_keys(): - ec2.delete_key(key_name) - - key_pair = ec2.client.create_key_pair(KeyName=key_name) - private_key_path = "ec2-test.pem" - with open(private_key_path, "w", encoding="utf-8") as stream: - stream.write(key_pair["KeyMaterial"]) - os.chmod(private_key_path, 0o600) - - # Since we are using a pem file, we don't have distinct public and - # private key paths - ec2.use_key( - public_key_path=private_key_path, - private_key_path=private_key_path, - name=key_name, - ) - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - ec2 = pycloudlib.EC2(tag="examples") - key_name = "test-ec2" - handle_ssh_key(ec2, key_name) - - daily_pro = ec2.daily_image(release="focal", image_type=ImageType.PRO) - - print("Launching Pro instance...") - instance = ec2.launch(daily_pro, instance_type="m5.large") - instance.execute("touch custom_config_file") - print(instance.execute("sudo ua status --wait")) - print(instance.execute("sudo apt update")) - print(instance.execute("sudo apt install ubuntu-advantage-tools")) - - print("") - print("install ua version with the fix (must be at ./ua.deb)") - print("") - instance.push_file("./ua.deb", "/tmp/ua.deb") - print(instance.execute("sudo dpkg -i /tmp/ua.deb")) - - print("") - print("snapshotting") - print("") - image = ec2.snapshot(instance) - - print("") - print("launching clone - if this finishes, then success!") - print("") - new_instance = ec2.launch(image, instance_type="m5.large") - print(new_instance.execute("sudo ua status --wait")) - - print("") - print("Deleting Pro instances and image") - print("") - new_instance.delete() - ec2.delete_image(image) - instance.delete() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/sru/release-27.11.3/test_apport_wrong_encoding.sh ubuntu-advantage-tools-27.12~22.04.1/sru/release-27.11.3/test_apport_wrong_encoding.sh --- ubuntu-advantage-tools-27.11.3~22.04.1/sru/release-27.11.3/test_apport_wrong_encoding.sh 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/sru/release-27.11.3/test_apport_wrong_encoding.sh 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +series=$1 +name=$series-dev + + +function cleanup { + lxc delete $name --force +} + +function on_err { + echo -e "Test Failed" + cleanup + exit 1 +} + +trap on_err ERR + +lxc launch ubuntu-daily:$series $name +sleep 10 + +# Update ubuntu-advantage-tools package +lxc exec $name -- sudo apt-get update > /dev/null +lxc exec $name -- sudo apt-get upgrade -y > /dev/null +echo "###########################################" +lxc exec $name -- apt-cache policy ubuntu-advantage-tools +echo -e "###########################################\n" + +# Create a (collected) file with invalid utf-8 encoding +lxc exec $name -- touch file +lxc exec $name -- tar -zcf invalid.tar.gz file +lxc exec $name -- mv invalid.tar.gz /var/lib/ubuntu-advantage/jobs-status.json + +# Error out while trying to create a bug report +echo -e "\n* Check the error in collect-logs" +echo "###########################################" +lxc exec $name -- pro collect-logs || true +echo -e "###########################################\n" + +echo -e "\n* Check the error in apport-bug" +echo "###########################################" +lxc exec $name -- ubuntu-bug --save=/tmp/test1 ubuntu-advantage-tools +echo -e "###########################################\n" + +# Upgrading UA to the new version +echo -e "\n* Upgrading UA to new version" +lxc exec $name -- sudo add-apt-repository ppa:ua-client/staging -y > /dev/null +lxc exec $name -- sudo apt-get update > /dev/null +lxc exec $name -- sudo apt-get upgrade -y > /dev/null +echo "###########################################" +lxc exec $name -- apt-cache policy ubuntu-advantage-tools +echo -e "###########################################\n" + +# Only see a warning when creating the bug report +echo -e "\n* Only a warning in collect-logs" +echo "###########################################" +lxc exec $name -- pro collect-logs +echo -e "###########################################\n" + +echo -e "\n* Only a warning in apport-bug" +echo "###########################################" +lxc exec $name -- env APPORT_DISABLE_DISTRO_CHECK=1 ubuntu-bug --save=/tmp/test2 ubuntu-advantage-tools +echo -e "###########################################\n" + +cleanup diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/sru/release-27.11.3/test-apt-news-disable.sh ubuntu-advantage-tools-27.12~22.04.1/sru/release-27.11.3/test-apt-news-disable.sh --- ubuntu-advantage-tools-27.11.3~22.04.1/sru/release-27.11.3/test-apt-news-disable.sh 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/sru/release-27.11.3/test-apt-news-disable.sh 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +series=$1 +name=$series-dev + + +function cleanup { + lxc delete $name --force +} + +function on_err { + echo -e "Test Failed" + cleanup + exit 1 +} + +trap on_err ERR + +lxc launch ubuntu-daily:$series $name +sleep 10 + +# Update ubuntu-advantage-tools package +lxc exec $name -- sudo apt-get update > /dev/null +lxc exec $name -- sudo apt-get upgrade -y > /dev/null +lxc exec $name -- apt-cache policy ubuntu-advantage-tools + +# Creating apt new message +echo -e "\n* Ubuntu Pro message on apt upgrade" +echo "###########################################" +lxc exec $name -- sudo pro refresh messages +lxc exec $name -- sudo apt upgrade -y +echo -e "###########################################\n" + +echo -e "\n* Ubuntu Pro message also on apt-get upgrade" +echo "###########################################" +lxc exec $name -- sudo apt-get upgrade -y +echo -e "###########################################\n" + +# Upgrading UA to new version +lxc exec $name -- sudo add-apt-repository ppa:ua-client/staging -y > /dev/null +lxc exec $name -- sudo apt-get update > /dev/null +lxc exec $name -- sudo apt-get upgrade -y > /dev/null +echo -e "\n* Upgrading UA to new version" +echo "###########################################" +lxc exec $name -- apt-cache policy ubuntu-advantage-tools +echo -e "###########################################\n" + +# Show updated apt news message +echo -e "\n* apt news messages instead of Ubuntu Pro advertisement" +echo "###########################################" +lxc exec $name -- sudo pro refresh messages +lxc exec $name -- sudo apt upgrade -y +echo -e "###########################################\n" + +echo -e "\n* Apt news message not on apt-get upgrade" +echo "###########################################" +lxc exec $name -- sudo apt-get upgrade -y +echo -e "###########################################\n" + +# Disabling apt news messages +lxc exec $name -- sudo pro config set apt_news=False + +# Show that apt news message is no longer on apt upgrade +echo -e "\n* Apt news message not in apt upgrade after disabling it" +echo "###########################################" +lxc exec $name -- sudo pro refresh messages +lxc exec $name -- sudo apt upgrade -y +echo -e "###########################################\n" + +# Enabling apt news messages +lxc exec $name -- sudo pro config set apt_news=True + +# Show apt news message is back +echo -e "\n* apt news messages are back after enabling it" +echo "###########################################" +lxc exec $name -- sudo pro refresh messages +lxc exec $name -- sudo apt upgrade -y +echo -e "###########################################\n" + +echo -e "\n* Apt news message still not on apt-get upgrade" +echo "###########################################" +lxc exec $name -- sudo apt-get upgrade -y +echo -e "###########################################\n" + +cleanup diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/systemd/ua-auto-attach.service ubuntu-advantage-tools-27.12~22.04.1/systemd/ua-auto-attach.service --- ubuntu-advantage-tools-27.11.3~22.04.1/systemd/ua-auto-attach.service 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/systemd/ua-auto-attach.service 2022-11-22 13:06:26.000000000 +0000 @@ -3,6 +3,9 @@ Before=cloud-config.service After=cloud-config.target +# Only run if not already attached +ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json + [Service] Type=oneshot ExecStart=/usr/bin/python3 /usr/lib/ubuntu-advantage/auto_attach.py diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/systemd/ubuntu-advantage.service ubuntu-advantage-tools-27.12~22.04.1/systemd/ubuntu-advantage.service --- ubuntu-advantage-tools-27.11.3~22.04.1/systemd/ubuntu-advantage.service 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/systemd/ubuntu-advantage.service 2022-11-22 13:06:26.000000000 +0000 @@ -1,20 +1,26 @@ -# This service only runs on GCP to enable auto-attaching to Ubuntu Advantage +# This service runs on GCP to enable auto-attaching to Ubuntu Advantage # services when an Ubuntu Pro license is added to a GCP machine. -# If you are uninterested in the (free for personal use) Ubuntu Advantage -# services, including security updates after standard EOL and kernel patching -# without rebooting, then you can safely stop and disable this service: +# It also serves as the retry service if an auto-attach fails and will +# retry for up to one month after the failed attempt. +# If you are uninterested in Ubuntu Pro services, then you can safely +# stop and disable this service: # sudo systemctl stop ubuntu-advantage.service # sudo systemctl disable ubuntu-advantage.service [Unit] -Description=Ubuntu Advantage GCP Auto Attach Daemon +Description=Ubuntu Pro Background Auto Attach Documentation=man:ubuntu-advantage https://ubuntu.com/advantage After=network.target network-online.target systemd-networkd.service ua-auto-attach.service cloud-config.service ubuntu-advantage-cloud-id-shim.service # Only run if not already attached ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json -# Only run on GCP -ConditionPathExists=/run/cloud-init/cloud-id-gce + +# This service has two modes: +# 1. GCP detect pro mode - only on GCP +# 2. auto-attach retry mode - only if ua-auto-attach.service fails +# The following conditions correspond to those two modes. +ConditionPathExists=|/run/cloud-init/cloud-id-gce +ConditionPathExists=|/run/ubuntu-advantage/flags/auto-attach-failed [Service] Type=notify diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/tools/create-lp-release-branches.sh ubuntu-advantage-tools-27.12~22.04.1/tools/create-lp-release-branches.sh --- ubuntu-advantage-tools-27.11.3~22.04.1/tools/create-lp-release-branches.sh 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/tools/create-lp-release-branches.sh 2022-11-22 13:06:26.000000000 +0000 @@ -56,7 +56,7 @@ jammy) version=${UA_VERSION}~22.04.1;; kinetic) version=${UA_VERSION}~22.10.1;; esac - dch_cmd=(dch -v "${version}" -D "${release}" -b "Backport new upstream release: (LP: #${SRU_BUG}) to $release") + dch_cmd=(dch -m -v "${version}" -D "${release}" -b "Backport new upstream release: (LP: #${SRU_BUG}) to $release") if [ -z "$DO_IT" ]; then echo "${dch_cmd[@]}" else diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/tox.ini ubuntu-advantage-tools-27.12~22.04.1/tox.ini --- ubuntu-advantage-tools-27.11.3~22.04.1/tox.ini 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/tox.ini 2022-11-22 13:06:26.000000000 +0000 @@ -55,10 +55,10 @@ commands = test: py.test --junitxml=pytest_results.xml {posargs:--cov uaclient uaclient} flake8: flake8 uaclient lib setup.py features - mypy: mypy --python-version 3.5 uaclient/ features/ lib/ - mypy: mypy --python-version 3.6 uaclient/ features/ lib/ - mypy: mypy --python-version 3.8 uaclient/ features/ lib/ - mypy: mypy --python-version 3.10 uaclient/ features/ lib/ + mypy: mypy --explicit-package-bases --python-version 3.5 uaclient/ features/ lib/ + mypy: mypy --explicit-package-bases --python-version 3.6 uaclient/ features/ lib/ + mypy: mypy --explicit-package-bases --python-version 3.8 uaclient/ features/ lib/ + mypy: mypy --explicit-package-bases --python-version 3.10 uaclient/ features/ lib/ black: black --check --diff uaclient/ features/ lib/ setup.py isort: isort --check --diff uaclient/ features/ lib/ setup.py shellcheck: bash -O extglob -O nullglob -c "shellcheck -S warning tools/{*.sh,make-release,make-tarball} debian/*.{config,postinst,postrm,prerm} lib/*.sh sru/*.sh update-motd.d/*" diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/actions.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/actions.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/actions.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/actions.py 2022-11-22 13:06:26.000000000 +0000 @@ -85,16 +85,9 @@ auto-attach support. """ contract_client = contract.UAContractClient(cfg) - try: - tokenResponse = contract_client.request_auto_attach_contract_token( - instance=cloud - ) - except exceptions.ContractAPIError as e: - if e.code and 400 <= e.code < 500: - raise exceptions.NonAutoAttachImageError( - messages.UNSUPPORTED_AUTO_ATTACH - ) - raise e + tokenResponse = contract_client.request_auto_attach_contract_token( + instance=cloud + ) token = tokenResponse["contractToken"] @@ -149,7 +142,7 @@ def _write_command_output_to_file( - cmd, filename: str, return_codes: List[int] = None + cmd, filename: str, return_codes: Optional[List[int]] = None ) -> None: """Helper which runs a command and writes output or error to filename.""" try: @@ -238,18 +231,6 @@ ) -def should_disable_auto_attach(cfg: config.UAConfig) -> bool: - disable_auto_attach = util.is_config_value_true( - config=cfg.cfg, path_to_value="features.disable_auto_attach" - ) - if disable_auto_attach: - msg = "Skipping auto-attach. Config disable_auto_attach is set." - logging.debug(msg) - print(msg) - - return disable_auto_attach - - def get_cloud_instance( cfg: config.UAConfig, ) -> AutoAttachCloudInstance: @@ -257,9 +238,6 @@ try: instance = identity.cloud_instance_factory() except exceptions.CloudFactoryError as e: - if cfg.is_attached: - # We are attached on non-Pro Image, just report already attached - raise exceptions.AlreadyAttachedError(cfg) if isinstance(e, exceptions.CloudFactoryNoCloudError): raise exceptions.UserFacingError( messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/api.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/api.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/api.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/api.py 2022-11-22 13:06:26.000000000 +0000 @@ -16,12 +16,18 @@ from uaclient.version import check_for_new_version VALID_ENDPOINTS = [ - "u.pro.version.v1", + "u.pro.attach.auto.configure_retry_service.v1", + "u.pro.attach.auto.full_auto_attach.v1", + "u.pro.attach.auto.should_auto_attach.v1", "u.pro.attach.magic.initiate.v1", - "u.pro.attach.magic.wait.v1", "u.pro.attach.magic.revoke.v1", - "u.pro.attach.auto.should_auto_attach.v1", - "u.pro.attach.auto.full_auto_attach.v1", + "u.pro.attach.magic.wait.v1", + "u.pro.packages.summary.v1", + "u.pro.packages.updates.v1", + "u.pro.security.status.livepatch_cves.v1", + "u.pro.security.status.reboot_required.v1", + "u.pro.version.v1", + "u.security.package_manifest.v1", ] diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/exceptions.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/exceptions.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/exceptions.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/exceptions.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,9 +1,13 @@ +from typing import List, Tuple + from uaclient import messages from uaclient.exceptions import ( AlreadyAttachedError, - BetaServiceError, + ConnectivityError, ContractAPIError, EntitlementNotFoundError, + InvalidProImage, + LockHeldError, NonAutoAttachImageError, UrlError, UserFacingError, @@ -11,30 +15,35 @@ __all__ = [ "AlreadyAttachedError", - "BetaServiceError", + "ConnectivityError", "ContractAPIError", "EntitlementNotFoundError", + "InvalidProImage", + "LockHeldError", "NonAutoAttachImageError", "UrlError", "UserFacingError", ] -class EntitlementNotEnabledError(UserFacingError): - """An exception raised when enabling of an entitlement fails""" - - pass - +class EntitlementsNotEnabledError(UserFacingError): + def __init__( + self, failed_services: List[Tuple[str, messages.NamedMessage]] + ): + info_dicts = [ + {"name": f[0], "code": f[1].name, "title": f[1].msg} + for f in failed_services + ] + super().__init__( + messages.ENTITLEMENTS_NOT_ENABLED_ERROR.msg, + messages.ENTITLEMENTS_NOT_ENABLED_ERROR.name, + additional_info={"services": info_dicts}, + ) -class FullAutoAttachFailureError(UserFacingError): - """An exception raised when auto attach at boot fails""" +class AutoAttachDisabledError(UserFacingError): def __init__(self): super().__init__( - msg=messages.FULL_AUTO_ATTACH_ERROR.msg, - msg_code=messages.FULL_AUTO_ATTACH_ERROR.name, + messages.AUTO_ATTACH_DISABLED_ERROR.msg, + messages.AUTO_ATTACH_DISABLED_ERROR.name, ) - - -class IncompatibleEntitlementsDetected(UserFacingError): - pass diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api.py 2022-11-22 13:06:26.000000000 +0000 @@ -137,7 +137,7 @@ @mock.patch("uaclient.api.api.check_for_new_version", return_value=None) @mock.patch("uaclient.api.api.import_module") def test_warning_on_extra_args( - self, m_import_module, _m_new_version, FakeConfig + self, m_import_module, _m_new_version_api, FakeConfig ): mock_endpoint = mock.MagicMock() mock_endpoint.fn.return_value.warnings = [] diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_configure_retry_service.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_configure_retry_service.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_configure_retry_service.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_configure_retry_service.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,60 @@ +import mock +import pytest + +from uaclient.api.u.pro.attach.auto.configure_retry_service.v1 import ( + ConfigureRetryServiceOptions, + _configure_retry_service, +) +from uaclient.daemon import retry_auto_attach +from uaclient.files import state_files + +M_PATH = "uaclient.api.u.pro.attach.auto.configure_retry_service.v1." + + +@mock.patch(M_PATH + "system.create_file") +@mock.patch(M_PATH + "state_files.retry_auto_attach_options_file.write") +class TestConfigureRetryServiceV1: + @pytest.mark.parametrize( + "options, expected_options_write_calls, expected_create_file_calls", + [ + ( + ConfigureRetryServiceOptions(), + [mock.call(state_files.RetryAutoAttachOptions())], + [mock.call(retry_auto_attach.FLAG_FILE_PATH)], + ), + ( + ConfigureRetryServiceOptions(enable=["cis"]), + [ + mock.call( + state_files.RetryAutoAttachOptions(enable=["cis"]) + ) + ], + [mock.call(retry_auto_attach.FLAG_FILE_PATH)], + ), + ( + ConfigureRetryServiceOptions( + enable=["cis"], enable_beta=["esm-infra"] + ), + [ + mock.call( + state_files.RetryAutoAttachOptions( + enable=["cis"], enable_beta=["esm-infra"] + ) + ) + ], + [mock.call(retry_auto_attach.FLAG_FILE_PATH)], + ), + ], + ) + def test_configure_retry_service( + self, + m_options_write, + m_create_file, + options, + expected_options_write_calls, + expected_create_file_calls, + FakeConfig, + ): + _configure_retry_service(options, FakeConfig()) + assert expected_options_write_calls == m_options_write.call_args_list + assert expected_create_file_calls == m_create_file.call_args_list diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,378 @@ +import mock +import pytest + +from uaclient import event_logger, messages +from uaclient.api import exceptions +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, + FullAutoAttachResult, + _enable_services_by_name, + _full_auto_attach, + _full_auto_attach_in_lock, +) +from uaclient.entitlements.entitlement_status import ( + CanEnableFailure, + CanEnableFailureReason, +) +from uaclient.testing.helpers import does_not_raise + +M_PATH = "uaclient.api.u.pro.attach.auto.full_auto_attach.v1." + + +class TestEnableServicesByName: + @pytest.mark.parametrize( + [ + "services", + "allow_beta", + "enable_side_effect", + "expected_enable_call_args", + "expected_ret", + ], + [ + # success one service, allow beta + ( + ["esm-infra"], + True, + [(True, None)], + [ + mock.call( + mock.ANY, "esm-infra", assume_yes=True, allow_beta=True + ) + ], + [], + ), + # success multi service, no allow beta + ( + ["esm-apps", "esm-infra", "livepatch"], + False, + [(True, None), (True, None), (True, None)], + [ + mock.call( + mock.ANY, "esm-apps", assume_yes=True, allow_beta=False + ), + mock.call( + mock.ANY, + "esm-infra", + assume_yes=True, + allow_beta=False, + ), + mock.call( + mock.ANY, + "livepatch", + assume_yes=True, + allow_beta=False, + ), + ], + [], + ), + # fail via user facing error + ( + ["esm-apps", "esm-infra", "livepatch"], + False, + [ + (True, None), + exceptions.UserFacingError("msg"), + exceptions.UserFacingError("msg", "code"), + ], + [ + mock.call( + mock.ANY, "esm-apps", assume_yes=True, allow_beta=False + ), + mock.call( + mock.ANY, + "esm-infra", + assume_yes=True, + allow_beta=False, + ), + mock.call( + mock.ANY, + "livepatch", + assume_yes=True, + allow_beta=False, + ), + ], + [ + ("esm-infra", messages.NamedMessage("unknown", "msg")), + ("livepatch", messages.NamedMessage("code", "msg")), + ], + ), + # fail via return + ( + ["esm-apps", "esm-infra", "livepatch"], + False, + [ + (True, None), + (False, None), + ( + False, + CanEnableFailure( + CanEnableFailureReason.ALREADY_ENABLED, + messages.NamedMessage("test", "test"), + ), + ), + ], + [ + mock.call( + mock.ANY, "esm-apps", assume_yes=True, allow_beta=False + ), + mock.call( + mock.ANY, + "esm-infra", + assume_yes=True, + allow_beta=False, + ), + mock.call( + mock.ANY, + "livepatch", + assume_yes=True, + allow_beta=False, + ), + ], + [ + ( + "esm-infra", + messages.NamedMessage("unknown", "failed to enable"), + ), + ("livepatch", messages.NamedMessage("test", "test")), + ], + ), + ], + ) + @mock.patch(M_PATH + "actions.enable_entitlement_by_name") + def test_enable_services_by_name( + self, + m_enable_entitlement_by_name, + services, + allow_beta, + enable_side_effect, + expected_enable_call_args, + expected_ret, + ): + m_enable_entitlement_by_name.side_effect = enable_side_effect + ret = _enable_services_by_name(mock.MagicMock(), services, allow_beta) + assert ( + m_enable_entitlement_by_name.call_args_list + == expected_enable_call_args + ) + assert ret == expected_ret + + +class TestFullAutoAttachV1: + @mock.patch( + "uaclient.actions.enable_entitlement_by_name", + ) + @mock.patch("uaclient.actions.get_cloud_instance") + @mock.patch("uaclient.actions.auto_attach") + def test_error_invalid_ent_names( + self, + _auto_attach, + _get_cloud_instance, + m_enable_ent_by_name, + FakeConfig, + ): + cfg = FakeConfig(root_mode=True) + + def enable_ent_side_effect(cfg, name, assume_yes, allow_beta): + if name != "wrong": + return (True, None) + + return (False, None) + + m_enable_ent_by_name.side_effect = enable_ent_side_effect + options = FullAutoAttachOptions( + enable=["esm-infra", "cis"], + enable_beta=["esm-apps", "realtime-kernel", "wrong"], + ) + with pytest.raises(exceptions.EntitlementsNotEnabledError): + _full_auto_attach(options, cfg) + + assert 5 == m_enable_ent_by_name.call_count + + @mock.patch( + "uaclient.actions.enable_entitlement_by_name", + return_value=(False, None), + ) + @mock.patch("uaclient.actions.get_cloud_instance") + @mock.patch("uaclient.actions.auto_attach") + def test_error_full_auto_attach_fail( + self, + _auto_attach, + _get_cloud_instance, + enable_ent_by_name, + FakeConfig, + ): + cfg = FakeConfig(root_mode=True) + options = FullAutoAttachOptions( + enable=["esm-infra", "fips"], + enable_beta=["esm-apps", "ros"], + ) + with pytest.raises(exceptions.EntitlementsNotEnabledError): + _full_auto_attach(options, cfg) + + assert 4 == enable_ent_by_name.call_count + + @mock.patch( + "uaclient.lock.SpinLock.__enter__", + side_effect=[ + exceptions.LockHeldError("request", "holder", 10), + ], + ) + def test_lock_held(self, _m_spinlock_enter, FakeConfig): + with pytest.raises(exceptions.LockHeldError): + _full_auto_attach(FullAutoAttachOptions, FakeConfig()) + + @pytest.mark.parametrize( + "mode", + list(map(lambda e: e.value, event_logger.EventLoggerMode)), + ) + @pytest.mark.parametrize( + [ + "options", + "is_attached", + "is_disabled", + "expected_auto_attach_call_args", + "enable_services_by_name_side_effect", + "expected_enable_services_by_name_call_args", + "raise_expectation", + "expected_error_message", + "expected_ret", + ], + [ + # already attached + ( + FullAutoAttachOptions(), + True, + False, + [], + [], + [], + pytest.raises(exceptions.AlreadyAttachedError), + messages.ALREADY_ATTACHED.format( + account_name="test_account" + ).msg, + None, + ), + # disable_auto_attach: true + ( + FullAutoAttachOptions(), + False, + True, + [], + [], + [], + pytest.raises(exceptions.AutoAttachDisabledError), + messages.AUTO_ATTACH_DISABLED_ERROR.msg, + None, + ), + # success no options + ( + FullAutoAttachOptions(), + False, + False, + [mock.call(mock.ANY, mock.ANY, allow_enable=True)], + [], + [], + does_not_raise(), + None, + FullAutoAttachResult(), + ), + # success enable + ( + FullAutoAttachOptions(enable=["cis"]), + False, + False, + [mock.call(mock.ANY, mock.ANY, allow_enable=False)], + [[]], + [mock.call(mock.ANY, ["cis"], allow_beta=False)], + does_not_raise(), + None, + FullAutoAttachResult(), + ), + # success enable_beta + ( + FullAutoAttachOptions(enable_beta=["cis"]), + False, + False, + [mock.call(mock.ANY, mock.ANY, allow_enable=False)], + [[]], + [mock.call(mock.ANY, ["cis"], allow_beta=True)], + does_not_raise(), + None, + FullAutoAttachResult(), + ), + # success enable and enable_beta + ( + FullAutoAttachOptions(enable=["fips"], enable_beta=["cis"]), + False, + False, + [mock.call(mock.ANY, mock.ANY, allow_enable=False)], + [[], []], + [ + mock.call(mock.ANY, ["fips"], allow_beta=False), + mock.call(mock.ANY, ["cis"], allow_beta=True), + ], + does_not_raise(), + None, + FullAutoAttachResult(), + ), + # fail to enable + ( + FullAutoAttachOptions(enable=["fips"], enable_beta=["cis"]), + False, + False, + [mock.call(mock.ANY, mock.ANY, allow_enable=False)], + [ + [("fips", messages.NamedMessage("one", "two"))], + [("cis", messages.NamedMessage("three", "four"))], + ], + [ + mock.call(mock.ANY, ["fips"], allow_beta=False), + mock.call(mock.ANY, ["cis"], allow_beta=True), + ], + pytest.raises(exceptions.EntitlementsNotEnabledError), + messages.ENTITLEMENTS_NOT_ENABLED_ERROR.msg, + None, + ), + ], + ) + @mock.patch(M_PATH + "_enable_services_by_name") + @mock.patch(M_PATH + "actions.auto_attach") + @mock.patch(M_PATH + "actions.get_cloud_instance") + @mock.patch(M_PATH + "util.is_config_value_true") + def test_full_auto_attach_v1( + self, + m_is_config_value_true, + m_get_cloud_instance, + m_auto_attach, + m_enable_services_by_name, + options, + is_attached, + is_disabled, + expected_auto_attach_call_args, + enable_services_by_name_side_effect, + expected_enable_services_by_name_call_args, + raise_expectation, + expected_error_message, + expected_ret, + mode, + FakeConfig, + ): + if is_attached: + cfg = FakeConfig.for_attached_machine() + else: + cfg = FakeConfig() + m_is_config_value_true.return_value = is_disabled + m_enable_services_by_name.side_effect = ( + enable_services_by_name_side_effect + ) + with raise_expectation as e: + ret = _full_auto_attach_in_lock(options, cfg, mode) + assert m_auto_attach.call_args_list == expected_auto_attach_call_args + assert ( + m_enable_services_by_name.call_args_list + == expected_enable_services_by_name_call_args + ) + if expected_error_message is not None: + assert e.value.msg == expected_error_message + if expected_ret is not None: + assert ret == expected_ret diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_should_auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_should_auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_should_auto_attach.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_should_auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -18,7 +18,7 @@ ), ) @mock.patch(M_PATH + ".cloud_instance_factory") - @mock.patch("uaclient.apt.get_installed_packages") + @mock.patch("uaclient.apt.get_installed_packages_names") def test_detect_is_pro( self, m_get_installed_pkgs, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_packages_summary.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_packages_summary.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_packages_summary.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_packages_summary.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,37 @@ +import mock + +from uaclient.api.u.pro.packages.summary.v1 import ( + PackageSummaryResult, + _summary, +) + +M_PATH = "uaclient.api.u.pro.packages.summary.v1." + + +@mock.patch(M_PATH + "get_installed_packages_by_origin") +class TestPackagesSummaryV1: + def test_package_summary(self, m_packages, FakeConfig): + m_packages.return_value = { + "all": ["pkg"], + "esm-apps": ["pkg"] * 2, + "esm-infra": ["pkg"] * 3, + "main": ["pkg"] * 4, + "multiverse": ["pkg"] * 5, + "restricted": ["pkg"] * 6, + "third-party": ["pkg"] * 7, + "universe": ["pkg"] * 8, + "unknown": ["pkg"] * 9, + } + + result = _summary(cfg=FakeConfig()) + + assert isinstance(result, PackageSummaryResult) + assert result.summary.num_installed_packages == 1 + assert result.summary.num_esm_apps_packages == 2 + assert result.summary.num_esm_infra_packages == 3 + assert result.summary.num_main_packages == 4 + assert result.summary.num_multiverse_packages == 5 + assert result.summary.num_restricted_packages == 6 + assert result.summary.num_third_party_packages == 7 + assert result.summary.num_universe_packages == 8 + assert result.summary.num_unknown_packages == 9 diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_packages_updates.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_packages_updates.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_packages_updates.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_packages_updates.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,52 @@ +import mock + +from uaclient.api.u.pro.packages.updates.v1 import ( + PackageUpdatesResult, + _updates, +) + +M_PATH = "uaclient.api.u.pro.packages.updates.v1." + + +class TestPackagesUpdatesV1: + @mock.patch(M_PATH + "get_ua_info") + @mock.patch(M_PATH + "get_installed_packages_by_origin") + @mock.patch(M_PATH + "filter_security_updates") + @mock.patch(M_PATH + "create_updates_list") + def test_package_updates( + self, m_updates, m_filter, _m_packages, _m_ua_info, FakeConfig + ): + m_filter.return_value = { + "esm-apps": ["update"], + "esm-infra": ["update"] * 2, + "standard-security": ["update"] * 3, + "standard-updates": ["update"] * 4, + } + m_updates.return_value = [ + { + "download_size": 123, + "origin": "somewhere", + "package": "pkg", + "service_name": "service", + "status": "status", + "version": "version", + } + ] + + result = _updates(cfg=FakeConfig()) + + assert isinstance(result, PackageUpdatesResult) + + assert result.summary.num_esm_apps_updates == 1 + assert result.summary.num_esm_infra_updates == 2 + assert result.summary.num_standard_security_updates == 3 + assert result.summary.num_standard_updates == 4 + assert result.summary.num_updates == 10 + + assert len(result.updates) == 1 + assert result.updates[0].download_size == 123 + assert result.updates[0].origin == "somewhere" + assert result.updates[0].package == "pkg" + assert result.updates[0].provided_by == "service" + assert result.updates[0].status == "status" + assert result.updates[0].version == "version" diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_get_package_manifest.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_get_package_manifest.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_get_package_manifest.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_get_package_manifest.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,71 @@ +import mock + +from uaclient import apt +from uaclient.api.u.security.package_manifest.v1 import _package_manifest + +M_PATH = "uaclient.api.u.security.package_manifest.v1" + + +@mock.patch("uaclient.snap.system.subp") +@mock.patch(M_PATH + ".apt.get_installed_packages") +class TestPackageInstalledV1: + def test_snap_packages_added( + self, installed_apt_pkgs, sys_subp, FakeConfig + ): + installed_apt_pkgs.return_value = [] + sys_subp.return_value = ( + "Name Version Rev Tracking Publisher Notes\n" + "helloworld 6.0.16 126 latest/stable dev1 -\n" + "bare 1.0 5 latest/stable canonical** base\n" + "canonical-livepatch 10.2.3 146 latest/stable canonical** -\n" + ), "" + result = _package_manifest(FakeConfig()) + assert ( + "snap:helloworld\tlatest/stable\t126\n" + + "snap:bare\tlatest/stable\t5\n" + + "snap:canonical-livepatch\tlatest/stable\t146\n" + == result.manifest_data + ) + + def test_apt_packages_added( + self, installed_apt_pkgs, sys_subp, FakeConfig + ): + sys_subp.return_value = "", "" + apt_pkgs = [ + apt.InstalledAptPackages( + name="one", arch="all", version="4:1.0.2" + ), + apt.InstalledAptPackages( + name="two", arch="amd64", version="0.1.1" + ), + ] + installed_apt_pkgs.return_value = apt_pkgs + result = _package_manifest(FakeConfig()) + assert "one\t4:1.0.2\ntwo:amd64\t0.1.1\n" == result.manifest_data + + def test_apt_snap_packages_added( + self, installed_apt_pkgs, sys_subp, FakeConfig + ): + apt_pkgs = [ + apt.InstalledAptPackages( + name="one", arch="all", version="4:1.0.2" + ), + apt.InstalledAptPackages( + name="two", arch="amd64", version="0.1.1" + ), + ] + sys_subp.return_value = ( + "Name Version Rev Tracking Publisher Notes\n" + "helloworld 6.0.16 126 latest/stable dev1 -\n" + "bare 1.0 5 latest/stable canonical** base\n" + "canonical-livepatch 10.2.3 146 latest/stable canonical** -\n" + ), "" + installed_apt_pkgs.return_value = apt_pkgs + result = _package_manifest(FakeConfig()) + assert ( + "one\t4:1.0.2\ntwo:amd64\t0.1.1\n" + + "snap:helloworld\tlatest/stable\t126\n" + + "snap:bare\tlatest/stable\t5\n" + + "snap:canonical-livepatch\tlatest/stable\t146\n" + == result.manifest_data + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_livepatch_cves.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_livepatch_cves.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_livepatch_cves.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_livepatch_cves.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,30 @@ +import mock + +from uaclient.api.u.pro.security.status.livepatch_cves.v1 import ( + LivepatchCVEsResult, + _livepatch_cves, +) + +M_PATH = "uaclient.api.u.pro.security.status.livepatch_cves.v1." + + +@mock.patch(M_PATH + "get_livepatch_fixed_cves") +class TestLivepatchCvesV1: + def test_empty_livepatch_cves(self, m_cves, FakeConfig): + m_cves.return_value = [] + result = _livepatch_cves(cfg=FakeConfig()) + assert isinstance(result, LivepatchCVEsResult) + assert result.fixed_cves == [] + + def test_livepatch_cves(self, m_cves, FakeConfig): + m_cves.return_value = [ + {"name": "CVE-123456", "patched": True}, + {"name": "CVE-45678", "patched": False}, + ] + result = _livepatch_cves(cfg=FakeConfig()) + assert isinstance(result, LivepatchCVEsResult) + assert len(result.fixed_cves) == 2 + assert result.fixed_cves[0].name == "CVE-123456" + assert result.fixed_cves[0].patched is True + assert result.fixed_cves[1].name == "CVE-45678" + assert result.fixed_cves[1].patched is False diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_reboot_required_v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_reboot_required_v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_reboot_required_v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_api_u_pro_security_status_reboot_required_v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,27 @@ +import mock +import pytest + +from uaclient.api.u.pro.security.status.reboot_required.v1 import ( + _reboot_required, +) +from uaclient.security_status import RebootStatus + +PATH = "uaclient.api.u.pro.security.status.reboot_required.v1." + + +class TestRebootStatus: + @pytest.mark.parametrize( + "reboot_state", + ( + (RebootStatus.REBOOT_REQUIRED), + (RebootStatus.REBOOT_NOT_REQUIRED), + (RebootStatus.REBOOT_REQUIRED_LIVEPATCH_APPLIED), + ), + ) + @mock.patch(PATH + "get_reboot_status") + def test_reboot_status_api(self, m_get_reboot_status, reboot_state): + m_get_reboot_status.return_value = reboot_state + assert ( + reboot_state.value + == _reboot_required(mock.MagicMock()).reboot_required + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_u_pro_attach_auto_full_auto_attach_v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_u_pro_attach_auto_full_auto_attach_v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/tests/test_u_pro_attach_auto_full_auto_attach_v1.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/tests/test_u_pro_attach_auto_full_auto_attach_v1.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,161 +0,0 @@ -import mock -import pytest - -from uaclient.api import exceptions -from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( - FullAutoAttachOptions, - _full_auto_attach, - _is_incompatible_services_present, -) -from uaclient.entitlements.entitlement_status import ( - CanEnableFailure, - CanEnableFailureReason, -) -from uaclient.messages import NamedMessage - -M_API = "uaclient.api.u.pro." - - -class TestFullAutoAttachV1: - @mock.patch( - "uaclient.actions.enable_entitlement_by_name", - return_value=(True, None), - ) - def test_error_when_beta_in_enable_list( - self, - enable_ent_by_name, - FakeConfig, - ): - cfg = FakeConfig(root_mode=True) - options = FullAutoAttachOptions( - enable=["esm-infra", "realtime-kernel"] - ) - with pytest.raises(exceptions.BetaServiceError): - _full_auto_attach(options, cfg) - - assert 0 == enable_ent_by_name.call_count - - @mock.patch( - "uaclient.actions.enable_entitlement_by_name", - return_value=(True, None), - ) - @mock.patch("uaclient.actions.get_cloud_instance") - @mock.patch("uaclient.actions.auto_attach") - def test_error_invalid_ent_names( - self, - _auto_attach, - _get_cloud_instance, - enable_ent_by_name, - FakeConfig, - ): - cfg = FakeConfig(root_mode=True) - options = FullAutoAttachOptions( - enable=["esm-infra", "cis"], - enable_beta=["esm-apps", "realtime-kernel", "test", "wrong"], - ) - with pytest.raises(exceptions.EntitlementNotFoundError): - _full_auto_attach(options, cfg) - - assert 0 == enable_ent_by_name.call_count - - @mock.patch( - "uaclient.actions.enable_entitlement_by_name", - return_value=( - False, - CanEnableFailure( - CanEnableFailureReason.ALREADY_ENABLED, - NamedMessage("test", "test"), - ), - ), - ) - @mock.patch("uaclient.actions.get_cloud_instance") - @mock.patch("uaclient.actions.auto_attach") - def test_error_ent_not_enabled( - self, - _auto_attach, - _get_cloud_instance, - enable_ent_by_name, - FakeConfig, - ): - cfg = FakeConfig(root_mode=True) - options = FullAutoAttachOptions( - enable=["esm-infra", "cis"], - enable_beta=["esm-apps", "realtime-kernel"], - ) - with pytest.raises(exceptions.EntitlementNotEnabledError): - _full_auto_attach(options, cfg) - - assert 1 == enable_ent_by_name.call_count - - @mock.patch( - "uaclient.actions.enable_entitlement_by_name", - return_value=(False, None), - ) - @mock.patch("uaclient.actions.get_cloud_instance") - @mock.patch("uaclient.actions.auto_attach") - def test_error_full_auto_attach_fail( - self, - _auto_attach, - _get_cloud_instance, - enable_ent_by_name, - FakeConfig, - ): - cfg = FakeConfig(root_mode=True) - options = FullAutoAttachOptions( - enable=["esm-infra", "fips"], - enable_beta=["esm-apps", "ros"], - ) - with pytest.raises(exceptions.EntitlementNotEnabledError): - _full_auto_attach(options, cfg) - - assert 1 == enable_ent_by_name.call_count - - def test_error_incompatible_services( - self, - FakeConfig, - ): - cfg = FakeConfig(root_mode=True) - options = FullAutoAttachOptions( - enable=["esm-infra", "fips"], - enable_beta=["esm-apps", "livepatch"], - ) - with pytest.raises(exceptions.IncompatibleEntitlementsDetected) as e: - _full_auto_attach(options, cfg) - - expected_info = { - "service": "fips", - "incompatible_services": "livepatch,fips-updates,realtime-kernel", - } - - assert expected_info == e.value.additional_info - - @pytest.mark.parametrize( - "ent_list,service,incompatible_services,detected", - ( - ( - ["fips", "esm-infra", "fips-updates", "esm-apps"], - "fips", - ["livepatch", "fips-updates", "realtime-kernel"], - True, - ), - ( - ["fips-updates", "esm-infra", "realtime-kernel", "esm-apps"], - "fips-updates", - ["fips", "realtime-kernel"], - True, - ), - ( - ["livepatch", "esm-infra", "esm-apps"], - "", - [], - False, - ), - ), - ) - def test_incompatible_services( - self, ent_list, service, incompatible_services, detected, FakeConfig - ): - res = _is_incompatible_services_present(FakeConfig(), ent_list) - assert detected == res[0] - assert service == res[1] - assert incompatible_services == res[2] diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/attach/auto/configure_retry_service/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/attach/auto/configure_retry_service/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/attach/auto/configure_retry_service/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/attach/auto/configure_retry_service/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,42 @@ +from uaclient import system +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, +) +from uaclient.config import UAConfig +from uaclient.daemon import retry_auto_attach +from uaclient.data_types import DataObject +from uaclient.files import state_files + +ConfigureRetryServiceOptions = FullAutoAttachOptions + + +class ConfigureRetryServiceResult(DataObject, AdditionalInfo): + pass + + +def configure_retry_service( + options: ConfigureRetryServiceOptions, +) -> ConfigureRetryServiceResult: + return _configure_retry_service(options, UAConfig()) + + +def _configure_retry_service( + options: ConfigureRetryServiceOptions, cfg: UAConfig +) -> ConfigureRetryServiceResult: + state_files.retry_auto_attach_options_file.write( + state_files.RetryAutoAttachOptions( + enable=options.enable, enable_beta=options.enable_beta + ) + ) + system.create_file(retry_auto_attach.FLAG_FILE_PATH) + return ConfigureRetryServiceResult() + + +endpoint = APIEndpoint( + version="v1", + name="ConfigureRetryService", + fn=_configure_retry_service, + options_cls=ConfigureRetryServiceOptions, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,13 +1,12 @@ -from typing import List, Optional, Tuple, Type # noqa: F401 +from typing import List, Optional, Tuple -from uaclient import actions, entitlements, event_logger +from uaclient import actions, event_logger, lock, messages, util from uaclient.api import exceptions from uaclient.api.api import APIEndpoint from uaclient.api.data_types import AdditionalInfo -from uaclient.clouds import AutoAttachCloudInstance # noqa: F401 from uaclient.config import UAConfig from uaclient.data_types import DataObject, Field, StringDataValue, data_list -from uaclient.entitlements.base import UAEntitlement # noqa: F401 +from uaclient.entitlements import order_entitlements_for_enabling from uaclient.entitlements.entitlement_status import CanEnableFailure event = event_logger.get_event_logger() @@ -32,111 +31,94 @@ pass -def _is_any_beta(cfg: UAConfig, ents: List[str]) -> Tuple[bool, str]: - for name in ents: +def _enable_services_by_name( + cfg: UAConfig, services: List[str], allow_beta: bool +) -> List[Tuple[str, messages.NamedMessage]]: + failed_services = [] + for name in order_entitlements_for_enabling(cfg, services): try: - ent_cls = entitlements.entitlement_factory(cfg, name) - if ent_cls.is_beta: - return True, name - except exceptions.EntitlementNotFoundError: + ent_ret, reason = actions.enable_entitlement_by_name( + cfg, name, assume_yes=True, allow_beta=allow_beta + ) + except exceptions.UserFacingError as e: + failed_services.append( + (name, messages.NamedMessage(e.msg_code or "unknown", e.msg)) + ) continue - return False, "" - - -def _is_incompatible_services_present( - cfg, ent_list: List[str] -) -> Tuple[bool, str, List[str]]: - ent_cls = list() # type: List[Type[UAEntitlement]] - for e in ent_list: - ent_cls.append(entitlements.entitlement_factory(cfg, e)) - - for ent in ent_cls: - ent_inst = ent(cfg) - if ent_inst.incompatible_services: - incompat = [ - service.entitlement - for service in ent_inst.incompatible_services - ] - if any(e in incompat for e in ent_cls): - return True, ent_inst.name, [e(cfg).name for e in incompat] - return False, "", [] + if not ent_ret: + if ( + reason is not None + and isinstance(reason, CanEnableFailure) + and reason.message is not None + ): + failed_services.append((name, reason.message)) + else: + failed_services.append( + ( + name, + messages.NamedMessage("unknown", "failed to enable"), + ) + ) + return failed_services def full_auto_attach(options: FullAutoAttachOptions) -> FullAutoAttachResult: return _full_auto_attach(options, UAConfig(root_mode=True)) -def _full_auto_attach(options: FullAutoAttachOptions, cfg: UAConfig): - event.set_event_mode(event_logger.EventLoggerMode.JSON) - - services = set() - if options.enable: - is_beta_found, service = _is_any_beta(cfg, options.enable) - if is_beta_found: - raise exceptions.BetaServiceError( - msg="beta service found in the enable list", - msg_code="beta-service-found", - additional_info={"beta_service": service}, - ) - services.update(options.enable) - if options.enable_beta: - services.update(options.enable_beta) - - service_list = list(services) - - found, not_found = entitlements.get_valid_entitlement_names( - service_list, cfg - ) - if not_found: - msg = entitlements.create_enable_entitlements_not_found_message( - not_found, cfg=cfg, allow_beta=True +def _full_auto_attach( + options: FullAutoAttachOptions, + cfg: UAConfig, + *, + mode: event_logger.EventLoggerMode = event_logger.EventLoggerMode.JSON +) -> FullAutoAttachResult: + try: + with lock.SpinLock( + cfg=cfg, + lock_holder="pro.api.u.pro.attach.auto.full_auto_attach.v1", + ): + ret = _full_auto_attach_in_lock(options, cfg, mode=mode) + except Exception as e: + lock.clear_lock_file_if_present() + raise e + return ret + + +def _full_auto_attach_in_lock( + options: FullAutoAttachOptions, + cfg: UAConfig, + mode: event_logger.EventLoggerMode, +) -> FullAutoAttachResult: + event.set_event_mode(mode) + + if cfg.is_attached: + raise exceptions.AlreadyAttachedError( + cfg.machine_token_file.account.get("name", "") ) - raise exceptions.EntitlementNotFoundError(msg.msg, not_found) - incompat_detected, ent, incompat_ents = _is_incompatible_services_present( - cfg, sorted(found) - ) # sort for easy testing exceptions raised - if incompat_detected: - err_msg = "{ent} is incompatible with any of these services {incompat}" - raise exceptions.IncompatibleEntitlementsDetected( - msg=err_msg.format(ent=ent, incompat=incompat_ents), - msg_code="incompatible-services-detected", - additional_info={ - "service": ent, - "incompatible_services": ",".join(incompat_ents), - }, - ) + if util.is_config_value_true( + config=cfg.cfg, path_to_value="features.disable_auto_attach" + ): + raise exceptions.AutoAttachDisabledError() instance = actions.get_cloud_instance(cfg) enable_default_services = ( options.enable is None and options.enable_beta is None ) - actions.auto_attach(cfg, instance, enable_default_services) - - if enable_default_services: - return FullAutoAttachResult() + actions.auto_attach(cfg, instance, allow_enable=enable_default_services) - for name in found: - ent_ret, reason = actions.enable_entitlement_by_name( - cfg, name, assume_yes=True, allow_beta=True + failed = [] + if options.enable is not None: + failed += _enable_services_by_name( + cfg, options.enable, allow_beta=False ) - if not ent_ret: - if ( - reason is not None - and isinstance(reason, CanEnableFailure) - and reason.message is not None - ): - raise exceptions.EntitlementNotEnabledError( - msg=reason.message.msg, - msg_code=reason.message.name, - additional_info={"service": name}, - ) - else: - raise exceptions.EntitlementNotEnabledError( - msg="Failed to enable service: {}".format(name), - msg_code="entitlement-not-enabled", - additional_info={"service": name}, - ) + if options.enable_beta is not None: + failed += _enable_services_by_name( + cfg, options.enable_beta, allow_beta=True + ) + + if len(failed) > 0: + raise exceptions.EntitlementsNotEnabledError(failed) return FullAutoAttachResult() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/packages/summary/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/packages/summary/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/packages/summary/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/packages/summary/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,76 @@ +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.config import UAConfig +from uaclient.data_types import DataObject, Field, IntDataValue +from uaclient.security_status import get_installed_packages_by_origin + + +class PackageSummary(DataObject): + fields = [ + Field("num_installed_packages", IntDataValue), + Field("num_esm_apps_packages", IntDataValue), + Field("num_esm_infra_packages", IntDataValue), + Field("num_main_packages", IntDataValue), + Field("num_multiverse_packages", IntDataValue), + Field("num_restricted_packages", IntDataValue), + Field("num_third_party_packages", IntDataValue), + Field("num_universe_packages", IntDataValue), + Field("num_unknown_packages", IntDataValue), + ] + + def __init__( + self, + num_installed_packages: int, + num_esm_apps_packages: int, + num_esm_infra_packages: int, + num_main_packages: int, + num_multiverse_packages: int, + num_restricted_packages: int, + num_third_party_packages: int, + num_universe_packages: int, + num_unknown_packages: int, + ): + self.num_installed_packages = num_installed_packages + self.num_esm_apps_packages = num_esm_apps_packages + self.num_esm_infra_packages = num_esm_infra_packages + self.num_main_packages = num_main_packages + self.num_multiverse_packages = num_multiverse_packages + self.num_restricted_packages = num_restricted_packages + self.num_third_party_packages = num_third_party_packages + self.num_universe_packages = num_universe_packages + self.num_unknown_packages = num_unknown_packages + + +class PackageSummaryResult(DataObject, AdditionalInfo): + fields = [Field("summary", PackageSummary)] + + def __init__(self, summary): + self.summary = summary + + +def summary() -> PackageSummaryResult: + return _summary(UAConfig()) + + +def _summary(cfg: UAConfig) -> PackageSummaryResult: + packages = get_installed_packages_by_origin() + summary = PackageSummary( + num_installed_packages=len(packages["all"]), + num_esm_apps_packages=len(packages["esm-apps"]), + num_esm_infra_packages=len(packages["esm-infra"]), + num_main_packages=len(packages["main"]), + num_multiverse_packages=len(packages["multiverse"]), + num_restricted_packages=len(packages["restricted"]), + num_third_party_packages=len(packages["third-party"]), + num_universe_packages=len(packages["universe"]), + num_unknown_packages=len(packages["unknown"]), + ) + return PackageSummaryResult(summary=summary) + + +endpoint = APIEndpoint( + version="v1", + name="PackageSummary", + fn=_summary, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/packages/updates/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/packages/updates/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/packages/updates/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/packages/updates/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,130 @@ +from typing import List + +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.config import UAConfig +from uaclient.data_types import ( + DataObject, + Field, + IntDataValue, + StringDataValue, + data_list, +) +from uaclient.security_status import ( + create_updates_list, + filter_security_updates, + get_installed_packages_by_origin, + get_ua_info, +) + + +class UpdateSummary(DataObject): + fields = [ + Field("num_updates", IntDataValue), + Field("num_esm_apps_updates", IntDataValue), + Field("num_esm_infra_updates", IntDataValue), + Field("num_standard_security_updates", IntDataValue), + Field("num_standard_updates", IntDataValue), + ] + + def __init__( + self, + num_updates: int, + num_esm_apps_updates: int, + num_esm_infra_updates: int, + num_standard_security_updates: int, + num_standard_updates: int, + ): + self.num_updates = num_updates + self.num_esm_apps_updates = num_esm_apps_updates + self.num_esm_infra_updates = num_esm_infra_updates + self.num_standard_security_updates = num_standard_security_updates + self.num_standard_updates = num_standard_updates + + +class UpdateInfo(DataObject): + fields = [ + Field("download_size", IntDataValue), + Field("origin", StringDataValue), + Field("package", StringDataValue), + Field("provided_by", StringDataValue), + Field("status", StringDataValue), + Field("version", StringDataValue), + ] + + def __init__( + self, + download_size: int, + origin: str, + package: str, + provided_by: str, + status: str, + version: str, + ): + self.download_size = download_size + self.origin = origin + self.package = package + self.provided_by = provided_by + self.status = status + self.version = version + + +class PackageUpdatesResult(DataObject, AdditionalInfo): + fields = [ + Field("summary", UpdateSummary), + Field("updates", data_list(UpdateInfo)), + ] + + def __init__(self, summary: UpdateSummary, updates: List[UpdateInfo]): + self.summary = summary + self.updates = updates + + +def updates() -> PackageUpdatesResult: + return _updates(UAConfig()) + + +def _updates(cfg: UAConfig) -> PackageUpdatesResult: + ua_info = get_ua_info(cfg) + packages = get_installed_packages_by_origin() + upgradable_versions = filter_security_updates(packages["all"]) + update_list = create_updates_list(upgradable_versions, ua_info) + + num_esm_apps_updates = len(upgradable_versions["esm-apps"]) + num_esm_infra_updates = len(upgradable_versions["esm-infra"]) + num_standard_security_updates = len( + upgradable_versions["standard-security"] + ) + num_standard_updates = len(upgradable_versions["standard-updates"]) + + summary = UpdateSummary( + num_updates=num_esm_apps_updates + + num_esm_infra_updates + + num_standard_security_updates + + num_standard_updates, + num_esm_apps_updates=num_esm_apps_updates, + num_esm_infra_updates=num_esm_infra_updates, + num_standard_security_updates=num_standard_security_updates, + num_standard_updates=num_standard_updates, + ) + updates = [ + UpdateInfo( + download_size=update["download_size"], + origin=update["origin"], + package=update["package"], + provided_by=update["service_name"], + status=update["status"], + version=update["version"], + ) + for update in update_list + ] + + return PackageUpdatesResult(summary=summary, updates=updates) + + +endpoint = APIEndpoint( + version="v1", + name="PackageUpdates", + fn=_updates, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/security/status/livepatch_cves/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/security/status/livepatch_cves/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/security/status/livepatch_cves/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/security/status/livepatch_cves/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,54 @@ +from typing import List + +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.config import UAConfig +from uaclient.data_types import ( + BoolDataValue, + DataObject, + Field, + StringDataValue, + data_list, +) +from uaclient.security_status import get_livepatch_fixed_cves + + +class LivepatchCVEObject(DataObject): + fields = [Field("name", StringDataValue), Field("patched", BoolDataValue)] + + def __init__(self, name: str, patched: bool): + self.name = name + self.patched = patched + + +class LivepatchCVEsResult(DataObject, AdditionalInfo): + fields = [ + Field("fixed_cves", data_list(LivepatchCVEObject)), + ] + + def __init__( + self, + fixed_cves: List[LivepatchCVEObject], + ): + self.fixed_cves = fixed_cves + + +def livepatch_cves() -> LivepatchCVEsResult: + return _livepatch_cves(UAConfig()) + + +def _livepatch_cves(cfg: UAConfig) -> LivepatchCVEsResult: + return LivepatchCVEsResult( + fixed_cves=[ + LivepatchCVEObject(name=cve["name"], patched=cve["patched"]) + for cve in get_livepatch_fixed_cves() + ] + ) + + +endpoint = APIEndpoint( + version="v1", + name="LivepatchCVEs", + fn=_livepatch_cves, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/security/status/reboot_required/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/security/status/reboot_required/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/pro/security/status/reboot_required/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/pro/security/status/reboot_required/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,34 @@ +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.config import UAConfig +from uaclient.data_types import DataObject, Field, StringDataValue +from uaclient.security_status import get_reboot_status + + +class RebootRequiredResult(DataObject, AdditionalInfo): + fields = [ + Field("reboot_required", StringDataValue), + ] + + def __init__( + self, + reboot_required: str, + ): + self.reboot_required = reboot_required + + +def reboot_required() -> RebootRequiredResult: + return _reboot_required(UAConfig()) + + +def _reboot_required(cfg: UAConfig) -> RebootRequiredResult: + status = get_reboot_status() + return RebootRequiredResult(reboot_required=status.value) + + +endpoint = APIEndpoint( + version="v1", + name="RebootRequired", + fn=_reboot_required, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/security/package_manifest/v1.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/security/package_manifest/v1.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/api/u/security/package_manifest/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/api/u/security/package_manifest/v1.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,47 @@ +from uaclient import apt, snap +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.config import UAConfig +from uaclient.data_types import DataObject, Field, StringDataValue + + +class PackageManifestResults(DataObject, AdditionalInfo): + fields = [ + Field("manifest_data", StringDataValue), + ] + + def __init__(self, manifest_data: str): + self.manifest_data = manifest_data + + +def package_manifest() -> PackageManifestResults: + return _package_manifest(UAConfig()) + + +def _package_manifest(cfg: UAConfig) -> PackageManifestResults: + """Returns the status of installed packages (apt and snap packages) + Returns a string in manifest format i.e. package_name\tversion + """ + manifest = "" + apt_pkgs = apt.get_installed_packages() + for apt_pkg in apt_pkgs: + arch = "" if apt_pkg.arch == "all" else ":" + apt_pkg.arch + manifest += "{}{}\t{}\n".format(apt_pkg.name, arch, apt_pkg.version) + + pkgs = snap.get_installed_snaps() + for pkg in pkgs: + manifest += "snap:{name}\t{tracking}\t{rev}\n".format( + name=pkg.name, + tracking=pkg.tracking, + rev=pkg.rev, + ) + + return PackageManifestResults(manifest_data=manifest) + + +endpoint = APIEndpoint( + version="v1", + name="Packages", + fn=_package_manifest, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/apt.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/apt.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/apt.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/apt.py 2022-11-22 13:06:26.000000000 +0000 @@ -7,7 +7,7 @@ import sys import tempfile from functools import lru_cache -from typing import Dict, List, Optional +from typing import Dict, List, NamedTuple, Optional from uaclient import event_logger, exceptions, gpg, messages, system @@ -48,6 +48,11 @@ UACLIENT = object() +InstalledAptPackages = NamedTuple( + "InstalledAptPackages", [("name", str), ("version", str), ("arch", str)] +) + + def assert_valid_apt_credentials(repo_url, username, password): """Validate apt credentials for a PPA. @@ -377,7 +382,7 @@ def remove_auth_apt_repo( - repo_filename: str, repo_url: str, keyring_file: str = None + repo_filename: str, repo_url: str, keyring_file: Optional[str] = None ) -> None: """Remove an authenticated apt repo and credentials to the system""" system.remove_file(repo_filename) @@ -484,13 +489,26 @@ def is_installed(pkg: str) -> bool: - return pkg in get_installed_packages() + return pkg in get_installed_packages_names() -def get_installed_packages() -> List[str]: +def get_installed_packages() -> List[InstalledAptPackages]: out, _ = system.subp(["apt", "list", "--installed"]) package_list = out.splitlines()[1:] - return [entry.split("/")[0] for entry in package_list] + return [ + InstalledAptPackages( + name=entry.split("/")[0], + version=entry.split(" ")[1], + arch=entry.split(" ")[2], + ) + for entry in package_list + ] + + +def get_installed_packages_names(include_versions: bool = False) -> List[str]: + package_list = get_installed_packages() + pkg_names = [pkg.name for pkg in package_list] + return pkg_names def setup_apt_proxy( diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/cli.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/cli.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/cli.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/cli.py 2022-11-22 13:06:26.000000000 +0000 @@ -12,7 +12,7 @@ import textwrap import time from functools import wraps -from typing import List, Tuple # noqa +from typing import List, Optional, Tuple # noqa import yaml @@ -34,6 +34,13 @@ from uaclient import status as ua_status from uaclient import util, version from uaclient.api.api import call_api +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, + _full_auto_attach, +) +from uaclient.api.u.pro.security.status.reboot_required.v1 import ( + _reboot_required, +) from uaclient.apt import AptProxyScope, setup_apt_proxy from uaclient.data_types import AttachActionsConfigFile, IncorrectTypeError from uaclient.defaults import DEFAULT_LOG_FORMAT, PRINT_WRAP_WIDTH @@ -95,7 +102,7 @@ usage=None, epilog=None, formatter_class=argparse.HelpFormatter, - base_desc: str = None, + base_desc: Optional[str] = None, ): super().__init__( prog=prog, @@ -258,7 +265,9 @@ @wraps(f) def new_f(args, cfg): if cfg.is_attached: - raise exceptions.AlreadyAttachedError(cfg) + raise exceptions.AlreadyAttachedError( + cfg.machine_token_file.account.get("name", "") + ) return f(args, cfg=cfg) return new_f @@ -740,6 +749,52 @@ return parser +def system_parser(parser): + """Build or extend an arg parser for system subcommand.""" + parser.usage = USAGE_TMPL.format(name=NAME, command="system ") + parser.description = ( + "Output system related information related to Pro services" + ) + parser.prog = "system" + parser._optionals.title = "Flags" + subparsers = parser.add_subparsers( + title="Available Commands", dest="command", metavar="" + ) + parser_reboot_required = subparsers.add_parser( + "reboot-required", help="does the system need to be rebooted" + ) + parser_reboot_required.set_defaults(action=action_system_reboot_required) + reboot_required_parser(parser_reboot_required) + + return parser + + +def reboot_required_parser(parser): + # This formatter_class ensures that our formatting below isn't lost + parser.usage = USAGE_TMPL.format( + name=NAME, command="system reboot-required" + ) + parser.pro = "reboot-required" + parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.description = textwrap.dedent( + """\ + Report the current reboot-required status for the machine. + + This command will output one of the three following states + for the machine regarding reboot: + + * no: The machine doesn't require a reboot + * yes: The machine requires a reboot + * yes-kernel-livepatches-applied: There are only kernel related + packages that require a reboot, but Livepatch has already provided + patches for the current running kernel. The machine still needs a + reboot, but you can assess if the reboot can be performed in the + nearest maintenance window. + """ + ) + return parser + + def status_parser(parser): """Build or extend an arg parser for status subcommand.""" usage = USAGE_TMPL.format(name=NAME, command="status") @@ -819,6 +874,19 @@ 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.UserFacingError( + "\n must be one of: {}".format(", ".join(valid_choices)) + ) + + def _perform_disable(entitlement, cfg, *, assume_yes, update_status=True): """Perform the disable action on a named entitlement. @@ -856,14 +924,10 @@ :return: 0 on success, 1 otherwise """ - parser = get_parser(cfg=cfg) - subparser = parser._get_positional_actions()[0].choices["config"] - valid_choices = subparser._get_positional_actions()[0].choices.keys() - if args.command not in valid_choices: - parser._get_positional_actions()[0].choices["config"].print_help() - raise exceptions.UserFacingError( - "\n must be one of: {}".format(", ".join(valid_choices)) - ) + _print_help_for_subcommand( + cfg, cmd_name="config", subcmd_name=args.command + ) + return 0 def action_config_show(args, *, cfg, **kwargs): @@ -1307,6 +1371,7 @@ event.info(messages.ATTACH_SUCCESS_NO_CONTRACT_NAME) daemon.stop() + daemon.cleanup(cfg) status, _ret = actions.status(cfg) output = ua_status.format_tabular(status) @@ -1321,19 +1386,13 @@ @assert_root -@assert_lock_file("pro auto-attach") -def action_auto_attach(args, *, cfg): - if cfg.is_attached: - raise exceptions.AlreadyAttachedOnPROError() - - skip_auto_attach = actions.should_disable_auto_attach(cfg) - if skip_auto_attach: - return 0 - - instance = actions.get_cloud_instance(cfg) - +def action_auto_attach(args, *, cfg: config.UAConfig) -> int: try: - actions.auto_attach(cfg, instance) + _full_auto_attach( + FullAutoAttachOptions(), + cfg=cfg, + mode=event_logger.EventLoggerMode.CLI, + ) except exceptions.UrlError: event.info(messages.ATTACH_FAILURE.msg) return 1 @@ -1545,6 +1604,12 @@ ) parser_version.set_defaults(action=print_version) + parser_system = subparsers.add_parser( + "system", help="show system information related to Pro services" + ) + parser_system.set_defaults(action=action_system) + system_parser(parser_system) + return parser @@ -1560,6 +1625,10 @@ cfg.notice_file.try_add( "", messages.NOTICE_REFRESH_CONTRACT_WARNING ) + else: + cfg.notice_file.try_remove( + "", messages.NOTICE_REFRESH_CONTRACT_WARNING + ) except exceptions.UrlError as e: with util.disable_log_to_console(): err_msg = messages.UPDATE_CHECK_CONTRACT_FAILURE.format( @@ -1590,6 +1659,23 @@ return ret +def action_system(args, *, cfg, **kwargs): + """Perform the system action. + + :return: 0 on success, 1 otherwise + """ + _print_help_for_subcommand( + cfg, cmd_name="system", subcmd_name=args.command + ) + return 0 + + +def action_system_reboot_required(args, *, cfg: config.UAConfig): + result = _reboot_required(cfg) + event.info(result.reboot_required) + return 0 + + def print_version(_args=None, cfg=None): print(version.get_version()) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/config.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/config.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/config.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/config.py 2022-11-22 13:06:26.000000000 +0000 @@ -93,9 +93,6 @@ "marker-reboot-cmds": DataPath( "marker-reboot-cmds-required", False, False ), - "services-once-enabled": DataPath( - "services-once-enabled", False, True - ), "jobs-status": DataPath("jobs-status.json", False, True), } # type: Dict[str, DataPath] @@ -111,8 +108,8 @@ def __init__( self, - cfg: Dict[str, Any] = None, - series: str = None, + cfg: Optional[Dict[str, Any]] = None, + series: Optional[str] = None, root_mode: bool = False, ) -> None: """""" diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/conftest.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/conftest.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/conftest.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/conftest.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,3 +1,4 @@ +import datetime import io import logging import sys @@ -139,7 +140,15 @@ "accountInfo": { "id": "acct-1", "name": account_name, - "createdAt": "2019-06-14T06:45:50Z", + "createdAt": datetime.datetime( + 2019, + 6, + 14, + 6, + 45, + 50, + tzinfo=datetime.timezone.utc, + ), "externalAccountIDs": [ {"IDs": ["id1"], "origin": "AWS"} ], @@ -147,9 +156,33 @@ "contractInfo": { "id": "cid", "name": "test_contract", - "createdAt": "2020-05-08T19:02:26Z", - "effectiveFrom": "2000-05-08T19:02:26Z", - "effectiveTo": "2040-05-08T19:02:26Z", + "createdAt": datetime.datetime( + 2020, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), + "effectiveFrom": datetime.datetime( + 2000, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), + "effectiveTo": datetime.datetime( + 2040, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), "resourceEntitlements": [], "products": ["free"], }, @@ -160,7 +193,7 @@ status_cache = {"attached": True} config = cls(root_mode=root_mode) - config.machine_token_file.write(machine_token) + config.machine_token_file._machine_token = machine_token config.write_cache("status-cache", status_cache) return config diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/contract.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/contract.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/contract.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/contract.py 2022-11-22 13:06:26.000000000 +0000 @@ -142,14 +142,16 @@ return resource_access def request_machine_token_update( - self, machine_token: str, contract_id: str, machine_id: str = None + self, + machine_token: str, + contract_id: str, + machine_id: Optional[str] = None, ) -> Dict: """Update existing machine-token for an attached machine.""" return self._request_machine_token_update( machine_token=machine_token, contract_id=contract_id, machine_id=machine_id, - detach=False, ) def report_machine_activity(self): @@ -229,10 +231,7 @@ raise e except exceptions.UrlError as e: logging.exception(str(e)) - raise exceptions.UserFacingError( - msg=messages.CONNECTIVITY_ERROR.msg, - msg_code=messages.CONNECTIVITY_ERROR.name, - ) + raise exceptions.ConnectivityError() return response @@ -258,10 +257,7 @@ raise e except exceptions.UrlError as e: logging.exception(str(e)) - raise exceptions.UserFacingError( - msg=messages.CONNECTIVITY_ERROR.msg, - msg_code=messages.CONNECTIVITY_ERROR.name, - ) + raise exceptions.ConnectivityError() def get_updated_contract_info( self, @@ -302,8 +298,7 @@ self, machine_token: str, contract_id: str, - machine_id: str = None, - detach: bool = False, + machine_id: Optional[str] = None, ) -> Dict: """Request machine token refresh from contract server. @@ -312,8 +307,6 @@ @param contract_id: Unique contract id provided by contract service. @param machine_id: Optional unique system machine id. When absent, contents of /etc/machine-id will be used. - @param detach: Boolean set True if detaching this machine from the - active contract. Default is False. @return: Dict of the JSON response containing refreshed machine-token """ @@ -324,24 +317,27 @@ url = API_V1_TMPL_CONTEXT_MACHINE_TOKEN_RESOURCE.format( contract=contract_id, machine=data["machineId"] ) - kwargs = {"headers": headers} - if detach: - kwargs["method"] = "DELETE" - else: - kwargs["method"] = "POST" - kwargs["data"] = data - response, headers = self.request_url(url, **kwargs) + response, headers = self.request_url( + url, headers=headers, method="POST", data=data + ) if headers.get("expires"): response["expires"] = headers["expires"] - if not detach: - self.cfg.machine_token_file.write(response) - system.get_machine_id.cache_clear() - machine_id = response.get("machineTokenInfo", {}).get( - "machineId", data.get("machineId") - ) - self.cfg.write_cache("machine-id", machine_id) + machine_id = response.get("machineTokenInfo", {}).get( + "machineId", data.get("machineId") + ) return response + def update_files_after_machine_token_update( + self, response: Dict[str, Any] + ): + self.cfg.machine_token_file.write(response) + system.get_machine_id.cache_clear() + data = self._get_platform_data(None) + machine_id = response.get("machineTokenInfo", {}).get( + "machineId", data.get("machineId") + ) + self.cfg.write_cache("machine-id", machine_id) + def _get_platform_data(self, machine_id): """Return a dict of platform-related data for contract requests""" if not machine_id: @@ -604,16 +600,14 @@ raise e with util.disable_log_to_console(): logging.exception(str(e)) - raise exceptions.UserFacingError( - msg=messages.CONNECTIVITY_ERROR.msg, - msg_code=messages.CONNECTIVITY_ERROR.name, - ) + raise exceptions.ConnectivityError() else: machine_token = orig_token["machineToken"] contract_id = orig_token["machineTokenInfo"]["contractInfo"]["id"] - contract_client.request_machine_token_update( + resp = contract_client.request_machine_token_update( machine_token=machine_token, contract_id=contract_id ) + contract_client.update_files_after_machine_token_update(resp) process_entitlements_delta( cfg, @@ -643,8 +637,11 @@ contract_id = ( orig_token.get("machineTokenInfo", {}) .get("contractInfo", {}) - .get("id", "") + .get("id", None) ) + if not contract_id: + return False + contract_client = UAContractClient(cfg) resp = contract_client.get_updated_contract_info( machine_token, contract_id diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/__init__.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/__init__.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/__init__.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,56 @@ +import logging +import os +import sys +from subprocess import TimeoutExpired + +from uaclient import exceptions, system +from uaclient.config import UAConfig +from uaclient.defaults import DEFAULT_DATA_DIR, DEFAULT_LOG_FORMAT + +LOG = logging.getLogger("pro.daemon") + +AUTO_ATTACH_STATUS_MOTD_FILE = os.path.join( + DEFAULT_DATA_DIR, "messages", "motd-auto-attach-status" +) + + +def start(): + try: + system.subp( + ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 + ) + except (exceptions.ProcessExecutionError, TimeoutExpired) as e: + LOG.warning(e) + + +def stop(): + try: + system.subp( + ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 + ) + except (exceptions.ProcessExecutionError, TimeoutExpired) as e: + LOG.warning(e) + + +def cleanup(cfg: UAConfig): + from uaclient.daemon import retry_auto_attach + + retry_auto_attach.cleanup(cfg) + + +def setup_logging(console_level, log_level, log_file, logger): + logger.setLevel(log_level) + + logger.handlers = [] + + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setFormatter(logging.Formatter("%(message)s")) + console_handler.setLevel(console_level) + console_handler.set_name("ua-console") + logger.addHandler(console_handler) + + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(log_level) + file_handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) + file_handler.set_name("ua-file") + logger.addHandler(file_handler) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/poll_for_pro_license.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/poll_for_pro_license.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/poll_for_pro_license.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/poll_for_pro_license.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,105 @@ +import logging +import time + +from uaclient import actions, exceptions, lock, system, util +from uaclient.clouds import AutoAttachCloudInstance +from uaclient.clouds.gcp import UAAutoAttachGCPInstance +from uaclient.clouds.identity import cloud_instance_factory +from uaclient.config import UAConfig +from uaclient.daemon import retry_auto_attach + +LOG = logging.getLogger("pro.daemon.poll_for_pro_license") + + +def attempt_auto_attach(cfg: UAConfig, cloud: AutoAttachCloudInstance): + try: + with lock.SpinLock( + cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach" + ): + actions.auto_attach(cfg, cloud) + except Exception as e: + LOG.error(e) + lock.clear_lock_file_if_present() + LOG.info("creating flag file to trigger retries") + system.create_file(retry_auto_attach.FLAG_FILE_PATH) + return + LOG.debug("Successful auto attach") + + +def poll_for_pro_license(cfg: UAConfig): + if util.is_config_value_true( + config=cfg.cfg, path_to_value="features.disable_auto_attach" + ): + LOG.debug("Configured to not auto attach, shutting down") + return + if cfg.is_attached: + LOG.debug("Already attached, shutting down") + return + if not system.is_current_series_lts(): + LOG.debug("Not on LTS, shutting down") + return + + try: + cloud = cloud_instance_factory() + except exceptions.CloudFactoryError: + LOG.debug("Not on cloud, shutting down") + return + + if not isinstance(cloud, UAAutoAttachGCPInstance): + LOG.debug("Not on gcp, shutting down") + return + + if not cloud.should_poll_for_pro_license(): + LOG.debug("Not on supported instance, shutting down") + return + + try: + pro_license_present = cloud.is_pro_license_present( + wait_for_change=False + ) + except exceptions.CancelProLicensePolling: + LOG.debug("Cancelling polling") + return + except exceptions.DelayProLicensePolling: + # Continue to polling loop anyway and handle error there if it occurs + # again + pass + else: + if pro_license_present: + attempt_auto_attach(cfg, cloud) + return + + if not cfg.poll_for_pro_license: + LOG.debug("Configured to not poll for pro license, shutting down") + return + + while True: + try: + start = time.time() + pro_license_present = cloud.is_pro_license_present( + wait_for_change=True + ) + end = time.time() + except exceptions.CancelProLicensePolling: + LOG.debug("Cancelling polling") + return + except exceptions.DelayProLicensePolling: + time.sleep(cfg.polling_error_retry_delay) + continue + else: + if cfg.is_attached: + # This could have changed during the long poll or sleep + LOG.debug("Already attached, shutting down") + return + if pro_license_present: + attempt_auto_attach(cfg, cloud) + return + if end - start < 10: + LOG.debug( + "wait_for_change returned quickly and no pro license" + " present. Waiting {} seconds before polling again".format( + cfg.polling_error_retry_delay + ) + ) + time.sleep(cfg.polling_error_retry_delay) + continue diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/retry_auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/retry_auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/retry_auto_attach.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/retry_auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,186 @@ +import datetime +import logging +import time + +from uaclient import exceptions, lock, messages, system +from uaclient.api import exceptions as api_exceptions +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, + full_auto_attach, +) +from uaclient.config import UAConfig +from uaclient.daemon import AUTO_ATTACH_STATUS_MOTD_FILE +from uaclient.files import state_files + +LOG = logging.getLogger("pro.daemon.retry_auto_attach") + +RETRY_INTERVALS = [ + 900, # 15m (T+15m) + 900, # 15m (T+30m) + 1800, # 30m (T+1h) + 3600, # 1h (T+2h) + 7200, # 2h (T+4h) + 14400, # 4h (T+8h) + 28800, # 8h (T+16h) + 28800, # 8h (T+1d) + 86400, # 1d (T+2d) + 86400, # 1d (T+3d) + 172800, # 2d (T+5d) + 172800, # 2d (T+7d) + 259200, # 3d (T+10d) + 259200, # 3d (T+13d) + 345600, # 4d (T+17d) + 345600, # 4d (T+21d) + 432000, # 5d (T+26d) + 432000, # 5d (T+31d) +] +FLAG_FILE_PATH = "/run/ubuntu-advantage/flags/auto-attach-failed" + + +def full_auto_attach_exception_to_failure_reason(e: Exception) -> str: + if isinstance(e, api_exceptions.InvalidProImage): + return messages.RETRY_ERROR_DETAIL_INVALID_PRO_IMAGE.format( + e.contract_server_msg + ) + elif isinstance(e, api_exceptions.NonAutoAttachImageError): + return messages.RETRY_ERROR_DETAIL_NON_AUTO_ATTACH_IMAGE + elif isinstance(e, api_exceptions.LockHeldError): + return messages.RETRY_ERROR_DETAIL_LOCK_HELD.format(pid=e.pid) + elif isinstance(e, api_exceptions.ContractAPIError): + return messages.RETRY_ERROR_DETAIL_CONTRACT_API_ERROR.format( + e.api_error + ) + elif isinstance(e, api_exceptions.ConnectivityError): + return messages.RETRY_ERROR_DETAIL_CONNECTIVITY_ERROR + elif isinstance(e, api_exceptions.UrlError): + if e.url: + if e.code: + failure_reason = ( + messages.RETRY_ERROR_DETAIL_URL_ERROR_CODE.format( + code=e.code, url=e.url + ) + ) + else: + failure_reason = ( + messages.RETRY_ERROR_DETAIL_URL_ERROR_URL.format(url=e.url) + ) + else: + failure_reason = messages.RETRY_ERROR_DETAIL_URL_ERROR_GENERIC + failure_reason += ': "{}"'.format(str(e)) + return failure_reason + elif isinstance(e, api_exceptions.UserFacingError): + return '"{}"'.format(e.msg) + else: + LOG.error("Unexpected exception: {}".format(e)) + return str(e) or messages.RETRY_ERROR_DETAIL_UNKNOWN + + +def cleanup(cfg: UAConfig): + state_files.retry_auto_attach_state_file.delete() + state_files.retry_auto_attach_options_file.delete() + system.remove_file(AUTO_ATTACH_STATUS_MOTD_FILE) + cfg.notice_file.remove("", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX) + + +def retry_auto_attach(cfg: UAConfig) -> None: + # in case we got started while already attached somehow + if cfg.is_attached: + return + + # pick up where we left off + persisted_state = state_files.retry_auto_attach_state_file.read() + if persisted_state is not None: + # skip intervals we've already waited + offset = persisted_state.interval_index + intervals = RETRY_INTERVALS[offset:] + failure_reason = persisted_state.failure_reason + else: + offset = 0 + intervals = RETRY_INTERVALS + failure_reason = None + + for index, interval in enumerate(intervals): + last_attempt = datetime.datetime.now(datetime.timezone.utc) + next_attempt = last_attempt + datetime.timedelta(seconds=interval) + next_attempt = next_attempt.replace(second=0, microsecond=0) + state_files.retry_auto_attach_state_file.write( + state_files.RetryAutoAttachState( + interval_index=offset + index, + failure_reason=failure_reason, + ) + ) + msg_reason = failure_reason + if msg_reason is None: + msg_reason = messages.RETRY_ERROR_DETAIL_UNKNOWN + try: + next_attempt = next_attempt.astimezone() + except Exception: + pass + auto_attach_status_msg = messages.AUTO_ATTACH_RETRY_NOTICE.format( + num_attempts=offset + index + 1, + reason=msg_reason, + next_run_datestring=next_attempt.isoformat(), + ) + system.write_file( + AUTO_ATTACH_STATUS_MOTD_FILE, auto_attach_status_msg + "\n\n" + ) + try: + with lock.SpinLock( + cfg=cfg, + lock_holder="pro.daemon.retry_auto_attach.notice_updates", + ): + cfg.notice_file.remove( + "", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX + ) + cfg.notice_file.add("", auto_attach_status_msg) + except exceptions.LockHeldError: + pass + + time.sleep(interval) + + if cfg.is_attached: + # We attached while sleeping - hooray! + break + + try: + persisted_options = ( + state_files.retry_auto_attach_options_file.read() + ) + options = FullAutoAttachOptions() + if persisted_options is not None: + options.enable = persisted_options.enable + options.enable_beta = persisted_options.enable_beta + full_auto_attach(options) + break + except api_exceptions.AlreadyAttachedError: + LOG.info("already attached, ending retry service") + break + except api_exceptions.EntitlementsNotEnabledError as e: + LOG.warning(e.msg) + break + except Exception as e: + failure_reason = full_auto_attach_exception_to_failure_reason(e) + LOG.error(e) + + cleanup(cfg) + + if not cfg.is_attached: + # Total failure!! + state_files.retry_auto_attach_state_file.write( + state_files.RetryAutoAttachState( + interval_index=len(RETRY_INTERVALS), + failure_reason=failure_reason, + ) + ) + msg_reason = failure_reason + if msg_reason is None: + msg_reason = messages.RETRY_ERROR_DETAIL_UNKNOWN + auto_attach_status_msg = ( + messages.AUTO_ATTACH_RETRY_TOTAL_FAILURE_NOTICE.format( + num_attempts=len(RETRY_INTERVALS) + 1, reason=msg_reason + ) + ) + system.write_file( + AUTO_ATTACH_STATUS_MOTD_FILE, auto_attach_status_msg + "\n\n" + ) + cfg.notice_file.add("", auto_attach_status_msg) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_daemon.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_daemon.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_daemon.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_daemon.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,67 @@ +from subprocess import TimeoutExpired + +import mock +import pytest + +from uaclient import exceptions +from uaclient.daemon import start, stop + +M_PATH = "uaclient.daemon." + + +@mock.patch(M_PATH + "system.subp") +class TestStart: + def test_start_success(self, m_subp): + start() + assert [ + mock.call( + ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 + ) + ] == m_subp.call_args_list + + @pytest.mark.parametrize( + "err", + ( + exceptions.ProcessExecutionError("cmd"), + TimeoutExpired("cmd", 2.0), + ), + ) + @mock.patch(M_PATH + "LOG.warning") + def test_start_warning(self, m_log_warning, m_subp, err): + m_subp.side_effect = err + start() + assert [ + mock.call( + ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 + ) + ] == m_subp.call_args_list + assert [mock.call(err)] == m_log_warning.call_args_list + + +@mock.patch(M_PATH + "system.subp") +class TestStop: + def test_stop_success(self, m_subp): + stop() + assert [ + mock.call( + ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 + ) + ] == m_subp.call_args_list + + @pytest.mark.parametrize( + "err", + ( + exceptions.ProcessExecutionError("cmd"), + TimeoutExpired("cmd", 2.0), + ), + ) + @mock.patch(M_PATH + "LOG.warning") + def test_stop_warning(self, m_log_warning, m_subp, err): + m_subp.side_effect = err + stop() + assert [ + mock.call( + ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 + ) + ] == m_subp.call_args_list + assert [mock.call(err)] == m_log_warning.call_args_list diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,446 @@ +import mock +import pytest + +from uaclient import exceptions +from uaclient.clouds.aws import UAAutoAttachAWSInstance +from uaclient.clouds.gcp import UAAutoAttachGCPInstance +from uaclient.daemon.poll_for_pro_license import ( + attempt_auto_attach, + poll_for_pro_license, +) + +M_PATH = "uaclient.daemon.poll_for_pro_license." + + +time_mock_curr_value = 0 + + +def time_mock_side_effect_increment_by(increment): + def _time_mock_side_effect(): + global time_mock_curr_value + time_mock_curr_value += increment + return time_mock_curr_value + + return _time_mock_side_effect + + +@mock.patch(M_PATH + "LOG.debug") +@mock.patch(M_PATH + "actions.auto_attach") +@mock.patch(M_PATH + "lock.SpinLock") +class TestAttemptAutoAttach: + def test_success( + self, m_spin_lock, m_auto_attach, m_log_debug, FakeConfig + ): + cfg = FakeConfig() + cloud = mock.MagicMock() + + attempt_auto_attach(cfg, cloud) + + assert [ + mock.call(cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach") + ] == m_spin_lock.call_args_list + assert [mock.call(cfg, cloud)] == m_auto_attach.call_args_list + assert [ + mock.call("Successful auto attach") + ] == m_log_debug.call_args_list + + @mock.patch(M_PATH + "system.create_file") + @mock.patch(M_PATH + "lock.clear_lock_file_if_present") + @mock.patch(M_PATH + "LOG.error") + @mock.patch(M_PATH + "LOG.info") + def test_exception( + self, + m_log_info, + m_log_error, + m_clear_lock, + m_create_file, + m_spin_lock, + m_auto_attach, + m_log_debug, + FakeConfig, + ): + err = Exception() + m_auto_attach.side_effect = err + cfg = FakeConfig() + cfg.notice_file.add = mock.MagicMock() + cloud = mock.MagicMock() + + attempt_auto_attach(cfg, cloud) + + assert [ + mock.call(cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach") + ] == m_spin_lock.call_args_list + assert [mock.call(cfg, cloud)] == m_auto_attach.call_args_list + assert [mock.call(err)] == m_log_error.call_args_list + assert [mock.call()] == m_clear_lock.call_args_list + assert [ + mock.call("creating flag file to trigger retries") + ] == m_log_info.call_args_list + assert [ + mock.call("/run/ubuntu-advantage/flags/auto-attach-failed") + ] == m_create_file.call_args_list + + +@mock.patch(M_PATH + "LOG.debug") +@mock.patch(M_PATH + "time.sleep") +@mock.patch(M_PATH + "time.time") +@mock.patch(M_PATH + "attempt_auto_attach") +@mock.patch(M_PATH + "UAAutoAttachGCPInstance.is_pro_license_present") +@mock.patch(M_PATH + "UAAutoAttachGCPInstance.should_poll_for_pro_license") +@mock.patch(M_PATH + "cloud_instance_factory") +@mock.patch(M_PATH + "system.is_current_series_lts") +@mock.patch(M_PATH + "util.is_config_value_true") +class TestPollForProLicense: + @pytest.mark.parametrize( + "is_config_value_true," + "is_attached," + "is_current_series_lts," + "cloud_instance," + "should_poll," + "is_pro_license_present," + "cfg_poll_for_pro_licenses," + "expected_log_debug_calls," + "expected_is_pro_license_present_calls," + "expected_attempt_auto_attach_calls", + [ + ( + True, + None, + None, + None, + None, + None, + None, + [mock.call("Configured to not auto attach, shutting down")], + [], + [], + ), + ( + False, + True, + None, + None, + None, + None, + None, + [mock.call("Already attached, shutting down")], + [], + [], + ), + ( + False, + False, + False, + None, + None, + None, + None, + [mock.call("Not on LTS, shutting down")], + [], + [], + ), + ( + False, + False, + True, + exceptions.CloudFactoryError("none"), + None, + None, + None, + [mock.call("Not on cloud, shutting down")], + [], + [], + ), + ( + False, + False, + True, + UAAutoAttachAWSInstance(), + None, + None, + None, + [mock.call("Not on gcp, shutting down")], + [], + [], + ), + ( + False, + False, + True, + UAAutoAttachGCPInstance(), + False, + None, + None, + [mock.call("Not on supported instance, shutting down")], + [], + [], + ), + ( + False, + False, + True, + UAAutoAttachGCPInstance(), + True, + True, + None, + [], + [mock.call(wait_for_change=False)], + [mock.call(mock.ANY, mock.ANY)], + ), + ( + False, + False, + True, + UAAutoAttachGCPInstance(), + True, + exceptions.CancelProLicensePolling(), + None, + [mock.call("Cancelling polling")], + [mock.call(wait_for_change=False)], + [], + ), + ( + False, + False, + True, + UAAutoAttachGCPInstance(), + True, + False, + False, + [ + mock.call( + "Configured to not poll for pro license, shutting down" + ) + ], + [mock.call(wait_for_change=False)], + [], + ), + ( + False, + False, + True, + UAAutoAttachGCPInstance(), + True, + False, + False, + [ + mock.call( + "Configured to not poll for pro license, shutting down" + ) + ], + [mock.call(wait_for_change=False)], + [], + ), + ], + ) + def test_before_polling_loop_checks( + self, + m_is_config_value_true, + m_is_current_series_lts, + m_cloud_instance_factory, + m_should_poll, + m_is_pro_license_present, + m_attempt_auto_attach, + m_time, + m_sleep, + m_log_debug, + is_config_value_true, + is_attached, + is_current_series_lts, + cloud_instance, + should_poll, + is_pro_license_present, + cfg_poll_for_pro_licenses, + expected_log_debug_calls, + expected_is_pro_license_present_calls, + expected_attempt_auto_attach_calls, + FakeConfig, + ): + if is_attached: + cfg = FakeConfig.for_attached_machine() + else: + cfg = FakeConfig() + cfg.cfg.update( + {"ua_config": {"poll_for_pro_license": cfg_poll_for_pro_licenses}} + ) + + m_is_config_value_true.return_value = is_config_value_true + m_is_current_series_lts.return_value = is_current_series_lts + m_cloud_instance_factory.side_effect = [cloud_instance] + m_should_poll.return_value = should_poll + m_is_pro_license_present.side_effect = [is_pro_license_present] + + poll_for_pro_license(cfg) + + assert expected_log_debug_calls == m_log_debug.call_args_list + assert ( + expected_is_pro_license_present_calls + == m_is_pro_license_present.call_args_list + ) + assert ( + expected_attempt_auto_attach_calls + == m_attempt_auto_attach.call_args_list + ) + + @pytest.mark.parametrize( + "is_pro_license_present_side_effect," + "time_side_effect," + "expected_is_pro_license_present_calls," + "expected_attempt_auto_attach_calls," + "expected_log_debug_calls," + "expected_sleep_calls", + [ + ( + [False, True], + time_mock_side_effect_increment_by(100), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + ], + [mock.call(mock.ANY, mock.ANY)], + [], + [], + ), + ( + [False, False, False, False, False, True], + time_mock_side_effect_increment_by(100), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + ], + [mock.call(mock.ANY, mock.ANY)], + [], + [], + ), + ( + [False, False, True], + time_mock_side_effect_increment_by(1), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + ], + [mock.call(mock.ANY, mock.ANY)], + [ + mock.call( + "wait_for_change returned quickly and no pro license" + " present. Waiting 123 seconds before polling again" + ) + ], + [mock.call(123)], + ), + ( + [False, False, False, False, False, True], + time_mock_side_effect_increment_by(1), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + ], + [mock.call(mock.ANY, mock.ANY)], + [ + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + ], + [ + mock.call(123), + mock.call(123), + mock.call(123), + mock.call(123), + ], + ), + ( + [ + False, + False, + exceptions.DelayProLicensePolling(), + False, + exceptions.DelayProLicensePolling(), + True, + ], + time_mock_side_effect_increment_by(100), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + ], + [mock.call(mock.ANY, mock.ANY)], + [], + [mock.call(123), mock.call(123)], + ), + ( + [False, False, exceptions.CancelProLicensePolling()], + time_mock_side_effect_increment_by(100), + [ + mock.call(wait_for_change=False), + mock.call(wait_for_change=True), + mock.call(wait_for_change=True), + ], + [], + [mock.call("Cancelling polling")], + [], + ), + ], + ) + def test_polling_loop( + self, + m_is_config_value_true, + m_is_current_series_lts, + m_cloud_instance_factory, + m_should_poll, + m_is_pro_license_present, + m_attempt_auto_attach, + m_time, + m_sleep, + m_log_debug, + is_pro_license_present_side_effect, + time_side_effect, + expected_is_pro_license_present_calls, + expected_attempt_auto_attach_calls, + expected_log_debug_calls, + expected_sleep_calls, + FakeConfig, + ): + cfg = FakeConfig() + cfg.cfg.update( + { + "ua_config": { + "poll_for_pro_license": True, + "polling_error_retry_delay": 123, + } + } + ) + + m_is_config_value_true.return_value = False + m_is_current_series_lts.return_value = True + m_cloud_instance_factory.return_value = UAAutoAttachGCPInstance() + m_should_poll.return_value = True + m_is_pro_license_present.side_effect = ( + is_pro_license_present_side_effect + ) + m_time.side_effect = time_side_effect + + poll_for_pro_license(cfg) + + assert expected_sleep_calls == m_sleep.call_args_list + assert expected_log_debug_calls == m_log_debug.call_args_list + assert ( + expected_is_pro_license_present_calls + == m_is_pro_license_present.call_args_list + ) + assert ( + expected_attempt_auto_attach_calls + == m_attempt_auto_attach.call_args_list + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_retry_auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_retry_auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon/tests/test_retry_auto_attach.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon/tests/test_retry_auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,731 @@ +from urllib import error + +import mock +import pytest + +from uaclient import exceptions, messages +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, +) +from uaclient.daemon import AUTO_ATTACH_STATUS_MOTD_FILE +from uaclient.daemon.retry_auto_attach import ( + full_auto_attach_exception_to_failure_reason, + retry_auto_attach, +) +from uaclient.files import state_files + +M_PATH = "uaclient.daemon.retry_auto_attach." + + +class TestFullAutoAttachToFailureReason: + @pytest.mark.parametrize( + "e, expected_reason", + [ + ( + exceptions.InvalidProImage("invalid pro"), + 'Canonical servers did not recognize this machine as Ubuntu Pro: "invalid pro"', # noqa: E501 + ), + ( + exceptions.NonAutoAttachImageError("msg"), + "Canonical servers did not recognize this image as Ubuntu Pro", + ), + ( + exceptions.LockHeldError("request", "holder", 123), + "the pro lock was held by pid 123", + ), + ( + exceptions.ContractAPIError( + exceptions.UrlError(error.URLError("urlerror")), "response" + ), + 'an error from Canonical servers: "response"', + ), + ( + exceptions.ConnectivityError(), + "a connectivity error", + ), + ( + exceptions.UrlError( + error.URLError("urlerror"), 123, url="url" + ), + 'a 123 while reaching url: "urlerror"', + ), + ( + exceptions.UrlError(error.URLError("urlerror"), url="url"), + 'an error while reaching url: "urlerror"', + ), + ( + exceptions.UrlError(error.URLError("urlerror")), + 'a network error: "urlerror"', + ), + (exceptions.UserFacingError("msg"), '"msg"'), + (Exception("hello"), "hello"), + (Exception(), "an unknown error"), + ], + ) + def test(self, e, expected_reason): + assert expected_reason == full_auto_attach_exception_to_failure_reason( + e + ) + + +@mock.patch(M_PATH + "cleanup") +@mock.patch(M_PATH + "full_auto_attach") +@mock.patch(M_PATH + "state_files.retry_auto_attach_options_file.read") +@mock.patch(M_PATH + "time.sleep") +@mock.patch(M_PATH + "system.write_file") +@mock.patch(M_PATH + "state_files.retry_auto_attach_state_file.write") +@mock.patch(M_PATH + "state_files.retry_auto_attach_state_file.read") +class TestRetryAutoAttach: + @pytest.mark.parametrize( + "is_attached, expected_state_read_calls", + [ + (False, [mock.call()]), + (True, []), + ], + ) + def test_early_return_when_attached( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + is_attached, + expected_state_read_calls, + FakeConfig, + ): + if is_attached: + cfg = FakeConfig.for_attached_machine() + else: + cfg = FakeConfig() + retry_auto_attach(cfg) + assert expected_state_read_calls == m_state_read.call_args_list + + def test_early_return_when_attached_during_sleep( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + FakeConfig, + ): + with mock.patch( + "uaclient.config.UAConfig.is_attached", + new_callable=mock.PropertyMock, + side_effect=[False, True, True], + ): + cfg = FakeConfig() + retry_auto_attach(cfg) + assert [mock.call(900)] == m_sleep.call_args_list + + @pytest.mark.parametrize( + "state_read_content, expected_state_write_calls", + [ + ( + None, + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=0, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=1, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=2, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=3, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=4, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=5, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=6, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=7, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=8, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=9, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=10, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=11, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=12, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=13, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=14, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=15, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=16, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ( + state_files.RetryAutoAttachState( + interval_index=0, failure_reason=None + ), + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=0, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=1, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=2, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=3, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=4, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=5, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=6, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=7, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=8, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=9, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=10, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=11, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=12, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=13, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=14, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=15, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=16, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ( + state_files.RetryAutoAttachState( + interval_index=1, failure_reason=None + ), + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=1, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=2, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=3, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=4, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=5, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=6, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=7, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=8, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=9, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=10, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=11, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=12, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=13, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=14, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=15, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=16, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ( + state_files.RetryAutoAttachState( + interval_index=12, failure_reason=None + ), + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=12, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=13, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=14, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=15, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=16, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=None + ), + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=17, failure_reason=mock.ANY + ) + ), + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=None + ), + [ + mock.call( + state_files.RetryAutoAttachState( + interval_index=18, failure_reason=mock.ANY + ) + ), + ], + ), + ], + ) + def test_state_file_is_used_and_updated( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + state_read_content, + expected_state_write_calls, + FakeConfig, + ): + # we want to test all state writes, so auto-attach can never succeed + m_full_auto_attach.side_effect = Exception() + m_state_read.return_value = state_read_content + cfg = FakeConfig() + retry_auto_attach(cfg) + assert expected_state_write_calls == m_state_write.call_args_list + + def test_already_attached_error_ends_early( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + FakeConfig, + ): + cfg = FakeConfig() + m_full_auto_attach.side_effect = exceptions.AlreadyAttachedError( + "test_account" + ) + retry_auto_attach(cfg) + assert [mock.call(mock.ANY)] == m_full_auto_attach.call_args_list + + @pytest.mark.parametrize( + "full_auto_attach_side_effect," "expected_full_auto_attach_calls", + [ + ( + [ + Exception(), + None, + ], + [ + mock.call(mock.ANY), + mock.call(mock.ANY), + ], + ), + ( + [ + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + None, + ], + [ + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + ], + ), + ( + [ + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + Exception(), + ], + [ + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + mock.call(mock.ANY), + ], + ), + ], + ) + def test_multiple_attempts_on_errors_with_limit( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + full_auto_attach_side_effect, + expected_full_auto_attach_calls, + FakeConfig, + ): + m_full_auto_attach.side_effect = full_auto_attach_side_effect + cfg = FakeConfig() + retry_auto_attach(cfg) + assert ( + expected_full_auto_attach_calls + == m_full_auto_attach.call_args_list + ) + + @pytest.mark.parametrize( + "is_attached_at_end, expected_cleanup_calls, expected_write_file_call", + [ + (True, [mock.call(mock.ANY)], None), + ( + False, + [mock.call(mock.ANY)], + mock.call( + AUTO_ATTACH_STATUS_MOTD_FILE, + messages.AUTO_ATTACH_RETRY_TOTAL_FAILURE_NOTICE.format( + num_attempts=19, reason="an unknown error" + ) + + "\n\n", + ), + ), + ], + ) + def test_cleanup_and_total_fail_message( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + is_attached_at_end, + expected_cleanup_calls, + expected_write_file_call, + FakeConfig, + ): + # skip all the attempts + m_state_read.return_value = state_files.RetryAutoAttachState( + interval_index=18, failure_reason=None + ) + with mock.patch( + "uaclient.config.UAConfig.is_attached", + new_callable=mock.PropertyMock, + side_effect=[False, is_attached_at_end], + ): + cfg = FakeConfig() + retry_auto_attach(cfg) + assert expected_cleanup_calls == m_cleanup.call_args_list + if expected_write_file_call: + assert expected_write_file_call in m_write_file.call_args_list + + @pytest.mark.parametrize( + [ + "option_file_contents", + "expected_full_auto_attach_calls", + ], + [ + (None, [mock.call(FullAutoAttachOptions())]), + ( + state_files.RetryAutoAttachOptions(), + [mock.call(FullAutoAttachOptions())], + ), + ( + state_files.RetryAutoAttachOptions(enable=["one"]), + [mock.call(FullAutoAttachOptions(enable=["one"]))], + ), + ( + state_files.RetryAutoAttachOptions( + enable=["one"], enable_beta=["two"] + ), + [ + mock.call( + FullAutoAttachOptions( + enable=["one"], enable_beta=["two"] + ) + ) + ], + ), + ], + ) + def test_uses_options_file( + self, + m_state_read, + m_state_write, + m_write_file, + m_sleep, + m_options_read, + m_full_auto_attach, + m_cleanup, + option_file_contents, + expected_full_auto_attach_calls, + FakeConfig, + ): + m_options_read.return_value = option_file_contents + cfg = FakeConfig() + retry_auto_attach(cfg) + assert ( + expected_full_auto_attach_calls + == m_full_auto_attach.call_args_list + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/daemon.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/daemon.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -import logging -import time -from subprocess import TimeoutExpired - -from uaclient import actions, exceptions, lock, messages, system, util -from uaclient.clouds import AutoAttachCloudInstance -from uaclient.clouds.gcp import UAAutoAttachGCPInstance -from uaclient.clouds.identity import cloud_instance_factory -from uaclient.config import UAConfig - -LOG = logging.getLogger("pro.daemon") - - -def start(): - try: - system.subp( - ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 - ) - except (exceptions.ProcessExecutionError, TimeoutExpired) as e: - LOG.warning(e) - - -def stop(): - try: - system.subp( - ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 - ) - except (exceptions.ProcessExecutionError, TimeoutExpired) as e: - LOG.warning(e) - - -def attempt_auto_attach(cfg: UAConfig, cloud: AutoAttachCloudInstance): - try: - with lock.SpinLock( - cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach" - ): - actions.auto_attach(cfg, cloud) - except exceptions.LockHeldError as e: - LOG.error(e) - cfg.notice_file.add( - "", - messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format( - operation=e.lock_holder - ), - ) - LOG.debug("Failed to auto attach") - return - except Exception as e: - LOG.exception(e) - cfg.notice_file.add("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED) - lock.clear_lock_file_if_present() - LOG.debug("Failed to auto attach") - return - LOG.debug("Successful auto attach") - - -def poll_for_pro_license(cfg: UAConfig): - if util.is_config_value_true( - config=cfg.cfg, path_to_value="features.disable_auto_attach" - ): - LOG.debug("Configured to not auto attach, shutting down") - return - if cfg.is_attached: - LOG.debug("Already attached, shutting down") - return - if not system.is_current_series_lts(): - LOG.debug("Not on LTS, shutting down") - return - - try: - cloud = cloud_instance_factory() - except exceptions.CloudFactoryError: - LOG.debug("Not on cloud, shutting down") - return - - if not isinstance(cloud, UAAutoAttachGCPInstance): - LOG.debug("Not on gcp, shutting down") - return - - if not cloud.should_poll_for_pro_license(): - LOG.debug("Not on supported instance, shutting down") - return - - try: - pro_license_present = cloud.is_pro_license_present( - wait_for_change=False - ) - except exceptions.CancelProLicensePolling: - LOG.debug("Cancelling polling") - return - except exceptions.DelayProLicensePolling: - # Continue to polling loop anyway and handle error there if it occurs - # again - pass - else: - if pro_license_present: - attempt_auto_attach(cfg, cloud) - return - - if not cfg.poll_for_pro_license: - LOG.debug("Configured to not poll for pro license, shutting down") - return - - while True: - try: - start = time.time() - pro_license_present = cloud.is_pro_license_present( - wait_for_change=True - ) - end = time.time() - except exceptions.CancelProLicensePolling: - LOG.debug("Cancelling polling") - return - except exceptions.DelayProLicensePolling: - time.sleep(cfg.polling_error_retry_delay) - continue - else: - if cfg.is_attached: - # This could have changed during the long poll or sleep - LOG.debug("Already attached, shutting down") - return - if pro_license_present: - attempt_auto_attach(cfg, cloud) - return - if end - start < 10: - LOG.debug( - "wait_for_change returned quickly and no pro license" - " present. Waiting {} seconds before polling again".format( - cfg.polling_error_retry_delay - ) - ) - time.sleep(cfg.polling_error_retry_delay) - continue diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/data_types.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/data_types.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/data_types.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/data_types.py 2022-11-22 13:06:26.000000000 +0000 @@ -216,6 +216,19 @@ def __init__(self, **_kwargs): pass + def __eq__(self, other): + for field in self.fields: + self_val = getattr(self, field.key, None) + other_val = getattr(other, field.key, None) + if self_val != other_val: + return False + return True + + def __repr__(self): + return "{}{}".format( + self.__class__.__name__, self.to_dict().__repr__() + ) + def to_dict(self, keep_none: bool = True) -> dict: d = {} for field in self.fields: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/base.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/base.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/base.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/base.py 2022-11-22 13:06:26.000000000 +0000 @@ -554,12 +554,13 @@ affordance_arches is not None and platform["arch"] not in affordance_arches ): + deduplicated_arches = util.deduplicate_arches(affordance_arches) return ( ApplicabilityStatus.INAPPLICABLE, messages.INAPPLICABLE_ARCH.format( title=self.title, arch=platform["arch"], - supported_arches=", ".join(affordance_arches), + supported_arches=", ".join(deduplicated_arches), ), ) affordance_series = affordances.get("series", None) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/fips.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/fips.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/fips.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/fips.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,6 +1,5 @@ import logging import os -import re from itertools import groupby from typing import List, Optional, Tuple # noqa: F401 @@ -9,6 +8,10 @@ from uaclient.entitlements import repo from uaclient.entitlements.base import IncompatibleService from uaclient.entitlements.entitlement_status import ApplicationStatus +from uaclient.files.state_files import ( + ServicesOnceEnabledData, + services_once_enabled_file, +) from uaclient.types import ( # noqa: F401 MessagingOperations, MessagingOperationsDict, @@ -130,7 +133,7 @@ def install_packages( self, - package_list: List[str] = None, + package_list: Optional[List[str]] = None, cleanup_on_failure: bool = True, verbose: bool = True, ) -> None: @@ -155,7 +158,7 @@ # Any conditional packages should still be installed, but if # they fail to install we should not block the enable operation. desired_packages = [] # type: List[str] - installed_packages = apt.get_installed_packages() + installed_packages = apt.get_installed_packages_names() pkg_groups = groupby( sorted(self.conditional_packages), key=lambda pkg_name: pkg_name.replace("-hmac", ""), @@ -253,53 +256,11 @@ ), ) - def _replace_metapackage_on_cloud_instance( - self, packages: List[str] - ) -> List[str]: - """ - Identify correct metapackage to be used if in a cloud instance. - - Currently, the contract backend is not delivering the right - metapackage on a Bionic Azure or AWS cloud instance. For those - clouds, we have cloud specific fips metapackages and we should - use them. We are now performing that correction here, but this - is a temporary fix. - """ - cfg_disable_fips_metapackage_override = util.is_config_value_true( - config=self.cfg.cfg, - path_to_value="features.disable_fips_metapackage_override", - ) - - if cfg_disable_fips_metapackage_override: - return packages - - series = system.get_platform_info().get("series") - if series not in ("bionic", "focal"): - return packages - - cloud_id, _ = get_cloud_type() - if cloud_id is None: - cloud_id = "" - - cloud_match = re.match(r"^(?P(azure|aws|gce)).*", cloud_id) - cloud_id = cloud_match.group("cloud") if cloud_match else "" - - if cloud_id not in ("azure", "aws", "gce"): - return packages - - cloud_id = "gcp" if cloud_id == "gce" else cloud_id - cloud_metapkg = "ubuntu-{}-fips".format(cloud_id) - # Replace only the ubuntu-fips meta package if exists - return [ - cloud_metapkg if pkg == "ubuntu-fips" else pkg for pkg in packages - ] - @property def packages(self) -> List[str]: if system.is_container(): return [] - packages = super().packages - return self._replace_metapackage_on_cloud_instance(packages) + return super().packages def application_status( self, @@ -360,7 +321,7 @@ FIPS meta-package will unset grub config options which will deactivate FIPS on any related packages. """ - installed_packages = set(apt.get_installed_packages()) + installed_packages = set(apt.get_installed_packages_names()) fips_metapackage = set(self.packages).difference( set(self.conditional_packages) ) @@ -441,30 +402,30 @@ def static_affordances(self) -> Tuple[StaticAffordance, ...]: static_affordances = super().static_affordances - fips_update = FIPSUpdatesEntitlement(self.cfg) + fips_updates = FIPSUpdatesEntitlement(self.cfg) enabled_status = ApplicationStatus.ENABLED - is_fips_update_enabled = bool( - fips_update.application_status()[0] == enabled_status + is_fips_updates_enabled = bool( + fips_updates.application_status()[0] == enabled_status ) - services_once_enabled = ( - self.cfg.read_cache("services-once-enabled") or {} - ) - fips_updates_once_enabled = services_once_enabled.get( - fips_update.name, False + services_once_enabled_obj = services_once_enabled_file.read() + fips_updates_once_enabled = ( + services_once_enabled_obj.fips_updates + if services_once_enabled_obj + else False ) return static_affordances + ( ( messages.FIPS_ERROR_WHEN_FIPS_UPDATES_ENABLED.format( - fips=self.title, fips_updates=fips_update.title + fips=self.title, fips_updates=fips_updates.title ), - lambda: is_fips_update_enabled, + lambda: is_fips_updates_enabled, False, ), ( messages.FIPS_ERROR_WHEN_FIPS_UPDATES_ONCE_ENABLED.format( - fips=self.title, fips_updates=fips_update.title + fips=self.title, fips_updates=fips_updates.title ), lambda: fips_updates_once_enabled, False, @@ -577,14 +538,10 @@ self.cfg.notice_file.try_remove( "", messages.FIPS_DISABLE_REBOOT_REQUIRED ) - services_once_enabled = ( - self.cfg.read_cache("services-once-enabled") or {} - ) - services_once_enabled.update({self.name: True}) - self.cfg.write_cache( - key="services-once-enabled", content=services_once_enabled - ) + services_once_enabled_file.write( + ServicesOnceEnabledData(fips_updates=True) + ) return True return False diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/__init__.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/__init__.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/__init__.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/__init__.py 2022-11-22 13:06:26.000000000 +0000 @@ -44,7 +44,7 @@ for entitlement in ENTITLEMENT_CLASSES: if name in entitlement(cfg=cfg).valid_names: return entitlement - raise EntitlementNotFoundError() + raise EntitlementNotFoundError(name) def valid_services( @@ -83,6 +83,23 @@ ) +def order_entitlements_for_enabling( + cfg: UAConfig, ents: List[str] +) -> List[str]: + """ + A function to sort entitlments for enabling that preserves invalid names + """ + valid_ents_ordered = entitlements_enable_order(cfg) + + def sort_order_with_nonexistent_last(ent): + try: + return valid_ents_ordered.index(ent) + except ValueError: + return len(valid_ents_ordered) + + return sorted(ents, key=lambda ent: sort_order_with_nonexistent_last(ent)) + + @enum.unique class SortOrder(enum.Enum): REQUIRED_SERVICES = object() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/realtime.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/realtime.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/realtime.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/realtime.py 2022-11-22 13:06:26.000000000 +0000 @@ -16,11 +16,10 @@ class RealtimeKernelEntitlement(repo.RepoEntitlement): name = "realtime-kernel" - title = "Real-Time Kernel" - description = "Beta-version Ubuntu Kernel with PREEMPT_RT patches" + title = "Real-time kernel" + description = "Ubuntu kernel with PREEMPT_RT patches integrated" help_doc_url = REALTIME_KERNEL_DOCS_URL repo_key_file = "ubuntu-advantage-realtime-kernel.gpg" - is_beta = True apt_noninteractive = True supports_access_only = True @@ -74,7 +73,7 @@ ( util.prompt_for_confirmation, { - "msg": messages.REALTIME_BETA_PROMPT, + "msg": messages.REALTIME_PROMPT, "assume_yes": self.assume_yes, "default": True, }, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/repo.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/repo.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/repo.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/repo.py 2022-11-22 13:06:26.000000000 +0000 @@ -228,7 +228,7 @@ def install_packages( self, - package_list: List[str] = None, + package_list: Optional[List[str]] = None, cleanup_on_failure: bool = True, verbose: bool = True, ) -> None: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/conftest.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/conftest.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/conftest.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/conftest.py 2022-11-22 13:06:26.000000000 +0000 @@ -99,7 +99,6 @@ assume_yes: Optional[bool] = None, suites: List[str] = None, additional_packages: List[str] = None, - services_once_enabled: Dict[str, bool] = None, cfg: Optional[config.UAConfig] = None, cfg_extension: Optional[Dict[str, Any]] = None ): @@ -120,9 +119,6 @@ ), ) - if services_once_enabled: - cfg.write_cache("services-once-enabled", services_once_enabled) - args = { "allow_beta": allow_beta, "called_name": called_name, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_cc.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_cc.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_cc.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_cc.py 2022-11-22 13:06:26.000000000 +0000 @@ -50,7 +50,7 @@ "xenial", "16.04 LTS (Xenial Xerus)", "CC EAL2 is not available for platform arm64.\n" - "Supported platforms are: x86_64, ppc64le, s390x.", + "Supported platforms are: amd64, ppc64el, s390x.", ), ( "s390x", diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_entitlements.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_entitlements.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_entitlements.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_entitlements.py 2022-11-22 13:06:26.000000000 +0000 @@ -184,3 +184,63 @@ "ent6", "ent4", ] == entitlements.entitlements_enable_order(cfg=FakeConfig()) + + def test_order_entitlements_for_enabling(self, FakeConfig): + m_cls_2 = mock.MagicMock() + m_obj_2 = m_cls_2.return_value + type(m_obj_2).required_services = mock.PropertyMock(return_value=()) + type(m_cls_2).name = mock.PropertyMock(return_value="ent2") + + m_cls_1 = mock.MagicMock() + m_obj_1 = m_cls_1.return_value + type(m_obj_1).required_services = mock.PropertyMock( + return_value=(m_cls_2,) + ) + type(m_cls_1).name = mock.PropertyMock(return_value="ent1") + + m_cls_3 = mock.MagicMock() + m_obj_3 = m_cls_3.return_value + type(m_obj_3).required_services = mock.PropertyMock( + return_value=(m_cls_1, m_cls_2) + ) + type(m_cls_3).name = mock.PropertyMock(return_value="ent3") + + m_cls_5 = mock.MagicMock() + m_obj_5 = m_cls_5.return_value + type(m_obj_5).required_services = mock.PropertyMock(return_value=()) + type(m_cls_5).name = mock.PropertyMock(return_value="ent5") + + m_cls_6 = mock.MagicMock() + m_obj_6 = m_cls_6.return_value + type(m_obj_6).required_services = mock.PropertyMock(return_value=()) + type(m_cls_6).name = mock.PropertyMock(return_value="ent6") + + m_cls_4 = mock.MagicMock() + m_obj_4 = m_cls_4.return_value + type(m_obj_4).required_services = mock.PropertyMock( + return_value=(m_cls_5, m_cls_6) + ) + type(m_cls_4).name = mock.PropertyMock(return_value="ent4") + + m_entitlements = [ + m_cls_1, + m_cls_2, + m_cls_3, + m_cls_4, + m_cls_5, + m_cls_6, + ] + + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASSES", m_entitlements + ): + assert [ + "ent2", + "ent5", + "ent4", + "notthere", + "ent6typo", + ] == entitlements.order_entitlements_for_enabling( + cfg=FakeConfig(), + ents=["ent4", "notthere", "ent2", "ent6typo", "ent5"], + ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_fips.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_fips.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_fips.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_fips.py 2022-11-22 13:06:26.000000000 +0000 @@ -10,6 +10,7 @@ import mock import pytest +import uaclient.entitlements.fips as fips from uaclient import apt, defaults, exceptions, messages, system, util from uaclient.clouds.identity import NoCloudTypeReason from uaclient.entitlements.entitlement_status import ( @@ -263,7 +264,10 @@ @mock.patch("uaclient.apt.setup_apt_proxy") @mock.patch(M_PATH + "get_cloud_type", return_value=("", None)) def test_enable_configures_apt_sources_and_auth_files( - self, _m_get_cloud_type, m_setup_apt_proxy, entitlement + self, + _m_get_cloud_type, + m_setup_apt_proxy, + entitlement, ): """When entitled, configure apt repo auth token, pinning and url.""" patched_packages = ["a", "b"] @@ -283,7 +287,7 @@ ) m_installed_pkgs = stack.enter_context( mock.patch( - "uaclient.apt.get_installed_packages", + "uaclient.apt.get_installed_packages_names", return_value=["openssh-server", "strongswan"], ) ) @@ -300,6 +304,9 @@ mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) ) stack.enter_context(mock.patch(M_REPOPATH + "os.path.exists")) + stack.enter_context( + mock.patch.object(fips, "services_once_enabled_file") + ) # Note that this patch uses a PropertyMock and happens on the # entitlement's type because packages is a property m_packages = mock.PropertyMock(return_value=patched_packages) @@ -449,7 +456,8 @@ entitlement, ): m_repo_enable.return_value = repo_enable_return_value - assert repo_enable_return_value is entitlement._perform_enable() + with mock.patch.object(fips, "services_once_enabled_file"): + assert repo_enable_return_value is entitlement._perform_enable() assert ( expected_remove_notice_calls == m_remove_notice.call_args_list[:2] ) @@ -660,20 +668,25 @@ entitlement_factory, ): m_handle_message_op.return_value = True - fips_entitlement = entitlement_factory( - FIPSEntitlement, services_once_enabled={"fips-updates": True} - ) + fake_dof = mock.MagicMock() + fake_ua_file = mock.MagicMock() + fake_ua_file.to_json.return_value = {"fips_updates": True} + fake_dof.read.return_value = fake_ua_file + fips_entitlement = entitlement_factory(FIPSEntitlement) with mock.patch.object( fips_entitlement, "_allow_fips_on_cloud_instance" ) as m_allow_fips_on_cloud: m_allow_fips_on_cloud.return_value = True - result, reason = fips_entitlement.enable() - assert not result - expected_msg = ( - "Cannot enable FIPS because FIPS Updates was once enabled." - ) - assert expected_msg.strip() == reason.message.msg.strip() + with mock.patch.object( + fips, "services_once_enabled_file", fake_dof + ): + result, reason = fips_entitlement.enable() + assert not result + expected_msg = ( + "Cannot enable FIPS because FIPS Updates was once enabled." + ) + assert expected_msg.strip() == reason.message.msg.strip() @mock.patch("uaclient.system.get_platform_info") @mock.patch("uaclient.entitlements.fips.get_cloud_type") @@ -843,7 +856,7 @@ @pytest.mark.parametrize("installed_pkgs", (["sl"], ["ubuntu-fips", "sl"])) @mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) @mock.patch(M_PATH + "system.subp") - @mock.patch(M_PATH + "apt.get_installed_packages") + @mock.patch(M_PATH + "apt.get_installed_packages_names") def test_remove_packages_only_removes_if_package_is_installed( self, m_get_installed_packages, @@ -875,7 +888,7 @@ @mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) @mock.patch(M_PATH + "system.subp") - @mock.patch(M_PATH + "apt.get_installed_packages") + @mock.patch(M_PATH + "apt.get_installed_packages_names") def test_remove_packages_output_message_when_fail( self, m_get_installed_packages, m_subp, _m_get_platform, entitlement ): @@ -1073,7 +1086,7 @@ with pytest.raises(exceptions.UserFacingError): entitlement.install_packages() - @mock.patch(M_PATH + "apt.get_installed_packages") + @mock.patch(M_PATH + "apt.get_installed_packages_names") @mock.patch(M_PATH + "apt.run_apt_install_command") def test_install_packages_dont_fail_if_conditional_pkgs_not_installed( self, @@ -1173,7 +1186,7 @@ class TestFipsEntitlementPackages: - @mock.patch(M_PATH + "apt.get_installed_packages", return_value=[]) + @mock.patch(M_PATH + "apt.get_installed_packages_names", return_value=[]) @mock.patch("uaclient.system.get_platform_info") def test_packages_is_list(self, m_platform_info, _mock, entitlement): """RepoEntitlement.enable will fail if it isn't""" @@ -1184,7 +1197,7 @@ assert isinstance(entitlement.packages, list) - @mock.patch(M_PATH + "apt.get_installed_packages", return_value=[]) + @mock.patch(M_PATH + "apt.get_installed_packages_names", return_value=[]) @mock.patch("uaclient.system.get_platform_info") def test_fips_required_packages_included( self, m_platform_info, _mock, entitlement @@ -1210,7 +1223,7 @@ assert sorted(FIPS_ADDITIONAL_PACKAGES) == sorted(entitlement.packages) - @mock.patch(M_PATH + "apt.get_installed_packages") + @mock.patch(M_PATH + "apt.get_installed_packages_names") @mock.patch("uaclient.system.get_platform_info") def test_multiple_packages_calls_dont_mutate_state( self, m_platform_info, m_get_installed_packages, entitlement @@ -1230,77 +1243,6 @@ assert before == after - @pytest.mark.parametrize( - "cfg_disable_fips_metapckage_override", (True, False) - ) - @pytest.mark.parametrize("series", ("xenial", "bionic", "focal")) - @pytest.mark.parametrize( - "cloud_id", - ( - "azure-china", - "aws-gov", - "aws-china", - "azure", - "aws", - "gce", - None, - ), - ) - @mock.patch("uaclient.util.is_config_value_true") - @mock.patch(M_PATH + "get_cloud_type") - @mock.patch("uaclient.system.get_platform_info") - @mock.patch("uaclient.apt.get_installed_packages") - def test_packages_are_override_on_cloud_instance( - self, - m_installed_packages, - m_platform_info, - m_get_cloud_type, - m_is_config_value, - cloud_id, - series, - cfg_disable_fips_metapckage_override, - fips_entitlement_factory, - ): - m_platform_info.return_value = {"series": series} - m_get_cloud_type.return_value = ( - (cloud_id, None) - if cloud_id is not None - else (None, NoCloudTypeReason.NO_CLOUD_DETECTED) - ) - m_installed_packages.return_value = [] - m_is_config_value.return_value = cfg_disable_fips_metapckage_override - additional_packages = ["test1", "ubuntu-fips", "test2"] - entitlement = fips_entitlement_factory( - additional_packages=additional_packages - ) - - packages = entitlement.packages - - if all( - [ - series in ("bionic", "focal"), - cloud_id - in ( - "azure", - "aws", - "aws-china", - "aws-gov", - "azure-china", - "gce", - ), - not cfg_disable_fips_metapckage_override, - ] - ): - cloud_id = cloud_id.split("-")[0] - cloud_id = "gcp" if cloud_id == "gce" else cloud_id - assert packages == [ - "test1", - "ubuntu-{}-fips".format(cloud_id), - "test2", - ] - else: - assert packages == additional_packages - class TestFIPSUpdatesEntitlementEnable: @pytest.mark.parametrize("enable_ret", ((True), (False))) @@ -1316,32 +1258,26 @@ entitlement_factory, ): m_perform_enable.return_value = enable_ret - m_write_cache = mock.MagicMock() - m_read_cache = mock.MagicMock() - m_read_cache.return_value = {} - - cfg = mock.MagicMock() - cfg.read_cache = m_read_cache - cfg.write_cache = m_write_cache - cfg.notice_file.try_remove = m_remove_notice + fake_file = mock.MagicMock() + fake_file.read.return_value = None - fips_updates_ent = entitlement_factory(FIPSUpdatesEntitlement, cfg=cfg) - assert fips_updates_ent._perform_enable() == enable_ret + with mock.patch.object( + fips, "services_once_enabled_file", fake_file + ) as m_services_once_enabled: + cfg = mock.MagicMock() + cfg.notice_file.try_remove = m_remove_notice + fips_updates_ent = entitlement_factory( + FIPSUpdatesEntitlement, cfg=cfg + ) + assert fips_updates_ent._perform_enable() == enable_ret if enable_ret: - assert 1 == m_read_cache.call_count - assert 1 == m_write_cache.call_count - assert [ - mock.call( - key="services-once-enabled", content={"fips-updates": True} - ) - ] == m_write_cache.call_args_list + assert 1 == m_services_once_enabled.write.call_count assert [ mock.call("", messages.FIPS_DISABLE_REBOOT_REQUIRED) ] == m_remove_notice.call_args_list else: - assert not m_read_cache.call_count - assert not m_write_cache.call_count + assert not m_services_once_enabled.write.call_count class TestFIPSUpdatesEntitlementCanEnable: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_livepatch.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_livepatch.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/entitlements/tests/test_livepatch.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/entitlements/tests/test_livepatch.py 2022-11-22 13:06:26.000000000 +0000 @@ -562,7 +562,7 @@ assert CanEnableFailureReason.INAPPLICABLE == reason.reason msg = ( "Livepatch is not available for platform ppc64le.\n" - "Supported platforms are: x86_64." + "Supported platforms are: amd64." ) assert msg == reason.message.msg diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/exceptions.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/exceptions.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/exceptions.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/exceptions.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,5 +1,5 @@ import textwrap -from typing import Dict, Optional +from typing import Any, Dict, Optional from urllib import error from uaclient import messages @@ -21,7 +21,7 @@ self, msg: str, msg_code: Optional[str] = None, - additional_info: Optional[Dict[str, str]] = None, + additional_info: Optional[Dict[str, Any]] = None, ) -> None: self.msg = msg self.msg_code = msg_code @@ -131,44 +131,19 @@ ) -class BetaServiceError(UserFacingError): - """ - An exception to be raised trying to interact with beta service - without the right parameters. - - :param msg: - Takes a single parameter, which is the beta service error message that - should be emitted before exiting non-zero. - """ - - pass - - class NonAutoAttachImageError(UserFacingError): """Raised when machine isn't running an auto-attach enabled image""" exit_code = 0 -class AlreadyAttachedOnPROError(UserFacingError): - """Raised when a PRO machine retries attaching with the same instance-id""" - - exit_code = 0 - - def __init__(self): - msg = messages.ALREADY_ATTACHED_ON_PRO - super().__init__(msg=msg.msg, msg_code=msg.name) - - class AlreadyAttachedError(UserFacingError): """An exception to be raised when a command needs an unattached system.""" exit_code = 2 - def __init__(self, cfg): - msg = messages.ALREADY_ATTACHED.format( - account_name=cfg.machine_token_file.accounts[0].get("name", "") - ) + def __init__(self, account_name: str): + msg = messages.ALREADY_ATTACHED.format(account_name=account_name) super().__init__(msg=msg.msg, msg_code=msg.name) @@ -256,6 +231,7 @@ def __init__(self, lock_request: str, lock_holder: str, pid: int): self.lock_holder = lock_holder + self.pid = pid msg = messages.LOCK_HELD_ERROR.format( lock_request=lock_request, lock_holder=lock_holder, pid=pid ) @@ -304,6 +280,7 @@ class InvalidProImage(UserFacingError): def __init__(self, error_msg: str): + self.contract_server_msg = error_msg msg = messages.INVALID_PRO_IMAGE.format(msg=error_msg) super().__init__(msg=msg.msg, msg_code=msg.name) @@ -338,8 +315,10 @@ pass -class EntitlementNotFoundError(Exception): - pass +class EntitlementNotFoundError(UserFacingError): + def __init__(self, entitlement_name: str): + msg = messages.ENTITLEMENT_NOT_FOUND.format(name=entitlement_name) + super().__init__(msg=msg.msg, msg_code=msg.name) class UrlError(IOError): diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/data_types.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/data_types.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/data_types.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/data_types.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,72 @@ +import json +from enum import Enum +from typing import Callable, Dict, Generic, Optional, Type, TypeVar + +import yaml + +from uaclient import exceptions +from uaclient.data_types import DataObject +from uaclient.files.files import UAFile + + +class DataObjectFileFormat(Enum): + JSON = "json" + YAML = "yaml" + + +DOFType = TypeVar("DOFType", bound=DataObject) + + +class DataObjectFile(Generic[DOFType]): + def __init__( + self, + data_object_cls: Type[DOFType], + ua_file: UAFile, + file_format: DataObjectFileFormat = DataObjectFileFormat.JSON, + preprocess_data: Optional[Callable[[Dict], Dict]] = None, + ): + self.data_object_cls = data_object_cls + self.ua_file = ua_file + self.file_format = file_format + self.preprocess_data = preprocess_data + + def read(self) -> Optional[DOFType]: + raw_data = self.ua_file.read() + if raw_data is None: + return None + + parsed_data = None + if self.file_format == DataObjectFileFormat.JSON: + try: + parsed_data = json.loads(raw_data) + except json.JSONDecodeError: + raise exceptions.InvalidFileFormatError( + self.ua_file.path, "json" + ) + elif self.file_format == DataObjectFileFormat.YAML: + try: + parsed_data = yaml.safe_load(raw_data) + except yaml.parser.ParserError: + raise exceptions.InvalidFileFormatError( + self.ua_file.path, "yaml" + ) + + if parsed_data is None: + return None + + if self.preprocess_data: + parsed_data = self.preprocess_data(parsed_data) + + return self.data_object_cls.from_dict(parsed_data) + + def write(self, content: DOFType): + if self.file_format == DataObjectFileFormat.JSON: + str_content = content.to_json() + elif self.file_format == DataObjectFileFormat.YAML: + data = content.to_dict() + str_content = yaml.dump(data, default_flow_style=False) + + self.ua_file.write(str_content) + + def delete(self): + self.ua_file.delete() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/files.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/files.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/files.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,366 @@ +import json +import logging +import os +import re +from datetime import datetime +from typing import Any, Dict, Optional + +from uaclient import defaults, event_logger, exceptions, messages, system, util +from uaclient.contract_data_types import PublicMachineTokenData + +event = event_logger.get_event_logger() +LOG = logging.getLogger(__name__) + + +class UAFile: + def __init__( + self, + name: str, + directory: str = defaults.DEFAULT_DATA_DIR, + private: bool = True, + ): + self._directory = directory + self._file_name = name + self._is_private = private + self._path = os.path.join(self._directory, self._file_name) + + @property + def path(self) -> str: + return self._path + + @property + def is_private(self) -> bool: + return self._is_private + + @property + def is_present(self): + return os.path.exists(self.path) + + def write(self, content: str): + file_mode = ( + defaults.ROOT_READABLE_MODE + if self.is_private + else defaults.WORLD_READABLE_MODE + ) + if not os.path.exists(self._directory): + os.makedirs(self._directory) + if os.path.basename(self._directory) == defaults.PRIVATE_SUBDIR: + os.chmod(self._directory, 0o700) + system.write_file(self.path, content, file_mode) + + def read(self) -> Optional[str]: + content = None + try: + content = system.load_file(self.path) + except FileNotFoundError: + LOG.debug("File does not exist: {}".format(self.path)) + return content + + def delete(self): + system.remove_file(self.path) + + +class MachineTokenFile: + def __init__( + self, + directory: str = defaults.DEFAULT_DATA_DIR, + root_mode: bool = True, + machine_token_overlay_path: Optional[str] = None, + ): + file_name = defaults.MACHINE_TOKEN_FILE + self.is_root = root_mode + self.private_file = UAFile( + file_name, directory + defaults.PRIVATE_SUBDIR + ) + self.public_file = UAFile(file_name, directory, False) + self.machine_token_overlay_path = machine_token_overlay_path + self._machine_token = None + self._entitlements = None + self._contract_expiry_datetime = None + + def write(self, content: dict): + """Update the machine_token file for both pub/private files""" + if self.is_root: + content_str = json.dumps( + content, cls=util.DatetimeAwareJSONEncoder + ) + self.private_file.write(content_str) + content = json.loads(content_str) # taking care of datetime type + content = PublicMachineTokenData.from_dict(content).to_dict(False) + content_str = json.dumps( + content, cls=util.DatetimeAwareJSONEncoder + ) + self.public_file.write(content_str) + + self._machine_token = None + self._entitlements = None + self._contract_expiry_datetime = None + else: + raise exceptions.NonRootUserError() + + def delete(self): + """Delete both pub and private files""" + if self.is_root: + self.public_file.delete() + self.private_file.delete() + + self._machine_token = None + self._entitlements = None + self._contract_expiry_datetime = None + else: + raise exceptions.NonRootUserError() + + def read(self) -> Optional[dict]: + if self.is_root: + file_handler = self.private_file + else: + file_handler = self.public_file + content = file_handler.read() + if not content: + return None + try: + content = json.loads(content, cls=util.DatetimeAwareJSONDecoder) + except Exception: + pass + return content # type: ignore + + @property + def is_present(self): + if self.is_root: + return self.public_file.is_present and self.private_file.is_present + else: + return self.public_file.is_present + + @property + def machine_token(self): + """Return the machine-token if cached in the machine token response.""" + if not self._machine_token: + content = self.read() + if content and self.machine_token_overlay_path: + machine_token_overlay = self.parse_machine_token_overlay( + self.machine_token_overlay_path + ) + + if machine_token_overlay: + util.depth_first_merge_overlay_dict( + base_dict=content, + overlay_dict=machine_token_overlay, + ) + self._machine_token = content + return self._machine_token + + def parse_machine_token_overlay(self, machine_token_overlay_path): + if not os.path.exists(machine_token_overlay_path): + raise exceptions.UserFacingError( + messages.INVALID_PATH_FOR_MACHINE_TOKEN_OVERLAY.format( + file_path=machine_token_overlay_path + ) + ) + + try: + machine_token_overlay_content = system.load_file( + machine_token_overlay_path + ) + + return json.loads( + machine_token_overlay_content, + cls=util.DatetimeAwareJSONDecoder, + ) + except ValueError as e: + raise exceptions.UserFacingError( + messages.ERROR_JSON_DECODING_IN_FILE.format( + error=str(e), file_path=machine_token_overlay_path + ) + ) + + @property + def account(self) -> Optional[dict]: + if bool(self.machine_token): + return self.machine_token["machineTokenInfo"]["accountInfo"] + return None + + @property + def entitlements(self): + """Return configured entitlements keyed by entitlement named""" + if self._entitlements: + return self._entitlements + if not self.machine_token: + return {} + self._entitlements = self.get_entitlements_from_token( + self.machine_token + ) + return self._entitlements + + @staticmethod + def get_entitlements_from_token(machine_token: Dict): + """Return a dictionary of entitlements keyed by entitlement name. + + Return an empty dict if no entitlements are present. + """ + from uaclient.contract import apply_contract_overrides + + if not machine_token: + return {} + + entitlements = {} + contractInfo = machine_token.get("machineTokenInfo", {}).get( + "contractInfo" + ) + if not contractInfo: + return {} + + tokens_by_name = dict( + (e.get("type"), e.get("token")) + for e in machine_token.get("resourceTokens", []) + ) + ent_by_name = dict( + (e.get("type"), e) + for e in contractInfo.get("resourceEntitlements", []) + ) + for entitlement_name, ent_value in ent_by_name.items(): + entitlement_cfg = {"entitlement": ent_value} + if entitlement_name in tokens_by_name: + entitlement_cfg["resourceToken"] = tokens_by_name[ + entitlement_name + ] + apply_contract_overrides(entitlement_cfg) + entitlements[entitlement_name] = entitlement_cfg + return entitlements + + @property + def contract_expiry_datetime(self) -> datetime: + """Return a datetime of the attached contract expiration.""" + if not self._contract_expiry_datetime: + self._contract_expiry_datetime = self.machine_token[ + "machineTokenInfo" + ]["contractInfo"]["effectiveTo"] + + return self._contract_expiry_datetime # type: ignore + + @property + def is_attached(self): + """Report whether this machine configuration is attached to UA.""" + return bool(self.machine_token) # machine_token is removed on detach + + @property + def contract_remaining_days(self) -> int: + """Report num days until contract expiration based on effectiveTo + + :return: A positive int representing the number of days the attached + contract remains in effect. Return a negative int for the number + of days beyond contract's effectiveTo date. + """ + delta = self.contract_expiry_datetime.date() - datetime.utcnow().date() + return delta.days + + @property + def activity_token(self) -> "Optional[str]": + if self.machine_token: + return self.machine_token.get("activityInfo", {}).get( + "activityToken" + ) + return None + + @property + def activity_id(self) -> "Optional[str]": + if self.machine_token: + return self.machine_token.get("activityInfo", {}).get("activityID") + return None + + @property + def activity_ping_interval(self) -> "Optional[int]": + if self.machine_token: + return self.machine_token.get("activityInfo", {}).get( + "activityPingInterval" + ) + return None + + @property + def contract_id(self): + if self.machine_token: + return ( + self.machine_token.get("machineTokenInfo", {}) + .get("contractInfo", {}) + .get("id") + ) + return None + + +class NoticeFile: + def __init__( + self, + directory: str = defaults.DEFAULT_DATA_DIR, + root_mode: bool = True, + ): + file_name = "notices.json" + self.file = UAFile(file_name, directory, False) + self.is_root = root_mode + + def add(self, label: str, description: str): + """ + Adds a notice to the notices.json file. + Raises a NonRootUserError if the user is not root. + """ + if self.is_root: + notices = self.read() or [] + notice = [label, description] + if notice not in notices: + notices.append(notice) + self.write(notices) + else: + raise exceptions.NonRootUserError + + def try_add(self, label: str, description: str): + """ + Adds a notice to the notices.json file. + Logs a warning when adding as non-root + """ + try: + self.add(label, description) + except exceptions.NonRootUserError: + event.warning("Trying to add notice as non-root user") + + def remove(self, label_regex: str, descr_regex: str): + """ + Removes a notice to the notices.json file. + Raises a NonRootUserError if the user is not root. + """ + if self.is_root: + notices = [] + cached_notices = self.read() or [] + if cached_notices: + for notice_label, notice_descr in cached_notices: + if re.match(label_regex, notice_label): + if re.match(descr_regex, notice_descr): + continue + notices.append((notice_label, notice_descr)) + if notices: + self.write(notices) + elif os.path.exists(self.file.path): + self.file.delete() + else: + raise exceptions.NonRootUserError + + def try_remove(self, label_regex: str, descr_regex: str): + """ + Removes a notice to the notices.json file. + Logs a warning when removing as non-root + """ + try: + self.remove(label_regex, descr_regex) + except exceptions.NonRootUserError: + event.warning("Trying to remove notice as non-root user") + + def read(self): + content = self.file.read() + if not content: + return None + try: + return json.loads(content, cls=util.DatetimeAwareJSONDecoder) + except ValueError: + return content + + def write(self, content: Any): + if not isinstance(content, str): + content = json.dumps(content, cls=util.DatetimeAwareJSONEncoder) + self.file.write(content) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/__init__.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/__init__.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/__init__.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,10 @@ +from uaclient.files.data_types import DataObjectFile, DataObjectFileFormat +from uaclient.files.files import MachineTokenFile, NoticeFile, UAFile + +__all__ = [ + "DataObjectFile", + "DataObjectFileFormat", + "MachineTokenFile", + "NoticeFile", + "UAFile", +] diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/state_files.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/state_files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/state_files.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/state_files.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,96 @@ +from typing import Any, Dict, List, Optional + +from uaclient.data_types import ( + BoolDataValue, + DataObject, + Field, + IntDataValue, + StringDataValue, + data_list, +) +from uaclient.files.data_types import DataObjectFile, DataObjectFileFormat +from uaclient.files.files import UAFile + +SERVICES_ONCE_ENABLED = "services-once-enabled" + + +class ServicesOnceEnabledData(DataObject): + fields = [ + Field("fips_updates", BoolDataValue, False), + ] + + def __init__(self, fips_updates: bool): + self.fips_updates = fips_updates + + +def _services_once_enable_preprocess_data( + data: Dict[str, Any] +) -> Dict[str, Any]: + # Since we are using now returning DataObject instances from read, we + # cannot have variables with "-" in them. We need to explictly convert + # them before creating the object + updated_data = {} + for key in data.keys(): + if "-" in key: + updated_data[key.replace("-", "_")] = True + else: + updated_data[key] = True + + return updated_data + + +services_once_enabled_file = DataObjectFile( + data_object_cls=ServicesOnceEnabledData, + ua_file=UAFile( + name=SERVICES_ONCE_ENABLED, + private=False, + ), + preprocess_data=_services_once_enable_preprocess_data, +) + + +class RetryAutoAttachOptions(DataObject): + fields = [ + Field("enable", data_list(StringDataValue), False), + Field("enable_beta", data_list(StringDataValue), False), + ] + + def __init__( + self, + enable: Optional[List[str]] = None, + enable_beta: Optional[List[str]] = None, + ): + self.enable = enable + self.enable_beta = enable_beta + + +retry_auto_attach_options_file = DataObjectFile( + RetryAutoAttachOptions, + UAFile( + "retry-auto-attach-options.json", + private=True, + ), + DataObjectFileFormat.JSON, +) + + +class RetryAutoAttachState(DataObject): + fields = [ + Field("interval_index", IntDataValue), + Field("failure_reason", StringDataValue, required=False), + ] + + def __init__( + self, + interval_index: int, + failure_reason: Optional[str], + ): + self.interval_index = interval_index + self.failure_reason = failure_reason + + +retry_auto_attach_state_file = DataObjectFile( + RetryAutoAttachState, + UAFile("retry-auto-attach-state.json", private=True), + DataObjectFileFormat.JSON, +) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_data_types.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_data_types.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_data_types.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_data_types.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,123 @@ +import mock +import pytest + +from uaclient import exceptions +from uaclient.data_types import ( + DataObject, + Field, + IncorrectFieldTypeError, + IntDataValue, + StringDataValue, +) +from uaclient.files.data_types import DataObjectFile, DataObjectFileFormat + + +class MockUAFile: + def __init__(self): + self.write = mock.MagicMock() + self.read = mock.MagicMock() + self.delete = mock.MagicMock() + self.path = mock.MagicMock() + + +class NestedTestData(DataObject): + fields = [ + Field("integer", IntDataValue), + ] + + def __init__(self, integer: int): + self.integer = integer + + +class TestData(DataObject): + fields = [ + Field("string", StringDataValue), + Field("nested", NestedTestData), + ] + + def __init__(self, string: str, nested: NestedTestData): + self.string = string + self.nested = nested + + +class TestDataObjectFile: + def test_write_valid_json(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + ) + dof.write(TestData(string="test", nested=NestedTestData(integer=1))) + assert mock_file.write.call_args_list == [ + mock.call("""{"nested": {"integer": 1}, "string": "test"}""") + ] + + def test_write_valid_yaml(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + DataObjectFileFormat.YAML, + ) + dof.write(TestData(string="test", nested=NestedTestData(integer=1))) + assert mock_file.write.call_args_list == [ + mock.call("""nested:\n integer: 1\nstring: test\n""") + ] + + def test_read_valid_json(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + ) + mock_file.read.return_value = ( + """{"string": "test", "nested": {"integer": 1}}""" + ) + do = dof.read() + assert do.string == "test" + assert do.nested.integer == 1 + + def test_read_valid_yaml(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + DataObjectFileFormat.YAML, + ) + mock_file.read.return_value = ( + """nested:\n integer: 1\nstring: test\n""" + ) + do = dof.read() + assert do.string == "test" + assert do.nested.integer == 1 + + def test_read_invalid_data(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + ) + mock_file.read.return_value = """{"nested": {"integer": 1}}""" + with pytest.raises(IncorrectFieldTypeError): + dof.read() + + def test_read_invalid_json(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + ) + mock_file.read.return_value = """{"nested": {""" + with pytest.raises(exceptions.InvalidFileFormatError): + dof.read() + + def test_read_invalid_yaml(self): + mock_file = MockUAFile() + dof = DataObjectFile( + TestData, + mock_file, + DataObjectFileFormat.YAML, + ) + mock_file.read.return_value = """nested": {""" + with pytest.raises(exceptions.InvalidFileFormatError): + dof.read() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_files.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_files.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_files.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,49 @@ +import os + +from uaclient import system +from uaclient.files import MachineTokenFile, UAFile + + +class TestUAFile: + def test_read_write(self, tmpdir): + file_name = "temp_file" + file = UAFile(file_name, tmpdir.strpath, False) + content = "dummy file words" + file.write(content) + path = os.path.join(tmpdir.strpath, file_name) + res = system.load_file(path) + assert res == file.read() + assert res == content + + +class TestMachineTokenFile: + def test_deleting(self, tmpdir): + token_file = MachineTokenFile( + directory=tmpdir.strpath, + ) + token = {"machineTokenInfo": {"machineId": "random-id"}} + token_file.write(token) + assert token_file.machine_token == token + token_file.delete() + assert token_file.machine_token is None + + def test_public_file_filtering(self, tmpdir): + # root access of machine token file + token_file = MachineTokenFile( + directory=tmpdir.strpath, + ) + token = { + "machineTokenInfo": {"machineId": "random-id"}, + "machineToken": "token", + } + token_file.write(token) + root_token = token_file.machine_token + assert token == root_token + # non root access of machine token file + token_file = MachineTokenFile( + directory=tmpdir.strpath, root_mode=False + ) + nonroot_token = token_file.machine_token + assert root_token != nonroot_token + machine_token = nonroot_token.get("machineToken", None) + assert machine_token is None diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_state_files.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_state_files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files/tests/test_state_files.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files/tests/test_state_files.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,17 @@ +import pytest + +from uaclient.files.state_files import _services_once_enable_preprocess_data + + +class TestServicesOnceEnabledPreprocessData: + @pytest.mark.parametrize( + "content", + ( + ({"fips-updates": True}), + ({"fips_updates": True}), + ), + ) + def test_add_service(self, content): + assert _services_once_enable_preprocess_data(content) == { + "fips_updates": True + } diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/files.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/files.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/files.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,433 +0,0 @@ -import json -import logging -import os -import re -from datetime import datetime -from enum import Enum -from typing import Any, Dict, Generic, Optional, Type, TypeVar - -import yaml - -from uaclient import ( - data_types, - defaults, - event_logger, - exceptions, - messages, - system, - util, -) -from uaclient.contract_data_types import PublicMachineTokenData - -event = event_logger.get_event_logger() -LOG = logging.getLogger(__name__) - - -class UAFile: - def __init__( - self, - name: str, - directory: str = defaults.DEFAULT_DATA_DIR, - private: bool = True, - ): - self._directory = directory - self._file_name = name - self._is_private = private - self._path = os.path.join(self._directory, self._file_name) - - @property - def path(self) -> str: - return self._path - - @property - def is_private(self) -> bool: - return self._is_private - - @property - def is_present(self): - return os.path.exists(self.path) - - def write(self, content: str): - file_mode = ( - defaults.ROOT_READABLE_MODE - if self.is_private - else defaults.WORLD_READABLE_MODE - ) - if not os.path.exists(self._directory): - os.makedirs(self._directory) - if os.path.basename(self._directory) == defaults.PRIVATE_SUBDIR: - os.chmod(self._directory, 0o700) - system.write_file(self.path, content, file_mode) - - def read(self) -> Optional[str]: - content = None - try: - content = system.load_file(self.path) - except FileNotFoundError: - LOG.debug("File does not exist: {}".format(self.path)) - return content - - def delete(self): - system.remove_file(self.path) - - -class DataObjectFileFormat(Enum): - JSON = "json" - YAML = "yaml" - - -DOFType = TypeVar("DOFType", bound=data_types.DataObject) - - -class DataObjectFile(Generic[DOFType]): - def __init__( - self, - data_object_cls: Type[DOFType], - ua_file: UAFile, - file_format: DataObjectFileFormat = DataObjectFileFormat.JSON, - ): - self.data_object_cls = data_object_cls - self.ua_file = ua_file - self.file_format = file_format - - def read(self) -> Optional[DOFType]: - raw_data = self.ua_file.read() - if raw_data is None: - return None - - parsed_data = None - if self.file_format == DataObjectFileFormat.JSON: - try: - parsed_data = json.loads(raw_data) - except json.JSONDecodeError: - raise exceptions.InvalidFileFormatError( - self.ua_file.path, "json" - ) - elif self.file_format == DataObjectFileFormat.YAML: - try: - parsed_data = yaml.safe_load(raw_data) - except yaml.parser.ParserError: - raise exceptions.InvalidFileFormatError( - self.ua_file.path, "yaml" - ) - - if parsed_data is None: - return None - - return self.data_object_cls.from_dict(parsed_data) - - def write(self, content: DOFType): - if self.file_format == DataObjectFileFormat.JSON: - str_content = content.to_json() - elif self.file_format == DataObjectFileFormat.YAML: - data = content.to_dict() - str_content = yaml.dump(data, default_flow_style=False) - - self.ua_file.write(str_content) - - -class MachineTokenFile: - def __init__( - self, - directory: str = defaults.DEFAULT_DATA_DIR, - root_mode: bool = True, - machine_token_overlay_path: Optional[str] = None, - ): - file_name = defaults.MACHINE_TOKEN_FILE - self.is_root = root_mode - self.private_file = UAFile( - file_name, directory + defaults.PRIVATE_SUBDIR - ) - self.public_file = UAFile(file_name, directory, False) - self.machine_token_overlay_path = machine_token_overlay_path - self._machine_token = None - self._entitlements = None - self._contract_expiry_datetime = None - - def write(self, content: dict): - """Update the machine_token file for both pub/private files""" - if self.is_root: - content_str = json.dumps( - content, cls=util.DatetimeAwareJSONEncoder - ) - self.private_file.write(content_str) - content = json.loads(content_str) # taking care of datetime type - content = PublicMachineTokenData.from_dict(content).to_dict(False) - content_str = json.dumps( - content, cls=util.DatetimeAwareJSONEncoder - ) - self.public_file.write(content_str) - - self._machine_token = None - self._entitlements = None - else: - raise exceptions.NonRootUserError() - - def delete(self): - """Delete both pub and private files""" - if self.is_root: - self.public_file.delete() - self.private_file.delete() - - self._machine_token = None - self._entitlements = None - else: - raise exceptions.NonRootUserError() - - def read(self) -> Optional[dict]: - if self.is_root: - file_handler = self.private_file - else: - file_handler = self.public_file - content = file_handler.read() - if not content: - return None - try: - content = json.loads(content, cls=util.DatetimeAwareJSONDecoder) - except Exception: - pass - return content # type: ignore - - @property - def is_present(self): - if self.is_root: - return self.public_file.is_present and self.private_file.is_present - else: - return self.public_file.is_present - - @property - def machine_token(self): - """Return the machine-token if cached in the machine token response.""" - if not self._machine_token: - content = self.read() - if content and self.machine_token_overlay_path: - machine_token_overlay = self.parse_machine_token_overlay( - self.machine_token_overlay_path - ) - - if machine_token_overlay: - util.depth_first_merge_overlay_dict( - base_dict=content, - overlay_dict=machine_token_overlay, - ) - self._machine_token = content - return self._machine_token - - def parse_machine_token_overlay(self, machine_token_overlay_path): - if not os.path.exists(machine_token_overlay_path): - raise exceptions.UserFacingError( - messages.INVALID_PATH_FOR_MACHINE_TOKEN_OVERLAY.format( - file_path=machine_token_overlay_path - ) - ) - - try: - machine_token_overlay_content = system.load_file( - machine_token_overlay_path - ) - - return json.loads( - machine_token_overlay_content, - cls=util.DatetimeAwareJSONDecoder, - ) - except ValueError as e: - raise exceptions.UserFacingError( - messages.ERROR_JSON_DECODING_IN_FILE.format( - error=str(e), file_path=machine_token_overlay_path - ) - ) - - @property - def accounts(self): - if bool(self.machine_token): - account_info = self.machine_token["machineTokenInfo"][ - "accountInfo" - ] - return [account_info] - return [] - - @property - def entitlements(self): - """Return configured entitlements keyed by entitlement named""" - if self._entitlements: - return self._entitlements - if not self.machine_token: - return {} - self._entitlements = self.get_entitlements_from_token( - self.machine_token - ) - return self._entitlements - - @staticmethod - def get_entitlements_from_token(machine_token: Dict): - """Return a dictionary of entitlements keyed by entitlement name. - - Return an empty dict if no entitlements are present. - """ - from uaclient.contract import apply_contract_overrides - - if not machine_token: - return {} - - entitlements = {} - contractInfo = machine_token.get("machineTokenInfo", {}).get( - "contractInfo" - ) - if not contractInfo: - return {} - - tokens_by_name = dict( - (e.get("type"), e.get("token")) - for e in machine_token.get("resourceTokens", []) - ) - ent_by_name = dict( - (e.get("type"), e) - for e in contractInfo.get("resourceEntitlements", []) - ) - for entitlement_name, ent_value in ent_by_name.items(): - entitlement_cfg = {"entitlement": ent_value} - if entitlement_name in tokens_by_name: - entitlement_cfg["resourceToken"] = tokens_by_name[ - entitlement_name - ] - apply_contract_overrides(entitlement_cfg) - entitlements[entitlement_name] = entitlement_cfg - return entitlements - - @property - def contract_expiry_datetime(self) -> datetime: - """Return a datetime of the attached contract expiration.""" - if not self._contract_expiry_datetime: - self._contract_expiry_datetime = self.machine_token[ - "machineTokenInfo" - ]["contractInfo"]["effectiveTo"] - - return self._contract_expiry_datetime # type: ignore - - @property - def is_attached(self): - """Report whether this machine configuration is attached to UA.""" - return bool(self.machine_token) # machine_token is removed on detach - - @property - def contract_remaining_days(self) -> int: - """Report num days until contract expiration based on effectiveTo - - :return: A positive int representing the number of days the attached - contract remains in effect. Return a negative int for the number - of days beyond contract's effectiveTo date. - """ - delta = self.contract_expiry_datetime.date() - datetime.utcnow().date() - return delta.days - - @property - def activity_token(self) -> "Optional[str]": - if self.machine_token: - return self.machine_token.get("activityInfo", {}).get( - "activityToken" - ) - return None - - @property - def activity_id(self) -> "Optional[str]": - if self.machine_token: - return self.machine_token.get("activityInfo", {}).get("activityID") - return None - - @property - def activity_ping_interval(self) -> "Optional[int]": - if self.machine_token: - return self.machine_token.get("activityInfo", {}).get( - "activityPingInterval" - ) - return None - - @property - def contract_id(self): - if self.machine_token: - return ( - self.machine_token.get("machineTokenInfo", {}) - .get("contractInfo", {}) - .get("id") - ) - return None - - -class NoticeFile: - def __init__( - self, - directory: str = defaults.DEFAULT_DATA_DIR, - root_mode: bool = True, - ): - file_name = "notices.json" - self.file = UAFile(file_name, directory, False) - self.is_root = root_mode - - def add(self, label: str, description: str): - """ - Adds a notice to the notices.json file. - Raises a NonRootUserError if the user is not root. - """ - if self.is_root: - notices = self.read() or [] - notice = [label, description] - if notice not in notices: - notices.append(notice) - self.write(notices) - else: - raise exceptions.NonRootUserError - - def try_add(self, label: str, description: str): - """ - Adds a notice to the notices.json file. - Logs a warning when adding as non-root - """ - try: - self.add(label, description) - except exceptions.NonRootUserError: - event.warning("Trying to add notice as non-root user") - - def remove(self, label_regex: str, descr_regex: str): - """ - Removes a notice to the notices.json file. - Raises a NonRootUserError if the user is not root. - """ - if self.is_root: - notices = [] - cached_notices = self.read() or [] - if cached_notices: - for notice_label, notice_descr in cached_notices: - if re.match(label_regex, notice_label): - if re.match(descr_regex, notice_descr): - continue - notices.append((notice_label, notice_descr)) - if notices: - self.write(notices) - elif os.path.exists(self.file.path): - self.file.delete() - else: - raise exceptions.NonRootUserError - - def try_remove(self, label_regex: str, descr_regex: str): - """ - Removes a notice to the notices.json file. - Logs a warning when removing as non-root - """ - try: - self.remove(label_regex, descr_regex) - except exceptions.NonRootUserError: - event.warning("Trying to remove notice as non-root user") - - def read(self): - content = self.file.read() - if not content: - return None - try: - return json.loads(content, cls=util.DatetimeAwareJSONDecoder) - except ValueError: - return content - - def write(self, content: Any): - if not isinstance(content, str): - content = json.dumps(content, cls=util.DatetimeAwareJSONEncoder) - self.file.write(content) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/jobs/tests/test_update_messaging.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/jobs/tests/test_update_messaging.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/jobs/tests/test_update_messaging.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/jobs/tests/test_update_messaging.py 2022-11-22 13:06:26.000000000 +0000 @@ -671,3 +671,59 @@ ) assert write_apt_calls == write_apt_and_motd_templates.call_args_list assert subp_calls == subp.call_args_list + + @pytest.mark.parametrize( + "expiry_status,call_count", + ( + (ContractExpiryStatus.ACTIVE, 0), + (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, 1), + (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, 1), + ), + ) + @mock.patch(M_PATH + "update_contract_expiry") + @mock.patch(M_PATH + "get_contract_expiry_status") + @mock.patch(M_PATH + "system.is_lts") + @mock.patch(M_PATH + "system.is_active_esm") + @mock.patch(M_PATH + "write_apt_and_motd_templates") + @mock.patch(M_PATH + "write_esm_announcement_message") + @mock.patch(M_PATH + "system.subp") + @mock.patch(M_PATH + "system.get_platform_info") + def test_contract_expiry_motd_and_apt_messages( + self, + _get_platform_info, + _subp, + _write_esm_announcement_message, + _write_apt_and_motd_templates, + _is_active_esm, + _util_is_lts, + get_contract_expiry_status, + update_contract_expiry, + expiry_status, + call_count, + FakeConfig, + ): + get_contract_expiry_status.return_value = expiry_status, 0 + cfg = FakeConfig.for_attached_machine() + update_apt_and_motd_messages(cfg) + assert update_contract_expiry.call_count == call_count + + @pytest.mark.parametrize( + "expiry,is_updated", + (("2040-05-08T19:02:26Z", False), ("2042-05-08T19:02:26Z", True)), + ) + @mock.patch("uaclient.files.MachineTokenFile.write") + @mock.patch(M_PATH + "contract.UAContractClient.get_updated_contract_info") + def test_update_contract_expiry( + self, + get_updated_contract_info, + machine_token_write, + expiry, + is_updated, + ): + get_updated_contract_info.return_value = { + "machineTokenInfo": {"contractInfo": {"effectiveTo": expiry}} + } + if is_updated: + 1 == machine_token_write.call_count + else: + 0 == machine_token_write.call_count diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/jobs/update_messaging.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/jobs/update_messaging.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/jobs/update_messaging.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/jobs/update_messaging.py 2022-11-22 13:06:26.000000000 +0000 @@ -13,7 +13,7 @@ from os.path import exists from typing import List, Tuple -from uaclient import config, defaults, entitlements, system, util +from uaclient import config, contract, defaults, entitlements, system, util from uaclient.clouds import identity from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.messages import ( @@ -378,6 +378,13 @@ system.remove_file(msg_path.replace(".tmpl", "")) return True + expiry_status, _ = get_contract_expiry_status(cfg) + if expiry_status not in ( + ContractExpiryStatus.ACTIVE, + ContractExpiryStatus.NONE, + ): + update_contract_expiry(cfg) + # Announce ESM availabilty on active ESM LTS releases # write_esm_announcement_message(cfg, series) @@ -407,3 +414,33 @@ logging.exception(exc) system.subp(["sudo", "systemctl", "restart", "motd-news.service"]) + + +def update_contract_expiry(cfg: config.UAConfig): + orig_token = cfg.machine_token + machine_token = orig_token.get("machineToken", "") + contract_id = ( + orig_token.get("machineTokenInfo", {}) + .get("contractInfo", {}) + .get("id", None) + ) + + contract_client = contract.UAContractClient(cfg) + resp = contract_client.get_updated_contract_info( + machine_token, contract_id + ) + resp_expiry = ( + resp.get("machineTokenInfo", {}) + .get("contractInfo", {}) + .get("effectiveTo", None) + ) + new_expiry = ( + util.parse_rfc3339_date(resp_expiry) + if resp_expiry + else cfg.machine_token_file.contract_expiry_datetime + ) + if cfg.machine_token_file.contract_expiry_datetime != new_expiry: + orig_token["machineTokenInfo"]["contractInfo"][ + "effectiveTo" + ] = new_expiry + cfg.machine_token_file.write(orig_token) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/messages.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/messages.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/messages.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/messages.py 2022-11-22 13:06:26.000000000 +0000 @@ -12,6 +12,20 @@ # useful if the message represents an error. self.additional_info = None # type: Optional[Dict[str, str]] + def __eq__(self, other): + return ( + self.msg == other.msg + and self.name == other.name + and self.additional_info == other.additional_info + ) + + def __repr__(self): + return "NamedMessage({}, {}, {})".format( + self.name.__repr__(), + self.msg.__repr__(), + self.additional_info.__repr__(), + ) + class FormattedNamedMessage(NamedMessage): def __init__(self, name: str, msg: str): @@ -581,12 +595,6 @@ ), ) -ALREADY_ATTACHED_ON_PRO = NamedMessage( - "already-attached-on-pro", - """\ -Skipping auto-attach: Instance is already attached.""", -) - CONNECTIVITY_ERROR = NamedMessage( "connectivity-error", """\ @@ -781,21 +789,20 @@ "Use `pro enable realtime-kernel --beta` to acknowledge the real-time" " kernel is currently in beta and comes with no support.", ) -REALTIME_BETA_PROMPT = """\ -The real-time kernel is a beta version of the 22.04 Ubuntu kernel with the -PREEMPT_RT patchset integrated for x86_64 and ARM64. +REALTIME_PROMPT = """\ +The real-time kernel is an Ubuntu kernel with PREEMPT_RT patches integrated. {bold}\ -This will change your kernel. You will need to manually configure grub to -revert back to your original kernel after enabling real-time.\ +This will change your kernel. To revert to your original kernel, you will need +to make the change manually.\ {end_bold} Do you want to continue? [ default = Yes ]: (Y/n) """.format( bold=TxtColor.BOLD, end_bold=TxtColor.ENDC ) REALTIME_PRE_DISABLE_PROMPT = """\ -This will disable the Real-Time Kernel entitlement but the Real-Time Kernel\ - will remain installed. +This will disable Ubuntu Pro updates to the real-time kernel on this machine. +The real-time kernel will remain installed.\ Are you sure? (y/N) """ REALTIME_ERROR_INSTALL_ON_CONTAINER = NamedMessage( @@ -810,10 +817,6 @@ "For more information, " "see https://cloud.google.com/iam/docs/service-accounts", ) -FULL_AUTO_ATTACH_ERROR = NamedMessage( - "full-auto-attach-error", - "full_auto_attach was not successful", -) LOG_CONNECTIVITY_ERROR_TMPL = CONNECTIVITY_ERROR.msg + " {error}" LOG_CONNECTIVITY_ERROR_WITH_URL_TMPL = ( @@ -859,16 +862,6 @@ 3. sudo pro enable fips --assume-yes 4. sudo reboot """ -NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD = """\ -Detected an Ubuntu Pro license but failed to auto attach because -"{operation}" was in progress. -Please run `pro auto-attach` to upgrade to Ubuntu Pro. -""" -NOTICE_DAEMON_AUTO_ATTACH_FAILED = """\ -Detected an Ubuntu Pro license but failed to auto attach. -Please run `pro auto-attach` to upgrade to Ubuntu Pro. -If that fails then please contact support. -""" PROMPT_YES_NO = """Are you sure? (y/N) """ PROMPT_FIPS_PRE_ENABLE = ( @@ -1034,3 +1027,64 @@ with '{{service}}' enabled""".format( bold=TxtColor.BOLD, end_bold=TxtColor.ENDC ) + +ENTITLEMENT_NOT_FOUND = FormattedNamedMessage( + "entitlement-not-found", + 'could not find entitlement named "{name}"', +) + +TRY_UBUNTU_PRO_BETA = """\ +Try Ubuntu Pro beta with a free personal subscription on up to 5 machines. +Learn more at https://ubuntu.com/pro""" + +INVALID_STATE_FILE = "Invalid state file: {}" + +ENTITLEMENTS_NOT_ENABLED_ERROR = NamedMessage( + "entitlements-not-enabled", + "failed to enable some services", +) + +AUTO_ATTACH_DISABLED_ERROR = NamedMessage( + "auto-attach-disabled", + "features.disable_auto_attach set in config", +) + +AUTO_ATTACH_RUNNING = ( + "Currently attempting to automatically attach this machine to " + "Ubuntu Pro services" +) + +# prefix used for removing notices +AUTO_ATTACH_RETRY_NOTICE_PREFIX = """\ +Failed to automatically attach to Ubuntu Pro services""" +AUTO_ATTACH_RETRY_NOTICE = ( + AUTO_ATTACH_RETRY_NOTICE_PREFIX + + """\ + {num_attempts} time(s). +The failure was due to: {reason}. +The next attempt is scheduled for {next_run_datestring}. +You can try manually with `sudo pro auto-attach`.""" +) + +AUTO_ATTACH_RETRY_TOTAL_FAILURE_NOTICE = ( + AUTO_ATTACH_RETRY_NOTICE_PREFIX + + """\ + {num_attempts} times. +The most recent failure was due to: {reason}. +Try re-launching the instance or report this issue by running `ubuntu-bug ubuntu-advantage-tools` +You can try manually with `sudo pro auto-attach`.""" # noqa: E501 +) + +RETRY_ERROR_DETAIL_INVALID_PRO_IMAGE = ( + 'Canonical servers did not recognize this machine as Ubuntu Pro: "{}"' +) +RETRY_ERROR_DETAIL_NON_AUTO_ATTACH_IMAGE = ( + "Canonical servers did not recognize this image as Ubuntu Pro" +) +RETRY_ERROR_DETAIL_LOCK_HELD = "the pro lock was held by pid {pid}" +RETRY_ERROR_DETAIL_CONTRACT_API_ERROR = 'an error from Canonical servers: "{}"' +RETRY_ERROR_DETAIL_CONNECTIVITY_ERROR = "a connectivity error" +RETRY_ERROR_DETAIL_URL_ERROR_CODE = "a {code} while reaching {url}" +RETRY_ERROR_DETAIL_URL_ERROR_URL = "an error while reaching {url}" +RETRY_ERROR_DETAIL_URL_ERROR_GENERIC = "a network error" +RETRY_ERROR_DETAIL_UNKNOWN = "an unknown error" diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/security_status.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/security_status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/security_status.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/security_status.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,5 +1,6 @@ import json import logging +import re import textwrap from collections import defaultdict from enum import Enum @@ -21,12 +22,16 @@ from uaclient.exceptions import ProcessExecutionError from uaclient.status import status from uaclient.system import ( + REBOOT_PKGS_FILE_PATH, get_distro_info, get_kernel_info, get_platform_info, is_current_series_lts, is_supported, + load_file, + should_reboot, subp, + which, ) ESM_SERVICES = ("esm-infra", "esm-apps") @@ -40,6 +45,12 @@ UNAVAILABLE = "upgrade_unavailable" +class RebootStatus(Enum): + REBOOT_REQUIRED = "yes" + REBOOT_NOT_REQUIRED = "no" + REBOOT_REQUIRED_LIVEPATCH_APPLIED = "yes-kernel-livepatches-applied" + + @lru_cache(maxsize=None) def get_origin_information_to_service_map(): series = get_platform_info()["series"] @@ -104,7 +115,7 @@ For ESM-[Infra|Apps] packages, first checks if Pro is attached. If this is the case, also check for availability of the service. """ - if service_name == "standard-security" or ( + if service_name in ("standard-security", "standard-updates") or ( ua_info["attached"] and service_name in ua_info["enabled_services"] ): return UpdateStatus.AVAILABLE.value @@ -118,23 +129,35 @@ def filter_security_updates( packages: List[apt_package.Package], ) -> DefaultDict[str, List[Tuple[apt_package.Version, str]]]: - """Filters a list of packages looking for available security updates. + """Filters a list of packages looking for available updates. - Checks if the package has a greater version available, and if the origin of - this version matches any of the series' security repositories. + All versions greater than the installed one are reported, based on where + it is provided, including ESM pockets, excluding backports. """ result = defaultdict(list) for package in packages: - for version in package.versions: - if version > package.installed: - for origin in version.origins: - service = get_origin_information_to_service_map().get( - (origin.origin, origin.archive) - ) - if service: - result[service].append((version, origin.site)) - break # No need to loop through all the origins + if package.is_installed: + for version in package.versions: + if version > package.installed: + counted_as_security = False + for origin in version.origins: + service = get_origin_information_to_service_map().get( + (origin.origin, origin.archive) + ) + if service: + result[service].append((version, origin.site)) + counted_as_security = True + break # No need to loop through all the origins + # Also no need to report backports at least for now... + expected_origin = version.origins[0] + if ( + not counted_as_security + and "backports" not in expected_origin.archive + ): + result["standard-updates"].append( + (version, expected_origin.site) + ) return result @@ -160,7 +183,8 @@ return ua_info -def get_livepatch_fixed_cves() -> List[Dict[str, str]]: +# Yeah Any is bad, but so is python<3.8 without TypedDict +def get_livepatch_fixed_cves() -> List[Dict[str, Any]]: try: out, _err = subp([LIVEPATCH_CMD, "status", "--format", "json"]) except ProcessExecutionError: @@ -196,6 +220,92 @@ return [] +def get_reboot_status(): + if not should_reboot(): + return RebootStatus.REBOOT_NOT_REQUIRED + + try: + pkg_list = load_file(REBOOT_PKGS_FILE_PATH) + except FileNotFoundError: + # If we cannot load that file, we cannot evaluate if we have + # kernel related packages that require a reboot. Therefore, we + # will just say that a reboot is required + return RebootStatus.REBOOT_REQUIRED + + pkg_list = pkg_list.split() + num_pkgs = len(pkg_list) + + kernel_regex = "^(linux-image|linux-base).*" + num_kernel_pkgs = 0 + + for pkg in pkg_list: + if re.match(kernel_regex, pkg): + num_kernel_pkgs += 1 + + # We will only check the Livepatch state if all the + # packages that require a reboot are kernel related + if num_kernel_pkgs != num_pkgs: + return RebootStatus.REBOOT_REQUIRED + + if not which("canonical-livepatch"): + return RebootStatus.REBOOT_REQUIRED + + livepatch_status, _ = subp([LIVEPATCH_CMD, "status", "--format", "json"]) + + try: + livepatch_status_dict = json.loads(livepatch_status) + except json.JSONDecodeError: + # If we cannot parse the json file, we will return a + # normal reboot state + msg = "Could not parse Livepatch Status JSON: {}".format( + livepatch_status + ) + logging.debug(msg) + return RebootStatus.REBOOT_REQUIRED + + patch_statuses = livepatch_status_dict.get("Status", [{}]) + + kernel_info = get_kernel_info() + kernel_name = kernel_info.proc_version_signature_version + + patch_state = "" + for patch_status in patch_statuses: + livepatch_kernel = patch_status.get("Kernel") + if livepatch_kernel and livepatch_kernel == kernel_name: + patch_state = patch_status.get("Livepatch", {}).get("State", "") + + if patch_state == "applied": + return RebootStatus.REBOOT_REQUIRED_LIVEPATCH_APPLIED + + # Any other Livepatch status will not be considered here to modify the + # reboot state + return RebootStatus.REBOOT_REQUIRED + + +def create_updates_list( + security_upgradable_versions: DefaultDict[ + str, List[Tuple[apt_package.Version, str]] + ], + ua_info: Dict[str, Any], +) -> List[Dict[str, Any]]: + updates = [] + for service, version_list in security_upgradable_versions.items(): + status = get_update_status(service, ua_info) + for version, origin in version_list: + updates.append( + { + "package": version.package.name, + "version": version.version, + "service_name": service, + "status": status, + "origin": origin, + "download_size": version.size, + } + ) + + return updates + + def security_status_dict(cfg: UAConfig) -> Dict[str, Any]: """Returns the status of security updates on a system. @@ -209,27 +319,16 @@ ua_info = get_ua_info(cfg) summary = {"ua": ua_info} # type: Dict[str, Any] - packages = [] packages_by_origin = get_installed_packages_by_origin() installed_packages = packages_by_origin["all"] summary["num_installed_packages"] = len(installed_packages) security_upgradable_versions = filter_security_updates(installed_packages) + # This version of security-status only cares about security updates + security_upgradable_versions["standard-updates"] = [] - for service, version_list in security_upgradable_versions.items(): - status = get_update_status(service, ua_info) - for version, origin in version_list: - packages.append( - { - "package": version.package.name, - "version": version.version, - "service_name": service, - "status": status, - "origin": origin, - "download_size": version.size, - } - ) + updates = create_updates_list(security_upgradable_versions, ua_info) summary["num_main_packages"] = len(packages_by_origin["main"]) summary["num_restricted_packages"] = len(packages_by_origin["restricted"]) @@ -251,11 +350,12 @@ summary["num_standard_security_updates"] = len( security_upgradable_versions["standard-security"] ) + summary["reboot_required"] = get_reboot_status().value return { "_schema_version": "0.1", "summary": summary, - "packages": packages, + "packages": updates, "livepatch": {"fixed_cves": get_livepatch_fixed_cves()}, } @@ -427,13 +527,13 @@ security_upgradable_versions_infra = filter_security_updates( packages_by_origin["main"] + packages_by_origin["restricted"] - + packages_by_origin["esm-infra"] + + packages_by_origin["esm-infra"], )["esm-infra"] security_upgradable_versions_apps = filter_security_updates( packages_by_origin["universe"] + packages_by_origin["multiverse"] - + packages_by_origin["esm-apps"] + + packages_by_origin["esm-apps"], )["esm-apps"] _print_package_summary(packages_by_origin) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/snap.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/snap.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/snap.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/snap.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,4 +1,5 @@ import logging +import re from typing import List, Optional from uaclient import apt, event_logger, exceptions, messages, system @@ -7,13 +8,14 @@ SNAP_INSTALL_RETRIES = [0.5, 1.0, 5.0] HTTP_PROXY_OPTION = "proxy.http" HTTPS_PROXY_OPTION = "proxy.https" +SNAP_CHANNEL_SHORTEN_VALUE = "…" event = event_logger.get_event_logger() def is_installed() -> bool: """Returns whether or not snap is installed""" - return "snapd" in apt.get_installed_packages() + return "snapd" in apt.get_installed_packages_names() def configure_snap_proxy( @@ -93,3 +95,41 @@ return out.strip() except exceptions.ProcessExecutionError: return None + + +def get_snap_package_info_tracking(package: str) -> Optional[str]: + out, _ = system.subp( + ["snap", "info", package, "--color", "never", "--unicode", "never"] + ) + match = re.search(r"tracking:\s+(?P.*)", out) + if match: + return match.group("tracking") + return None + + +class SnapPackage: + def __init__(self, name, version, rev, tracking, publisher, notes): + self.name = name + self.version = version + self.rev = rev + self.tracking = tracking + self.publisher = publisher + self.notes = notes + + +def get_installed_snaps() -> List[SnapPackage]: + out, _ = system.subp( + ["snap", "list", "--color", "never", "--unicode", "never"] + ) + apps = out.splitlines() + apps = apps[1:] + snaps = [] + for line in apps: + pkg = line.split() + snap = SnapPackage(*pkg) + if snap.tracking.endswith(SNAP_CHANNEL_SHORTEN_VALUE): + channel = get_snap_package_info_tracking(snap.name) + snap.tracking = channel if channel else snap.tracking + snaps.append(snap) + + return snaps diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/status.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/status.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/status.py 2022-11-22 13:06:26.000000000 +0000 @@ -149,12 +149,7 @@ def _attached_status(cfg: UAConfig) -> Dict[str, Any]: """Return configuration of attached status as a dictionary.""" - - cfg.notice_file.try_remove( - "", - messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format(operation=".*"), - ) - cfg.notice_file.try_remove("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED) + cfg.notice_file.try_remove("", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX) response = copy.deepcopy(DEFAULT_STATUS) machineTokenInfo = cfg.machine_token["machineTokenInfo"] @@ -174,12 +169,12 @@ "tech_support_level": tech_support_level, }, "account": { - "name": cfg.machine_token_file.accounts[0]["name"], - "id": cfg.machine_token_file.accounts[0]["id"], - "created_at": cfg.machine_token_file.accounts[0].get( + "name": cfg.machine_token_file.account["name"], + "id": cfg.machine_token_file.account["id"], + "created_at": cfg.machine_token_file.account.get( "createdAt", "" ), - "external_account_ids": cfg.machine_token_file.accounts[0].get( + "external_account_ids": cfg.machine_token_file.account.get( "externalAccountIDs", [] ), }, @@ -606,6 +601,13 @@ for service in status["services"]: content.append(STATUS_UNATTACHED_TMPL.format(**service)) + if status.get("notices"): + content.extend( + get_section_column_content( + status.get("notices") or [], header="NOTICES" + ) + ) + if status.get("features"): content.append("\nFEATURES") for key, value in sorted(status["features"].items()): diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/system.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/system.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/system.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/system.py 2022-11-22 13:06:26.000000000 +0000 @@ -371,6 +371,7 @@ @param mode: The filesystem mode to set on the file. """ logging.debug("Writing file: %s", filename) + os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, "wb") as fh: fh.write(content.encode("utf-8")) fh.flush() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/testing/helpers.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/testing/helpers.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/testing/helpers.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/testing/helpers.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,33 @@ +try: # Drop try-except after xenial EOL + from contextlib import AbstractContextManager +except ImportError: + AbstractContextManager = object + + +class does_not_raise(AbstractContextManager): + """Reentrant noop context manager. + Useful to parametrize tests raising and not raising exceptions. + + Note: In python-3.7+, this can be substituted by contextlib.nullcontext + More info: + https://docs.pytest.org/en/6.2.x/example/parametrize.html?highlight=does_not_raise#parametrizing-conditional-raising + + Example: + -------- + >>> @pytest.mark.parametrize( + >>> "example_input,expectation", + >>> [ + >>> (1, does_not_raise()), + >>> (0, pytest.raises(ZeroDivisionError)), + >>> ], + >>> ) + >>> def test_division(example_input, expectation): + >>> with expectation: + >>> assert (0 / example_input) is not None + """ + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + pass diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_actions.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_actions.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_actions.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_actions.py 2022-11-22 13:06:26.000000000 +0000 @@ -4,13 +4,31 @@ import pytest from uaclient import exceptions, messages -from uaclient.actions import attach_with_token, auto_attach, collect_logs -from uaclient.exceptions import ContractAPIError, NonAutoAttachImageError -from uaclient.tests.test_cli_auto_attach import fake_instance_factory +from uaclient.actions import ( + attach_with_token, + auto_attach, + collect_logs, + get_cloud_instance, +) +from uaclient.exceptions import ( + CloudFactoryError, + CloudFactoryNoCloudError, + CloudFactoryNonViableCloudError, + CloudFactoryUnsupportedCloudError, + ContractAPIError, + NonAutoAttachImageError, + UserFacingError, +) M_PATH = "uaclient.actions." +def fake_instance_factory(): + m_instance = mock.Mock() + m_instance.identity_doc = "pkcs7-validated-by-backend" + return m_instance + + class TestAttachWithToken: @pytest.mark.parametrize( "request_updated_contract_side_effect, expected_error_class," @@ -82,42 +100,6 @@ mock.call(cfg, token="token", allow_enable=True) ] == m_attach_with_token.call_args_list - @pytest.mark.parametrize( - "http_msg,http_code,http_response", - ( - ("Not found", 404, {"message": "missing instance information"}), - ( - "Forbidden", - 403, - {"message": "forbidden: cannot verify signing certificate"}, - ), - ), - ) - @mock.patch( - M_PATH + "contract.UAContractClient.request_auto_attach_contract_token" - ) - @mock.patch(M_PATH + "identity.get_instance_id", return_value="old-iid") - def test_handles_4XX_contract_errors( - self, - _m_get_instance_id, - m_request_auto_attach_contract_token, - http_msg, - http_code, - http_response, - FakeConfig, - ): - """VMs running on non-auto-attach images do not return a token.""" - cfg = FakeConfig() - m_request_auto_attach_contract_token.side_effect = ContractAPIError( - exceptions.UrlError( - http_msg, code=http_code, url="http://me", headers={} - ), - error_response=http_response, - ) - with pytest.raises(NonAutoAttachImageError) as excinfo: - auto_attach(cfg, fake_instance_factory()) - assert messages.UNSUPPORTED_AUTO_ATTACH == str(excinfo.value) - @mock.patch( M_PATH + "contract.UAContractClient.request_auto_attach_contract_token" ) @@ -176,3 +158,56 @@ assert 1 == m_write_file.call_count assert [mock.call("test/b", "test")] == m_write_file.call_args_list assert "Failed to load file: a\n" in caplog_text() + + +class TestGetCloudInstance: + @pytest.mark.parametrize( + "cloud_factory_error, expected_error_cls, expected_error_msg", + [ + ( + CloudFactoryNoCloudError("test"), + UserFacingError, + messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, + ), + ( + CloudFactoryNonViableCloudError("test"), + UserFacingError, + messages.UNSUPPORTED_AUTO_ATTACH, + ), + ( + CloudFactoryUnsupportedCloudError("test"), + NonAutoAttachImageError, + messages.UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format( + cloud_type="test" + ), + ), + ( + CloudFactoryNoCloudError("test"), + UserFacingError, + messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, + ), + ( + CloudFactoryError("test"), + UserFacingError, + messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, + ), + ], + ) + @mock.patch(M_PATH + "identity.cloud_instance_factory") + def test_handle_cloud_factory_errors( + self, + m_cloud_instance_factory, + cloud_factory_error, + expected_error_cls, + expected_error_msg, + FakeConfig, + ): + """Non-supported clouds will error.""" + m_cloud_instance_factory.side_effect = cloud_factory_error + cfg = FakeConfig() + + with pytest.raises(expected_error_cls) as excinfo: + get_cloud_instance(cfg=cfg) + + if expected_error_msg: + assert expected_error_msg == str(excinfo.value) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_apt.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_apt.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_apt.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_apt.py 2022-11-22 13:06:26.000000000 +0000 @@ -28,7 +28,7 @@ find_apt_list_files, get_apt_cache_policy, get_apt_cache_time, - get_installed_packages, + get_installed_packages_names, is_installed, remove_apt_list_files, remove_auth_apt_repo, @@ -848,7 +848,7 @@ class TestGetInstalledPackages: @mock.patch("uaclient.apt.system.subp", return_value=("", "")) def test_correct_command_called(self, m_subp): - get_installed_packages() + get_installed_packages_names() expected_call = mock.call(["apt", "list", "--installed"]) assert [expected_call] == m_subp.call_args_list @@ -857,7 +857,7 @@ "uaclient.apt.system.subp", return_value=("Listing... Done\n", "") ) def test_empty_output_means_empty_list(self, m_subp): - assert [] == get_installed_packages() + assert [] == get_installed_packages_names() @pytest.mark.parametrize( "apt_list_return", @@ -866,7 +866,7 @@ @mock.patch("uaclient.apt.system.subp") def test_lines_are_split(self, m_subp, apt_list_return): m_subp.return_value = apt_list_return, "" - assert ["a", "b"] == get_installed_packages() + assert ["a", "b"] == get_installed_packages_names() class TestRunAptCommand: @@ -1056,7 +1056,7 @@ (False, ("foo", "bar")), ), ) - @mock.patch("uaclient.apt.get_installed_packages") + @mock.patch("uaclient.apt.get_installed_packages_names") def test_is_installed_pkgs( self, m_get_installed_pkgs, expected, installed_pkgs ): diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_attach.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -478,10 +478,12 @@ @mock.patch("uaclient.util.handle_unicode_characters") @mock.patch("uaclient.status.format_tabular") @mock.patch(M_PATH + "actions.status") + @mock.patch("uaclient.daemon.cleanup") @mock.patch("uaclient.daemon.stop") def test_attach_config_enable_services( self, _m_daemon_stop, + _m_daemon_cleanup, m_status, m_format_tabular, m_handle_unicode, diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_auto_attach.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -3,23 +3,17 @@ import mock import pytest -from uaclient import messages +from uaclient import event_logger, exceptions +from uaclient.api import exceptions as api_exceptions +from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import ( + FullAutoAttachOptions, +) from uaclient.cli import ( action_auto_attach, auto_attach_parser, get_parser, main, -) -from uaclient.exceptions import ( - AlreadyAttachedOnPROError, - CloudFactoryError, - CloudFactoryNoCloudError, - CloudFactoryNonViableCloudError, - CloudFactoryUnsupportedCloudError, - LockHeldError, - NonAutoAttachImageError, - NonRootUserError, - UserFacingError, + main_error_handler, ) M_PATH = "uaclient.cli." @@ -43,16 +37,10 @@ getuid.return_value = 1 cfg = FakeConfig() - with pytest.raises(NonRootUserError): + with pytest.raises(exceptions.NonRootUserError): action_auto_attach(mock.MagicMock(), cfg=cfg) -def fake_instance_factory(): - m_instance = mock.Mock() - m_instance.identity_doc = "pkcs7-validated-by-backend" - return m_instance - - # For all of these tests we want to appear as root, so mock on the class @mock.patch(M_PATH + "os.getuid", return_value=0) class TestActionAutoAttach: @@ -70,115 +58,90 @@ out, _err = capsys.readouterr() assert HELP_OUTPUT == out - @mock.patch("uaclient.system.subp") - def test_lock_file_exists(self, m_subp, _getuid, FakeConfig): - """Check inability to auto-attach if operation holds lock file.""" - cfg = FakeConfig() - cfg.write_cache("lock", "123:pro disable") - with pytest.raises(LockHeldError) as err: - action_auto_attach(mock.MagicMock(), cfg=cfg) - assert [mock.call(["ps", "123"])] == m_subp.call_args_list - assert ( - "Unable to perform: pro auto-attach.\n" - "Operation in progress: pro disable (pid:123)" - ) == err.value.msg - - def test_error_if_attached( + @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "_full_auto_attach") + def test_happy_path( self, + m_full_auto_attach, + m_post_cli_attach, _m_getuid, FakeConfig, ): - cfg = FakeConfig.for_attached_machine() - with pytest.raises(AlreadyAttachedOnPROError): - action_auto_attach(mock.MagicMock(), cfg=cfg) + cfg = FakeConfig() - @pytest.mark.parametrize( - "features_override", ((None), ({"disable_auto_attach": True})) - ) + assert 0 == action_auto_attach(mock.MagicMock(), cfg=cfg) + + assert [ + mock.call( + FullAutoAttachOptions(enable=None, enable_beta=None), + cfg=cfg, + mode=event_logger.EventLoggerMode.CLI, + ) + ] == m_full_auto_attach.call_args_list + assert [mock.call(cfg)] == m_post_cli_attach.call_args_list + + @mock.patch(M_PATH + "event") @mock.patch(M_PATH + "_post_cli_attach") - @mock.patch(M_PATH + "actions.auto_attach") - @mock.patch( - M_ID_PATH + "cloud_instance_factory", - side_effect=fake_instance_factory, - ) - def test_disable_auto_attach_config( + @mock.patch(M_PATH + "_full_auto_attach") + def test_handle_full_auto_attach_errors( self, - m_cloud_instance_factory, - m_auto_attach, + m_full_auto_attach, m_post_cli_attach, + m_event, _m_getuid, - features_override, FakeConfig, ): + m_full_auto_attach.side_effect = exceptions.UrlError( + cause="does-not-matter" + ) cfg = FakeConfig() - if features_override: - cfg.override_features(features_override) - ret = action_auto_attach(mock.MagicMock(), cfg=cfg) + assert 1 == action_auto_attach(mock.MagicMock(), cfg=cfg) - assert 0 == ret - if features_override: - assert [] == m_cloud_instance_factory.call_args_list - assert [] == m_auto_attach.call_args_list - assert [] == m_post_cli_attach.call_args_list - else: - assert [mock.call()] == m_cloud_instance_factory.call_args_list - assert [ - mock.call(mock.ANY, mock.ANY) - ] == m_auto_attach.call_args_list - assert [mock.call(mock.ANY)] == m_post_cli_attach.call_args_list + assert [ + mock.call("Failed to attach machine. See https://ubuntu.com/pro") + ] == m_event.info.call_args_list + assert [] == m_post_cli_attach.call_args_list @pytest.mark.parametrize( - "cloud_factory_error, expected_error_cls, expected_error_msg", + "api_side_effect, expected_err", [ + (exceptions.UserFacingError("foo"), "foo\n"), ( - CloudFactoryNoCloudError("test"), - UserFacingError, - messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, - ), - ( - CloudFactoryNonViableCloudError("test"), - UserFacingError, - messages.UNSUPPORTED_AUTO_ATTACH, - ), - ( - CloudFactoryUnsupportedCloudError("test"), - NonAutoAttachImageError, - messages.UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format( - cloud_type="test" - ), - ), - ( - CloudFactoryNoCloudError("test"), - UserFacingError, - messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, + exceptions.AlreadyAttachedError("foo"), + "This machine is already attached to 'foo'\n" + "To use a different subscription first run: sudo pro" + " detach.\n", ), ( - CloudFactoryError("test"), - UserFacingError, - messages.UNABLE_TO_DETERMINE_CLOUD_TYPE, + api_exceptions.AutoAttachDisabledError, + "features.disable_auto_attach set in config\n", ), ], ) - @mock.patch(M_ID_PATH + "cloud_instance_factory") - def test_handle_cloud_factory_errors( + @mock.patch(M_PATH + "logging") + @mock.patch(M_PATH + "_post_cli_attach") + @mock.patch(M_PATH + "_full_auto_attach") + def test_uncaught_errors_are_handled( self, - m_cloud_instance_factory, + m_full_auto_attach, + m_post_cli_attach, + m_logging, _m_getuid, - cloud_factory_error, - expected_error_cls, - expected_error_msg, + api_side_effect, + expected_err, + capsys, FakeConfig, ): - """Non-supported clouds will error.""" - m_cloud_instance_factory.side_effect = cloud_factory_error + m_full_auto_attach.side_effect = api_side_effect cfg = FakeConfig() - - with pytest.raises(expected_error_cls) as excinfo: - action_auto_attach(mock.MagicMock(), cfg=cfg) - - if expected_error_msg: - assert expected_error_msg == str(excinfo.value) + with pytest.raises(SystemExit): + assert 1 == main_error_handler(action_auto_attach)( + mock.MagicMock(), cfg=cfg + ) + _out, err = capsys.readouterr() + assert expected_err == err + assert [] == m_post_cli_attach.call_args_list class TestParser: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_disable.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_disable.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_disable.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_disable.py 2022-11-22 13:06:26.000000000 +0000 @@ -40,7 +40,7 @@ Arguments: service the name(s) of the Ubuntu Pro services to disable. One of: cc-eal, cis, esm-infra, fips, fips-updates, - livepatch + livepatch, realtime-kernel Flags: -h, --help show this help message and exit diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_enable.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_enable.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_enable.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_enable.py 2022-11-22 13:06:26.000000000 +0000 @@ -21,7 +21,7 @@ Arguments: service the name(s) of the Ubuntu Pro services to enable. One of: cc-eal, cis, esm-infra, fips, fips-updates, - livepatch + livepatch, realtime-kernel Flags: -h, --help show this help message and exit diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_reboot_required.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_reboot_required.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_reboot_required.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_reboot_required.py 2022-11-22 13:06:26.000000000 +0000 @@ -0,0 +1,37 @@ +import mock +import pytest + +from uaclient.cli import main + +HELP_OUTPUT = """\ +usage: pro system reboot-required [flags] + +Report the current reboot-required status for the machine. + +This command will output one of the three following states +for the machine regarding reboot: + +* no: The machine doesn't require a reboot +* yes: The machine requires a reboot +* yes-kernel-livepatches-applied: There are only kernel related + packages that require a reboot, but Livepatch has already provided + patches for the current running kernel. The machine still needs a + reboot, but you can assess if the reboot can be performed in the + nearest maintenance window. +""" + + +class TestActionRebootRequired: + def test_enable_help(self, capsys, FakeConfig): + with pytest.raises(SystemExit): + with mock.patch( + "sys.argv", + ["/usr/bin/ua", "system", "reboot-required", "--help"], + ): + with mock.patch( + "uaclient.config.UAConfig", + return_value=FakeConfig(), + ): + main() + out, _err = capsys.readouterr() + assert HELP_OUTPUT in out diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_status.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_cli_status.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_cli_status.py 2022-11-22 13:06:26.000000000 +0000 @@ -75,7 +75,7 @@ fips no no no NIST-certified core packages fips-updates no no no NIST-certified core packages with priority security updates livepatch yes yes no Canonical Livepatch service -realtime-kernel no no no Beta-version Ubuntu Kernel with PREEMPT_RT patches +realtime-kernel no no no Ubuntu kernel with PREEMPT_RT patches integrated ros no no no Security Updates for the Robot Operating System ros-updates no no no All Updates for the Robot Operating System """ # noqa: E501 @@ -93,7 +93,7 @@ fips no NIST-certified core packages fips-updates no NIST-certified core packages with priority security updates livepatch yes Canonical Livepatch service -realtime-kernel no Beta-version Ubuntu Kernel with PREEMPT_RT patches +realtime-kernel no Ubuntu kernel with PREEMPT_RT patches integrated ros no Security Updates for the Robot Operating System ros-updates no All Updates for the Robot Operating System @@ -117,7 +117,7 @@ fips no {dash} NIST-certified core packages fips-updates no {dash} NIST-certified core packages with priority security updates livepatch no {dash} Canonical Livepatch service -realtime-kernel no {dash} Beta-version Ubuntu Kernel with PREEMPT_RT patches +realtime-kernel no {dash} Ubuntu kernel with PREEMPT_RT patches integrated ros no {dash} Security Updates for the Robot Operating System ros-updates no {dash} All Updates for the Robot Operating System {notices}{features} @@ -199,7 +199,7 @@ "blocked_by": [], }, { - "description": "Beta-version Ubuntu Kernel with PREEMPT_RT patches", + "description": "Ubuntu kernel with PREEMPT_RT patches integrated", "description_override": None, "entitled": "no", "name": "realtime-kernel", @@ -492,6 +492,7 @@ def fake_sleep(seconds): if m_sleep.call_count == 3: os.unlink(lock_file) + os.unlink(cfg.notice_file.file.path) m_sleep.side_effect = fake_sleep @@ -789,14 +790,6 @@ beta_services = [ { - "auto_enabled": "no", - "available": "no", - "description": "Beta-version Ubuntu Kernel with PREEMPT_RT" - " patches", - "entitled": "no", - "name": "realtime-kernel", - }, - { "auto_enabled": "yes", "available": "yes", "description": "Expanded Security Maintenance for Applications", # noqa @@ -850,6 +843,14 @@ "entitled": "yes", "name": "livepatch", }, + { + "auto_enabled": "no", + "available": "no", + "description": "Ubuntu kernel with PREEMPT_RT patches" + " integrated", + "entitled": "no", + "name": "realtime-kernel", + }, ] expected_services = sorted( @@ -1097,10 +1098,12 @@ (False, False), ), ) + @mock.patch("uaclient.files.NoticeFile.try_remove") @mock.patch("uaclient.files.NoticeFile.add") def test_is_contract_changed( self, m_add_notice, + m_try_remove_notice, _m_getuid, _m_get_contract_information, _m_get_available_resources, @@ -1131,6 +1134,9 @@ assert [ mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) ] not in m_add_notice.call_args_list + assert [ + mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) + ] in m_try_remove_notice.call_args_list else: assert _m_contract_changed.call_count == 0 diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_config.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_config.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_config.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_config.py 2022-11-22 13:06:26.000000000 +0000 @@ -194,16 +194,16 @@ class TestAccounts: - def test_accounts_returns_empty_list_when_no_cached_account_value( + def test_accounts_returns_none_when_no_cached_account_value( self, tmpdir, FakeConfig, all_resources_available ): """Config.accounts property returns an empty list when no cache.""" cfg = FakeConfig() - assert [] == cfg.machine_token_file.accounts + assert cfg.machine_token_file.account is None @pytest.mark.usefixtures("all_resources_available") - def test_accounts_extracts_accounts_key_from_machine_token_cache( + def test_accounts_extracts_account_key_from_machine_token_cache( self, all_resources_available, tmpdir, FakeConfig ): """Use machine_token cached accountInfo when no accounts cache.""" @@ -217,7 +217,7 @@ }, ) - assert [accountInfo] == cfg.machine_token_file.accounts + assert accountInfo == cfg.machine_token_file.account class TestDataPath: diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_contract.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_contract.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_contract.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_contract.py 2022-11-22 13:06:26.000000000 +0000 @@ -48,10 +48,6 @@ @pytest.mark.parametrize( "machine_id_response", (("contract-machine-id"), None) ) - @pytest.mark.parametrize( - "detach,expected_http_method", - ((None, "POST"), (False, "POST"), (True, "DELETE")), - ) @pytest.mark.parametrize("activity_id", ((None), ("test-acid"))) @mock.patch("uaclient.contract.system.get_platform_info") def test__request_machine_token_update( @@ -59,8 +55,6 @@ get_platform_info, get_machine_id, request_url, - detach, - expected_http_method, machine_id_response, activity_id, FakeConfig, @@ -82,8 +76,6 @@ cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) kwargs = {"machine_token": "mToken", "contract_id": "cId"} - if detach is not None: - kwargs["detach"] = detach enabled_services = ["esm-apps", "livepatch"] def entitlement_user_facing_status(self): @@ -101,13 +93,14 @@ ): client._request_machine_token_update(**kwargs) - if not detach: # Then we have written the updated cache - assert machine_token == cfg.machine_token_file.machine_token - expected_machine_id = "machineId" - if machine_id_response: - expected_machine_id = machine_id_response + assert machine_token != cfg.machine_token_file.machine_token + client.update_files_after_machine_token_update(machine_token) + assert machine_token == cfg.machine_token_file.machine_token + expected_machine_id = "machineId" + if machine_id_response: + expected_machine_id = machine_id_response - assert expected_machine_id == cfg.read_cache("machine-id") + assert expected_machine_id == cfg.read_cache("machine-id") params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), @@ -115,20 +108,19 @@ "content-type": "application/json", "Authorization": "Bearer mToken", }, - "method": expected_http_method, + "method": "POST", + } + expected_activity_id = activity_id if activity_id else "machineId" + params["data"] = { + "machineId": "machineId", + "architecture": "arch", + "os": {"kernel": "kernel"}, + "activityInfo": { + "activityToken": None, + "activityID": expected_activity_id, + "resources": enabled_services, + }, } - if expected_http_method != "DELETE": - expected_activity_id = activity_id if activity_id else "machineId" - params["data"] = { - "machineId": "machineId", - "architecture": "arch", - "os": {"kernel": "kernel"}, - "activityInfo": { - "activityToken": None, - "activityID": expected_activity_id, - "resources": enabled_services, - }, - } assert request_url.call_args_list == [ mock.call("/v1/contracts/cId/context/machines/machineId", **params) ] @@ -379,7 +371,7 @@ ( ( exceptions.UrlError("test"), - exceptions.UserFacingError, + exceptions.ConnectivityError, messages.CONNECTIVITY_ERROR, ), ( @@ -448,7 +440,7 @@ magic_token = "test-id" request_url.side_effect = exceptions.UrlError("test") - with pytest.raises(exceptions.UserFacingError) as exc_error: + with pytest.raises(exceptions.ConnectivityError) as exc_error: client.get_magic_attach_token_info(magic_token=magic_token) assert messages.CONNECTIVITY_ERROR.msg == exc_error.value.msg @@ -492,7 +484,7 @@ magic_token = "test-id" request_url.side_effect = exceptions.UrlError("test") - with pytest.raises(exceptions.UserFacingError) as exc_error: + with pytest.raises(exceptions.ConnectivityError) as exc_error: client.revoke_magic_attach_token(magic_token=magic_token) assert messages.CONNECTIVITY_ERROR.msg == exc_error.value.msg diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_daemon.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_daemon.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_daemon.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_daemon.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,532 +0,0 @@ -from subprocess import TimeoutExpired - -import mock -import pytest - -from uaclient import exceptions, messages -from uaclient.clouds.aws import UAAutoAttachAWSInstance -from uaclient.clouds.gcp import UAAutoAttachGCPInstance -from uaclient.daemon import ( - attempt_auto_attach, - poll_for_pro_license, - start, - stop, -) - -M_PATH = "uaclient.daemon." - - -@mock.patch(M_PATH + "system.subp") -class TestStart: - def test_start_success(self, m_subp): - start() - assert [ - mock.call( - ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 - ) - ] == m_subp.call_args_list - - @pytest.mark.parametrize( - "err", - ( - exceptions.ProcessExecutionError("cmd"), - TimeoutExpired("cmd", 2.0), - ), - ) - @mock.patch(M_PATH + "LOG.warning") - def test_start_warning(self, m_log_warning, m_subp, err): - m_subp.side_effect = err - start() - assert [ - mock.call( - ["systemctl", "start", "ubuntu-advantage.service"], timeout=2.0 - ) - ] == m_subp.call_args_list - assert [mock.call(err)] == m_log_warning.call_args_list - - -@mock.patch(M_PATH + "system.subp") -class TestStop: - def test_stop_success(self, m_subp): - stop() - assert [ - mock.call( - ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 - ) - ] == m_subp.call_args_list - - @pytest.mark.parametrize( - "err", - ( - exceptions.ProcessExecutionError("cmd"), - TimeoutExpired("cmd", 2.0), - ), - ) - @mock.patch(M_PATH + "LOG.warning") - def test_stop_warning(self, m_log_warning, m_subp, err): - m_subp.side_effect = err - stop() - assert [ - mock.call( - ["systemctl", "stop", "ubuntu-advantage.service"], timeout=2.0 - ) - ] == m_subp.call_args_list - assert [mock.call(err)] == m_log_warning.call_args_list - - -time_mock_curr_value = 0 - - -def time_mock_side_effect_increment_by(increment): - def _time_mock_side_effect(): - global time_mock_curr_value - time_mock_curr_value += increment - return time_mock_curr_value - - return _time_mock_side_effect - - -@mock.patch(M_PATH + "LOG.debug") -@mock.patch(M_PATH + "actions.auto_attach") -@mock.patch(M_PATH + "lock.SpinLock") -class TestAttemptAutoAttach: - def test_success( - self, m_spin_lock, m_auto_attach, m_log_debug, FakeConfig - ): - cfg = FakeConfig() - cloud = mock.MagicMock() - - attempt_auto_attach(cfg, cloud) - - assert [ - mock.call(cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach") - ] == m_spin_lock.call_args_list - assert [mock.call(cfg, cloud)] == m_auto_attach.call_args_list - assert [ - mock.call("Successful auto attach") - ] == m_log_debug.call_args_list - - @mock.patch(M_PATH + "LOG.error") - def test_lock_held( - self, m_log_error, m_spin_lock, m_auto_attach, m_log_debug, FakeConfig - ): - err = exceptions.LockHeldError("test", "test_holder", 1) - m_spin_lock.side_effect = err - cfg = FakeConfig() - cfg.notice_file.add = mock.MagicMock() - cloud = mock.MagicMock() - - attempt_auto_attach(cfg, cloud) - assert [ - mock.call(cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach") - ] == m_spin_lock.call_args_list - assert [] == m_auto_attach.call_args_list - assert [mock.call(err)] == m_log_error.call_args_list - assert [ - mock.call( - "", - messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format( - operation="test_holder" - ), - ) - ] == cfg.notice_file.add.call_args_list - assert [ - mock.call("Failed to auto attach") - ] == m_log_debug.call_args_list - - @mock.patch(M_PATH + "lock.clear_lock_file_if_present") - @mock.patch(M_PATH + "LOG.exception") - def test_exception( - self, - m_log_exception, - m_clear_lock, - m_spin_lock, - m_auto_attach, - m_log_debug, - FakeConfig, - ): - err = Exception() - m_auto_attach.side_effect = err - cfg = FakeConfig() - cfg.notice_file.add = mock.MagicMock() - cloud = mock.MagicMock() - - attempt_auto_attach(cfg, cloud) - - assert [ - mock.call(cfg=cfg, lock_holder="pro.daemon.attempt_auto_attach") - ] == m_spin_lock.call_args_list - assert [mock.call(cfg, cloud)] == m_auto_attach.call_args_list - assert [mock.call(err)] == m_log_exception.call_args_list - assert [ - mock.call("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED) - ] == cfg.notice_file.add.call_args_list - assert [mock.call()] == m_clear_lock.call_args_list - assert [ - mock.call("Failed to auto attach") - ] == m_log_debug.call_args_list - - -@mock.patch(M_PATH + "LOG.debug") -@mock.patch(M_PATH + "time.sleep") -@mock.patch(M_PATH + "time.time") -@mock.patch(M_PATH + "attempt_auto_attach") -@mock.patch(M_PATH + "UAAutoAttachGCPInstance.is_pro_license_present") -@mock.patch(M_PATH + "UAAutoAttachGCPInstance.should_poll_for_pro_license") -@mock.patch(M_PATH + "cloud_instance_factory") -@mock.patch(M_PATH + "system.is_current_series_lts") -@mock.patch(M_PATH + "util.is_config_value_true") -class TestPollForProLicense: - @pytest.mark.parametrize( - "is_config_value_true," - "is_attached," - "is_current_series_lts," - "cloud_instance," - "should_poll," - "is_pro_license_present," - "cfg_poll_for_pro_licenses," - "expected_log_debug_calls," - "expected_is_pro_license_present_calls," - "expected_attempt_auto_attach_calls", - [ - ( - True, - None, - None, - None, - None, - None, - None, - [mock.call("Configured to not auto attach, shutting down")], - [], - [], - ), - ( - False, - True, - None, - None, - None, - None, - None, - [mock.call("Already attached, shutting down")], - [], - [], - ), - ( - False, - False, - False, - None, - None, - None, - None, - [mock.call("Not on LTS, shutting down")], - [], - [], - ), - ( - False, - False, - True, - exceptions.CloudFactoryError("none"), - None, - None, - None, - [mock.call("Not on cloud, shutting down")], - [], - [], - ), - ( - False, - False, - True, - UAAutoAttachAWSInstance(), - None, - None, - None, - [mock.call("Not on gcp, shutting down")], - [], - [], - ), - ( - False, - False, - True, - UAAutoAttachGCPInstance(), - False, - None, - None, - [mock.call("Not on supported instance, shutting down")], - [], - [], - ), - ( - False, - False, - True, - UAAutoAttachGCPInstance(), - True, - True, - None, - [], - [mock.call(wait_for_change=False)], - [mock.call(mock.ANY, mock.ANY)], - ), - ( - False, - False, - True, - UAAutoAttachGCPInstance(), - True, - exceptions.CancelProLicensePolling(), - None, - [mock.call("Cancelling polling")], - [mock.call(wait_for_change=False)], - [], - ), - ( - False, - False, - True, - UAAutoAttachGCPInstance(), - True, - False, - False, - [ - mock.call( - "Configured to not poll for pro license, shutting down" - ) - ], - [mock.call(wait_for_change=False)], - [], - ), - ( - False, - False, - True, - UAAutoAttachGCPInstance(), - True, - False, - False, - [ - mock.call( - "Configured to not poll for pro license, shutting down" - ) - ], - [mock.call(wait_for_change=False)], - [], - ), - ], - ) - def test_before_polling_loop_checks( - self, - m_is_config_value_true, - m_is_current_series_lts, - m_cloud_instance_factory, - m_should_poll, - m_is_pro_license_present, - m_attempt_auto_attach, - m_time, - m_sleep, - m_log_debug, - is_config_value_true, - is_attached, - is_current_series_lts, - cloud_instance, - should_poll, - is_pro_license_present, - cfg_poll_for_pro_licenses, - expected_log_debug_calls, - expected_is_pro_license_present_calls, - expected_attempt_auto_attach_calls, - FakeConfig, - ): - if is_attached: - cfg = FakeConfig.for_attached_machine() - else: - cfg = FakeConfig() - cfg.cfg.update( - {"ua_config": {"poll_for_pro_license": cfg_poll_for_pro_licenses}} - ) - - m_is_config_value_true.return_value = is_config_value_true - m_is_current_series_lts.return_value = is_current_series_lts - m_cloud_instance_factory.side_effect = [cloud_instance] - m_should_poll.return_value = should_poll - m_is_pro_license_present.side_effect = [is_pro_license_present] - - poll_for_pro_license(cfg) - - assert expected_log_debug_calls == m_log_debug.call_args_list - assert ( - expected_is_pro_license_present_calls - == m_is_pro_license_present.call_args_list - ) - assert ( - expected_attempt_auto_attach_calls - == m_attempt_auto_attach.call_args_list - ) - - @pytest.mark.parametrize( - "is_pro_license_present_side_effect," - "time_side_effect," - "expected_is_pro_license_present_calls," - "expected_attempt_auto_attach_calls," - "expected_log_debug_calls," - "expected_sleep_calls", - [ - ( - [False, True], - time_mock_side_effect_increment_by(100), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - ], - [mock.call(mock.ANY, mock.ANY)], - [], - [], - ), - ( - [False, False, False, False, False, True], - time_mock_side_effect_increment_by(100), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - ], - [mock.call(mock.ANY, mock.ANY)], - [], - [], - ), - ( - [False, False, True], - time_mock_side_effect_increment_by(1), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - ], - [mock.call(mock.ANY, mock.ANY)], - [ - mock.call( - "wait_for_change returned quickly and no pro license" - " present. Waiting 123 seconds before polling again" - ) - ], - [mock.call(123)], - ), - ( - [False, False, False, False, False, True], - time_mock_side_effect_increment_by(1), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - ], - [mock.call(mock.ANY, mock.ANY)], - [ - mock.call(mock.ANY), - mock.call(mock.ANY), - mock.call(mock.ANY), - mock.call(mock.ANY), - ], - [ - mock.call(123), - mock.call(123), - mock.call(123), - mock.call(123), - ], - ), - ( - [ - False, - False, - exceptions.DelayProLicensePolling(), - False, - exceptions.DelayProLicensePolling(), - True, - ], - time_mock_side_effect_increment_by(100), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - ], - [mock.call(mock.ANY, mock.ANY)], - [], - [mock.call(123), mock.call(123)], - ), - ( - [False, False, exceptions.CancelProLicensePolling()], - time_mock_side_effect_increment_by(100), - [ - mock.call(wait_for_change=False), - mock.call(wait_for_change=True), - mock.call(wait_for_change=True), - ], - [], - [mock.call("Cancelling polling")], - [], - ), - ], - ) - def test_polling_loop( - self, - m_is_config_value_true, - m_is_current_series_lts, - m_cloud_instance_factory, - m_should_poll, - m_is_pro_license_present, - m_attempt_auto_attach, - m_time, - m_sleep, - m_log_debug, - is_pro_license_present_side_effect, - time_side_effect, - expected_is_pro_license_present_calls, - expected_attempt_auto_attach_calls, - expected_log_debug_calls, - expected_sleep_calls, - FakeConfig, - ): - cfg = FakeConfig() - cfg.cfg.update( - { - "ua_config": { - "poll_for_pro_license": True, - "polling_error_retry_delay": 123, - } - } - ) - - m_is_config_value_true.return_value = False - m_is_current_series_lts.return_value = True - m_cloud_instance_factory.return_value = UAAutoAttachGCPInstance() - m_should_poll.return_value = True - m_is_pro_license_present.side_effect = ( - is_pro_license_present_side_effect - ) - m_time.side_effect = time_side_effect - - poll_for_pro_license(cfg) - - assert expected_sleep_calls == m_sleep.call_args_list - assert expected_log_debug_calls == m_log_debug.call_args_list - assert ( - expected_is_pro_license_present_calls - == m_is_pro_license_present.call_args_list - ) - assert ( - expected_attempt_auto_attach_calls - == m_attempt_auto_attach.call_args_list - ) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_lib_auto_attach.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_lib_auto_attach.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_lib_auto_attach.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_lib_auto_attach.py 2022-11-22 13:06:26.000000000 +0000 @@ -1,8 +1,15 @@ +import logging + import mock import pytest from lib.auto_attach import check_cloudinit_userdata_for_ua_info, main -from uaclient.exceptions import AlreadyAttachedOnPROError +from uaclient import messages +from uaclient.api.exceptions import ( + AlreadyAttachedError, + AutoAttachDisabledError, +) +from uaclient.daemon import AUTO_ATTACH_STATUS_MOTD_FILE class TestCheckCloudinitUserdataForUAInfo: @@ -46,6 +53,7 @@ assert expected is check_cloudinit_userdata_for_ua_info() +@mock.patch("lib.auto_attach.system.write_file") class TestMain: @pytest.mark.parametrize( "ubuntu_advantage_in_userdata", @@ -55,11 +63,12 @@ ), ) @mock.patch("lib.auto_attach.check_cloudinit_userdata_for_ua_info") - @mock.patch("lib.auto_attach.action_auto_attach") + @mock.patch("lib.auto_attach.full_auto_attach") def test_main( self, - m_cli_auto_attach, + m_api_full_auto_attach, m_check_cloudinit, + m_write_file, ubuntu_advantage_in_userdata, caplog_text, FakeConfig, @@ -68,9 +77,14 @@ main(cfg=FakeConfig()) if not ubuntu_advantage_in_userdata: - assert 1 == m_cli_auto_attach.call_count + assert [ + mock.call( + AUTO_ATTACH_STATUS_MOTD_FILE, messages.AUTO_ATTACH_RUNNING + ) + ] == m_write_file.call_args_list + assert 1 == m_api_full_auto_attach.call_count else: - assert 0 == m_cli_auto_attach.call_count + assert 0 == m_api_full_auto_attach.call_count assert ( "cloud-init userdata has ubuntu-advantage key." ) in caplog_text() @@ -79,20 +93,43 @@ "to setup and configure auto-attach" ) in caplog_text() + @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) + @pytest.mark.parametrize( + "api_side_effect, log_msg", + [ + ( + AlreadyAttachedError("test_account"), + "This machine is already attached to 'test_account'", + ), + ( + AutoAttachDisabledError, + "Skipping auto-attach. Config disable_auto_attach is set.\n", + ), + ], + ) @mock.patch("lib.auto_attach.check_cloudinit_userdata_for_ua_info") - @mock.patch("lib.auto_attach.action_auto_attach") - def test_main_when_already_attached( + @mock.patch("lib.auto_attach.full_auto_attach") + def test_main_handles_errors( self, - m_cli_auto_attach, + m_api_full_auto_attach, m_check_cloudinit, - FakeConfig, + m_write_file, + api_side_effect, + log_msg, caplog_text, + FakeConfig, ): + cfg = FakeConfig.for_attached_machine() m_check_cloudinit.return_value = False - m_cli_auto_attach.side_effect = AlreadyAttachedOnPROError() - main(cfg=FakeConfig()) + m_api_full_auto_attach.side_effect = api_side_effect + + main(cfg=cfg) assert ( - "Skipping auto-attach: Instance is already attached." - in caplog_text() + mock.call( + AUTO_ATTACH_STATUS_MOTD_FILE, messages.AUTO_ATTACH_RUNNING + ) + in m_write_file.call_args_list ) + assert m_api_full_auto_attach.call_count == 1 + assert log_msg in caplog_text() diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_reboot_cmds.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_reboot_cmds.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_reboot_cmds.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_reboot_cmds.py 2022-11-22 13:06:26.000000000 +0000 @@ -162,8 +162,7 @@ m_check_lock_info.return_value = (0, 0) m_fix_pro_pkg_holds.side_effect = ProcessExecutionError("error") - cfg = FakeConfig() - cfg.for_attached_machine() + cfg = FakeConfig.for_attached_machine() with mock.patch("os.path.exists", return_value=True): with mock.patch("uaclient.config.UAConfig.write_cache"): process_reboot_operations(cfg=cfg) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_security_status.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_security_status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_security_status.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_security_status.py 2022-11-22 13:06:26.000000000 +0000 @@ -8,10 +8,12 @@ from uaclient.exceptions import ProcessExecutionError from uaclient.security_status import ( + RebootStatus, UpdateStatus, filter_security_updates, get_livepatch_fixed_cves, get_origin_for_package, + get_reboot_status, get_ua_info, get_update_status, security_status_dict, @@ -55,6 +57,7 @@ mock_package.versions = [] mock_package.is_installed = bool(installed_version) + mock_package.installed = None if installed_version: mock_package.installed = installed_version installed_version.package = mock_package @@ -64,6 +67,7 @@ version.package = mock_package mock_package.versions.append(version) + mock_package.candidate = None if mock_package.versions: mock_package.candidate = max(mock_package.versions) @@ -88,6 +92,9 @@ "archive_universe": mock_origin( "universe", "example-updates", "Ubuntu", "archive.ubuntu.com" ), + "archive_backports": mock_origin( + "universe", "example-backports", "Ubuntu", "archive.ubuntu.com" + ), } ORIGIN_TO_SERVICE_MOCK = { @@ -247,7 +254,13 @@ "2.0", [MOCK_ORIGINS["standard-security"]] ), "security.ubuntu.com", - ) + ), + ( + mock_version( + "2.1", [MOCK_ORIGINS["standard-security"]] + ), + "security.ubuntu.com", + ), ], "esm-apps": [ ( @@ -255,6 +268,12 @@ "esm.ubuntu.com", ) ], + "standard-updates": [ + ( + mock_version("2.0", [MOCK_ORIGINS["archive_main"]]), + "archive.ubuntu.com", + ) + ], }, ) package_list = [ @@ -273,31 +292,48 @@ other_versions=[mock_version("1.0", [MOCK_ORIGINS["infra"]])], ), mock_package( - name="update-available", + name="infra-update-available", installed_version=mock_version( "1.0", [MOCK_ORIGINS["now"], MOCK_ORIGINS["archive_main"]] ), other_versions=[expected_return["esm-infra"][0][0]], ), mock_package( - name="not-a-security-update", + name="security-update-available", installed_version=mock_version( "1.0", [MOCK_ORIGINS["now"], MOCK_ORIGINS["archive_main"]] ), other_versions=[ - mock_version("2.0", [MOCK_ORIGINS["archive_main"]]) + expected_return["standard-security"][0][0], ], ), mock_package( + name="not-a-security-update", + installed_version=mock_version( + "1.0", [MOCK_ORIGINS["now"], MOCK_ORIGINS["archive_main"]] + ), + other_versions=[expected_return["standard-updates"][0][0]], + ), + mock_package( name="more-than-one-update", installed_version=mock_version( "1.0", [MOCK_ORIGINS["now"], MOCK_ORIGINS["archive_main"]] ), other_versions=[ - expected_return["standard-security"][0][0], + expected_return["standard-security"][1][0], expected_return["esm-apps"][0][0], ], ), + mock_package( + name="upgrade-is-backports-boo", + installed_version=mock_version( + "1.0", + [MOCK_ORIGINS["now"], MOCK_ORIGINS["archive_universe"]], + ), + other_versions=[ + mock_version("2.0", [MOCK_ORIGINS["archive_backports"]]) + ], + ), ] with mock.patch( M_PATH + "get_origin_information_to_service_map", @@ -307,17 +343,26 @@ assert expected_return == filtered_versions assert ( filtered_versions["esm-infra"][0][0].package.name - == "update-available" + == "infra-update-available" ) assert ( filtered_versions["standard-security"][0][0].package.name + == "security-update-available" + ) + assert ( + filtered_versions["standard-security"][1][0].package.name == "more-than-one-update" ) assert ( filtered_versions["esm-apps"][0][0].package.name == "more-than-one-update" ) + assert ( + filtered_versions["standard-updates"][0][0].package.name + == "not-a-security-update" + ) + @mock.patch(M_PATH + "get_reboot_status") @mock.patch(M_PATH + "get_livepatch_fixed_cves", return_value=[]) @mock.patch(M_PATH + "status", return_value={"attached": False}) @mock.patch(M_PATH + "get_origin_for_package", return_value="main") @@ -330,6 +375,7 @@ _m_get_origin, _m_status, _m_livepatch_cves, + m_reboot_status, FakeConfig, ): """Make sure the output format matches the expected JSON""" @@ -343,6 +389,7 @@ "esm-apps": [], "standard-security": [], } + m_reboot_status.return_value = RebootStatus.REBOOT_NOT_REQUIRED expected_output = { "_schema_version": "0.1", @@ -382,6 +429,7 @@ "num_esm_infra_updates": 2, "num_esm_apps_updates": 0, "num_standard_security_updates": 0, + "reboot_required": "no", }, "livepatch": {"fixed_cves": []}, } @@ -473,3 +521,194 @@ assert [ {"name": "cve-example", "patched": True} ] == get_livepatch_fixed_cves() + + +class TestRebootStatus: + @mock.patch("uaclient.security_status.should_reboot", return_value=False) + def test_get_reboot_status_no_reboot_needed(self, m_should_reboot): + assert get_reboot_status() == RebootStatus.REBOOT_NOT_REQUIRED + assert 1 == m_should_reboot.call_count + + @mock.patch("uaclient.security_status.load_file") + @mock.patch("uaclient.security_status.should_reboot", return_value=True) + def test_get_reboot_status_no_reboot_pkgs_file( + self, m_should_reboot, m_load_file + ): + m_load_file.side_effect = FileNotFoundError() + assert get_reboot_status() == RebootStatus.REBOOT_REQUIRED + assert 1 == m_should_reboot.call_count + assert 1 == m_load_file.call_count + + @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) + @mock.patch("uaclient.security_status.subp") + @mock.patch("uaclient.security_status.which", return_value=True) + @mock.patch("uaclient.security_status.load_file") + @mock.patch("uaclient.security_status.should_reboot", return_value=True) + def test_get_reboot_status_fail_to_parse_livepatch_output( + self, + m_should_reboot, + m_load_file, + _m_which, + m_subp, + caplog_text, + ): + m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" + m_subp.return_value = ('{"test": 123', "") + + assert get_reboot_status() == RebootStatus.REBOOT_REQUIRED + assert "Could not parse Livepatch Status JSON" in caplog_text() + + @pytest.mark.parametrize( + "pkgs,expected_state", + ( + ("pkg1\npkg2", RebootStatus.REBOOT_REQUIRED), + ( + "linux-image-5.4.0-1074\nlinux-base\npkg2", + RebootStatus.REBOOT_REQUIRED, + ), + ), + ) + @mock.patch("uaclient.security_status.load_file") + @mock.patch("uaclient.security_status.should_reboot", return_value=True) + def test_get_reboot_status_reboot_pkgs_file_present( + self, m_should_reboot, m_load_file, pkgs, expected_state + ): + m_load_file.return_value = pkgs + assert get_reboot_status() == expected_state + assert 1 == m_should_reboot.call_count + assert 1 == m_load_file.call_count + + @pytest.mark.parametrize( + "livepatch_state,expected_state,kernel_name", + ( + ( + "applied", + RebootStatus.REBOOT_REQUIRED_LIVEPATCH_APPLIED, + "4.15.0-187.198-generic", + ), + ("applied", RebootStatus.REBOOT_REQUIRED, "test"), + ( + "nothing-to-apply", + RebootStatus.REBOOT_REQUIRED, + "4.15.0-187.198-generic", + ), + ( + "applying", + RebootStatus.REBOOT_REQUIRED, + "4.15.0-187.198-generic", + ), + ( + "apply-failed", + RebootStatus.REBOOT_REQUIRED, + "4.15.0-187.198-generic", + ), + ), + ) + @mock.patch("uaclient.security_status.get_kernel_info") + @mock.patch("uaclient.security_status.which") + @mock.patch("uaclient.security_status.subp") + @mock.patch("uaclient.security_status.load_file") + @mock.patch("uaclient.security_status.should_reboot", return_value=True) + def test_get_reboot_status_reboot_pkgs_file_only_kernel_pkgs( + self, + m_should_reboot, + m_load_file, + m_subp, + m_which, + m_kernel_info, + livepatch_state, + expected_state, + kernel_name, + ): + m_kernel_info.return_value = mock.MagicMock( + proc_version_signature_version=kernel_name + ) + m_which.return_value = True + m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" + m_subp.return_value = ( + """ + {{ + "Client-Version": "version", + "Machine-Id": "machine-id", + "Architecture": "x86_64", + "CPU-Model": "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz", + "Last-Check": "2022-07-05T18:29:00Z", + "Boot-Time": "2022-07-05T18:27:12Z", + "Uptime": "203", + "Status": [ + {{ + "Kernel": "4.15.0-187.198-generic", + "Running": true, + "Livepatch": {{ + "CheckState": "checked", + "State": "{}", + "Version": "" + }} + }} + ], + "tier": "stable" + }} + """.format( + livepatch_state + ), + "", + ) + + assert get_reboot_status() == expected_state + assert 1 == m_should_reboot.call_count + assert 1 == m_load_file.call_count + assert 1 == m_which.call_count + assert 1 == m_subp.call_count + assert 1 == m_kernel_info.call_count + + @mock.patch("uaclient.security_status.get_kernel_info") + @mock.patch("uaclient.security_status.which") + @mock.patch("uaclient.security_status.subp") + @mock.patch("uaclient.security_status.load_file") + @mock.patch("uaclient.security_status.should_reboot", return_value=True) + def test_get_reboot_status_fail_parsing_kernel_info( + self, + m_should_reboot, + m_load_file, + m_subp, + m_which, + m_kernel_info, + ): + m_kernel_info.return_value = mock.MagicMock( + proc_version_signature_version=None + ) + m_which.return_value = True + m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" + m_subp.return_value = ( + """ + { + "Client-Version": "version", + "Machine-Id": "machine-id", + "Architecture": "x86_64", + "CPU-Model": "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz", + "Last-Check": "2022-07-05T18:29:00Z", + "Boot-Time": "2022-07-05T18:27:12Z", + "Uptime": "203", + "Status": [ + { + "Kernel": "4.15.0-187.198-generic", + "Running": true, + "Livepatch": { + "CheckState": "checked", + "State": "applied", + "Version": "" + } + } + ], + "tier": "stable" + } + """, + "", + ) + + assert get_reboot_status() == RebootStatus.REBOOT_REQUIRED + assert 1 == m_should_reboot.call_count + assert 1 == m_load_file.call_count + assert 1 == m_which.call_count + assert 1 == m_subp.call_count + assert 1 == m_kernel_info.call_count diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_snap.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_snap.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_snap.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_snap.py 2022-11-22 13:06:26.000000000 +0000 @@ -5,8 +5,11 @@ from uaclient import exceptions, messages from uaclient.snap import ( + SnapPackage, configure_snap_proxy, get_config_option_value, + get_installed_snaps, + get_snap_package_info_tracking, unconfigure_snap_proxy, ) @@ -114,3 +117,58 @@ assert None is unconfigure_snap_proxy(**kwargs) assert [mock.call("/usr/bin/snap")] == which.call_args_list assert subp_calls == subp.call_args_list + + +@mock.patch("uaclient.snap.system.subp") +class TestSnapPackagesInstalled: + def test_snap_packages_installed(self, sys_subp): + sys_subp.return_value = ( + "Name Version Rev Tracking Publisher Notes\n" + "helloworld 6.0.16 126 latest/stable dev1 -\n" + "bare 1.0 5 latest/stable canonical** base\n" + "canonical-livepatch 10.2.3 146 latest/stable canonical** -\n" + ), "" + expected_snaps = [ + SnapPackage( + "helloworld", "6.0.16", "126", "latest/stable", "dev1", "-" + ), + SnapPackage( + "bare", "1.0", "5", "latest/stable", "canonical**", "base" + ), + SnapPackage( + "canonical-livepatch", + "10.2.3", + "146", + "latest/stable", + "canonical**", + "-", + ), + ] + snaps = get_installed_snaps() + assert snaps[0].name == expected_snaps[0].name + assert snaps[0].rev == expected_snaps[0].rev + assert snaps[1].name == expected_snaps[1].name + assert snaps[1].publisher == expected_snaps[1].publisher + assert snaps[2].tracking == expected_snaps[2].tracking + assert snaps[2].notes == expected_snaps[2].notes + + @pytest.mark.parametrize( + "subp_ret,channel_ret", + [ + ( + """snap-id: ID001 + tracking: latest/stable + """, + "latest/stable", + ), + ( + """snap-id: ID001 + """, + None, + ), + ], + ) + def test_snap_package_info_tracking(self, sys_subp, subp_ret, channel_ret): + sys_subp.return_value = subp_ret, "" + channel = get_snap_package_info_tracking("test") + assert channel == channel_ret diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_status.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_status.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_status.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_status.py 2022-11-22 13:06:26.000000000 +0000 @@ -412,15 +412,47 @@ "accountInfo": { "id": "acct-1", "name": "test_account", - "createdAt": "2019-06-14T06:45:50Z", + "createdAt": datetime.datetime( + 2019, + 6, + 14, + 6, + 45, + 50, + tzinfo=datetime.timezone.utc, + ), "externalAccountIDs": [{"IDs": ["id1"], "origin": "AWS"}], }, "contractInfo": { "id": "cid", "name": "test_contract", - "createdAt": "2020-05-08T19:02:26Z", - "effectiveFrom": "2000-05-08T19:02:26Z", - "effectiveTo": "2040-05-08T19:02:26Z", + "createdAt": datetime.datetime( + 2020, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), + "effectiveFrom": datetime.datetime( + 2000, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), + "effectiveTo": datetime.datetime( + 2040, + 5, + 8, + 19, + 2, + 26, + tzinfo=datetime.timezone.utc, + ), "resourceEntitlements": entitled_res, "products": ["free"], }, @@ -543,7 +575,7 @@ ): root_cfg = FakeConfig.for_attached_machine() root_status = status.status(cfg=root_cfg) - normal_cfg = FakeConfig(root_mode=False) + normal_cfg = FakeConfig.for_attached_machine(root_mode=False) normal_status = status.status(cfg=normal_cfg) assert normal_status == root_status @@ -646,13 +678,17 @@ "accountInfo": { "id": "1", "name": "accountname", - "createdAt": "2019-06-14T06:45:50Z", + "createdAt": datetime.datetime( + 2019, 6, 14, 6, 45, 50, tzinfo=datetime.timezone.utc + ), "externalAccountIDs": [{"IDs": ["id1"], "origin": "AWS"}], }, "contractInfo": { "id": "contract-1", "name": "contractname", - "createdAt": "2020-05-08T19:02:26Z", + "createdAt": datetime.datetime( + 2020, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc + ), "resourceEntitlements": entitlements, "products": ["free"], }, @@ -740,11 +776,8 @@ expected_calls = [ mock.call( "", - messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format( - operation=".*" - ), + messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX, ), - mock.call("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED), mock.call( "", messages.ENABLE_REBOOT_REQUIRED_TMPL.format( @@ -773,8 +806,12 @@ "contractInfo": { "name": "contractname", "id": "contract-1", - "effectiveTo": "2020-07-18T00:00:00Z", - "createdAt": "2020-05-08T19:02:26Z", + "effectiveTo": datetime.datetime( + 2020, 7, 18, 0, 0, 0, tzinfo=datetime.timezone.utc + ), + "createdAt": datetime.datetime( + 2020, 5, 8, 0, 0, 0, tzinfo=datetime.timezone.utc + ), "resourceEntitlements": [], "products": ["free"], }, @@ -793,7 +830,11 @@ # Test that the read from the status cache work properly for non-root # users - cfg = FakeConfig(root_mode=False) + cfg = FakeConfig.for_attached_machine( + account_name="accountname", + machine_token=token, + root_mode=False, + ) assert expected_dt == status.status(cfg=cfg)["expires"] @mock.patch("uaclient.status.get_available_resources", return_value={}) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_util.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_util.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/tests/test_util.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/tests/test_util.py 2022-11-22 13:06:26.000000000 +0000 @@ -697,3 +697,41 @@ "UA_LOG_LEVEL": "DEBUG", } assert expected == util.get_pro_environment() + + +class TestDeduplicateArches: + @pytest.mark.parametrize( + ["arches", "expected"], + [ + ([], []), + (["anything"], ["anything"]), + (["amd64"], ["amd64"]), + (["amd64", "x86_64"], ["amd64"]), + ( + ["amd64", "ppc64el", "ppc64le", "s390x", "x86_64"], + ["amd64", "ppc64el", "s390x"], + ), + (["amd64", "i386", "i686", "x86_64"], ["amd64", "i386"]), + ( + ["amd64", "i386", "i686", "x86_64", "armhf", "arm64"], + ["amd64", "arm64", "armhf", "i386"], + ), + ( + [ + "x86_64", + "amd64", + "i686", + "i386", + "ppc64le", + "aarch64", + "arm64", + "armv7l", + "armhf", + "s390x", + ], + ["amd64", "arm64", "armhf", "i386", "ppc64el", "s390x"], + ), + ], + ) + def test_deduplicate_arches(self, arches, expected): + assert expected == util.deduplicate_arches(arches) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/util.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/util.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/util.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/util.py 2022-11-22 13:06:26.000000000 +0000 @@ -605,3 +605,17 @@ base_dict[key] = value else: base_dict[key] = value + + +def deduplicate_arches(arches: List[str]) -> List[str]: + deduplicated_arches = set() + arch_aliases = { + "x86_64": "amd64", + "i686": "i386", + "ppc64le": "ppc64el", + "aarch64": "arm64", + "armv7l": "armhf", + } + for arch in arches: + deduplicated_arches.add(arch_aliases.get(arch.lower(), arch)) + return sorted(list(deduplicated_arches)) diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/version.py ubuntu-advantage-tools-27.12~22.04.1/uaclient/version.py --- ubuntu-advantage-tools-27.11.3~22.04.1/uaclient/version.py 2022-10-25 16:46:23.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/uaclient/version.py 2022-11-22 13:06:26.000000000 +0000 @@ -18,7 +18,7 @@ from uaclient.exceptions import ProcessExecutionError from uaclient.system import subp -__VERSION__ = "27.11.3" +__VERSION__ = "27.12" PACKAGED_VERSION = "@@PACKAGED_VERSION@@" CANDIDATE_REGEX = r"Candidate: (?P.*?)\n" diff -Nru ubuntu-advantage-tools-27.11.3~22.04.1/update-motd.d/91-contract-ua-esm-status ubuntu-advantage-tools-27.12~22.04.1/update-motd.d/91-contract-ua-esm-status --- ubuntu-advantage-tools-27.11.3~22.04.1/update-motd.d/91-contract-ua-esm-status 2022-02-02 14:34:07.000000000 +0000 +++ ubuntu-advantage-tools-27.12~22.04.1/update-motd.d/91-contract-ua-esm-status 2022-11-22 13:06:26.000000000 +0000 @@ -1,4 +1,8 @@ #!/bin/sh -stamp="/var/lib/ubuntu-advantage/messages/motd-esm-service-status" +esm_stamp="/var/lib/ubuntu-advantage/messages/motd-esm-service-status" -[ ! -r "$stamp" ] || cat "$stamp" +[ ! -r "$esm_stamp" ] || cat "$esm_stamp" + +auto_attach_stamp="/var/lib/ubuntu-advantage/messages/motd-auto-attach-status" + +[ ! -r "$auto_attach_stamp" ] || cat "$auto_attach_stamp"