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
1diff --git a/README.md b/README.md
2index c2c236b..27818e1 100644
3--- a/README.md
4+++ b/README.md
5@@ -1,7 +1,5 @@
6 # Ubuntu Advantage Client
7
8-[![Build Status](https://travis-ci.com/canonical/ubuntu-advantage-client.svg?branch=master)](https://travis-ci.com/github/canonical/ubuntu-advantage-client)
9-
10 The Ubuntu Advantage client provides users with a simple mechanism to
11 view, enable, and disable offerings from Canonical on their system. The
12 following entitlements are supported:
13@@ -105,7 +103,7 @@ https://ubuntu.com/advantage.
14 token, service credentials, affordances, directives and obligations to allow
15 enabling and disabling Ubuntu Advantage services
16 * UA client writes the machine token API response to the root-readonly
17- /var/lib/ubuntu-advantage/machine-token.json
18+ /var/lib/ubuntu-advantage/private/machine-token.json
19 * UA client auto-enables any services defined with
20 `obligations:{enableByDefault: true}`
21
22@@ -152,7 +150,7 @@ Jobs are executed by the timer script if:
23 - Their interval since last successful run is already exceeded.
24
25 There is a random delay applied to the timer, to desynchronize job execution time
26-on machines spinned at the same time, avoiding multiple synchronized calls to the
27+on machines spun at the same time, avoiding multiple synchronized calls to the
28 same service.
29
30 Current jobs being checked and executed are:
31@@ -576,5 +574,5 @@ sudo shutdown -h now
32 * Use your cloud platform to clone or snapshot this VM as a golden image
33
34
35-## Releasing ubuntu-adantage-tools
36+## Releasing ubuntu-advantage-tools
37 see [RELEASES.md](RELEASES.md)
38diff --git a/RELEASES.md b/RELEASES.md
39index f629962..b4948f7 100644
40--- a/RELEASES.md
41+++ b/RELEASES.md
42@@ -183,22 +183,21 @@ If this is your first time releasing ubuntu-advantage-tools, you'll need to do t
43
44 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.
45
46- 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).
47+ 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.
48 * Some issues may just be filed for addressing in the future if they are not urgent or pertinent to this release.
49 * 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.
50+ * 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.
51
52 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.
53
54- d. Check `rmadison ubuntu-advantage-tools` for updated version in devel release
55+ d. At this point the Server Team member should **not** upload the version to the devel release.
56+ * If they do, then any changes to the code after this point will require a bump in the patch version of the release.
57
58- 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`
59- * Note that any changes to the code after this point will likely require a bump in the patch version of the release.
60+ 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.
61
62- 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.
63+ f. Once upload review is complete and approved, confirm that Ubuntu Server approver will upload ua-tools via dput to the `-proposed` queue.
64
65- g. Once upload review is complete and approved, confirm that Ubuntu Server approver will upload ua-tools via dput to the `-proposed` queue.
66-
67- 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".
68+ 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".
69
70 5. SRU Review
71
72@@ -240,7 +239,13 @@ If this is your first time releasing ubuntu-advantage-tools, you'll need to do t
73
74 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).
75
76- 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.
77+ 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.
78+
79+ j. Ping the Ubuntu Server team member who approved the version in step `II.4` to now upload to the devel release.
80+
81+ k. Check `rmadison ubuntu-advantage-tools` for updated version in devel release
82+
83+ 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`
84
85 ### III. Final release to team infrastructure
86
87diff --git a/debian/changelog b/debian/changelog
88index 9779f56..a55d9dd 100644
89--- a/debian/changelog
90+++ b/debian/changelog
91@@ -1,3 +1,33 @@
92+ubuntu-advantage-tools (27.5~22.04.1) jammy; urgency=medium
93+
94+ * d/control:
95+ - Update homepage URL
96+ * d/tools.postinst:
97+ - Refactor to use valid_services
98+ * d/tools.postrm:
99+ - Use a wildcard to remove ua related gpg files
100+ * New upstream release 27.5 (LP: #1956456)
101+ - aws: add support for the IPv6 metadata endpoint
102+ - cis: update URL for the documentation
103+ - cli:
104+ + add endpoint to simulate the status using a specific contract token
105+ + fix return code when attaching an already attached machine (GH: #1867)
106+ + fix security-status to consider all possible origins to show updates
107+ + include cloud build.info in the collect-logs tarball
108+ + only show services which exist in the contracts server in ua status
109+ - docs: fix typos and wrong/outdated information
110+ - livepatch: always use the full path in livepatch calls (LP: #1951954)
111+ - logs:
112+ + improve rules to redact sensitive information from all log files
113+ + redact sensitive information from older unredacted log files
114+ + log errors from external software execution, for debugging purposes
115+ - usg:
116+ + support the presentedAs affordance from the contract server, showing
117+ services in the CLI with the appropriate names
118+ + replace the CIS entitlement by USG on Focal and onwards
119+
120+ -- Renan Rodrigo <renanrodrigo@canonical.com> Tue, 04 Jan 2022 17:30:26 -0300
121+
122 ubuntu-advantage-tools (27.4.2~22.04.1) jammy; urgency=medium
123
124 * d/tools.postinst:
125diff --git a/debian/control b/debian/control
126index 9ec72f6..6aabba2 100644
127--- a/debian/control
128+++ b/debian/control
129@@ -29,7 +29,7 @@ Build-Depends: bash-completion,
130 python3-setuptools,
131 python3-yaml
132 Standards-Version: 4.5.1
133-Homepage: https://buy.ubuntu.com
134+Homepage: https://ubuntu.com/advantage
135 Vcs-Git: https://github.com/CanonicalLtd/ubuntu-advantage-script.git
136 Vcs-Browser: https://github.com/CanonicalLtd/ubuntu-advantage-script
137 Rules-Requires-Root: no
138diff --git a/debian/ubuntu-advantage-tools.postinst b/debian/ubuntu-advantage-tools.postinst
139index 8ba7c64..191f0db 100644
140--- a/debian/ubuntu-advantage-tools.postinst
141+++ b/debian/ubuntu-advantage-tools.postinst
142@@ -57,8 +57,8 @@ MACHINE_TOKEN_FILE="/var/lib/ubuntu-advantage/private/machine-token.json"
143 redact_ubuntu_release_from_ua_apt_filenames() {
144 DIR=$1
145 UA_SERVICES=$(/usr/bin/python3 -c "
146-from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
147-print(*ENTITLEMENT_CLASS_BY_NAME.keys(), sep=' ')
148+from uaclient.entitlements import valid_services
149+print(*valid_services(allow_beta=True, all_names=True), sep=' ')
150 ")
151
152 for file in "$DIR"/*; do
153@@ -118,8 +118,8 @@ check_service_is_beta() {
154 service_name=$1
155 _IS_BETA_SVC=$(/usr/bin/python3 -c "
156 from uaclient.config import UAConfig
157-from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
158-ent_cls = ENTITLEMENT_CLASS_BY_NAME.get('${service_name}')
159+from uaclient.entitlements import entitlement_factory
160+ent_cls = entitlement_factory('${service_name}')
161 if ent_cls:
162 cfg = UAConfig()
163 allow_beta = cfg.features.get('allow_beta', False)
164diff --git a/debian/ubuntu-advantage-tools.postrm b/debian/ubuntu-advantage-tools.postrm
165index 978b492..ce79341 100644
166--- a/debian/ubuntu-advantage-tools.postrm
167+++ b/debian/ubuntu-advantage-tools.postrm
168@@ -19,9 +19,7 @@ remove_logs(){
169 }
170
171 remove_gpg_files(){
172- rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-esm-infra-trusty.gpg
173- rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-esm-apps.gpg
174- rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-fips.gpg
175+ rm -f /etc/apt/trusted.gpg.d/ubuntu-advantage-*.gpg
176 }
177
178 case "$1" in
179diff --git a/features/_version.feature b/features/_version.feature
180index 0ee9f1b..e265769 100644
181--- a/features/_version.feature
182+++ b/features/_version.feature
183@@ -12,18 +12,27 @@ Feature: UA is expected version
184 @uses.config.machine_type.gcp.pro
185 Scenario Outline: Check ua version
186 Given a `<release>` machine with ubuntu-advantage-tools installed
187- When I run `ua version` with sudo
188- Then I will see the following on stdout
189+ When I run `dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools` with sudo
190+ Then stdout matches regexp:
191 """
192 {UACLIENT_BEHAVE_CHECK_VERSION}
193 """
194+ When I run `ua version` with sudo
195+ Then stdout matches regexp:
196+ # We are adding that regex here to match possible config overrides
197+ # we add. For example, on PRO machines we add a config override to
198+ # disable auto-attach on boot
199+ """
200+ {UACLIENT_BEHAVE_CHECK_VERSION}.*
201+ """
202 Examples: version
203 | release |
204 | xenial |
205 | bionic |
206 | focal |
207 | hirsute |
208- | impish |
209+ | impish |
210+ | jammy |
211
212 @series.all
213 @uses.config.check_version
214@@ -31,15 +40,23 @@ Feature: UA is expected version
215 @upgrade
216 Scenario Outline: Check ua version
217 Given a `<release>` machine with ubuntu-advantage-tools installed
218- When I run `ua version` with sudo
219+ When I run `dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools` with sudo
220 Then I will see the following on stdout
221 """
222 {UACLIENT_BEHAVE_CHECK_VERSION}
223 """
224+ When I run `ua version` with sudo
225+ Then stdout matches regexp:
226+ # We are adding that regex here to match possible config overrides
227+ # we add. For example, on PRO machines we add a config override to
228+ # disable auto-attach on boot
229+ """
230+ {UACLIENT_BEHAVE_CHECK_VERSION}.*
231+ """
232 Examples: version
233 | release |
234 | xenial |
235 | bionic |
236 | focal |
237 | hirsute |
238- | impish |
239+ | impish |
240diff --git a/features/attach_invalidtoken.feature b/features/attach_invalidtoken.feature
241index 776ca00..6ad2d93 100644
242--- a/features/attach_invalidtoken.feature
243+++ b/features/attach_invalidtoken.feature
244@@ -21,7 +21,8 @@ Feature: Command behaviour when trying to attach a machine to an Ubuntu
245 | bionic |
246 | focal |
247 | hirsute |
248- | impish |
249+ | impish |
250+ | jammy |
251
252 @uses.config.contract_token_staging_expired
253 @series.all
254@@ -41,4 +42,5 @@ Feature: Command behaviour when trying to attach a machine to an Ubuntu
255 | bionic |
256 | focal |
257 | hirsute |
258- | impish |
259+ | impish |
260+ | jammy |
261diff --git a/features/attach_validtoken.feature b/features/attach_validtoken.feature
262index cfd1e44..ec4400b 100644
263--- a/features/attach_validtoken.feature
264+++ b/features/attach_validtoken.feature
265@@ -2,6 +2,7 @@
266 Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
267 subscription using a valid token
268
269+ @series.jammy
270 @series.hirsute
271 @series.impish
272 @uses.config.machine_type.lxd.container
273@@ -13,7 +14,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
274 """
275 SERVICE ENTITLED STATUS DESCRIPTION
276 cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages
277- cis +yes +n/a +Center for Internet Security Audit Tools
278+ cis +yes +n/a +Security compliance and audit tools
279 esm-apps +yes +n/a +UA Apps: Extended Security Maintenance \(ESM\)
280 esm-infra +yes +n/a +UA Infra: Extended Security Maintenance \(ESM\)
281 fips +yes +n/a +NIST-certified core packages
282@@ -24,7 +25,8 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
283 Examples: ubuntu release
284 | release |
285 | hirsute |
286- | impish |
287+ | impish |
288+ | jammy |
289
290 @series.lts
291 @uses.config.machine_type.lxd.container
292@@ -47,7 +49,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
293 """
294 \d+ update(s)? can be applied immediately.
295 """
296- When I attach `contract_token` with sudo
297+ When I attach `contract_token_staging` with sudo
298 Then stdout matches regexp:
299 """
300 UA Infra: ESM enabled
301@@ -60,17 +62,29 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
302 """
303 SERVICE ENTITLED STATUS DESCRIPTION
304 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
305- cis +yes +disabled +Center for Internet Security Audit Tools
306+ """
307+ And stdout matches regexp:
308+ """
309 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
310 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
311 fips +yes +n/a +NIST-certified core packages
312 fips-updates +yes +n/a +NIST-certified core packages with priority security updates
313 livepatch +yes +n/a +Canonical Livepatch service
314 """
315+ And stdout matches regexp:
316+ """
317+ <cis_or_usg> +yes +disabled +Security compliance and audit tools
318+ """
319 And stderr matches regexp:
320 """
321 Enabling default service esm-infra
322 """
323+ When I verify that running `ua attach contract_token` `with sudo` exits `2`
324+ Then stderr matches regexp:
325+ """
326+ This machine is already attached to '.+'
327+ To use a different subscription first run: sudo ua detach.
328+ """
329 When I run `ua disable esm-apps --assume-yes` with sudo
330 When I append the following on uaclient config:
331 """
332@@ -154,10 +168,10 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
333
334 """
335 Examples: ubuntu release packages
336- | release | downrev_pkg | cc_status |
337- | xenial | libkrad0=1.13.2+dfsg-5 | disabled |
338- | bionic | libkrad0=1.16-2build1 | n/a |
339- | focal | hello=2.10-2ubuntu2 | n/a |
340+ | release | downrev_pkg | cc_status | cis_or_usg |
341+ | xenial | libkrad0=1.13.2+dfsg-5 | disabled | cis |
342+ | bionic | libkrad0=1.16-2build1 | n/a | cis |
343+ | focal | hello=2.10-2ubuntu2 | n/a | usg |
344
345 @series.all
346 @uses.config.machine_type.aws.generic
347@@ -196,22 +210,28 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
348 """
349 SERVICE ENTITLED STATUS DESCRIPTION
350 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
351- cis +yes +disabled +Center for Internet Security Audit Tools
352+ """
353+ And stdout matches regexp:
354+ """
355 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
356 fips +yes +<fips_status> +NIST-certified core packages
357 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
358 livepatch +yes +<lp_status> +<lp_desc>
359 """
360+ And stdout matches regexp:
361+ """
362+ <cis_or_usg> +yes +disabled +Security compliance and audit tools
363+ """
364 And stderr matches regexp:
365 """
366 Enabling default service esm-infra
367 """
368
369 Examples: ubuntu release livepatch status
370- | release | fips_status |lp_status | lp_desc | cc_status |
371- | xenial | disabled |enabled | Canonical Livepatch service | disabled |
372- | bionic | disabled |enabled | Canonical Livepatch service | n/a |
373- | focal | n/a |enabled | Canonical Livepatch service | n/a |
374+ | release | fips_status |lp_status | lp_desc | cc_status | cis_or_usg |
375+ | xenial | disabled |enabled | Canonical Livepatch service | disabled | cis |
376+ | bionic | disabled |enabled | Canonical Livepatch service | n/a | cis |
377+ | focal | n/a |enabled | Canonical Livepatch service | n/a | usg |
378
379 @series.all
380 @uses.config.machine_type.azure.generic
381@@ -250,22 +270,28 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
382 """
383 SERVICE ENTITLED STATUS DESCRIPTION
384 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
385- cis +yes +disabled +Center for Internet Security Audit Tools
386+ """
387+ And stdout matches regexp:
388+ """
389 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
390 fips +yes +<fips_status> +NIST-certified core packages
391 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
392 livepatch +yes +<lp_status> +Canonical Livepatch service
393 """
394+ And stdout matches regexp:
395+ """
396+ <cis_or_usg> +yes +disabled +Security compliance and audit tools
397+ """
398 And stderr matches regexp:
399 """
400 Enabling default service esm-infra
401 """
402
403 Examples: ubuntu release livepatch status
404- | release | lp_status | fips_status | cc_status |
405- | xenial | n/a | n/a | disabled |
406- | bionic | n/a | disabled | n/a |
407- | focal | enabled | n/a | n/a |
408+ | release | lp_status | fips_status | cc_status | cis_or_usg |
409+ | xenial | n/a | n/a | disabled | cis |
410+ | bionic | n/a | disabled | n/a | cis |
411+ | focal | enabled | n/a | n/a | usg |
412
413 @series.all
414 @uses.config.machine_type.gcp.generic
415@@ -291,7 +317,7 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
416 features:
417 machine_token_overlay: "/tmp/machine-token-overlay.json"
418 """
419- And I attach `contract_token` with sudo
420+ And I attach `contract_token_staging` with sudo
421 Then stdout matches regexp:
422 """
423 UA Infra: ESM enabled
424@@ -304,19 +330,25 @@ Feature: Command behaviour when attaching a machine to an Ubuntu Advantage
425 """
426 SERVICE ENTITLED STATUS DESCRIPTION
427 cc-eal +yes +<cc_status> +Common Criteria EAL2 Provisioning Packages
428- cis +yes +disabled +Center for Internet Security Audit Tools
429+ """
430+ And stdout matches regexp:
431+ """
432 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
433 fips +yes +<fips_status> +NIST-certified core packages
434 fips-updates +yes +<fips_status> +NIST-certified core packages with priority security updates
435 livepatch +yes +<lp_status> +Canonical Livepatch service
436 """
437+ And stdout matches regexp:
438+ """
439+ <cis_or_usg> +yes +disabled +Security compliance and audit tools
440+ """
441 And stderr matches regexp:
442 """
443 Enabling default service esm-infra
444 """
445
446 Examples: ubuntu release livepatch status
447- | release | lp_status | fips_status | cc_status |
448- | xenial | n/a | n/a | disabled |
449- | bionic | n/a | disabled | n/a |
450- | focal | enabled | n/a | n/a |
451+ | release | lp_status | fips_status | cc_status | cis_or_usg |
452+ | xenial | n/a | n/a | disabled | cis |
453+ | bionic | n/a | disabled | n/a | cis |
454+ | focal | enabled | n/a | n/a | usg |
455diff --git a/features/attached_commands.feature b/features/attached_commands.feature
456index 0ef79db..d8718ae 100644
457--- a/features/attached_commands.feature
458+++ b/features/attached_commands.feature
459@@ -49,6 +49,7 @@ Feature: Command behaviour when attached to an UA subscription
460 | xenial |
461 | hirsute |
462 | impish |
463+ | jammy |
464
465 @series.all
466 @uses.config.machine_type.lxd.container
467@@ -65,6 +66,7 @@ Feature: Command behaviour when attached to an UA subscription
468 | xenial |
469 | hirsute |
470 | impish |
471+ | jammy |
472
473 @series.all
474 @uses.config.machine_type.lxd.container
475@@ -91,6 +93,7 @@ Feature: Command behaviour when attached to an UA subscription
476 | xenial |
477 | hirsute |
478 | impish |
479+ | jammy |
480
481 @series.all
482 @uses.config.machine_type.lxd.container
483@@ -116,6 +119,7 @@ Feature: Command behaviour when attached to an UA subscription
484 | xenial |
485 | hirsute |
486 | impish |
487+ | jammy |
488
489 @series.lts
490 @uses.config.machine_type.lxd.container
491@@ -161,7 +165,7 @@ Feature: Command behaviour when attached to an UA subscription
492 @uses.config.machine_type.lxd.container
493 Scenario Outline: Attached detach in an ubuntu machine
494 Given a `<release>` machine with ubuntu-advantage-tools installed
495- When I attach `contract_token` with sudo
496+ When I attach `contract_token_staging` with sudo
497 Then I verify that running `ua detach` `as non-root` exits `1`
498 And stderr matches regexp:
499 """
500@@ -182,7 +186,9 @@ Feature: Command behaviour when attached to an UA subscription
501 """
502 SERVICE AVAILABLE DESCRIPTION
503 cc-eal +<cc-eal> +Common Criteria EAL2 Provisioning Packages
504- cis +<cis> +Center for Internet Security Audit Tools
505+ """
506+ Then stdout matches regexp:
507+ """
508 esm-apps +<esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
509 esm-infra +yes +UA Infra: Extended Security Maintenance \(ESM\)
510 fips +<fips> +NIST-certified core packages
511@@ -191,6 +197,10 @@ Feature: Command behaviour when attached to an UA subscription
512 ros +<ros> +Security Updates for the Robot Operating System
513 ros-updates +<ros> +All Updates for the Robot Operating System
514 """
515+ Then stdout matches regexp:
516+ """
517+ <cis_or_usg> +<cis> +Security compliance and audit tools
518+ """
519 And stdout matches regexp:
520 """
521 This machine is not attached to a UA subscription.
522@@ -198,10 +208,10 @@ Feature: Command behaviour when attached to an UA subscription
523 And I verify that running `apt update` `with sudo` exits `0`
524
525 Examples: ubuntu release
526- | release | esm-apps | cc-eal | cis | fips | fips-update | ros |
527- | bionic | yes | no | yes | yes | yes | yes |
528- | focal | yes | no | yes | yes | yes | no |
529- | xenial | yes | yes | yes | yes | yes | yes |
530+ | release | esm-apps | cc-eal | cis | fips | fips-update | ros | cis_or_usg |
531+ | xenial | yes | yes | yes | yes | yes | yes | cis |
532+ | bionic | yes | no | yes | yes | yes | yes | cis |
533+ | focal | yes | no | yes | yes | yes | no | usg |
534
535 @series.all
536 @uses.config.machine_type.lxd.container
537@@ -213,7 +223,7 @@ Feature: Command behaviour when attached to an UA subscription
538 """
539 This command must be run as root \(try using sudo\).
540 """
541- When I run `ua auto-attach` with sudo
542+ When I verify that running `ua auto-attach` `with sudo` exits `2`
543 Then stderr matches regexp:
544 """
545 This machine is already attached
546@@ -226,6 +236,7 @@ Feature: Command behaviour when attached to an UA subscription
547 | xenial |
548 | hirsute |
549 | impish |
550+ | jammy |
551
552 @series.all
553 @uses.config.machine_type.lxd.container
554@@ -248,6 +259,7 @@ Feature: Command behaviour when attached to an UA subscription
555 | xenial |
556 | hirsute |
557 | impish |
558+ | jammy |
559
560 @series.all
561 @uses.config.machine_type.lxd.container
562@@ -299,6 +311,7 @@ Feature: Command behaviour when attached to an UA subscription
563 | xenial |
564 | hirsute |
565 | impish |
566+ | jammy |
567
568 @series.lts
569 @uses.config.machine_type.lxd.container
570@@ -389,8 +402,8 @@ Feature: Command behaviour when attached to an UA subscription
571 Client to manage Ubuntu Advantage services on a machine.
572 - cc-eal: Common Criteria EAL2 Provisioning Packages
573 \(https://ubuntu.com/cc-eal\)
574- - cis: Center for Internet Security Audit Tools
575- \(https://ubuntu.com/security/certifications#cis\)
576+ - cis: Security compliance and audit tools
577+ \(https://ubuntu.com/security/certifications/docs/usg\)
578 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
579 \(https://ubuntu.com/security/esm\)
580 - fips-updates: NIST-certified core packages with priority security updates
581@@ -406,8 +419,8 @@ Feature: Command behaviour when attached to an UA subscription
582 Client to manage Ubuntu Advantage services on a machine.
583 - cc-eal: Common Criteria EAL2 Provisioning Packages
584 \(https://ubuntu.com/cc-eal\)
585- - cis: Center for Internet Security Audit Tools
586- \(https://ubuntu.com/security/certifications#cis\)
587+ - cis: Security compliance and audit tools
588+ \(https://ubuntu.com/security/certifications/docs/usg\)
589 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
590 \(https://ubuntu.com/security/esm\)
591 - fips-updates: NIST-certified core packages with priority security updates
592@@ -423,8 +436,8 @@ Feature: Command behaviour when attached to an UA subscription
593 Client to manage Ubuntu Advantage services on a machine.
594 - cc-eal: Common Criteria EAL2 Provisioning Packages
595 \(https://ubuntu.com/cc-eal\)
596- - cis: Center for Internet Security Audit Tools
597- \(https://ubuntu.com/security/certifications#cis\)
598+ - cis: Security compliance and audit tools
599+ \(https://ubuntu.com/security/certifications/docs/usg\)
600 - esm-apps: UA Apps: Extended Security Maintenance \(ESM\)
601 \(https://ubuntu.com/security/esm\)
602 - esm-infra: UA Infra: Extended Security Maintenance \(ESM\)
603@@ -448,6 +461,7 @@ Feature: Command behaviour when attached to an UA subscription
604 | xenial | enabled |
605 | hirsute | n/a |
606 | impish | n/a |
607+ | jammy | n/a |
608
609 @series.lts
610 @uses.config.machine_type.lxd.container
611@@ -570,6 +584,7 @@ Feature: Command behaviour when attached to an UA subscription
612 | focal |
613 | hirsute |
614 | impish |
615+ | jammy |
616
617 @series.lts
618 @uses.config.machine_type.lxd.container
619@@ -591,6 +606,7 @@ Feature: Command behaviour when attached to an UA subscription
620 # So the -error suffix does not appear there.
621 Then stdout matches regexp:
622 """
623+ build.info
624 cloud-id.txt
625 jobs-status.json
626 journalctl.txt
627diff --git a/features/attached_enable.feature b/features/attached_enable.feature
628index 289f048..2c29429 100644
629--- a/features/attached_enable.feature
630+++ b/features/attached_enable.feature
631@@ -142,6 +142,7 @@ Feature: Enable command behaviour when attached to an UA subscription
632 | xenial |
633 | hirsute |
634 | impish |
635+ | jammy |
636
637 @series.lts
638 @uses.config.machine_type.lxd.container
639@@ -187,11 +188,12 @@ Feature: Enable command behaviour when attached to an UA subscription
640 | focal |
641 | xenial |
642
643- @series.lts
644+ @series.xenial
645+ @series.bionic
646 @uses.config.machine_type.lxd.container
647 Scenario Outline: Attached enable of cis service in a ubuntu machine
648 Given a `<release>` machine with ubuntu-advantage-tools installed
649- When I attach `contract_token` with sudo
650+ When I attach `contract_token_staging` with sudo
651 And I verify that running `ua enable cis` `with sudo` exits `0`
652 Then I will see the following on stdout:
653 """
654@@ -199,7 +201,7 @@ Feature: Enable command behaviour when attached to an UA subscription
655 Updating package lists
656 Installing CIS Audit packages
657 CIS Audit enabled
658- Visit https://security-certs.docs.ubuntu.com/en/cis to learn how to use CIS
659+ Visit https://ubuntu.com/security/cis to learn how to use CIS
660 """
661 When I run `apt-cache policy usg-cisbenchmark` as non-root
662 Then stdout does not match regexp:
663@@ -208,7 +210,7 @@ Feature: Enable command behaviour when attached to an UA subscription
664 """
665 And stdout matches regexp:
666 """
667- \s* 500 https://esm.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
668+ \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
669 """
670 When I run `apt-cache policy usg-common` as non-root
671 Then stdout does not match regexp:
672@@ -217,7 +219,7 @@ Feature: Enable command behaviour when attached to an UA subscription
673 """
674 And stdout matches regexp:
675 """
676- \s* 500 https://esm.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
677+ \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
678 """
679 When I verify that running `ua enable cis` `with sudo` exits `1`
680 Then stdout matches regexp
681@@ -256,29 +258,171 @@ Feature: Enable command behaviour when attached to an UA subscription
682 CIS audit scan completed
683 """
684
685- Examples: not entitled services
686+ Examples: cis script
687 | release | cis_script |
688- | focal | Canonical_Ubuntu_20.04_CIS-harden.sh |
689 | bionic | Canonical_Ubuntu_18.04_CIS-harden.sh |
690 | xenial | Canonical_Ubuntu_16.04_CIS_v1.1.0-harden.sh |
691
692 @series.focal
693- @uses.config.machine_type.lxd.vm
694- Scenario: Attached enable of vm-based services in a focal lxd vm
695- Given a `focal` machine with ubuntu-advantage-tools installed
696- When I attach `contract_token` with sudo
697- Then I verify that running `ua enable fips --assume-yes` `with sudo` exits `1`
698- And I will see the following on stdout:
699+ @uses.config.machine_type.lxd.container
700+ Scenario Outline: Attached enable of cis service in a ubuntu machine
701+ Given a `<release>` machine with ubuntu-advantage-tools installed
702+ When I attach `contract_token_staging` with sudo
703+ And I verify that running `ua enable cis` `with sudo` exits `0`
704+ Then I will see the following on stdout:
705 """
706 One moment, checking your subscription first
707- FIPS is not available for Ubuntu 20.04 LTS (Focal Fossa).
708+ From Ubuntu 20.04 and onwards 'ua enable cis' has been
709+ replaced by 'ua enable usg'. See more information at:
710+ https://ubuntu.com/security/certifications/docs/usg
711+ Updating package lists
712+ Installing CIS Audit packages
713+ CIS Audit enabled
714+ Visit https://ubuntu.com/security/cis to learn how to use CIS
715 """
716- And I verify that running `ua enable fips-updates --assume-yes` `with sudo` exits `1`
717- And I will see the following on stdout:
718+ When I run `apt-cache policy usg-cisbenchmark` as non-root
719+ Then stdout does not match regexp:
720+ """
721+ .*Installed: \(none\)
722+ """
723+ And stdout matches regexp:
724+ """
725+ \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
726+ """
727+ When I run `apt-cache policy usg-common` as non-root
728+ Then stdout does not match regexp:
729+ """
730+ .*Installed: \(none\)
731+ """
732+ And stdout matches regexp:
733+ """
734+ \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu <release>/main amd64 Packages
735+ """
736+ When I verify that running `ua enable cis` `with sudo` exits `1`
737+ Then stdout matches regexp
738+ """
739+ One moment, checking your subscription first
740+ From Ubuntu 20.04 and onwards 'ua enable cis' has been
741+ replaced by 'ua enable usg'. See more information at:
742+ https://ubuntu.com/security/certifications/docs/usg
743+ CIS Audit is already enabled.
744+ See: sudo ua status
745+ """
746+ When I run `cis-audit level1_server` with sudo
747+ Then stdout matches regexp
748+ """
749+ Title.*Ensure no duplicate UIDs exist
750+ Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
751+ Result.*pass
752+ """
753+ And stdout matches regexp:
754+ """
755+ Title.*Ensure default user umask is 027 or more restrictive
756+ Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
757+ Result.*fail
758+ """
759+ And stdout matches regexp
760+ """
761+ CIS audit scan completed
762+ """
763+ When I verify that running `/usr/share/ubuntu-scap-security-guides/cis-hardening/<cis_script> lvl1_server` `with sudo` exits `0`
764+ And I run `cis-audit level1_server` with sudo
765+ Then stdout matches regexp:
766+ """
767+ Title.*Ensure default user umask is 027 or more restrictive
768+ Rule.*xccdf_com.ubuntu.<release>.cis_rule_CIS-.*
769+ Result.*pass
770+ """
771+ And stdout matches regexp
772+ """
773+ CIS audit scan completed
774+ """
775+
776+ Examples: cis script
777+ | release | cis_script |
778+ | focal | Canonical_Ubuntu_20.04_CIS-harden.sh |
779+
780+ @series.bionic
781+ @series.xenial
782+ @uses.config.machine_type.lxd.container
783+ Scenario Outline: Attached enable of usg service in a ubuntu machine
784+ Given a `<release>` machine with ubuntu-advantage-tools installed
785+ When I attach `contract_token_staging` with sudo
786+ And I verify that running `ua enable usg` `with sudo` exits `1`
787+ Then I will see the following on stdout:
788 """
789 One moment, checking your subscription first
790- FIPS Updates is not available for Ubuntu 20.04 LTS (Focal Fossa).
791 """
792+ Then I will see the following on stderr:
793+ """
794+ Cannot enable unknown service 'usg'.
795+ Try cc-eal, cis, esm-infra, fips, fips-updates, livepatch.
796+ """
797+
798+ Examples: cis service
799+ | release |
800+ | bionic |
801+ | xenial |
802+
803+ @series.focal
804+ @uses.config.machine_type.lxd.container
805+ Scenario Outline: Attached enable of usg service in a focal machine
806+ Given a `<release>` machine with ubuntu-advantage-tools installed
807+ When I attach `contract_token_staging` with sudo
808+ And I run `ua enable usg` with sudo
809+ Then I will see the following on stdout:
810+ """
811+ One moment, checking your subscription first
812+ Updating package lists
813+ Ubuntu Security Guide enabled
814+ Visit https://ubuntu.com/security/certifications/docs/usg for the next steps
815+ """
816+ When I run `ua status` with sudo
817+ Then stdout matches regexp:
818+ """
819+ usg +yes +enabled +Security compliance and audit tools
820+ """
821+ When I run `ua disable usg` with sudo
822+ Then stdout matches regexp:
823+ """
824+ Updating package lists
825+ """
826+ When I run `ua status` with sudo
827+ Then stdout matches regexp:
828+ """
829+ usg +yes +disabled +Security compliance and audit tools
830+ """
831+ When I run `ua enable cis` with sudo
832+ Then I will see the following on stdout:
833+ """
834+ One moment, checking your subscription first
835+ From Ubuntu 20.04 and onwards 'ua enable cis' has been
836+ replaced by 'ua enable usg'. See more information at:
837+ https://ubuntu.com/security/certifications/docs/usg
838+ Updating package lists
839+ Installing CIS Audit packages
840+ CIS Audit enabled
841+ Visit https://ubuntu.com/security/cis to learn how to use CIS
842+ """
843+ When I run `ua status` with sudo
844+ Then stdout matches regexp:
845+ """
846+ usg +yes +enabled +Security compliance and audit tools
847+ """
848+ When I run `ua disable usg` with sudo
849+ Then stdout matches regexp:
850+ """
851+ Updating package lists
852+ """
853+ When I run `ua status` with sudo
854+ Then stdout matches regexp:
855+ """
856+ usg +yes +disabled +Security compliance and audit tools
857+ """
858+
859+ Examples: cis service
860+ | release |
861+ | focal |
862
863 @series.bionic
864 @series.xenial
865@@ -289,7 +433,6 @@ Feature: Enable command behaviour when attached to an UA subscription
866 And I run `ua status` with sudo
867 Then stdout matches regexp:
868 """
869- cis +yes +disabled +Center for Internet Security Audit Tools
870 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
871 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
872 fips +yes +disabled +NIST-certified core packages
873@@ -306,7 +449,6 @@ Feature: Enable command behaviour when attached to an UA subscription
874 When I run `ua status` with sudo
875 Then stdout matches regexp:
876 """
877- cis +yes +disabled +Center for Internet Security Audit Tools
878 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
879 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
880 fips +yes +disabled +NIST-certified core packages
881diff --git a/features/attached_status.feature b/features/attached_status.feature
882index db826a7..78483ed 100644
883--- a/features/attached_status.feature
884+++ b/features/attached_status.feature
885@@ -11,14 +11,14 @@ Feature: Attached status
886 """
887 _doc _schema_version account attached config config_path contract effective
888 environment_vars execution_details execution_status expires machine_id notices
889- services version
890+ services version simulated
891 """
892 When I run `ua status --format yaml` as non-root
893 Then stdout is formatted as `yaml` and has keys:
894 """
895 _doc _schema_version account attached config config_path contract effective
896 environment_vars execution_details execution_status expires machine_id notices
897- services version
898+ services version simulated
899 """
900
901 Examples: ubuntu release
902@@ -28,3 +28,4 @@ Feature: Attached status
903 | xenial |
904 | hirsute |
905 | impish |
906+ | jammy |
907diff --git a/features/install_uninstall.feature b/features/install_uninstall.feature
908index 586f2ef..66426c8 100644
909--- a/features/install_uninstall.feature
910+++ b/features/install_uninstall.feature
911@@ -14,6 +14,7 @@ Feature: UA Install and Uninstall related tests
912 | focal |
913 | hirsute |
914 | impish |
915+ | jammy |
916
917 @series.lts
918 @uses.config.contract_token
919diff --git a/features/license_check.feature b/features/license_check.feature
920index 37c5bd0..4f41279 100644
921--- a/features/license_check.feature
922+++ b/features/license_check.feature
923@@ -92,6 +92,7 @@ Feature: License check timer only runs in environments where necessary
924 | focal |
925 | hirsute |
926 | impish |
927+ | jammy |
928
929 @series.lts
930 @uses.config.machine_type.aws.pro
931diff --git a/features/steps/steps.py b/features/steps/steps.py
932index e69595b..2c0d5bf 100644
933--- a/features/steps/steps.py
934+++ b/features/steps/steps.py
935@@ -229,6 +229,20 @@ def when_i_run_command_with_stdin(
936 )
937
938
939+@when("I do a preflight check for `{contract_token}` {user_spec}")
940+def when_i_preflight(context, contract_token, user_spec):
941+ token = getattr(context.config, contract_token)
942+ command = "ua status --simulate-with-token {}".format(token)
943+ if user_spec == "with the all flag":
944+ command += " --all"
945+ if "formatted as" in user_spec:
946+ output_format = user_spec.split()[2]
947+ command += " --format {}".format(output_format)
948+ when_i_run_command(
949+ context=context, command=command, user_spec="as non-root"
950+ )
951+
952+
953 @when("I run `{command}` {user_spec}")
954 def when_i_run_command(
955 context,
956diff --git a/features/ubuntu_pro.feature b/features/ubuntu_pro.feature
957index fca719a..cd95c86 100644
958--- a/features/ubuntu_pro.feature
959+++ b/features/ubuntu_pro.feature
960@@ -27,13 +27,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
961 """
962 SERVICE ENTITLED STATUS DESCRIPTION
963 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
964- cis +yes +<cis-s> +Center for Internet Security Audit Tools
965+ """
966+ Then stdout matches regexp:
967+ """
968 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
969 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
970 fips +yes +<fips-s> +NIST-certified core packages
971 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
972 livepatch +yes +enabled +Canonical Livepatch service
973 """
974+ Then stdout matches regexp:
975+ """
976+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
977+ """
978 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
979 Then stdout matches regexp:
980 """
981@@ -44,10 +50,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
982 .*CONNECT 169.254.169.254.*
983 """
984 Examples: ubuntu release
985- | release | fips-s | cc-eal-s | cis-s |
986- | xenial | disabled | disabled | disabled |
987- | bionic | disabled | n/a | disabled |
988- | focal | n/a | n/a | disabled |
989+ | release | fips-s | cc-eal-s | cis-s | cis_or_usg |
990+ | xenial | disabled | disabled | disabled | cis |
991+ | bionic | disabled | n/a | disabled | cis |
992+ | focal | n/a | n/a | disabled | usg |
993
994 @series.lts
995 @uses.config.machine_type.azure.pro
996@@ -76,13 +82,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
997 """
998 SERVICE ENTITLED STATUS DESCRIPTION
999 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1000- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1001+ """
1002+ Then stdout matches regexp:
1003+ """
1004 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1005 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1006 fips +yes +<fips-s> +NIST-certified core packages
1007 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1008 livepatch +yes +<livepatch-s> +Canonical Livepatch service
1009 """
1010+ Then stdout matches regexp:
1011+ """
1012+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1013+ """
1014 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
1015 Then stdout matches regexp:
1016 """
1017@@ -93,10 +105,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1018 .*CONNECT 169.254.169.254.*
1019 """
1020 Examples: ubuntu release
1021- | release | fips-s | cc-eal-s | cis-s | livepatch-s |
1022- | xenial | n/a | disabled | disabled | enabled |
1023- | bionic | disabled | n/a | disabled | n/a |
1024- | focal | n/a | n/a | disabled | enabled |
1025+ | release | fips-s | cc-eal-s | cis-s | livepatch-s | cis_or_usg |
1026+ | xenial | n/a | disabled | disabled | enabled | cis |
1027+ | bionic | disabled | n/a | disabled | n/a | cis |
1028+ | focal | n/a | n/a | disabled | enabled | usg |
1029
1030 @series.lts
1031 @uses.config.machine_type.gcp.pro
1032@@ -125,13 +137,19 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1033 """
1034 SERVICE ENTITLED STATUS DESCRIPTION
1035 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1036- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1037+ """
1038+ Then stdout matches regexp:
1039+ """
1040 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1041 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1042 fips +yes +<fips-s> +NIST-certified core packages
1043 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1044 livepatch +yes +<livepatch-s> +Canonical Livepatch service
1045 """
1046+ Then stdout matches regexp:
1047+ """
1048+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1049+ """
1050 When I run `cat /var/log/squid/access.log` `with sudo` on the `proxy` machine
1051 Then stdout matches regexp:
1052 """
1053@@ -142,10 +160,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1054 .*CONNECT metadata.*
1055 """
1056 Examples: ubuntu release
1057- | release | fips-s | cc-eal-s | cis-s | livepatch-s |
1058- | xenial | n/a | disabled | disabled | n/a |
1059- | bionic | disabled | n/a | disabled | n/a |
1060- | focal | n/a | n/a | disabled | enabled |
1061+ | release | fips-s | cc-eal-s | cis-s | livepatch-s | cis_or_usg |
1062+ | xenial | n/a | disabled | disabled | n/a | cis |
1063+ | bionic | disabled | n/a | disabled | n/a | cis |
1064+ | focal | n/a | n/a | disabled | enabled | usg |
1065
1066 @series.lts
1067 @uses.config.machine_type.aws.pro
1068@@ -165,27 +183,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1069 """
1070 SERVICE ENTITLED STATUS DESCRIPTION
1071 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1072- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1073+ """
1074+ Then stdout matches regexp:
1075+ """
1076 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1077 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1078 fips +yes +<fips-s> +NIST-certified core packages
1079 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1080 livepatch +yes +enabled +Canonical Livepatch service
1081 """
1082+ Then stdout matches regexp:
1083+ """
1084+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1085+ """
1086 When I run `ua status --all` as non-root
1087 Then stdout matches regexp:
1088 """
1089 SERVICE ENTITLED STATUS DESCRIPTION
1090 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1091- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1092+ """
1093+ Then stdout matches regexp:
1094+ """
1095 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1096 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1097 fips +yes +<fips-s> +NIST-certified core packages
1098 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1099 livepatch +yes +enabled +Canonical Livepatch service
1100- ros +no +(-|—) +Security Updates for the Robot Operating System
1101- ros-updates +no +(-|—) +All Updates for the Robot Operating System
1102 """
1103+ Then stdout matches regexp:
1104+ """
1105+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1106+ """
1107+ When I run `systemctl start ua-auto-attach.service` with sudo
1108+ And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
1109+ Then stdout matches regexp:
1110+ """
1111+ .*status=0\/SUCCESS.*
1112+ """
1113+ And stdout matches regexp:
1114+ """
1115+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1116+ """
1117+ When I run `ua auto-attach` with sudo
1118+ Then stderr matches regexp:
1119+ """
1120+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1121+ """
1122 When I run `apt-cache policy` with sudo
1123 Then apt-cache policy for the following url has permission `500`
1124 """
1125@@ -235,10 +278,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1126 """
1127
1128 Examples: ubuntu release
1129- | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg |
1130- | xenial | disabled | disabled | disabled | libkrad0 | jq |
1131- | bionic | disabled | n/a | disabled | libkrad0 | bundler |
1132- | focal | n/a | n/a | disabled | hello | ant |
1133+ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | cis_or_usg |
1134+ | xenial | disabled | disabled | disabled | libkrad0 | jq | cis |
1135+ | bionic | disabled | n/a | disabled | libkrad0 | bundler | cis |
1136+ | focal | n/a | n/a | disabled | hello | ant | usg |
1137
1138 @series.lts
1139 @uses.config.machine_type.azure.pro
1140@@ -258,27 +301,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1141 """
1142 SERVICE ENTITLED STATUS DESCRIPTION
1143 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1144- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1145+ """
1146+ Then stdout matches regexp:
1147+ """
1148 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1149 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1150 fips +yes +<fips-s> +NIST-certified core packages
1151 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1152 livepatch +yes +<livepatch> +Canonical Livepatch service
1153 """
1154+ Then stdout matches regexp:
1155+ """
1156+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1157+ """
1158 When I run `ua status --all` as non-root
1159 Then stdout matches regexp:
1160 """
1161 SERVICE ENTITLED STATUS DESCRIPTION
1162 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1163- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1164+ """
1165+ Then stdout matches regexp:
1166+ """
1167 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1168 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1169 fips +yes +<fips-s> +NIST-certified core packages
1170 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1171 livepatch +yes +<livepatch> +Canonical Livepatch service
1172- ros +no +(-|—) +Security Updates for the Robot Operating System
1173- ros-updates +no +(-|—) +All Updates for the Robot Operating System
1174 """
1175+ Then stdout matches regexp:
1176+ """
1177+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1178+ """
1179+ When I run `systemctl start ua-auto-attach.service` with sudo
1180+ And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
1181+ Then stdout matches regexp:
1182+ """
1183+ .*status=0\/SUCCESS.*
1184+ """
1185+ And stdout matches regexp:
1186+ """
1187+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1188+ """
1189+ When I run `ua auto-attach` with sudo
1190+ Then stderr matches regexp:
1191+ """
1192+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1193+ """
1194 When I run `apt-cache policy` with sudo
1195 Then apt-cache policy for the following url has permission `500`
1196 """
1197@@ -328,10 +396,10 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1198 """
1199
1200 Examples: ubuntu release
1201- | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch |
1202- | xenial | n/a | disabled | disabled | libkrad0 | jq | enabled |
1203- | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a |
1204- | focal | n/a | n/a | disabled | hello | ant | enabled |
1205+ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch | cis_or_usg |
1206+ | xenial | n/a | disabled | disabled | libkrad0 | jq | enabled | cis |
1207+ | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a | cis |
1208+ | focal | n/a | n/a | disabled | hello | ant | enabled | usg |
1209
1210 @series.lts
1211 @uses.config.machine_type.gcp.pro
1212@@ -351,27 +419,52 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1213 """
1214 SERVICE ENTITLED STATUS DESCRIPTION
1215 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1216- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1217+ """
1218+ Then stdout matches regexp:
1219+ """
1220 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1221 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1222 fips +yes +<fips-s> +NIST-certified core packages
1223 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1224 livepatch +yes +<livepatch> +Canonical Livepatch service
1225 """
1226+ Then stdout matches regexp:
1227+ """
1228+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1229+ """
1230 When I run `ua status --all` as non-root
1231 Then stdout matches regexp:
1232 """
1233 SERVICE ENTITLED STATUS DESCRIPTION
1234 cc-eal +yes +<cc-eal-s> +Common Criteria EAL2 Provisioning Packages
1235- cis +yes +<cis-s> +Center for Internet Security Audit Tools
1236+ """
1237+ Then stdout matches regexp:
1238+ """
1239 esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\)
1240 esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\)
1241 fips +yes +<fips-s> +NIST-certified core packages
1242 fips-updates +yes +<fips-s> +NIST-certified core packages with priority security updates
1243 livepatch +yes +<livepatch> +Canonical Livepatch service
1244- ros +no +(-|—) +Security Updates for the Robot Operating System
1245- ros-updates +no +(-|—) +All Updates for the Robot Operating System
1246 """
1247+ Then stdout matches regexp:
1248+ """
1249+ <cis_or_usg> +yes +<cis-s> +Security compliance and audit tools
1250+ """
1251+ When I run `systemctl start ua-auto-attach.service` with sudo
1252+ And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3`
1253+ Then stdout matches regexp:
1254+ """
1255+ .*status=0\/SUCCESS.*
1256+ """
1257+ And stdout matches regexp:
1258+ """
1259+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1260+ """
1261+ When I run `ua auto-attach` with sudo
1262+ Then stderr matches regexp:
1263+ """
1264+ Skipping attach: Instance '[0-9a-z\-]+' is already attached.
1265+ """
1266 When I run `apt-cache policy` with sudo
1267 Then apt-cache policy for the following url has permission `500`
1268 """
1269@@ -421,7 +514,7 @@ Feature: Command behaviour when auto-attached in an ubuntu PRO image
1270 """
1271
1272 Examples: ubuntu release
1273- | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch |
1274- | xenial | n/a | disabled | disabled | libkrad0 | jq | n/a |
1275- | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a |
1276- | focal | n/a | n/a | disabled | hello | ant | enabled |
1277+ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | livepatch | cis_or_usg |
1278+ | xenial | n/a | disabled | disabled | libkrad0 | jq | n/a | cis |
1279+ | bionic | disabled | n/a | disabled | libkrad0 | bundler | n/a | cis |
1280+ | focal | n/a | n/a | disabled | hello | ant | enabled | usg |
1281diff --git a/features/ubuntu_upgrade.feature b/features/ubuntu_upgrade.feature
1282index d565d77..d987975 100644
1283--- a/features/ubuntu_upgrade.feature
1284+++ b/features/ubuntu_upgrade.feature
1285@@ -162,3 +162,44 @@ Feature: Upgrade between releases when uaclient is attached
1286 | release | next_release | fips-service | fips-name | source-file |
1287 | xenial | bionic | fips | FIPS | ubuntu-fips |
1288 | xenial | bionic | fips-updates | FIPS Updates | ubuntu-fips-updates |
1289+
1290+ @slow
1291+ @series.bionic
1292+ @uses.config.machine_type.lxd.container
1293+ @upgrade
1294+ Scenario Outline: Attached upgrade with cis enabled across LTS releases
1295+ Given a `<release>` machine with ubuntu-advantage-tools installed
1296+ When I attach `contract_token_staging` with sudo
1297+ And I run `ua enable cis` with sudo
1298+ # update-manager-core requires ua < 28. Our tests that build the package will
1299+ # generate ua with version 28. We are removing that package here to make sure
1300+ # do-release-upgrade will be able to run
1301+ And I run `apt remove update-manager-core -y` with sudo
1302+ And I run `apt-get dist-upgrade --assume-yes` with sudo
1303+ # Some packages upgrade may require a reboot
1304+ And I reboot the `<release>` machine
1305+ And I create the file `/etc/update-manager/release-upgrades.d/ua-test.cfg` with the following
1306+ """
1307+ [Sources]
1308+ AllowThirdParty=yes
1309+ """
1310+ Then I verify that running `do-release-upgrade --frontend DistUpgradeViewNonInteractive` `with sudo` exits `0`
1311+ When I reboot the `<release>` machine
1312+ And I run `lsb_release -cs` as non-root
1313+ Then I will see the following on stdout:
1314+ """
1315+ <next_release>
1316+ """
1317+ And I verify that running `egrep "<release>|disabled" /etc/apt/sources.list.d/*` `as non-root` exits `2`
1318+ And I will see the following on stdout:
1319+ """
1320+ """
1321+ When I run `ua status` with sudo
1322+ Then stdout matches regexp:
1323+ """
1324+ usg +yes +enabled
1325+ """
1326+
1327+ Examples: ubuntu release
1328+ | release | next_release |
1329+ | bionic | focal |
1330diff --git a/features/ubuntu_upgrade_unattached.feature b/features/ubuntu_upgrade_unattached.feature
1331index 9db750f..5195d2b 100644
1332--- a/features/ubuntu_upgrade_unattached.feature
1333+++ b/features/ubuntu_upgrade_unattached.feature
1334@@ -73,7 +73,7 @@ Feature: Upgrade between releases when uaclient is unattached
1335 When I run `ua status` with sudo
1336 Then stdout matches regexp:
1337 """
1338- cis yes +Center for Internet Security Audit Tools
1339+ cis yes +Security compliance and audit tools
1340 esm-infra yes +UA Infra: Extended Security Maintenance \(ESM\)
1341 """
1342 When I attach `contract_token` with sudo
1343diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature
1344index 77a90e0..bda7597 100644
1345--- a/features/unattached_commands.feature
1346+++ b/features/unattached_commands.feature
1347@@ -29,6 +29,7 @@ Feature: Command behaviour when unattached
1348 | xenial |
1349 | hirsute |
1350 | impish |
1351+ | jammy |
1352
1353 @series.xenial
1354 @uses.config.machine_type.lxd.container
1355@@ -134,6 +135,8 @@ Feature: Command behaviour when unattached
1356 | hirsute | refresh |
1357 | impish | detach |
1358 | impish | refresh |
1359+ | jammy | detach |
1360+ | jammy | refresh |
1361
1362 @series.all
1363 @uses.config.machine_type.lxd.container
1364@@ -174,6 +177,10 @@ Feature: Command behaviour when unattached
1365 | impish | disable | livepatch |
1366 | impish | enable | unknown |
1367 | impish | disable | unknown |
1368+ | jammy | enable | livepatch |
1369+ | jammy | disable | livepatch |
1370+ | jammy | enable | unknown |
1371+ | jammy | disable | unknown |
1372
1373 @series.all
1374 @uses.config.machine_type.lxd.container
1375@@ -215,6 +222,7 @@ Feature: Command behaviour when unattached
1376 | xenial | yes |
1377 | hirsute | no |
1378 | impish | no |
1379+ | jammy | no |
1380
1381
1382 @series.all
1383@@ -223,18 +231,18 @@ Feature: Command behaviour when unattached
1384 Given a `<release>` machine with ubuntu-advantage-tools installed
1385 When I run `apt remove ca-certificates -y` with sudo
1386 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`
1387- Then I will see the following on stderr:
1388+ Then stderr matches regexp:
1389 """
1390- Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json
1391+ Failed to access URL: https://.*
1392 Cannot verify certificate of server
1393 Please install "ca-certificates" and try again.
1394 """
1395 When I run `apt install ca-certificates -y` with sudo
1396 When I run `mv /etc/ssl/certs /etc/ssl/wronglocation` with sudo
1397 When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1`
1398- Then I will see the following on stderr:
1399+ Then stderr matches regexp:
1400 """
1401- Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json
1402+ Failed to access URL: https://.*
1403 Cannot verify certificate of server
1404 Please check your openssl configuration.
1405 """
1406@@ -245,6 +253,7 @@ Feature: Command behaviour when unattached
1407 | focal |
1408 | hirsute |
1409 | impish |
1410+ | jammy |
1411
1412 @series.focal
1413 @uses.config.machine_type.lxd.container
1414@@ -503,6 +512,7 @@ Feature: Command behaviour when unattached
1415 When I run `ls -1 logs/` as non-root
1416 Then stdout matches regexp:
1417 """
1418+ build.info
1419 cloud-id.txt
1420 jobs-status.json
1421 journalctl.txt
1422@@ -527,3 +537,4 @@ Feature: Command behaviour when unattached
1423 | focal |
1424 | hirsute |
1425 | impish |
1426+ | jammy |
1427diff --git a/features/unattached_status.feature b/features/unattached_status.feature
1428index 5d26e27..7284afb 100644
1429--- a/features/unattached_status.feature
1430+++ b/features/unattached_status.feature
1431@@ -9,14 +9,14 @@ Feature: Unattached status
1432 """
1433 _doc _schema_version account attached config config_path contract effective
1434 environment_vars execution_details execution_status expires machine_id notices
1435- services version
1436+ services version simulated
1437 """
1438 When I run `ua status --format yaml` as non-root
1439 Then stdout is formatted as `yaml` and has keys:
1440 """
1441 _doc _schema_version account attached config config_path contract effective
1442 environment_vars execution_details execution_status expires machine_id notices
1443- services version
1444+ services version simulated
1445 """
1446
1447 Examples: ubuntu release
1448@@ -26,21 +26,24 @@ Feature: Unattached status
1449 | xenial |
1450 | hirsute |
1451 | impish |
1452+ | jammy |
1453
1454 @series.all
1455 @uses.config.machine_type.lxd.container
1456 Scenario Outline: Unattached status in a ubuntu machine
1457 Given a `<release>` machine with ubuntu-advantage-tools installed
1458+ When I run `sed -i 's/contracts.can/contracts.staging.can/' /etc/ubuntu-advantage/uaclient.conf` with sudo
1459 When I run `ua status` as non-root
1460 Then stdout matches regexp:
1461 """
1462 SERVICE AVAILABLE DESCRIPTION
1463 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
1464- cis <cis> +Center for Internet Security Audit Tools
1465- esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)
1466+ ?<cis>( +<cis-available> +Security compliance and audit tools)?
1467+ ?esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
1468 fips <fips> +NIST-certified core packages
1469 fips-updates <fips> +NIST-certified core packages with priority security updates
1470 livepatch <livepatch> +Canonical Livepatch service
1471+ ?<usg>( +<cis-available> +Security compliance and audit tools)?
1472
1473 This machine is not attached to a UA subscription.
1474 See https://ubuntu.com/advantage
1475@@ -50,14 +53,15 @@ Feature: Unattached status
1476 """
1477 SERVICE AVAILABLE DESCRIPTION
1478 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
1479- cis <cis> +Center for Internet Security Audit Tools
1480- esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1481- esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)
1482+ ?<cis>( +<cis-available> +Security compliance and audit tools)?
1483+ ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1484+ esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
1485 fips <fips> +NIST-certified core packages
1486 fips-updates <fips> +NIST-certified core packages with priority security updates
1487 livepatch <livepatch> +Canonical Livepatch service
1488 ros <ros> +Security Updates for the Robot Operating System
1489 ros-updates <ros> +All Updates for the Robot Operating System
1490+ ?<usg>( +<cis-available> +Security compliance and audit tools)?
1491
1492 This machine is not attached to a UA subscription.
1493 See https://ubuntu.com/advantage
1494@@ -67,11 +71,12 @@ Feature: Unattached status
1495 """
1496 SERVICE AVAILABLE DESCRIPTION
1497 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
1498- cis <cis> +Center for Internet Security Audit Tools
1499- esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)
1500+ ?<cis>( +<cis-available> +Security compliance and audit tools)?
1501+ ?esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
1502 fips <fips> +NIST-certified core packages
1503 fips-updates <fips> +NIST-certified core packages with priority security updates
1504 livepatch <livepatch> +Canonical Livepatch service
1505+ ?<usg>( +<cis-available> +Security compliance and audit tools)?
1506
1507 This machine is not attached to a UA subscription.
1508 See https://ubuntu.com/advantage
1509@@ -81,14 +86,15 @@ Feature: Unattached status
1510 """
1511 SERVICE AVAILABLE DESCRIPTION
1512 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
1513- cis <cis> +Center for Internet Security Audit Tools
1514- esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1515- esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)
1516+ ?<cis>( +<cis-available> +Security compliance and audit tools)?
1517+ ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1518+ esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
1519 fips <fips> +NIST-certified core packages
1520 fips-updates <fips> +NIST-certified core packages with priority security updates
1521 livepatch <livepatch> +Canonical Livepatch service
1522 ros <ros> +Security Updates for the Robot Operating System
1523 ros-updates <ros> +All Updates for the Robot Operating System
1524+ ?<usg>( +<cis-available> +Security compliance and audit tools)?
1525
1526 This machine is not attached to a UA subscription.
1527 See https://ubuntu.com/advantage
1528@@ -103,23 +109,83 @@ Feature: Unattached status
1529 """
1530 SERVICE AVAILABLE DESCRIPTION
1531 cc-eal <cc-eal> +Common Criteria EAL2 Provisioning Packages
1532- cis <cis> +Center for Internet Security Audit Tools
1533- esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1534- esm-infra <infra> +UA Infra: Extended Security Maintenance \(ESM\)
1535+ ?<cis>( +<cis-available> +Security compliance and audit tools)?
1536+ ?esm-apps <esm-apps> +UA Apps: Extended Security Maintenance \(ESM\)
1537+ esm-infra <esm-infra> +UA Infra: Extended Security Maintenance \(ESM\)
1538 fips <fips> +NIST-certified core packages
1539 fips-updates <fips> +NIST-certified core packages with priority security updates
1540 livepatch <livepatch> +Canonical Livepatch service
1541 ros <ros> +Security Updates for the Robot Operating System
1542 ros-updates <ros> +All Updates for the Robot Operating System
1543+ ?<usg>( +<cis-available> +Security compliance and audit tools)?
1544
1545 This machine is not attached to a UA subscription.
1546 See https://ubuntu.com/advantage
1547 """
1548
1549 Examples: ubuntu release
1550- | release | esm-apps | cc-eal | cis | fips | fips-update | infra | ros | livepatch |
1551- | xenial | yes | yes | yes | yes | yes | yes | yes | yes |
1552- | bionic | yes | no | yes | yes | yes | yes | yes | yes |
1553- | focal | yes | no | yes | yes | yes | yes | no | yes |
1554- | hirsute | no | no | no | no | no | no | no | no |
1555- | impish | no | no | no | no | no | no | no | no |
1556+ | release | esm-apps | cc-eal | cis | cis-available | fips | esm-infra | ros | livepatch | usg |
1557+ | xenial | yes | yes | cis | yes | yes | yes | yes | yes | |
1558+ | bionic | yes | no | cis | yes | yes | yes | yes | yes | |
1559+ | focal | yes | no | | yes | yes | yes | no | yes | usg |
1560+ | hirsute | no | no | cis | yes | no | no | no | no | |
1561+ | impish | no | no | cis | yes | no | no | no | no | |
1562+ | jammy | no | no | cis | yes | no | no | no | no | |
1563+
1564+ @series.all
1565+ @uses.config.machine_type.lxd.container
1566+ @uses.config.contract_token
1567+ Scenario Outline: Simulate status in a ubuntu machine
1568+ Given a `<release>` machine with ubuntu-advantage-tools installed
1569+ When I run `sed -i 's/contracts.can/contracts.staging.can/' /etc/ubuntu-advantage/uaclient.conf` with sudo
1570+ When I do a preflight check for `contract_token_staging` without the all flag
1571+ Then stdout matches regexp:
1572+ """
1573+ SERVICE AVAILABLE ENTITLED AUTO_ENABLED DESCRIPTION
1574+ cc-eal <cc-eal> +yes +no +Common Criteria EAL2 Provisioning Packages
1575+ ?<cis>( +<cis-available> +yes +no +Security compliance and audit tools)?
1576+ ?esm-infra <esm-infra> +yes +yes +UA Infra: Extended Security Maintenance \(ESM\)
1577+ fips <fips> +yes +no +NIST-certified core packages
1578+ fips-updates <fips> +yes +no +NIST-certified core packages with priority security updates
1579+ livepatch <livepatch> +yes +yes +Canonical Livepatch service
1580+ ?<usg>( +<cis-available> +yes +no +Security compliance and audit tools)?
1581+ """
1582+ When I do a preflight check for `contract_token_staging` with the all flag
1583+ Then stdout matches regexp:
1584+ """
1585+ SERVICE AVAILABLE ENTITLED AUTO_ENABLED DESCRIPTION
1586+ cc-eal <cc-eal> +yes +no +Common Criteria EAL2 Provisioning Packages
1587+ ?<cis>( +<cis-available> +yes +no +Security compliance and audit tools)?
1588+ ?esm-apps <esm-apps> +yes +yes +UA Apps: Extended Security Maintenance \(ESM\)
1589+ esm-infra <esm-infra> +yes +yes +UA Infra: Extended Security Maintenance \(ESM\)
1590+ fips <fips> +yes +no +NIST-certified core packages
1591+ fips-updates <fips> +yes +no +NIST-certified core packages with priority security updates
1592+ livepatch <livepatch> +yes +yes +Canonical Livepatch service
1593+ ros <ros> +yes +no +Security Updates for the Robot Operating System
1594+ ros-updates <ros> +yes +no +All Updates for the Robot Operating System
1595+ ?<usg>( +<cis-available> +yes +no +Security compliance and audit tools)?
1596+ """
1597+ When I do a preflight check for `contract_token_staging` formatted as json
1598+ Then stdout is formatted as `json` and has keys:
1599+ """
1600+ _doc _schema_version account attached config config_path contract effective
1601+ environment_vars execution_details execution_status expires machine_id notices
1602+ services version simulated
1603+ """
1604+ When I do a preflight check for `contract_token_staging` formatted as yaml
1605+ Then stdout is formatted as `yaml` and has keys:
1606+ """
1607+ _doc _schema_version account attached config config_path contract effective
1608+ environment_vars execution_details execution_status expires machine_id notices
1609+ services version simulated
1610+ """
1611+
1612+
1613+ Examples: ubuntu release
1614+ | release | esm-apps | cc-eal | cis | cis-available | fips | esm-infra | ros | livepatch | usg |
1615+ | xenial | yes | yes | cis | yes | yes | yes | yes | yes | |
1616+ | bionic | yes | no | cis | yes | yes | yes | yes | yes | |
1617+ | focal | yes | no | | yes | yes | yes | no | yes | usg |
1618+ | hirsute | no | no | cis | yes | no | no | no | no | |
1619+ | impish | no | no | cis | yes | no | no | no | no | |
1620+ | jammy | no | no | cis | yes | no | no | no | no | |
1621diff --git a/help_data.yaml b/help_data.yaml
1622index 3c93645..d5d6486 100644
1623--- a/help_data.yaml
1624+++ b/help_data.yaml
1625@@ -7,17 +7,11 @@ cc-eal:
1626
1627 cis:
1628 help: |
1629- CIS benchmarks locks down your systems by removing non-secure programs,
1630- disabling unused filesystems, disabling unnecessary ports or services to
1631- prevent cyber attacks and malware, auditing privileged operations and
1632- restricting administrative privileges. The cis command installs
1633- tooling needed to automate audit and hardening according to a desired
1634- CIS profile - level 1 or level 2 for server or workstation on
1635- Ubuntu 18.04 LTS or 16.04 LTS. The audit tooling uses OpenSCAP libraries
1636- to do a scan of the system. The tool provides options to generate a
1637- report in XML or a html format. The report shows compliance for all the
1638- rules against the profile selected during the scan. You can find out
1639- more at https://ubuntu.com/security/certifications#cis
1640+ Ubuntu Security Guide is a tool for hardening and auditing and allows for
1641+ environment-specific customizations. It enables compliance with profiles
1642+ such as DISA-STIG and the CIS benchmarks. Find out more at
1643+ https://ubuntu.com/security/certifications/docs/usg
1644+
1645
1646 esm-apps:
1647 help: |
1648diff --git a/lib/reboot_cmds.py b/lib/reboot_cmds.py
1649index 7636f1b..569b46a 100644
1650--- a/lib/reboot_cmds.py
1651+++ b/lib/reboot_cmds.py
1652@@ -17,16 +17,17 @@ should run at next boot to process any pending/unresovled config operations.
1653 import logging
1654 import os
1655 import sys
1656-import time
1657
1658-from uaclient import config, contract, entitlements, status
1659-from uaclient.cli import assert_lock_file, setup_logging
1660+from uaclient import config, contract, lock, status
1661+from uaclient.cli import setup_logging
1662+from uaclient.entitlements.fips import FIPSEntitlement
1663 from uaclient.exceptions import LockHeldError, UserFacingError
1664 from uaclient.util import ProcessExecutionError, UrlError, subp
1665
1666 # Retry sleep backoff algorithm if lock is held.
1667 # Lock may be held by auto-attach on systems with ubuntu-advantage-pro.
1668-SLEEP_RETRIES_ON_LOCK_HELD = [1, 1, 5]
1669+SLEEP_ON_LOCK_HELD = 1
1670+MAX_RETRIES_ON_LOCK_HELD = 7
1671
1672
1673 def run_command(cmd, cfg):
1674@@ -57,9 +58,7 @@ def fix_pro_pkg_holds(cfg):
1675 if service.get("name") == "fips":
1676 service_status = service.get("status")
1677 if service_status == "enabled":
1678- ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[
1679- service.get("name")
1680- ]
1681+ ent_cls = FIPSEntitlement
1682 logging.debug(
1683 "Attempting to remove Ubuntu Pro FIPS package holds"
1684 )
1685@@ -102,7 +101,6 @@ def process_remaining_deltas(cfg):
1686 cfg.remove_notice("", status.MESSAGE_LIVEPATCH_LTS_REBOOT_REQUIRED)
1687
1688
1689-@assert_lock_file("ua-reboot-cmds")
1690 def process_reboot_operations(cfg):
1691
1692 reboot_cmd_marker_file = cfg.data_path("marker-reboot-cmds")
1693@@ -139,21 +137,17 @@ def main(cfg):
1694 :raises: LockHeldError when lock still held by auto-attach after retries.
1695 UserFacingError for all other errors
1696 """
1697- while True:
1698- try:
1699+ try:
1700+ with lock.SpinLock(
1701+ cfg=cfg,
1702+ lock_holder="ua-reboot-cmds",
1703+ sleep_time=SLEEP_ON_LOCK_HELD,
1704+ max_retries=MAX_RETRIES_ON_LOCK_HELD,
1705+ ):
1706 process_reboot_operations(cfg=cfg)
1707- break
1708- except LockHeldError as e:
1709- logging.debug(
1710- "Retrying ua-reboot-cmds {} times on held lock".format(
1711- len(SLEEP_RETRIES_ON_LOCK_HELD)
1712- )
1713- )
1714- if SLEEP_RETRIES_ON_LOCK_HELD:
1715- time.sleep(SLEEP_RETRIES_ON_LOCK_HELD.pop(0))
1716- else:
1717- logging.warning("Lock not released. %s", str(e.msg))
1718- sys.exit(1)
1719+ except LockHeldError as e:
1720+ logging.warning("Lock not released. %s", str(e.msg))
1721+ sys.exit(1)
1722
1723
1724 if __name__ == "__main__":
1725diff --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
1726new file mode 100755
1727index 0000000..586ea2c
1728--- /dev/null
1729+++ b/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh
1730@@ -0,0 +1,34 @@
1731+series=$1
1732+name=test-$series
1733+set -x
1734+lxc launch ubuntu-daily:$series $name >/dev/null 2>&1
1735+sleep 3
1736+
1737+echo "Confirming we are runnign a ${series} machine"
1738+lxc exec $name -- lsb_release -a
1739+
1740+echo "Updating to the latest version of UA"
1741+lxc exec $name -- apt-get update >/dev/null
1742+lxc exec $name -- apt-get install -y ubuntu-advantage-tools >/dev/null
1743+lxc exec $name -- ua version
1744+echo "Running ua status to persist cache"
1745+lxc exec $name -- ua status
1746+echo "Modifying ESM_SUPPORTED_ARCHS to emulate the issue"
1747+lxc exec $name -- sed -i "s/ESM_SUPPORTED_ARCHS=\"i386 amd64\"/ESM_SUPPORTED_ARCHS=\"\"/" /var/lib/dpkg/info/ubuntu-advantage-tools.postinst
1748+echo "Re-running the postinst script. Confirming that KeyError is reported on stdout"
1749+lxc exec $name -- dpkg-reconfigure ubuntu-advantage-tools
1750+
1751+echo "Updating UA to the version with the fix"
1752+lxc exec $name -- sh -c "echo \"deb http://archive.ubuntu.com/ubuntu $series-proposed main\" | tee /etc/apt/sources.list.d/proposed.list"
1753+lxc exec $name -- apt-get update >/dev/null
1754+lxc exec $name -- apt-get install -y ubuntu-advantage-tools >/dev/null
1755+lxc exec $name -- ua version
1756+
1757+echo "Running ua status to persist cache"
1758+lxc exec $name -- ua status
1759+echo "Modifying ESM_SUPPORTED_ARCHS to emulate the issue"
1760+lxc exec $name -- sed -i "s/ESM_SUPPORTED_ARCHS=\"i386 amd64\"/ESM_SUPPORTED_ARCHS=\"\"/" /var/lib/dpkg/info/ubuntu-advantage-tools.postinst
1761+echo "Re-running the postinst script. Confirming that KeyError is no longer reported"
1762+lxc exec $name -- dpkg-reconfigure ubuntu-advantage-tools
1763+
1764+lxc delete --force $name
1765diff --git a/sru/release-27.4/test-unattached-status-job.sh b/sru/release-27.4/test-unattached-status-job.sh
1766new file mode 100755
1767index 0000000..e8e3253
1768--- /dev/null
1769+++ b/sru/release-27.4/test-unattached-status-job.sh
1770@@ -0,0 +1,40 @@
1771+series=$1
1772+set -x
1773+lxc launch ubuntu-daily:$series test >/dev/null 2>&1
1774+sleep 10
1775+
1776+echo
1777+echo "showing the network call in current ua version 27.3"
1778+lxc exec test -- apt-get update >/dev/null
1779+lxc exec test -- apt-get install -y ubuntu-advantage-tools >/dev/null
1780+lxc exec test -- ua version
1781+echo "disable all jobs but the status job"
1782+lxc exec test -- ua config set metering_timer=0
1783+lxc exec test -- ua config set update_messaging_timer=0
1784+echo "run the status update timer job by removing current timer state and executing the timer script"
1785+echo "and run tcpdump while executing the script, filtering by the current IPs of contracts.canonical.com"
1786+lxc exec test -- rm -f /var/lib/ubuntu-advantage/jobs-status.json
1787+lxc 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"
1788+echo "Verify that tcpdump saw packets in above output"
1789+echo "Verify that the job was actually processed by the timer: update_status should be there."
1790+lxc exec test -- grep "update_status" /var/lib/ubuntu-advantage/jobs-status.json
1791+
1792+
1793+echo
1794+echo
1795+echo "installing new version from proposed"
1796+lxc exec test -- sh -c "echo \"deb http://archive.ubuntu.com/ubuntu $series-proposed main\" | tee /etc/apt/sources.list.d/proposed.list"
1797+lxc exec test -- apt-get update >/dev/null
1798+lxc 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
1799+lxc exec test -- ua version
1800+
1801+
1802+echo "run the status update timer job by removing current timer state and executing the timer script"
1803+echo "and run tcpdump while executing the script, filtering by the current IPs of contracts.canonical.com"
1804+lxc exec test -- rm -f /var/lib/ubuntu-advantage/jobs-status.json
1805+lxc 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"
1806+echo "Verify that tcpdump DID NOT see packets in above output"
1807+echo "Verify that the job was actually processed by the timer: update_status should be there."
1808+lxc exec test -- grep "update_status" /var/lib/ubuntu-advantage/jobs-status.json
1809+
1810+lxc delete test --force
1811diff --git a/sru/release-27.5/test-aws-ipv6.sh b/sru/release-27.5/test-aws-ipv6.sh
1812new file mode 100644
1813index 0000000..31353da
1814--- /dev/null
1815+++ b/sru/release-27.5/test-aws-ipv6.sh
1816@@ -0,0 +1,66 @@
1817+#!/bin/sh
1818+
1819+set -e
1820+
1821+KEY_PATH=$1
1822+DEB_PATH=$2
1823+sshopts=( -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR )
1824+
1825+REGION=us-west-2
1826+INSTANCE_TYPE=t3.micro
1827+KEY_NAME=test-ipv6
1828+PRO_IMAGE_ID=ami-07e00b8a1a054fdbf # bionic PRO image for us-west-2
1829+
1830+# You need to have a subnet that supports IPv6. The easiest path here is to launch an ec2
1831+# instance through pycloudlib, which will already create a VPC with a subnet that supports
1832+# IPv6. You can also use the security group created by pycloudlib
1833+SUBNET_ID=<SUBNET-ID>
1834+SECURITY_GROUP_ID=<SG-ID>
1835+
1836+# Make sure that the awscli being used has support for the --metadata-options params, otherwise the
1837+# IPv6 endpoint will not work as expected
1838+instance_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)
1839+instance_id=$(echo $instance_info | jq -r ".Instances[0].InstanceId")
1840+instance_ip=$(aws ec2 describe-instances --region $REGION --instance-ids $instance_id --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
1841+
1842+echo "---------------------------------------------"
1843+echo "Checking instance info"
1844+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- lsb_release -a
1845+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status --wait
1846+echo "---------------------------------------------"
1847+echo -e "\n"
1848+
1849+echo "---------------------------------------------"
1850+echo "Detaching PRO instance"
1851+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua detach --assume-yes
1852+echo "---------------------------------------------"
1853+echo -e "\n"
1854+
1855+echo "---------------------------------------------"
1856+echo "Installing package with IPv6 support"
1857+scp "${sshopts[@]}" -i $KEY_PATH $DEB_PATH ubuntu@$instance_ip:/home/ubuntu/ua.deb
1858+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo dpkg -i ua.deb
1859+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- ua version
1860+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status --wait
1861+echo "---------------------------------------------"
1862+echo -e "\n"
1863+
1864+echo "---------------------------------------------"
1865+echo "Modifying IPv4 address to make it fail"
1866+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo rm /var/log/ubuntu-advantage.log
1867+ssh "${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
1868+echo "---------------------------------------------"
1869+echo -e "\n"
1870+
1871+echo "---------------------------------------------"
1872+echo "Verify that auto-attach still works and IPv6 route was used instead"
1873+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua auto-attach
1874+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo ua status
1875+ssh "${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
1876+ssh "${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
1877+ssh "${sshopts[@]}" -i $KEY_PATH ubuntu@$instance_ip -- sudo grep -F \"URL [PUT]: http://[fd00:ec2::254]/latest/api/token\" /var/log/ubuntu-advantage.log
1878+ssh "${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
1879+ssh "${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
1880+ssh "${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
1881+echo "---------------------------------------------"
1882+echo -e "\n"
1883diff --git a/tools/build.sh b/tools/build.sh
1884index 1e50767..7660908 100755
1885--- a/tools/build.sh
1886+++ b/tools/build.sh
1887@@ -1,2 +1,3 @@
1888 #!/usr/bin/bash
1889 env PYTHONPATH=. python3 tools/build.py "$@"
1890+notify-send "Build finished!" || true
1891diff --git a/tools/create-lp-release-branches.sh b/tools/create-lp-release-branches.sh
1892index 436abdf..604df74 100755
1893--- a/tools/create-lp-release-branches.sh
1894+++ b/tools/create-lp-release-branches.sh
1895@@ -32,7 +32,7 @@ else
1896 set -e
1897 fi
1898
1899-for release in xenial bionic focal hirsute
1900+for release in xenial bionic focal hirsute impish
1901 do
1902 echo
1903 echo $release
1904@@ -49,6 +49,7 @@ do
1905 bionic) version=${UA_VERSION}~18.04.1;;
1906 focal) version=${UA_VERSION}~20.04.1;;
1907 hirsute) version=${UA_VERSION}~21.04.1;;
1908+ impish) version=${UA_VERSION}~21.10.1;;
1909 esac
1910 dch_cmd=(dch -v ${version} -D ${release} -b "Backport new upstream release: (LP: #${SRU_BUG}) to $release")
1911 if [ -z "$DO_IT" ]; then
1912diff --git a/tools/run-integration-tests.py b/tools/run-integration-tests.py
1913index 2ab8786..10b4a55 100644
1914--- a/tools/run-integration-tests.py
1915+++ b/tools/run-integration-tests.py
1916@@ -14,6 +14,7 @@ SERIES_TO_VERSION = {
1917 "focal": "20.04",
1918 "hirsute": "21.04",
1919 "impish": "21.10",
1920+ "jammy": "22.04",
1921 }
1922
1923 TOKEN_TO_ENVVAR = {
1924@@ -30,7 +31,7 @@ PLATFORM_SERIES_TESTS = {
1925 "gcpgeneric": ["xenial", "bionic", "focal", "hirsute"],
1926 "gcppro": ["xenial", "bionic", "focal"],
1927 "vm": ["xenial", "bionic", "focal"],
1928- "lxd": ["xenial", "bionic", "focal", "hirsute", "impish"],
1929+ "lxd": ["xenial", "bionic", "focal", "hirsute", "impish", "jammy"],
1930 "upgrade": ["xenial", "bionic", "focal", "hirsute", "impish"],
1931 }
1932
1933diff --git a/tools/test-in-lxd.sh b/tools/test-in-lxd.sh
1934index 17d40cf..86ffdfe 100755
1935--- a/tools/test-in-lxd.sh
1936+++ b/tools/test-in-lxd.sh
1937@@ -12,5 +12,17 @@ lxc delete $name --force
1938 lxc launch ubuntu-daily:$series $name
1939 sleep 5
1940 lxc file push $deb $name/tmp/ua.deb
1941+
1942+if [ -n "$SHELL_BEFORE" ]; then
1943+ set +x
1944+ echo
1945+ echo
1946+ echo "New version of ua has not been installed yet."
1947+ echo "After you exit the shell we'll upgrade ua and bring you right back."
1948+ echo
1949+ set -x
1950+ lxc exec $name bash
1951+fi
1952+
1953 lxc exec $name -- dpkg -i /tmp/ua.deb
1954 lxc exec $name bash
1955diff --git a/tox.ini b/tox.ini
1956index 701eae3..9c98f03 100644
1957--- a/tox.ini
1958+++ b/tox.ini
1959@@ -54,6 +54,7 @@ commands =
1960 behave-lxd-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.focal,series.lts,series.all" --tags="~upgrade"
1961 behave-lxd-21.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.hirsute,series.all" --tags="~upgrade"
1962 behave-lxd-21.10: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.impish,series.all" --tags="~upgrade"
1963+ behave-lxd-22.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.container" --tags="series.jammy,series.all" --tags="~upgrade"
1964 behave-vm-16.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.xenial,series.all,series.lts" --tags="~upgrade"
1965 behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all,series.lts" --tags="~upgrade"
1966 behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all,series.lts" --tags="~upgrade"
1967diff --git a/uaclient/actions.py b/uaclient/actions.py
1968new file mode 100644
1969index 0000000..3f98c7f
1970--- /dev/null
1971+++ b/uaclient/actions.py
1972@@ -0,0 +1,66 @@
1973+import logging
1974+
1975+from uaclient import clouds, config, contract, exceptions, status, util
1976+from uaclient.clouds import identity
1977+
1978+LOG = logging.getLogger("ua.actions")
1979+
1980+
1981+def attach_with_token(
1982+ cfg: config.UAConfig, token: str, allow_enable: bool
1983+) -> None:
1984+ """
1985+ Common functionality to take a token and attach via contract backend
1986+ :raise UrlError: On unexpected connectivity issues to contract
1987+ server or inability to access identity doc from metadata service.
1988+ :raise ContractAPIError: On unexpected errors when talking to the contract
1989+ server.
1990+ """
1991+ try:
1992+ contract.request_updated_contract(
1993+ cfg, token, allow_enable=allow_enable
1994+ )
1995+ except util.UrlError as exc:
1996+ with util.disable_log_to_console():
1997+ LOG.exception(exc)
1998+ cfg.status() # Persist updated status in the event of partial attach
1999+ config.update_ua_messages(cfg)
2000+ raise exc
2001+ except exceptions.UserFacingError as exc:
2002+ LOG.warning(exc.msg)
2003+ cfg.status() # Persist updated status in the event of partial attach
2004+ config.update_ua_messages(cfg)
2005+ raise exc
2006+
2007+ config.update_ua_messages(cfg)
2008+
2009+
2010+def auto_attach(
2011+ cfg: config.UAConfig, cloud: clouds.AutoAttachCloudInstance
2012+) -> None:
2013+ """
2014+ :raise UrlError: On unexpected connectivity issues to contract
2015+ server or inability to access identity doc from metadata service.
2016+ :raise ContractAPIError: On unexpected errors when talking to the contract
2017+ server.
2018+ :raise NonAutoAttachImageError: If this cloud type does not have
2019+ auto-attach support.
2020+ """
2021+ contract_client = contract.UAContractClient(cfg)
2022+ try:
2023+ tokenResponse = contract_client.request_auto_attach_contract_token(
2024+ instance=cloud
2025+ )
2026+ except contract.ContractAPIError as e:
2027+ if e.code and 400 <= e.code < 500:
2028+ raise exceptions.NonAutoAttachImageError(
2029+ status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
2030+ )
2031+ raise e
2032+ current_iid = identity.get_instance_id()
2033+ if current_iid:
2034+ cfg.write_cache("instance-id", current_iid)
2035+
2036+ token = tokenResponse["contractToken"]
2037+
2038+ attach_with_token(cfg, token=token, allow_enable=True)
2039diff --git a/uaclient/cli.py b/uaclient/cli.py
2040index 77af6f7..f76e570 100644
2041--- a/uaclient/cli.py
2042+++ b/uaclient/cli.py
2043@@ -15,23 +15,36 @@ import tempfile
2044 import textwrap
2045 import time
2046 from functools import wraps
2047+from typing import Optional # noqa: F401
2048 from typing import List
2049
2050 import yaml
2051
2052 from uaclient import (
2053+ actions,
2054 config,
2055 contract,
2056 entitlements,
2057 exceptions,
2058 jobs,
2059+ lock,
2060 security,
2061 security_status,
2062 )
2063 from uaclient import status as ua_status
2064 from uaclient import util, version
2065+from uaclient.clouds import AutoAttachCloudInstance # noqa: F401
2066 from uaclient.clouds import identity
2067-from uaclient.defaults import CONFIG_FIELD_ENVVAR_ALLOWLIST
2068+from uaclient.defaults import (
2069+ CLOUD_BUILD_INFO,
2070+ CONFIG_FIELD_ENVVAR_ALLOWLIST,
2071+ DEFAULT_CONFIG_FILE,
2072+)
2073+
2074+# TODO: Better address service commands running on cli
2075+# It is not ideal for us to import an entitlement directly on the cli module.
2076+# We need to refactor this to avoid that type of coupling in the code.
2077+from uaclient.entitlements.livepatch import LIVEPATCH_CMD
2078
2079 NAME = "ua"
2080
2081@@ -67,11 +80,6 @@ UA_SERVICES = (
2082 "ua-license-check.timer",
2083 )
2084
2085-# Set a module-level callable here so we don't have to reinstantiate
2086-# UAConfig in order to determine dynamic data_path exception handling of
2087-# main_error_handler
2088-_CLEAR_LOCK_FILE = None
2089-
2090
2091 class UAArgumentParser(argparse.ArgumentParser):
2092 def __init__(
2093@@ -114,41 +122,13 @@ class UAArgumentParser(argparse.ArgumentParser):
2094
2095
2096 def assert_lock_file(lock_holder=None):
2097- """Decorator asserting exclusive access to lock file
2098-
2099- Create a lock file if absent. The lock file will contain a pid of the
2100- running process, and a customer-visible description of the lock holder.
2101-
2102- :param lock_holder: String with the service name or command which is
2103- holding the lock.
2104-
2105- This lock_holder string will be customer visible in status.json.
2106-
2107- :raises: LockHeldError if lock is held.
2108- """
2109+ """Decorator asserting exclusive access to lock file"""
2110
2111 def wrapper(f):
2112 @wraps(f)
2113 def new_f(*args, cfg, **kwargs):
2114- global _CLEAR_LOCK_FILE
2115- (lock_pid, cur_lock_holder) = cfg.check_lock_info()
2116- if lock_pid > 0:
2117- raise exceptions.LockHeldError(
2118- lock_request=lock_holder,
2119- lock_holder=cur_lock_holder,
2120- pid=lock_pid,
2121- )
2122- cfg.write_cache("lock", "{}:{}".format(os.getpid(), lock_holder))
2123- notice_msg = "Operation in progress: {}".format(lock_holder)
2124- cfg.add_notice("", notice_msg)
2125- _CLEAR_LOCK_FILE = cfg.delete_cache_key
2126-
2127- try:
2128+ with lock.SingleAttemptLock(cfg=cfg, lock_holder=lock_holder):
2129 retval = f(*args, cfg=cfg, **kwargs)
2130- finally:
2131- cfg.delete_cache_key("lock")
2132- _CLEAR_LOCK_FILE = None # Unset due to successful lock delete
2133-
2134 return retval
2135
2136 return new_f
2137@@ -584,6 +564,14 @@ def status_parser(parser):
2138
2139 * AVAILABLE: whether this service would be available if this machine
2140 were attached. The possible values are yes or no.
2141+
2142+ If --simulate-with-token is used, then the output has five
2143+ columns. SERVICE, AVAILABLE, ENTITLED and DESCRIPTION are the same
2144+ as mentioned above, and AUTO_ENABLED shows whether the service is set
2145+ to be enabled when that token is attached.
2146+
2147+ If the --all flag is set, beta and unavailable services are also
2148+ listed in the output.
2149 """
2150 )
2151
2152@@ -605,6 +593,12 @@ def status_parser(parser):
2153 ),
2154 )
2155 parser.add_argument(
2156+ "--simulate-with-token",
2157+ metavar="TOKEN",
2158+ action="store",
2159+ help=("simulate the output status using a provided token"),
2160+ )
2161+ parser.add_argument(
2162 "--all",
2163 action="store_true",
2164 help="Allow the visualization of beta services",
2165@@ -623,7 +617,7 @@ def _perform_disable(entitlement_name, cfg, *, assume_yes):
2166
2167 @return: True on success, False otherwise
2168 """
2169- ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[entitlement_name]
2170+ ent_cls = entitlements.entitlement_factory(entitlement_name)
2171 entitlement = ent_cls(cfg, assume_yes=assume_yes)
2172 ret = entitlement.disable()
2173 cfg.status() # Update the status cache
2174@@ -639,7 +633,9 @@ def get_valid_entitlement_names(names: List[str]):
2175 entitlements_found = []
2176
2177 for ent_name in names:
2178- if ent_name in entitlements.ENTITLEMENT_CLASS_BY_NAME:
2179+ if ent_name in entitlements.valid_services(
2180+ allow_beta=True, all_names=True
2181+ ):
2182 entitlements_found.append(ent_name)
2183
2184 entitlements_not_found = sorted(set(names) - set(entitlements_found))
2185@@ -878,12 +874,14 @@ def action_enable(args, *, cfg, **kwargs):
2186 )
2187 valid_services_names = entitlements.valid_services(allow_beta=args.beta)
2188 ret = True
2189-
2190 for ent_name in entitlements_found:
2191 try:
2192- ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[ent_name]
2193+ ent_cls = entitlements.entitlement_factory(ent_name)
2194 entitlement = ent_cls(
2195- cfg, assume_yes=args.assume_yes, allow_beta=args.beta
2196+ cfg,
2197+ assume_yes=args.assume_yes,
2198+ allow_beta=args.beta,
2199+ called_name=ent_name,
2200 )
2201 ent_ret, reason = entitlement.enable()
2202 cfg.status() # Update the status cache
2203@@ -991,27 +989,7 @@ def _detach(cfg: config.UAConfig, assume_yes: bool) -> int:
2204 return 0
2205
2206
2207-def _attach_with_token(
2208- cfg: config.UAConfig, token: str, allow_enable: bool
2209-) -> int:
2210- """Common functionality to take a token and attach via contract backend"""
2211- try:
2212- contract.request_updated_contract(
2213- cfg, token, allow_enable=allow_enable
2214- )
2215- except util.UrlError as exc:
2216- with util.disable_log_to_console():
2217- logging.exception(exc)
2218- print(ua_status.MESSAGE_ATTACH_FAILURE)
2219- cfg.status() # Persist updated status in the event of partial attach
2220- config.update_ua_messages(cfg)
2221- return 1
2222- except exceptions.UserFacingError as exc:
2223- logging.warning(exc.msg)
2224- cfg.status() # Persist updated status in the event of partial attach
2225- config.update_ua_messages(cfg)
2226- return 1
2227-
2228+def _post_cli_attach(cfg: config.UAConfig) -> None:
2229 contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][
2230 "name"
2231 ]
2232@@ -1025,75 +1003,74 @@ def _attach_with_token(
2233 else:
2234 print(ua_status.MESSAGE_ATTACH_SUCCESS_NO_CONTRACT_NAME)
2235
2236- config.update_ua_messages(cfg)
2237- jobs.disable_license_check_if_applicable(cfg)
2238 action_status(args=None, cfg=cfg)
2239- return 0
2240-
2241
2242-def _get_contract_token_from_cloud_identity(cfg: config.UAConfig) -> str:
2243- """Detect cloud_type and request a contract token from identity info.
2244
2245- :param cfg: a ``config.UAConfig`` instance
2246-
2247- :raise NonAutoAttachImageError: When not on an auto-attach image type.
2248- :raise UrlError: On unexpected connectivity issues to contract
2249- server or inability to access identity doc from metadata service.
2250- :raise ContractAPIError: On unexpected errors when talking to the contract
2251- server.
2252- :raise NonAutoAttachImageError: If this cloud type does not have
2253- auto-attach support.
2254+@assert_root
2255+@assert_lock_file("ua auto-attach")
2256+def action_auto_attach(args, *, cfg):
2257+ disable_auto_attach = util.is_config_value_true(
2258+ config=cfg.cfg, path_to_value="features.disable_auto_attach"
2259+ )
2260+ if disable_auto_attach:
2261+ msg = "Skipping auto-attach. Config disable_auto_attach is set."
2262+ logging.debug(msg)
2263+ print(msg)
2264+ return 0
2265
2266- :return: contract token obtained from identity doc
2267- """
2268+ instance = None # type: Optional[AutoAttachCloudInstance]
2269 try:
2270 instance = identity.cloud_instance_factory()
2271- except exceptions.UserFacingError as e:
2272+ except exceptions.CloudFactoryError as e:
2273 if cfg.is_attached:
2274 # We are attached on non-Pro Image, just report already attached
2275 raise exceptions.AlreadyAttachedError(cfg)
2276- # Unattached on non-Pro return UserFacing error msg details
2277- raise e
2278+ if isinstance(e, exceptions.CloudFactoryNoCloudError):
2279+ raise exceptions.UserFacingError(
2280+ ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
2281+ )
2282+ if isinstance(e, exceptions.CloudFactoryNonViableCloudError):
2283+ raise exceptions.UserFacingError(
2284+ ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
2285+ )
2286+ if isinstance(e, exceptions.CloudFactoryUnsupportedCloudError):
2287+ raise exceptions.NonAutoAttachImageError(
2288+ ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
2289+ cloud_type=e.cloud_type
2290+ )
2291+ )
2292+ # we shouldn't get here, but this is a reasonable default just in case
2293+ raise exceptions.UserFacingError(
2294+ ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
2295+ )
2296+
2297+ if not instance:
2298+ # we shouldn't get here, but this is a reasonable default just in case
2299+ raise exceptions.UserFacingError(
2300+ ua_status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
2301+ )
2302+
2303 current_iid = identity.get_instance_id()
2304 if cfg.is_attached:
2305 prev_iid = cfg.read_cache("instance-id")
2306 if str(current_iid) == str(prev_iid):
2307- raise exceptions.AlreadyAttachedError(cfg)
2308+ raise exceptions.AlreadyAttachedOnPROError(str(current_iid))
2309 print("Re-attaching Ubuntu Advantage subscription on new instance")
2310 if _detach(cfg, assume_yes=True) != 0:
2311 raise exceptions.UserFacingError(
2312 ua_status.MESSAGE_DETACH_AUTOMATION_FAILURE
2313 )
2314- contract_client = contract.UAContractClient(cfg)
2315- try:
2316- tokenResponse = contract_client.request_auto_attach_contract_token(
2317- instance=instance
2318- )
2319- except contract.ContractAPIError as e:
2320- if e.code and 400 <= e.code < 500:
2321- raise exceptions.NonAutoAttachImageError(
2322- ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
2323- )
2324- raise e
2325- if current_iid:
2326- cfg.write_cache("instance-id", current_iid)
2327-
2328- return tokenResponse["contractToken"]
2329-
2330
2331-@assert_root
2332-@assert_lock_file("ua auto-attach")
2333-def action_auto_attach(args, *, cfg):
2334- disable_auto_attach = util.is_config_value_true(
2335- config=cfg.cfg, path_to_value="features.disable_auto_attach"
2336- )
2337- if disable_auto_attach:
2338- msg = "Skipping auto-attach. Config disable_auto_attach is set."
2339- logging.debug(msg)
2340- print(msg)
2341+ try:
2342+ actions.auto_attach(cfg, instance)
2343+ except util.UrlError:
2344+ print(ua_status.MESSAGE_ATTACH_FAILURE)
2345+ return 1
2346+ except exceptions.UserFacingError:
2347+ return 1
2348+ else:
2349+ _post_cli_attach(cfg)
2350 return 0
2351- token = _get_contract_token_from_cloud_identity(cfg)
2352- return _attach_with_token(cfg, token=token, allow_enable=True)
2353
2354
2355 @assert_not_attached
2356@@ -1104,9 +1081,18 @@ def action_attach(args, *, cfg):
2357 raise exceptions.UserFacingError(
2358 ua_status.MESSAGE_ATTACH_REQUIRES_TOKEN
2359 )
2360- return _attach_with_token(
2361- cfg, token=args.token, allow_enable=args.auto_enable
2362- )
2363+ try:
2364+ actions.attach_with_token(
2365+ cfg, token=args.token, allow_enable=args.auto_enable
2366+ )
2367+ except util.UrlError:
2368+ print(ua_status.MESSAGE_ATTACH_FAILURE)
2369+ return 1
2370+ except exceptions.UserFacingError:
2371+ return 1
2372+ else:
2373+ _post_cli_attach(cfg)
2374+ return 0
2375
2376
2377 def _write_command_output_to_file(
2378@@ -1135,7 +1121,7 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
2379 "ua status --format json", "{}/ua-status.json".format(output_dir)
2380 )
2381 _write_command_output_to_file(
2382- "canonical-livepatch status",
2383+ "{} status".format(LIVEPATCH_CMD),
2384 "{}/livepatch-status.txt".format(output_dir),
2385 )
2386 _write_command_output_to_file(
2387@@ -1164,11 +1150,12 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
2388 )
2389
2390 ua_logs = (
2391- cfg.cfg_path or "/etc/ubuntu-advantage/uaclient.conf",
2392+ cfg.cfg_path or DEFAULT_CONFIG_FILE,
2393 cfg.log_file,
2394 cfg.timer_log_file,
2395 cfg.license_check_log_file,
2396 cfg.data_path("jobs-status"),
2397+ CLOUD_BUILD_INFO,
2398 *(
2399 entitlement.repo_list_file_tmpl.format(name=entitlement.name)
2400 for entitlement in entitlements.ENTITLEMENT_CLASSES
2401@@ -1178,6 +1165,9 @@ def action_collect_logs(args, *, cfg: config.UAConfig):
2402
2403 for log in ua_logs:
2404 if os.path.isfile(log):
2405+ log_content = util.load_file(log)
2406+ log_content = util.redact_sensitive_logs(log_content)
2407+ util.write_file(log, log_content)
2408 shutil.copy(log, output_dir)
2409
2410 with tarfile.open(output_file, "w:gz") as results:
2411@@ -1189,29 +1179,36 @@ def get_parser():
2412 base_desc = __doc__
2413 non_beta_services_desc = []
2414 beta_services_desc = []
2415- sorted_classes = sorted(entitlements.ENTITLEMENT_CLASS_BY_NAME.items())
2416- for name, ent_cls in sorted_classes:
2417- if ent_cls.help_doc_url:
2418- url = " ({})".format(ent_cls.help_doc_url)
2419- else:
2420- url = ""
2421- service_line = service_line_tmpl.format(
2422- name=name, description=ent_cls.description, url=url
2423- )
2424- if len(service_line) <= 80:
2425- service_info = [service_line]
2426- else:
2427- wrapped_words = []
2428- line = service_line
2429- while len(line) > 80:
2430- [line, wrapped_word] = line.rsplit(" ", 1)
2431- wrapped_words.insert(0, wrapped_word)
2432- service_info = [line + "\n " + " ".join(wrapped_words)]
2433-
2434- if ent_cls.is_beta:
2435- beta_services_desc.extend(service_info)
2436- else:
2437- non_beta_services_desc.extend(service_info)
2438+
2439+ resources = contract.get_available_resources(config.UAConfig())
2440+ for resource in resources:
2441+ ent_cls = entitlements.entitlement_factory(resource["name"])
2442+ if ent_cls:
2443+ # Because we are not sure of the presentation name if unattached
2444+ presentation_name = resource.get("presentedAs", resource["name"])
2445+ if ent_cls.help_doc_url:
2446+ url = " ({})".format(ent_cls.help_doc_url)
2447+ else:
2448+ url = ""
2449+ service_line = service_line_tmpl.format(
2450+ name=presentation_name,
2451+ description=ent_cls.description,
2452+ url=url,
2453+ )
2454+ if len(service_line) <= 80:
2455+ service_info = [service_line]
2456+ else:
2457+ wrapped_words = []
2458+ line = service_line
2459+ while len(line) > 80:
2460+ [line, wrapped_word] = line.rsplit(" ", 1)
2461+ wrapped_words.insert(0, wrapped_word)
2462+ service_info = [line + "\n " + " ".join(wrapped_words)]
2463+
2464+ if ent_cls.is_beta:
2465+ beta_services_desc.extend(service_info)
2466+ else:
2467+ non_beta_services_desc.extend(service_info)
2468
2469 parser = UAArgumentParser(
2470 prog=NAME,
2471@@ -1327,7 +1324,11 @@ def action_status(args, *, cfg):
2472 if not cfg:
2473 cfg = config.UAConfig()
2474 show_beta = args.all if args else False
2475- status = cfg.status(show_beta=show_beta)
2476+ token = args.simulate_with_token if args else None
2477+ if token:
2478+ status = cfg.simulate_status(token=token, show_beta=show_beta)
2479+ else:
2480+ status = cfg.status(show_beta=show_beta)
2481 active_value = ua_status.UserFacingConfigStatus.ACTIVE.value
2482 config_active = bool(status["execution_status"] == active_value)
2483 if args and args.wait and config_active:
2484@@ -1460,8 +1461,7 @@ def main_error_handler(func):
2485 with util.disable_log_to_console():
2486 logging.error("KeyboardInterrupt")
2487 print("Interrupt received; exiting.", file=sys.stderr)
2488- if _CLEAR_LOCK_FILE:
2489- _CLEAR_LOCK_FILE("lock")
2490+ lock.clear_lock_file_if_present()
2491 sys.exit(1)
2492 except util.UrlError as exc:
2493 if "CERTIFICATE_VERIFY_FAILED" in str(exc):
2494@@ -1482,23 +1482,20 @@ def main_error_handler(func):
2495 msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_TMPL
2496 logging.exception(msg_tmpl.format(**msg_args))
2497 print(ua_status.MESSAGE_CONNECTIVITY_ERROR, file=sys.stderr)
2498- if _CLEAR_LOCK_FILE:
2499- _CLEAR_LOCK_FILE("lock")
2500+ lock.clear_lock_file_if_present()
2501 sys.exit(1)
2502 except exceptions.UserFacingError as exc:
2503 with util.disable_log_to_console():
2504 logging.error(exc.msg)
2505 print("{}".format(exc.msg), file=sys.stderr)
2506- if _CLEAR_LOCK_FILE:
2507- if not isinstance(exc, exceptions.LockHeldError):
2508- # Only clear the lock if it is ours.
2509- _CLEAR_LOCK_FILE("lock")
2510+ if not isinstance(exc, exceptions.LockHeldError):
2511+ # Only clear the lock if it is ours.
2512+ lock.clear_lock_file_if_present()
2513 sys.exit(exc.exit_code)
2514 except Exception:
2515 with util.disable_log_to_console():
2516 logging.exception("Unhandled exception, please file a bug")
2517- if _CLEAR_LOCK_FILE:
2518- _CLEAR_LOCK_FILE("lock")
2519+ lock.clear_lock_file_if_present()
2520 print(ua_status.MESSAGE_UNEXPECTED_ERROR, file=sys.stderr)
2521 sys.exit(1)
2522
2523diff --git a/uaclient/clouds/aws.py b/uaclient/clouds/aws.py
2524index 120ded6..e8a1c07 100644
2525--- a/uaclient/clouds/aws.py
2526+++ b/uaclient/clouds/aws.py
2527@@ -1,11 +1,17 @@
2528+import logging
2529 from typing import Any, Dict
2530 from urllib.error import HTTPError
2531
2532-from uaclient import util
2533+from uaclient import exceptions, util
2534 from uaclient.clouds import AutoAttachCloudInstance
2535
2536-IMDS_V2_TOKEN_URL = "http://169.254.169.254/latest/api/token"
2537-IMDS_URL = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7"
2538+IMDS_IPV4_ADDRESS = "169.254.169.254"
2539+IMDS_IPV6_ADDRESS = "[fd00:ec2::254]"
2540+
2541+IMDS_IP_ADDRESS = (IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
2542+IMDS_V2_TOKEN_URL = "http://{}/latest/api/token"
2543+IMDS_URL = "http://{}/latest/dynamic/instance-identity/pkcs7"
2544+
2545 SYS_HYPERVISOR_PRODUCT_UUID = "/sys/hypervisor/uuid"
2546 DMI_PRODUCT_SERIAL = "/sys/class/dmi/id/product_serial"
2547 DMI_PRODUCT_UUID = "/sys/class/dmi/id/product_uuid"
2548@@ -18,27 +24,58 @@ AWS_TOKEN_REQ_HEADER = AWS_TOKEN_PUT_HEADER + "-ttl-seconds"
2549 class UAAutoAttachAWSInstance(AutoAttachCloudInstance):
2550
2551 _api_token = None
2552+ _ip_address = None
2553+
2554+ def _get_imds_url_response(self):
2555+ headers = self._request_imds_v2_token_headers()
2556+ return util.readurl(
2557+ IMDS_URL.format(self._ip_address), headers=headers, timeout=1
2558+ )
2559
2560 # mypy does not handle @property around inner decorators
2561 # https://github.com/python/mypy/issues/1362
2562 @property # type: ignore
2563 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])
2564 def identity_doc(self) -> Dict[str, Any]:
2565- headers = self._get_imds_v2_token_headers()
2566- response, _headers = util.readurl(IMDS_URL, headers=headers)
2567+ response, _headers = self._get_imds_url_response()
2568 return {"pkcs7": response}
2569
2570+ def _request_imds_v2_token_headers(self):
2571+ for address in IMDS_IP_ADDRESS:
2572+ try:
2573+ headers = self._get_imds_v2_token_headers(ip_address=address)
2574+ except HTTPError as e:
2575+ raise e
2576+ except Exception as e:
2577+ msg = (
2578+ "Could not reach AWS IMDS at http://{endpoint}:"
2579+ " {reason}\n".format(
2580+ endpoint=address, reason=getattr(e, "reason", "")
2581+ )
2582+ )
2583+ logging.debug(msg)
2584+ else:
2585+ self._ip_address = address
2586+ break
2587+ if self._ip_address is None:
2588+ raise exceptions.UserFacingError(
2589+ "No valid AWS IMDS endpoint discovered at addresses: %s"
2590+ % ", ".join(IMDS_IP_ADDRESS)
2591+ )
2592+ return headers
2593+
2594 @util.retry(HTTPError, retry_sleeps=[1, 2, 5])
2595- def _get_imds_v2_token_headers(self):
2596+ def _get_imds_v2_token_headers(self, ip_address):
2597 if self._api_token == "IMDSv1":
2598 return None
2599 elif self._api_token:
2600 return {AWS_TOKEN_PUT_HEADER: self._api_token}
2601 try:
2602 response, _headers = util.readurl(
2603- IMDS_V2_TOKEN_URL,
2604+ IMDS_V2_TOKEN_URL.format(ip_address),
2605 method="PUT",
2606 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2607+ timeout=1,
2608 )
2609 except HTTPError as e:
2610 if e.code == 404:
2611@@ -46,6 +83,7 @@ class UAAutoAttachAWSInstance(AutoAttachCloudInstance):
2612 return None
2613 else:
2614 raise
2615+
2616 self._api_token = response
2617 return {AWS_TOKEN_PUT_HEADER: self._api_token}
2618
2619diff --git a/uaclient/clouds/identity.py b/uaclient/clouds/identity.py
2620index dee23f6..cd8f7b4 100644
2621--- a/uaclient/clouds/identity.py
2622+++ b/uaclient/clouds/identity.py
2623@@ -2,7 +2,7 @@ import logging
2624 from enum import Enum
2625 from typing import Dict, Optional, Tuple, Type # noqa: F401
2626
2627-from uaclient import clouds, exceptions, status, util
2628+from uaclient import clouds, exceptions, util
2629 from uaclient.config import apply_config_settings_override
2630
2631 # Mapping of datasource names to cloud-id responses. Trusty compat with Xenial+
2632@@ -50,6 +50,15 @@ def get_cloud_type() -> Tuple[Optional[str], Optional[NoCloudTypeReason]]:
2633
2634
2635 def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:
2636+ """
2637+ :raises CloudFactoryError: if no cloud instance object can be constructed
2638+ :raises CloudFactoryNoCloudError: if no cloud instance object can be
2639+ constructed because we are not on a cloud
2640+ :raises CloudFactoryUnsupportedCloudError: if no cloud instance object can
2641+ be constructed because we don't have a class for the cloud we're on
2642+ :raises CloudFactoryNonViableCloudError: if no cloud instance object can be
2643+ constructed because we explicitly do not support the cloud we're on
2644+ """
2645 from uaclient.clouds import aws, azure, gcp
2646
2647 cloud_instance_map = {
2648@@ -62,19 +71,11 @@ def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:
2649
2650 cloud_type, _ = get_cloud_type()
2651 if not cloud_type:
2652- raise exceptions.UserFacingError(
2653- status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE
2654- )
2655+ raise exceptions.CloudFactoryNoCloudError(cloud_type)
2656 cls = cloud_instance_map.get(cloud_type)
2657 if not cls:
2658- raise exceptions.NonAutoAttachImageError(
2659- status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
2660- cloud_type=cloud_type
2661- )
2662- )
2663+ raise exceptions.CloudFactoryUnsupportedCloudError(cloud_type)
2664 instance = cls()
2665 if not instance.is_viable:
2666- raise exceptions.UserFacingError(
2667- status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
2668- )
2669+ raise exceptions.CloudFactoryNonViableCloudError(cloud_type)
2670 return instance
2671diff --git a/uaclient/clouds/tests/test_aws.py b/uaclient/clouds/tests/test_aws.py
2672index 68aa555..d319427 100644
2673--- a/uaclient/clouds/tests/test_aws.py
2674+++ b/uaclient/clouds/tests/test_aws.py
2675@@ -1,4 +1,5 @@
2676 import logging
2677+import re
2678 from io import BytesIO
2679 from urllib.error import HTTPError
2680
2681@@ -9,8 +10,13 @@ from uaclient.clouds.aws import (
2682 AWS_TOKEN_PUT_HEADER,
2683 AWS_TOKEN_REQ_HEADER,
2684 AWS_TOKEN_TTL_SECONDS,
2685+ IMDS_IPV4_ADDRESS,
2686+ IMDS_IPV6_ADDRESS,
2687+ IMDS_URL,
2688+ IMDS_V2_TOKEN_URL,
2689 UAAutoAttachAWSInstance,
2690 )
2691+from uaclient.exceptions import UserFacingError
2692
2693 M_PATH = "uaclient.clouds.aws."
2694
2695@@ -27,10 +33,12 @@ class TestUAAutoAttachAWSInstance:
2696 "http://me", 404, "No IMDSv2 support", None, BytesIO()
2697 )
2698 instance = UAAutoAttachAWSInstance()
2699- assert None is instance._get_imds_v2_token_headers()
2700+ assert None is instance._get_imds_v2_token_headers(
2701+ ip_address=IMDS_IPV4_ADDRESS
2702+ )
2703 assert "IMDSv1" == instance._api_token
2704 # No retries on 404. It is a permanent indication of no IMDSv2 support.
2705- instance._get_imds_v2_token_headers()
2706+ instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
2707 assert 1 == readurl.call_count
2708
2709 @mock.patch(M_PATH + "util.readurl")
2710@@ -41,14 +49,15 @@ class TestUAAutoAttachAWSInstance:
2711 readurl.return_value = "somebase64token==", {"header": "stuff"}
2712 assert {
2713 AWS_TOKEN_PUT_HEADER: "somebase64token=="
2714- } == instance._get_imds_v2_token_headers()
2715- instance._get_imds_v2_token_headers()
2716+ } == instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
2717+ instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS)
2718 assert "somebase64token==" == instance._api_token
2719 assert [
2720 mock.call(
2721 url,
2722 method="PUT",
2723 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2724+ timeout=1,
2725 )
2726 ] == readurl.call_args_list
2727
2728@@ -61,7 +70,7 @@ class TestUAAutoAttachAWSInstance:
2729 ):
2730 """Retry backoff before failing _get_imds_v2_token_headers."""
2731
2732- def fake_someurlerrors(url, method=None, headers=None):
2733+ def fake_someurlerrors(url, method=None, headers=None, timeout=1):
2734 if readurl.call_count <= fail_count:
2735 raise HTTPError(
2736 "http://me",
2737@@ -76,12 +85,16 @@ class TestUAAutoAttachAWSInstance:
2738 instance = UAAutoAttachAWSInstance()
2739 if exception:
2740 with pytest.raises(HTTPError) as excinfo:
2741- instance._get_imds_v2_token_headers()
2742+ instance._get_imds_v2_token_headers(
2743+ ip_address=IMDS_IPV4_ADDRESS
2744+ )
2745 assert 704 == excinfo.value.code
2746 else:
2747 assert {
2748 AWS_TOKEN_PUT_HEADER: "base64token=="
2749- } == instance._get_imds_v2_token_headers()
2750+ } == instance._get_imds_v2_token_headers(
2751+ ip_address=IMDS_IPV4_ADDRESS
2752+ )
2753
2754 expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)]
2755 assert expected_sleep_calls == sleep.call_args_list
2756@@ -107,12 +120,15 @@ class TestUAAutoAttachAWSInstance:
2757 token_url,
2758 method="PUT",
2759 headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2760+ timeout=1,
2761+ ),
2762+ mock.call(
2763+ url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}, timeout=1
2764 ),
2765- mock.call(url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}),
2766 ] == readurl.call_args_list
2767
2768 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
2769- @pytest.mark.parametrize("fail_count,exception", ((3, False), (4, True)))
2770+ @pytest.mark.parametrize("fail_count,exception", ((3, False),))
2771 @mock.patch(M_PATH + "util.time.sleep")
2772 @mock.patch(M_PATH + "util.readurl")
2773 def test_retry_backoff_on_failed_identity_doc(
2774@@ -120,7 +136,7 @@ class TestUAAutoAttachAWSInstance:
2775 ):
2776 """Retry backoff is attempted before failing to get AWS.identity_doc"""
2777
2778- def fake_someurlerrors(url, method=None, headers=None):
2779+ def fake_someurlerrors(url, method=None, headers=None, timeout=1):
2780 # due to _get_imds_v2_token_headers
2781 if "latest/api/token" in url:
2782 return "base64token==", {"header": "stuff"}
2783@@ -195,3 +211,92 @@ class TestUAAutoAttachAWSInstance:
2784 load_file.side_effect = fake_load_file
2785 instance = UAAutoAttachAWSInstance()
2786 assert viable is instance.is_viable
2787+
2788+ @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
2789+ @mock.patch(M_PATH + "util.readurl")
2790+ def test_identity_doc_default_to_ipv6_if_ipv4_fail(
2791+ self, readurl, caplog_text
2792+ ):
2793+ instance = UAAutoAttachAWSInstance()
2794+ ipv4_address = IMDS_IPV4_ADDRESS
2795+ ipv6_address = IMDS_IPV6_ADDRESS
2796+
2797+ def fake_someurlerrors(url, method=None, headers=None, timeout=1):
2798+ if ipv4_address in url:
2799+ raise Exception("IPv4 exception")
2800+
2801+ if url == IMDS_V2_TOKEN_URL.format(ipv6_address):
2802+ return "base64token==", {"header": "stuff"}
2803+
2804+ if url == IMDS_URL.format(ipv6_address):
2805+ return "pkcs7WOOT!==", {"header": "stuff"}
2806+
2807+ readurl.side_effect = fake_someurlerrors
2808+ assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc
2809+ expected = [
2810+ mock.call(
2811+ IMDS_V2_TOKEN_URL.format(ipv4_address),
2812+ method="PUT",
2813+ headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2814+ timeout=1,
2815+ ),
2816+ mock.call(
2817+ IMDS_V2_TOKEN_URL.format(ipv6_address),
2818+ method="PUT",
2819+ headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2820+ timeout=1,
2821+ ),
2822+ mock.call(
2823+ IMDS_URL.format(ipv6_address),
2824+ headers={AWS_TOKEN_PUT_HEADER: "base64token=="},
2825+ timeout=1,
2826+ ),
2827+ ]
2828+
2829+ assert expected == readurl.call_args_list
2830+
2831+ expected_log = "Could not reach AWS IMDS at http://169.254.169.254:"
2832+ assert expected_log in caplog_text()
2833+
2834+ @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
2835+ @mock.patch(M_PATH + "util.readurl")
2836+ def test_identity_doc_logs_error_if_both_ipv4_and_ipv6_fails(
2837+ self, readurl, caplog_text
2838+ ):
2839+
2840+ instance = UAAutoAttachAWSInstance()
2841+ ipv4_address = IMDS_IPV4_ADDRESS
2842+ ipv6_address = IMDS_IPV6_ADDRESS
2843+
2844+ readurl.side_effect = Exception("Exception")
2845+
2846+ expected_error = (
2847+ "No valid AWS IMDS endpoint discovered at "
2848+ "addresses: {}, {}".format(IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
2849+ )
2850+ with pytest.raises(UserFacingError, match=re.escape(expected_error)):
2851+ instance.identity_doc
2852+
2853+ expected = [
2854+ mock.call(
2855+ IMDS_V2_TOKEN_URL.format(ipv4_address),
2856+ method="PUT",
2857+ headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2858+ timeout=1,
2859+ ),
2860+ mock.call(
2861+ IMDS_V2_TOKEN_URL.format(ipv6_address),
2862+ method="PUT",
2863+ headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
2864+ timeout=1,
2865+ ),
2866+ ]
2867+ assert expected == readurl.call_args_list
2868+
2869+ expected_logs = [
2870+ "Could not reach AWS IMDS at http://169.254.169.254:",
2871+ "Could not reach AWS IMDS at http://[fd00:ec2::254]:",
2872+ ]
2873+
2874+ for expected_log in expected_logs:
2875+ assert expected_log in caplog_text()
2876diff --git a/uaclient/clouds/tests/test_identity.py b/uaclient/clouds/tests/test_identity.py
2877index 79faf0d..f223f80 100644
2878--- a/uaclient/clouds/tests/test_identity.py
2879+++ b/uaclient/clouds/tests/test_identity.py
2880@@ -1,13 +1,17 @@
2881 import mock
2882 import pytest
2883
2884-from uaclient import exceptions, status
2885 from uaclient.clouds.identity import (
2886 NoCloudTypeReason,
2887 cloud_instance_factory,
2888 get_cloud_type,
2889 get_instance_id,
2890 )
2891+from uaclient.exceptions import (
2892+ CloudFactoryNoCloudError,
2893+ CloudFactoryNonViableCloudError,
2894+ CloudFactoryUnsupportedCloudError,
2895+)
2896 from uaclient.util import ProcessExecutionError
2897
2898 M_PATH = "uaclient.clouds.identity."
2899@@ -89,22 +93,15 @@ class TestCloudInstanceFactory:
2900 None,
2901 NoCloudTypeReason.NO_CLOUD_DETECTED,
2902 )
2903- with pytest.raises(exceptions.UserFacingError) as excinfo:
2904+ with pytest.raises(CloudFactoryNoCloudError):
2905 cloud_instance_factory()
2906 assert 1 == m_get_cloud_type.call_count
2907- assert status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE == str(
2908- excinfo.value
2909- )
2910
2911- def test_raise_error_when_not_aws_or_azure(self, m_get_cloud_type):
2912+ def test_raise_error_when_not_supported(self, m_get_cloud_type):
2913 """Raise appropriate error when unable to determine cloud_type."""
2914 m_get_cloud_type.return_value = ("unsupported-cloud", None)
2915- with pytest.raises(exceptions.UserFacingError) as excinfo:
2916+ with pytest.raises(CloudFactoryUnsupportedCloudError):
2917 cloud_instance_factory()
2918- error_msg = status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
2919- cloud_type="unsupported-cloud"
2920- )
2921- assert error_msg == str(excinfo.value)
2922
2923 @pytest.mark.parametrize("cloud_type", ("aws", "azure"))
2924 def test_raise_error_when_not_viable_for_ubuntu_pro(
2925@@ -125,10 +122,8 @@ class TestCloudInstanceFactory:
2926
2927 with mock.patch(M_INSTANCE_PATH) as m_instance:
2928 m_instance.side_effect = fake_invalid_instance
2929- with pytest.raises(exceptions.UserFacingError) as excinfo:
2930+ with pytest.raises(CloudFactoryNonViableCloudError):
2931 cloud_instance_factory()
2932- error_msg = status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
2933- assert error_msg == str(excinfo.value)
2934
2935 @pytest.mark.parametrize(
2936 "cloud_type", ("aws", "aws-gov", "aws-china", "azure")
2937diff --git a/uaclient/config.py b/uaclient/config.py
2938index 6275839..f66322e 100644
2939--- a/uaclient/config.py
2940+++ b/uaclient/config.py
2941@@ -7,7 +7,7 @@ import sys
2942 from collections import OrderedDict, namedtuple
2943 from datetime import datetime
2944 from functools import wraps
2945-from typing import Any, Dict, Optional, Tuple, cast
2946+from typing import Any, Dict, List, Optional, Tuple, cast
2947
2948 import yaml
2949
2950@@ -47,6 +47,7 @@ DEFAULT_STATUS = {
2951 "created_at": "",
2952 "external_account_ids": [],
2953 },
2954+ "simulated": False,
2955 } # type: Dict[str, Any]
2956
2957 LOG = logging.getLogger(__name__)
2958@@ -326,13 +327,19 @@ class UAConfig:
2959 return {}
2960
2961 self._entitlements = {}
2962- contractInfo = machine_token["machineTokenInfo"]["contractInfo"]
2963+ contractInfo = machine_token.get("machineTokenInfo", {}).get(
2964+ "contractInfo"
2965+ )
2966+ if not contractInfo:
2967+ return {}
2968+
2969 tokens_by_name = dict(
2970- (e["type"], e["token"])
2971+ (e.get("type"), e.get("token"))
2972 for e in machine_token.get("resourceTokens", [])
2973 )
2974 ent_by_name = dict(
2975- (e["type"], e) for e in contractInfo["resourceEntitlements"]
2976+ (e.get("type"), e)
2977+ for e in contractInfo.get("resourceEntitlements", [])
2978 )
2979 for entitlement_name, ent_value in ent_by_name.items():
2980 entitlement_cfg = {"entitlement": ent_value}
2981@@ -551,16 +558,23 @@ class UAConfig:
2982 mode = 0o644
2983 util.write_file(filepath, content, mode=mode)
2984
2985- def _remove_beta_resources(self, response) -> Dict[str, Any]:
2986- """Remove beta services from response dict"""
2987- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
2988+ def _handle_beta_resources(self, show_beta, response) -> Dict[str, Any]:
2989+ """Remove beta services from response dict if needed"""
2990+ from uaclient.entitlements import entitlement_factory
2991+
2992+ config_allow_beta = util.is_config_value_true(
2993+ config=self.cfg, path_to_value="features.allow_beta"
2994+ )
2995+ show_beta |= config_allow_beta
2996+ if show_beta:
2997+ return response
2998
2999 new_response = copy.deepcopy(response)
3000
3001 released_resources = []
3002 for resource in new_response.get("services", {}):
3003 resource_name = resource["name"]
3004- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(resource_name)
3005+ ent_cls = entitlement_factory(resource_name)
3006
3007 if ent_cls is None:
3008 """
3009@@ -624,34 +638,36 @@ class UAConfig:
3010 def _unattached_status(self) -> Dict[str, Any]:
3011 """Return unattached status as a dict."""
3012 from uaclient.contract import get_available_resources
3013- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3014+ from uaclient.entitlements import entitlement_factory
3015
3016 response = copy.deepcopy(DEFAULT_STATUS)
3017 response["version"] = version.get_version(features=self.features)
3018
3019 resources = get_available_resources(self)
3020- for resource in sorted(resources, key=lambda x: x["name"]):
3021- if resource["available"]:
3022+ for resource in resources:
3023+ if resource.get("available"):
3024 available = status.UserFacingAvailability.AVAILABLE.value
3025 else:
3026 available = status.UserFacingAvailability.UNAVAILABLE.value
3027- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(resource["name"])
3028+ ent_cls = entitlement_factory(resource.get("name", ""))
3029
3030 if not ent_cls:
3031 LOG.debug(
3032 "Ignoring availability of unknown service %s"
3033 " from contract server",
3034- resource["name"],
3035+ resource.get("name", "without a 'name' key"),
3036 )
3037 continue
3038
3039 response["services"].append(
3040 {
3041- "name": resource["name"],
3042+ "name": resource.get("presentedAs", resource["name"]),
3043 "description": ent_cls.description,
3044 "available": available,
3045 }
3046 )
3047+ response["services"].sort(key=lambda x: x.get("name", ""))
3048+
3049 return response
3050
3051 def _attached_service_status(
3052@@ -670,7 +686,7 @@ class UAConfig:
3053 ent_status, details = ent.user_facing_status()
3054
3055 return {
3056- "name": ent.name,
3057+ "name": ent.presentation_name,
3058 "description": ent.description,
3059 "entitled": contract_status.value,
3060 "status": ent_status.value,
3061@@ -684,7 +700,7 @@ class UAConfig:
3062 def _attached_status(self) -> Dict[str, Any]:
3063 """Return configuration of attached status as a dictionary."""
3064 from uaclient.contract import get_available_resources
3065- from uaclient.entitlements import ENTITLEMENT_CLASSES
3066+ from uaclient.entitlements import entitlement_factory
3067
3068 response = copy.deepcopy(DEFAULT_STATUS)
3069 machineTokenInfo = self.machine_token["machineTokenInfo"]
3070@@ -725,15 +741,18 @@ class UAConfig:
3071
3072 inapplicable_resources = {
3073 resource["name"]: resource.get("description")
3074- for resource in sorted(resources, key=lambda x: x["name"])
3075- if not resource["available"]
3076+ for resource in sorted(resources, key=lambda x: x.get("name", ""))
3077+ if not resource.get("available")
3078 }
3079
3080- for ent_cls in ENTITLEMENT_CLASSES:
3081- ent = ent_cls(self)
3082- response["services"].append(
3083- self._attached_service_status(ent, inapplicable_resources)
3084- )
3085+ for resource in resources:
3086+ ent_cls = entitlement_factory(resource.get("name", ""))
3087+ if ent_cls:
3088+ ent = ent_cls(self)
3089+ response["services"].append(
3090+ self._attached_service_status(ent, inapplicable_resources)
3091+ )
3092+ response["services"].sort(key=lambda x: x.get("name", ""))
3093
3094 support = self.entitlements.get("support", {}).get("entitlement")
3095 if support:
3096@@ -742,7 +761,115 @@ class UAConfig:
3097 response["contract"]["tech_support_level"] = supportLevel
3098 return response
3099
3100- def status(self, show_beta=False) -> Dict[str, Any]:
3101+ def _get_entitlement_information(
3102+ self, entitlements: List[Dict[str, Any]], entitlement_name: str
3103+ ) -> Dict[str, Any]:
3104+ """Extract information from the entitlements array."""
3105+ for entitlement in entitlements:
3106+ if entitlement.get("type") == entitlement_name:
3107+ return {
3108+ "entitled": "yes" if entitlement.get("entitled") else "no",
3109+ "auto_enabled": "yes"
3110+ if entitlement.get("obligations", {}).get(
3111+ "enableByDefault"
3112+ )
3113+ else "no",
3114+ "affordances": entitlement.get("affordances", {}),
3115+ }
3116+ return {"entitled": "no", "auto_enabled": "no", "affordances": {}}
3117+
3118+ def simulate_status(
3119+ self, token: str, show_beta: bool = False
3120+ ) -> Dict[str, Any]:
3121+ """Return a status dictionary based on a token."""
3122+ from uaclient.contract import (
3123+ get_available_resources,
3124+ get_contract_information,
3125+ )
3126+ from uaclient.entitlements import entitlement_factory
3127+
3128+ response = copy.deepcopy(DEFAULT_STATUS)
3129+
3130+ contract_information = get_contract_information(self, token)
3131+
3132+ contract_info = contract_information.get("contractInfo", {})
3133+ account_info = contract_information.get("accountInfo", {})
3134+
3135+ response.update(
3136+ {
3137+ "version": version.get_version(features=self.features),
3138+ "contract": {
3139+ "id": contract_info.get("id", ""),
3140+ "name": contract_info.get("name", ""),
3141+ "created_at": contract_info.get("createdAt", ""),
3142+ "products": contract_info.get("products", []),
3143+ },
3144+ "account": {
3145+ "name": account_info.get("name", ""),
3146+ "id": account_info.get("id"),
3147+ "created_at": account_info.get("createdAt", ""),
3148+ "external_account_ids": account_info.get(
3149+ "externalAccountIDs", []
3150+ ),
3151+ },
3152+ "simulated": True,
3153+ }
3154+ )
3155+
3156+ if contract_info.get("effectiveTo"):
3157+ response["expires"] = contract_info.get("effectiveTo")
3158+ if contract_info.get("effectiveFrom"):
3159+ response["effective"] = contract_info.get("effectiveFrom")
3160+
3161+ status_cache = self.read_cache("status-cache")
3162+ if status_cache:
3163+ resources = status_cache.get("services")
3164+ else:
3165+ resources = get_available_resources(self)
3166+
3167+ entitlements = contract_info.get("resourceEntitlements", [])
3168+
3169+ inapplicable_resources = [
3170+ resource["name"]
3171+ for resource in sorted(resources, key=lambda x: x["name"])
3172+ if not resource["available"]
3173+ ]
3174+
3175+ for resource in resources:
3176+ entitlement_name = resource.get("name", "")
3177+ ent_cls = entitlement_factory(entitlement_name)
3178+ if ent_cls:
3179+ ent = ent_cls(self)
3180+ entitlement_information = self._get_entitlement_information(
3181+ entitlements, entitlement_name
3182+ )
3183+ response["services"].append(
3184+ {
3185+ "name": resource.get("presentedAs", ent.name),
3186+ "description": ent.description,
3187+ "entitled": entitlement_information["entitled"],
3188+ "auto_enabled": entitlement_information[
3189+ "auto_enabled"
3190+ ],
3191+ "available": "yes"
3192+ if ent.name not in inapplicable_resources
3193+ else "no",
3194+ }
3195+ )
3196+ response["services"].sort(key=lambda x: x.get("name", ""))
3197+
3198+ support = self._get_entitlement_information(entitlements, "support")
3199+ if support["entitled"]:
3200+ supportLevel = support["affordances"].get("supportLevel")
3201+ if supportLevel:
3202+ response["contract"]["tech_support_level"] = supportLevel
3203+
3204+ response.update(self._get_config_status())
3205+ response = self._handle_beta_resources(show_beta, response)
3206+
3207+ return response
3208+
3209+ def status(self, show_beta: bool = False) -> Dict[str, Any]:
3210 """Return status as a dict, using a cache for non-root users
3211
3212 When unattached, get available resources from the contract service
3213@@ -751,7 +878,6 @@ class UAConfig:
3214
3215 Write the status-cache when called by root.
3216 """
3217-
3218 if os.getuid() != 0:
3219 response = cast("Dict[str, Any]", self.read_cache("status-cache"))
3220 if not response:
3221@@ -760,7 +886,9 @@ class UAConfig:
3222 response = self._unattached_status()
3223 else:
3224 response = self._attached_status()
3225+
3226 response.update(self._get_config_status())
3227+
3228 if os.getuid() == 0:
3229 self.write_cache("status-cache", response)
3230
3231@@ -773,12 +901,7 @@ class UAConfig:
3232 ),
3233 )
3234
3235- config_allow_beta = util.is_config_value_true(
3236- config=self.cfg, path_to_value="features.allow_beta"
3237- )
3238- show_beta |= config_allow_beta
3239- if not show_beta:
3240- response = self._remove_beta_resources(response)
3241+ response = self._handle_beta_resources(show_beta, response)
3242
3243 return response
3244
3245@@ -790,7 +913,7 @@ class UAConfig:
3246 :raises: UserFacingError when no help is available.
3247 """
3248 from uaclient.contract import get_available_resources
3249- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3250+ from uaclient.entitlements import entitlement_factory
3251
3252 resources = get_available_resources(self)
3253 help_resource = None
3254@@ -802,11 +925,12 @@ class UAConfig:
3255 response_dict["name"] = name
3256
3257 for resource in resources:
3258- if resource["name"] == name and name in ENTITLEMENT_CLASS_BY_NAME:
3259- help_resource = resource
3260- help_ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(name)
3261- help_ent = help_ent_cls(self)
3262- break
3263+ if resource["name"] == name or resource.get("presentedAs") == name:
3264+ help_ent_cls = entitlement_factory(resource["name"])
3265+ if help_ent_cls:
3266+ help_resource = resource
3267+ help_ent = help_ent_cls(self)
3268+ break
3269
3270 if help_resource is None:
3271 raise exceptions.UserFacingError(
3272diff --git a/uaclient/contract.py b/uaclient/contract.py
3273index 0390bf4..f29c5d4 100644
3274--- a/uaclient/contract.py
3275+++ b/uaclient/contract.py
3276@@ -2,6 +2,7 @@ import logging
3277 from typing import Any, Dict, List, Optional
3278
3279 from uaclient import clouds, exceptions, serviceclient, status, util
3280+from uaclient.config import UAConfig
3281
3282 API_V1_CONTEXT_MACHINE_TOKEN = "/v1/context/machines/token"
3283 API_V1_TMPL_CONTEXT_MACHINE_TOKEN_RESOURCE = (
3284@@ -13,6 +14,7 @@ API_V1_TMPL_RESOURCE_MACHINE_ACCESS = (
3285 )
3286 API_V1_AUTO_ATTACH_CLOUD_TOKEN = "/v1/clouds/{cloud_type}/token"
3287 API_V1_MACHINE_ACTIVITY = "/v1/contracts/{contract}/machine-activity/{machine}"
3288+API_V1_CONTRACT_INFORMATION = "/v1/contract"
3289 ATTACH_FAIL_DATE_FORMAT = "%B %d, %Y"
3290
3291
3292@@ -100,6 +102,16 @@ class UAContractClient(serviceclient.UAServiceClient):
3293 )
3294 return resource_response
3295
3296+ def request_contract_information(
3297+ self, contract_token: str
3298+ ) -> Dict[str, Any]:
3299+ headers = self.headers()
3300+ headers.update({"Authorization": "Bearer {}".format(contract_token)})
3301+ response_data, _response_headers = self.request_url(
3302+ API_V1_CONTRACT_INFORMATION, headers=headers
3303+ )
3304+ return response_data
3305+
3306 def request_auto_attach_contract_token(
3307 self, *, instance: clouds.AutoAttachCloudInstance
3308 ):
3309@@ -355,7 +367,7 @@ def process_entitlement_delta(
3310 :raise UserFacingError: on failure to process deltas.
3311 :return: Dict of processed deltas
3312 """
3313- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3314+ from uaclient.entitlements import entitlement_factory
3315
3316 if series_overrides:
3317 util.apply_series_overrides(new_access)
3318@@ -371,9 +383,8 @@ def process_entitlement_delta(
3319 orig_access, new_access
3320 )
3321 )
3322- try:
3323- ent_cls = ENTITLEMENT_CLASS_BY_NAME[name]
3324- except KeyError:
3325+ ent_cls = entitlement_factory(name)
3326+ if not ent_cls:
3327 logging.debug(
3328 'Skipping entitlement deltas for "%s". No such class', name
3329 )
3330@@ -469,8 +480,14 @@ def request_updated_contract(
3331 )
3332
3333
3334-def get_available_resources(cfg) -> List[Dict]:
3335- """Query available resources from the contrct server for this machine."""
3336+def get_available_resources(cfg: UAConfig) -> List[Dict]:
3337+ """Query available resources from the contract server for this machine."""
3338 client = UAContractClient(cfg)
3339 resources = client.request_resources()
3340 return resources.get("resources", [])
3341+
3342+
3343+def get_contract_information(cfg: UAConfig, token: str) -> Dict[str, Any]:
3344+ """Query contract information for a specific token"""
3345+ client = UAContractClient(cfg)
3346+ return client.request_contract_information(token)
3347diff --git a/uaclient/defaults.py b/uaclient/defaults.py
3348index ee218de..e556abc 100644
3349--- a/uaclient/defaults.py
3350+++ b/uaclient/defaults.py
3351@@ -16,6 +16,7 @@ BASE_SECURITY_URL = "https://ubuntu.com/security"
3352 BASE_UA_URL = "https://ubuntu.com/advantage"
3353 EOL_UA_URL_TMPL = "https://ubuntu.com/{hyphenatedrelease}"
3354 BASE_ESM_URL = "https://ubuntu.com/esm"
3355+CLOUD_BUILD_INFO = "/etc/cloud/build.info"
3356 DOCUMENTATION_URL = (
3357 "https://discourse.ubuntu.com/t/ubuntu-advantage-client/21788"
3358 )
3359diff --git a/uaclient/entitlements/__init__.py b/uaclient/entitlements/__init__.py
3360index d8cc495..18b0b3d 100644
3361--- a/uaclient/entitlements/__init__.py
3362+++ b/uaclient/entitlements/__init__.py
3363@@ -1,4 +1,4 @@
3364-from typing import Dict, List, Type, cast # noqa: F401
3365+from typing import List, Type # noqa: F401
3366
3367 from uaclient.config import UAConfig
3368 from uaclient.entitlements import fips
3369@@ -23,27 +23,46 @@ ENTITLEMENT_CLASSES = [
3370 ] # type: List[Type[UAEntitlement]]
3371
3372
3373-ENTITLEMENT_CLASS_BY_NAME = dict(
3374- (cast(str, cls.name), cls) for cls in ENTITLEMENT_CLASSES
3375-) # type: Dict[str, Type[UAEntitlement]]
3376+def entitlement_factory(name: str):
3377+ """Returns a UAEntitlement class based on the provided name.
3378+
3379+ The return type is Optional[Type[UAEntitlement]].
3380+ It cannot be explicit because of the Python version on Xenial (3.5.2).
3381+ """
3382+ for entitlement in ENTITLEMENT_CLASSES:
3383+ if name in entitlement().valid_names:
3384+ return entitlement
3385+ return None
3386
3387
3388-def valid_services(allow_beta: bool = False) -> List[str]:
3389+def valid_services(
3390+ allow_beta: bool = False, all_names: bool = False
3391+) -> List[str]:
3392 """Return a list of valid (non-beta) services.
3393
3394 @param allow_beta: if we should allow beta services to be marked as valid
3395+ @param all_names: if we should return all the names for a service instead
3396+ of just the presentation_name
3397 """
3398 cfg = UAConfig()
3399 allow_beta_cfg = is_config_value_true(cfg.cfg, "features.allow_beta")
3400 allow_beta |= allow_beta_cfg
3401
3402- if allow_beta:
3403- return sorted(ENTITLEMENT_CLASS_BY_NAME.keys())
3404+ entitlements = ENTITLEMENT_CLASSES
3405+ if not allow_beta:
3406+ entitlements = [
3407+ entitlement
3408+ for entitlement in entitlements
3409+ if not entitlement.is_beta
3410+ ]
3411+
3412+ if all_names:
3413+ names = []
3414+ for entitlement in entitlements:
3415+ names.extend(entitlement().valid_names)
3416+
3417+ return sorted(names)
3418
3419 return sorted(
3420- [
3421- ent_name
3422- for ent_name, ent_cls in ENTITLEMENT_CLASS_BY_NAME.items()
3423- if not ent_cls.is_beta
3424- ]
3425+ [entitlement().presentation_name for entitlement in entitlements]
3426 )
3427diff --git a/uaclient/entitlements/base.py b/uaclient/entitlements/base.py
3428index 4bbe943..42376d4 100644
3429--- a/uaclient/entitlements/base.py
3430+++ b/uaclient/entitlements/base.py
3431@@ -58,6 +58,14 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3432 pass
3433
3434 @property
3435+ def valid_names(self) -> List[str]:
3436+ """The list of names this entitlement may be called."""
3437+ valid_names = [self.name]
3438+ if self.presentation_name != self.name:
3439+ valid_names.append(self.presentation_name)
3440+ return valid_names
3441+
3442+ @property
3443 @abc.abstractmethod
3444 def title(self) -> str:
3445 """The human readable title of this entitlement"""
3446@@ -70,6 +78,16 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3447 pass
3448
3449 @property
3450+ def presentation_name(self) -> str:
3451+ """The user-facing name shown for this entitlement"""
3452+ return (
3453+ self.cfg.entitlements.get(self.name, {})
3454+ .get("entitlement", {})
3455+ .get("affordances", {})
3456+ .get("presentedAs", self.name)
3457+ )
3458+
3459+ @property
3460 def help_info(self) -> str:
3461 """Help information for the entitlement"""
3462 if self._help_info is None:
3463@@ -132,6 +150,7 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3464 cfg: Optional[config.UAConfig] = None,
3465 assume_yes: bool = False,
3466 allow_beta: bool = False,
3467+ called_name: str = "",
3468 ) -> None:
3469 """Setup UAEntitlement instance
3470
3471@@ -142,6 +161,7 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3472 self.cfg = cfg
3473 self.assume_yes = assume_yes
3474 self.allow_beta = allow_beta
3475+ self._called_name = called_name
3476 self._valid_service = None
3477
3478 @property
3479@@ -310,10 +330,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3480 True if all required services are active
3481 False is at least one of the required services is disabled
3482 """
3483- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3484+ from uaclient.entitlements import entitlement_factory
3485
3486 for required_service in self.required_services:
3487- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(required_service)
3488+ ent_cls = entitlement_factory(required_service)
3489 if ent_cls:
3490 ent_status, _ = ent_cls(self.cfg).application_status()
3491 if ent_status != status.ApplicationStatus.ENABLED:
3492@@ -329,10 +349,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3493 True if there are incompatible services enabled
3494 False if there are no incompatible services enabled
3495 """
3496- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3497+ from uaclient.entitlements import entitlement_factory
3498
3499 for incompatible_service in self.incompatible_services:
3500- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(incompatible_service)
3501+ ent_cls = entitlement_factory(incompatible_service)
3502 if ent_cls:
3503 ent_status, _ = ent_cls(self.cfg).application_status()
3504 if ent_status == status.ApplicationStatus.ENABLED:
3505@@ -355,14 +375,14 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3506 features:
3507 block_disable_on_enable: true
3508 """
3509- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3510+ from uaclient.entitlements import entitlement_factory
3511
3512 cfg_block_disable_on_enable = util.is_config_value_true(
3513 config=self.cfg.cfg,
3514 path_to_value="features.block_disable_on_enable",
3515 )
3516 for incompatible_service in self.incompatible_services:
3517- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(incompatible_service)
3518+ ent_cls = entitlement_factory(incompatible_service)
3519
3520 if ent_cls:
3521 ent = ent_cls(self.cfg)
3522@@ -412,10 +432,15 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3523 that must be enabled first. In that situation, we can ask the user
3524 if the required service should be enabled before proceeding.
3525 """
3526- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3527+ from uaclient.entitlements import entitlement_factory
3528
3529 for required_service in self.required_services:
3530- ent_cls = ENTITLEMENT_CLASS_BY_NAME[required_service]
3531+ ent_cls = entitlement_factory(required_service)
3532+ if not ent_cls:
3533+ msg = "Required service {} not found.".format(required_service)
3534+ logging.error(msg)
3535+ return False
3536+
3537 ent = ent_cls(self.cfg, allow_beta=True)
3538
3539 is_service_disabled = (
3540@@ -555,10 +580,10 @@ class UAEntitlement(metaclass=abc.ABCMeta):
3541 and prompt for confirmation to disable these services
3542 as well.
3543 """
3544- from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
3545+ from uaclient.entitlements import entitlement_factory
3546
3547 for dependent_service in self.dependent_services:
3548- ent_cls = ENTITLEMENT_CLASS_BY_NAME[dependent_service]
3549+ ent_cls = entitlement_factory(dependent_service)
3550 ent = ent_cls(self.cfg)
3551
3552 is_service_enabled = (
3553diff --git a/uaclient/entitlements/cis.py b/uaclient/entitlements/cis.py
3554index 90d06b8..384bfd3 100644
3555--- a/uaclient/entitlements/cis.py
3556+++ b/uaclient/entitlements/cis.py
3557@@ -2,22 +2,47 @@ from typing import Callable, Dict, List, Tuple, Union
3558
3559 from uaclient.entitlements import repo
3560
3561-CIS_DOCS_URL = "https://security-certs.docs.ubuntu.com/en/cis"
3562+CIS_DOCS_URL = "https://ubuntu.com/security/cis"
3563+USG_DOCS_URL = "https://ubuntu.com/security/certifications/docs/usg"
3564
3565
3566 class CISEntitlement(repo.RepoEntitlement):
3567
3568- help_doc_url = "https://ubuntu.com/security/certifications#cis"
3569+ help_doc_url = USG_DOCS_URL
3570 name = "cis"
3571- title = "CIS Audit"
3572- description = "Center for Internet Security Audit Tools"
3573+ description = "Security compliance and audit tools"
3574 repo_key_file = "ubuntu-advantage-cis.gpg"
3575 apt_noninteractive = True
3576
3577 @property
3578 def messaging(self,) -> Dict[str, List[Union[str, Tuple[Callable, Dict]]]]:
3579- return {
3580+ if self._called_name == "usg":
3581+ return {
3582+ "post_enable": [
3583+ "Visit {} for the next steps".format(USG_DOCS_URL)
3584+ ]
3585+ }
3586+ messages = {
3587 "post_enable": [
3588 "Visit {} to learn how to use CIS".format(CIS_DOCS_URL)
3589 ]
3590- }
3591+ } # type: Dict[str, List[Union[str, Tuple[Callable, Dict]]]]
3592+ if "usg" in self.valid_names:
3593+ messages["pre_enable"] = [
3594+ "From Ubuntu 20.04 and onwards 'ua enable cis' has been",
3595+ "replaced by 'ua enable usg'. See more information at:",
3596+ USG_DOCS_URL,
3597+ ]
3598+ return messages
3599+
3600+ @property
3601+ def packages(self) -> List[str]:
3602+ if self._called_name == "usg":
3603+ return []
3604+ return super().packages
3605+
3606+ @property
3607+ def title(self) -> str:
3608+ if self._called_name == "cis":
3609+ return "CIS Audit"
3610+ return "Ubuntu Security Guide"
3611diff --git a/uaclient/entitlements/livepatch.py b/uaclient/entitlements/livepatch.py
3612index aec4ae0..f422f32 100644
3613--- a/uaclient/entitlements/livepatch.py
3614+++ b/uaclient/entitlements/livepatch.py
3615@@ -16,6 +16,8 @@ ERROR_MSG_MAP = {
3616 "unsupported kernel": "Your running kernel is not supported by Livepatch.",
3617 }
3618
3619+LIVEPATCH_CMD = "/snap/bin/canonical-livepatch"
3620+
3621
3622 def unconfigure_livepatch_proxy(
3623 protocol_type: str, retry_sleeps: Optional[List[float]] = None
3624@@ -29,10 +31,10 @@ def unconfigure_livepatch_proxy(
3625 on failure; sleeping half a second before the first retry and 1 second
3626 before the second retry.
3627 """
3628- if not util.which("/snap/bin/canonical-livepatch"):
3629+ if not util.which(LIVEPATCH_CMD):
3630 return
3631 util.subp(
3632- ["canonical-livepatch", "config", "{}-proxy=".format(protocol_type)],
3633+ [LIVEPATCH_CMD, "config", "{}-proxy=".format(protocol_type)],
3634 retry_sleeps=retry_sleeps,
3635 )
3636
3637@@ -61,21 +63,13 @@ def configure_livepatch_proxy(
3638
3639 if http_proxy:
3640 util.subp(
3641- [
3642- "canonical-livepatch",
3643- "config",
3644- "http-proxy={}".format(http_proxy),
3645- ],
3646+ [LIVEPATCH_CMD, "config", "http-proxy={}".format(http_proxy)],
3647 retry_sleeps=retry_sleeps,
3648 )
3649
3650 if https_proxy:
3651 util.subp(
3652- [
3653- "canonical-livepatch",
3654- "config",
3655- "https-proxy={}".format(https_proxy),
3656- ],
3657+ [LIVEPATCH_CMD, "config", "https-proxy={}".format(https_proxy)],
3658 retry_sleeps=retry_sleeps,
3659 )
3660
3661@@ -86,7 +80,7 @@ def get_config_option_value(key: str) -> Optional[str]:
3662 :param protocol: can be any valid livepatch config option
3663 :return: the value of the livepatch config option, or None if not set
3664 """
3665- out, _ = util.subp(["canonical-livepatch", "config"])
3666+ out, _ = util.subp([LIVEPATCH_CMD, "config"])
3667 match = re.search("^{}: (.*)$".format(key), out, re.MULTILINE)
3668 value = match.group(1) if match else None
3669 if value:
3670@@ -151,8 +145,8 @@ class LivepatchEntitlement(base.UAEntitlement):
3671 )
3672 elif "snapd" not in apt.get_installed_packages():
3673 raise exceptions.UserFacingError(
3674- "/usr/bin/snap is present but snapd is not installed;"
3675- " cannot enable {}".format(self.title)
3676+ "{} is present but snapd is not installed;"
3677+ " cannot enable {}".format(snap.SNAP_CMD, self.title)
3678 )
3679
3680 try:
3681@@ -177,7 +171,7 @@ class LivepatchEntitlement(base.UAEntitlement):
3682 retry_sleeps=snap.SNAP_INSTALL_RETRIES,
3683 )
3684
3685- if not util.which("/snap/bin/canonical-livepatch"):
3686+ if not util.which(LIVEPATCH_CMD):
3687 print("Installing canonical-livepatch snap")
3688 try:
3689 util.subp(
3690@@ -230,18 +224,13 @@ class LivepatchEntitlement(base.UAEntitlement):
3691 self.title,
3692 )
3693 try:
3694- util.subp(["/snap/bin/canonical-livepatch", "disable"])
3695+ util.subp([LIVEPATCH_CMD, "disable"])
3696 except util.ProcessExecutionError as e:
3697 logging.error(str(e))
3698 return False
3699 try:
3700 util.subp(
3701- [
3702- "/snap/bin/canonical-livepatch",
3703- "enable",
3704- livepatch_token,
3705- ],
3706- capture=True,
3707+ [LIVEPATCH_CMD, "enable", livepatch_token], capture=True
3708 )
3709 except util.ProcessExecutionError as e:
3710 msg = "Unable to enable Livepatch: "
3711@@ -261,15 +250,15 @@ class LivepatchEntitlement(base.UAEntitlement):
3712
3713 @return: True on success, False otherwise.
3714 """
3715- if not util.which("/snap/bin/canonical-livepatch"):
3716+ if not util.which(LIVEPATCH_CMD):
3717 return True
3718- util.subp(["/snap/bin/canonical-livepatch", "disable"], capture=True)
3719+ util.subp([LIVEPATCH_CMD, "disable"], capture=True)
3720 return True
3721
3722 def application_status(self) -> Tuple[ApplicationStatus, str]:
3723 status = (ApplicationStatus.ENABLED, "")
3724
3725- if not util.which("/snap/bin/canonical-livepatch"):
3726+ if not util.which(LIVEPATCH_CMD):
3727 return (
3728 ApplicationStatus.DISABLED,
3729 "canonical-livepatch snap is not installed.",
3730@@ -277,8 +266,7 @@ class LivepatchEntitlement(base.UAEntitlement):
3731
3732 try:
3733 util.subp(
3734- ["/snap/bin/canonical-livepatch", "status"],
3735- retry_sleeps=LIVEPATCH_RETRIES,
3736+ [LIVEPATCH_CMD, "status"], retry_sleeps=LIVEPATCH_RETRIES
3737 )
3738 except util.ProcessExecutionError as e:
3739 # TODO(May want to parse INACTIVE/failure assessment)
3740@@ -350,11 +338,7 @@ def process_config_directives(cfg):
3741 ca_certs = directives.get("caCerts")
3742 if ca_certs:
3743 util.subp(
3744- [
3745- "/snap/bin/canonical-livepatch",
3746- "config",
3747- "ca-certs={}".format(ca_certs),
3748- ],
3749+ [LIVEPATCH_CMD, "config", "ca-certs={}".format(ca_certs)],
3750 capture=True,
3751 )
3752 remote_server = directives.get("remoteServer", "")
3753@@ -363,7 +347,7 @@ def process_config_directives(cfg):
3754 if remote_server:
3755 util.subp(
3756 [
3757- "/snap/bin/canonical-livepatch",
3758+ LIVEPATCH_CMD,
3759 "config",
3760 "remote-server={}".format(remote_server),
3761 ],
3762diff --git a/uaclient/entitlements/repo.py b/uaclient/entitlements/repo.py
3763index 8c2e4d1..153ea09 100644
3764--- a/uaclient/entitlements/repo.py
3765+++ b/uaclient/entitlements/repo.py
3766@@ -120,10 +120,22 @@ class RepoEntitlement(base.UAEntitlement):
3767 :return: False if apt url is already found on the source file.
3768 True otherwise.
3769 """
3770+ apt_file = self.repo_list_file_tmpl.format(name=self.name)
3771+ # If the apt file is commented out, we will assume that we need
3772+ # to regenerate the apt file, regardless of the apt url delta
3773+ if all(
3774+ line.startswith("#")
3775+ for line in util.load_file(apt_file).strip().split("\n")
3776+ ):
3777+ return False
3778+
3779+ # If the file is not commented out and we don't have delta,
3780+ # we will not do anything
3781 if not apt_url:
3782 return True
3783
3784- apt_file = self.repo_list_file_tmpl.format(name=self.name)
3785+ # If the delta is already in the file, we won't reconfigure it
3786+ # again
3787 return bool(apt_url in util.load_file(apt_file))
3788
3789 def process_contract_deltas(
3790diff --git a/uaclient/entitlements/tests/conftest.py b/uaclient/entitlements/tests/conftest.py
3791index 054bc2b..03f56af 100644
3792--- a/uaclient/entitlements/tests/conftest.py
3793+++ b/uaclient/entitlements/tests/conftest.py
3794@@ -94,6 +94,7 @@ def entitlement_factory(tmpdir):
3795 obligations: Dict[str, Any] = None,
3796 entitled: bool = True,
3797 allow_beta: bool = False,
3798+ called_name: str = "",
3799 assume_yes: Optional[bool] = None,
3800 suites: List[str] = None,
3801 additional_packages: List[str] = None,
3802@@ -122,7 +123,7 @@ def entitlement_factory(tmpdir):
3803 if services_once_enabled:
3804 cfg.write_cache("services-once-enabled", services_once_enabled)
3805
3806- args = {"allow_beta": allow_beta}
3807+ args = {"allow_beta": allow_beta, "called_name": called_name}
3808 if assume_yes is not None:
3809 args["assume_yes"] = assume_yes
3810 return cls(cfg, **args)
3811diff --git a/uaclient/entitlements/tests/test_base.py b/uaclient/entitlements/tests/test_base.py
3812index f78f29a..76834d5 100644
3813--- a/uaclient/entitlements/tests/test_base.py
3814+++ b/uaclient/entitlements/tests/test_base.py
3815@@ -286,8 +286,6 @@ class TestUaEntitlement:
3816 def test_can_enable_when_incompatible_service_found(
3817 self, concrete_entitlement_factory
3818 ):
3819- import uaclient.entitlements as ent
3820-
3821 base_ent = concrete_entitlement_factory(
3822 entitled=True,
3823 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),
3824@@ -306,8 +304,9 @@ class TestUaEntitlement:
3825 with mock.patch.object(
3826 base_ent, "is_access_expired", return_value=False
3827 ):
3828- with mock.patch.object(
3829- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
3830+ with mock.patch(
3831+ "uaclient.entitlements.entitlement_factory",
3832+ return_value=m_entitlement_cls,
3833 ):
3834 ret, reason = base_ent.can_enable()
3835
3836@@ -320,8 +319,6 @@ class TestUaEntitlement:
3837 def test_can_enable_when_required_service_found(
3838 self, concrete_entitlement_factory
3839 ):
3840- import uaclient.entitlements as ent
3841-
3842 base_ent = concrete_entitlement_factory(
3843 entitled=True,
3844 applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""),
3845@@ -337,8 +334,9 @@ class TestUaEntitlement:
3846 ]
3847 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
3848
3849- with mock.patch.object(
3850- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
3851+ with mock.patch(
3852+ "uaclient.entitlements.entitlement_factory",
3853+ return_value=m_entitlement_cls,
3854 ):
3855 ret, reason = base_ent.can_enable()
3856
3857@@ -363,8 +361,6 @@ class TestUaEntitlement:
3858 assume_yes,
3859 concrete_entitlement_factory,
3860 ):
3861- import uaclient.entitlements as ent
3862-
3863 m_prompt.return_value = assume_yes
3864 m_is_config_value_true.return_value = block_disable_on_enable
3865 base_ent = concrete_entitlement_factory(
3866@@ -386,8 +382,9 @@ class TestUaEntitlement:
3867 with mock.patch.object(
3868 base_ent, "is_access_expired", return_value=False
3869 ):
3870- with mock.patch.object(
3871- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
3872+ with mock.patch(
3873+ "uaclient.entitlements.entitlement_factory",
3874+ return_value=m_entitlement_cls,
3875 ):
3876 ret, reason = base_ent.enable()
3877
3878@@ -416,8 +413,6 @@ class TestUaEntitlement:
3879 def test_enable_when_required_service_found(
3880 self, m_prompt, assume_yes, concrete_entitlement_factory
3881 ):
3882- import uaclient.entitlements as ent
3883-
3884 m_prompt.return_value = assume_yes
3885 base_ent = concrete_entitlement_factory(
3886 entitled=True,
3887@@ -436,8 +431,9 @@ class TestUaEntitlement:
3888 m_entitlement_obj.enable.return_value = (True, "")
3889 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
3890
3891- with mock.patch.object(
3892- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
3893+ with mock.patch(
3894+ "uaclient.entitlements.entitlement_factory",
3895+ return_value=m_entitlement_cls,
3896 ):
3897 ret, reason = base_ent.enable()
3898
3899@@ -678,8 +674,6 @@ class TestUaEntitlement:
3900 def test_disable_when_dependent_service_found(
3901 self, m_prompt, concrete_entitlement_factory
3902 ):
3903- import uaclient.entitlements as ent
3904-
3905 m_prompt.return_value = True
3906 base_ent = concrete_entitlement_factory(
3907 entitled=True,
3908@@ -697,8 +691,9 @@ class TestUaEntitlement:
3909 m_entitlement_obj.disable.return_value = True
3910 type(m_entitlement_obj).title = mock.PropertyMock(return_value="test")
3911
3912- with mock.patch.object(
3913- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
3914+ with mock.patch(
3915+ "uaclient.entitlements.entitlement_factory",
3916+ return_value=m_entitlement_cls,
3917 ):
3918 ret = base_ent.disable()
3919
3920@@ -710,6 +705,39 @@ class TestUaEntitlement:
3921 assert m_prompt.call_count == expected_prompt_call
3922 assert m_entitlement_obj.disable.call_count == expected_disable_call
3923
3924+ @pytest.mark.parametrize(
3925+ "p_name,expected",
3926+ (
3927+ ("pretty_name", ["testconcreteentitlement", "pretty_name"]),
3928+ ("testconcreteentitlement", ["testconcreteentitlement"]),
3929+ ),
3930+ )
3931+ @mock.patch(
3932+ "uaclient.entitlements.base.UAEntitlement.presentation_name",
3933+ new_callable=mock.PropertyMock,
3934+ )
3935+ def test_valid_names(
3936+ self, m_p_name, p_name, expected, concrete_entitlement_factory
3937+ ):
3938+ m_p_name.return_value = p_name
3939+ entitlement = concrete_entitlement_factory(entitled=True)
3940+ assert expected == entitlement.valid_names
3941+
3942+ def test_presentation_name(self, concrete_entitlement_factory):
3943+ entitlement = concrete_entitlement_factory(entitled=True)
3944+ assert "testconcreteentitlement" == entitlement.presentation_name
3945+ m_entitlements = {
3946+ "testconcreteentitlement": {
3947+ "entitlement": {
3948+ "affordances": {"presentedAs": "something_else"}
3949+ }
3950+ }
3951+ }
3952+ with mock.patch(
3953+ "uaclient.config.UAConfig.entitlements", m_entitlements
3954+ ):
3955+ assert "something_else" == entitlement.presentation_name
3956+
3957
3958 class TestUaEntitlementUserFacingStatus:
3959 def test_inapplicable_when_not_applicable(
3960diff --git a/uaclient/entitlements/tests/test_cis.py b/uaclient/entitlements/tests/test_cis.py
3961index 838c784..d6f1611 100644
3962--- a/uaclient/entitlements/tests/test_cis.py
3963+++ b/uaclient/entitlements/tests/test_cis.py
3964@@ -12,7 +12,10 @@ M_REPOPATH = "uaclient.entitlements.repo."
3965 @pytest.fixture
3966 def entitlement(entitlement_factory):
3967 return entitlement_factory(
3968- CISEntitlement, allow_beta=True, additional_packages=["pkg1"]
3969+ CISEntitlement,
3970+ allow_beta=True,
3971+ called_name="cis",
3972+ additional_packages=["pkg1"],
3973 )
3974
3975
3976diff --git a/uaclient/entitlements/tests/test_entitlements.py b/uaclient/entitlements/tests/test_entitlements.py
3977index a7210fe..06e0939 100644
3978--- a/uaclient/entitlements/tests/test_entitlements.py
3979+++ b/uaclient/entitlements/tests/test_entitlements.py
3980@@ -6,23 +6,54 @@ from uaclient import entitlements
3981
3982
3983 class TestValidServices:
3984+ @pytest.mark.parametrize("show_all_names", ((True), (False)))
3985 @pytest.mark.parametrize("allow_beta", ((True), (False)))
3986 @pytest.mark.parametrize("is_beta", ((True), (False)))
3987 @mock.patch("uaclient.entitlements.is_config_value_true")
3988- def test_valid_services(self, m_is_config_value, is_beta, allow_beta):
3989+ def test_valid_services(
3990+ self, m_is_config_value, show_all_names, allow_beta, is_beta
3991+ ):
3992 m_is_config_value.return_value = allow_beta
3993+
3994 m_cls_1 = mock.MagicMock()
3995- type(m_cls_1).is_beta = mock.PropertyMock(return_value=False)
3996+ m_inst_1 = mock.MagicMock()
3997+ m_cls_1.is_beta = False
3998+ m_inst_1.presentation_name = "ent1"
3999+ m_inst_1.valid_names = ["ent1", "othername"]
4000+ m_cls_1.return_value = m_inst_1
4001
4002 m_cls_2 = mock.MagicMock()
4003- type(m_cls_2).is_beta = mock.PropertyMock(return_value=is_beta)
4004- ents_dict = {"ent1": m_cls_1, "ent2": m_cls_2}
4005+ m_inst_2 = mock.MagicMock()
4006+ m_cls_2.is_beta = is_beta
4007+ m_inst_2.presentation_name = "ent2"
4008+ m_inst_2.valid_names = ["ent2"]
4009+ m_cls_2.return_value = m_inst_2
4010+
4011+ ents = {m_cls_1, m_cls_2}
4012
4013- with mock.patch.object(
4014- entitlements, "ENTITLEMENT_CLASS_BY_NAME", ents_dict
4015- ):
4016+ with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents):
4017 expected_services = ["ent1"]
4018 if allow_beta or not is_beta:
4019 expected_services.append("ent2")
4020+ if show_all_names:
4021+ expected_services.append("othername")
4022+
4023+ assert expected_services == entitlements.valid_services(
4024+ all_names=show_all_names
4025+ )
4026+
4027+
4028+class TestEntitlementFactory:
4029+ def test_entitlement_factory(self):
4030+ m_cls_1 = mock.MagicMock()
4031+ m_cls_1.return_value.valid_names = ["ent1", "othername"]
4032+
4033+ m_cls_2 = mock.MagicMock()
4034+ m_cls_2.return_value.valid_names = ["ent2"]
4035+
4036+ ents = {m_cls_1, m_cls_2}
4037
4038- assert expected_services == entitlements.valid_services()
4039+ with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents):
4040+ assert m_cls_1 == entitlements.entitlement_factory("othername")
4041+ assert m_cls_2 == entitlements.entitlement_factory("ent2")
4042+ assert None is entitlements.entitlement_factory("nonexistent")
4043diff --git a/uaclient/entitlements/tests/test_fips.py b/uaclient/entitlements/tests/test_fips.py
4044index 1a040bc..4d28026 100644
4045--- a/uaclient/entitlements/tests/test_fips.py
4046+++ b/uaclient/entitlements/tests/test_fips.py
4047@@ -145,7 +145,12 @@ class TestFIPSEntitlementCanEnable:
4048 "application_status",
4049 return_value=(status.ApplicationStatus.DISABLED, ""),
4050 ):
4051- assert (True, None) == entitlement.can_enable()
4052+ with mock.patch.object(
4053+ entitlement,
4054+ "detect_incompatible_services",
4055+ return_value=False,
4056+ ):
4057+ assert (True, None) == entitlement.can_enable()
4058 assert ("", "") == capsys.readouterr()
4059
4060
4061@@ -931,8 +936,9 @@ class TestFipsEntitlementInstallPackages:
4062 self, m_run_apt, entitlement
4063 ):
4064 m_run_apt.side_effect = exceptions.UserFacingError("error")
4065- with pytest.raises(exceptions.UserFacingError):
4066- entitlement.install_packages()
4067+ with mock.patch.object(entitlement, "_cleanup"):
4068+ with pytest.raises(exceptions.UserFacingError):
4069+ entitlement.install_packages()
4070
4071 @mock.patch(M_PATH + "apt.get_installed_packages")
4072 @mock.patch(M_PATH + "apt.run_apt_command")
4073diff --git a/uaclient/entitlements/tests/test_livepatch.py b/uaclient/entitlements/tests/test_livepatch.py
4074index 5d6dc3a..579aa70 100644
4075--- a/uaclient/entitlements/tests/test_livepatch.py
4076+++ b/uaclient/entitlements/tests/test_livepatch.py
4077@@ -12,6 +12,7 @@ import pytest
4078
4079 from uaclient import apt, exceptions, status
4080 from uaclient.entitlements.livepatch import (
4081+ LIVEPATCH_CMD,
4082 LivepatchEntitlement,
4083 configure_livepatch_proxy,
4084 get_config_option_value,
4085@@ -84,7 +85,7 @@ class TestConfigureLivepatchProxy:
4086 expected_calls.append(
4087 mock.call(
4088 [
4089- "canonical-livepatch",
4090+ LIVEPATCH_CMD,
4091 "config",
4092 "http-proxy={}".format(http_proxy),
4093 ],
4094@@ -96,7 +97,7 @@ class TestConfigureLivepatchProxy:
4095 expected_calls.append(
4096 mock.call(
4097 [
4098- "canonical-livepatch",
4099+ LIVEPATCH_CMD,
4100 "config",
4101 "https-proxy={}".format(https_proxy),
4102 ],
4103@@ -183,7 +184,7 @@ check-interval: 60 # minutes""",
4104 ret = get_config_option_value(key)
4105 assert ret == expected_ret
4106 assert [
4107- mock.call(["canonical-livepatch", "config"])
4108+ mock.call([LIVEPATCH_CMD, "config"])
4109 ] == m_util_subp.call_args_list
4110
4111
4112@@ -203,7 +204,7 @@ class TestUnconfigureLivepatchProxy:
4113 self, subp, which, livepatch_installed, protocol_type, retry_sleeps
4114 ):
4115 if livepatch_installed:
4116- which.return_value = "/snap/bin/canonical-livepatch"
4117+ which.return_value = LIVEPATCH_CMD
4118 else:
4119 which.return_value = None
4120 kwargs = {"protocol_type": protocol_type}
4121@@ -213,11 +214,7 @@ class TestUnconfigureLivepatchProxy:
4122 if livepatch_installed:
4123 expected_calls = [
4124 mock.call(
4125- [
4126- "canonical-livepatch",
4127- "config",
4128- protocol_type + "-proxy=",
4129- ],
4130+ [LIVEPATCH_CMD, "config", protocol_type + "-proxy="],
4131 retry_sleeps=retry_sleeps,
4132 )
4133 ]
4134@@ -291,7 +288,7 @@ class TestLivepatchProcessConfigDirectives:
4135 process_config_directives(cfg)
4136 expected_subp = mock.call(
4137 [
4138- "/snap/bin/canonical-livepatch",
4139+ LIVEPATCH_CMD,
4140 "config",
4141 livepatch_param_tmpl.format(directive_value),
4142 ],
4143@@ -314,16 +311,10 @@ class TestLivepatchProcessConfigDirectives:
4144 process_config_directives(cfg)
4145 expected_calls = [
4146 mock.call(
4147- ["/snap/bin/canonical-livepatch", "config", "ca-certs=value2"],
4148- capture=True,
4149+ [LIVEPATCH_CMD, "config", "ca-certs=value2"], capture=True
4150 ),
4151 mock.call(
4152- [
4153- "/snap/bin/canonical-livepatch",
4154- "config",
4155- "remote-server=value1",
4156- ],
4157- capture=True,
4158+ [LIVEPATCH_CMD, "config", "remote-server=value1"], capture=True
4159 ),
4160 ]
4161 assert expected_calls == m_subp.call_args_list
4162@@ -609,17 +600,14 @@ class TestLivepatchEntitlementEnable:
4163 mocks_config = [
4164 mock.call(
4165 [
4166- "/snap/bin/canonical-livepatch",
4167+ LIVEPATCH_CMD,
4168 "config",
4169 "remote-server=https://alt.livepatch.com",
4170 ],
4171 capture=True,
4172 ),
4173- mock.call(["/snap/bin/canonical-livepatch", "disable"]),
4174- mock.call(
4175- ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],
4176- capture=True,
4177- ),
4178+ mock.call([LIVEPATCH_CMD, "disable"]),
4179+ mock.call([LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True),
4180 ]
4181
4182 @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
4183@@ -677,10 +665,7 @@ class TestLivepatchEntitlementEnable:
4184 assert expected_log not in caplog_text()
4185 else:
4186 assert expected_log in caplog_text()
4187- expected_calls = [
4188- mock.call("/usr/bin/snap"),
4189- mock.call("/snap/bin/canonical-livepatch"),
4190- ]
4191+ expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)]
4192 assert expected_calls == m_which.call_args_list
4193 assert m_validate_proxy.call_count == 2
4194 assert m_snap_proxy.call_count == 1
4195@@ -723,10 +708,7 @@ class TestLivepatchEntitlementEnable:
4196 "Canonical livepatch enabled.\n"
4197 )
4198 assert (msg, "") == capsys.readouterr()
4199- expected_calls = [
4200- mock.call("/usr/bin/snap"),
4201- mock.call("/snap/bin/canonical-livepatch"),
4202- ]
4203+ expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)]
4204 assert expected_calls == m_which.call_args_list
4205 assert m_validate_proxy.call_count == 2
4206 assert m_snap_proxy.call_count == 1
4207@@ -801,16 +783,15 @@ class TestLivepatchEntitlementEnable:
4208 ),
4209 mock.call(
4210 [
4211- "/snap/bin/canonical-livepatch",
4212+ LIVEPATCH_CMD,
4213 "config",
4214 "remote-server=https://alt.livepatch.com",
4215 ],
4216 capture=True,
4217 ),
4218- mock.call(["/snap/bin/canonical-livepatch", "disable"]),
4219+ mock.call([LIVEPATCH_CMD, "disable"]),
4220 mock.call(
4221- ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],
4222- capture=True,
4223+ [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True
4224 ),
4225 ]
4226 assert subp_calls == m_subp.call_args_list
4227@@ -851,15 +832,14 @@ class TestLivepatchEntitlementEnable:
4228 ),
4229 mock.call(
4230 [
4231- "/snap/bin/canonical-livepatch",
4232+ LIVEPATCH_CMD,
4233 "config",
4234 "remote-server=https://alt.livepatch.com",
4235 ],
4236 capture=True,
4237 ),
4238 mock.call(
4239- ["/snap/bin/canonical-livepatch", "enable", "livepatch-token"],
4240- capture=True,
4241+ [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True
4242 ),
4243 ]
4244 assert subp_no_livepatch_disable == m_subp.call_args_list
4245diff --git a/uaclient/entitlements/tests/test_repo.py b/uaclient/entitlements/tests/test_repo.py
4246index 34aa979..233e3d2 100644
4247--- a/uaclient/entitlements/tests/test_repo.py
4248+++ b/uaclient/entitlements/tests/test_repo.py
4249@@ -980,6 +980,33 @@ class TestSetupAptConfig:
4250 ] == m_run_apt_command.call_args_list
4251
4252
4253+class TestCheckAptURLIsApplied:
4254+ @pytest.mark.parametrize("apt_url", (("test"), (None)))
4255+ @mock.patch("uaclient.util.load_file")
4256+ def test_check_apt_url_for_commented_apt_source_file(
4257+ self, m_load_file, apt_url, entitlement
4258+ ):
4259+ m_load_file.return_value = "#test1\n#test2\n"
4260+ assert not entitlement._check_apt_url_is_applied(apt_url)
4261+
4262+ @mock.patch("uaclient.util.load_file")
4263+ def test_check_apt_url_when_delta_apt_url_is_none(
4264+ self, m_load_file, entitlement
4265+ ):
4266+ m_load_file.return_value = "test1\n#test2\n"
4267+ assert entitlement._check_apt_url_is_applied(apt_url=None)
4268+
4269+ @pytest.mark.parametrize(
4270+ "apt_url,expected", (("test", True), ("blah", False))
4271+ )
4272+ @mock.patch("uaclient.util.load_file")
4273+ def test_check_apt_url_inspects_apt_source_file(
4274+ self, m_load_file, apt_url, expected, entitlement
4275+ ):
4276+ m_load_file.return_value = "test\n#test2\n"
4277+ assert expected == entitlement._check_apt_url_is_applied(apt_url)
4278+
4279+
4280 class TestApplicationStatus:
4281 # TODO: Write tests for all functionality
4282
4283diff --git a/uaclient/exceptions.py b/uaclient/exceptions.py
4284index 2599561..daf2b5e 100644
4285--- a/uaclient/exceptions.py
4286+++ b/uaclient/exceptions.py
4287@@ -1,3 +1,5 @@
4288+from typing import Optional
4289+
4290 from uaclient import status
4291
4292
4293@@ -35,10 +37,23 @@ class NonAutoAttachImageError(UserFacingError):
4294 exit_code = 0
4295
4296
4297+class AlreadyAttachedOnPROError(UserFacingError):
4298+ """Raised when a PRO machine retries attaching with the same instance-id"""
4299+
4300+ exit_code = 0
4301+
4302+ def __init__(self, instance_id: str):
4303+ super().__init__(
4304+ status.MESSAGE_ALREADY_ATTACHED_ON_PRO.format(
4305+ instance_id=instance_id
4306+ )
4307+ )
4308+
4309+
4310 class AlreadyAttachedError(UserFacingError):
4311 """An exception to be raised when a command needs an unattached system."""
4312
4313- exit_code = 0
4314+ exit_code = 2
4315
4316 def __init__(self, cfg):
4317 super().__init__(
4318@@ -102,3 +117,20 @@ class SecurityAPIMetadataError(UserFacingError):
4319 + "\n"
4320 + status.MESSAGE_SECURITY_ISSUE_NOT_RESOLVED.format(issue=issue_id)
4321 )
4322+
4323+
4324+class CloudFactoryError(Exception):
4325+ def __init__(self, cloud_type: Optional[str]) -> None:
4326+ self.cloud_type = cloud_type
4327+
4328+
4329+class CloudFactoryNoCloudError(CloudFactoryError):
4330+ pass
4331+
4332+
4333+class CloudFactoryUnsupportedCloudError(CloudFactoryError):
4334+ pass
4335+
4336+
4337+class CloudFactoryNonViableCloudError(CloudFactoryError):
4338+ pass
4339diff --git a/uaclient/jobs/update_messaging.py b/uaclient/jobs/update_messaging.py
4340index bfc185c..b63fa20 100644
4341--- a/uaclient/jobs/update_messaging.py
4342+++ b/uaclient/jobs/update_messaging.py
4343@@ -206,13 +206,13 @@ def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None:
4344 no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value
4345 msg_dir = os.path.join(cfg.data_dir, "messages")
4346
4347- apps_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-apps"]
4348+ apps_cls = entitlements.entitlement_factory("esm-apps")
4349 apps_inst = apps_cls(cfg)
4350 config_allow_beta = util.is_config_value_true(
4351 config=cfg.cfg, path_to_value="features.allow_beta"
4352 )
4353 apps_valid = bool(config_allow_beta or not apps_cls.is_beta)
4354- infra_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-infra"]
4355+ infra_cls = entitlements.entitlement_factory("esm-infra")
4356 infra_inst = infra_cls(cfg)
4357
4358 expiry_status, remaining_days = get_contract_expiry_status(cfg)
4359@@ -292,7 +292,7 @@ def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None:
4360 :param cfg: UAConfig instance for this environment.
4361 :param series: string of Ubuntu release series: 'xenial'.
4362 """
4363- apps_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-apps"]
4364+ apps_cls = entitlements.entitlement_factory("esm-apps")
4365 apps_inst = apps_cls(cfg)
4366 enabled_status = ApplicationStatus.ENABLED
4367 apps_not_enabled = apps_inst.application_status()[0] != enabled_status
4368diff --git a/uaclient/lock.py b/uaclient/lock.py
4369new file mode 100644
4370index 0000000..77ef76f
4371--- /dev/null
4372+++ b/uaclient/lock.py
4373@@ -0,0 +1,106 @@
4374+import functools
4375+import logging
4376+import os
4377+import time
4378+
4379+from uaclient import config, exceptions
4380+
4381+LOG = logging.getLogger("ua.lock")
4382+
4383+# Set a module-level callable here so we don't have to reinstantiate
4384+# UAConfig in order to determine dynamic data_path exception handling of
4385+# main_error_handler
4386+clear_lock_file = None
4387+
4388+
4389+def clear_lock_file_if_present():
4390+ global clear_lock_file
4391+ if clear_lock_file:
4392+ clear_lock_file()
4393+
4394+
4395+class SingleAttemptLock:
4396+ """
4397+ Context manager for gaining exclusive access to the lock file.
4398+ Create a lock file if absent. The lock file will contain a pid of the
4399+ running process, and a customer-visible description of the lock holder.
4400+
4401+ :param lock_holder: String with the service name or command which is
4402+ holding the lock. This lock_holder string will be customer visible in
4403+ status.json.
4404+ :raises: LockHeldError if lock is held.
4405+ """
4406+
4407+ def __init__(self, *_args, cfg: config.UAConfig, lock_holder: str):
4408+ self.cfg = cfg
4409+ self.lock_holder = lock_holder
4410+
4411+ def __enter__(self):
4412+ global clear_lock_file
4413+ (lock_pid, cur_lock_holder) = self.cfg.check_lock_info()
4414+ if lock_pid > 0:
4415+ raise exceptions.LockHeldError(
4416+ lock_request=self.lock_holder,
4417+ lock_holder=cur_lock_holder,
4418+ pid=lock_pid,
4419+ )
4420+ self.cfg.write_cache(
4421+ "lock", "{}:{}".format(os.getpid(), self.lock_holder)
4422+ )
4423+ notice_msg = "Operation in progress: {}".format(self.lock_holder)
4424+ self.cfg.add_notice("", notice_msg)
4425+ clear_lock_file = functools.partial(self.cfg.delete_cache_key, "lock")
4426+
4427+ def __exit__(self, _exc_type, _exc_value, _traceback):
4428+ global clear_lock_file
4429+ self.cfg.delete_cache_key("lock")
4430+ clear_lock_file = None # Unset due to successful lock delete
4431+
4432+
4433+class SpinLock(SingleAttemptLock):
4434+ """
4435+ Context manager for gaining exclusive access to the lock file. In contrast
4436+ to the SingleAttemptLock, the SpinLock will try several times to acquire
4437+ the lock before giving up. The number of times to try and how long to sleep
4438+ in between tries is configurable.
4439+
4440+ :param lock_holder: String with the service name or command which is
4441+ holding the lock. This lock_holder string will be customer visible in
4442+ status.json.
4443+ :param sleep_time: Number of seconds to sleep before retrying if the lock
4444+ is already held.
4445+ :param max_retries: Maximum number of times to try to grab the lock before
4446+ giving up and raising a LockHeldError.
4447+ :raises: LockHeldError if lock is held after (sleep_time * max_retries)
4448+ """
4449+
4450+ def __init__(
4451+ self,
4452+ *_args,
4453+ cfg: config.UAConfig,
4454+ lock_holder: str,
4455+ sleep_time: int = 10,
4456+ max_retries: int = 12
4457+ ):
4458+ super().__init__(cfg=cfg, lock_holder=lock_holder)
4459+ self.sleep_time = sleep_time
4460+ self.max_retries = max_retries
4461+
4462+ def __enter__(self):
4463+ LOG.debug("spin lock starting for {}".format(self.lock_holder))
4464+ tries = 0
4465+ while True:
4466+ try:
4467+ super().__enter__()
4468+ break
4469+ except exceptions.LockHeldError as e:
4470+ LOG.debug(
4471+ "SpinLock Attempt {}. {}. Spinning...".format(
4472+ tries + 1, e.msg
4473+ )
4474+ )
4475+ tries += 1
4476+ if tries >= self.max_retries:
4477+ raise e
4478+ else:
4479+ time.sleep(self.sleep_time)
4480diff --git a/uaclient/security.py b/uaclient/security.py
4481index 818f472..94c0e3a 100644
4482--- a/uaclient/security.py
4483+++ b/uaclient/security.py
4484@@ -16,7 +16,7 @@ from uaclient.clouds.identity import (
4485 )
4486 from uaclient.config import UAConfig
4487 from uaclient.defaults import BASE_UA_URL, PRINT_WRAP_WIDTH
4488-from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME
4489+from uaclient.entitlements import entitlement_factory
4490
4491 CVE_OR_USN_REGEX = (
4492 r"((CVE|cve)-\d{4}-\d{4,7}$|(USN|usn|LSN|lsn)-\d{1,5}-\d{1,2}$)"
4493@@ -763,7 +763,7 @@ def _get_service_for_pocket(pocket: str, cfg: UAConfig):
4494 elif pocket == UA_APPS_POCKET:
4495 service_to_check = "esm-apps"
4496
4497- ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(service_to_check)
4498+ ent_cls = entitlement_factory(service_to_check)
4499 return ent_cls(cfg) if ent_cls else None
4500
4501
4502diff --git a/uaclient/security_status.py b/uaclient/security_status.py
4503index 0f42704..d1e9484 100644
4504--- a/uaclient/security_status.py
4505+++ b/uaclient/security_status.py
4506@@ -73,13 +73,11 @@ def filter_security_updates(
4507 ]
4508
4509 return [
4510- package
4511+ version
4512 for package in packages
4513- if max(package.versions) > package.installed
4514- and any(
4515- origin.archive in security_repos
4516- for origin in max(package.versions).origins
4517- )
4518+ for version in package.versions
4519+ if version > package.installed
4520+ and any(origin.archive in security_repos for origin in version.origins)
4521 ]
4522
4523
4524@@ -122,20 +120,18 @@ def security_status(cfg: UAConfig) -> Dict[str, Any]:
4525 installed_packages = [package for package in cache if package.is_installed]
4526 summary["num_installed_packages"] = len(installed_packages)
4527
4528- security_upgradable_packages = filter_security_updates(installed_packages)
4529+ security_upgradable_versions = filter_security_updates(installed_packages)
4530
4531 package_count = {"esm-infra": 0, "esm-apps": 0, "standard-security": 0}
4532
4533- for package in security_upgradable_packages:
4534- candidate = max(package.versions)
4535- version = candidate.version
4536+ for candidate in security_upgradable_versions:
4537 service_name = get_service_name(candidate.origins)
4538 status = get_update_status(service_name, ua_info)
4539 package_count[service_name] += 1
4540 packages.append(
4541 {
4542- "package": package.name,
4543- "version": version,
4544+ "package": candidate.package.name,
4545+ "version": candidate.version,
4546 "service_name": service_name,
4547 "status": status,
4548 }
4549diff --git a/uaclient/serviceclient.py b/uaclient/serviceclient.py
4550index 48a1273..09703fe 100644
4551--- a/uaclient/serviceclient.py
4552+++ b/uaclient/serviceclient.py
4553@@ -68,11 +68,15 @@ class UAServiceClient(metaclass=abc.ABCMeta):
4554 timeout=self.url_timeout,
4555 )
4556 except error.URLError as e:
4557- if hasattr(e, "read"):
4558+ body = None
4559+ if hasattr(e, "body"):
4560+ body = e.body
4561+ elif hasattr(e, "read"):
4562+ body = e.read().decode("utf-8")
4563+ if body:
4564 try:
4565 error_details = json.loads(
4566- e.read().decode("utf-8"),
4567- cls=util.DatetimeAwareJSONDecoder,
4568+ body, cls=util.DatetimeAwareJSONDecoder
4569 )
4570 except ValueError:
4571 error_details = None
4572diff --git a/uaclient/status.py b/uaclient/status.py
4573index 5dbf65e..e537150 100644
4574--- a/uaclient/status.py
4575+++ b/uaclient/status.py
4576@@ -227,6 +227,8 @@ MESSAGE_ENABLED_TMPL = "{title} enabled"
4577 MESSAGE_ALREADY_ATTACHED = """\
4578 This machine is already attached to '{account_name}'
4579 To use a different subscription first run: sudo ua detach."""
4580+MESSAGE_ALREADY_ATTACHED_ON_PRO = """\
4581+Skipping attach: Instance '{instance_id}' is already attached."""
4582 MESSAGE_ALREADY_ENABLED_TMPL = """\
4583 {title} is already enabled.\nSee: sudo ua status"""
4584 MESSAGE_INAPPLICABLE_ARCH_TMPL = """\
4585@@ -346,6 +348,9 @@ Open a browser to: {}/subscribe""".format(
4586
4587 STATUS_UNATTACHED_TMPL = "{name: <14}{available: <11}{description}"
4588
4589+STATUS_SIMULATED_TMPL = """\
4590+{name: <14}{available: <11}{entitled: <11}{auto_enabled: <14}{description}"""
4591+
4592 STATUS_HEADER = "SERVICE ENTITLED STATUS DESCRIPTION"
4593 # The widths listed below for entitled and status are actually 9 characters
4594 # less than reality because we colorize the values in entitled and status
4595@@ -631,7 +636,21 @@ def get_section_column_content(
4596
4597 def format_tabular(status: Dict[str, Any]) -> str:
4598 """Format status dict for tabular output."""
4599- if not status["attached"]:
4600+ if not status.get("attached"):
4601+ if status.get("simulated"):
4602+ content = [
4603+ STATUS_SIMULATED_TMPL.format(
4604+ name="SERVICE",
4605+ available="AVAILABLE",
4606+ entitled="ENTITLED",
4607+ auto_enabled="AUTO_ENABLED",
4608+ description="DESCRIPTION",
4609+ )
4610+ ]
4611+ for service in status["services"]:
4612+ content.append(STATUS_SIMULATED_TMPL.format(**service))
4613+ return "\n".join(content)
4614+
4615 content = [
4616 STATUS_UNATTACHED_TMPL.format(
4617 name="SERVICE",
4618@@ -696,12 +715,13 @@ def _format_status_output(status: Dict[str, Any]) -> Dict[str, Any]:
4619 or name == "UA_CONFIG_FILE"
4620 ]
4621
4622- available_services = [
4623- service
4624- for service in status.get("services", [])
4625- if service.get("available", "yes") == "yes"
4626- ]
4627- status["services"] = available_services
4628+ if not status.get("simulated"):
4629+ available_services = [
4630+ service
4631+ for service in status.get("services", [])
4632+ if service.get("available", "yes") == "yes"
4633+ ]
4634+ status["services"] = available_services
4635
4636 # We don't need the origin info in the json output
4637 status.pop("origin", "")
4638diff --git a/uaclient/tests/test_actions.py b/uaclient/tests/test_actions.py
4639new file mode 100644
4640index 0000000..cc9a25f
4641--- /dev/null
4642+++ b/uaclient/tests/test_actions.py
4643@@ -0,0 +1,145 @@
4644+import mock
4645+import pytest
4646+
4647+from uaclient import exceptions, status, util
4648+from uaclient.actions import attach_with_token, auto_attach
4649+from uaclient.contract import ContractAPIError
4650+from uaclient.exceptions import NonAutoAttachImageError
4651+from uaclient.tests.test_cli_auto_attach import fake_instance_factory
4652+
4653+M_PATH = "uaclient.actions."
4654+
4655+
4656+class TestAttachWithToken:
4657+ @pytest.mark.parametrize(
4658+ "request_updated_contract_side_effect, expected_error_class,"
4659+ " expect_status_call",
4660+ [
4661+ (None, None, False),
4662+ (util.UrlError("cause"), util.UrlError, True),
4663+ (
4664+ exceptions.UserFacingError("test"),
4665+ exceptions.UserFacingError,
4666+ True,
4667+ ),
4668+ ],
4669+ )
4670+ @mock.patch(M_PATH + "config.update_ua_messages")
4671+ @mock.patch(M_PATH + "config.UAConfig.status")
4672+ @mock.patch(M_PATH + "contract.request_updated_contract")
4673+ def test_attach_with_token(
4674+ self,
4675+ m_request_updated_contract,
4676+ m_status,
4677+ m_update_ua_messages,
4678+ request_updated_contract_side_effect,
4679+ expected_error_class,
4680+ expect_status_call,
4681+ FakeConfig,
4682+ ):
4683+ cfg = FakeConfig()
4684+ m_request_updated_contract.side_effect = (
4685+ request_updated_contract_side_effect
4686+ )
4687+ if expected_error_class:
4688+ with pytest.raises(expected_error_class):
4689+ attach_with_token(cfg, "token", False)
4690+ else:
4691+ attach_with_token(cfg, "token", False)
4692+ if expect_status_call:
4693+ assert [mock.call()] == m_status.call_args_list
4694+ assert [mock.call(cfg)] == m_update_ua_messages.call_args_list
4695+
4696+
4697+class TestAutoAttach:
4698+ @mock.patch(M_PATH + "attach_with_token")
4699+ @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid")
4700+ @mock.patch(
4701+ M_PATH
4702+ + "contract.UAContractClient.request_auto_attach_contract_token",
4703+ return_value={"contractToken": "token"},
4704+ )
4705+ @mock.patch(M_PATH + "config.update_ua_messages")
4706+ @mock.patch(M_PATH + "config.UAConfig.write_cache")
4707+ def test_happy_path_on_auto_attach(
4708+ self,
4709+ m_write_cache,
4710+ m_update_ua_messages,
4711+ m_request_auto_attach_contract_token,
4712+ m_get_instance_id,
4713+ m_attach_with_token,
4714+ FakeConfig,
4715+ ):
4716+ cfg = FakeConfig()
4717+
4718+ auto_attach(cfg, fake_instance_factory())
4719+
4720+ assert [
4721+ mock.call(cfg, token="token", allow_enable=True)
4722+ ] == m_attach_with_token.call_args_list
4723+
4724+ assert [
4725+ mock.call("instance-id", "my-iid")
4726+ ] == m_write_cache.call_args_list
4727+
4728+ @pytest.mark.parametrize(
4729+ "http_msg,http_code,http_response",
4730+ (
4731+ ("Not found", 404, {"message": "missing instance information"}),
4732+ (
4733+ "Forbidden",
4734+ 403,
4735+ {"message": "forbidden: cannot verify signing certificate"},
4736+ ),
4737+ ),
4738+ )
4739+ @mock.patch(
4740+ M_PATH + "contract.UAContractClient.request_auto_attach_contract_token"
4741+ )
4742+ @mock.patch(M_PATH + "identity.get_instance_id", return_value="old-iid")
4743+ def test_handles_4XX_contract_errors(
4744+ self,
4745+ _m_get_instance_id,
4746+ m_request_auto_attach_contract_token,
4747+ http_msg,
4748+ http_code,
4749+ http_response,
4750+ FakeConfig,
4751+ ):
4752+ """VMs running on non-auto-attach images do not return a token."""
4753+ cfg = FakeConfig()
4754+ m_request_auto_attach_contract_token.side_effect = ContractAPIError(
4755+ util.UrlError(
4756+ http_msg, code=http_code, url="http://me", headers={}
4757+ ),
4758+ error_response=http_response,
4759+ )
4760+ with pytest.raises(NonAutoAttachImageError) as excinfo:
4761+ auto_attach(cfg, fake_instance_factory())
4762+ assert status.MESSAGE_UNSUPPORTED_AUTO_ATTACH == str(excinfo.value)
4763+
4764+ @mock.patch(
4765+ M_PATH + "contract.UAContractClient.request_auto_attach_contract_token"
4766+ )
4767+ @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid")
4768+ def test_raise_unexpected_errors(
4769+ self,
4770+ _m_get_instance_id,
4771+ m_request_auto_attach_contract_token,
4772+ FakeConfig,
4773+ ):
4774+ """Any unexpected errors will be raised."""
4775+ cfg = FakeConfig()
4776+
4777+ unexpected_error = ContractAPIError(
4778+ util.UrlError(
4779+ "Server error", code=500, url="http://me", headers={}
4780+ ),
4781+ error_response={"message": "something unexpected"},
4782+ )
4783+ m_request_auto_attach_contract_token.side_effect = unexpected_error
4784+
4785+ with pytest.raises(ContractAPIError) as excinfo:
4786+ auto_attach(cfg, fake_instance_factory())
4787+
4788+ assert unexpected_error == excinfo.value
4789diff --git a/uaclient/tests/test_apt.py b/uaclient/tests/test_apt.py
4790index b440b6e..2810d7e 100644
4791--- a/uaclient/tests/test_apt.py
4792+++ b/uaclient/tests/test_apt.py
4793@@ -559,7 +559,7 @@ class TestCleanAptFiles:
4794 repo_tmpl = tmpdir.join("source-{name}").strpath
4795 pref_tmpl = tmpdir.join("pref-{name}").strpath
4796
4797- class DummyRepo(request.param):
4798+ class TestRepo(request.param):
4799 name = entitlement_name
4800 repo_list_file_tmpl = repo_tmpl
4801 repo_pref_file_tmpl = pref_tmpl
4802@@ -575,7 +575,7 @@ class TestCleanAptFiles:
4803 with open(pref_name, "w") as f:
4804 f.write("")
4805
4806- return DummyRepo
4807+ return TestRepo
4808
4809 @mock.patch("uaclient.apt.os.unlink")
4810 def test_no_removals_for_no_repo_entitlements(self, m_os_unlink):
4811diff --git a/uaclient/tests/test_cli.py b/uaclient/tests/test_cli.py
4812index 3208603..a02d2fe 100644
4813--- a/uaclient/tests/test_cli.py
4814+++ b/uaclient/tests/test_cli.py
4815@@ -34,14 +34,25 @@ from uaclient.exceptions import (
4816 BIG_DESC = "123456789 " * 7 + "next line"
4817 BIG_URL = "http://" + "adsf" * 10
4818
4819+AVAILABLE_RESOURCES = [
4820+ {"name": "cc-eal"},
4821+ {"name": "cis"},
4822+ {"name": "esm-apps"},
4823+ {"name": "esm-infra"},
4824+ {"name": "fips-updates"},
4825+ {"name": "fips"},
4826+ {"name": "livepatch"},
4827+ {"name": "ros-updates"},
4828+ {"name": "ros"},
4829+]
4830
4831 ALL_SERVICES_WRAPPED_HELP = textwrap.dedent(
4832 """
4833 Client to manage Ubuntu Advantage services on a machine.
4834 - cc-eal: Common Criteria EAL2 Provisioning Packages
4835 (https://ubuntu.com/cc-eal)
4836- - cis: Center for Internet Security Audit Tools
4837- (https://ubuntu.com/security/certifications#cis)
4838+ - cis: Security compliance and audit tools
4839+ (https://ubuntu.com/security/certifications/docs/usg)
4840 - esm-apps: UA Apps: Extended Security Maintenance (ESM)
4841 (https://ubuntu.com/security/esm)
4842 - esm-infra: UA Infra: Extended Security Maintenance (ESM)
4843@@ -64,8 +75,8 @@ SERVICES_WRAPPED_HELP = textwrap.dedent(
4844 Client to manage Ubuntu Advantage services on a machine.
4845 - cc-eal: Common Criteria EAL2 Provisioning Packages
4846 (https://ubuntu.com/cc-eal)
4847- - cis: Center for Internet Security Audit Tools
4848- (https://ubuntu.com/security/certifications#cis)
4849+ - cis: Security compliance and audit tools
4850+ (https://ubuntu.com/security/certifications/docs/usg)
4851 - esm-infra: UA Infra: Extended Security Maintenance (ESM)
4852 (https://ubuntu.com/security/esm)
4853 - fips-updates: NIST-certified core packages with priority security updates
4854@@ -119,17 +130,21 @@ class TestCLIParser:
4855 maxDiff = None
4856
4857 @mock.patch("uaclient.cli.entitlements")
4858+ @mock.patch("uaclient.cli.contract")
4859 def test_help_descr_and_url_is_wrapped_at_eighty_chars(
4860- self, m_entitlements, get_help
4861+ self, m_contract, m_entitlements, get_help
4862 ):
4863 """Help lines are wrapped at 80 chars"""
4864
4865- def cls_mock_factory(desc, url):
4866- return mock.Mock(description=desc, help_doc_url=url, is_beta=False)
4867+ mocked_ent = mock.MagicMock(
4868+ presentation_name="test",
4869+ description=BIG_DESC,
4870+ help_doc_url=BIG_URL,
4871+ is_beta=False,
4872+ )
4873
4874- m_entitlements.ENTITLEMENT_CLASS_BY_NAME = {
4875- "test": cls_mock_factory(BIG_DESC, BIG_URL)
4876- }
4877+ m_entitlements.entitlement_factory.return_value = mocked_ent
4878+ m_contract.get_available_resources.return_value = [{"name": "test"}]
4879
4880 lines = [
4881 " - test: " + " ".join(["123456789"] * 7),
4882@@ -138,10 +153,13 @@ class TestCLIParser:
4883 out, _ = get_help()
4884 assert "\n".join(lines) in out
4885
4886- def test_help_sourced_dynamically_from_each_entitlement(self, get_help):
4887+ @mock.patch("uaclient.cli.contract")
4888+ def test_help_sourced_dynamically_from_each_entitlement(
4889+ self, m_contract, get_help
4890+ ):
4891 """Help output is sourced from entitlement name and description."""
4892+ m_contract.get_available_resources.return_value = AVAILABLE_RESOURCES
4893 out, type_request = get_help()
4894-
4895 if type_request == "base":
4896 assert SERVICES_WRAPPED_HELP in out
4897 else:
4898@@ -167,8 +185,6 @@ class TestCLIParser:
4899 self, m_attached, m_available_resources, out_format, expected_return
4900 ):
4901 """Test help command for a valid service in an unnatached ua client."""
4902- import uaclient.entitlements as ent
4903-
4904 m_args = mock.MagicMock()
4905 m_service_name = mock.PropertyMock(return_value="test")
4906 type(m_args).service = m_service_name
4907@@ -189,8 +205,9 @@ class TestCLIParser:
4908 ]
4909
4910 fake_stdout = io.StringIO()
4911- with mock.patch.object(
4912- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
4913+ with mock.patch(
4914+ "uaclient.entitlements.entitlement_factory",
4915+ return_value=m_entitlement_cls,
4916 ):
4917 with contextlib.redirect_stdout(fake_stdout):
4918 action_help(m_args, cfg=None)
4919@@ -222,8 +239,6 @@ class TestCLIParser:
4920 self, m_attached, m_available_resources, ent_status, ent_msg, is_beta
4921 ):
4922 """Test help command for a valid service in an attached ua client."""
4923- import uaclient.entitlements as ent
4924-
4925 m_args = mock.MagicMock()
4926 m_service_name = mock.PropertyMock(return_value="test")
4927 type(m_args).service = m_service_name
4928@@ -256,7 +271,7 @@ class TestCLIParser:
4929
4930 status_msg = "enabled" if ent_msg == "yes" else "—"
4931 ufs_call_count = 1 if ent_msg == "yes" else 0
4932- ent_name_call_count = 3 if ent_msg == "yes" else 2
4933+ ent_name_call_count = 2 if ent_msg == "yes" else 1
4934 is_beta_call_count = 1 if status_msg == "enabled" else 0
4935
4936 expected_msgs = [
4937@@ -275,8 +290,9 @@ class TestCLIParser:
4938 expected_msg = "\n\n".join(expected_msgs)
4939
4940 fake_stdout = io.StringIO()
4941- with mock.patch.object(
4942- ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls}
4943+ with mock.patch(
4944+ "uaclient.entitlements.entitlement_factory",
4945+ return_value=m_entitlement_cls,
4946 ):
4947 with contextlib.redirect_stdout(fake_stdout):
4948 action_help(m_args, cfg=None)
4949@@ -527,7 +543,7 @@ class TestMain:
4950 "exception,expected_exit_code",
4951 [
4952 (UserFacingError("You need to know about this."), 1),
4953- (AlreadyAttachedError(mock.MagicMock()), 0),
4954+ (AlreadyAttachedError(mock.MagicMock()), 2),
4955 (
4956 LockHeldError(
4957 pid="123",
4958@@ -653,7 +669,8 @@ class TestMain:
4959 assert "UA_FEATURES_WOW=XYZ" in log
4960 assert "NOT_UA_ENV=YES" not in log
4961
4962- def test_argparse_errors_well_formatted(self, capsys):
4963+ @mock.patch("uaclient.cli.contract.get_available_resources")
4964+ def test_argparse_errors_well_formatted(self, _m_resources, capsys):
4965 parser = get_parser()
4966 with mock.patch("sys.argv", ["ua", "enable"]):
4967 with pytest.raises(SystemExit) as excinfo:
4968@@ -834,14 +851,11 @@ class TestSetupLogging:
4969
4970
4971 class TestGetValidEntitlementNames:
4972- @mock.patch("uaclient.cli.entitlements")
4973- def test_get_valid_entitlements(self, m_entitlements):
4974- m_entitlements.ENTITLEMENT_CLASS_BY_NAME = {
4975- "ent1": True,
4976- "ent2": True,
4977- "ent3": True,
4978- }
4979-
4980+ @mock.patch(
4981+ "uaclient.cli.entitlements.valid_services",
4982+ return_value=["ent1", "ent2", "ent3"],
4983+ )
4984+ def test_get_valid_entitlements(self, _m_valid_services):
4985 service = ["ent1", "ent3", "ent4"]
4986 expected_ents_found = ["ent1", "ent3"]
4987 expected_ents_not_found = ["ent4"]
4988diff --git a/uaclient/tests/test_cli_attach.py b/uaclient/tests/test_cli_attach.py
4989index 0be920d..49f7282 100644
4990--- a/uaclient/tests/test_cli_attach.py
4991+++ b/uaclient/tests/test_cli_attach.py
4992@@ -237,26 +237,27 @@ class TestActionAttach:
4993 assert [mock.call(cfg)] == update_ua_messages.call_args_list
4994
4995
4996+@mock.patch(M_PATH + "contract.get_available_resources")
4997 class TestParser:
4998- def test_attach_parser_usage(self):
4999+ def test_attach_parser_usage(self, _m_resources):
5000 parser = attach_parser(mock.Mock())
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: