Merge ~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools:upload-27.5-jammy into ubuntu/+source/ubuntu-advantage-tools:ubuntu/devel

Proposed by Renan Rodrigo
Status: Merged
Merged at revision: ffc1e5c67a131126b2d4debd89eb0314f5893747
Proposed branch: ~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools:upload-27.5-jammy
Merge into: ubuntu/+source/ubuntu-advantage-tools:ubuntu/devel
Diff against target: 7955 lines (+3177/-1162)
86 files modified
README.md (+3/-5)
RELEASES.md (+14/-9)
debian/changelog (+30/-0)
debian/control (+1/-1)
debian/ubuntu-advantage-tools.postinst (+4/-4)
debian/ubuntu-advantage-tools.postrm (+1/-3)
features/_version.feature (+22/-5)
features/attach_invalidtoken.feature (+4/-2)
features/attach_validtoken.feature (+56/-24)
features/attached_commands.feature (+29/-13)
features/attached_enable.feature (+161/-19)
features/attached_status.feature (+3/-2)
features/install_uninstall.feature (+1/-0)
features/license_check.feature (+1/-0)
features/steps/steps.py (+14/-0)
features/ubuntu_pro.feature (+132/-39)
features/ubuntu_upgrade.feature (+41/-0)
features/ubuntu_upgrade_unattached.feature (+1/-1)
features/unattached_commands.feature (+15/-4)
features/unattached_status.feature (+87/-21)
help_data.yaml (+5/-11)
lib/reboot_cmds.py (+16/-22)
sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh (+34/-0)
sru/release-27.4/test-unattached-status-job.sh (+40/-0)
sru/release-27.5/test-aws-ipv6.sh (+66/-0)
tools/build.sh (+1/-0)
tools/create-lp-release-branches.sh (+2/-1)
tools/run-integration-tests.py (+2/-1)
tools/test-in-lxd.sh (+12/-0)
tox.ini (+1/-0)
uaclient/actions.py (+66/-0)
uaclient/cli.py (+147/-150)
uaclient/clouds/aws.py (+45/-7)
uaclient/clouds/identity.py (+13/-12)
uaclient/clouds/tests/test_aws.py (+115/-10)
uaclient/clouds/tests/test_identity.py (+9/-14)
uaclient/config.py (+161/-37)
uaclient/contract.py (+23/-6)
uaclient/defaults.py (+1/-0)
uaclient/entitlements/__init__.py (+31/-12)
uaclient/entitlements/base.py (+35/-10)
uaclient/entitlements/cis.py (+31/-6)
uaclient/entitlements/livepatch.py (+18/-34)
uaclient/entitlements/repo.py (+13/-1)
uaclient/entitlements/tests/conftest.py (+2/-1)
uaclient/entitlements/tests/test_base.py (+48/-20)
uaclient/entitlements/tests/test_cis.py (+4/-1)
uaclient/entitlements/tests/test_entitlements.py (+39/-8)
uaclient/entitlements/tests/test_fips.py (+9/-3)
uaclient/entitlements/tests/test_livepatch.py (+19/-39)
uaclient/entitlements/tests/test_repo.py (+27/-0)
uaclient/exceptions.py (+33/-1)
uaclient/jobs/update_messaging.py (+3/-3)
uaclient/lock.py (+106/-0)
uaclient/security.py (+2/-2)
uaclient/security_status.py (+8/-12)
uaclient/serviceclient.py (+7/-3)
uaclient/status.py (+27/-7)
uaclient/tests/test_actions.py (+145/-0)
uaclient/tests/test_apt.py (+2/-2)
uaclient/tests/test_cli.py (+45/-31)
uaclient/tests/test_cli_attach.py (+11/-8)
uaclient/tests/test_cli_auto_attach.py (+162/-264)
uaclient/tests/test_cli_collect_logs.py (+12/-5)
uaclient/tests/test_cli_config_set.py (+12/-4)
uaclient/tests/test_cli_config_show.py (+3/-2)
uaclient/tests/test_cli_config_unset.py (+2/-1)
uaclient/tests/test_cli_detach.py (+4/-2)
uaclient/tests/test_cli_disable.py (+36/-18)
uaclient/tests/test_cli_enable.py (+123/-83)
uaclient/tests/test_cli_fix.py (+2/-1)
uaclient/tests/test_cli_refresh.py (+2/-1)
uaclient/tests/test_cli_security_status.py (+14/-4)
uaclient/tests/test_cli_status.py (+303/-60)
uaclient/tests/test_config.py (+23/-9)
uaclient/tests/test_contract.py (+44/-0)
uaclient/tests/test_lock.py (+166/-0)
uaclient/tests/test_reboot_cmds.py (+6/-6)
uaclient/tests/test_security.py (+12/-17)
uaclient/tests/test_security_status.py (+40/-15)
uaclient/tests/test_update_messaging.py (+31/-22)
uaclient/tests/test_upgrade_lts_contract.py (+1/-1)
uaclient/tests/test_util.py (+83/-6)
uaclient/util.py (+37/-12)
uaclient/version.py (+1/-1)
ubuntu-advantage.1 (+14/-1)
Reviewer Review Type Date Requested Status
Robie Basak sru Approve
Athos Ribeiro (community) Approve
Review via email: mp+413641@code.launchpad.net

Description of the change

This is the second release candidate for release 27.5 (LP: #1956456) of ubuntu-advantage-tools, for Jammy.

The main changes it brings are related to CLI capabilities of the tool. There is also a transition from the CIS service to USG to be applied. Please see the changelog for a broader list of changes.

From the packaging perspective, only a refactor in postinst was performed.

Please let us know if there is something that we need to fix/change for this release.

SRU MRs for other releases:
Xenial: https://code.launchpad.net/~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/413671
Bionic: https://code.launchpad.net/~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/413672
Focal: https://code.launchpad.net/~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/413673
Hirsute: https://code.launchpad.net/~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/413674
Impish: https://code.launchpad.net/~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/413675

To post a comment you must log in.
Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

Hi Renan,

As we discussed offline, I am leaving some notes here:

- The changes in the postrm script are missing in the changelog;

- The documentation (including help commands) changed to include a link for a page which does not exist (https://ubuntu.com/security/certifications/docs/usg);

- There are several new calls to `resource["name"]` and to resource.get("name"). This was somewhat confusing while I was reviewing the code, when I wondered if we should always expect that key to be present in that dict or not. The reason I am pointing this out is that we did need a hotfix in the past for a similar issue with a different dict key access;

- and finally, it is worth mentioning that there was a subtle behavior change when calls to `entitlements.entitlement_factory` replaced calls to `entitlements.ENTITLEMENT_CLASS_BY_NAME`. When a non-existent name would be passed to ENTITLEMENT_CLASS_BY_NAME, it would instantly raise a KeyError exception. Now, the code will either explode at a later, different point, or just present an odd behavior.

Revision history for this message
Renan Rodrigo (renanrodrigo) wrote :

Thanks for your feedback, Athos,

- Ack - will update the MR to include the missing changelog entries;

- I will ping the security team about the missing page, they should handle it;

- Yes, technically we wouldn't need those .get calls. Yes, we do always expect that every resource has a name and this is not meant to change. The .get calls were inserted based on a review to make us extra-safe where it could fail.

- Yes, this is true and the team is aware of that. The change to the factory function helped us sort out some functionality on how to get the presentation name for resources, we understand the changes and their impact, and we consider it to be aligned with what we wanted to deliver. The passing test scenarios indicate to us that our flows are working as expected.

Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

AFAICT, The documentation for the CIS->USG change will be performed in all series, while the change is only valid from focal (through the contract API). While updating other series documentation in this sense does not seem to be ideal, Renan did let me know this had been discussed with the security team, which did acknowledged the change.

LGTM, thanks, Renan :)

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

Approved branch renanrodrigo/upload-27.5-jammy with commit ffc1e5c67a131126b2d4debd89eb0314f5893747

review: Approve (sru)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/README.md b/README.md
index c2c236b..27818e1 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
1# Ubuntu Advantage Client1# Ubuntu Advantage Client
22
3[![Build Status](https://travis-ci.com/canonical/ubuntu-advantage-client.svg?branch=master)](https://travis-ci.com/github/canonical/ubuntu-advantage-client)
4
5The Ubuntu Advantage client provides users with a simple mechanism to3The Ubuntu Advantage client provides users with a simple mechanism to
6view, enable, and disable offerings from Canonical on their system. The4view, enable, and disable offerings from Canonical on their system. The
7following entitlements are supported:5following entitlements are supported:
@@ -105,7 +103,7 @@ https://ubuntu.com/advantage.
105 token, service credentials, affordances, directives and obligations to allow103 token, service credentials, affordances, directives and obligations to allow
106 enabling and disabling Ubuntu Advantage services104 enabling and disabling Ubuntu Advantage services
107* UA client writes the machine token API response to the root-readonly105* UA client writes the machine token API response to the root-readonly
108 /var/lib/ubuntu-advantage/machine-token.json106 /var/lib/ubuntu-advantage/private/machine-token.json
109* UA client auto-enables any services defined with107* UA client auto-enables any services defined with
110 `obligations:{enableByDefault: true}`108 `obligations:{enableByDefault: true}`
111109
@@ -152,7 +150,7 @@ Jobs are executed by the timer script if:
152- Their interval since last successful run is already exceeded.150- Their interval since last successful run is already exceeded.
153151
154There is a random delay applied to the timer, to desynchronize job execution time152There is a random delay applied to the timer, to desynchronize job execution time
155on machines spinned at the same time, avoiding multiple synchronized calls to the153on machines spun at the same time, avoiding multiple synchronized calls to the
156same service.154same service.
157155
158Current jobs being checked and executed are:156Current jobs being checked and executed are:
@@ -576,5 +574,5 @@ sudo shutdown -h now
576* Use your cloud platform to clone or snapshot this VM as a golden image574* Use your cloud platform to clone or snapshot this VM as a golden image
577575
578576
579## Releasing ubuntu-adantage-tools577## Releasing ubuntu-advantage-tools
580see [RELEASES.md](RELEASES.md)578see [RELEASES.md](RELEASES.md)
diff --git a/RELEASES.md b/RELEASES.md
index f629962..b4948f7 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -183,22 +183,21 @@ If this is your first time releasing ubuntu-advantage-tools, you'll need to do t
183183
184 a. Ask in ~Server for a review of your MPs. Include a link to the primary MP into ubuntu/devel and mention the other MPs are only changelog MPs for the SRUs into past releases.184 a. Ask in ~Server for a review of your MPs. Include a link to the primary MP into ubuntu/devel and mention the other MPs are only changelog MPs for the SRUs into past releases.
185 185
186 b. If they request changes, create a PR into `main` on github and ask UAClient team for review. After a merge, open another PR on github, cherry-picking the change into the release branch. After that is merged, cherry-pick the commit into your `upload-<this-version>-impish` branch and push to launchpad. Then notify the Server Team member that you have addressed their requests. (This can probably be simplified).186 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-<this-version>-<devel-release>` branch and push to launchpad. You'll also need to rebase the other `upload-<this-version>-<series>` branches and force push them to launchpad. Then notify the Server Team member that you have addressed their requests.
187 * Some issues may just be filed for addressing in the future if they are not urgent or pertinent to this release.187 * Some issues may just be filed for addressing in the future if they are not urgent or pertinent to this release.
188 * 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.188 * 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.
189 * 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.
189190
190 c. Once review is complete and approved, confirm that Ubuntu Server approver will be tagging the PR with the appropriate `upload/<version>` tag so git-ubuntu will import rich commit history.191 c. Once review is complete and approved, confirm that Ubuntu Server approver will be tagging the PR with the appropriate `upload/<version>` tag so git-ubuntu will import rich commit history.
191192
192 d. Check `rmadison ubuntu-advantage-tools` for updated version in devel release193 d. At this point the Server Team member should **not** upload the version to the devel release.
194 * If they do, then any changes to the code after this point will require a bump in the patch version of the release.
193195
194 e. Confirm availability in <devel>-updates pocket via `lxc launch ubuntu-daily:impish dev-i; lxc exec dev-i -- apt update; lxc exec dev-i -- apt-cache policy ubuntu-advantage-tools`196 e. Ask Ubuntu Server approver if they also have upload rights to the proposed queue. If they do, request that they upload ubuntu-advantage-tools for all releases. If they do not, ask in ~Server channel for a Ubuntu Server team member with upload rights for an upload review of the MP for the proposed queue.
195 * Note that any changes to the code after this point will likely require a bump in the patch version of the release.
196197
197 f. Ask Ubuntu Server approver if they also have upload rights to the proposed queue. If they do, request that they upload ubuntu-advantage-tools for all releases. If they do not, ask in ~Server channel for a Ubuntu Server team member with upload rights for an upload review of the MP for the proposed queue.198 f. Once upload review is complete and approved, confirm that Ubuntu Server approver will upload ua-tools via dput to the `-proposed` queue.
198199
199 g. Once upload review is complete and approved, confirm that Ubuntu Server approver will upload ua-tools via dput to the `-proposed` queue.200 g. Check the [-proposed release queue](https://launchpad.net/ubuntu/xenial/+queue?queue_state=1&queue_text=ubuntu-advantage-tools) for presence of ua-tools in unapproved state for each supported release. Note: libera chat #ubuntu-release IRC channel has a bot that reports queued uploads of any package in a message like "Unapproved: ubuntu-advantage-tools .. version".
200
201 h. Check the [-proposed release queue](https://launchpad.net/ubuntu/xenial/+queue?queue_state=1&queue_text=ubuntu-advantage-tools) for presence of ua-tools in unapproved state for each supported release. Note: libera chat #ubuntu-release IRC channel has a bot that reports queued uploads of any package in a message like "Unapproved: ubuntu-advantage-tools .. version".
202201
2035. SRU Review2025. SRU Review
204203
@@ -240,7 +239,13 @@ If this is your first time releasing ubuntu-advantage-tools, you'll need to do t
240239
241 h. Once all SRU bugs are tagged as `verification*-done`, all SRU-bugs should be listed as green in [the pending_sru page](https://people.canonical.com/~ubuntu-archive/pending-sru.html).240 h. Once all SRU bugs are tagged as `verification*-done`, all SRU-bugs should be listed as green in [the pending_sru page](https://people.canonical.com/~ubuntu-archive/pending-sru.html).
242 241
243 i. After the pending_sru page says that ubuntu-advantage-tools has been in proposed for 7 days, it is now time to ping the [current SRU vanguard](https://wiki.ubuntu.com/StableReleaseUpdates#Publishing) for acceptance of ubuntu-advantage-tools into -updates.242 i. After the pending sru page says that ubuntu-advantage-tools has been in proposed for 7 days, it is now time to ping the [current SRU vanguard](https://wiki.ubuntu.com/StableReleaseUpdates#Publishing) for acceptance of ubuntu-advantage-tools into -updates.
243
244 j. Ping the Ubuntu Server team member who approved the version in step `II.4` to now upload to the devel release.
245
246 k. Check `rmadison ubuntu-advantage-tools` for updated version in devel release
247
248 l. Confirm availability in <devel-series>-updates pocket via `lxc launch ubuntu-daily:<devel-series> dev-i; lxc exec dev-i -- apt update; lxc exec dev-i -- apt-cache policy ubuntu-advantage-tools`
244249
245### III. Final release to team infrastructure250### III. Final release to team infrastructure
246251
diff --git a/debian/changelog b/debian/changelog
index 9779f56..a55d9dd 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,33 @@
1ubuntu-advantage-tools (27.5~22.04.1) jammy; urgency=medium
2
3 * d/control:
4 - Update homepage URL
5 * d/tools.postinst:
6 - Refactor to use valid_services
7 * d/tools.postrm:
8 - Use a wildcard to remove ua related gpg files
9 * New upstream release 27.5 (LP: #1956456)
10 - aws: add support for the IPv6 metadata endpoint
11 - cis: update URL for the documentation
12 - cli:
13 + add endpoint to simulate the status using a specific contract token
14 + fix return code when attaching an already attached machine (GH: #1867)
15 + fix security-status to consider all possible origins to show updates
16 + include cloud build.info in the collect-logs tarball
17 + only show services which exist in the contracts server in ua status
18 - docs: fix typos and wrong/outdated information
19 - livepatch: always use the full path in livepatch calls (LP: #1951954)
20 - logs:
21 + improve rules to redact sensitive information from all log files
22 + redact sensitive information from older unredacted log files
23 + log errors from external software execution, for debugging purposes
24 - usg:
25 + support the presentedAs affordance from the contract server, showing
26 services in the CLI with the appropriate names
27 + replace the CIS entitlement by USG on Focal and onwards
28
29 -- Renan Rodrigo <renanrodrigo@canonical.com> Tue, 04 Jan 2022 17:30:26 -0300
30
1ubuntu-advantage-tools (27.4.2~22.04.1) jammy; urgency=medium31ubuntu-advantage-tools (27.4.2~22.04.1) jammy; urgency=medium
232
3 * d/tools.postinst:33 * d/tools.postinst:
diff --git a/debian/control b/debian/control
index 9ec72f6..6aabba2 100644
--- a/debian/control
+++ b/debian/control
@@ -29,7 +29,7 @@ Build-Depends: bash-completion,
29 python3-setuptools,29 python3-setuptools,
30 python3-yaml30 python3-yaml
31Standards-Version: 4.5.131Standards-Version: 4.5.1
32Homepage: https://buy.ubuntu.com32Homepage: https://ubuntu.com/advantage
33Vcs-Git: https://github.com/CanonicalLtd/ubuntu-advantage-script.git33Vcs-Git: https://github.com/CanonicalLtd/ubuntu-advantage-script.git
34Vcs-Browser: https://github.com/CanonicalLtd/ubuntu-advantage-script34Vcs-Browser: https://github.com/CanonicalLtd/ubuntu-advantage-script
35Rules-Requires-Root: no35Rules-Requires-Root: no
diff --git a/debian/ubuntu-advantage-tools.postinst b/debian/ubuntu-advantage-tools.postinst
index 8ba7c64..191f0db 100644
--- a/debian/ubuntu-advantage-tools.postinst
+++ b/debian/ubuntu-advantage-tools.postinst
@@ -57,8 +57,8 @@ MACHINE_TOKEN_FILE="/var/lib/ubuntu-advantage/private/machine-token.json"
57redact_ubuntu_release_from_ua_apt_filenames() {57redact_ubuntu_release_from_ua_apt_filenames() {
58 DIR=$158 DIR=$1
59 UA_SERVICES=$(/usr/bin/python3 -c "59 UA_SERVICES=$(/usr/bin/python3 -c "
60from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME60from uaclient.entitlements import valid_services
61print(*ENTITLEMENT_CLASS_BY_NAME.keys(), sep=' ')61print(*valid_services(allow_beta=True, all_names=True), sep=' ')
62")62")
6363
64 for file in "$DIR"/*; do64 for file in "$DIR"/*; do
@@ -118,8 +118,8 @@ check_service_is_beta() {
118 service_name=$1118 service_name=$1
119 _IS_BETA_SVC=$(/usr/bin/python3 -c "119 _IS_BETA_SVC=$(/usr/bin/python3 -c "
120from uaclient.config import UAConfig120from uaclient.config import UAConfig
121from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME121from uaclient.entitlements import entitlement_factory
122ent_cls = ENTITLEMENT_CLASS_BY_NAME.get('${service_name}')122ent_cls = entitlement_factory('${service_name}')
123if ent_cls:123if ent_cls:
124 cfg = UAConfig()124 cfg = UAConfig()
125 allow_beta = cfg.features.get('allow_beta', False)125 allow_beta = cfg.features.get('allow_beta', False)
diff --git a/debian/ubuntu-advantage-tools.postrm b/debian/ubuntu-advantage-tools.postrm
index 978b492..ce79341 100644
--- a/debian/ubuntu-advantage-tools.postrm
+++ b/debian/ubuntu-advantage-tools.postrm
@@ -19,9 +19,7 @@ remove_logs(){
19}19}
2020
21remove_gpg_files(){21remove_gpg_files(){
22 rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-esm-infra-trusty.gpg22 rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-*.gpg
23 rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-esm-apps.gpg
24 rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-fips.gpg
25}23}
2624
27case "$1" in25case "$1" in
diff --git a/features/_version.feature b/features/_version.feature
index 0ee9f1b..e265769 100644
--- a/features/_version.feature
+++ b/features/_version.feature
@@ -12,18 +12,27 @@ Feature: UA is expected version
12 @uses.config.machine_type.gcp.pro12 @uses.config.machine_type.gcp.pro
13 Scenario Outline: Check ua version13 Scenario Outline: Check ua version
14 Given a `<release>` machine with ubuntu-advantage-tools installed14 Given a `<release>` machine with ubuntu-advantage-tools installed
15 When I run `ua version` with sudo15 When I run `dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools` with sudo
16 Then I will see the following on stdout16 Then stdout matches regexp:
17 """17 """
18 {UACLIENT_BEHAVE_CHECK_VERSION}18 {UACLIENT_BEHAVE_CHECK_VERSION}
19 """19 """
20 When I run `ua version` with sudo
21 Then stdout matches regexp:
22 # We are adding that regex here to match possible config overrides
23 # we add. For example, on PRO machines we add a config override to
24 # disable auto-attach on boot
25 """
26 {UACLIENT_BEHAVE_CHECK_VERSION}.*
27 """
20 Examples: version28 Examples: version
21 | release |29 | release |
22 | xenial |30 | xenial |
23 | bionic |31 | bionic |
24 | focal |32 | focal |
25 | hirsute |33 | hirsute |
26 | impish |34 | impish |
35 | jammy |
2736
28 @series.all37 @series.all
29 @uses.config.check_version38 @uses.config.check_version
@@ -31,15 +40,23 @@ Feature: UA is expected version
31 @upgrade40 @upgrade
32 Scenario Outline: Check ua version41 Scenario Outline: Check ua version
33 Given a `<release>` machine with ubuntu-advantage-tools installed42 Given a `<release>` machine with ubuntu-advantage-tools installed
34 When I run `ua version` with sudo43 When I run `dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools` with sudo
35 Then I will see the following on stdout44 Then I will see the following on stdout
36 """45 """
37 {UACLIENT_BEHAVE_CHECK_VERSION}46 {UACLIENT_BEHAVE_CHECK_VERSION}
38 """47 """
48 When I run `ua version` with sudo
49 Then stdout matches regexp:
50 # We are adding that regex here to match possible config overrides
51 # we add. For example, on PRO machines we add a config override to
52 # disable auto-attach on boot
53 """
54 {UACLIENT_BEHAVE_CHECK_VERSION}.*
55 """
39 Examples: version56 Examples: version
40 | release |57 | release |
41 | xenial |58 | xenial |
42 | bionic |59 | bionic |
43 | focal |60 | focal |
44 | hirsute |61 | hirsute |
45 | impish |62 | impish |
diff --git a/features/attach_invalidtoken.feature b/features/attach_invalidtoken.feature
index 776ca00..6ad2d93 100644
--- a/features/attach_invalidtoken.feature
+++ b/features/attach_invalidtoken.feature
@@ -21,7 +21,8 @@ Feature: Command behaviour when trying to attach a machine to an Ubuntu
21 | bionic |21 | bionic |
22 | focal |22 | focal |
23 | hirsute |23 | hirsute |
24 | impish |24 | impish |
25 | jammy |
2526
26 @uses.config.contract_token_staging_expired27 @uses.config.contract_token_staging_expired
27 @series.all28 @series.all
@@ -41,4 +42,5 @@ Feature: Command behaviour when trying to attach a machine to an Ubuntu
41 | bionic |42 | bionic |
42 | focal |43 | focal |
43 | hirsute |44 | hirsute |
44 | impish |45 | impish |
46 | jammy |
diff --git a/features/attach_validtoken.feature b/features/attach_validtoken.feature
index cfd1e44..ec4400b 100644
--- a/features/attach_validtoken.feature
+++ b/features/attach_validtoken.feature
@@ -2,6 +2,7 @@
2Feature: Command behaviour when attaching a machine to an Ubuntu Advantage2Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
3 subscription using a valid token3 subscription using a valid token
44
5 @series.jammy
5 @series.hirsute6 @series.hirsute
6 @series.impish7 @series.impish
7 @uses.config.machine_type.lxd.container8 @uses.config.machine_type.lxd.container
@@ -13,7 +14,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
13 """14 """
14 SERVICE ENTITLED STATUS DESCRIPTION15 SERVICE ENTITLED STATUS DESCRIPTION
15 cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages16 cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages
16 cis +yes +n/a +Center for Internet Security Audit Tools17 cis +yes +n/a +Security compliance and audit tools
17 esm-apps +yes +n/a +UA Apps: Extended Security Maintenance \(ESM\)18 esm-apps +yes +n/a +UA Apps: Extended Security Maintenance \(ESM\)
18 esm-infra +yes +n/a +UA Infra: Extended Security Maintenance \(ESM\)19 esm-infra +yes +n/a +UA Infra: Extended Security Maintenance \(ESM\)
19 fips +yes +n/a +NIST-certified core packages20 fips +yes +n/a +NIST-certified core packages
@@ -24,7 +25,8 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
24 Examples: ubuntu release25 Examples: ubuntu release
25 | release |26 | release |
26 | hirsute |27 | hirsute |
27 | impish |28 | impish |
29 | jammy |
2830
29 @series.lts31 @series.lts
30 @uses.config.machine_type.lxd.container32 @uses.config.machine_type.lxd.container
@@ -47,7 +49,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
47 """49 """
48 \d+ update(s)? can be applied immediately.50 \d+ update(s)? can be applied immediately.
49 """51 """
50 When I attach `contract_token` with sudo52 When I attach `contract_token_staging` with sudo
51 Then stdout matches regexp:53 Then stdout matches regexp:
52 """54 """
53 UA Infra: ESM enabled55 UA Infra: ESM enabled
@@ -60,17 +62,29 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
60 """62 """
61 SERVICE ENTITLED STATUS DESCRIPTION63 SERVICE ENTITLED STATUS DESCRIPTION
62 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages64 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
63 cis +yes +disabled +Center for Internet Security Audit Tools65 """
66 And stdout matches regexp:
67 """
64 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)68 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
65 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)69 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
66 fips +yes +n/a +NIST-certified core packages70 fips +yes +n/a +NIST-certified core packages
67 fips-updates +yes +n/a +NIST-certified core packages with priority security updates71 fips-updates +yes +n/a +NIST-certified core packages with priority security updates
68 livepatch +yes +n/a +Canonical Livepatch service72 livepatch +yes +n/a +Canonical Livepatch service
69 """73 """
74 And stdout matches regexp:
75 """
76 <cis_or_usg> +yes +disabled +Security compliance and audit tools
77 """
70 And stderr matches regexp:78 And stderr matches regexp:
71 """79 """
72 Enabling default service esm-infra80 Enabling default service esm-infra
73 """81 """
82 When I verify that running `ua attach contract_token` `with sudo` exits `2`
83 Then stderr matches regexp:
84 """
85 This machine is already attached to '.+'
86 To use a different subscription first run: sudo ua detach.
87 """
74 When I run `ua disable esm-apps --assume-yes` with sudo88 When I run `ua disable esm-apps --assume-yes` with sudo
75 When I append the following on uaclient config:89 When I append the following on uaclient config:
76 """90 """
@@ -154,10 +168,10 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
154168
155 """169 """
156 Examples: ubuntu release packages170 Examples: ubuntu release packages
157 | release | downrev_pkg | cc_status |171 | release | downrev_pkg | cc_status | cis_or_usg |
158 | xenial | libkrad0=1.13.2+dfsg-5 | disabled |172 | xenial | libkrad0=1.13.2+dfsg-5 | disabled | cis |
159 | bionic | libkrad0=1.16-2build1 | n/a |173 | bionic | libkrad0=1.16-2build1 | n/a | cis |
160 | focal | hello=2.10-2ubuntu2 | n/a |174 | focal | hello=2.10-2ubuntu2 | n/a | usg |
161175
162 @series.all176 @series.all
163 @uses.config.machine_type.aws.generic177 @uses.config.machine_type.aws.generic
@@ -196,22 +210,28 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
196 """210 """
197 SERVICE ENTITLED STATUS DESCRIPTION211 SERVICE ENTITLED STATUS DESCRIPTION
198 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages212 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
199 cis +yes +disabled +Center for Internet Security Audit Tools213 """
214 And stdout matches regexp:
215 """
200 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)216 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
201 fips +yes +<fips_status> +NIST-certified core packages217 fips +yes +<fips_status> +NIST-certified core packages
202 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates218 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
203 livepatch +yes +<lp_status> +<lp_desc>219 livepatch +yes +<lp_status> +<lp_desc>
204 """220 """
221 And stdout matches regexp:
222 """
223 <cis_or_usg> +yes +disabled +Security compliance and audit tools
224 """
205 And stderr matches regexp:225 And stderr matches regexp:
206 """226 """
207 Enabling default service esm-infra227 Enabling default service esm-infra
208 """228 """
209229
210 Examples: ubuntu release livepatch status230 Examples: ubuntu release livepatch status
211 | release | fips_status |lp_status | lp_desc | cc_status |231 | release | fips_status |lp_status | lp_desc | cc_status | cis_or_usg |
212 | xenial | disabled |enabled | Canonical Livepatch service | disabled |232 | xenial | disabled |enabled | Canonical Livepatch service | disabled | cis |
213 | bionic | disabled |enabled | Canonical Livepatch service | n/a |233 | bionic | disabled |enabled | Canonical Livepatch service | n/a | cis |
214 | focal | n/a |enabled | Canonical Livepatch service | n/a |234 | focal | n/a |enabled | Canonical Livepatch service | n/a | usg |
215235
216 @series.all236 @series.all
217 @uses.config.machine_type.azure.generic237 @uses.config.machine_type.azure.generic
@@ -250,22 +270,28 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
250 """270 """
251 SERVICE ENTITLED STATUS DESCRIPTION271 SERVICE ENTITLED STATUS DESCRIPTION
252 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages272 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
253 cis +yes +disabled +Center for Internet Security Audit Tools273 """
274 And stdout matches regexp:
275 """
254 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)276 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
255 fips +yes +<fips_status> +NIST-certified core packages277 fips +yes +<fips_status> +NIST-certified core packages
256 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates278 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
257 livepatch +yes +<lp_status> +Canonical Livepatch service279 livepatch +yes +<lp_status> +Canonical Livepatch service
258 """280 """
281 And stdout matches regexp:
282 """
283 <cis_or_usg> +yes +disabled +Security compliance and audit tools
284 """
259 And stderr matches regexp:285 And stderr matches regexp:
260 """286 """
261 Enabling default service esm-infra287 Enabling default service esm-infra
262 """288 """
263289
264 Examples: ubuntu release livepatch status290 Examples: ubuntu release livepatch status
265 | release | lp_status | fips_status | cc_status |291 | release | lp_status | fips_status | cc_status | cis_or_usg |
266 | xenial | n/a | n/a | disabled |292 | xenial | n/a | n/a | disabled | cis |
267 | bionic | n/a | disabled | n/a |293 | bionic | n/a | disabled | n/a | cis |
268 | focal | enabled | n/a | n/a |294 | focal | enabled | n/a | n/a | usg |
269295
270 @series.all296 @series.all
271 @uses.config.machine_type.gcp.generic297 @uses.config.machine_type.gcp.generic
@@ -291,7 +317,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
291 features:317 features:
292 machine_token_overlay: "/tmp/machine-token-overlay.json"318 machine_token_overlay: "/tmp/machine-token-overlay.json"
293 """319 """
294 And I attach `contract_token` with sudo320 And I attach `contract_token_staging` with sudo
295 Then stdout matches regexp:321 Then stdout matches regexp:
296 """322 """
297 UA Infra: ESM enabled323 UA Infra: ESM enabled
@@ -304,19 +330,25 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
304 """330 """
305 SERVICE ENTITLED STATUS DESCRIPTION331 SERVICE ENTITLED STATUS DESCRIPTION
306 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages332 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
307 cis +yes +disabled +Center for Internet Security Audit Tools333 """
334 And stdout matches regexp:
335 """
308 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)336 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
309 fips +yes +<fips_status> +NIST-certified core packages337 fips +yes +<fips_status> +NIST-certified core packages
310 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates338 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
311 livepatch +yes +<lp_status> +Canonical Livepatch service339 livepatch +yes +<lp_status> +Canonical Livepatch service
312 """340 """
341 And stdout matches regexp:
342 """
343 <cis_or_usg> +yes +disabled +Security compliance and audit tools
344 """
313 And stderr matches regexp:345 And stderr matches regexp:
314 """346 """
315 Enabling default service esm-infra347 Enabling default service esm-infra
316 """348 """
317349
318 Examples: ubuntu release livepatch status350 Examples: ubuntu release livepatch status
319 | release | lp_status | fips_status | cc_status |351 | release | lp_status | fips_status | cc_status | cis_or_usg |
320 | xenial | n/a | n/a | disabled |352 | xenial | n/a | n/a | disabled | cis |
321 | bionic | n/a | disabled | n/a |353 | bionic | n/a | disabled | n/a | cis |
322 | focal | enabled | n/a | n/a |354 | focal | enabled | n/a | n/a | usg |
diff --git a/features/attached_commands.feature b/features/attached_commands.feature
index 0ef79db..d8718ae 100644
--- a/features/attached_commands.feature
+++ b/features/attached_commands.feature
@@ -49,6 +49,7 @@ Feature: Command behaviour when attached to an UA subscription
49 | xenial |49 | xenial |
50 | hirsute |50 | hirsute |
51 | impish |51 | impish |
52 | jammy |
5253
53 @series.all54 @series.all
54 @uses.config.machine_type.lxd.container55 @uses.config.machine_type.lxd.container
@@ -65,6 +66,7 @@ Feature: Command behaviour when attached to an UA subscription
65 | xenial |66 | xenial |
66 | hirsute |67 | hirsute |
67 | impish |68 | impish |
69 | jammy |
6870
69 @series.all71 @series.all
70 @uses.config.machine_type.lxd.container72 @uses.config.machine_type.lxd.container
@@ -91,6 +93,7 @@ Feature: Command behaviour when attached to an UA subscription
91 | xenial |93 | xenial |
92 | hirsute |94 | hirsute |
93 | impish |95 | impish |
96 | jammy |
9497
95 @series.all98 @series.all
96 @uses.config.machine_type.lxd.container99 @uses.config.machine_type.lxd.container
@@ -116,6 +119,7 @@ Feature: Command behaviour when attached to an UA subscription
116 | xenial |119 | xenial |
117 | hirsute |120 | hirsute |
118 | impish |121 | impish |
122 | jammy |
119123
120 @series.lts124 @series.lts
121 @uses.config.machine_type.lxd.container125 @uses.config.machine_type.lxd.container
@@ -161,7 +165,7 @@ Feature: Command behaviour when attached to an UA subscription
161 @uses.config.machine_type.lxd.container165 @uses.config.machine_type.lxd.container
162 Scenario Outline: Attached detach in an ubuntu machine166 Scenario Outline: Attached detach in an ubuntu machine
163 Given a `<release>` machine with ubuntu-advantage-tools installed167 Given a `<release>` machine with ubuntu-advantage-tools installed
164 When I attach `contract_token` with sudo168 When I attach `contract_token_staging` with sudo
165 Then I verify that running `ua detach` `as non-root` exits `1`169 Then I verify that running `ua detach` `as non-root` exits `1`
166 And stderr matches regexp:170 And stderr matches regexp:
167 """171 """
@@ -182,7 +186,9 @@ Feature: Command behaviour when attached to an UA subscription
182 """186 """
183 SERVICE AVAILABLE DESCRIPTION187 SERVICE AVAILABLE DESCRIPTION
184 cc-eal +<cc-eal> +Common Criteria EAL2 Provisioning Packages188 cc-eal +<cc-eal> +Common Criteria EAL2 Provisioning Packages
185 cis +<cis> +Center for Internet Security Audit Tools189 """
190 Then stdout matches regexp:
191 """
186 esm-apps +<esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)192 esm-apps +<esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
187 esm-infra +yes +UA Infra: Extended Security Maintenance \(ESM\)193 esm-infra +yes +UA Infra: Extended Security Maintenance \(ESM\)
188 fips +<fips> +NIST-certified core packages194 fips +<fips> +NIST-certified core packages
@@ -191,6 +197,10 @@ Feature: Command behaviour when attached to an UA subscription
191 ros +<ros> +Security Updates for the Robot Operating System197 ros +<ros> +Security Updates for the Robot Operating System
192 ros-updates +<ros> +All Updates for the Robot Operating System198 ros-updates +<ros> +All Updates for the Robot Operating System
193 """199 """
200 Then stdout matches regexp:
201 """
202 <cis_or_usg> +<cis> +Security compliance and audit tools
203 """
194 And stdout matches regexp:204 And stdout matches regexp:
195 """205 """
196 This machine is not attached to a UA subscription.206 This machine is not attached to a UA subscription.
@@ -198,10 +208,10 @@ Feature: Command behaviour when attached to an UA subscription
198 And I verify that running `apt update` `with sudo` exits `0`208 And I verify that running `apt update` `with sudo` exits `0`
199209
200 Examples: ubuntu release210 Examples: ubuntu release
201 | release | esm-apps | cc-eal | cis | fips | fips-update | ros |211 | release | esm-apps | cc-eal | cis | fips | fips-update | ros | cis_or_usg |
202 | bionic | yes | no | yes | yes | yes | yes |212 | xenial | yes | yes | yes | yes | yes | yes | cis |
203 | focal | yes | no | yes | yes | yes | no |213 | bionic | yes | no | yes | yes | yes | yes | cis |
204 | xenial | yes | yes | yes | yes | yes | yes |214 | focal | yes | no | yes | yes | yes | no | usg |
205215
206 @series.all216 @series.all
207 @uses.config.machine_type.lxd.container217 @uses.config.machine_type.lxd.container
@@ -213,7 +223,7 @@ Feature: Command behaviour when attached to an UA subscription
213 """223 """
214 This command must be run as root \(try using sudo\).224 This command must be run as root \(try using sudo\).
215 """225 """
216 When I run `ua auto-attach` with sudo226 When I verify that running `ua auto-attach` `with sudo` exits `2`
217 Then stderr matches regexp:227 Then stderr matches regexp:
218 """228 """
219 This machine is already attached229 This machine is already attached
@@ -226,6 +236,7 @@ Feature: Command behaviour when attached to an UA subscription
226 | xenial |236 | xenial |
227 | hirsute |237 | hirsute |
228 | impish |238 | impish |
239 | jammy |
229240
230 @series.all241 @series.all
231 @uses.config.machine_type.lxd.container242 @uses.config.machine_type.lxd.container
@@ -248,6 +259,7 @@ Feature: Command behaviour when attached to an UA subscription
248 | xenial |259 | xenial |
249 | hirsute |260 | hirsute |
250 | impish |261 | impish |
262 | jammy |
251263
252 @series.all264 @series.all
253 @uses.config.machine_type.lxd.container265 @uses.config.machine_type.lxd.container
@@ -299,6 +311,7 @@ Feature: Command behaviour when attached to an UA subscription
299 | xenial |311 | xenial |
300 | hirsute |312 | hirsute |
301 | impish |313 | impish |
314 | jammy |
302315
303 @series.lts316 @series.lts
304 @uses.config.machine_type.lxd.container317 @uses.config.machine_type.lxd.container
@@ -389,8 +402,8 @@ Feature: Command behaviour when attached to an UA subscription
389 Client to manage Ubuntu Advantage services on a machine.402 Client to manage Ubuntu Advantage services on a machine.
390 - cc-eal: Common Criteria EAL2 Provisioning Packages403 - cc-eal: Common Criteria EAL2 Provisioning Packages
391 \(https://ubuntu.com/cc-eal\)404 \(https://ubuntu.com/cc-eal\)
392 - cis: Center for Internet Security Audit Tools405 - cis: Security compliance and audit tools
393 \(https://ubuntu.com/security/certifications#cis\)406 \(https://ubuntu.com/security/certifications/docs/usg\)
394 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)407 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
395 \(https://ubuntu.com/security/esm\)408 \(https://ubuntu.com/security/esm\)
396 - fips-updates: NIST-certified core packages with priority security updates409 - fips-updates: NIST-certified core packages with priority security updates
@@ -406,8 +419,8 @@ Feature: Command behaviour when attached to an UA subscription
406 Client to manage Ubuntu Advantage services on a machine.419 Client to manage Ubuntu Advantage services on a machine.
407 - cc-eal: Common Criteria EAL2 Provisioning Packages420 - cc-eal: Common Criteria EAL2 Provisioning Packages
408 \(https://ubuntu.com/cc-eal\)421 \(https://ubuntu.com/cc-eal\)
409 - cis: Center for Internet Security Audit Tools422 - cis: Security compliance and audit tools
410 \(https://ubuntu.com/security/certifications#cis\)423 \(https://ubuntu.com/security/certifications/docs/usg\)
411 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)424 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
412 \(https://ubuntu.com/security/esm\)425 \(https://ubuntu.com/security/esm\)
413 - fips-updates: NIST-certified core packages with priority security updates426 - fips-updates: NIST-certified core packages with priority security updates
@@ -423,8 +436,8 @@ Feature: Command behaviour when attached to an UA subscription
423 Client to manage Ubuntu Advantage services on a machine.436 Client to manage Ubuntu Advantage services on a machine.
424 - cc-eal: Common Criteria EAL2 Provisioning Packages437 - cc-eal: Common Criteria EAL2 Provisioning Packages
425 \(https://ubuntu.com/cc-eal\)438 \(https://ubuntu.com/cc-eal\)
426 - cis: Center for Internet Security Audit Tools439 - cis: Security compliance and audit tools
427 \(https://ubuntu.com/security/certifications#cis\)440 \(https://ubuntu.com/security/certifications/docs/usg\)
428 - esm-apps: UA Apps: Extended Security Maintenance \(ESM\)441 - esm-apps: UA Apps: Extended Security Maintenance \(ESM\)
429 \(https://ubuntu.com/security/esm\)442 \(https://ubuntu.com/security/esm\)
430 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)443 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
@@ -448,6 +461,7 @@ Feature: Command behaviour when attached to an UA subscription
448 | xenial | enabled |461 | xenial | enabled |
449 | hirsute | n/a |462 | hirsute | n/a |
450 | impish | n/a |463 | impish | n/a |
464 | jammy | n/a |
451465
452 @series.lts466 @series.lts
453 @uses.config.machine_type.lxd.container467 @uses.config.machine_type.lxd.container
@@ -570,6 +584,7 @@ Feature: Command behaviour when attached to an UA subscription
570 | focal |584 | focal |
571 | hirsute |585 | hirsute |
572 | impish |586 | impish |
587 | jammy |
573588
574 @series.lts589 @series.lts
575 @uses.config.machine_type.lxd.container590 @uses.config.machine_type.lxd.container
@@ -591,6 +606,7 @@ Feature: Command behaviour when attached to an UA subscription
591 # So the -error suffix does not appear there.606 # So the -error suffix does not appear there.
592 Then stdout matches regexp:607 Then stdout matches regexp:
593 """608 """
609 build.info
594 cloud-id.txt610 cloud-id.txt
595 jobs-status.json611 jobs-status.json
596 journalctl.txt612 journalctl.txt
diff --git a/features/attached_enable.feature b/features/attached_enable.feature
index 289f048..2c29429 100644
--- a/features/attached_enable.feature
+++ b/features/attached_enable.feature
@@ -142,6 +142,7 @@ Feature: Enable command behaviour when attached to an UA subscription
142 | xenial |142 | xenial |
143 | hirsute |143 | hirsute |
144 | impish |144 | impish |
145 | jammy |
145146
146 @series.lts147 @series.lts
147 @uses.config.machine_type.lxd.container148 @uses.config.machine_type.lxd.container
@@ -187,11 +188,12 @@ Feature: Enable command behaviour when attached to an UA subscription
187 | focal |188 | focal |
188 | xenial |189 | xenial |
189190
190 @series.lts191 @series.xenial
192 @series.bionic
191 @uses.config.machine_type.lxd.container193 @uses.config.machine_type.lxd.container
192 Scenario Outline: Attached enable of cis service in a ubuntu machine194 Scenario Outline: Attached enable of cis service in a ubuntu machine
193 Given a `<release>` machine with ubuntu-advantage-tools installed195 Given a `<release>` machine with ubuntu-advantage-tools installed
194 When I attach `contract_token` with sudo196 When I attach `contract_token_staging` with sudo
195 And I verify that running `ua enable cis` `with sudo` exits `0`197 And I verify that running `ua enable cis` `with sudo` exits `0`
196 Then I will see the following on stdout:198 Then I will see the following on stdout:
197 """199 """
@@ -199,7 +201,7 @@ Feature: Enable command behaviour when attached to an UA subscription
199 Updating package lists201 Updating package lists
200 Installing CIS Audit packages202 Installing CIS Audit packages
201 CIS Audit enabled203 CIS Audit enabled
202 Visit https://security-certs.docs.ubuntu.com/en/cis to learn how to use CIS204 Visit https://ubuntu.com/security/cis to learn how to use CIS
203 """205 """
204 When I run `apt-cache policy usg-cisbenchmark` as non-root206 When I run `apt-cache policy usg-cisbenchmark` as non-root
205 Then stdout does not match regexp:207 Then stdout does not match regexp:
@@ -208,7 +210,7 @@ Feature: Enable command behaviour when attached to an UA subscription
208 """210 """
209 And stdout matches regexp:211 And stdout matches regexp:
210 """212 """
211 \s* 500 https://esm.ubuntu.com/cis/ubuntu <release>/main amd64 Packages213 \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
212 """214 """
213 When I run `apt-cache policy usg-common` as non-root215 When I run `apt-cache policy usg-common` as non-root
214 Then stdout does not match regexp:216 Then stdout does not match regexp:
@@ -217,7 +219,7 @@ Feature: Enable command behaviour when attached to an UA subscription
217 """219 """
218 And stdout matches regexp:220 And stdout matches regexp:
219 """221 """
220 \s* 500 https://esm.ubuntu.com/cis/ubuntu <release>/main amd64 Packages222 \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
221 """223 """
222 When I verify that running `ua enable cis` `with sudo` exits `1`224 When I verify that running `ua enable cis` `with sudo` exits `1`
223 Then stdout matches regexp225 Then stdout matches regexp
@@ -256,29 +258,171 @@ Feature: Enable command behaviour when attached to an UA subscription
256 CIS audit scan completed258 CIS audit scan completed
257 """259 """
258260
259 Examples: not entitled services261 Examples: cis script
260 | release | cis_script |262 | release | cis_script |
261 | focal | Canonical_Ubuntu_20.04_CIS-harden.sh |
262 | bionic | Canonical_Ubuntu_18.04_CIS-harden.sh |263 | bionic | Canonical_Ubuntu_18.04_CIS-harden.sh |
263 | xenial | Canonical_Ubuntu_16.04_CIS_v1.1.0-harden.sh |264 | xenial | Canonical_Ubuntu_16.04_CIS_v1.1.0-harden.sh |
264265
265 @series.focal266 @series.focal
266 @uses.config.machine_type.lxd.vm267 @uses.config.machine_type.lxd.container
267 Scenario: Attached enable of vm-based services in a focal lxd vm268 Scenario Outline: Attached enable of cis service in a ubuntu machine
268 Given a `focal` machine with ubuntu-advantage-tools installed269 Given a `<release>` machine with ubuntu-advantage-tools installed
269 When I attach `contract_token` with sudo270 When I attach `contract_token_staging` with sudo
270 Then I verify that running `ua enable fips --assume-yes` `with sudo` exits `1`271 And I verify that running `ua enable cis` `with sudo` exits `0`
271 And I will see the following on stdout:272 Then I will see the following on stdout:
272 """273 """
273 One moment, checking your subscription first274 One moment, checking your subscription first
274 FIPS is not available for Ubuntu 20.04 LTS (Focal Fossa).275 From Ubuntu 20.04 and onwards 'ua enable cis' has been
276 replaced by 'ua enable usg'. See more information at:
277 https://ubuntu.com/security/certifications/docs/usg
278 Updating package lists
279 Installing CIS Audit packages
280 CIS Audit enabled
281 Visit https://ubuntu.com/security/cis to learn how to use CIS
275 """282 """
276 And I verify that running `ua enable fips-updates --assume-yes` `with sudo` exits `1`283 When I run `apt-cache policy usg-cisbenchmark` as non-root
277 And I will see the following on stdout:284 Then stdout does not match regexp:
285 """
286 .*Installed: \(none\)
287 """
288 And stdout matches regexp:
289 """
290 \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
291 """
292 When I run `apt-cache policy usg-common` as non-root
293 Then stdout does not match regexp:
294 """
295 .*Installed: \(none\)
296 """
297 And stdout matches regexp:
298 """
299 \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
300 """
301 When I verify that running `ua enable cis` `with sudo` exits `1`
302 Then stdout matches regexp
303 """
304 One moment, checking your subscription first
305 From Ubuntu 20.04 and onwards 'ua enable cis' has been
306 replaced by 'ua enable usg'. See more information at:
307 https://ubuntu.com/security/certifications/docs/usg
308 CIS Audit is already enabled.
309 See: sudo ua status
310 """
311 When I run `cis-audit level1_server` with sudo
312 Then stdout matches regexp
313 """
314 Title.*Ensure no duplicate UIDs exist
315 Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
316 Result.*pass
317 """
318 And stdout matches regexp:
319 """
320 Title.*Ensure default user umask is 027 or more restrictive
321 Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
322 Result.*fail
323 """
324 And stdout matches regexp
325 """
326 CIS audit scan completed
327 """
328 When I verify that running `/usr/share/ubuntu-scap-security-guides/cis-hardening/<cis_script> lvl1_server` `with sudo` exits `0`
329 And I run `cis-audit level1_server` with sudo
330 Then stdout matches regexp:
331 """
332 Title.*Ensure default user umask is 027 or more restrictive
333 Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
334 Result.*pass
335 """
336 And stdout matches regexp
337 """
338 CIS audit scan completed
339 """
340
341 Examples: cis script
342 | release | cis_script |
343 | focal | Canonical_Ubuntu_20.04_CIS-harden.sh |
344
345 @series.bionic
346 @series.xenial
347 @uses.config.machine_type.lxd.container
348 Scenario Outline: Attached enable of usg service in a ubuntu machine
349 Given a `<release>` machine with ubuntu-advantage-tools installed
350 When I attach `contract_token_staging` with sudo
351 And I verify that running `ua enable usg` `with sudo` exits `1`
352 Then I will see the following on stdout:
278 """353 """
279 One moment, checking your subscription first354 One moment, checking your subscription first
280 FIPS Updates is not available for Ubuntu 20.04 LTS (Focal Fossa).
281 """355 """
356 Then I will see the following on stderr:
357 """
358 Cannot enable unknown service 'usg'.
359 Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch.
360 """
361
362 Examples: cis service
363 | release |
364 | bionic |
365 | xenial |
366
367 @series.focal
368 @uses.config.machine_type.lxd.container
369 Scenario Outline: Attached enable of usg service in a focal machine
370 Given a `<release>` machine with ubuntu-advantage-tools installed
371 When I attach `contract_token_staging` with sudo
372 And I run `ua enable usg` with sudo
373 Then I will see the following on stdout:
374 """
375 One moment, checking your subscription first
376 Updating package lists
377 Ubuntu Security Guide enabled
378 Visit https://ubuntu.com/security/certifications/docs/usg for the next steps
379 """
380 When I run `ua status` with sudo
381 Then stdout matches regexp:
382 """
383 usg +yes +enabled +Security compliance and audit tools
384 """
385 When I run `ua disable usg` with sudo
386 Then stdout matches regexp:
387 """
388 Updating package lists
389 """
390 When I run `ua status` with sudo
391 Then stdout matches regexp:
392 """
393 usg +yes +disabled +Security compliance and audit tools
394 """
395 When I run `ua enable cis` with sudo
396 Then I will see the following on stdout:
397 """
398 One moment, checking your subscription first
399 From Ubuntu 20.04 and onwards 'ua enable cis' has been
400 replaced by 'ua enable usg'. See more information at:
401 https://ubuntu.com/security/certifications/docs/usg
402 Updating package lists
403 Installing CIS Audit packages
404 CIS Audit enabled
405 Visit https://ubuntu.com/security/cis to learn how to use CIS
406 """
407 When I run `ua status` with sudo
408 Then stdout matches regexp:
409 """
410 usg +yes +enabled +Security compliance and audit tools
411 """
412 When I run `ua disable usg` with sudo
413 Then stdout matches regexp:
414 """
415 Updating package lists
416 """
417 When I run `ua status` with sudo
418 Then stdout matches regexp:
419 """
420 usg +yes +disabled +Security compliance and audit tools
421 """
422
423 Examples: cis service
424 | release |
425 | focal |
282426
283 @series.bionic427 @series.bionic
284 @series.xenial428 @series.xenial
@@ -289,7 +433,6 @@ Feature: Enable command behaviour when attached to an UA subscription
289 And I run `ua status` with sudo433 And I run `ua status` with sudo
290 Then stdout matches regexp:434 Then stdout matches regexp:
291 """435 """
292 cis +yes +disabled +Center for Internet Security Audit Tools
293 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)436 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
294 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)437 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
295 fips +yes +disabled +NIST-certified core packages438 fips +yes +disabled +NIST-certified core packages
@@ -306,7 +449,6 @@ Feature: Enable command behaviour when attached to an UA subscription
306 When I run `ua status` with sudo449 When I run `ua status` with sudo
307 Then stdout matches regexp:450 Then stdout matches regexp:
308 """451 """
309 cis +yes +disabled +Center for Internet Security Audit Tools
310 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)452 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
311 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)453 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
312 fips +yes +disabled +NIST-certified core packages454 fips +yes +disabled +NIST-certified core packages
diff --git a/features/attached_status.feature b/features/attached_status.feature
index db826a7..78483ed 100644
--- a/features/attached_status.feature
+++ b/features/attached_status.feature
@@ -11,14 +11,14 @@ Feature: Attached status
11 """11 """
12 _doc _schema_version account attached config config_path contract effective12 _doc _schema_version account attached config config_path contract effective
13 environment_vars execution_details execution_status expires machine_id notices13 environment_vars execution_details execution_status expires machine_id notices
14 services version14 services version simulated
15 """15 """
16 When I run `ua status --format yaml` as non-root16 When I run `ua status --format yaml` as non-root
17 Then stdout is formatted as `yaml` and has keys:17 Then stdout is formatted as `yaml` and has keys:
18 """18 """
19 _doc _schema_version account attached config config_path contract effective19 _doc _schema_version account attached config config_path contract effective
20 environment_vars execution_details execution_status expires machine_id notices20 environment_vars execution_details execution_status expires machine_id notices
21 services version21 services version simulated
22 """22 """
2323
24 Examples: ubuntu release24 Examples: ubuntu release
@@ -28,3 +28,4 @@ Feature: Attached status
28 | xenial |28 | xenial |
29 | hirsute |29 | hirsute |
30 | impish |30 | impish |
31 | jammy |
diff --git a/features/install_uninstall.feature b/features/install_uninstall.feature
index 586f2ef..66426c8 100644
--- a/features/install_uninstall.feature
+++ b/features/install_uninstall.feature
@@ -14,6 +14,7 @@ Feature: UA Install and Uninstall related tests
14 | focal |14 | focal |
15 | hirsute |15 | hirsute |
16 | impish |16 | impish |
17 | jammy |
1718
18 @series.lts19 @series.lts
19 @uses.config.contract_token20 @uses.config.contract_token
diff --git a/features/license_check.feature b/features/license_check.feature
index 37c5bd0..4f41279 100644
--- a/features/license_check.feature
+++ b/features/license_check.feature
@@ -92,6 +92,7 @@ Feature: License check timer only runs in environments where necessary
92 | focal |92 | focal |
93 | hirsute |93 | hirsute |
94 | impish |94 | impish |
95 | jammy |
9596
96 @series.lts97 @series.lts
97 @uses.config.machine_type.aws.pro98 @uses.config.machine_type.aws.pro
diff --git a/features/steps/steps.py b/features/steps/steps.py
index e69595b..2c0d5bf 100644
--- a/features/steps/steps.py
+++ b/features/steps/steps.py
@@ -229,6 +229,20 @@ def when_i_run_command_with_stdin(
229 )229 )
230230
231231
232@when("I do a preflight check for `{contract_token}` {user_spec}")
233def when_i_preflight(context, contract_token, user_spec):
234 token = getattr(context.config, contract_token)
235 command = "ua status --simulate-with-token {}".format(token)
236 if user_spec == "with the all flag":
237 command += " --all"
238 if "formatted as" in user_spec:
239 output_format = user_spec.split()[2]
240 command += " --format {}".format(output_format)
241 when_i_run_command(
242 context=context, command=command, user_spec="as non-root"
243 )
244
245
232@when("I run `{command}` {user_spec}")246@when("I run `{command}` {user_spec}")
233def when_i_run_command(247def when_i_run_command(
234 context,248 context,
diff --git a/features/ubuntu_pro.feature b/features/ubuntu_pro.feature
index fca719a..cd95c86 100644
--- a/features/ubuntu_pro.feature
+++ b/features/ubuntu_pro.feature
@@ -27,13 +27,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
27 """27 """
28 SERVICE ENTITLED STATUS DESCRIPTION28 SERVICE ENTITLED STATUS DESCRIPTION
29 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages29 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
30 cis +yes +<cis-s> +Center for Internet Security Audit Tools30 """
31 Then stdout matches regexp:
32 """
31 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)33 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
32 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)34 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
33 fips +yes +<fips-s> +NIST-certified core packages35 fips +yes +<fips-s> +NIST-certified core packages
34 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates36 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
35 livepatch +yes +enabled +Canonical Livepatch service37 livepatch +yes +enabled +Canonical Livepatch service
36 """38 """
39 Then stdout matches regexp:
40 """
41 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
42 """
37 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine43 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
38 Then stdout matches regexp:44 Then stdout matches regexp:
39 """45 """
@@ -44,10 +50,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
44 .*CONNECT 169.254.169.254.*50 .*CONNECT 169.254.169.254.*
45 """51 """
46 Examples: ubuntu release52 Examples: ubuntu release
47 | release | fips-s | cc-eal-s | cis-s |53 | release | fips-s | cc-eal-s | cis-s | cis_or_usg |
48 | xenial | disabled | disabled | disabled |54 | xenial | disabled | disabled | disabled | cis |
49 | bionic | disabled | n/a | disabled |55 | bionic | disabled | n/a | disabled | cis |
50 | focal | n/a | n/a | disabled |56 | focal | n/a | n/a | disabled | usg |
5157
52 @series.lts58 @series.lts
53 @uses.config.machine_type.azure.pro59 @uses.config.machine_type.azure.pro
@@ -76,13 +82,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
76 """82 """
77 SERVICE ENTITLED STATUS DESCRIPTION83 SERVICE ENTITLED STATUS DESCRIPTION
78 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages84 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
79 cis +yes +<cis-s> +Center for Internet Security Audit Tools85 """
86 Then stdout matches regexp:
87 """
80 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)88 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
81 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)89 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
82 fips +yes +<fips-s> +NIST-certified core packages90 fips +yes +<fips-s> +NIST-certified core packages
83 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates91 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
84 livepatch +yes +<livepatch-s> +Canonical Livepatch service92 livepatch +yes +<livepatch-s> +Canonical Livepatch service
85 """93 """
94 Then stdout matches regexp:
95 """
96 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
97 """
86 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine98 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
87 Then stdout matches regexp:99 Then stdout matches regexp:
88 """100 """
@@ -93,10 +105,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
93 .*CONNECT 169.254.169.254.*105 .*CONNECT 169.254.169.254.*
94 """106 """
95 Examples: ubuntu release107 Examples: ubuntu release
96 | release | fips-s | cc-eal-s | cis-s | livepatch-s |108 | release | fips-s | cc-eal-s | cis-s | livepatch-s | cis_or_usg |
97 | xenial | n/a | disabled | disabled | enabled |109 | xenial | n/a | disabled | disabled | enabled | cis |
98 | bionic | disabled | n/a | disabled | n/a |110 | bionic | disabled | n/a | disabled | n/a | cis |
99 | focal | n/a | n/a | disabled | enabled |111 | focal | n/a | n/a | disabled | enabled | usg |
100112
101 @series.lts113 @series.lts
102 @uses.config.machine_type.gcp.pro114 @uses.config.machine_type.gcp.pro
@@ -125,13 +137,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
125 """137 """
126 SERVICE ENTITLED STATUS DESCRIPTION138 SERVICE ENTITLED STATUS DESCRIPTION
127 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages139 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
128 cis +yes +<cis-s> +Center for Internet Security Audit Tools140 """
141 Then stdout matches regexp:
142 """
129 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)143 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
130 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)144 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
131 fips +yes +<fips-s> +NIST-certified core packages145 fips +yes +<fips-s> +NIST-certified core packages
132 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates146 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
133 livepatch +yes +<livepatch-s> +Canonical Livepatch service147 livepatch +yes +<livepatch-s> +Canonical Livepatch service
134 """148 """
149 Then stdout matches regexp:
150 """
151 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
152 """
135 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine153 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
136 Then stdout matches regexp:154 Then stdout matches regexp:
137 """155 """
@@ -142,10 +160,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
142 .*CONNECT metadata.*160 .*CONNECT metadata.*
143 """161 """
144 Examples: ubuntu release162 Examples: ubuntu release
145 | release | fips-s | cc-eal-s | cis-s | livepatch-s |163 | release | fips-s | cc-eal-s | cis-s | livepatch-s | cis_or_usg |
146 | xenial | n/a | disabled | disabled | n/a |164 | xenial | n/a | disabled | disabled | n/a | cis |
147 | bionic | disabled | n/a | disabled | n/a |165 | bionic | disabled | n/a | disabled | n/a | cis |
148 | focal | n/a | n/a | disabled | enabled |166 | focal | n/a | n/a | disabled | enabled | usg |
149167
150 @series.lts168 @series.lts
151 @uses.config.machine_type.aws.pro169 @uses.config.machine_type.aws.pro
@@ -165,27 +183,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
165 """183 """
166 SERVICE ENTITLED STATUS DESCRIPTION184 SERVICE ENTITLED STATUS DESCRIPTION
167 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages185 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
168 cis +yes +<cis-s> +Center for Internet Security Audit Tools186 """
187 Then stdout matches regexp:
188 """
169 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)189 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
170 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)190 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
171 fips +yes +<fips-s> +NIST-certified core packages191 fips +yes +<fips-s> +NIST-certified core packages
172 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates192 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
173 livepatch +yes +enabled +Canonical Livepatch service193 livepatch +yes +enabled +Canonical Livepatch service
174 """194 """
195 Then stdout matches regexp:
196 """
197 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
198 """
175 When I run `ua status --all` as non-root199 When I run `ua status --all` as non-root
176 Then stdout matches regexp:200 Then stdout matches regexp:
177 """201 """
178 SERVICE ENTITLED STATUS DESCRIPTION202 SERVICE ENTITLED STATUS DESCRIPTION
179 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages203 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
180 cis +yes +<cis-s> +Center for Internet Security Audit Tools204 """
205 Then stdout matches regexp:
206 """
181 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)207 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
182 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)208 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
183 fips +yes +<fips-s> +NIST-certified core packages209 fips +yes +<fips-s> +NIST-certified core packages
184 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates210 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
185 livepatch +yes +enabled +Canonical Livepatch service211 livepatch +yes +enabled +Canonical Livepatch service
186 ros +no +(-|—) +Security Updates for the Robot Operating System
187 ros-updates +no +(-|—) +All Updates for the Robot Operating System
188 """212 """
213 Then stdout matches regexp:
214 """
215 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
216 """
217 When I run `systemctl start ua-auto-attach.service` with sudo
218 And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
219 Then stdout matches regexp:
220 """
221 .*status=0\/SUCCESS.*
222 """
223 And stdout matches regexp:
224 """
225 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
226 """
227 When I run `ua auto-attach` with sudo
228 Then stderr matches regexp:
229 """
230 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
231 """
189 When I run `apt-cache policy` with sudo232 When I run `apt-cache policy` with sudo
190 Then apt-cache policy for the following url has permission `500`233 Then apt-cache policy for the following url has permission `500`
191 """234 """
@@ -235,10 +278,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
235 """278 """
236279
237 Examples: ubuntu release280 Examples: ubuntu release
238 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg |281 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | cis_or_usg |
239 | xenial | disabled | disabled | disabled | libkrad0 | jq |282 | xenial | disabled | disabled | disabled | libkrad0 | jq | cis |
240 | bionic | disabled | n/a | disabled | libkrad0 | bundler |283 | bionic | disabled | n/a | disabled | libkrad0 | bundler | cis |
241 | focal | n/a | n/a | disabled | hello | ant |284 | focal | n/a | n/a | disabled | hello | ant | usg |
242285
243 @series.lts286 @series.lts
244 @uses.config.machine_type.azure.pro287 @uses.config.machine_type.azure.pro
@@ -258,27 +301,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
258 """301 """
259 SERVICE ENTITLED STATUS DESCRIPTION302 SERVICE ENTITLED STATUS DESCRIPTION
260 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages303 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
261 cis +yes +<cis-s> +Center for Internet Security Audit Tools304 """
305 Then stdout matches regexp:
306 """
262 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)307 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
263 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)308 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
264 fips +yes +<fips-s> +NIST-certified core packages309 fips +yes +<fips-s> +NIST-certified core packages
265 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates310 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
266 livepatch +yes +<livepatch> +Canonical Livepatch service311 livepatch +yes +<livepatch> +Canonical Livepatch service
267 """312 """
313 Then stdout matches regexp:
314 """
315 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
316 """
268 When I run `ua status --all` as non-root317 When I run `ua status --all` as non-root
269 Then stdout matches regexp:318 Then stdout matches regexp:
270 """319 """
271 SERVICE ENTITLED STATUS DESCRIPTION320 SERVICE ENTITLED STATUS DESCRIPTION
272 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages321 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
273 cis +yes +<cis-s> +Center for Internet Security Audit Tools322 """
323 Then stdout matches regexp:
324 """
274 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)325 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
275 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)326 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
276 fips +yes +<fips-s> +NIST-certified core packages327 fips +yes +<fips-s> +NIST-certified core packages
277 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates328 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
278 livepatch +yes +<livepatch> +Canonical Livepatch service329 livepatch +yes +<livepatch> +Canonical Livepatch service
279 ros +no +(-|—) +Security Updates for the Robot Operating System
280 ros-updates +no +(-|—) +All Updates for the Robot Operating System
281 """330 """
331 Then stdout matches regexp:
332 """
333 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
334 """
335 When I run `systemctl start ua-auto-attach.service` with sudo
336 And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
337 Then stdout matches regexp:
338 """
339 .*status=0\/SUCCESS.*
340 """
341 And stdout matches regexp:
342 """
343 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
344 """
345 When I run `ua auto-attach` with sudo
346 Then stderr matches regexp:
347 """
348 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
349 """
282 When I run `apt-cache policy` with sudo350 When I run `apt-cache policy` with sudo
283 Then apt-cache policy for the following url has permission `500`351 Then apt-cache policy for the following url has permission `500`
284 """352 """
@@ -328,10 +396,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
328 """396 """
329397
330 Examples: ubuntu release398 Examples: ubuntu release
331 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch |399 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch | cis_or_usg |
332 | xenial | n/a | disabled | disabled | libkrad0 | jq | enabled |400 | xenial | n/a | disabled | disabled | libkrad0 | jq | enabled | cis |
333 | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a |401 | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a | cis |
334 | focal | n/a | n/a | disabled | hello | ant | enabled |402 | focal | n/a | n/a | disabled | hello | ant | enabled | usg |
335403
336 @series.lts404 @series.lts
337 @uses.config.machine_type.gcp.pro405 @uses.config.machine_type.gcp.pro
@@ -351,27 +419,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
351 """419 """
352 SERVICE ENTITLED STATUS DESCRIPTION420 SERVICE ENTITLED STATUS DESCRIPTION
353 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages421 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
354 cis +yes +<cis-s> +Center for Internet Security Audit Tools422 """
423 Then stdout matches regexp:
424 """
355 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)425 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
356 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)426 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
357 fips +yes +<fips-s> +NIST-certified core packages427 fips +yes +<fips-s> +NIST-certified core packages
358 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates428 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
359 livepatch +yes +<livepatch> +Canonical Livepatch service429 livepatch +yes +<livepatch> +Canonical Livepatch service
360 """430 """
431 Then stdout matches regexp:
432 """
433 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
434 """
361 When I run `ua status --all` as non-root435 When I run `ua status --all` as non-root
362 Then stdout matches regexp:436 Then stdout matches regexp:
363 """437 """
364 SERVICE ENTITLED STATUS DESCRIPTION438 SERVICE ENTITLED STATUS DESCRIPTION
365 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages439 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
366 cis +yes +<cis-s> +Center for Internet Security Audit Tools440 """
441 Then stdout matches regexp:
442 """
367 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)443 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
368 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)444 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
369 fips +yes +<fips-s> +NIST-certified core packages445 fips +yes +<fips-s> +NIST-certified core packages
370 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates446 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
371 livepatch +yes +<livepatch> +Canonical Livepatch service447 livepatch +yes +<livepatch> +Canonical Livepatch service
372 ros +no +(-|—) +Security Updates for the Robot Operating System
373 ros-updates +no +(-|—) +All Updates for the Robot Operating System
374 """448 """
449 Then stdout matches regexp:
450 """
451 <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
452 """
453 When I run `systemctl start ua-auto-attach.service` with sudo
454 And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
455 Then stdout matches regexp:
456 """
457 .*status=0\/SUCCESS.*
458 """
459 And stdout matches regexp:
460 """
461 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
462 """
463 When I run `ua auto-attach` with sudo
464 Then stderr matches regexp:
465 """
466 Skipping attach: Instance '[0-9a-z\-]+' is already attached.
467 """
375 When I run `apt-cache policy` with sudo468 When I run `apt-cache policy` with sudo
376 Then apt-cache policy for the following url has permission `500`469 Then apt-cache policy for the following url has permission `500`
377 """470 """
@@ -421,7 +514,7 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
421 """514 """
422515
423 Examples: ubuntu release516 Examples: ubuntu release
424 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch |517 | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch | cis_or_usg |
425 | xenial | n/a | disabled | disabled | libkrad0 | jq | n/a |518 | xenial | n/a | disabled | disabled | libkrad0 | jq | n/a | cis |
426 | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a |519 | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a | cis |
427 | focal | n/a | n/a | disabled | hello | ant | enabled |520 | focal | n/a | n/a | disabled | hello | ant | enabled | usg |
diff --git a/features/ubuntu_upgrade.feature b/features/ubuntu_upgrade.feature
index d565d77..d987975 100644
--- a/features/ubuntu_upgrade.feature
+++ b/features/ubuntu_upgrade.feature
@@ -162,3 +162,44 @@ Feature: Upgrade between releases when uaclient is attached
162 | release | next_release | fips-service | fips-name | source-file |162 | release | next_release | fips-service | fips-name | source-file |
163 | xenial | bionic | fips | FIPS | ubuntu-fips |163 | xenial | bionic | fips | FIPS | ubuntu-fips |
164 | xenial | bionic | fips-updates | FIPS Updates | ubuntu-fips-updates |164 | xenial | bionic | fips-updates | FIPS Updates | ubuntu-fips-updates |
165
166 @slow
167 @series.bionic
168 @uses.config.machine_type.lxd.container
169 @upgrade
170 Scenario Outline: Attached upgrade with cis enabled across LTS releases
171 Given a `<release>` machine with ubuntu-advantage-tools installed
172 When I attach `contract_token_staging` with sudo
173 And I run `ua enable cis` with sudo
174 # update-manager-core requires ua < 28. Our tests that build the package will
175 # generate ua with version 28. We are removing that package here to make sure
176 # do-release-upgrade will be able to run
177 And I run `apt remove update-manager-core -y` with sudo
178 And I run `apt-get dist-upgrade --assume-yes` with sudo
179 # Some packages upgrade may require a reboot
180 And I reboot the `<release>` machine
181 And I create the file `/etc/update-manager/release-upgrades.d/ua-test.cfg` with the following
182 """
183 [Sources]
184 AllowThirdParty=yes
185 """
186 Then I verify that running `do-release-upgrade --frontend DistUpgradeViewNonInteractive` `with sudo` exits `0`
187 When I reboot the `<release>` machine
188 And I run `lsb_release -cs` as non-root
189 Then I will see the following on stdout:
190 """
191 <next_release>
192 """
193 And I verify that running `egrep "<release>|disabled" /etc/apt/sources.list.d/*` `as non-root` exits `2`
194 And I will see the following on stdout:
195 """
196 """
197 When I run `ua status` with sudo
198 Then stdout matches regexp:
199 """
200 usg +yes +enabled
201 """
202
203 Examples: ubuntu release
204 | release | next_release |
205 | bionic | focal |
diff --git a/features/ubuntu_upgrade_unattached.feature b/features/ubuntu_upgrade_unattached.feature
index 9db750f..5195d2b 100644
--- a/features/ubuntu_upgrade_unattached.feature
+++ b/features/ubuntu_upgrade_unattached.feature
@@ -73,7 +73,7 @@ Feature: Upgrade between releases when uaclient is unattached
73 When I run `ua status` with sudo73 When I run `ua status` with sudo
74 Then stdout matches regexp:74 Then stdout matches regexp:
75 """75 """
76 cis yes +Center for Internet Security Audit Tools76 cis yes +Security compliance and audit tools
77 esm-infra yes +UA Infra: Extended Security Maintenance \(ESM\)77 esm-infra yes +UA Infra: Extended Security Maintenance \(ESM\)
78 """78 """
79 When I attach `contract_token` with sudo79 When I attach `contract_token` with sudo
diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature
index 77a90e0..bda7597 100644
--- a/features/unattached_commands.feature
+++ b/features/unattached_commands.feature
@@ -29,6 +29,7 @@ Feature: Command behaviour when unattached
29 | xenial |29 | xenial |
30 | hirsute |30 | hirsute |
31 | impish |31 | impish |
32 | jammy |
3233
33 @series.xenial34 @series.xenial
34 @uses.config.machine_type.lxd.container35 @uses.config.machine_type.lxd.container
@@ -134,6 +135,8 @@ Feature: Command behaviour when unattached
134 | hirsute | refresh |135 | hirsute | refresh |
135 | impish | detach |136 | impish | detach |
136 | impish | refresh |137 | impish | refresh |
138 | jammy | detach |
139 | jammy | refresh |
137140
138 @series.all141 @series.all
139 @uses.config.machine_type.lxd.container142 @uses.config.machine_type.lxd.container
@@ -174,6 +177,10 @@ Feature: Command behaviour when unattached
174 | impish | disable | livepatch |177 | impish | disable | livepatch |
175 | impish | enable | unknown |178 | impish | enable | unknown |
176 | impish | disable | unknown |179 | impish | disable | unknown |
180 | jammy | enable | livepatch |
181 | jammy | disable | livepatch |
182 | jammy | enable | unknown |
183 | jammy | disable | unknown |
177184
178 @series.all185 @series.all
179 @uses.config.machine_type.lxd.container186 @uses.config.machine_type.lxd.container
@@ -215,6 +222,7 @@ Feature: Command behaviour when unattached
215 | xenial | yes |222 | xenial | yes |
216 | hirsute | no |223 | hirsute | no |
217 | impish | no |224 | impish | no |
225 | jammy | no |
218226
219227
220 @series.all228 @series.all
@@ -223,18 +231,18 @@ Feature: Command behaviour when unattached
223 Given a `<release>` machine with ubuntu-advantage-tools installed231 Given a `<release>` machine with ubuntu-advantage-tools installed
224 When I run `apt remove ca-certificates -y` with sudo232 When I run `apt remove ca-certificates -y` with sudo
225 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`233 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`
226 Then I will see the following on stderr:234 Then stderr matches regexp:
227 """235 """
228 Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json236 Failed to access URL: https://.*
229 Cannot verify certificate of server237 Cannot verify certificate of server
230 Please install "ca-certificates" and try again.238 Please install "ca-certificates" and try again.
231 """239 """
232 When I run `apt install ca-certificates -y` with sudo240 When I run `apt install ca-certificates -y` with sudo
233 When I run `mv /etc/ssl/certs /etc/ssl/wronglocation` with sudo241 When I run `mv /etc/ssl/certs /etc/ssl/wronglocation` with sudo
234 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`242 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`
235 Then I will see the following on stderr:243 Then stderr matches regexp:
236 """244 """
237 Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json245 Failed to access URL: https://.*
238 Cannot verify certificate of server246 Cannot verify certificate of server
239 Please check your openssl configuration.247 Please check your openssl configuration.
240 """248 """
@@ -245,6 +253,7 @@ Feature: Command behaviour when unattached
245 | focal |253 | focal |
246 | hirsute |254 | hirsute |
247 | impish |255 | impish |
256 | jammy |
248257
249 @series.focal258 @series.focal
250 @uses.config.machine_type.lxd.container259 @uses.config.machine_type.lxd.container
@@ -503,6 +512,7 @@ Feature: Command behaviour when unattached
503 When I run `ls -1 logs/` as non-root512 When I run `ls -1 logs/` as non-root
504 Then stdout matches regexp:513 Then stdout matches regexp:
505 """514 """
515 build.info
506 cloud-id.txt516 cloud-id.txt
507 jobs-status.json517 jobs-status.json
508 journalctl.txt518 journalctl.txt
@@ -527,3 +537,4 @@ Feature: Command behaviour when unattached
527 | focal |537 | focal |
528 | hirsute |538 | hirsute |
529 | impish |539 | impish |
540 | jammy |
diff --git a/features/unattached_status.feature b/features/unattached_status.feature
index 5d26e27..7284afb 100644
--- a/features/unattached_status.feature
+++ b/features/unattached_status.feature
@@ -9,14 +9,14 @@ Feature: Unattached status
9 """9 """
10 _doc _schema_version account attached config config_path contract effective10 _doc _schema_version account attached config config_path contract effective
11 environment_vars execution_details execution_status expires machine_id notices11 environment_vars execution_details execution_status expires machine_id notices
12 services version12 services version simulated
13 """13 """
14 When I run `ua status --format yaml` as non-root14 When I run `ua status --format yaml` as non-root
15 Then stdout is formatted as `yaml` and has keys:15 Then stdout is formatted as `yaml` and has keys:
16 """16 """
17 _doc _schema_version account attached config config_path contract effective17 _doc _schema_version account attached config config_path contract effective
18 environment_vars execution_details execution_status expires machine_id notices18 environment_vars execution_details execution_status expires machine_id notices
19 services version19 services version simulated
20 """20 """
2121
22 Examples: ubuntu release22 Examples: ubuntu release
@@ -26,21 +26,24 @@ Feature: Unattached status
26 | xenial |26 | xenial |
27 | hirsute |27 | hirsute |
28 | impish |28 | impish |
29 | jammy |
2930
30 @series.all31 @series.all
31 @uses.config.machine_type.lxd.container32 @uses.config.machine_type.lxd.container
32 Scenario Outline: Unattached status in a ubuntu machine33 Scenario Outline: Unattached status in a ubuntu machine
33 Given a `<release>` machine with ubuntu-advantage-tools installed34 Given a `<release>` machine with ubuntu-advantage-tools installed
35 When I run `sed -i 's/contracts.can/contracts.staging.can/' /etc/ubuntu-advantage/uaclient.conf` with sudo
34 When I run `ua status` as non-root36 When I run `ua status` as non-root
35 Then stdout matches regexp:37 Then stdout matches regexp:
36 """38 """
37 SERVICE AVAILABLE DESCRIPTION39 SERVICE AVAILABLE DESCRIPTION
38 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages40 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
39 cis <cis> +Center for Internet Security Audit Tools41 ?<cis>( +<cis-available> +Security compliance and audit tools)?
40 esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)42 ?esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
41 fips <fips> +NIST-certified core packages43 fips <fips> +NIST-certified core packages
42 fips-updates <fips> +NIST-certified core packages with priority security updates44 fips-updates <fips> +NIST-certified core packages with priority security updates
43 livepatch <livepatch> +Canonical Livepatch service45 livepatch <livepatch> +Canonical Livepatch service
46 ?<usg>( +<cis-available> +Security compliance and audit tools)?
4447
45 This machine is not attached to a UA subscription.48 This machine is not attached to a UA subscription.
46 See https://ubuntu.com/advantage49 See https://ubuntu.com/advantage
@@ -50,14 +53,15 @@ Feature: Unattached status
50 """53 """
51 SERVICE AVAILABLE DESCRIPTION54 SERVICE AVAILABLE DESCRIPTION
52 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages55 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
53 cis <cis> +Center for Internet Security Audit Tools56 ?<cis>( +<cis-available> +Security compliance and audit tools)?
54 esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)57 ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
55 esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)58 esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
56 fips <fips> +NIST-certified core packages59 fips <fips> +NIST-certified core packages
57 fips-updates <fips> +NIST-certified core packages with priority security updates60 fips-updates <fips> +NIST-certified core packages with priority security updates
58 livepatch <livepatch> +Canonical Livepatch service61 livepatch <livepatch> +Canonical Livepatch service
59 ros <ros> +Security Updates for the Robot Operating System62 ros <ros> +Security Updates for the Robot Operating System
60 ros-updates <ros> +All Updates for the Robot Operating System63 ros-updates <ros> +All Updates for the Robot Operating System
64 ?<usg>( +<cis-available> +Security compliance and audit tools)?
6165
62 This machine is not attached to a UA subscription.66 This machine is not attached to a UA subscription.
63 See https://ubuntu.com/advantage67 See https://ubuntu.com/advantage
@@ -67,11 +71,12 @@ Feature: Unattached status
67 """71 """
68 SERVICE AVAILABLE DESCRIPTION72 SERVICE AVAILABLE DESCRIPTION
69 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages73 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
70 cis <cis> +Center for Internet Security Audit Tools74 ?<cis>( +<cis-available> +Security compliance and audit tools)?
71 esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)75 ?esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
72 fips <fips> +NIST-certified core packages76 fips <fips> +NIST-certified core packages
73 fips-updates <fips> +NIST-certified core packages with priority security updates77 fips-updates <fips> +NIST-certified core packages with priority security updates
74 livepatch <livepatch> +Canonical Livepatch service78 livepatch <livepatch> +Canonical Livepatch service
79 ?<usg>( +<cis-available> +Security compliance and audit tools)?
7580
76 This machine is not attached to a UA subscription.81 This machine is not attached to a UA subscription.
77 See https://ubuntu.com/advantage82 See https://ubuntu.com/advantage
@@ -81,14 +86,15 @@ Feature: Unattached status
81 """86 """
82 SERVICE AVAILABLE DESCRIPTION87 SERVICE AVAILABLE DESCRIPTION
83 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages88 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
84 cis <cis> +Center for Internet Security Audit Tools89 ?<cis>( +<cis-available> +Security compliance and audit tools)?
85 esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)90 ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
86 esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)91 esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
87 fips <fips> +NIST-certified core packages92 fips <fips> +NIST-certified core packages
88 fips-updates <fips> +NIST-certified core packages with priority security updates93 fips-updates <fips> +NIST-certified core packages with priority security updates
89 livepatch <livepatch> +Canonical Livepatch service94 livepatch <livepatch> +Canonical Livepatch service
90 ros <ros> +Security Updates for the Robot Operating System95 ros <ros> +Security Updates for the Robot Operating System
91 ros-updates <ros> +All Updates for the Robot Operating System96 ros-updates <ros> +All Updates for the Robot Operating System
97 ?<usg>( +<cis-available> +Security compliance and audit tools)?
9298
93 This machine is not attached to a UA subscription.99 This machine is not attached to a UA subscription.
94 See https://ubuntu.com/advantage100 See https://ubuntu.com/advantage
@@ -103,23 +109,83 @@ Feature: Unattached status
103 """109 """
104 SERVICE AVAILABLE DESCRIPTION110 SERVICE AVAILABLE DESCRIPTION
105 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages111 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
106 cis <cis> +Center for Internet Security Audit Tools112 ?<cis>( +<cis-available> +Security compliance and audit tools)?
107 esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)113 ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
108 esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)114 esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
109 fips <fips> +NIST-certified core packages115 fips <fips> +NIST-certified core packages
110 fips-updates <fips> +NIST-certified core packages with priority security updates116 fips-updates <fips> +NIST-certified core packages with priority security updates
111 livepatch <livepatch> +Canonical Livepatch service117 livepatch <livepatch> +Canonical Livepatch service
112 ros <ros> +Security Updates for the Robot Operating System118 ros <ros> +Security Updates for the Robot Operating System
113 ros-updates <ros> +All Updates for the Robot Operating System119 ros-updates <ros> +All Updates for the Robot Operating System
120 ?<usg>( +<cis-available> +Security compliance and audit tools)?
114121
115 This machine is not attached to a UA subscription.122 This machine is not attached to a UA subscription.
116 See https://ubuntu.com/advantage123 See https://ubuntu.com/advantage
117 """ 124 """
118125
119 Examples: ubuntu release126 Examples: ubuntu release
120 | release | esm-apps | cc-eal | cis | fips | fips-update | infra | ros | livepatch |127 | release | esm-apps | cc-eal | cis | cis-available | fips | esm-infra | ros | livepatch | usg |
121 | xenial | yes | yes | yes | yes | yes | yes | yes | yes |128 | xenial | yes | yes | cis | yes | yes | yes | yes | yes | |
122 | bionic | yes | no | yes | yes | yes | yes | yes | yes |129 | bionic | yes | no | cis | yes | yes | yes | yes | yes | |
123 | focal | yes | no | yes | yes | yes | yes | no | yes |130 | focal | yes | no | | yes | yes | yes | no | yes | usg |
124 | hirsute | no | no | no | no | no | no | no | no |131 | hirsute | no | no | cis | yes | no | no | no | no | |
125 | impish | no | no | no | no | no | no | no | no |132 | impish | no | no | cis | yes | no | no | no | no | |
133 | jammy | no | no | cis | yes | no | no | no | no | |
134
135 @series.all
136 @uses.config.machine_type.lxd.container
137 @uses.config.contract_token
138 Scenario Outline: Simulate status in a ubuntu machine
139 Given a `<release>` machine with ubuntu-advantage-tools installed
140 When I run `sed -i 's/contracts.can/contracts.staging.can/' /etc/ubuntu-advantage/uaclient.conf` with sudo
141 When I do a preflight check for `contract_token_staging` without the all flag
142 Then stdout matches regexp:
143 """
144 SERVICE AVAILABLE ENTITLED AUTO_ENABLED DESCRIPTION
145 cc-eal <cc-eal> +yes +no +Common Criteria EAL2 Provisioning Packages
146 ?<cis>( +<cis-available> +yes +no +Security compliance and audit tools)?
147 ?esm-infra <esm-infra> +yes +yes +UA Infra: Extended Security Maintenance \(ESM\)
148 fips <fips> +yes +no +NIST-certified core packages
149 fips-updates <fips> +yes +no +NIST-certified core packages with priority security updates
150 livepatch <livepatch> +yes +yes +Canonical Livepatch service
151 ?<usg>( +<cis-available> +yes +no +Security compliance and audit tools)?
152 """
153 When I do a preflight check for `contract_token_staging` with the all flag
154 Then stdout matches regexp:
155 """
156 SERVICE AVAILABLE ENTITLED AUTO_ENABLED DESCRIPTION
157 cc-eal <cc-eal> +yes +no +Common Criteria EAL2 Provisioning Packages
158 ?<cis>( +<cis-available> +yes +no +Security compliance and audit tools)?
159 ?esm-apps <esm-apps> +yes +yes +UA Apps: Extended Security Maintenance \(ESM\)
160 esm-infra <esm-infra> +yes +yes +UA Infra: Extended Security Maintenance \(ESM\)
161 fips <fips> +yes +no +NIST-certified core packages
162 fips-updates <fips> +yes +no +NIST-certified core packages with priority security updates
163 livepatch <livepatch> +yes +yes +Canonical Livepatch service
164 ros <ros> +yes +no +Security Updates for the Robot Operating System
165 ros-updates <ros> +yes +no +All Updates for the Robot Operating System
166 ?<usg>( +<cis-available> +yes +no +Security compliance and audit tools)?
167 """
168 When I do a preflight check for `contract_token_staging` formatted as json
169 Then stdout is formatted as `json` and has keys:
170 """
171 _doc _schema_version account attached config config_path contract effective
172 environment_vars execution_details execution_status expires machine_id notices
173 services version simulated
174 """
175 When I do a preflight check for `contract_token_staging` formatted as yaml
176 Then stdout is formatted as `yaml` and has keys:
177 """
178 _doc _schema_version account attached config config_path contract effective
179 environment_vars execution_details execution_status expires machine_id notices
180 services version simulated
181 """
182
183
184 Examples: ubuntu release
185 | release | esm-apps | cc-eal | cis | cis-available | fips | esm-infra | ros | livepatch | usg |
186 | xenial | yes | yes | cis | yes | yes | yes | yes | yes | |
187 | bionic | yes | no | cis | yes | yes | yes | yes | yes | |
188 | focal | yes | no | | yes | yes | yes | no | yes | usg |
189 | hirsute | no | no | cis | yes | no | no | no | no | |
190 | impish | no | no | cis | yes | no | no | no | no | |
191 | jammy | no | no | cis | yes | no | no | no | no | |
diff --git a/help_data.yaml b/help_data.yaml
index 3c93645..d5d6486 100644
--- a/help_data.yaml
+++ b/help_data.yaml
@@ -7,17 +7,11 @@ cc-eal:
77
8cis:8cis:
9 help: |9 help: |
10 CIS benchmarks locks down your systems by removing non-secure programs,10 Ubuntu Security Guide is a tool for hardening and auditing and allows for
11 disabling unused filesystems, disabling unnecessary ports or services to11 environment-specific customizations. It enables compliance with profiles
12 prevent cyber attacks and malware, auditing privileged operations and12 such as DISA-STIG and the CIS benchmarks. Find out more at
13 restricting administrative privileges. The cis command installs13 https://ubuntu.com/security/certifications/docs/usg
14 tooling needed to automate audit and hardening according to a desired14
15 CIS profile - level 1 or level 2 for server or workstation on
16 Ubuntu 18.04 LTS or 16.04 LTS. The audit tooling uses OpenSCAP libraries
17 to do a scan of the system. The tool provides options to generate a
18 report in XML or a html format. The report shows compliance for all the
19 rules against the profile selected during the scan. You can find out
20 more at https://ubuntu.com/security/certifications#cis
2115
22esm-apps:16esm-apps:
23 help: |17 help: |
diff --git a/lib/reboot_cmds.py b/lib/reboot_cmds.py
index 7636f1b..569b46a 100644
--- a/lib/reboot_cmds.py
+++ b/lib/reboot_cmds.py
@@ -17,16 +17,17 @@ should run at next boot to process any pending/unresovled config operations.
17import logging17import logging
18import os18import os
19import sys19import sys
20import time
2120
22from uaclient import config, contract, entitlements, status21from uaclient import config, contract, lock, status
23from uaclient.cli import assert_lock_file, setup_logging22from uaclient.cli import setup_logging
23from uaclient.entitlements.fips import FIPSEntitlement
24from uaclient.exceptions import LockHeldError, UserFacingError24from uaclient.exceptions import LockHeldError, UserFacingError
25from uaclient.util import ProcessExecutionError, UrlError, subp25from uaclient.util import ProcessExecutionError, UrlError, subp
2626
27# Retry sleep backoff algorithm if lock is held.27# Retry sleep backoff algorithm if lock is held.
28# Lock may be held by auto-attach on systems with ubuntu-advantage-pro.28# Lock may be held by auto-attach on systems with ubuntu-advantage-pro.
29SLEEP_RETRIES_ON_LOCK_HELD = [1, 1, 5]29SLEEP_ON_LOCK_HELD = 1
30MAX_RETRIES_ON_LOCK_HELD = 7
3031
3132
32def run_command(cmd, cfg):33def run_command(cmd, cfg):
@@ -57,9 +58,7 @@ def fix_pro_pkg_holds(cfg):
57 if service.get("name") == "fips":58 if service.get("name") == "fips":
58 service_status = service.get("status")59 service_status = service.get("status")
59 if service_status == "enabled":60 if service_status == "enabled":
60 ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[61 ent_cls = FIPSEntitlement
61 service.get("name")
62 ]
63 logging.debug(62 logging.debug(
64 "Attempting to remove Ubuntu Pro FIPS package holds"63 "Attempting to remove Ubuntu Pro FIPS package holds"
65 )64 )
@@ -102,7 +101,6 @@ def process_remaining_deltas(cfg):
102 cfg.remove_notice("", status.MESSAGE_LIVEPATCH_LTS_REBOOT_REQUIRED)101 cfg.remove_notice("", status.MESSAGE_LIVEPATCH_LTS_REBOOT_REQUIRED)
103102
104103
105@assert_lock_file("ua-reboot-cmds")
106def process_reboot_operations(cfg):104def process_reboot_operations(cfg):
107105
108 reboot_cmd_marker_file = cfg.data_path("marker-reboot-cmds")106 reboot_cmd_marker_file = cfg.data_path("marker-reboot-cmds")
@@ -139,21 +137,17 @@ def main(cfg):
139 :raises: LockHeldError when lock still held by auto-attach after retries.137 :raises: LockHeldError when lock still held by auto-attach after retries.
140 UserFacingError for all other errors138 UserFacingError for all other errors
141 """139 """
142 while True:140 try:
143 try:141 with lock.SpinLock(
142 cfg=cfg,
143 lock_holder="ua-reboot-cmds",
144 sleep_time=SLEEP_ON_LOCK_HELD,
145 max_retries=MAX_RETRIES_ON_LOCK_HELD,
146 ):
144 process_reboot_operations(cfg=cfg)147 process_reboot_operations(cfg=cfg)
145 break148 except LockHeldError as e:
146 except LockHeldError as e:149 logging.warning("Lock not released. %s", str(e.msg))
147 logging.debug(150 sys.exit(1)
148 "Retrying ua-reboot-cmds {} times on held lock".format(
149 len(SLEEP_RETRIES_ON_LOCK_HELD)
150 )
151 )
152 if SLEEP_RETRIES_ON_LOCK_HELD:
153 time.sleep(SLEEP_RETRIES_ON_LOCK_HELD.pop(0))
154 else:
155 logging.warning("Lock not released. %s", str(e.msg))
156 sys.exit(1)
157151
158152
159if __name__ == "__main__":153if __name__ == "__main__":
diff --git a/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh b/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh
160new file mode 100755154new file mode 100755
index 0000000..586ea2c
--- /dev/null
+++ b/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh
@@ -0,0 +1,34 @@
1series=$1
2name=test-$series
3set -x
4lxc launch ubuntu-daily:$series $name >/dev/null 2>&1
5sleep 3
6
7echo "Confirming we are runnign a ${series} machine"
8lxc exec $name -- lsb_release -a
9
10echo "Updating to the latest version of UA"
11lxc exec $name -- apt-get update >/dev/null
12lxc exec $name -- apt-get install -y ubuntu-advantage-tools >/dev/null
13lxc exec $name -- ua version
14echo "Running ua status to persist cache"
15lxc exec $name -- ua status
16echo "Modifying ESM_SUPPORTED_ARCHS to emulate the issue"
17lxc exec $name -- sed -i "s/ESM_SUPPORTED_ARCHS=\"i386 amd64\"/ESM_SUPPORTED_ARCHS=\"\"/" /var/lib/dpkg/info/ubuntu-advantage-tools.postinst
18echo "Re-running the postinst script. Confirming that KeyError is reported on stdout"
19lxc exec $name -- dpkg-reconfigure ubuntu-advantage-tools
20
21echo "Updating UA to the version with the fix"
22lxc exec $name -- sh -c "echo \"deb http://archive.ubuntu.com/ubuntu $series-proposed main\" | tee /etc/apt/sources.list.d/proposed.list"
23lxc exec $name -- apt-get update >/dev/null
24lxc exec $name -- apt-get install -y ubuntu-advantage-tools >/dev/null
25lxc exec $name -- ua version
26
27echo "Running ua status to persist cache"
28lxc exec $name -- ua status
29echo "Modifying ESM_SUPPORTED_ARCHS to emulate the issue"
30lxc exec $name -- sed -i "s/ESM_SUPPORTED_ARCHS=\"i386 amd64\"/ESM_SUPPORTED_ARCHS=\"\"/" /var/lib/dpkg/info/ubuntu-advantage-tools.postinst
31echo "Re-running the postinst script. Confirming that KeyError is no longer reported"
32lxc exec $name -- dpkg-reconfigure ubuntu-advantage-tools
33
34lxc delete --force $name
diff --git a/sru/release-27.4/test-unattached-status-job.sh b/sru/release-27.4/test-unattached-status-job.sh
0new file mode 10075535new file mode 100755
index 0000000..e8e3253
--- /dev/null
+++ b/sru/release-27.4/test-unattached-status-job.sh
@@ -0,0 +1,40 @@
1series=$1
2set -x
3lxc launch ubuntu-daily:$series test >/dev/null 2>&1
4sleep 10
5
6echo
7echo "showing the network call in current ua version 27.3"
8lxc exec test -- apt-get update >/dev/null
9lxc exec test -- apt-get install -y ubuntu-advantage-tools >/dev/null
10lxc exec test -- ua version
11echo "disable all jobs but the status job"
12lxc exec test -- ua config set metering_timer=0
13lxc exec test -- ua config set update_messaging_timer=0
14echo "run the status update timer job by removing current timer state and executing the timer script"
15echo "and run tcpdump while executing the script, filtering by the current IPs of contracts.canonical.com"
16lxc exec test -- rm -f /var/lib/ubuntu-advantage/jobs-status.json
17lxc exec test -- sh -c "tcpdump \"(host 91.189.92.68 or host 91.189.92.69)\" & pid=\$! && sleep 5 && python3 /usr/lib/ubuntu-advantage/timer.py && kill \$pid"
18echo "Verify that tcpdump saw packets in above output"
19echo "Verify that the job was actually processed by the timer: update_status should be there."
20lxc exec test -- grep "update_status" /var/lib/ubuntu-advantage/jobs-status.json
21
22
23echo
24echo
25echo "installing new version from proposed"
26lxc exec test -- sh -c "echo \"deb http://archive.ubuntu.com/ubuntu $series-proposed main\" | tee /etc/apt/sources.list.d/proposed.list"
27lxc exec test -- apt-get update >/dev/null
28lxc exec test -- sh -c "DEBIAN_FRONTEND=noninteractive apt-get install -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" -y ubuntu-advantage-tools" > /dev/null
29lxc exec test -- ua version
30
31
32echo "run the status update timer job by removing current timer state and executing the timer script"
33echo "and run tcpdump while executing the script, filtering by the current IPs of contracts.canonical.com"
34lxc exec test -- rm -f /var/lib/ubuntu-advantage/jobs-status.json
35lxc exec test -- sh -c "tcpdump \"(host 91.189.92.68 or host 91.189.92.69)\" & pid=\$! && sleep 5 && python3 /usr/lib/ubuntu-advantage/timer.py && kill \$pid"
36echo "Verify that tcpdump DID NOT see packets in above output"
37echo "Verify that the job was actually processed by the timer: update_status should be there."
38lxc exec test -- grep "update_status" /var/lib/ubuntu-advantage/jobs-status.json
39
40lxc delete test --force
diff --git a/sru/release-27.5/test-aws-ipv6.sh b/sru/release-27.5/test-aws-ipv6.sh
0new file mode 10064441new file mode 100644
index 0000000..31353da
--- /dev/null
+++ b/sru/release-27.5/test-aws-ipv6.sh
@@ -0,0 +1,66 @@
1#!/bin/sh
2
3set -e
4
5KEY_PATH=$1
6DEB_PATH=$2
7sshopts=( -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR )
8
9REGION=us-west-2
10INSTANCE_TYPE=t3.micro
11KEY_NAME=test-ipv6
12PRO_IMAGE_ID=ami-07e00b8a1a054fdbf # bionic PRO image for us-west-2
13
14# You need to have a subnet that supports IPv6. The easiest path here is to launch an ec2
15# instance through pycloudlib, which will already create a VPC with a subnet that supports
16# IPv6. You can also use the security group created by pycloudlib
17SUBNET_ID=<SUBNET-ID>
18SECURITY_GROUP_ID=<SG-ID>
19
20# Make sure that the awscli being used has support for the --metadata-options params, otherwise the
21# IPv6 endpoint will not work as expected
22instance_info=$(aws --region $REGION ec2 run-instances --instance-type $INSTANCE_TYPE --image-id $PRO_IMAGE_ID --subnet-id $SUBNET_ID --key-name $KEY_NAME --associate-public-ip-address --ipv6-address-count 1 --metadata-options HttpEndpoint=enabled,HttpProtocolIpv6=enabled --security-group-ids $SECURITY_GROUP_ID)
23instance_id=$(echo $instance_info | jq -r ".Instances[0].InstanceId")
24instance_ip=$(aws ec2 describe-instances --region $REGION --instance-ids $instance_id --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
25
26echo "---------------------------------------------"
27echo "Checking instance info"
28ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- lsb_release -a
29ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status --wait
30echo "---------------------------------------------"
31echo -e "\n"
32
33echo "---------------------------------------------"
34echo "Detaching PRO instance"
35ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua detach --assume-yes
36echo "---------------------------------------------"
37echo -e "\n"
38
39echo "---------------------------------------------"
40echo "Installing package with IPv6 support"
41scp "${sshopts[@]}" -i $KEY_PATH $DEB_PATH ubuntu@$instance_ip:/home/ubuntu/ua.deb
42ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo dpkg -i ua.deb
43ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- ua version
44ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status --wait
45echo "---------------------------------------------"
46echo -e "\n"
47
48echo "---------------------------------------------"
49echo "Modifying IPv4 address to make it fail"
50ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo rm /var/log/ubuntu-advantage.log
51ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo sed -i "s/169.254.169.254/169.254.169.1/g" /usr/lib/python3/dist-packages/uaclient/clouds/aws.py
52echo "---------------------------------------------"
53echo -e "\n"
54
55echo "---------------------------------------------"
56echo "Verify that auto-attach still works and IPv6 route was used instead"
57ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua auto-attach
58ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status
59ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"Could not reach AWS IMDS at http://169.254.169.1\" /var/log/ubuntu-advantage.log
60ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [PUT]: http://169.254.169.1/latest/api/token\" /var/log/ubuntu-advantage.log
61ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [PUT]: http://[fd00:ec2::254]/latest/api/token\" /var/log/ubuntu-advantage.log
62ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [PUT] response: http://[fd00:ec2::254]/latest/api/token\" /var/log/ubuntu-advantage.log
63ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [GET]: http://[fd00:ec2::254]/latest/dynamic/instance-identity/pkcs7\" /var/log/ubuntu-advantage.log
64ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [GET] response: http://[fd00:ec2::254]/latest/dynamic/instance-identity/pkcs7\" /var/log/ubuntu-advantage.log
65echo "---------------------------------------------"
66echo -e "\n"
diff --git a/tools/build.sh b/tools/build.sh
index 1e50767..7660908 100755
--- a/tools/build.sh
+++ b/tools/build.sh
@@ -1,2 +1,3 @@
1#!/usr/bin/bash1#!/usr/bin/bash
2env PYTHONPATH=. python3 tools/build.py "$@"2env PYTHONPATH=. python3 tools/build.py "$@"
3notify-send "Build finished!" || true
diff --git a/tools/create-lp-release-branches.sh b/tools/create-lp-release-branches.sh
index 436abdf..604df74 100755
--- a/tools/create-lp-release-branches.sh
+++ b/tools/create-lp-release-branches.sh
@@ -32,7 +32,7 @@ else
32 set -e32 set -e
33fi33fi
3434
35for release in xenial bionic focal hirsute35for release in xenial bionic focal hirsute impish
36do36do
37 echo37 echo
38 echo $release38 echo $release
@@ -49,6 +49,7 @@ do
49 bionic) version=${UA_VERSION}~18.04.1;;49 bionic) version=${UA_VERSION}~18.04.1;;
50 focal) version=${UA_VERSION}~20.04.1;;50 focal) version=${UA_VERSION}~20.04.1;;
51 hirsute) version=${UA_VERSION}~21.04.1;;51 hirsute) version=${UA_VERSION}~21.04.1;;
52 impish) version=${UA_VERSION}~21.10.1;;
52 esac53 esac
53 dch_cmd=(dch -v ${version} -D ${release} -b "Backport new upstream release: (LP: #${SRU_BUG}) to $release")54 dch_cmd=(dch -v ${version} -D ${release} -b "Backport new upstream release: (LP: #${SRU_BUG}) to $release")
54 if [ -z "$DO_IT" ]; then55 if [ -z "$DO_IT" ]; then
diff --git a/tools/run-integration-tests.py b/tools/run-integration-tests.py
index 2ab8786..10b4a55 100644
--- a/tools/run-integration-tests.py
+++ b/tools/run-integration-tests.py
@@ -14,6 +14,7 @@ SERIES_TO_VERSION = {
14 "focal": "20.04",14 "focal": "20.04",
15 "hirsute": "21.04",15 "hirsute": "21.04",
16 "impish": "21.10",16 "impish": "21.10",
17 "jammy": "22.04",
17}18}
1819
19TOKEN_TO_ENVVAR = {20TOKEN_TO_ENVVAR = {
@@ -30,7 +31,7 @@ PLATFORM_SERIES_TESTS = {
30 "gcpgeneric": ["xenial", "bionic", "focal", "hirsute"],31 "gcpgeneric": ["xenial", "bionic", "focal", "hirsute"],
31 "gcppro": ["xenial", "bionic", "focal"],32 "gcppro": ["xenial", "bionic", "focal"],
32 "vm": ["xenial", "bionic", "focal"],33 "vm": ["xenial", "bionic", "focal"],
33 "lxd": ["xenial", "bionic", "focal", "hirsute", "impish"],34 "lxd": ["xenial", "bionic", "focal", "hirsute", "impish", "jammy"],
34 "upgrade": ["xenial", "bionic", "focal", "hirsute", "impish"],35 "upgrade": ["xenial", "bionic", "focal", "hirsute", "impish"],
35}36}
3637
diff --git a/tools/test-in-lxd.sh b/tools/test-in-lxd.sh
index 17d40cf..86ffdfe 100755
--- a/tools/test-in-lxd.sh
+++ b/tools/test-in-lxd.sh
@@ -12,5 +12,17 @@ lxc delete $name --force
12lxc launch ubuntu-daily:$series $name12lxc launch ubuntu-daily:$series $name
13sleep 513sleep 5
14lxc file push $deb $name/tmp/ua.deb14lxc file push $deb $name/tmp/ua.deb
15
16if [ -n "$SHELL_BEFORE" ]; then
17 set +x
18 echo
19 echo
20 echo "New version of ua has not been installed yet."
21 echo "After you exit the shell we'll upgrade ua and bring you right back."
22 echo
23 set -x
24 lxc exec $name bash
25fi
26
15lxc exec $name -- dpkg -i /tmp/ua.deb27lxc exec $name -- dpkg -i /tmp/ua.deb
16lxc exec $name bash28lxc exec $name bash
diff --git a/tox.ini b/tox.ini
index 701eae3..9c98f03 100644
--- a/tox.ini
+++ b/tox.ini
@@ -54,6 +54,7 @@ commands =
54 behave-lxd-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.focal,series.lts,series.all" --tags="~upgrade"54 behave-lxd-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.focal,series.lts,series.all" --tags="~upgrade"
55 behave-lxd-21.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.hirsute,series.all" --tags="~upgrade"55 behave-lxd-21.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.hirsute,series.all" --tags="~upgrade"
56 behave-lxd-21.10: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.impish,series.all" --tags="~upgrade"56 behave-lxd-21.10: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.impish,series.all" --tags="~upgrade"
57 behave-lxd-22.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.jammy,series.all" --tags="~upgrade"
57 behave-vm-16.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.xenial,series.all,series.lts" --tags="~upgrade"58 behave-vm-16.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.xenial,series.all,series.lts" --tags="~upgrade"
58 behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all,series.lts" --tags="~upgrade"59 behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all,series.lts" --tags="~upgrade"
59 behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all,series.lts" --tags="~upgrade"60 behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all,series.lts" --tags="~upgrade"
diff --git a/uaclient/actions.py b/uaclient/actions.py
60new file mode 10064461new file mode 100644
index 0000000..3f98c7f
--- /dev/null
+++ b/uaclient/actions.py
@@ -0,0 +1,66 @@
1import logging
2
3from uaclient import clouds, config, contract, exceptions, status, util
4from uaclient.clouds import identity
5
6LOG = logging.getLogger("ua.actions")
7
8
9def attach_with_token(
10 cfg: config.UAConfig, token: str, allow_enable: bool
11) -> None:
12 """
13 Common functionality to take a token and attach via contract backend
14 :raise UrlError: On unexpected connectivity issues to contract
15 server or inability to access identity doc from metadata service.
16 :raise ContractAPIError: On unexpected errors when talking to the contract
17 server.
18 """
19 try:
20 contract.request_updated_contract(
21 cfg, token, allow_enable=allow_enable
22 )
23 except util.UrlError as exc:
24 with util.disable_log_to_console():
25 LOG.exception(exc)
26 cfg.status() # Persist updated status in the event of partial attach
27 config.update_ua_messages(cfg)
28 raise exc
29 except exceptions.UserFacingError as exc:
30 LOG.warning(exc.msg)
31 cfg.status() # Persist updated status in the event of partial attach
32 config.update_ua_messages(cfg)
33 raise exc
34
35 config.update_ua_messages(cfg)
36
37
38def auto_attach(
39 cfg: config.UAConfig, cloud: clouds.AutoAttachCloudInstance
40) -> None:
41 """
42 :raise UrlError: On unexpected connectivity issues to contract
43 server or inability to access identity doc from metadata service.
44 :raise ContractAPIError: On unexpected errors when talking to the contract
45 server.
46 :raise NonAutoAttachImageError: If this cloud type does not have
47 auto-attach support.
48 """
49 contract_client = contract.UAContractClient(cfg)
50 try:
51 tokenResponse = contract_client.request_auto_attach_contract_token(
52 instance=cloud
53 )
54 except contract.ContractAPIError as e:
55 if e.code and 400 <= e.code < 500:
56 raise exceptions.NonAutoAttachImageError(
57 status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
58 )
59 raise e
60 current_iid = identity.get_instance_id()
61 if current_iid:
62 cfg.write_cache("instance-id", current_iid)
63
64 token = tokenResponse["contractToken"]
65
66 attach_with_token(cfg, token=token, allow_enable=True)
diff --git a/uaclient/cli.py b/uaclient/cli.py
index 77af6f7..f76e570 100644
--- a/uaclient/cli.py
+++ b/uaclient/cli.py
@@ -15,23 +15,36 @@ import tempfile
15import textwrap15import textwrap
16import time16import time
17from functools import wraps17from functools import wraps
18from typing import Optional # noqa: F401
18from typing import List19from typing import List
1920
20import yaml21import yaml
2122
22from uaclient import (23from uaclient import (
24 actions,
23 config,25 config,
24 contract,26 contract,
25 entitlements,27 entitlements,
26 exceptions,28 exceptions,
27 jobs,29 jobs,
30 lock,
28 security,31 security,
29 security_status,32 security_status,
30)33)
31from uaclient import status as ua_status34from uaclient import status as ua_status
32from uaclient import util, version35from uaclient import util, version
36from uaclient.clouds import AutoAttachCloudInstance # noqa: F401
33from uaclient.clouds import identity37from uaclient.clouds import identity
34from uaclient.defaults import CONFIG_FIELD_ENVVAR_ALLOWLIST38from uaclient.defaults import (
39 CLOUD_BUILD_INFO,
40 CONFIG_FIELD_ENVVAR_ALLOWLIST,
41 DEFAULT_CONFIG_FILE,
42)
43
44# TODO: Better address service commands running on cli
45# It is not ideal for us to import an entitlement directly on the cli module.
46# We need to refactor this to avoid that type of coupling in the code.
47from uaclient.entitlements.livepatch import LIVEPATCH_CMD
3548
36NAME = "ua"49NAME = "ua"
3750
@@ -67,11 +80,6 @@ UA_SERVICES = (
67 "ua-license-check.timer",80 "ua-license-check.timer",
68)81)
6982
70# Set a module-level callable here so we don't have to reinstantiate
71# UAConfig in order to determine dynamic data_path exception handling of
72# main_error_handler
73_CLEAR_LOCK_FILE = None
74
7583
76class UAArgumentParser(argparse.ArgumentParser):84class UAArgumentParser(argparse.ArgumentParser):
77 def __init__(85 def __init__(
@@ -114,41 +122,13 @@ class UAArgumentParser(argparse.ArgumentParser):
114122
115123
116def assert_lock_file(lock_holder=None):124def assert_lock_file(lock_holder=None):
117 """Decorator asserting exclusive access to lock file125 """Decorator asserting exclusive access to lock file"""
118
119 Create a lock file if absent. The lock file will contain a pid of the
120 running process, and a customer-visible description of the lock holder.
121
122 :param lock_holder: String with the service name or command which is
123 holding the lock.
124
125 This lock_holder string will be customer visible in status.json.
126
127 :raises: LockHeldError if lock is held.
128 """
129126
130 def wrapper(f):127 def wrapper(f):
131 @wraps(f)128 @wraps(f)
132 def new_f(*args, cfg, **kwargs):129 def new_f(*args, cfg, **kwargs):
133 global _CLEAR_LOCK_FILE130 with lock.SingleAttemptLock(cfg=cfg, lock_holder=lock_holder):
134 (lock_pid, cur_lock_holder) = cfg.check_lock_info()
135 if lock_pid > 0:
136 raise exceptions.LockHeldError(
137 lock_request=lock_holder,
138 lock_holder=cur_lock_holder,
139 pid=lock_pid,
140 )
141 cfg.write_cache("lock", "{}:{}".format(os.getpid(), lock_holder))
142 notice_msg = "Operation in progress: {}".format(lock_holder)
143 cfg.add_notice("", notice_msg)
144 _CLEAR_LOCK_FILE = cfg.delete_cache_key
145
146 try:
147 retval = f(*args, cfg=cfg, **kwargs)131 retval = f(*args, cfg=cfg, **kwargs)
148 finally:
149 cfg.delete_cache_key("lock")
150 _CLEAR_LOCK_FILE = None # Unset due to successful lock delete
151
152 return retval132 return retval
153133
154 return new_f134 return new_f
@@ -584,6 +564,14 @@ def status_parser(parser):
584564
585 * AVAILABLE: whether this service would be available if this machine565 * AVAILABLE: whether this service would be available if this machine
586 were attached. The possible values are yes or no.566 were attached. The possible values are yes or no.
567
568 If --simulate-with-token is used, then the output has five
569 columns. SERVICE, AVAILABLE, ENTITLED and DESCRIPTION are the same
570 as mentioned above, and AUTO_ENABLED shows whether the service is set
571 to be enabled when that token is attached.
572
573 If the --all flag is set, beta and unavailable services are also
574 listed in the output.
587 """575 """
588 )576 )
589577
@@ -605,6 +593,12 @@ def status_parser(parser):
605 ),593 ),
606 )594 )
607 parser.add_argument(595 parser.add_argument(
596 "--simulate-with-token",
597 metavar="TOKEN",
598 action="store",
599 help=("simulate the output status using a provided token"),
600 )
601 parser.add_argument(
608 "--all",602 "--all",
609 action="store_true",603 action="store_true",
610 help="Allow the visualization of beta services",604 help="Allow the visualization of beta services",
@@ -623,7 +617,7 @@ def _perform_disable(entitlement_name, cfg, *, assume_yes):
623617
624 @return: True on success, False otherwise618 @return: True on success, False otherwise
625 """619 """
626 ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[entitlement_name]620 ent_cls = entitlements.entitlement_factory(entitlement_name)
627 entitlement = ent_cls(cfg, assume_yes=assume_yes)621 entitlement = ent_cls(cfg, assume_yes=assume_yes)
628 ret = entitlement.disable()622 ret = entitlement.disable()
629 cfg.status() # Update the status cache623 cfg.status() # Update the status cache
@@ -639,7 +633,9 @@ def get_valid_entitlement_names(names: List[str]):
639 entitlements_found = []633 entitlements_found = []
640634
641 for ent_name in names:635 for ent_name in names:
642 if ent_name in entitlements.ENTITLEMENT_CLASS_BY_NAME:636 if ent_name in entitlements.valid_services(
637 allow_beta=True, all_names=True
638 ):
643 entitlements_found.append(ent_name)639 entitlements_found.append(ent_name)
644640
645 entitlements_not_found = sorted(set(names) - set(entitlements_found))641 entitlements_not_found = sorted(set(names) - set(entitlements_found))
@@ -878,12 +874,14 @@ def action_enable(args, *, cfg, **kwargs):
878 )874 )
879 valid_services_names = entitlements.valid_services(allow_beta=args.beta)875 valid_services_names = entitlements.valid_services(allow_beta=args.beta)
880 ret = True876 ret = True
881
882 for ent_name in entitlements_found:877 for ent_name in entitlements_found:
883 try:878 try:
884 ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[ent_name]879 ent_cls = entitlements.entitlement_factory(ent_name)
885 entitlement = ent_cls(880 entitlement = ent_cls(
886 cfg, assume_yes=args.assume_yes, allow_beta=args.beta881 cfg,
882 assume_yes=args.assume_yes,
883 allow_beta=args.beta,
884 called_name=ent_name,
887 )885 )
888 ent_ret, reason = entitlement.enable()886 ent_ret, reason = entitlement.enable()
889 cfg.status() # Update the status cache887 cfg.status() # Update the status cache
@@ -991,27 +989,7 @@ def _detach(cfg: config.UAConfig, assume_yes: bool) -> int:
991 return 0989 return 0
992990
993991
994def _attach_with_token(992def _post_cli_attach(cfg: config.UAConfig) -> None:
995 cfg: config.UAConfig, token: str, allow_enable: bool
996) -> int:
997 """Common functionality to take a token and attach via contract backend"""
998 try:
999 contract.request_updated_contract(
1000 cfg, token, allow_enable=allow_enable
1001 )
1002 except util.UrlError as exc:
1003 with util.disable_log_to_console():
1004 logging.exception(exc)
1005 print(ua_status.MESSAGE_ATTACH_FAILURE)
1006 cfg.status() # Persist updated status in the event of partial attach
1007 config.update_ua_messages(cfg)
1008 return 1
1009 except exceptions.UserFacingError as exc:
1010 logging.warning(exc.msg)
1011 cfg.status() # Persist updated status in the event of partial attach
1012 config.update_ua_messages(cfg)
1013 return 1
1014
1015 contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][993 contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][
1016 "name"994 "name"
1017 ]995 ]
@@ -1025,75 +1003,74 @@ def _attach_with_token(
1025 else:1003 else:
1026 print(ua_status.MESSAGE_ATTACH_SUCCESS_NO_CONTRACT_NAME)1004 print(ua_status.MESSAGE_ATTACH_SUCCESS_NO_CONTRACT_NAME)
10271005
1028 config.update_ua_messages(cfg)
1029 jobs.disable_license_check_if_applicable(cfg)
1030 action_status(args=None, cfg=cfg)1006 action_status(args=None, cfg=cfg)
1031 return 0
1032
10331007
1034def _get_contract_token_from_cloud_identity(cfg: config.UAConfig) -> str:
1035 """Detect cloud_type and request a contract token from identity info.
10361008
1037 :param cfg: a ``config.UAConfig`` instance1009@assert_root
10381010@assert_lock_file("ua auto-attach")
1039 :raise NonAutoAttachImageError: When not on an auto-attach image type.1011def action_auto_attach(args, *, cfg):
1040 :raise UrlError: On unexpected connectivity issues to contract1012 disable_auto_attach = util.is_config_value_true(
1041 server or inability to access identity doc from metadata service.1013 config=cfg.cfg, path_to_value="features.disable_auto_attach"
1042 :raise ContractAPIError: On unexpected errors when talking to the contract1014 )
1043 server.1015 if disable_auto_attach:
1044 :raise NonAutoAttachImageError: If this cloud type does not have1016 msg = "Skipping auto-attach. Config disable_auto_attach is set."
1045 auto-attach support.1017 logging.debug(msg)
1018 print(msg)
1019 return 0
10461020
1047 :return: contract token obtained from identity doc1021 instance = None # type: Optional[AutoAttachCloudInstance]
1048 """
1049 try:1022 try:
1050 instance = identity.cloud_instance_factory()1023 instance = identity.cloud_instance_factory()
1051 except exceptions.UserFacingError as e:1024 except exceptions.CloudFactoryError as e:
1052 if cfg.is_attached:1025 if cfg.is_attached:
1053 # We are attached on non-Pro Image, just report already attached1026 # We are attached on non-Pro Image, just report already attached
1054 raise exceptions.AlreadyAttachedError(cfg)1027 raise exceptions.AlreadyAttachedError(cfg)
1055 # Unattached on non-Pro return UserFacing error msg details1028 if isinstance(e, exceptions.CloudFactoryNoCloudError):
1056 raise e1029 raise exceptions.UserFacingError(
1030 ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
1031 )
1032 if isinstance(e, exceptions.CloudFactoryNonViableCloudError):
1033 raise exceptions.UserFacingError(
1034 ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
1035 )
1036 if isinstance(e, exceptions.CloudFactoryUnsupportedCloudError):
1037 raise exceptions.NonAutoAttachImageError(
1038 ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
1039 cloud_type=e.cloud_type
1040 )
1041 )
1042 # we shouldn't get here, but this is a reasonable default just in case
1043 raise exceptions.UserFacingError(
1044 ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
1045 )
1046
1047 if not instance:
1048 # we shouldn't get here, but this is a reasonable default just in case
1049 raise exceptions.UserFacingError(
1050 ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
1051 )
1052
1057 current_iid = identity.get_instance_id()1053 current_iid = identity.get_instance_id()
1058 if cfg.is_attached:1054 if cfg.is_attached:
1059 prev_iid = cfg.read_cache("instance-id")1055 prev_iid = cfg.read_cache("instance-id")
1060 if str(current_iid) == str(prev_iid):1056 if str(current_iid) == str(prev_iid):
1061 raise exceptions.AlreadyAttachedError(cfg)1057 raise exceptions.AlreadyAttachedOnPROError(str(current_iid))
1062 print("Re-attaching Ubuntu Advantage subscription on new instance")1058 print("Re-attaching Ubuntu Advantage subscription on new instance")
1063 if _detach(cfg, assume_yes=True) != 0:1059 if _detach(cfg, assume_yes=True) != 0:
1064 raise exceptions.UserFacingError(1060 raise exceptions.UserFacingError(
1065 ua_status.MESSAGE_DETACH_AUTOMATION_FAILURE1061 ua_status.MESSAGE_DETACH_AUTOMATION_FAILURE
1066 )1062 )
1067 contract_client = contract.UAContractClient(cfg)
1068 try:
1069 tokenResponse = contract_client.request_auto_attach_contract_token(
1070 instance=instance
1071 )
1072 except contract.ContractAPIError as e:
1073 if e.code and 400 <= e.code < 500:
1074 raise exceptions.NonAutoAttachImageError(
1075 ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
1076 )
1077 raise e
1078 if current_iid:
1079 cfg.write_cache("instance-id", current_iid)
1080
1081 return tokenResponse["contractToken"]
1082
10831063
1084@assert_root1064 try:
1085@assert_lock_file("ua auto-attach")1065 actions.auto_attach(cfg, instance)
1086def action_auto_attach(args, *, cfg):1066 except util.UrlError:
1087 disable_auto_attach = util.is_config_value_true(1067 print(ua_status.MESSAGE_ATTACH_FAILURE)
1088 config=cfg.cfg, path_to_value="features.disable_auto_attach"1068 return 1
1089 )1069 except exceptions.UserFacingError:
1090 if disable_auto_attach:1070 return 1
1091 msg = "Skipping auto-attach. Config disable_auto_attach is set."1071 else:
1092 logging.debug(msg)1072 _post_cli_attach(cfg)
1093 print(msg)
1094 return 01073 return 0
1095 token = _get_contract_token_from_cloud_identity(cfg)
1096 return _attach_with_token(cfg, token=token, allow_enable=True)
10971074
10981075
1099@assert_not_attached1076@assert_not_attached
@@ -1104,9 +1081,18 @@ def action_attach(args, *, cfg):
1104 raise exceptions.UserFacingError(1081 raise exceptions.UserFacingError(
1105 ua_status.MESSAGE_ATTACH_REQUIRES_TOKEN1082 ua_status.MESSAGE_ATTACH_REQUIRES_TOKEN
1106 )1083 )
1107 return _attach_with_token(1084 try:
1108 cfg, token=args.token, allow_enable=args.auto_enable1085 actions.attach_with_token(
1109 )1086 cfg, token=args.token, allow_enable=args.auto_enable
1087 )
1088 except util.UrlError:
1089 print(ua_status.MESSAGE_ATTACH_FAILURE)
1090 return 1
1091 except exceptions.UserFacingError:
1092 return 1
1093 else:
1094 _post_cli_attach(cfg)
1095 return 0
11101096
11111097
1112def _write_command_output_to_file(1098def _write_command_output_to_file(
@@ -1135,7 +1121,7 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
1135 "ua status --format json", "{}/ua-status.json".format(output_dir)1121 "ua status --format json", "{}/ua-status.json".format(output_dir)
1136 )1122 )
1137 _write_command_output_to_file(1123 _write_command_output_to_file(
1138 "canonical-livepatch status",1124 "{} status".format(LIVEPATCH_CMD),
1139 "{}/livepatch-status.txt".format(output_dir),1125 "{}/livepatch-status.txt".format(output_dir),
1140 )1126 )
1141 _write_command_output_to_file(1127 _write_command_output_to_file(
@@ -1164,11 +1150,12 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
1164 )1150 )
11651151
1166 ua_logs = (1152 ua_logs = (
1167 cfg.cfg_path or "/etc/ubuntu-advantage/uaclient.conf",1153 cfg.cfg_path or DEFAULT_CONFIG_FILE,
1168 cfg.log_file,1154 cfg.log_file,
1169 cfg.timer_log_file,1155 cfg.timer_log_file,
1170 cfg.license_check_log_file,1156 cfg.license_check_log_file,
1171 cfg.data_path("jobs-status"),1157 cfg.data_path("jobs-status"),
1158 CLOUD_BUILD_INFO,
1172 *(1159 *(
1173 entitlement.repo_list_file_tmpl.format(name=entitlement.name)1160 entitlement.repo_list_file_tmpl.format(name=entitlement.name)
1174 for entitlement in entitlements.ENTITLEMENT_CLASSES1161 for entitlement in entitlements.ENTITLEMENT_CLASSES
@@ -1178,6 +1165,9 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
11781165
1179 for log in ua_logs:1166 for log in ua_logs:
1180 if os.path.isfile(log):1167 if os.path.isfile(log):
1168 log_content = util.load_file(log)
1169 log_content = util.redact_sensitive_logs(log_content)
1170 util.write_file(log, log_content)
1181 shutil.copy(log, output_dir)1171 shutil.copy(log, output_dir)
11821172
1183 with tarfile.open(output_file, "w:gz") as results:1173 with tarfile.open(output_file, "w:gz") as results:
@@ -1189,29 +1179,36 @@ def get_parser():
1189 base_desc = __doc__1179 base_desc = __doc__
1190 non_beta_services_desc = []1180 non_beta_services_desc = []
1191 beta_services_desc = []1181 beta_services_desc = []
1192 sorted_classes = sorted(entitlements.ENTITLEMENT_CLASS_BY_NAME.items())1182
1193 for name, ent_cls in sorted_classes:1183 resources = contract.get_available_resources(config.UAConfig())
1194 if ent_cls.help_doc_url:1184 for resource in resources:
1195 url = " ({})".format(ent_cls.help_doc_url)1185 ent_cls = entitlements.entitlement_factory(resource["name"])
1196 else:1186 if ent_cls:
1197 url = ""1187 # Because we are not sure of the presentation name if unattached
1198 service_line = service_line_tmpl.format(1188 presentation_name = resource.get("presentedAs", resource["name"])
1199 name=name, description=ent_cls.description, url=url1189 if ent_cls.help_doc_url:
1200 )1190 url = " ({})".format(ent_cls.help_doc_url)
1201 if len(service_line) <= 80:1191 else:
1202 service_info = [service_line]1192 url = ""
1203 else:1193 service_line = service_line_tmpl.format(
1204 wrapped_words = []1194 name=presentation_name,
1205 line = service_line1195 description=ent_cls.description,
1206 while len(line) > 80:1196 url=url,
1207 [line, wrapped_word] = line.rsplit(" ", 1)1197 )
1208 wrapped_words.insert(0, wrapped_word)1198 if len(service_line) <= 80:
1209 service_info = [line + "\n " + " ".join(wrapped_words)]1199 service_info = [service_line]
12101200 else:
1211 if ent_cls.is_beta:1201 wrapped_words = []
1212 beta_services_desc.extend(service_info)1202 line = service_line
1213 else:1203 while len(line) > 80:
1214 non_beta_services_desc.extend(service_info)1204 [line, wrapped_word] = line.rsplit(" ", 1)
1205 wrapped_words.insert(0, wrapped_word)
1206 service_info = [line + "\n " + " ".join(wrapped_words)]
1207
1208 if ent_cls.is_beta:
1209 beta_services_desc.extend(service_info)
1210 else:
1211 non_beta_services_desc.extend(service_info)
12151212
1216 parser = UAArgumentParser(1213 parser = UAArgumentParser(
1217 prog=NAME,1214 prog=NAME,
@@ -1327,7 +1324,11 @@ def action_status(args, *, cfg):
1327 if not cfg:1324 if not cfg:
1328 cfg = config.UAConfig()1325 cfg = config.UAConfig()
1329 show_beta = args.all if args else False1326 show_beta = args.all if args else False
1330 status = cfg.status(show_beta=show_beta)1327 token = args.simulate_with_token if args else None
1328 if token:
1329 status = cfg.simulate_status(token=token, show_beta=show_beta)
1330 else:
1331 status = cfg.status(show_beta=show_beta)
1331 active_value = ua_status.UserFacingConfigStatus.ACTIVE.value1332 active_value = ua_status.UserFacingConfigStatus.ACTIVE.value
1332 config_active = bool(status["execution_status"] == active_value)1333 config_active = bool(status["execution_status"] == active_value)
1333 if args and args.wait and config_active:1334 if args and args.wait and config_active:
@@ -1460,8 +1461,7 @@ def main_error_handler(func):
1460 with util.disable_log_to_console():1461 with util.disable_log_to_console():
1461 logging.error("KeyboardInterrupt")1462 logging.error("KeyboardInterrupt")
1462 print("Interrupt received; exiting.", file=sys.stderr)1463 print("Interrupt received; exiting.", file=sys.stderr)
1463 if _CLEAR_LOCK_FILE:1464 lock.clear_lock_file_if_present()
1464 _CLEAR_LOCK_FILE("lock")
1465 sys.exit(1)1465 sys.exit(1)
1466 except util.UrlError as exc:1466 except util.UrlError as exc:
1467 if "CERTIFICATE_VERIFY_FAILED" in str(exc):1467 if "CERTIFICATE_VERIFY_FAILED" in str(exc):
@@ -1482,23 +1482,20 @@ def main_error_handler(func):
1482 msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_TMPL1482 msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_TMPL
1483 logging.exception(msg_tmpl.format(**msg_args))1483 logging.exception(msg_tmpl.format(**msg_args))
1484 print(ua_status.MESSAGE_CONNECTIVITY_ERROR, file=sys.stderr)1484 print(ua_status.MESSAGE_CONNECTIVITY_ERROR, file=sys.stderr)
1485 if _CLEAR_LOCK_FILE:1485 lock.clear_lock_file_if_present()
1486 _CLEAR_LOCK_FILE("lock")
1487 sys.exit(1)1486 sys.exit(1)
1488 except exceptions.UserFacingError as exc:1487 except exceptions.UserFacingError as exc:
1489 with util.disable_log_to_console():1488 with util.disable_log_to_console():
1490 logging.error(exc.msg)1489 logging.error(exc.msg)
1491 print("{}".format(exc.msg), file=sys.stderr)1490 print("{}".format(exc.msg), file=sys.stderr)
1492 if _CLEAR_LOCK_FILE:1491 if not isinstance(exc, exceptions.LockHeldError):
1493 if not isinstance(exc, exceptions.LockHeldError):1492 # Only clear the lock if it is ours.
1494 # Only clear the lock if it is ours.1493 lock.clear_lock_file_if_present()
1495 _CLEAR_LOCK_FILE("lock")
1496 sys.exit(exc.exit_code)1494 sys.exit(exc.exit_code)
1497 except Exception:1495 except Exception:
1498 with util.disable_log_to_console():1496 with util.disable_log_to_console():
1499 logging.exception("Unhandled exception, please file a bug")1497 logging.exception("Unhandled exception, please file a bug")
1500 if _CLEAR_LOCK_FILE:1498 lock.clear_lock_file_if_present()
1501 _CLEAR_LOCK_FILE("lock")
1502 print(ua_status.MESSAGE_UNEXPECTED_ERROR, file=sys.stderr)1499 print(ua_status.MESSAGE_UNEXPECTED_ERROR, file=sys.stderr)
1503 sys.exit(1)1500 sys.exit(1)
15041501
diff --git a/uaclient/clouds/aws.py b/uaclient/clouds/aws.py
index 120ded6..e8a1c07 100644
--- a/uaclient/clouds/aws.py
+++ b/uaclient/clouds/aws.py
@@ -1,11 +1,17 @@
1import logging
1from typing import Any, Dict2from typing import Any, Dict
2from urllib.error import HTTPError3from urllib.error import HTTPError
34
4from uaclient import util5from uaclient import exceptions, util
5from uaclient.clouds import AutoAttachCloudInstance6from uaclient.clouds import AutoAttachCloudInstance
67
7IMDS_V2_TOKEN_URL = "http://169.254.169.254/latest/api/token"8IMDS_IPV4_ADDRESS = "169.254.169.254"
8IMDS_URL = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7"9IMDS_IPV6_ADDRESS = "[fd00:ec2::254]"
10
11IMDS_IP_ADDRESS = (IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
12IMDS_V2_TOKEN_URL = "http://{}/latest/api/token"
13IMDS_URL = "http://{}/latest/dynamic/instance-identity/pkcs7"
14
9SYS_HYPERVISOR_PRODUCT_UUID = "/sys/hypervisor/uuid"15SYS_HYPERVISOR_PRODUCT_UUID = "/sys/hypervisor/uuid"
10DMI_PRODUCT_SERIAL = "/sys/class/dmi/id/product_serial"16DMI_PRODUCT_SERIAL = "/sys/class/dmi/id/product_serial"
11DMI_PRODUCT_UUID = "/sys/class/dmi/id/product_uuid"17DMI_PRODUCT_UUID = "/sys/class/dmi/id/product_uuid"
@@ -18,27 +24,58 @@ AWS_TOKEN_REQ_HEADER = AWS_TOKEN_PUT_HEADER + "-ttl-seconds"
18class UAAutoAttachAWSInstance(AutoAttachCloudInstance):24class UAAutoAttachAWSInstance(AutoAttachCloudInstance):
1925
20 _api_token = None26 _api_token = None
27 _ip_address = None
28
29 def _get_imds_url_response(self):
30 headers = self._request_imds_v2_token_headers()
31 return util.readurl(
32 IMDS_URL.format(self._ip_address), headers=headers, timeout=1
33 )
2134
22 # mypy does not handle @property around inner decorators35 # mypy does not handle @property around inner decorators
23 # https://github.com/python/mypy/issues/136236 # https://github.com/python/mypy/issues/1362
24 @property # type: ignore37 @property # type: ignore
25 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])38 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])
26 def identity_doc(self) -> Dict[str, Any]:39 def identity_doc(self) -> Dict[str, Any]:
27 headers = self._get_imds_v2_token_headers()40 response, _headers = self._get_imds_url_response()
28 response, _headers = util.readurl(IMDS_URL, headers=headers)
29 return {"pkcs7": response}41 return {"pkcs7": response}
3042
43 def _request_imds_v2_token_headers(self):
44 for address in IMDS_IP_ADDRESS:
45 try:
46 headers = self._get_imds_v2_token_headers(ip_address=address)
47 except HTTPError as e:
48 raise e
49 except Exception as e:
50 msg = (
51 "Could not reach AWS IMDS at http://{endpoint}:"
52 " {reason}\n".format(
53 endpoint=address, reason=getattr(e, "reason", "")
54 )
55 )
56 logging.debug(msg)
57 else:
58 self._ip_address = address
59 break
60 if self._ip_address is None:
61 raise exceptions.UserFacingError(
62 "No valid AWS IMDS endpoint discovered at addresses: %s"
63 % ", ".join(IMDS_IP_ADDRESS)
64 )
65 return headers
66
31 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])67 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])
32 def _get_imds_v2_token_headers(self):68 def _get_imds_v2_token_headers(self, ip_address):
33 if self._api_token == "IMDSv1":69 if self._api_token == "IMDSv1":
34 return None70 return None
35 elif self._api_token:71 elif self._api_token:
36 return {AWS_TOKEN_PUT_HEADER: self._api_token}72 return {AWS_TOKEN_PUT_HEADER: self._api_token}
37 try:73 try:
38 response, _headers = util.readurl(74 response, _headers = util.readurl(
39 IMDS_V2_TOKEN_URL,75 IMDS_V2_TOKEN_URL.format(ip_address),
40 method="PUT",76 method="PUT",
41 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},77 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
78 timeout=1,
42 )79 )
43 except HTTPError as e:80 except HTTPError as e:
44 if e.code == 404:81 if e.code == 404:
@@ -46,6 +83,7 @@ class UAAutoAttachAWSInstance(AutoAttachCloudInstance):
46 return None83 return None
47 else:84 else:
48 raise85 raise
86
49 self._api_token = response87 self._api_token = response
50 return {AWS_TOKEN_PUT_HEADER: self._api_token}88 return {AWS_TOKEN_PUT_HEADER: self._api_token}
5189
diff --git a/uaclient/clouds/identity.py b/uaclient/clouds/identity.py
index dee23f6..cd8f7b4 100644
--- a/uaclient/clouds/identity.py
+++ b/uaclient/clouds/identity.py
@@ -2,7 +2,7 @@ import logging
2from enum import Enum2from enum import Enum
3from typing import Dict, Optional, Tuple, Type # noqa: F4013from typing import Dict, Optional, Tuple, Type # noqa: F401
44
5from uaclient import clouds, exceptions, status, util5from uaclient import clouds, exceptions, util
6from uaclient.config import apply_config_settings_override6from uaclient.config import apply_config_settings_override
77
8# Mapping of datasource names to cloud-id responses. Trusty compat with Xenial+8# Mapping of datasource names to cloud-id responses. Trusty compat with Xenial+
@@ -50,6 +50,15 @@ def get_cloud_type() -> Tuple[Optional[str], Optional[NoCloudTypeReason]]:
5050
5151
52def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:52def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:
53 """
54 :raises CloudFactoryError: if no cloud instance object can be constructed
55 :raises CloudFactoryNoCloudError: if no cloud instance object can be
56 constructed because we are not on a cloud
57 :raises CloudFactoryUnsupportedCloudError: if no cloud instance object can
58 be constructed because we don't have a class for the cloud we're on
59 :raises CloudFactoryNonViableCloudError: if no cloud instance object can be
60 constructed because we explicitly do not support the cloud we're on
61 """
53 from uaclient.clouds import aws, azure, gcp62 from uaclient.clouds import aws, azure, gcp
5463
55 cloud_instance_map = {64 cloud_instance_map = {
@@ -62,19 +71,11 @@ def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:
6271
63 cloud_type, _ = get_cloud_type()72 cloud_type, _ = get_cloud_type()
64 if not cloud_type:73 if not cloud_type:
65 raise exceptions.UserFacingError(74 raise exceptions.CloudFactoryNoCloudError(cloud_type)
66 status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
67 )
68 cls = cloud_instance_map.get(cloud_type)75 cls = cloud_instance_map.get(cloud_type)
69 if not cls:76 if not cls:
70 raise exceptions.NonAutoAttachImageError(77 raise exceptions.CloudFactoryUnsupportedCloudError(cloud_type)
71 status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
72 cloud_type=cloud_type
73 )
74 )
75 instance = cls()78 instance = cls()
76 if not instance.is_viable:79 if not instance.is_viable:
77 raise exceptions.UserFacingError(80 raise exceptions.CloudFactoryNonViableCloudError(cloud_type)
78 status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
79 )
80 return instance81 return instance
diff --git a/uaclient/clouds/tests/test_aws.py b/uaclient/clouds/tests/test_aws.py
index 68aa555..d319427 100644
--- a/uaclient/clouds/tests/test_aws.py
+++ b/uaclient/clouds/tests/test_aws.py
@@ -1,4 +1,5 @@
1import logging1import logging
2import re
2from io import BytesIO3from io import BytesIO
3from urllib.error import HTTPError4from urllib.error import HTTPError
45
@@ -9,8 +10,13 @@ from uaclient.clouds.aws import (
9 AWS_TOKEN_PUT_HEADER,10 AWS_TOKEN_PUT_HEADER,
10 AWS_TOKEN_REQ_HEADER,11 AWS_TOKEN_REQ_HEADER,
11 AWS_TOKEN_TTL_SECONDS,12 AWS_TOKEN_TTL_SECONDS,
13 IMDS_IPV4_ADDRESS,
14 IMDS_IPV6_ADDRESS,
15 IMDS_URL,
16 IMDS_V2_TOKEN_URL,
12 UAAutoAttachAWSInstance,17 UAAutoAttachAWSInstance,
13)18)
19from uaclient.exceptions import UserFacingError
1420
15M_PATH = "uaclient.clouds.aws."21M_PATH = "uaclient.clouds.aws."
1622
@@ -27,10 +33,12 @@ class TestUAAutoAttachAWSInstance:
27 "http://me", 404, "No IMDSv2 support", None, BytesIO()33 "http://me", 404, "No IMDSv2 support", None, BytesIO()
28 )34 )
29 instance = UAAutoAttachAWSInstance()35 instance = UAAutoAttachAWSInstance()
30 assert None is instance._get_imds_v2_token_headers()36 assert None is instance._get_imds_v2_token_headers(
37 ip_address=IMDS_IPV4_ADDRESS
38 )
31 assert "IMDSv1" == instance._api_token39 assert "IMDSv1" == instance._api_token
32 # No retries on 404. It is a permanent indication of no IMDSv2 support.40 # No retries on 404. It is a permanent indication of no IMDSv2 support.
33 instance._get_imds_v2_token_headers()41 instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
34 assert 1 == readurl.call_count42 assert 1 == readurl.call_count
3543
36 @mock.patch(M_PATH + "util.readurl")44 @mock.patch(M_PATH + "util.readurl")
@@ -41,14 +49,15 @@ class TestUAAutoAttachAWSInstance:
41 readurl.return_value = "somebase64token==", {"header": "stuff"}49 readurl.return_value = "somebase64token==", {"header": "stuff"}
42 assert {50 assert {
43 AWS_TOKEN_PUT_HEADER: "somebase64token=="51 AWS_TOKEN_PUT_HEADER: "somebase64token=="
44 } == instance._get_imds_v2_token_headers()52 } == instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
45 instance._get_imds_v2_token_headers()53 instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
46 assert "somebase64token==" == instance._api_token54 assert "somebase64token==" == instance._api_token
47 assert [55 assert [
48 mock.call(56 mock.call(
49 url,57 url,
50 method="PUT",58 method="PUT",
51 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},59 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
60 timeout=1,
52 )61 )
53 ] == readurl.call_args_list62 ] == readurl.call_args_list
5463
@@ -61,7 +70,7 @@ class TestUAAutoAttachAWSInstance:
61 ):70 ):
62 """Retry backoff before failing _get_imds_v2_token_headers."""71 """Retry backoff before failing _get_imds_v2_token_headers."""
6372
64 def fake_someurlerrors(url, method=None, headers=None):73 def fake_someurlerrors(url, method=None, headers=None, timeout=1):
65 if readurl.call_count <= fail_count:74 if readurl.call_count <= fail_count:
66 raise HTTPError(75 raise HTTPError(
67 "http://me",76 "http://me",
@@ -76,12 +85,16 @@ class TestUAAutoAttachAWSInstance:
76 instance = UAAutoAttachAWSInstance()85 instance = UAAutoAttachAWSInstance()
77 if exception:86 if exception:
78 with pytest.raises(HTTPError) as excinfo:87 with pytest.raises(HTTPError) as excinfo:
79 instance._get_imds_v2_token_headers()88 instance._get_imds_v2_token_headers(
89 ip_address=IMDS_IPV4_ADDRESS
90 )
80 assert 704 == excinfo.value.code91 assert 704 == excinfo.value.code
81 else:92 else:
82 assert {93 assert {
83 AWS_TOKEN_PUT_HEADER: "base64token=="94 AWS_TOKEN_PUT_HEADER: "base64token=="
84 } == instance._get_imds_v2_token_headers()95 } == instance._get_imds_v2_token_headers(
96 ip_address=IMDS_IPV4_ADDRESS
97 )
8598
86 expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)]99 expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)]
87 assert expected_sleep_calls == sleep.call_args_list100 assert expected_sleep_calls == sleep.call_args_list
@@ -107,12 +120,15 @@ class TestUAAutoAttachAWSInstance:
107 token_url,120 token_url,
108 method="PUT",121 method="PUT",
109 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},122 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
123 timeout=1,
124 ),
125 mock.call(
126 url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}, timeout=1
110 ),127 ),
111 mock.call(url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}),
112 ] == readurl.call_args_list128 ] == readurl.call_args_list
113129
114 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)130 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
115 @pytest.mark.parametrize("fail_count,exception", ((3, False), (4, True)))131 @pytest.mark.parametrize("fail_count,exception", ((3, False),))
116 @mock.patch(M_PATH + "util.time.sleep")132 @mock.patch(M_PATH + "util.time.sleep")
117 @mock.patch(M_PATH + "util.readurl")133 @mock.patch(M_PATH + "util.readurl")
118 def test_retry_backoff_on_failed_identity_doc(134 def test_retry_backoff_on_failed_identity_doc(
@@ -120,7 +136,7 @@ class TestUAAutoAttachAWSInstance:
120 ):136 ):
121 """Retry backoff is attempted before failing to get AWS.identity_doc"""137 """Retry backoff is attempted before failing to get AWS.identity_doc"""
122138
123 def fake_someurlerrors(url, method=None, headers=None):139 def fake_someurlerrors(url, method=None, headers=None, timeout=1):
124 # due to _get_imds_v2_token_headers140 # due to _get_imds_v2_token_headers
125 if "latest/api/token" in url:141 if "latest/api/token" in url:
126 return "base64token==", {"header": "stuff"}142 return "base64token==", {"header": "stuff"}
@@ -195,3 +211,92 @@ class TestUAAutoAttachAWSInstance:
195 load_file.side_effect = fake_load_file211 load_file.side_effect = fake_load_file
196 instance = UAAutoAttachAWSInstance()212 instance = UAAutoAttachAWSInstance()
197 assert viable is instance.is_viable213 assert viable is instance.is_viable
214
215 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
216 @mock.patch(M_PATH + "util.readurl")
217 def test_identity_doc_default_to_ipv6_if_ipv4_fail(
218 self, readurl, caplog_text
219 ):
220 instance = UAAutoAttachAWSInstance()
221 ipv4_address = IMDS_IPV4_ADDRESS
222 ipv6_address = IMDS_IPV6_ADDRESS
223
224 def fake_someurlerrors(url, method=None, headers=None, timeout=1):
225 if ipv4_address in url:
226 raise Exception("IPv4 exception")
227
228 if url == IMDS_V2_TOKEN_URL.format(ipv6_address):
229 return "base64token==", {"header": "stuff"}
230
231 if url == IMDS_URL.format(ipv6_address):
232 return "pkcs7WOOT!==", {"header": "stuff"}
233
234 readurl.side_effect = fake_someurlerrors
235 assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc
236 expected = [
237 mock.call(
238 IMDS_V2_TOKEN_URL.format(ipv4_address),
239 method="PUT",
240 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
241 timeout=1,
242 ),
243 mock.call(
244 IMDS_V2_TOKEN_URL.format(ipv6_address),
245 method="PUT",
246 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
247 timeout=1,
248 ),
249 mock.call(
250 IMDS_URL.format(ipv6_address),
251 headers={AWS_TOKEN_PUT_HEADER: "base64token=="},
252 timeout=1,
253 ),
254 ]
255
256 assert expected == readurl.call_args_list
257
258 expected_log = "Could not reach AWS IMDS at http://169.254.169.254:"
259 assert expected_log in caplog_text()
260
261 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
262 @mock.patch(M_PATH + "util.readurl")
263 def test_identity_doc_logs_error_if_both_ipv4_and_ipv6_fails(
264 self, readurl, caplog_text
265 ):
266
267 instance = UAAutoAttachAWSInstance()
268 ipv4_address = IMDS_IPV4_ADDRESS
269 ipv6_address = IMDS_IPV6_ADDRESS
270
271 readurl.side_effect = Exception("Exception")
272
273 expected_error = (
274 "No valid AWS IMDS endpoint discovered at "
275 "addresses: {}, {}".format(IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
276 )
277 with pytest.raises(UserFacingError, match=re.escape(expected_error)):
278 instance.identity_doc
279
280 expected = [
281 mock.call(
282 IMDS_V2_TOKEN_URL.format(ipv4_address),
283 method="PUT",
284 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
285 timeout=1,
286 ),
287 mock.call(
288 IMDS_V2_TOKEN_URL.format(ipv6_address),
289 method="PUT",
290 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
291 timeout=1,
292 ),
293 ]
294 assert expected == readurl.call_args_list
295
296 expected_logs = [
297 "Could not reach AWS IMDS at http://169.254.169.254:",
298 "Could not reach AWS IMDS at http://[fd00:ec2::254]:",
299 ]
300
301 for expected_log in expected_logs:
302 assert expected_log in caplog_text()
diff --git a/uaclient/clouds/tests/test_identity.py b/uaclient/clouds/tests/test_identity.py
index 79faf0d..f223f80 100644
--- a/uaclient/clouds/tests/test_identity.py
+++ b/uaclient/clouds/tests/test_identity.py
@@ -1,13 +1,17 @@
1import mock1import mock
2import pytest2import pytest
33
4from uaclient import exceptions, status
5from uaclient.clouds.identity import (4from uaclient.clouds.identity import (
6 NoCloudTypeReason,5 NoCloudTypeReason,
7 cloud_instance_factory,6 cloud_instance_factory,
8 get_cloud_type,7 get_cloud_type,
9 get_instance_id,8 get_instance_id,
10)9)
10from uaclient.exceptions import (
11 CloudFactoryNoCloudError,
12 CloudFactoryNonViableCloudError,
13 CloudFactoryUnsupportedCloudError,
14)
11from uaclient.util import ProcessExecutionError15from uaclient.util import ProcessExecutionError
1216
13M_PATH = "uaclient.clouds.identity."17M_PATH = "uaclient.clouds.identity."
@@ -89,22 +93,15 @@ class TestCloudInstanceFactory:
89 None,93 None,
90 NoCloudTypeReason.NO_CLOUD_DETECTED,94 NoCloudTypeReason.NO_CLOUD_DETECTED,
91 )95 )
92 with pytest.raises(exceptions.UserFacingError) as excinfo:96 with pytest.raises(CloudFactoryNoCloudError):
93 cloud_instance_factory()97 cloud_instance_factory()
94 assert 1 == m_get_cloud_type.call_count98 assert 1 == m_get_cloud_type.call_count
95 assert status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE == str(
96 excinfo.value
97 )
9899
99 def test_raise_error_when_not_aws_or_azure(self, m_get_cloud_type):100 def test_raise_error_when_not_supported(self, m_get_cloud_type):
100 """Raise appropriate error when unable to determine cloud_type."""101 """Raise appropriate error when unable to determine cloud_type."""
101 m_get_cloud_type.return_value = ("unsupported-cloud", None)102 m_get_cloud_type.return_value = ("unsupported-cloud", None)
102 with pytest.raises(exceptions.UserFacingError) as excinfo:103 with pytest.raises(CloudFactoryUnsupportedCloudError):
103 cloud_instance_factory()104 cloud_instance_factory()
104 error_msg = status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
105 cloud_type="unsupported-cloud"
106 )
107 assert error_msg == str(excinfo.value)
108105
109 @pytest.mark.parametrize("cloud_type", ("aws", "azure"))106 @pytest.mark.parametrize("cloud_type", ("aws", "azure"))
110 def test_raise_error_when_not_viable_for_ubuntu_pro(107 def test_raise_error_when_not_viable_for_ubuntu_pro(
@@ -125,10 +122,8 @@ class TestCloudInstanceFactory:
125122
126 with mock.patch(M_INSTANCE_PATH) as m_instance:123 with mock.patch(M_INSTANCE_PATH) as m_instance:
127 m_instance.side_effect = fake_invalid_instance124 m_instance.side_effect = fake_invalid_instance
128 with pytest.raises(exceptions.UserFacingError) as excinfo:125 with pytest.raises(CloudFactoryNonViableCloudError):
129 cloud_instance_factory()126 cloud_instance_factory()
130 error_msg = status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
131 assert error_msg == str(excinfo.value)
132127
133 @pytest.mark.parametrize(128 @pytest.mark.parametrize(
134 "cloud_type", ("aws", "aws-gov", "aws-china", "azure")129 "cloud_type", ("aws", "aws-gov", "aws-china", "azure")
diff --git a/uaclient/config.py b/uaclient/config.py
index 6275839..f66322e 100644
--- a/uaclient/config.py
+++ b/uaclient/config.py
@@ -7,7 +7,7 @@ import sys
7from collections import OrderedDict, namedtuple7from collections import OrderedDict, namedtuple
8from datetime import datetime8from datetime import datetime
9from functools import wraps9from functools import wraps
10from typing import Any, Dict, Optional, Tuple, cast10from typing import Any, Dict, List, Optional, Tuple, cast
1111
12import yaml12import yaml
1313
@@ -47,6 +47,7 @@ DEFAULT_STATUS = {
47 "created_at": "",47 "created_at": "",
48 "external_account_ids": [],48 "external_account_ids": [],
49 },49 },
50 "simulated": False,
50} # type: Dict[str, Any]51} # type: Dict[str, Any]
5152
52LOG = logging.getLogger(__name__)53LOG = logging.getLogger(__name__)
@@ -326,13 +327,19 @@ class UAConfig:
326 return {}327 return {}
327328
328 self._entitlements = {}329 self._entitlements = {}
329 contractInfo = machine_token["machineTokenInfo"]["contractInfo"]330 contractInfo = machine_token.get("machineTokenInfo", {}).get(
331 "contractInfo"
332 )
333 if not contractInfo:
334 return {}
335
330 tokens_by_name = dict(336 tokens_by_name = dict(
331 (e["type"], e["token"])337 (e.get("type"), e.get("token"))
332 for e in machine_token.get("resourceTokens", [])338 for e in machine_token.get("resourceTokens", [])
333 )339 )
334 ent_by_name = dict(340 ent_by_name = dict(
335 (e["type"], e) for e in contractInfo["resourceEntitlements"]341 (e.get("type"), e)
342 for e in contractInfo.get("resourceEntitlements", [])
336 )343 )
337 for entitlement_name, ent_value in ent_by_name.items():344 for entitlement_name, ent_value in ent_by_name.items():
338 entitlement_cfg = {"entitlement": ent_value}345 entitlement_cfg = {"entitlement": ent_value}
@@ -551,16 +558,23 @@ class UAConfig:
551 mode = 0o644558 mode = 0o644
552 util.write_file(filepath, content, mode=mode)559 util.write_file(filepath, content, mode=mode)
553560
554 def _remove_beta_resources(self, response) -> Dict[str, Any]:561 def _handle_beta_resources(self, show_beta, response) -> Dict[str, Any]:
555 """Remove beta services from response dict"""562 """Remove beta services from response dict if needed"""
556 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME563 from uaclient.entitlements import entitlement_factory
564
565 config_allow_beta = util.is_config_value_true(
566 config=self.cfg, path_to_value="features.allow_beta"
567 )
568 show_beta |= config_allow_beta
569 if show_beta:
570 return response
557571
558 new_response = copy.deepcopy(response)572 new_response = copy.deepcopy(response)
559573
560 released_resources = []574 released_resources = []
561 for resource in new_response.get("services", {}):575 for resource in new_response.get("services", {}):
562 resource_name = resource["name"]576 resource_name = resource["name"]
563 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(resource_name)577 ent_cls = entitlement_factory(resource_name)
564578
565 if ent_cls is None:579 if ent_cls is None:
566 """580 """
@@ -624,34 +638,36 @@ class UAConfig:
624 def _unattached_status(self) -> Dict[str, Any]:638 def _unattached_status(self) -> Dict[str, Any]:
625 """Return unattached status as a dict."""639 """Return unattached status as a dict."""
626 from uaclient.contract import get_available_resources640 from uaclient.contract import get_available_resources
627 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME641 from uaclient.entitlements import entitlement_factory
628642
629 response = copy.deepcopy(DEFAULT_STATUS)643 response = copy.deepcopy(DEFAULT_STATUS)
630 response["version"] = version.get_version(features=self.features)644 response["version"] = version.get_version(features=self.features)
631645
632 resources = get_available_resources(self)646 resources = get_available_resources(self)
633 for resource in sorted(resources, key=lambda x: x["name"]):647 for resource in resources:
634 if resource["available"]:648 if resource.get("available"):
635 available = status.UserFacingAvailability.AVAILABLE.value649 available = status.UserFacingAvailability.AVAILABLE.value
636 else:650 else:
637 available = status.UserFacingAvailability.UNAVAILABLE.value651 available = status.UserFacingAvailability.UNAVAILABLE.value
638 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(resource["name"])652 ent_cls = entitlement_factory(resource.get("name", ""))
639653
640 if not ent_cls:654 if not ent_cls:
641 LOG.debug(655 LOG.debug(
642 "Ignoring availability of unknown service %s"656 "Ignoring availability of unknown service %s"
643 " from contract server",657 " from contract server",
644 resource["name"],658 resource.get("name", "without a 'name' key"),
645 )659 )
646 continue660 continue
647661
648 response["services"].append(662 response["services"].append(
649 {663 {
650 "name": resource["name"],664 "name": resource.get("presentedAs", resource["name"]),
651 "description": ent_cls.description,665 "description": ent_cls.description,
652 "available": available,666 "available": available,
653 }667 }
654 )668 )
669 response["services"].sort(key=lambda x: x.get("name", ""))
670
655 return response671 return response
656672
657 def _attached_service_status(673 def _attached_service_status(
@@ -670,7 +686,7 @@ class UAConfig:
670 ent_status, details = ent.user_facing_status()686 ent_status, details = ent.user_facing_status()
671687
672 return {688 return {
673 "name": ent.name,689 "name": ent.presentation_name,
674 "description": ent.description,690 "description": ent.description,
675 "entitled": contract_status.value,691 "entitled": contract_status.value,
676 "status": ent_status.value,692 "status": ent_status.value,
@@ -684,7 +700,7 @@ class UAConfig:
684 def _attached_status(self) -> Dict[str, Any]:700 def _attached_status(self) -> Dict[str, Any]:
685 """Return configuration of attached status as a dictionary."""701 """Return configuration of attached status as a dictionary."""
686 from uaclient.contract import get_available_resources702 from uaclient.contract import get_available_resources
687 from uaclient.entitlements import ENTITLEMENT_CLASSES703 from uaclient.entitlements import entitlement_factory
688704
689 response = copy.deepcopy(DEFAULT_STATUS)705 response = copy.deepcopy(DEFAULT_STATUS)
690 machineTokenInfo = self.machine_token["machineTokenInfo"]706 machineTokenInfo = self.machine_token["machineTokenInfo"]
@@ -725,15 +741,18 @@ class UAConfig:
725741
726 inapplicable_resources = {742 inapplicable_resources = {
727 resource["name"]: resource.get("description")743 resource["name"]: resource.get("description")
728 for resource in sorted(resources, key=lambda x: x["name"])744 for resource in sorted(resources, key=lambda x: x.get("name", ""))
729 if not resource["available"]745 if not resource.get("available")
730 }746 }
731747
732 for ent_cls in ENTITLEMENT_CLASSES:748 for resource in resources:
733 ent = ent_cls(self)749 ent_cls = entitlement_factory(resource.get("name", ""))
734 response["services"].append(750 if ent_cls:
735 self._attached_service_status(ent, inapplicable_resources)751 ent = ent_cls(self)
736 )752 response["services"].append(
753 self._attached_service_status(ent, inapplicable_resources)
754 )
755 response["services"].sort(key=lambda x: x.get("name", ""))
737756
738 support = self.entitlements.get("support", {}).get("entitlement")757 support = self.entitlements.get("support", {}).get("entitlement")
739 if support:758 if support:
@@ -742,7 +761,115 @@ class UAConfig:
742 response["contract"]["tech_support_level"] = supportLevel761 response["contract"]["tech_support_level"] = supportLevel
743 return response762 return response
744763
745 def status(self, show_beta=False) -> Dict[str, Any]:764 def _get_entitlement_information(
765 self, entitlements: List[Dict[str, Any]], entitlement_name: str
766 ) -> Dict[str, Any]:
767 """Extract information from the entitlements array."""
768 for entitlement in entitlements:
769 if entitlement.get("type") == entitlement_name:
770 return {
771 "entitled": "yes" if entitlement.get("entitled") else "no",
772 "auto_enabled": "yes"
773 if entitlement.get("obligations", {}).get(
774 "enableByDefault"
775 )
776 else "no",
777 "affordances": entitlement.get("affordances", {}),
778 }
779 return {"entitled": "no", "auto_enabled": "no", "affordances": {}}
780
781 def simulate_status(
782 self, token: str, show_beta: bool = False
783 ) -> Dict[str, Any]:
784 """Return a status dictionary based on a token."""
785 from uaclient.contract import (
786 get_available_resources,
787 get_contract_information,
788 )
789 from uaclient.entitlements import entitlement_factory
790
791 response = copy.deepcopy(DEFAULT_STATUS)
792
793 contract_information = get_contract_information(self, token)
794
795 contract_info = contract_information.get("contractInfo", {})
796 account_info = contract_information.get("accountInfo", {})
797
798 response.update(
799 {
800 "version": version.get_version(features=self.features),
801 "contract": {
802 "id": contract_info.get("id", ""),
803 "name": contract_info.get("name", ""),
804 "created_at": contract_info.get("createdAt", ""),
805 "products": contract_info.get("products", []),
806 },
807 "account": {
808 "name": account_info.get("name", ""),
809 "id": account_info.get("id"),
810 "created_at": account_info.get("createdAt", ""),
811 "external_account_ids": account_info.get(
812 "externalAccountIDs", []
813 ),
814 },
815 "simulated": True,
816 }
817 )
818
819 if contract_info.get("effectiveTo"):
820 response["expires"] = contract_info.get("effectiveTo")
821 if contract_info.get("effectiveFrom"):
822 response["effective"] = contract_info.get("effectiveFrom")
823
824 status_cache = self.read_cache("status-cache")
825 if status_cache:
826 resources = status_cache.get("services")
827 else:
828 resources = get_available_resources(self)
829
830 entitlements = contract_info.get("resourceEntitlements", [])
831
832 inapplicable_resources = [
833 resource["name"]
834 for resource in sorted(resources, key=lambda x: x["name"])
835 if not resource["available"]
836 ]
837
838 for resource in resources:
839 entitlement_name = resource.get("name", "")
840 ent_cls = entitlement_factory(entitlement_name)
841 if ent_cls:
842 ent = ent_cls(self)
843 entitlement_information = self._get_entitlement_information(
844 entitlements, entitlement_name
845 )
846 response["services"].append(
847 {
848 "name": resource.get("presentedAs", ent.name),
849 "description": ent.description,
850 "entitled": entitlement_information["entitled"],
851 "auto_enabled": entitlement_information[
852 "auto_enabled"
853 ],
854 "available": "yes"
855 if ent.name not in inapplicable_resources
856 else "no",
857 }
858 )
859 response["services"].sort(key=lambda x: x.get("name", ""))
860
861 support = self._get_entitlement_information(entitlements, "support")
862 if support["entitled"]:
863 supportLevel = support["affordances"].get("supportLevel")
864 if supportLevel:
865 response["contract"]["tech_support_level"] = supportLevel
866
867 response.update(self._get_config_status())
868 response = self._handle_beta_resources(show_beta, response)
869
870 return response
871
872 def status(self, show_beta: bool = False) -> Dict[str, Any]:
746 """Return status as a dict, using a cache for non-root users873 """Return status as a dict, using a cache for non-root users
747874
748 When unattached, get available resources from the contract service875 When unattached, get available resources from the contract service
@@ -751,7 +878,6 @@ class UAConfig:
751878
752 Write the status-cache when called by root.879 Write the status-cache when called by root.
753 """880 """
754
755 if os.getuid() != 0:881 if os.getuid() != 0:
756 response = cast("Dict[str, Any]", self.read_cache("status-cache"))882 response = cast("Dict[str, Any]", self.read_cache("status-cache"))
757 if not response:883 if not response:
@@ -760,7 +886,9 @@ class UAConfig:
760 response = self._unattached_status()886 response = self._unattached_status()
761 else:887 else:
762 response = self._attached_status()888 response = self._attached_status()
889
763 response.update(self._get_config_status())890 response.update(self._get_config_status())
891
764 if os.getuid() == 0:892 if os.getuid() == 0:
765 self.write_cache("status-cache", response)893 self.write_cache("status-cache", response)
766894
@@ -773,12 +901,7 @@ class UAConfig:
773 ),901 ),
774 )902 )
775903
776 config_allow_beta = util.is_config_value_true(904 response = self._handle_beta_resources(show_beta, response)
777 config=self.cfg, path_to_value="features.allow_beta"
778 )
779 show_beta |= config_allow_beta
780 if not show_beta:
781 response = self._remove_beta_resources(response)
782905
783 return response906 return response
784907
@@ -790,7 +913,7 @@ class UAConfig:
790 :raises: UserFacingError when no help is available.913 :raises: UserFacingError when no help is available.
791 """914 """
792 from uaclient.contract import get_available_resources915 from uaclient.contract import get_available_resources
793 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME916 from uaclient.entitlements import entitlement_factory
794917
795 resources = get_available_resources(self)918 resources = get_available_resources(self)
796 help_resource = None919 help_resource = None
@@ -802,11 +925,12 @@ class UAConfig:
802 response_dict["name"] = name925 response_dict["name"] = name
803926
804 for resource in resources:927 for resource in resources:
805 if resource["name"] == name and name in ENTITLEMENT_CLASS_BY_NAME:928 if resource["name"] == name or resource.get("presentedAs") == name:
806 help_resource = resource929 help_ent_cls = entitlement_factory(resource["name"])
807 help_ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(name)930 if help_ent_cls:
808 help_ent = help_ent_cls(self)931 help_resource = resource
809 break932 help_ent = help_ent_cls(self)
933 break
810934
811 if help_resource is None:935 if help_resource is None:
812 raise exceptions.UserFacingError(936 raise exceptions.UserFacingError(
diff --git a/uaclient/contract.py b/uaclient/contract.py
index 0390bf4..f29c5d4 100644
--- a/uaclient/contract.py
+++ b/uaclient/contract.py
@@ -2,6 +2,7 @@ import logging
2from typing import Any, Dict, List, Optional2from typing import Any, Dict, List, Optional
33
4from uaclient import clouds, exceptions, serviceclient, status, util4from uaclient import clouds, exceptions, serviceclient, status, util
5from uaclient.config import UAConfig
56
6API_V1_CONTEXT_MACHINE_TOKEN = "/v1/context/machines/token"7API_V1_CONTEXT_MACHINE_TOKEN = "/v1/context/machines/token"
7API_V1_TMPL_CONTEXT_MACHINE_TOKEN_RESOURCE = (8API_V1_TMPL_CONTEXT_MACHINE_TOKEN_RESOURCE = (
@@ -13,6 +14,7 @@ API_V1_TMPL_RESOURCE_MACHINE_ACCESS = (
13)14)
14API_V1_AUTO_ATTACH_CLOUD_TOKEN = "/v1/clouds/{cloud_type}/token"15API_V1_AUTO_ATTACH_CLOUD_TOKEN = "/v1/clouds/{cloud_type}/token"
15API_V1_MACHINE_ACTIVITY = "/v1/contracts/{contract}/machine-activity/{machine}"16API_V1_MACHINE_ACTIVITY = "/v1/contracts/{contract}/machine-activity/{machine}"
17API_V1_CONTRACT_INFORMATION = "/v1/contract"
16ATTACH_FAIL_DATE_FORMAT = "%B %d, %Y"18ATTACH_FAIL_DATE_FORMAT = "%B %d, %Y"
1719
1820
@@ -100,6 +102,16 @@ class UAContractClient(serviceclient.UAServiceClient):
100 )102 )
101 return resource_response103 return resource_response
102104
105 def request_contract_information(
106 self, contract_token: str
107 ) -> Dict[str, Any]:
108 headers = self.headers()
109 headers.update({"Authorization": "Bearer {}".format(contract_token)})
110 response_data, _response_headers = self.request_url(
111 API_V1_CONTRACT_INFORMATION, headers=headers
112 )
113 return response_data
114
103 def request_auto_attach_contract_token(115 def request_auto_attach_contract_token(
104 self, *, instance: clouds.AutoAttachCloudInstance116 self, *, instance: clouds.AutoAttachCloudInstance
105 ):117 ):
@@ -355,7 +367,7 @@ def process_entitlement_delta(
355 :raise UserFacingError: on failure to process deltas.367 :raise UserFacingError: on failure to process deltas.
356 :return: Dict of processed deltas368 :return: Dict of processed deltas
357 """369 """
358 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME370 from uaclient.entitlements import entitlement_factory
359371
360 if series_overrides:372 if series_overrides:
361 util.apply_series_overrides(new_access)373 util.apply_series_overrides(new_access)
@@ -371,9 +383,8 @@ def process_entitlement_delta(
371 orig_access, new_access383 orig_access, new_access
372 )384 )
373 )385 )
374 try:386 ent_cls = entitlement_factory(name)
375 ent_cls = ENTITLEMENT_CLASS_BY_NAME[name]387 if not ent_cls:
376 except KeyError:
377 logging.debug(388 logging.debug(
378 'Skipping entitlement deltas for "%s". No such class', name389 'Skipping entitlement deltas for "%s". No such class', name
379 )390 )
@@ -469,8 +480,14 @@ def request_updated_contract(
469 )480 )
470481
471482
472def get_available_resources(cfg) -> List[Dict]:483def get_available_resources(cfg: UAConfig) -> List[Dict]:
473 """Query available resources from the contrct server for this machine."""484 """Query available resources from the contract server for this machine."""
474 client = UAContractClient(cfg)485 client = UAContractClient(cfg)
475 resources = client.request_resources()486 resources = client.request_resources()
476 return resources.get("resources", [])487 return resources.get("resources", [])
488
489
490def get_contract_information(cfg: UAConfig, token: str) -> Dict[str, Any]:
491 """Query contract information for a specific token"""
492 client = UAContractClient(cfg)
493 return client.request_contract_information(token)
diff --git a/uaclient/defaults.py b/uaclient/defaults.py
index ee218de..e556abc 100644
--- a/uaclient/defaults.py
+++ b/uaclient/defaults.py
@@ -16,6 +16,7 @@ BASE_SECURITY_URL = "https://ubuntu.com/security"
16BASE_UA_URL = "https://ubuntu.com/advantage"16BASE_UA_URL = "https://ubuntu.com/advantage"
17EOL_UA_URL_TMPL = "https://ubuntu.com/{hyphenatedrelease}"17EOL_UA_URL_TMPL = "https://ubuntu.com/{hyphenatedrelease}"
18BASE_ESM_URL = "https://ubuntu.com/esm"18BASE_ESM_URL = "https://ubuntu.com/esm"
19CLOUD_BUILD_INFO = "/etc/cloud/build.info"
19DOCUMENTATION_URL = (20DOCUMENTATION_URL = (
20 "https://discourse.ubuntu.com/t/ubuntu-advantage-client/21788"21 "https://discourse.ubuntu.com/t/ubuntu-advantage-client/21788"
21)22)
diff --git a/uaclient/entitlements/__init__.py b/uaclient/entitlements/__init__.py
index d8cc495..18b0b3d 100644
--- a/uaclient/entitlements/__init__.py
+++ b/uaclient/entitlements/__init__.py
@@ -1,4 +1,4 @@
1from typing import Dict, List, Type, cast # noqa: F4011from typing import List, Type # noqa: F401
22
3from uaclient.config import UAConfig3from uaclient.config import UAConfig
4from uaclient.entitlements import fips4from uaclient.entitlements import fips
@@ -23,27 +23,46 @@ ENTITLEMENT_CLASSES = [
23] # type: List[Type[UAEntitlement]]23] # type: List[Type[UAEntitlement]]
2424
2525
26ENTITLEMENT_CLASS_BY_NAME = dict(26def entitlement_factory(name: str):
27 (cast(str, cls.name), cls) for cls in ENTITLEMENT_CLASSES27 """Returns a UAEntitlement class based on the provided name.
28) # type: Dict[str, Type[UAEntitlement]]28
29 The return type is Optional[Type[UAEntitlement]].
30 It cannot be explicit because of the Python version on Xenial (3.5.2).
31 """
32 for entitlement in ENTITLEMENT_CLASSES:
33 if name in entitlement().valid_names:
34 return entitlement
35 return None
2936
3037
31def valid_services(allow_beta: bool = False) -> List[str]:38def valid_services(
39 allow_beta: bool = False, all_names: bool = False
40) -> List[str]:
32 """Return a list of valid (non-beta) services.41 """Return a list of valid (non-beta) services.
3342
34 @param allow_beta: if we should allow beta services to be marked as valid43 @param allow_beta: if we should allow beta services to be marked as valid
44 @param all_names: if we should return all the names for a service instead
45 of just the presentation_name
35 """46 """
36 cfg = UAConfig()47 cfg = UAConfig()
37 allow_beta_cfg = is_config_value_true(cfg.cfg, "features.allow_beta")48 allow_beta_cfg = is_config_value_true(cfg.cfg, "features.allow_beta")
38 allow_beta |= allow_beta_cfg49 allow_beta |= allow_beta_cfg
3950
40 if allow_beta:51 entitlements = ENTITLEMENT_CLASSES
41 return sorted(ENTITLEMENT_CLASS_BY_NAME.keys())52 if not allow_beta:
53 entitlements = [
54 entitlement
55 for entitlement in entitlements
56 if not entitlement.is_beta
57 ]
58
59 if all_names:
60 names = []
61 for entitlement in entitlements:
62 names.extend(entitlement().valid_names)
63
64 return sorted(names)
4265
43 return sorted(66 return sorted(
44 [67 [entitlement().presentation_name for entitlement in entitlements]
45 ent_name
46 for ent_name, ent_cls in ENTITLEMENT_CLASS_BY_NAME.items()
47 if not ent_cls.is_beta
48 ]
49 )68 )
diff --git a/uaclient/entitlements/base.py b/uaclient/entitlements/base.py
index 4bbe943..42376d4 100644
--- a/uaclient/entitlements/base.py
+++ b/uaclient/entitlements/base.py
@@ -58,6 +58,14 @@ class UAEntitlement(metaclass=abc.ABCMeta):
58 pass58 pass
5959
60 @property60 @property
61 def valid_names(self) -> List[str]:
62 """The list of names this entitlement may be called."""
63 valid_names = [self.name]
64 if self.presentation_name != self.name:
65 valid_names.append(self.presentation_name)
66 return valid_names
67
68 @property
61 @abc.abstractmethod69 @abc.abstractmethod
62 def title(self) -> str:70 def title(self) -> str:
63 """The human readable title of this entitlement"""71 """The human readable title of this entitlement"""
@@ -70,6 +78,16 @@ class UAEntitlement(metaclass=abc.ABCMeta):
70 pass78 pass
7179
72 @property80 @property
81 def presentation_name(self) -> str:
82 """The user-facing name shown for this entitlement"""
83 return (
84 self.cfg.entitlements.get(self.name, {})
85 .get("entitlement", {})
86 .get("affordances", {})
87 .get("presentedAs", self.name)
88 )
89
90 @property
73 def help_info(self) -> str:91 def help_info(self) -> str:
74 """Help information for the entitlement"""92 """Help information for the entitlement"""
75 if self._help_info is None:93 if self._help_info is None:
@@ -132,6 +150,7 @@ class UAEntitlement(metaclass=abc.ABCMeta):
132 cfg: Optional[config.UAConfig] = None,150 cfg: Optional[config.UAConfig] = None,
133 assume_yes: bool = False,151 assume_yes: bool = False,
134 allow_beta: bool = False,152 allow_beta: bool = False,
153 called_name: str = "",
135 ) -> None:154 ) -> None:
136 """Setup UAEntitlement instance155 """Setup UAEntitlement instance
137156
@@ -142,6 +161,7 @@ class UAEntitlement(metaclass=abc.ABCMeta):
142 self.cfg = cfg161 self.cfg = cfg
143 self.assume_yes = assume_yes162 self.assume_yes = assume_yes
144 self.allow_beta = allow_beta163 self.allow_beta = allow_beta
164 self._called_name = called_name
145 self._valid_service = None165 self._valid_service = None
146166
147 @property167 @property
@@ -310,10 +330,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
310 True if all required services are active330 True if all required services are active
311 False is at least one of the required services is disabled331 False is at least one of the required services is disabled
312 """332 """
313 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME333 from uaclient.entitlements import entitlement_factory
314334
315 for required_service in self.required_services:335 for required_service in self.required_services:
316 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(required_service)336 ent_cls = entitlement_factory(required_service)
317 if ent_cls:337 if ent_cls:
318 ent_status, _ = ent_cls(self.cfg).application_status()338 ent_status, _ = ent_cls(self.cfg).application_status()
319 if ent_status != status.ApplicationStatus.ENABLED:339 if ent_status != status.ApplicationStatus.ENABLED:
@@ -329,10 +349,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
329 True if there are incompatible services enabled349 True if there are incompatible services enabled
330 False if there are no incompatible services enabled350 False if there are no incompatible services enabled
331 """351 """
332 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME352 from uaclient.entitlements import entitlement_factory
333353
334 for incompatible_service in self.incompatible_services:354 for incompatible_service in self.incompatible_services:
335 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(incompatible_service)355 ent_cls = entitlement_factory(incompatible_service)
336 if ent_cls:356 if ent_cls:
337 ent_status, _ = ent_cls(self.cfg).application_status()357 ent_status, _ = ent_cls(self.cfg).application_status()
338 if ent_status == status.ApplicationStatus.ENABLED:358 if ent_status == status.ApplicationStatus.ENABLED:
@@ -355,14 +375,14 @@ class UAEntitlement(metaclass=abc.ABCMeta):
355 features:375 features:
356 block_disable_on_enable: true376 block_disable_on_enable: true
357 """377 """
358 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME378 from uaclient.entitlements import entitlement_factory
359379
360 cfg_block_disable_on_enable = util.is_config_value_true(380 cfg_block_disable_on_enable = util.is_config_value_true(
361 config=self.cfg.cfg,381 config=self.cfg.cfg,
362 path_to_value="features.block_disable_on_enable",382 path_to_value="features.block_disable_on_enable",
363 )383 )
364 for incompatible_service in self.incompatible_services:384 for incompatible_service in self.incompatible_services:
365 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(incompatible_service)385 ent_cls = entitlement_factory(incompatible_service)
366386
367 if ent_cls:387 if ent_cls:
368 ent = ent_cls(self.cfg)388 ent = ent_cls(self.cfg)
@@ -412,10 +432,15 @@ class UAEntitlement(metaclass=abc.ABCMeta):
412 that must be enabled first. In that situation, we can ask the user432 that must be enabled first. In that situation, we can ask the user
413 if the required service should be enabled before proceeding.433 if the required service should be enabled before proceeding.
414 """434 """
415 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME435 from uaclient.entitlements import entitlement_factory
416436
417 for required_service in self.required_services:437 for required_service in self.required_services:
418 ent_cls = ENTITLEMENT_CLASS_BY_NAME[required_service]438 ent_cls = entitlement_factory(required_service)
439 if not ent_cls:
440 msg = "Required service {} not found.".format(required_service)
441 logging.error(msg)
442 return False
443
419 ent = ent_cls(self.cfg, allow_beta=True)444 ent = ent_cls(self.cfg, allow_beta=True)
420445
421 is_service_disabled = (446 is_service_disabled = (
@@ -555,10 +580,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
555 and prompt for confirmation to disable these services580 and prompt for confirmation to disable these services
556 as well.581 as well.
557 """582 """
558 from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME583 from uaclient.entitlements import entitlement_factory
559584
560 for dependent_service in self.dependent_services:585 for dependent_service in self.dependent_services:
561 ent_cls = ENTITLEMENT_CLASS_BY_NAME[dependent_service]586 ent_cls = entitlement_factory(dependent_service)
562 ent = ent_cls(self.cfg)587 ent = ent_cls(self.cfg)
563588
564 is_service_enabled = (589 is_service_enabled = (
diff --git a/uaclient/entitlements/cis.py b/uaclient/entitlements/cis.py
index 90d06b8..384bfd3 100644
--- a/uaclient/entitlements/cis.py
+++ b/uaclient/entitlements/cis.py
@@ -2,22 +2,47 @@ from typing import Callable, Dict, List, Tuple, Union
22
3from uaclient.entitlements import repo3from uaclient.entitlements import repo
44
5CIS_DOCS_URL = "https://security-certs.docs.ubuntu.com/en/cis"5CIS_DOCS_URL = "https://ubuntu.com/security/cis"
6USG_DOCS_URL = "https://ubuntu.com/security/certifications/docs/usg"
67
78
8class CISEntitlement(repo.RepoEntitlement):9class CISEntitlement(repo.RepoEntitlement):
910
10 help_doc_url = "https://ubuntu.com/security/certifications#cis"11 help_doc_url = USG_DOCS_URL
11 name = "cis"12 name = "cis"
12 title = "CIS Audit"13 description = "Security compliance and audit tools"
13 description = "Center for Internet Security Audit Tools"
14 repo_key_file = "ubuntu-advantage-cis.gpg"14 repo_key_file = "ubuntu-advantage-cis.gpg"
15 apt_noninteractive = True15 apt_noninteractive = True
1616
17 @property17 @property
18 def messaging(self,) -> Dict[str, List[Union[str, Tuple[Callable, Dict]]]]:18 def messaging(self,) -> Dict[str, List[Union[str, Tuple[Callable, Dict]]]]:
19 return {19 if self._called_name == "usg":
20 return {
21 "post_enable": [
22 "Visit {} for the next steps".format(USG_DOCS_URL)
23 ]
24 }
25 messages = {
20 "post_enable": [26 "post_enable": [
21 "Visit {} to learn how to use CIS".format(CIS_DOCS_URL)27 "Visit {} to learn how to use CIS".format(CIS_DOCS_URL)
22 ]28 ]
23 }29 } # type: Dict[str, List[Union[str, Tuple[Callable, Dict]]]]
30 if "usg" in self.valid_names:
31 messages["pre_enable"] = [
32 "From Ubuntu 20.04 and onwards 'ua enable cis' has been",
33 "replaced by 'ua enable usg'. See more information at:",
34 USG_DOCS_URL,
35 ]
36 return messages
37
38 @property
39 def packages(self) -> List[str]:
40 if self._called_name == "usg":
41 return []
42 return super().packages
43
44 @property
45 def title(self) -> str:
46 if self._called_name == "cis":
47 return "CIS Audit"
48 return "Ubuntu Security Guide"
diff --git a/uaclient/entitlements/livepatch.py b/uaclient/entitlements/livepatch.py
index aec4ae0..f422f32 100644
--- a/uaclient/entitlements/livepatch.py
+++ b/uaclient/entitlements/livepatch.py
@@ -16,6 +16,8 @@ ERROR_MSG_MAP = {
16 "unsupported kernel": "Your running kernel is not supported by Livepatch.",16 "unsupported kernel": "Your running kernel is not supported by Livepatch.",
17}17}
1818
19LIVEPATCH_CMD = "/snap/bin/canonical-livepatch"
20
1921
20def unconfigure_livepatch_proxy(22def unconfigure_livepatch_proxy(
21 protocol_type: str, retry_sleeps: Optional[List[float]] = None23 protocol_type: str, retry_sleeps: Optional[List[float]] = None
@@ -29,10 +31,10 @@ def unconfigure_livepatch_proxy(
29 on failure; sleeping half a second before the first retry and 1 second31 on failure; sleeping half a second before the first retry and 1 second
30 before the second retry.32 before the second retry.
31 """33 """
32 if not util.which("/snap/bin/canonical-livepatch"):34 if not util.which(LIVEPATCH_CMD):
33 return35 return
34 util.subp(36 util.subp(
35 ["canonical-livepatch", "config", "{}-proxy=".format(protocol_type)],37 [LIVEPATCH_CMD, "config", "{}-proxy=".format(protocol_type)],
36 retry_sleeps=retry_sleeps,38 retry_sleeps=retry_sleeps,
37 )39 )
3840
@@ -61,21 +63,13 @@ def configure_livepatch_proxy(
6163
62 if http_proxy:64 if http_proxy:
63 util.subp(65 util.subp(
64 [66 [LIVEPATCH_CMD, "config", "http-proxy={}".format(http_proxy)],
65 "canonical-livepatch",
66 "config",
67 "http-proxy={}".format(http_proxy),
68 ],
69 retry_sleeps=retry_sleeps,67 retry_sleeps=retry_sleeps,
70 )68 )
7169
72 if https_proxy:70 if https_proxy:
73 util.subp(71 util.subp(
74 [72 [LIVEPATCH_CMD, "config", "https-proxy={}".format(https_proxy)],
75 "canonical-livepatch",
76 "config",
77 "https-proxy={}".format(https_proxy),
78 ],
79 retry_sleeps=retry_sleeps,73 retry_sleeps=retry_sleeps,
80 )74 )
8175
@@ -86,7 +80,7 @@ def get_config_option_value(key: str) -> Optional[str]:
86 :param protocol: can be any valid livepatch config option80 :param protocol: can be any valid livepatch config option
87 :return: the value of the livepatch config option, or None if not set81 :return: the value of the livepatch config option, or None if not set
88 """82 """
89 out, _ = util.subp(["canonical-livepatch", "config"])83 out, _ = util.subp([LIVEPATCH_CMD, "config"])
90 match = re.search("^{}: (.*)$".format(key), out, re.MULTILINE)84 match = re.search("^{}: (.*)$".format(key), out, re.MULTILINE)
91 value = match.group(1) if match else None85 value = match.group(1) if match else None
92 if value:86 if value:
@@ -151,8 +145,8 @@ class LivepatchEntitlement(base.UAEntitlement):
151 )145 )
152 elif "snapd" not in apt.get_installed_packages():146 elif "snapd" not in apt.get_installed_packages():
153 raise exceptions.UserFacingError(147 raise exceptions.UserFacingError(
154 "/usr/bin/snap is present but snapd is not installed;"148 "{} is present but snapd is not installed;"
155 " cannot enable {}".format(self.title)149 " cannot enable {}".format(snap.SNAP_CMD, self.title)
156 )150 )
157151
158 try:152 try:
@@ -177,7 +171,7 @@ class LivepatchEntitlement(base.UAEntitlement):
177 retry_sleeps=snap.SNAP_INSTALL_RETRIES,171 retry_sleeps=snap.SNAP_INSTALL_RETRIES,
178 )172 )
179173
180 if not util.which("/snap/bin/canonical-livepatch"):174 if not util.which(LIVEPATCH_CMD):
181 print("Installing canonical-livepatch snap")175 print("Installing canonical-livepatch snap")
182 try:176 try:
183 util.subp(177 util.subp(
@@ -230,18 +224,13 @@ class LivepatchEntitlement(base.UAEntitlement):
230 self.title,224 self.title,
231 )225 )
232 try:226 try:
233 util.subp(["/snap/bin/canonical-livepatch", "disable"])227 util.subp([LIVEPATCH_CMD, "disable"])
234 except util.ProcessExecutionError as e:228 except util.ProcessExecutionError as e:
235 logging.error(str(e))229 logging.error(str(e))
236 return False230 return False
237 try:231 try:
238 util.subp(232 util.subp(
239 [233 [LIVEPATCH_CMD, "enable", livepatch_token], capture=True
240 "/snap/bin/canonical-livepatch",
241 "enable",
242 livepatch_token,
243 ],
244 capture=True,
245 )234 )
246 except util.ProcessExecutionError as e:235 except util.ProcessExecutionError as e:
247 msg = "Unable to enable Livepatch: "236 msg = "Unable to enable Livepatch: "
@@ -261,15 +250,15 @@ class LivepatchEntitlement(base.UAEntitlement):
261250
262 @return: True on success, False otherwise.251 @return: True on success, False otherwise.
263 """252 """
264 if not util.which("/snap/bin/canonical-livepatch"):253 if not util.which(LIVEPATCH_CMD):
265 return True254 return True
266 util.subp(["/snap/bin/canonical-livepatch", "disable"], capture=True)255 util.subp([LIVEPATCH_CMD, "disable"], capture=True)
267 return True256 return True
268257
269 def application_status(self) -> Tuple[ApplicationStatus, str]:258 def application_status(self) -> Tuple[ApplicationStatus, str]:
270 status = (ApplicationStatus.ENABLED, "")259 status = (ApplicationStatus.ENABLED, "")
271260
272 if not util.which("/snap/bin/canonical-livepatch"):261 if not util.which(LIVEPATCH_CMD):
273 return (262 return (
274 ApplicationStatus.DISABLED,263 ApplicationStatus.DISABLED,
275 "canonical-livepatch snap is not installed.",264 "canonical-livepatch snap is not installed.",
@@ -277,8 +266,7 @@ class LivepatchEntitlement(base.UAEntitlement):
277266
278 try:267 try:
279 util.subp(268 util.subp(
280 ["/snap/bin/canonical-livepatch", "status"],269 [LIVEPATCH_CMD, "status"], retry_sleeps=LIVEPATCH_RETRIES
281 retry_sleeps=LIVEPATCH_RETRIES,
282 )270 )
283 except util.ProcessExecutionError as e:271 except util.ProcessExecutionError as e:
284 # TODO(May want to parse INACTIVE/failure assessment)272 # TODO(May want to parse INACTIVE/failure assessment)
@@ -350,11 +338,7 @@ def process_config_directives(cfg):
350 ca_certs = directives.get("caCerts")338 ca_certs = directives.get("caCerts")
351 if ca_certs:339 if ca_certs:
352 util.subp(340 util.subp(
353 [341 [LIVEPATCH_CMD, "config", "ca-certs={}".format(ca_certs)],
354 "/snap/bin/canonical-livepatch",
355 "config",
356 "ca-certs={}".format(ca_certs),
357 ],
358 capture=True,342 capture=True,
359 )343 )
360 remote_server = directives.get("remoteServer", "")344 remote_server = directives.get("remoteServer", "")
@@ -363,7 +347,7 @@ def process_config_directives(cfg):
363 if remote_server:347 if remote_server:
364 util.subp(348 util.subp(
365 [349 [
366 "/snap/bin/canonical-livepatch",350 LIVEPATCH_CMD,
367 "config",351 "config",
368 "remote-server={}".format(remote_server),352 "remote-server={}".format(remote_server),
369 ],353 ],
diff --git a/uaclient/entitlements/repo.py b/uaclient/entitlements/repo.py
index 8c2e4d1..153ea09 100644
--- a/uaclient/entitlements/repo.py
+++ b/uaclient/entitlements/repo.py
@@ -120,10 +120,22 @@ class RepoEntitlement(base.UAEntitlement):
120 :return: False if apt url is already found on the source file.120 :return: False if apt url is already found on the source file.
121 True otherwise.121 True otherwise.
122 """122 """
123 apt_file = self.repo_list_file_tmpl.format(name=self.name)
124 # If the apt file is commented out, we will assume that we need
125 # to regenerate the apt file, regardless of the apt url delta
126 if all(
127 line.startswith("#")
128 for line in util.load_file(apt_file).strip().split("\n")
129 ):
130 return False
131
132 # If the file is not commented out and we don't have delta,
133 # we will not do anything
123 if not apt_url:134 if not apt_url:
124 return True135 return True
125136
126 apt_file = self.repo_list_file_tmpl.format(name=self.name)137 # If the delta is already in the file, we won't reconfigure it
138 # again
127 return bool(apt_url in util.load_file(apt_file))139 return bool(apt_url in util.load_file(apt_file))
128140
129 def process_contract_deltas(141 def process_contract_deltas(
diff --git a/uaclient/entitlements/tests/conftest.py b/uaclient/entitlements/tests/conftest.py
index 054bc2b..03f56af 100644
--- a/uaclient/entitlements/tests/conftest.py
+++ b/uaclient/entitlements/tests/conftest.py
@@ -94,6 +94,7 @@ def entitlement_factory(tmpdir):
94 obligations: Dict[str, Any] = None,94 obligations: Dict[str, Any] = None,
95 entitled: bool = True,95 entitled: bool = True,
96 allow_beta: bool = False,96 allow_beta: bool = False,
97 called_name: str = "",
97 assume_yes: Optional[bool] = None,98 assume_yes: Optional[bool] = None,
98 suites: List[str] = None,99 suites: List[str] = None,
99 additional_packages: List[str] = None,100 additional_packages: List[str] = None,
@@ -122,7 +123,7 @@ def entitlement_factory(tmpdir):
122 if services_once_enabled:123 if services_once_enabled:
123 cfg.write_cache("services-once-enabled", services_once_enabled)124 cfg.write_cache("services-once-enabled", services_once_enabled)
124125
125 args = {"allow_beta": allow_beta}126 args = {"allow_beta": allow_beta, "called_name": called_name}
126 if assume_yes is not None:127 if assume_yes is not None:
127 args["assume_yes"] = assume_yes128 args["assume_yes"] = assume_yes
128 return cls(cfg, **args)129 return cls(cfg, **args)
diff --git a/uaclient/entitlements/tests/test_base.py b/uaclient/entitlements/tests/test_base.py
index f78f29a..76834d5 100644
--- a/uaclient/entitlements/tests/test_base.py
+++ b/uaclient/entitlements/tests/test_base.py
@@ -286,8 +286,6 @@ class TestUaEntitlement:
286 def test_can_enable_when_incompatible_service_found(286 def test_can_enable_when_incompatible_service_found(
287 self, concrete_entitlement_factory287 self, concrete_entitlement_factory
288 ):288 ):
289 import uaclient.entitlements as ent
290
291 base_ent = concrete_entitlement_factory(289 base_ent = concrete_entitlement_factory(
292 entitled=True,290 entitled=True,
293 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),291 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),
@@ -306,8 +304,9 @@ class TestUaEntitlement:
306 with mock.patch.object(304 with mock.patch.object(
307 base_ent, "is_access_expired", return_value=False305 base_ent, "is_access_expired", return_value=False
308 ):306 ):
309 with mock.patch.object(307 with mock.patch(
310 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}308 "uaclient.entitlements.entitlement_factory",
309 return_value=m_entitlement_cls,
311 ):310 ):
312 ret, reason = base_ent.can_enable()311 ret, reason = base_ent.can_enable()
313312
@@ -320,8 +319,6 @@ class TestUaEntitlement:
320 def test_can_enable_when_required_service_found(319 def test_can_enable_when_required_service_found(
321 self, concrete_entitlement_factory320 self, concrete_entitlement_factory
322 ):321 ):
323 import uaclient.entitlements as ent
324
325 base_ent = concrete_entitlement_factory(322 base_ent = concrete_entitlement_factory(
326 entitled=True,323 entitled=True,
327 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),324 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),
@@ -337,8 +334,9 @@ class TestUaEntitlement:
337 ]334 ]
338 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")335 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
339336
340 with mock.patch.object(337 with mock.patch(
341 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}338 "uaclient.entitlements.entitlement_factory",
339 return_value=m_entitlement_cls,
342 ):340 ):
343 ret, reason = base_ent.can_enable()341 ret, reason = base_ent.can_enable()
344342
@@ -363,8 +361,6 @@ class TestUaEntitlement:
363 assume_yes,361 assume_yes,
364 concrete_entitlement_factory,362 concrete_entitlement_factory,
365 ):363 ):
366 import uaclient.entitlements as ent
367
368 m_prompt.return_value = assume_yes364 m_prompt.return_value = assume_yes
369 m_is_config_value_true.return_value = block_disable_on_enable365 m_is_config_value_true.return_value = block_disable_on_enable
370 base_ent = concrete_entitlement_factory(366 base_ent = concrete_entitlement_factory(
@@ -386,8 +382,9 @@ class TestUaEntitlement:
386 with mock.patch.object(382 with mock.patch.object(
387 base_ent, "is_access_expired", return_value=False383 base_ent, "is_access_expired", return_value=False
388 ):384 ):
389 with mock.patch.object(385 with mock.patch(
390 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}386 "uaclient.entitlements.entitlement_factory",
387 return_value=m_entitlement_cls,
391 ):388 ):
392 ret, reason = base_ent.enable()389 ret, reason = base_ent.enable()
393390
@@ -416,8 +413,6 @@ class TestUaEntitlement:
416 def test_enable_when_required_service_found(413 def test_enable_when_required_service_found(
417 self, m_prompt, assume_yes, concrete_entitlement_factory414 self, m_prompt, assume_yes, concrete_entitlement_factory
418 ):415 ):
419 import uaclient.entitlements as ent
420
421 m_prompt.return_value = assume_yes416 m_prompt.return_value = assume_yes
422 base_ent = concrete_entitlement_factory(417 base_ent = concrete_entitlement_factory(
423 entitled=True,418 entitled=True,
@@ -436,8 +431,9 @@ class TestUaEntitlement:
436 m_entitlement_obj.enable.return_value = (True, "")431 m_entitlement_obj.enable.return_value = (True, "")
437 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")432 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
438433
439 with mock.patch.object(434 with mock.patch(
440 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}435 "uaclient.entitlements.entitlement_factory",
436 return_value=m_entitlement_cls,
441 ):437 ):
442 ret, reason = base_ent.enable()438 ret, reason = base_ent.enable()
443439
@@ -678,8 +674,6 @@ class TestUaEntitlement:
678 def test_disable_when_dependent_service_found(674 def test_disable_when_dependent_service_found(
679 self, m_prompt, concrete_entitlement_factory675 self, m_prompt, concrete_entitlement_factory
680 ):676 ):
681 import uaclient.entitlements as ent
682
683 m_prompt.return_value = True677 m_prompt.return_value = True
684 base_ent = concrete_entitlement_factory(678 base_ent = concrete_entitlement_factory(
685 entitled=True,679 entitled=True,
@@ -697,8 +691,9 @@ class TestUaEntitlement:
697 m_entitlement_obj.disable.return_value = True691 m_entitlement_obj.disable.return_value = True
698 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")692 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
699693
700 with mock.patch.object(694 with mock.patch(
701 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}695 "uaclient.entitlements.entitlement_factory",
696 return_value=m_entitlement_cls,
702 ):697 ):
703 ret = base_ent.disable()698 ret = base_ent.disable()
704699
@@ -710,6 +705,39 @@ class TestUaEntitlement:
710 assert m_prompt.call_count == expected_prompt_call705 assert m_prompt.call_count == expected_prompt_call
711 assert m_entitlement_obj.disable.call_count == expected_disable_call706 assert m_entitlement_obj.disable.call_count == expected_disable_call
712707
708 @pytest.mark.parametrize(
709 "p_name,expected",
710 (
711 ("pretty_name", ["testconcreteentitlement", "pretty_name"]),
712 ("testconcreteentitlement", ["testconcreteentitlement"]),
713 ),
714 )
715 @mock.patch(
716 "uaclient.entitlements.base.UAEntitlement.presentation_name",
717 new_callable=mock.PropertyMock,
718 )
719 def test_valid_names(
720 self, m_p_name, p_name, expected, concrete_entitlement_factory
721 ):
722 m_p_name.return_value = p_name
723 entitlement = concrete_entitlement_factory(entitled=True)
724 assert expected == entitlement.valid_names
725
726 def test_presentation_name(self, concrete_entitlement_factory):
727 entitlement = concrete_entitlement_factory(entitled=True)
728 assert "testconcreteentitlement" == entitlement.presentation_name
729 m_entitlements = {
730 "testconcreteentitlement": {
731 "entitlement": {
732 "affordances": {"presentedAs": "something_else"}
733 }
734 }
735 }
736 with mock.patch(
737 "uaclient.config.UAConfig.entitlements", m_entitlements
738 ):
739 assert "something_else" == entitlement.presentation_name
740
713741
714class TestUaEntitlementUserFacingStatus:742class TestUaEntitlementUserFacingStatus:
715 def test_inapplicable_when_not_applicable(743 def test_inapplicable_when_not_applicable(
diff --git a/uaclient/entitlements/tests/test_cis.py b/uaclient/entitlements/tests/test_cis.py
index 838c784..d6f1611 100644
--- a/uaclient/entitlements/tests/test_cis.py
+++ b/uaclient/entitlements/tests/test_cis.py
@@ -12,7 +12,10 @@ M_REPOPATH = "uaclient.entitlements.repo."
12@pytest.fixture12@pytest.fixture
13def entitlement(entitlement_factory):13def entitlement(entitlement_factory):
14 return entitlement_factory(14 return entitlement_factory(
15 CISEntitlement, allow_beta=True, additional_packages=["pkg1"]15 CISEntitlement,
16 allow_beta=True,
17 called_name="cis",
18 additional_packages=["pkg1"],
16 )19 )
1720
1821
diff --git a/uaclient/entitlements/tests/test_entitlements.py b/uaclient/entitlements/tests/test_entitlements.py
index a7210fe..06e0939 100644
--- a/uaclient/entitlements/tests/test_entitlements.py
+++ b/uaclient/entitlements/tests/test_entitlements.py
@@ -6,23 +6,54 @@ from uaclient import entitlements
66
77
8class TestValidServices:8class TestValidServices:
9 @pytest.mark.parametrize("show_all_names", ((True), (False)))
9 @pytest.mark.parametrize("allow_beta", ((True), (False)))10 @pytest.mark.parametrize("allow_beta", ((True), (False)))
10 @pytest.mark.parametrize("is_beta", ((True), (False)))11 @pytest.mark.parametrize("is_beta", ((True), (False)))
11 @mock.patch("uaclient.entitlements.is_config_value_true")12 @mock.patch("uaclient.entitlements.is_config_value_true")
12 def test_valid_services(self, m_is_config_value, is_beta, allow_beta):13 def test_valid_services(
14 self, m_is_config_value, show_all_names, allow_beta, is_beta
15 ):
13 m_is_config_value.return_value = allow_beta16 m_is_config_value.return_value = allow_beta
17
14 m_cls_1 = mock.MagicMock()18 m_cls_1 = mock.MagicMock()
15 type(m_cls_1).is_beta = mock.PropertyMock(return_value=False)19 m_inst_1 = mock.MagicMock()
20 m_cls_1.is_beta = False
21 m_inst_1.presentation_name = "ent1"
22 m_inst_1.valid_names = ["ent1", "othername"]
23 m_cls_1.return_value = m_inst_1
1624
17 m_cls_2 = mock.MagicMock()25 m_cls_2 = mock.MagicMock()
18 type(m_cls_2).is_beta = mock.PropertyMock(return_value=is_beta)26 m_inst_2 = mock.MagicMock()
19 ents_dict = {"ent1": m_cls_1, "ent2": m_cls_2}27 m_cls_2.is_beta = is_beta
28 m_inst_2.presentation_name = "ent2"
29 m_inst_2.valid_names = ["ent2"]
30 m_cls_2.return_value = m_inst_2
31
32 ents = {m_cls_1, m_cls_2}
2033
21 with mock.patch.object(34 with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents):
22 entitlements, "ENTITLEMENT_CLASS_BY_NAME", ents_dict
23 ):
24 expected_services = ["ent1"]35 expected_services = ["ent1"]
25 if allow_beta or not is_beta:36 if allow_beta or not is_beta:
26 expected_services.append("ent2")37 expected_services.append("ent2")
38 if show_all_names:
39 expected_services.append("othername")
40
41 assert expected_services == entitlements.valid_services(
42 all_names=show_all_names
43 )
44
45
46class TestEntitlementFactory:
47 def test_entitlement_factory(self):
48 m_cls_1 = mock.MagicMock()
49 m_cls_1.return_value.valid_names = ["ent1", "othername"]
50
51 m_cls_2 = mock.MagicMock()
52 m_cls_2.return_value.valid_names = ["ent2"]
53
54 ents = {m_cls_1, m_cls_2}
2755
28 assert expected_services == entitlements.valid_services()56 with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents):
57 assert m_cls_1 == entitlements.entitlement_factory("othername")
58 assert m_cls_2 == entitlements.entitlement_factory("ent2")
59 assert None is entitlements.entitlement_factory("nonexistent")
diff --git a/uaclient/entitlements/tests/test_fips.py b/uaclient/entitlements/tests/test_fips.py
index 1a040bc..4d28026 100644
--- a/uaclient/entitlements/tests/test_fips.py
+++ b/uaclient/entitlements/tests/test_fips.py
@@ -145,7 +145,12 @@ class TestFIPSEntitlementCanEnable:
145 "application_status",145 "application_status",
146 return_value=(status.ApplicationStatus.DISABLED, ""),146 return_value=(status.ApplicationStatus.DISABLED, ""),
147 ):147 ):
148 assert (True, None) == entitlement.can_enable()148 with mock.patch.object(
149 entitlement,
150 "detect_incompatible_services",
151 return_value=False,
152 ):
153 assert (True, None) == entitlement.can_enable()
149 assert ("", "") == capsys.readouterr()154 assert ("", "") == capsys.readouterr()
150155
151156
@@ -931,8 +936,9 @@ class TestFipsEntitlementInstallPackages:
931 self, m_run_apt, entitlement936 self, m_run_apt, entitlement
932 ):937 ):
933 m_run_apt.side_effect = exceptions.UserFacingError("error")938 m_run_apt.side_effect = exceptions.UserFacingError("error")
934 with pytest.raises(exceptions.UserFacingError):939 with mock.patch.object(entitlement, "_cleanup"):
935 entitlement.install_packages()940 with pytest.raises(exceptions.UserFacingError):
941 entitlement.install_packages()
936942
937 @mock.patch(M_PATH + "apt.get_installed_packages")943 @mock.patch(M_PATH + "apt.get_installed_packages")
938 @mock.patch(M_PATH + "apt.run_apt_command")944 @mock.patch(M_PATH + "apt.run_apt_command")
diff --git a/uaclient/entitlements/tests/test_livepatch.py b/uaclient/entitlements/tests/test_livepatch.py
index 5d6dc3a..579aa70 100644
--- a/uaclient/entitlements/tests/test_livepatch.py
+++ b/uaclient/entitlements/tests/test_livepatch.py
@@ -12,6 +12,7 @@ import pytest
1212
13from uaclient import apt, exceptions, status13from uaclient import apt, exceptions, status
14from uaclient.entitlements.livepatch import (14from uaclient.entitlements.livepatch import (
15 LIVEPATCH_CMD,
15 LivepatchEntitlement,16 LivepatchEntitlement,
16 configure_livepatch_proxy,17 configure_livepatch_proxy,
17 get_config_option_value,18 get_config_option_value,
@@ -84,7 +85,7 @@ class TestConfigureLivepatchProxy:
84 expected_calls.append(85 expected_calls.append(
85 mock.call(86 mock.call(
86 [87 [
87 "canonical-livepatch",88 LIVEPATCH_CMD,
88 "config",89 "config",
89 "http-proxy={}".format(http_proxy),90 "http-proxy={}".format(http_proxy),
90 ],91 ],
@@ -96,7 +97,7 @@ class TestConfigureLivepatchProxy:
96 expected_calls.append(97 expected_calls.append(
97 mock.call(98 mock.call(
98 [99 [
99 "canonical-livepatch",100 LIVEPATCH_CMD,
100 "config",101 "config",
101 "https-proxy={}".format(https_proxy),102 "https-proxy={}".format(https_proxy),
102 ],103 ],
@@ -183,7 +184,7 @@ check-interval: 60 # minutes""",
183 ret = get_config_option_value(key)184 ret = get_config_option_value(key)
184 assert ret == expected_ret185 assert ret == expected_ret
185 assert [186 assert [
186 mock.call(["canonical-livepatch", "config"])187 mock.call([LIVEPATCH_CMD, "config"])
187 ] == m_util_subp.call_args_list188 ] == m_util_subp.call_args_list
188189
189190
@@ -203,7 +204,7 @@ class TestUnconfigureLivepatchProxy:
203 self, subp, which, livepatch_installed, protocol_type, retry_sleeps204 self, subp, which, livepatch_installed, protocol_type, retry_sleeps
204 ):205 ):
205 if livepatch_installed:206 if livepatch_installed:
206 which.return_value = "/snap/bin/canonical-livepatch"207 which.return_value = LIVEPATCH_CMD
207 else:208 else:
208 which.return_value = None209 which.return_value = None
209 kwargs = {"protocol_type": protocol_type}210 kwargs = {"protocol_type": protocol_type}
@@ -213,11 +214,7 @@ class TestUnconfigureLivepatchProxy:
213 if livepatch_installed:214 if livepatch_installed:
214 expected_calls = [215 expected_calls = [
215 mock.call(216 mock.call(
216 [217 [LIVEPATCH_CMD, "config", protocol_type + "-proxy="],
217 "canonical-livepatch",
218 "config",
219 protocol_type + "-proxy=",
220 ],
221 retry_sleeps=retry_sleeps,218 retry_sleeps=retry_sleeps,
222 )219 )
223 ]220 ]
@@ -291,7 +288,7 @@ class TestLivepatchProcessConfigDirectives:
291 process_config_directives(cfg)288 process_config_directives(cfg)
292 expected_subp = mock.call(289 expected_subp = mock.call(
293 [290 [
294 "/snap/bin/canonical-livepatch",291 LIVEPATCH_CMD,
295 "config",292 "config",
296 livepatch_param_tmpl.format(directive_value),293 livepatch_param_tmpl.format(directive_value),
297 ],294 ],
@@ -314,16 +311,10 @@ class TestLivepatchProcessConfigDirectives:
314 process_config_directives(cfg)311 process_config_directives(cfg)
315 expected_calls = [312 expected_calls = [
316 mock.call(313 mock.call(
317 ["/snap/bin/canonical-livepatch", "config", "ca-certs=value2"],314 [LIVEPATCH_CMD, "config", "ca-certs=value2"], capture=True
318 capture=True,
319 ),315 ),
320 mock.call(316 mock.call(
321 [317 [LIVEPATCH_CMD, "config", "remote-server=value1"], capture=True
322 "/snap/bin/canonical-livepatch",
323 "config",
324 "remote-server=value1",
325 ],
326 capture=True,
327 ),318 ),
328 ]319 ]
329 assert expected_calls == m_subp.call_args_list320 assert expected_calls == m_subp.call_args_list
@@ -609,17 +600,14 @@ class TestLivepatchEntitlementEnable:
609 mocks_config = [600 mocks_config = [
610 mock.call(601 mock.call(
611 [602 [
612 "/snap/bin/canonical-livepatch",603 LIVEPATCH_CMD,
613 "config",604 "config",
614 "remote-server=https://alt.livepatch.com",605 "remote-server=https://alt.livepatch.com",
615 ],606 ],
616 capture=True,607 capture=True,
617 ),608 ),
618 mock.call(["/snap/bin/canonical-livepatch", "disable"]),609 mock.call([LIVEPATCH_CMD, "disable"]),
619 mock.call(610 mock.call([LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True),
620 ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],
621 capture=True,
622 ),
623 ]611 ]
624612
625 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)613 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
@@ -677,10 +665,7 @@ class TestLivepatchEntitlementEnable:
677 assert expected_log not in caplog_text()665 assert expected_log not in caplog_text()
678 else:666 else:
679 assert expected_log in caplog_text()667 assert expected_log in caplog_text()
680 expected_calls = [668 expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)]
681 mock.call("/usr/bin/snap"),
682 mock.call("/snap/bin/canonical-livepatch"),
683 ]
684 assert expected_calls == m_which.call_args_list669 assert expected_calls == m_which.call_args_list
685 assert m_validate_proxy.call_count == 2670 assert m_validate_proxy.call_count == 2
686 assert m_snap_proxy.call_count == 1671 assert m_snap_proxy.call_count == 1
@@ -723,10 +708,7 @@ class TestLivepatchEntitlementEnable:
723 "Canonical livepatch enabled.\n"708 "Canonical livepatch enabled.\n"
724 )709 )
725 assert (msg, "") == capsys.readouterr()710 assert (msg, "") == capsys.readouterr()
726 expected_calls = [711 expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)]
727 mock.call("/usr/bin/snap"),
728 mock.call("/snap/bin/canonical-livepatch"),
729 ]
730 assert expected_calls == m_which.call_args_list712 assert expected_calls == m_which.call_args_list
731 assert m_validate_proxy.call_count == 2713 assert m_validate_proxy.call_count == 2
732 assert m_snap_proxy.call_count == 1714 assert m_snap_proxy.call_count == 1
@@ -801,16 +783,15 @@ class TestLivepatchEntitlementEnable:
801 ),783 ),
802 mock.call(784 mock.call(
803 [785 [
804 "/snap/bin/canonical-livepatch",786 LIVEPATCH_CMD,
805 "config",787 "config",
806 "remote-server=https://alt.livepatch.com",788 "remote-server=https://alt.livepatch.com",
807 ],789 ],
808 capture=True,790 capture=True,
809 ),791 ),
810 mock.call(["/snap/bin/canonical-livepatch", "disable"]),792 mock.call([LIVEPATCH_CMD, "disable"]),
811 mock.call(793 mock.call(
812 ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],794 [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True
813 capture=True,
814 ),795 ),
815 ]796 ]
816 assert subp_calls == m_subp.call_args_list797 assert subp_calls == m_subp.call_args_list
@@ -851,15 +832,14 @@ class TestLivepatchEntitlementEnable:
851 ),832 ),
852 mock.call(833 mock.call(
853 [834 [
854 "/snap/bin/canonical-livepatch",835 LIVEPATCH_CMD,
855 "config",836 "config",
856 "remote-server=https://alt.livepatch.com",837 "remote-server=https://alt.livepatch.com",
857 ],838 ],
858 capture=True,839 capture=True,
859 ),840 ),
860 mock.call(841 mock.call(
861 ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],842 [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True
862 capture=True,
863 ),843 ),
864 ]844 ]
865 assert subp_no_livepatch_disable == m_subp.call_args_list845 assert subp_no_livepatch_disable == m_subp.call_args_list
diff --git a/uaclient/entitlements/tests/test_repo.py b/uaclient/entitlements/tests/test_repo.py
index 34aa979..233e3d2 100644
--- a/uaclient/entitlements/tests/test_repo.py
+++ b/uaclient/entitlements/tests/test_repo.py
@@ -980,6 +980,33 @@ class TestSetupAptConfig:
980 ] == m_run_apt_command.call_args_list980 ] == m_run_apt_command.call_args_list
981981
982982
983class TestCheckAptURLIsApplied:
984 @pytest.mark.parametrize("apt_url", (("test"), (None)))
985 @mock.patch("uaclient.util.load_file")
986 def test_check_apt_url_for_commented_apt_source_file(
987 self, m_load_file, apt_url, entitlement
988 ):
989 m_load_file.return_value = "#test1\n#test2\n"
990 assert not entitlement._check_apt_url_is_applied(apt_url)
991
992 @mock.patch("uaclient.util.load_file")
993 def test_check_apt_url_when_delta_apt_url_is_none(
994 self, m_load_file, entitlement
995 ):
996 m_load_file.return_value = "test1\n#test2\n"
997 assert entitlement._check_apt_url_is_applied(apt_url=None)
998
999 @pytest.mark.parametrize(
1000 "apt_url,expected", (("test", True), ("blah", False))
1001 )
1002 @mock.patch("uaclient.util.load_file")
1003 def test_check_apt_url_inspects_apt_source_file(
1004 self, m_load_file, apt_url, expected, entitlement
1005 ):
1006 m_load_file.return_value = "test\n#test2\n"
1007 assert expected == entitlement._check_apt_url_is_applied(apt_url)
1008
1009
983class TestApplicationStatus:1010class TestApplicationStatus:
984 # TODO: Write tests for all functionality1011 # TODO: Write tests for all functionality
9851012
diff --git a/uaclient/exceptions.py b/uaclient/exceptions.py
index 2599561..daf2b5e 100644
--- a/uaclient/exceptions.py
+++ b/uaclient/exceptions.py
@@ -1,3 +1,5 @@
1from typing import Optional
2
1from uaclient import status3from uaclient import status
24
35
@@ -35,10 +37,23 @@ class NonAutoAttachImageError(UserFacingError):
35 exit_code = 037 exit_code = 0
3638
3739
40class AlreadyAttachedOnPROError(UserFacingError):
41 """Raised when a PRO machine retries attaching with the same instance-id"""
42
43 exit_code = 0
44
45 def __init__(self, instance_id: str):
46 super().__init__(
47 status.MESSAGE_ALREADY_ATTACHED_ON_PRO.format(
48 instance_id=instance_id
49 )
50 )
51
52
38class AlreadyAttachedError(UserFacingError):53class AlreadyAttachedError(UserFacingError):
39 """An exception to be raised when a command needs an unattached system."""54 """An exception to be raised when a command needs an unattached system."""
4055
41 exit_code = 056 exit_code = 2
4257
43 def __init__(self, cfg):58 def __init__(self, cfg):
44 super().__init__(59 super().__init__(
@@ -102,3 +117,20 @@ class SecurityAPIMetadataError(UserFacingError):
102 + "\n"117 + "\n"
103 + status.MESSAGE_SECURITY_ISSUE_NOT_RESOLVED.format(issue=issue_id)118 + status.MESSAGE_SECURITY_ISSUE_NOT_RESOLVED.format(issue=issue_id)
104 )119 )
120
121
122class CloudFactoryError(Exception):
123 def __init__(self, cloud_type: Optional[str]) -> None:
124 self.cloud_type = cloud_type
125
126
127class CloudFactoryNoCloudError(CloudFactoryError):
128 pass
129
130
131class CloudFactoryUnsupportedCloudError(CloudFactoryError):
132 pass
133
134
135class CloudFactoryNonViableCloudError(CloudFactoryError):
136 pass
diff --git a/uaclient/jobs/update_messaging.py b/uaclient/jobs/update_messaging.py
index bfc185c..b63fa20 100644
--- a/uaclient/jobs/update_messaging.py
+++ b/uaclient/jobs/update_messaging.py
@@ -206,13 +206,13 @@ def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None:
206 no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value206 no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value
207 msg_dir = os.path.join(cfg.data_dir, "messages")207 msg_dir = os.path.join(cfg.data_dir, "messages")
208208
209 apps_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-apps"]209 apps_cls = entitlements.entitlement_factory("esm-apps")
210 apps_inst = apps_cls(cfg)210 apps_inst = apps_cls(cfg)
211 config_allow_beta = util.is_config_value_true(211 config_allow_beta = util.is_config_value_true(
212 config=cfg.cfg, path_to_value="features.allow_beta"212 config=cfg.cfg, path_to_value="features.allow_beta"
213 )213 )
214 apps_valid = bool(config_allow_beta or not apps_cls.is_beta)214 apps_valid = bool(config_allow_beta or not apps_cls.is_beta)
215 infra_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-infra"]215 infra_cls = entitlements.entitlement_factory("esm-infra")
216 infra_inst = infra_cls(cfg)216 infra_inst = infra_cls(cfg)
217217
218 expiry_status, remaining_days = get_contract_expiry_status(cfg)218 expiry_status, remaining_days = get_contract_expiry_status(cfg)
@@ -292,7 +292,7 @@ def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None:
292 :param cfg: UAConfig instance for this environment.292 :param cfg: UAConfig instance for this environment.
293 :param series: string of Ubuntu release series: 'xenial'.293 :param series: string of Ubuntu release series: 'xenial'.
294 """294 """
295 apps_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-apps"]295 apps_cls = entitlements.entitlement_factory("esm-apps")
296 apps_inst = apps_cls(cfg)296 apps_inst = apps_cls(cfg)
297 enabled_status = ApplicationStatus.ENABLED297 enabled_status = ApplicationStatus.ENABLED
298 apps_not_enabled = apps_inst.application_status()[0] != enabled_status298 apps_not_enabled = apps_inst.application_status()[0] != enabled_status
diff --git a/uaclient/lock.py b/uaclient/lock.py
299new file mode 100644299new file mode 100644
index 0000000..77ef76f
--- /dev/null
+++ b/uaclient/lock.py
@@ -0,0 +1,106 @@
1import functools
2import logging
3import os
4import time
5
6from uaclient import config, exceptions
7
8LOG = logging.getLogger("ua.lock")
9
10# Set a module-level callable here so we don't have to reinstantiate
11# UAConfig in order to determine dynamic data_path exception handling of
12# main_error_handler
13clear_lock_file = None
14
15
16def clear_lock_file_if_present():
17 global clear_lock_file
18 if clear_lock_file:
19 clear_lock_file()
20
21
22class SingleAttemptLock:
23 """
24 Context manager for gaining exclusive access to the lock file.
25 Create a lock file if absent. The lock file will contain a pid of the
26 running process, and a customer-visible description of the lock holder.
27
28 :param lock_holder: String with the service name or command which is
29 holding the lock. This lock_holder string will be customer visible in
30 status.json.
31 :raises: LockHeldError if lock is held.
32 """
33
34 def __init__(self, *_args, cfg: config.UAConfig, lock_holder: str):
35 self.cfg = cfg
36 self.lock_holder = lock_holder
37
38 def __enter__(self):
39 global clear_lock_file
40 (lock_pid, cur_lock_holder) = self.cfg.check_lock_info()
41 if lock_pid > 0:
42 raise exceptions.LockHeldError(
43 lock_request=self.lock_holder,
44 lock_holder=cur_lock_holder,
45 pid=lock_pid,
46 )
47 self.cfg.write_cache(
48 "lock", "{}:{}".format(os.getpid(), self.lock_holder)
49 )
50 notice_msg = "Operation in progress: {}".format(self.lock_holder)
51 self.cfg.add_notice("", notice_msg)
52 clear_lock_file = functools.partial(self.cfg.delete_cache_key, "lock")
53
54 def __exit__(self, _exc_type, _exc_value, _traceback):
55 global clear_lock_file
56 self.cfg.delete_cache_key("lock")
57 clear_lock_file = None # Unset due to successful lock delete
58
59
60class SpinLock(SingleAttemptLock):
61 """
62 Context manager for gaining exclusive access to the lock file. In contrast
63 to the SingleAttemptLock, the SpinLock will try several times to acquire
64 the lock before giving up. The number of times to try and how long to sleep
65 in between tries is configurable.
66
67 :param lock_holder: String with the service name or command which is
68 holding the lock. This lock_holder string will be customer visible in
69 status.json.
70 :param sleep_time: Number of seconds to sleep before retrying if the lock
71 is already held.
72 :param max_retries: Maximum number of times to try to grab the lock before
73 giving up and raising a LockHeldError.
74 :raises: LockHeldError if lock is held after (sleep_time * max_retries)
75 """
76
77 def __init__(
78 self,
79 *_args,
80 cfg: config.UAConfig,
81 lock_holder: str,
82 sleep_time: int = 10,
83 max_retries: int = 12
84 ):
85 super().__init__(cfg=cfg, lock_holder=lock_holder)
86 self.sleep_time = sleep_time
87 self.max_retries = max_retries
88
89 def __enter__(self):
90 LOG.debug("spin lock starting for {}".format(self.lock_holder))
91 tries = 0
92 while True:
93 try:
94 super().__enter__()
95 break
96 except exceptions.LockHeldError as e:
97 LOG.debug(
98 "SpinLock Attempt {}. {}. Spinning...".format(
99 tries + 1, e.msg
100 )
101 )
102 tries += 1
103 if tries >= self.max_retries:
104 raise e
105 else:
106 time.sleep(self.sleep_time)
diff --git a/uaclient/security.py b/uaclient/security.py
index 818f472..94c0e3a 100644
--- a/uaclient/security.py
+++ b/uaclient/security.py
@@ -16,7 +16,7 @@ from uaclient.clouds.identity import (
16)16)
17from uaclient.config import UAConfig17from uaclient.config import UAConfig
18from uaclient.defaults import BASE_UA_URL, PRINT_WRAP_WIDTH18from uaclient.defaults import BASE_UA_URL, PRINT_WRAP_WIDTH
19from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME19from uaclient.entitlements import entitlement_factory
2020
21CVE_OR_USN_REGEX = (21CVE_OR_USN_REGEX = (
22 r"((CVE|cve)-\d{4}-\d{4,7}$|(USN|usn|LSN|lsn)-\d{1,5}-\d{1,2}$)"22 r"((CVE|cve)-\d{4}-\d{4,7}$|(USN|usn|LSN|lsn)-\d{1,5}-\d{1,2}$)"
@@ -763,7 +763,7 @@ def _get_service_for_pocket(pocket: str, cfg: UAConfig):
763 elif pocket == UA_APPS_POCKET:763 elif pocket == UA_APPS_POCKET:
764 service_to_check = "esm-apps"764 service_to_check = "esm-apps"
765765
766 ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(service_to_check)766 ent_cls = entitlement_factory(service_to_check)
767 return ent_cls(cfg) if ent_cls else None767 return ent_cls(cfg) if ent_cls else None
768768
769769
diff --git a/uaclient/security_status.py b/uaclient/security_status.py
index 0f42704..d1e9484 100644
--- a/uaclient/security_status.py
+++ b/uaclient/security_status.py
@@ -73,13 +73,11 @@ def filter_security_updates(
73 ]73 ]
7474
75 return [75 return [
76 package76 version
77 for package in packages77 for package in packages
78 if max(package.versions) > package.installed78 for version in package.versions
79 and any(79 if version > package.installed
80 origin.archive in security_repos80 and any(origin.archive in security_repos for origin in version.origins)
81 for origin in max(package.versions).origins
82 )
83 ]81 ]
8482
8583
@@ -122,20 +120,18 @@ def security_status(cfg: UAConfig) -> Dict[str, Any]:
122 installed_packages = [package for package in cache if package.is_installed]120 installed_packages = [package for package in cache if package.is_installed]
123 summary["num_installed_packages"] = len(installed_packages)121 summary["num_installed_packages"] = len(installed_packages)
124122
125 security_upgradable_packages = filter_security_updates(installed_packages)123 security_upgradable_versions = filter_security_updates(installed_packages)
126124
127 package_count = {"esm-infra": 0, "esm-apps": 0, "standard-security": 0}125 package_count = {"esm-infra": 0, "esm-apps": 0, "standard-security": 0}
128126
129 for package in security_upgradable_packages:127 for candidate in security_upgradable_versions:
130 candidate = max(package.versions)
131 version = candidate.version
132 service_name = get_service_name(candidate.origins)128 service_name = get_service_name(candidate.origins)
133 status = get_update_status(service_name, ua_info)129 status = get_update_status(service_name, ua_info)
134 package_count[service_name] += 1130 package_count[service_name] += 1
135 packages.append(131 packages.append(
136 {132 {
137 "package": package.name,133 "package": candidate.package.name,
138 "version": version,134 "version": candidate.version,
139 "service_name": service_name,135 "service_name": service_name,
140 "status": status,136 "status": status,
141 }137 }
diff --git a/uaclient/serviceclient.py b/uaclient/serviceclient.py
index 48a1273..09703fe 100644
--- a/uaclient/serviceclient.py
+++ b/uaclient/serviceclient.py
@@ -68,11 +68,15 @@ class UAServiceClient(metaclass=abc.ABCMeta):
68 timeout=self.url_timeout,68 timeout=self.url_timeout,
69 )69 )
70 except error.URLError as e:70 except error.URLError as e:
71 if hasattr(e, "read"):71 body = None
72 if hasattr(e, "body"):
73 body = e.body
74 elif hasattr(e, "read"):
75 body = e.read().decode("utf-8")
76 if body:
72 try:77 try:
73 error_details = json.loads(78 error_details = json.loads(
74 e.read().decode("utf-8"),79 body, cls=util.DatetimeAwareJSONDecoder
75 cls=util.DatetimeAwareJSONDecoder,
76 )80 )
77 except ValueError:81 except ValueError:
78 error_details = None82 error_details = None
diff --git a/uaclient/status.py b/uaclient/status.py
index 5dbf65e..e537150 100644
--- a/uaclient/status.py
+++ b/uaclient/status.py
@@ -227,6 +227,8 @@ MESSAGE_ENABLED_TMPL = "{title} enabled"
227MESSAGE_ALREADY_ATTACHED = """\227MESSAGE_ALREADY_ATTACHED = """\
228This machine is already attached to '{account_name}'228This machine is already attached to '{account_name}'
229To use a different subscription first run: sudo ua detach."""229To use a different subscription first run: sudo ua detach."""
230MESSAGE_ALREADY_ATTACHED_ON_PRO = """\
231Skipping attach: Instance '{instance_id}' is already attached."""
230MESSAGE_ALREADY_ENABLED_TMPL = """\232MESSAGE_ALREADY_ENABLED_TMPL = """\
231{title} is already enabled.\nSee: sudo ua status"""233{title} is already enabled.\nSee: sudo ua status"""
232MESSAGE_INAPPLICABLE_ARCH_TMPL = """\234MESSAGE_INAPPLICABLE_ARCH_TMPL = """\
@@ -346,6 +348,9 @@ Open a browser to: {}/subscribe""".format(
346348
347STATUS_UNATTACHED_TMPL = "{name: <14}{available: <11}{description}"349STATUS_UNATTACHED_TMPL = "{name: <14}{available: <11}{description}"
348350
351STATUS_SIMULATED_TMPL = """\
352{name: <14}{available: <11}{entitled: <11}{auto_enabled: <14}{description}"""
353
349STATUS_HEADER = "SERVICE ENTITLED STATUS DESCRIPTION"354STATUS_HEADER = "SERVICE ENTITLED STATUS DESCRIPTION"
350# The widths listed below for entitled and status are actually 9 characters355# The widths listed below for entitled and status are actually 9 characters
351# less than reality because we colorize the values in entitled and status356# less than reality because we colorize the values in entitled and status
@@ -631,7 +636,21 @@ def get_section_column_content(
631636
632def format_tabular(status: Dict[str, Any]) -> str:637def format_tabular(status: Dict[str, Any]) -> str:
633 """Format status dict for tabular output."""638 """Format status dict for tabular output."""
634 if not status["attached"]:639 if not status.get("attached"):
640 if status.get("simulated"):
641 content = [
642 STATUS_SIMULATED_TMPL.format(
643 name="SERVICE",
644 available="AVAILABLE",
645 entitled="ENTITLED",
646 auto_enabled="AUTO_ENABLED",
647 description="DESCRIPTION",
648 )
649 ]
650 for service in status["services"]:
651 content.append(STATUS_SIMULATED_TMPL.format(**service))
652 return "\n".join(content)
653
635 content = [654 content = [
636 STATUS_UNATTACHED_TMPL.format(655 STATUS_UNATTACHED_TMPL.format(
637 name="SERVICE",656 name="SERVICE",
@@ -696,12 +715,13 @@ def _format_status_output(status: Dict[str, Any]) -> Dict[str, Any]:
696 or name == "UA_CONFIG_FILE"715 or name == "UA_CONFIG_FILE"
697 ]716 ]
698717
699 available_services = [718 if not status.get("simulated"):
700 service719 available_services = [
701 for service in status.get("services", [])720 service
702 if service.get("available", "yes") == "yes"721 for service in status.get("services", [])
703 ]722 if service.get("available", "yes") == "yes"
704 status["services"] = available_services723 ]
724 status["services"] = available_services
705725
706 # We don't need the origin info in the json output726 # We don't need the origin info in the json output
707 status.pop("origin", "")727 status.pop("origin", "")
diff --git a/uaclient/tests/test_actions.py b/uaclient/tests/test_actions.py
708new file mode 100644728new file mode 100644
index 0000000..cc9a25f
--- /dev/null
+++ b/uaclient/tests/test_actions.py
@@ -0,0 +1,145 @@
1import mock
2import pytest
3
4from uaclient import exceptions, status, util
5from uaclient.actions import attach_with_token, auto_attach
6from uaclient.contract import ContractAPIError
7from uaclient.exceptions import NonAutoAttachImageError
8from uaclient.tests.test_cli_auto_attach import fake_instance_factory
9
10M_PATH = "uaclient.actions."
11
12
13class TestAttachWithToken:
14 @pytest.mark.parametrize(
15 "request_updated_contract_side_effect, expected_error_class,"
16 " expect_status_call",
17 [
18 (None, None, False),
19 (util.UrlError("cause"), util.UrlError, True),
20 (
21 exceptions.UserFacingError("test"),
22 exceptions.UserFacingError,
23 True,
24 ),
25 ],
26 )
27 @mock.patch(M_PATH + "config.update_ua_messages")
28 @mock.patch(M_PATH + "config.UAConfig.status")
29 @mock.patch(M_PATH + "contract.request_updated_contract")
30 def test_attach_with_token(
31 self,
32 m_request_updated_contract,
33 m_status,
34 m_update_ua_messages,
35 request_updated_contract_side_effect,
36 expected_error_class,
37 expect_status_call,
38 FakeConfig,
39 ):
40 cfg = FakeConfig()
41 m_request_updated_contract.side_effect = (
42 request_updated_contract_side_effect
43 )
44 if expected_error_class:
45 with pytest.raises(expected_error_class):
46 attach_with_token(cfg, "token", False)
47 else:
48 attach_with_token(cfg, "token", False)
49 if expect_status_call:
50 assert [mock.call()] == m_status.call_args_list
51 assert [mock.call(cfg)] == m_update_ua_messages.call_args_list
52
53
54class TestAutoAttach:
55 @mock.patch(M_PATH + "attach_with_token")
56 @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid")
57 @mock.patch(
58 M_PATH
59 + "contract.UAContractClient.request_auto_attach_contract_token",
60 return_value={"contractToken": "token"},
61 )
62 @mock.patch(M_PATH + "config.update_ua_messages")
63 @mock.patch(M_PATH + "config.UAConfig.write_cache")
64 def test_happy_path_on_auto_attach(
65 self,
66 m_write_cache,
67 m_update_ua_messages,
68 m_request_auto_attach_contract_token,
69 m_get_instance_id,
70 m_attach_with_token,
71 FakeConfig,
72 ):
73 cfg = FakeConfig()
74
75 auto_attach(cfg, fake_instance_factory())
76
77 assert [
78 mock.call(cfg, token="token", allow_enable=True)
79 ] == m_attach_with_token.call_args_list
80
81 assert [
82 mock.call("instance-id", "my-iid")
83 ] == m_write_cache.call_args_list
84
85 @pytest.mark.parametrize(
86 "http_msg,http_code,http_response",
87 (
88 ("Not found", 404, {"message": "missing instance information"}),
89 (
90 "Forbidden",
91 403,
92 {"message": "forbidden: cannot verify signing certificate"},
93 ),
94 ),
95 )
96 @mock.patch(
97 M_PATH + "contract.UAContractClient.request_auto_attach_contract_token"
98 )
99 @mock.patch(M_PATH + "identity.get_instance_id", return_value="old-iid")
100 def test_handles_4XX_contract_errors(
101 self,
102 _m_get_instance_id,
103 m_request_auto_attach_contract_token,
104 http_msg,
105 http_code,
106 http_response,
107 FakeConfig,
108 ):
109 """VMs running on non-auto-attach images do not return a token."""
110 cfg = FakeConfig()
111 m_request_auto_attach_contract_token.side_effect = ContractAPIError(
112 util.UrlError(
113 http_msg, code=http_code, url="http://me", headers={}
114 ),
115 error_response=http_response,
116 )
117 with pytest.raises(NonAutoAttachImageError) as excinfo:
118 auto_attach(cfg, fake_instance_factory())
119 assert status.MESSAGE_UNSUPPORTED_AUTO_ATTACH == str(excinfo.value)
120
121 @mock.patch(
122 M_PATH + "contract.UAContractClient.request_auto_attach_contract_token"
123 )
124 @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid")
125 def test_raise_unexpected_errors(
126 self,
127 _m_get_instance_id,
128 m_request_auto_attach_contract_token,
129 FakeConfig,
130 ):
131 """Any unexpected errors will be raised."""
132 cfg = FakeConfig()
133
134 unexpected_error = ContractAPIError(
135 util.UrlError(
136 "Server error", code=500, url="http://me", headers={}
137 ),
138 error_response={"message": "something unexpected"},
139 )
140 m_request_auto_attach_contract_token.side_effect = unexpected_error
141
142 with pytest.raises(ContractAPIError) as excinfo:
143 auto_attach(cfg, fake_instance_factory())
144
145 assert unexpected_error == excinfo.value
diff --git a/uaclient/tests/test_apt.py b/uaclient/tests/test_apt.py
index b440b6e..2810d7e 100644
--- a/uaclient/tests/test_apt.py
+++ b/uaclient/tests/test_apt.py
@@ -559,7 +559,7 @@ class TestCleanAptFiles:
559 repo_tmpl = tmpdir.join("source-{name}").strpath559 repo_tmpl = tmpdir.join("source-{name}").strpath
560 pref_tmpl = tmpdir.join("pref-{name}").strpath560 pref_tmpl = tmpdir.join("pref-{name}").strpath
561561
562 class DummyRepo(request.param):562 class TestRepo(request.param):
563 name = entitlement_name563 name = entitlement_name
564 repo_list_file_tmpl = repo_tmpl564 repo_list_file_tmpl = repo_tmpl
565 repo_pref_file_tmpl = pref_tmpl565 repo_pref_file_tmpl = pref_tmpl
@@ -575,7 +575,7 @@ class TestCleanAptFiles:
575 with open(pref_name, "w") as f:575 with open(pref_name, "w") as f:
576 f.write("")576 f.write("")
577577
578 return DummyRepo578 return TestRepo
579579
580 @mock.patch("uaclient.apt.os.unlink")580 @mock.patch("uaclient.apt.os.unlink")
581 def test_no_removals_for_no_repo_entitlements(self, m_os_unlink):581 def test_no_removals_for_no_repo_entitlements(self, m_os_unlink):
diff --git a/uaclient/tests/test_cli.py b/uaclient/tests/test_cli.py
index 3208603..a02d2fe 100644
--- a/uaclient/tests/test_cli.py
+++ b/uaclient/tests/test_cli.py
@@ -34,14 +34,25 @@ from uaclient.exceptions import (
34BIG_DESC = "123456789 " * 7 + "next line"34BIG_DESC = "123456789 " * 7 + "next line"
35BIG_URL = "http://" + "adsf" * 1035BIG_URL = "http://" + "adsf" * 10
3636
37AVAILABLE_RESOURCES = [
38 {"name": "cc-eal"},
39 {"name": "cis"},
40 {"name": "esm-apps"},
41 {"name": "esm-infra"},
42 {"name": "fips-updates"},
43 {"name": "fips"},
44 {"name": "livepatch"},
45 {"name": "ros-updates"},
46 {"name": "ros"},
47]
3748
38ALL_SERVICES_WRAPPED_HELP = textwrap.dedent(49ALL_SERVICES_WRAPPED_HELP = textwrap.dedent(
39 """50 """
40Client to manage Ubuntu Advantage services on a machine.51Client to manage Ubuntu Advantage services on a machine.
41 - cc-eal: Common Criteria EAL2 Provisioning Packages52 - cc-eal: Common Criteria EAL2 Provisioning Packages
42 (https://ubuntu.com/cc-eal)53 (https://ubuntu.com/cc-eal)
43 - cis: Center for Internet Security Audit Tools54 - cis: Security compliance and audit tools
44 (https://ubuntu.com/security/certifications#cis)55 (https://ubuntu.com/security/certifications/docs/usg)
45 - esm-apps: UA Apps: Extended Security Maintenance (ESM)56 - esm-apps: UA Apps: Extended Security Maintenance (ESM)
46 (https://ubuntu.com/security/esm)57 (https://ubuntu.com/security/esm)
47 - esm-infra: UA Infra: Extended Security Maintenance (ESM)58 - esm-infra: UA Infra: Extended Security Maintenance (ESM)
@@ -64,8 +75,8 @@ SERVICES_WRAPPED_HELP = textwrap.dedent(
64Client to manage Ubuntu Advantage services on a machine.75Client to manage Ubuntu Advantage services on a machine.
65 - cc-eal: Common Criteria EAL2 Provisioning Packages76 - cc-eal: Common Criteria EAL2 Provisioning Packages
66 (https://ubuntu.com/cc-eal)77 (https://ubuntu.com/cc-eal)
67 - cis: Center for Internet Security Audit Tools78 - cis: Security compliance and audit tools
68 (https://ubuntu.com/security/certifications#cis)79 (https://ubuntu.com/security/certifications/docs/usg)
69 - esm-infra: UA Infra: Extended Security Maintenance (ESM)80 - esm-infra: UA Infra: Extended Security Maintenance (ESM)
70 (https://ubuntu.com/security/esm)81 (https://ubuntu.com/security/esm)
71 - fips-updates: NIST-certified core packages with priority security updates82 - fips-updates: NIST-certified core packages with priority security updates
@@ -119,17 +130,21 @@ class TestCLIParser:
119 maxDiff = None130 maxDiff = None
120131
121 @mock.patch("uaclient.cli.entitlements")132 @mock.patch("uaclient.cli.entitlements")
133 @mock.patch("uaclient.cli.contract")
122 def test_help_descr_and_url_is_wrapped_at_eighty_chars(134 def test_help_descr_and_url_is_wrapped_at_eighty_chars(
123 self, m_entitlements, get_help135 self, m_contract, m_entitlements, get_help
124 ):136 ):
125 """Help lines are wrapped at 80 chars"""137 """Help lines are wrapped at 80 chars"""
126138
127 def cls_mock_factory(desc, url):139 mocked_ent = mock.MagicMock(
128 return mock.Mock(description=desc, help_doc_url=url, is_beta=False)140 presentation_name="test",
141 description=BIG_DESC,
142 help_doc_url=BIG_URL,
143 is_beta=False,
144 )
129145
130 m_entitlements.ENTITLEMENT_CLASS_BY_NAME = {146 m_entitlements.entitlement_factory.return_value = mocked_ent
131 "test": cls_mock_factory(BIG_DESC, BIG_URL)147 m_contract.get_available_resources.return_value = [{"name": "test"}]
132 }
133148
134 lines = [149 lines = [
135 " - test: " + " ".join(["123456789"] * 7),150 " - test: " + " ".join(["123456789"] * 7),
@@ -138,10 +153,13 @@ class TestCLIParser:
138 out, _ = get_help()153 out, _ = get_help()
139 assert "\n".join(lines) in out154 assert "\n".join(lines) in out
140155
141 def test_help_sourced_dynamically_from_each_entitlement(self, get_help):156 @mock.patch("uaclient.cli.contract")
157 def test_help_sourced_dynamically_from_each_entitlement(
158 self, m_contract, get_help
159 ):
142 """Help output is sourced from entitlement name and description."""160 """Help output is sourced from entitlement name and description."""
161 m_contract.get_available_resources.return_value = AVAILABLE_RESOURCES
143 out, type_request = get_help()162 out, type_request = get_help()
144
145 if type_request == "base":163 if type_request == "base":
146 assert SERVICES_WRAPPED_HELP in out164 assert SERVICES_WRAPPED_HELP in out
147 else:165 else:
@@ -167,8 +185,6 @@ class TestCLIParser:
167 self, m_attached, m_available_resources, out_format, expected_return185 self, m_attached, m_available_resources, out_format, expected_return
168 ):186 ):
169 """Test help command for a valid service in an unnatached ua client."""187 """Test help command for a valid service in an unnatached ua client."""
170 import uaclient.entitlements as ent
171
172 m_args = mock.MagicMock()188 m_args = mock.MagicMock()
173 m_service_name = mock.PropertyMock(return_value="test")189 m_service_name = mock.PropertyMock(return_value="test")
174 type(m_args).service = m_service_name190 type(m_args).service = m_service_name
@@ -189,8 +205,9 @@ class TestCLIParser:
189 ]205 ]
190206
191 fake_stdout = io.StringIO()207 fake_stdout = io.StringIO()
192 with mock.patch.object(208 with mock.patch(
193 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}209 "uaclient.entitlements.entitlement_factory",
210 return_value=m_entitlement_cls,
194 ):211 ):
195 with contextlib.redirect_stdout(fake_stdout):212 with contextlib.redirect_stdout(fake_stdout):
196 action_help(m_args, cfg=None)213 action_help(m_args, cfg=None)
@@ -222,8 +239,6 @@ class TestCLIParser:
222 self, m_attached, m_available_resources, ent_status, ent_msg, is_beta239 self, m_attached, m_available_resources, ent_status, ent_msg, is_beta
223 ):240 ):
224 """Test help command for a valid service in an attached ua client."""241 """Test help command for a valid service in an attached ua client."""
225 import uaclient.entitlements as ent
226
227 m_args = mock.MagicMock()242 m_args = mock.MagicMock()
228 m_service_name = mock.PropertyMock(return_value="test")243 m_service_name = mock.PropertyMock(return_value="test")
229 type(m_args).service = m_service_name244 type(m_args).service = m_service_name
@@ -256,7 +271,7 @@ class TestCLIParser:
256271
257 status_msg = "enabled" if ent_msg == "yes" else "—"272 status_msg = "enabled" if ent_msg == "yes" else "—"
258 ufs_call_count = 1 if ent_msg == "yes" else 0273 ufs_call_count = 1 if ent_msg == "yes" else 0
259 ent_name_call_count = 3 if ent_msg == "yes" else 2274 ent_name_call_count = 2 if ent_msg == "yes" else 1
260 is_beta_call_count = 1 if status_msg == "enabled" else 0275 is_beta_call_count = 1 if status_msg == "enabled" else 0
261276
262 expected_msgs = [277 expected_msgs = [
@@ -275,8 +290,9 @@ class TestCLIParser:
275 expected_msg = "\n\n".join(expected_msgs)290 expected_msg = "\n\n".join(expected_msgs)
276291
277 fake_stdout = io.StringIO()292 fake_stdout = io.StringIO()
278 with mock.patch.object(293 with mock.patch(
279 ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}294 "uaclient.entitlements.entitlement_factory",
295 return_value=m_entitlement_cls,
280 ):296 ):
281 with contextlib.redirect_stdout(fake_stdout):297 with contextlib.redirect_stdout(fake_stdout):
282 action_help(m_args, cfg=None)298 action_help(m_args, cfg=None)
@@ -527,7 +543,7 @@ class TestMain:
527 "exception,expected_exit_code",543 "exception,expected_exit_code",
528 [544 [
529 (UserFacingError("You need to know about this."), 1),545 (UserFacingError("You need to know about this."), 1),
530 (AlreadyAttachedError(mock.MagicMock()), 0),546 (AlreadyAttachedError(mock.MagicMock()), 2),
531 (547 (
532 LockHeldError(548 LockHeldError(
533 pid="123",549 pid="123",
@@ -653,7 +669,8 @@ class TestMain:
653 assert "UA_FEATURES_WOW=XYZ" in log669 assert "UA_FEATURES_WOW=XYZ" in log
654 assert "NOT_UA_ENV=YES" not in log670 assert "NOT_UA_ENV=YES" not in log
655671
656 def test_argparse_errors_well_formatted(self, capsys):672 @mock.patch("uaclient.cli.contract.get_available_resources")
673 def test_argparse_errors_well_formatted(self, _m_resources, capsys):
657 parser = get_parser()674 parser = get_parser()
658 with mock.patch("sys.argv", ["ua", "enable"]):675 with mock.patch("sys.argv", ["ua", "enable"]):
659 with pytest.raises(SystemExit) as excinfo:676 with pytest.raises(SystemExit) as excinfo:
@@ -834,14 +851,11 @@ class TestSetupLogging:
834851
835852
836class TestGetValidEntitlementNames:853class TestGetValidEntitlementNames:
837 @mock.patch("uaclient.cli.entitlements")854 @mock.patch(
838 def test_get_valid_entitlements(self, m_entitlements):855 "uaclient.cli.entitlements.valid_services",
839 m_entitlements.ENTITLEMENT_CLASS_BY_NAME = {856 return_value=["ent1", "ent2", "ent3"],
840 "ent1": True,857 )
841 "ent2": True,858 def test_get_valid_entitlements(self, _m_valid_services):
842 "ent3": True,
843 }
844
845 service = ["ent1", "ent3", "ent4"]859 service = ["ent1", "ent3", "ent4"]
846 expected_ents_found = ["ent1", "ent3"]860 expected_ents_found = ["ent1", "ent3"]
847 expected_ents_not_found = ["ent4"]861 expected_ents_not_found = ["ent4"]
diff --git a/uaclient/tests/test_cli_attach.py b/uaclient/tests/test_cli_attach.py
index 0be920d..49f7282 100644
--- a/uaclient/tests/test_cli_attach.py
+++ b/uaclient/tests/test_cli_attach.py
@@ -237,26 +237,27 @@ class TestActionAttach:
237 assert [mock.call(cfg)] == update_ua_messages.call_args_list237 assert [mock.call(cfg)] == update_ua_messages.call_args_list
238238
239239
240@mock.patch(M_PATH + "contract.get_available_resources")
240class TestParser:241class TestParser:
241 def test_attach_parser_usage(self):242 def test_attach_parser_usage(self, _m_resources):
242 parser = attach_parser(mock.Mock())243 parser = attach_parser(mock.Mock())
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: