Merge ~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools:upload-27.5-jammy into ubuntu/+source/ubuntu-advantage-tools:ubuntu/devel
- Git
- lp:~renanrodrigo/ubuntu/+source/ubuntu-advantage-tools
- upload-27.5-jammy
- Merge into ubuntu/devel
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robie Basak | sru | Approve | |
Athos Ribeiro (community) | Approve | ||
Review via email: mp+413641@code.launchpad.net |
Commit message
Description of the change
This is the second release candidate for release 27.5 (LP: #1956456) of ubuntu-
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:/
Bionic: https:/
Focal: https:/
Hirsute: https:/
Impish: https:/
Athos Ribeiro (athos-ribeiro) wrote : | # |
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.
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 :)
Robie Basak (racb) wrote : | # |
Approved branch renanrodrigo/
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 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) |
38 | diff --git a/RELEASES.md b/RELEASES.md |
39 | index 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 | |
87 | diff --git a/debian/changelog b/debian/changelog |
88 | index 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: |
125 | diff --git a/debian/control b/debian/control |
126 | index 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 |
138 | diff --git a/debian/ubuntu-advantage-tools.postinst b/debian/ubuntu-advantage-tools.postinst |
139 | index 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) |
164 | diff --git a/debian/ubuntu-advantage-tools.postrm b/debian/ubuntu-advantage-tools.postrm |
165 | index 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 |
179 | diff --git a/features/_version.feature b/features/_version.feature |
180 | index 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 | |
240 | diff --git a/features/attach_invalidtoken.feature b/features/attach_invalidtoken.feature |
241 | index 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 | |
261 | diff --git a/features/attach_validtoken.feature b/features/attach_validtoken.feature |
262 | index 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 | |
455 | diff --git a/features/attached_commands.feature b/features/attached_commands.feature |
456 | index 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 |
627 | diff --git a/features/attached_enable.feature b/features/attached_enable.feature |
628 | index 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 |
881 | diff --git a/features/attached_status.feature b/features/attached_status.feature |
882 | index 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 | |
907 | diff --git a/features/install_uninstall.feature b/features/install_uninstall.feature |
908 | index 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 |
919 | diff --git a/features/license_check.feature b/features/license_check.feature |
920 | index 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 |
931 | diff --git a/features/steps/steps.py b/features/steps/steps.py |
932 | index 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, |
956 | diff --git a/features/ubuntu_pro.feature b/features/ubuntu_pro.feature |
957 | index 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 | |
1281 | diff --git a/features/ubuntu_upgrade.feature b/features/ubuntu_upgrade.feature |
1282 | index 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 | |
1330 | diff --git a/features/ubuntu_upgrade_unattached.feature b/features/ubuntu_upgrade_unattached.feature |
1331 | index 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 |
1343 | diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature |
1344 | index 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 | |
1427 | diff --git a/features/unattached_status.feature b/features/unattached_status.feature |
1428 | index 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 | | |
1621 | diff --git a/help_data.yaml b/help_data.yaml |
1622 | index 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: | |
1648 | diff --git a/lib/reboot_cmds.py b/lib/reboot_cmds.py |
1649 | index 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__": |
1725 | diff --git a/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh b/sru/release-27.4.2/test-postinst-check-service-is-enabled-fix.sh |
1726 | new file mode 100755 |
1727 | index 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 |
1765 | diff --git a/sru/release-27.4/test-unattached-status-job.sh b/sru/release-27.4/test-unattached-status-job.sh |
1766 | new file mode 100755 |
1767 | index 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 |
1811 | diff --git a/sru/release-27.5/test-aws-ipv6.sh b/sru/release-27.5/test-aws-ipv6.sh |
1812 | new file mode 100644 |
1813 | index 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" |
1883 | diff --git a/tools/build.sh b/tools/build.sh |
1884 | index 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 |
1891 | diff --git a/tools/create-lp-release-branches.sh b/tools/create-lp-release-branches.sh |
1892 | index 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 |
1912 | diff --git a/tools/run-integration-tests.py b/tools/run-integration-tests.py |
1913 | index 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 | |
1933 | diff --git a/tools/test-in-lxd.sh b/tools/test-in-lxd.sh |
1934 | index 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 |
1955 | diff --git a/tox.ini b/tox.ini |
1956 | index 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" |
1967 | diff --git a/uaclient/actions.py b/uaclient/actions.py |
1968 | new file mode 100644 |
1969 | index 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) |
2039 | diff --git a/uaclient/cli.py b/uaclient/cli.py |
2040 | index 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 | |
2523 | diff --git a/uaclient/clouds/aws.py b/uaclient/clouds/aws.py |
2524 | index 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 | |
2619 | diff --git a/uaclient/clouds/identity.py b/uaclient/clouds/identity.py |
2620 | index 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 |
2671 | diff --git a/uaclient/clouds/tests/test_aws.py b/uaclient/clouds/tests/test_aws.py |
2672 | index 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() |
2876 | diff --git a/uaclient/clouds/tests/test_identity.py b/uaclient/clouds/tests/test_identity.py |
2877 | index 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") |
2937 | diff --git a/uaclient/config.py b/uaclient/config.py |
2938 | index 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( |
3272 | diff --git a/uaclient/contract.py b/uaclient/contract.py |
3273 | index 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) |
3347 | diff --git a/uaclient/defaults.py b/uaclient/defaults.py |
3348 | index 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 | ) |
3359 | diff --git a/uaclient/entitlements/__init__.py b/uaclient/entitlements/__init__.py |
3360 | index 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 | ) |
3427 | diff --git a/uaclient/entitlements/base.py b/uaclient/entitlements/base.py |
3428 | index 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 = ( |
3553 | diff --git a/uaclient/entitlements/cis.py b/uaclient/entitlements/cis.py |
3554 | index 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" |
3611 | diff --git a/uaclient/entitlements/livepatch.py b/uaclient/entitlements/livepatch.py |
3612 | index 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 | ], |
3762 | diff --git a/uaclient/entitlements/repo.py b/uaclient/entitlements/repo.py |
3763 | index 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( |
3790 | diff --git a/uaclient/entitlements/tests/conftest.py b/uaclient/entitlements/tests/conftest.py |
3791 | index 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) |
3811 | diff --git a/uaclient/entitlements/tests/test_base.py b/uaclient/entitlements/tests/test_base.py |
3812 | index 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( |
3960 | diff --git a/uaclient/entitlements/tests/test_cis.py b/uaclient/entitlements/tests/test_cis.py |
3961 | index 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 | |
3976 | diff --git a/uaclient/entitlements/tests/test_entitlements.py b/uaclient/entitlements/tests/test_entitlements.py |
3977 | index 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") |
4043 | diff --git a/uaclient/entitlements/tests/test_fips.py b/uaclient/entitlements/tests/test_fips.py |
4044 | index 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") |
4073 | diff --git a/uaclient/entitlements/tests/test_livepatch.py b/uaclient/entitlements/tests/test_livepatch.py |
4074 | index 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 |
4245 | diff --git a/uaclient/entitlements/tests/test_repo.py b/uaclient/entitlements/tests/test_repo.py |
4246 | index 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 | |
4283 | diff --git a/uaclient/exceptions.py b/uaclient/exceptions.py |
4284 | index 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 |
4339 | diff --git a/uaclient/jobs/update_messaging.py b/uaclient/jobs/update_messaging.py |
4340 | index 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 |
4368 | diff --git a/uaclient/lock.py b/uaclient/lock.py |
4369 | new file mode 100644 |
4370 | index 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) |
4480 | diff --git a/uaclient/security.py b/uaclient/security.py |
4481 | index 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 | |
4502 | diff --git a/uaclient/security_status.py b/uaclient/security_status.py |
4503 | index 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 | } |
4549 | diff --git a/uaclient/serviceclient.py b/uaclient/serviceclient.py |
4550 | index 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 |
4572 | diff --git a/uaclient/status.py b/uaclient/status.py |
4573 | index 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", "") |
4638 | diff --git a/uaclient/tests/test_actions.py b/uaclient/tests/test_actions.py |
4639 | new file mode 100644 |
4640 | index 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 |
4789 | diff --git a/uaclient/tests/test_apt.py b/uaclient/tests/test_apt.py |
4790 | index 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): |
4811 | diff --git a/uaclient/tests/test_cli.py b/uaclient/tests/test_cli.py |
4812 | index 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"] |
4988 | diff --git a/uaclient/tests/test_cli_attach.py b/uaclient/tests/test_cli_attach.py |
4989 | index 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()) |
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.