diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/apt-hook/hook.cc ubuntu-advantage-tools-27.1~20.10.1/apt-hook/hook.cc --- ubuntu-advantage-tools-27.0.2~20.10.1/apt-hook/hook.cc 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/apt-hook/hook.cc 2021-05-27 19:05:18.000000000 +0000 @@ -151,7 +151,7 @@ { for (pkgCache::VerFileIterator pf = ver.FileList(); !pf.end(); pf++) { - if (pf.File().Archive() != 0 && pf.File().Origin() == std::string("UbuntuESM")) + if (pf.File().Archive() != 0 && DeNull(pf.File().Origin()) == std::string("UbuntuESM")) { if (std::find(res.esm_i_packages.begin(), res.esm_i_packages.end(), pkg.Name()) == res.esm_i_packages.end()) { res.esm_i_packages.push_back(pkg.Name()); @@ -167,7 +167,7 @@ } } } - if (pf.File().Archive() != 0 && pf.File().Origin() == std::string("UbuntuESMApps")) + if (pf.File().Archive() != 0 && DeNull(pf.File().Origin()) == std::string("UbuntuESMApps")) { if (std::find(res.esm_a_packages.begin(), res.esm_a_packages.end(), pkg.Name()) == res.esm_a_packages.end()) { res.esm_a_packages.push_back(pkg.Name()); @@ -265,6 +265,7 @@ std::string esm_i_pkgs ) { int bytes_written; + int length; std::array static_file_names = { APT_PRE_INVOKE_APPS_PKGS_STATIC_PATH, MOTD_APPS_PKGS_STATIC_PATH, @@ -334,10 +335,15 @@ for (uint i = 0; i < motd_static_files.size(); i++) { std::ifstream message_file(motd_static_files[i]); if (message_file.is_open()) { - if ( i > 0 ) { - motd_msg << std::endl; + message_file.seekg(0, message_file.end); + length = message_file.tellg(); + if ( length > 0 ) { + message_file.seekg(0, message_file.beg); + if ( motd_msg.tellp() > 0 ) { + motd_msg << std::endl; + } + motd_msg << message_file.rdbuf(); } - motd_msg << message_file.rdbuf(); message_file.close(); }; } diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/apt-hook/Makefile ubuntu-advantage-tools-27.1~20.10.1/apt-hook/Makefile --- ubuntu-advantage-tools-27.0.2~20.10.1/apt-hook/Makefile 2021-05-11 10:20:05.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/apt-hook/Makefile 2021-05-27 19:05:18.000000000 +0000 @@ -27,7 +27,7 @@ $(CXX) -Wall -Wextra -pedantic -std=c++11 $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -g -o hook hook.cc -lapt-pkg $(LDLIBS) json-hook: - [ $(SKIP_GO_HOOK) ] || (cd json-hook-src && GOCACHE=/tmp/ $(GO_BIN) build json-hook.go) + [ $(SKIP_GO_HOOK) ] || (cd json-hook-src && GOCACHE=/tmp/ $(GO_BIN) build -buildmode=pie -ldflags -extldflags=-z,relro json-hook.go) install: hook json-hook install -D -m 644 20apt-esm-hook.conf $(DESTDIR)/etc/apt/apt.conf.d/20apt-esm-hook.conf diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/debian/changelog ubuntu-advantage-tools-27.1~20.10.1/debian/changelog --- ubuntu-advantage-tools-27.0.2~20.10.1/debian/changelog 2021-05-12 16:10:45.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/debian/changelog 2021-05-27 19:05:18.000000000 +0000 @@ -1,8 +1,62 @@ -ubuntu-advantage-tools (27.0.2~20.10.1) groovy; urgency=medium +ubuntu-advantage-tools (27.1~20.10.1) groovy; urgency=medium - * Backport to Groovy + * Backport new upstream release: (LP: #1929597) to groovy - -- Chad Smith Wed, 12 May 2021 10:10:45 -0600 + -- Lucas Moura Thu, 27 May 2021 16:05:18 -0300 + +ubuntu-advantage-tools (27.1~21.10.1) impish; urgency=medium + + * d/control: + - specify debianutils min version + * d/changelog: + - fix lintian typos amend and redact incorrect 27.0 entry (GH: #1624) + * lintian: + - override ubuntu-advantage-pro wanted-by-target cloud-init + - override xenial specific errors + - rename package-specific overrides for pro vs tools + * New upstream release 27.1: + - apt-hook: + + avoid segfault when comparing null Apt file origin to esm + (LP: #1929123) + + avoid wrapping static message formats at 80 chars + + update go build flags based on lintian warnings (GH: #1626) + + only add newlines for MOTD if message file length is non-zero + - attach: do not print contract name if empty + - autocomplete: Do not show beta services in autocomplete (GH: #1594) + - cis: + + make service non-beta + + post enable message pointing to docs + + update cis help url + - docs: update releases.md per SRU review feedback on branch structuring + - enable: correct messaging for beta service (GH: #1588) + - errors: print a more helpful message when ssl fails (GH: #1618) + - fips: + + Block enabling fips if fips-updates once enabled (GH: #1600) + + Update output of fips commands (GH: #1631) + - livepatch: alert when snapd does not have wait cmd (LP: #1927329) + - logging: remove tracebacks for UserFacingErrors (GH: #1586) + - messaging: + + Infra and Apps messaging is mutually exclusive (GH: #1573) + + point to u.com/16-04 instead of u.com/advantage on ESM (GH: #1584) + + separate _remove_msg_template. emit no warranty on infra disabled + - pro: obtain AWS IMDSv2 API token before trying to grab pkcs7 doc + (GH: #1608) + - status: do not show info if not on contract (GH: #1592) + - tests: + + drop trusty specific tests + + fix mock for handle_message_operations + + fix motd message for bionic (GH: #1615) + + integration tests for hirsute and groovy + + manual test for trusty upgrade to xenial + + reboot after dist-upgrade for upgrade test + + test enabling CIS on focal (GH: #1582) + + update messages in integration tests (GH: #1635) + + use proposed pocket on xenial upgrade test + - jenkins: + + add pytest runs for xenial and bionic + + run focal lxd integration tests + + -- Grant Orndorff Mon, 24 May 2021 14:50:47 -0400 ubuntu-advantage-tools (27.0.2) impish; urgency=medium diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/debian/control ubuntu-advantage-tools-27.1~20.10.1/debian/control --- ubuntu-advantage-tools-27.0.2~20.10.1/debian/control 2021-05-11 10:20:05.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/debian/control 2021-05-27 19:05:18.000000000 +0000 @@ -4,7 +4,7 @@ Maintainer: Ubuntu Developers Build-Depends: bash-completion, debhelper (>=9), - debianutils, + debianutils (>= 4.7), dh-python, debhelper (>= 13.3) | dh-systemd, gettext, diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/debian/lintian-overrides ubuntu-advantage-tools-27.1~20.10.1/debian/lintian-overrides --- ubuntu-advantage-tools-27.0.2~20.10.1/debian/lintian-overrides 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/debian/lintian-overrides 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -# False positive -ubuntu-advantage-tools: maintainer-script-calls-service postinst:* - -# Ubuntu doesn't require init.d scripts -ubuntu-advantage-tools: package-supports-alternative-init-but-no-init.d-script diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/debian/source/lintian-overrides ubuntu-advantage-tools-27.1~20.10.1/debian/source/lintian-overrides --- ubuntu-advantage-tools-27.0.2~20.10.1/debian/source/lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/debian/source/lintian-overrides 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,6 @@ +# Lintian doesn't see dh-systemd alternative when building on xenial +ubuntu-advantage-tools: missing-build-dependency-for-dh_-command dh_systemd_start => dh-systemd +ubuntu-advantage-tools: missing-build-dependency-for-dh-addon systemd => dh-systemd + +# Lintian doesn't like mentioning riscv64 for older go package +ubuntu-advantage-tools: invalid-arch-string-in-source-relation riscv64 [build-depends: golang-1.10-go [!powerpc !riscv64]] diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/debian/ubuntu-advantage-pro.lintian-overrides ubuntu-advantage-tools-27.1~20.10.1/debian/ubuntu-advantage-pro.lintian-overrides --- ubuntu-advantage-tools-27.0.2~20.10.1/debian/ubuntu-advantage-pro.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/debian/ubuntu-advantage-pro.lintian-overrides 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,3 @@ +# Avoid warning on wanted-by-target + +ubuntu-advantage-pro: systemd-service-file-refers-to-unusual-wantedby-target diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/attached_commands.feature ubuntu-advantage-tools-27.1~20.10.1/features/attached_commands.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/attached_commands.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/attached_commands.feature 2021-05-27 19:05:18.000000000 +0000 @@ -20,8 +20,9 @@ | release | | bionic | | focal | - | trusty | | xenial | + | hirsute | + | groovy | @series.all Scenario Outline: Attached disable of an already disabled service in a ubuntu machine @@ -43,10 +44,11 @@ | release | | bionic | | focal | - | trusty | | xenial | + | hirsute | + | groovy | - @series.all + @series.lts Scenario Outline: Attached disable of a service in a ubuntu machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo @@ -84,8 +86,31 @@ | focal | | xenial | - @series.all - Scenario Outline: Attached detach in a trusty machine + @series.groovy + @series.hirsute + Scenario Outline: Attached detach in an ubuntu machine + Given a `` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + And I run `ua status --all` as non-root + Then stdout matches regexp: + """ + SERVICE ENTITLED STATUS DESCRIPTION + cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages + cis +yes +n/a +Center for Internet Security Audit Tools + esm-apps +no +— +UA Apps: Extended Security Maintenance \(ESM\) + esm-infra +yes +n/a +UA Infra: Extended Security Maintenance \(ESM\) + fips +yes +n/a +NIST-certified core packages + fips-updates +yes +n/a +NIST-certified core packages with priority security updates + livepatch +yes +n/a +Canonical Livepatch service + """ + + Examples: ubuntu release + | release | + | groovy | + | hirsute | + + @series.lts + Scenario Outline: Attached detach in an ubuntu machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo Then I verify that running `ua detach` `as non-root` exits `1` @@ -109,8 +134,8 @@ cis + +Center for Internet Security Audit Tools esm-apps + +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +UA Infra: Extended Security Maintenance \(ESM\) - fips + +NIST-certified FIPS modules - fips-updates + +Uncertified security updates to FIPS modules + fips + +NIST-certified core packages + fips-updates + +NIST-certified core packages with priority security updates livepatch +yes +Canonical Livepatch service """ And stdout matches regexp: @@ -122,8 +147,7 @@ Examples: ubuntu release | release | esm-apps | cc-eal | cis | fips | fips-update | | bionic | yes | no | yes | yes | yes | - | focal | yes | no | no | no | no | - | trusty | no | no | no | no | no | + | focal | yes | no | yes | no | no | | xenial | yes | yes | yes | yes | yes | @series.all @@ -145,8 +169,9 @@ | release | | bionic | | focal | - | trusty | | xenial | + | hirsute | + | groovy | @series.all Scenario Outline: Attached show version in a ubuntu machine @@ -165,8 +190,9 @@ | release | | bionic | | focal | - | trusty | | xenial | + | hirsute | + | groovy | @series.all Scenario Outline: Unattached status in a ubuntu machine with feature overrides @@ -214,10 +240,11 @@ | release | | bionic | | focal | - | trusty | | xenial | + | hirsute | + | groovy | - @series.all + @series.lts Scenario Outline: Attached disable of different services in a ubuntu machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo @@ -248,50 +275,8 @@ | release | | bionic | | focal | - | trusty | | xenial | - @series.trusty - Scenario: Attached disable of an already enabled service in a trusty machine - Given a `trusty` machine with ubuntu-advantage-tools installed - When I attach `contract_token` with sudo - Then I verify that running `ua disable foobar` `as non-root` exits `1` - And stderr matches regexp: - """ - This command must be run as root \(try using sudo\). - """ - And I verify that running `ua disable foobar` `with sudo` exits `1` - And stderr matches regexp: - """ - Cannot disable unknown service 'foobar'. - Try cc-eal, cis, esm-apps, esm-infra, fips, fips-updates, livepatch. - """ - And I verify that running `ua disable esm-infra` `as non-root` exits `1` - And stderr matches regexp: - """ - This command must be run as root \(try using sudo\) - """ - When I run `ua disable esm-infra` with sudo - Then I will see the following on stdout: - """ - Updating package lists - """ - When I run `ua status` with sudo - Then stdout matches regexp: - """ - esm-infra +yes +disabled +UA Infra: Extended Security Maintenance \(ESM\) - """ - And I verify that running `apt update` `with sudo` exits `0` - When I run `apt-cache policy` with sudo - Then apt-cache policy for the following url has permission `-32768` - """ - https://esm.ubuntu.com/ubuntu/ trusty-infra-security/main amd64 Packages - """ - And apt-cache policy for the following url has permission `-32768` - """ - https://esm.ubuntu.com/ubuntu/ trusty-infra-updates/main amd64 Packages - """ - @series.all Scenario Outline: Help command on an attached machine Given a `` machine with ubuntu-advantage-tools installed @@ -306,7 +291,7 @@ yes Status: - enabled + Help: esm-infra provides access to a private ppa which includes available high @@ -320,7 +305,7 @@ When I run `ua help esm-infra --format json` with sudo Then I will see the following on stdout: """ - {"name": "esm-infra", "entitled": "yes", "status": "enabled", "help": "esm-infra provides access to a private ppa which includes available high\nand critical CVE fixes for Ubuntu LTS packages in the Ubuntu Main\nrepository between the end of the standard Ubuntu LTS security\nmaintenance and its end of life. It is enabled by default with\nExtended Security Maintenance (ESM) for UA Apps and UA Infra.\nYou can find our more about the esm service at\nhttps://ubuntu.com/security/esm\n"} + {"name": "esm-infra", "entitled": "yes", "status": "", "help": "esm-infra provides access to a private ppa which includes available high\nand critical CVE fixes for Ubuntu LTS packages in the Ubuntu Main\nrepository between the end of the standard Ubuntu LTS security\nmaintenance and its end of life. It is enabled by default with\nExtended Security Maintenance (ESM) for UA Apps and UA Infra.\nYou can find our more about the esm service at\nhttps://ubuntu.com/security/esm\n"} """ And I verify that running `ua help invalid-service` `with sudo` exits `1` And I will see the following on stderr: @@ -331,11 +316,13 @@ Then stdout matches regexp: """ Client to manage Ubuntu Advantage services on a machine. + - cis: Center for Internet Security Audit Tools + \(https://ubuntu.com/security/certifications#cis\) - esm-infra: UA Infra: Extended Security Maintenance \(ESM\) \(https://ubuntu.com/security/esm\) - - fips-updates: Uncertified security updates to FIPS modules + - fips-updates: NIST-certified core packages with priority security updates \(https://ubuntu.com/security/certifications#fips\) - - fips: NIST-certified FIPS modules + - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) @@ -344,11 +331,13 @@ Then stdout matches regexp: """ Client to manage Ubuntu Advantage services on a machine. + - cis: Center for Internet Security Audit Tools + \(https://ubuntu.com/security/certifications#cis\) - esm-infra: UA Infra: Extended Security Maintenance \(ESM\) \(https://ubuntu.com/security/esm\) - - fips-updates: Uncertified security updates to FIPS modules + - fips-updates: NIST-certified core packages with priority security updates \(https://ubuntu.com/security/certifications#fips\) - - fips: NIST-certified FIPS modules + - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) @@ -365,22 +354,23 @@ \(https://ubuntu.com/security/esm\) - esm-infra: UA Infra: Extended Security Maintenance \(ESM\) \(https://ubuntu.com/security/esm\) - - fips-updates: Uncertified security updates to FIPS modules + - fips-updates: NIST-certified core packages with priority security updates \(https://ubuntu.com/security/certifications#fips\) - - fips: NIST-certified FIPS modules + - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) """ Examples: ubuntu release - | release | - | bionic | - | focal | - | trusty | - | xenial | + | release | infra-status | + | bionic | enabled | + | focal | enabled | + | xenial | enabled | + | hirsute | n/a | + | groovy | n/a | - @series.all + @series.lts Scenario Outline: Purge package after attaching it to a machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo @@ -407,15 +397,14 @@ | release | | bionic | | focal | - | trusty | | xenial | - @series.all + @series.lts Scenario Outline: Enable command with invalid repositories in user machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo And I run `ua disable esm-infra` with sudo - And I run `add-apt-repository ppa:ua-client/staging -y` with sudo, retrying exit [1] + And I run `add-apt-repository ppa:cloud-init-dev/daily -y` with sudo, retrying exit [1] And I run `apt update` with sudo And I run `sed -i 's/ubuntu/ubun/' /etc/apt/sources.list.d/.list` with sudo And I run `ua enable esm-infra` with sudo @@ -425,12 +414,11 @@ Updating package lists APT update failed. APT update failed to read APT config for the following URL: - - http://ppa.launchpad.net/ua-client/staging/ubun + - http://ppa.launchpad.net/cloud-init-dev/daily/ubun """ Examples: ubuntu release - | release | ppa_file | - | trusty | ua-client-staging-trusty | - | xenial | ua-client-ubuntu-staging-xenial | - | bionic | ua-client-ubuntu-staging-bionic | - | focal | ua-client-ubuntu-staging-focal | + | release | ppa_file | + | xenial | cloud-init-dev-ubuntu-daily-xenial | + | bionic | cloud-init-dev-ubuntu-daily-bionic | + | focal | cloud-init-dev-ubuntu-daily-focal | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/attached_enable.feature ubuntu-advantage-tools-27.1~20.10.1/features/attached_enable.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/attached_enable.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/attached_enable.feature 2021-05-27 19:05:18.000000000 +0000 @@ -24,16 +24,17 @@ And stderr matches regexp: """ Cannot enable unknown service 'cc-eal'. - Try esm-infra, fips, fips-updates, livepatch. + Try cis, esm-infra, fips, fips-updates, livepatch. """ Examples: ubuntu release | release | msg | | bionic | CC EAL2 is not available for Ubuntu 18.04 LTS (Bionic Beaver). | | focal | CC EAL2 is not available for Ubuntu 20.04 LTS (Focal Fossa). | - | trusty | CC EAL2 is not available for Ubuntu 14.04 LTS (Trusty Tahr). | + | groovy | CC EAL2 is not available for Ubuntu 20.10 (Groovy Gorilla). | + | hirsute | CC EAL2 is not available for Ubuntu 21.04 (Hirsute Hippo). | - @series.all + @series.lts Scenario Outline: Attached enable of a service in a ubuntu machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo @@ -50,7 +51,7 @@ And stderr matches regexp: """ Cannot enable unknown service 'foobar'. - Try esm-infra, fips, fips-updates, livepatch. + Try cis, esm-infra, fips, fips-updates, livepatch. """ And I verify that running `ua enable cc-eal foobar` `with sudo` exits `1` And I will see the following on stdout: @@ -60,7 +61,7 @@ And stderr matches regexp: """ Cannot enable unknown service 'foobar, cc-eal'. - Try esm-infra, fips, fips-updates, livepatch. + Try cis, esm-infra, fips, fips-updates, livepatch. """ And I verify that running `ua enable esm-infra` `with sudo` exits `1` Then I will see the following on stdout: @@ -87,7 +88,6 @@ | release | infra-pkg | esm-infra-url | | bionic | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | | focal | hello | https://esm.ubuntu.com/infra/ubuntu | - | trusty | libgit2-0 | https://esm.ubuntu.com/ubuntu/ | | xenial | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | @series.all @@ -123,25 +123,19 @@ | release | | bionic | | focal | - | trusty | | xenial | + | groovy | + | hirsute | - @series.all + @series.lts Scenario Outline: Attached enable not entitled service in a ubuntu machine Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo - Then I verify that running `ua enable ` `as non-root` exits `1` + Then I verify that running `ua enable esm-apps` `as non-root` exits `1` And I will see the following on stderr: """ This command must be run as root (try using sudo). """ - And I verify that running `ua enable cis --beta` `with sudo` exits `1` - And I will see the following on stdout: - """ - One moment, checking your subscription first - This subscription is not entitled to CIS Audit - For more information see: https://ubuntu.com/advantage. - """ And I verify that running `ua enable esm-apps --beta` `with sudo` exits `1` And I will see the following on stdout: """ @@ -154,9 +148,82 @@ | release | | bionic | | focal | - | trusty | | xenial | + @series.lts + Scenario Outline: Attached enable of cis service in a ubuntu machine + Given a `` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + And I verify that running `ua enable cis` `with sudo` exits `0` + Then I will see the following on stdout: + """ + One moment, checking your subscription first + Updating package lists + Installing CIS Audit packages + CIS Audit enabled + Visit https://security-certs.docs.ubuntu.com/en/cis to learn how to use CIS + """ + When I run `apt-cache policy usg-cisbenchmark` as non-root + Then stdout does not match regexp: + """ + .*Installed: \(none\) + """ + And stdout matches regexp: + """ + \s* 500 https://esm.ubuntu.com/cis/ubuntu /main amd64 Packages + """ + When I run `apt-cache policy usg-common` as non-root + Then stdout does not match regexp: + """ + .*Installed: \(none\) + """ + And stdout matches regexp: + """ + \s* 500 https://esm.ubuntu.com/cis/ubuntu /main amd64 Packages + """ + When I verify that running `ua enable cis` `with sudo` exits `1` + Then stdout matches regexp + """ + One moment, checking your subscription first + CIS Audit is already enabled. + See: sudo ua status + """ + When I run `cis-audit level1_server` with sudo + Then stdout matches regexp + """ + Title.*Ensure no duplicate UIDs exist + Rule.*xccdf_com.ubuntu..cis_rule_CIS-.* + Result.*pass + """ + And stdout matches regexp: + """ + Title.*Ensure default user umask is 027 or more restrictive + Rule.*xccdf_com.ubuntu..cis_rule_CIS-.* + Result.*fail + """ + And stdout matches regexp + """ + CIS audit scan completed + """ + When I verify that running `/usr/share/ubuntu-scap-security-guides/cis-hardening/ lvl1_server` `with sudo` exits `0` + And I run `cis-audit level1_server` with sudo + Then stdout matches regexp: + """ + Title.*Ensure default user umask is 027 or more restrictive + Rule.*xccdf_com.ubuntu..cis_rule_CIS-.* + Result.*pass + """ + And stdout matches regexp + """ + CIS audit scan completed + """ + + Examples: not entitled services + | release | cis_script | + | focal | Canonical_Ubuntu_20.04_CIS-harden.sh | + | bionic | Canonical_Ubuntu_18.04_CIS-harden.sh | + | xenial | Canonical_Ubuntu_16.04_CIS_v1.1.0-harden.sh | + @series.focal @uses.config.machine_type.lxd.vm Scenario: Attached enable of vm-based services in a focal lxd vm @@ -184,9 +251,10 @@ And I run `ua status` with sudo Then stdout matches regexp: """ + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +disabled +NIST-certified FIPS modules - fips-updates +yes +disabled +Uncertified security updates to FIPS modules + fips +yes +disabled +NIST-certified core packages + fips-updates +yes +disabled +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `ua disable livepatch` with sudo @@ -199,9 +267,10 @@ When I run `ua status` with sudo Then stdout matches regexp: """ + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +disabled +NIST-certified FIPS modules - fips-updates +yes +disabled +Uncertified security updates to FIPS modules + fips +yes +disabled +NIST-certified core packages + fips-updates +yes +disabled +NIST-certified core packages with priority security updates livepatch +yes +disabled +Canonical Livepatch service """ diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/attach_invalidtoken.feature ubuntu-advantage-tools-27.1~20.10.1/features/attach_invalidtoken.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/attach_invalidtoken.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/attach_invalidtoken.feature 2021-05-27 19:05:18.000000000 +0000 @@ -16,7 +16,8 @@ """ Examples: ubuntu release | release | - | trusty | | xenial | | bionic | | focal | + | groovy | + | hirsute | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/attach_validtoken.feature ubuntu-advantage-tools-27.1~20.10.1/features/attach_validtoken.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/attach_validtoken.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/attach_validtoken.feature 2021-05-27 19:05:18.000000000 +0000 @@ -11,15 +11,19 @@ When I run `apt-get update` with sudo, retrying exit [100] And I run `apt-get install -y ` with sudo, retrying exit [100] And I run `run-parts /etc/update-motd.d/` with sudo - Then if `` in `xenial or bionic` and stdout matches regexp: + Then if `` in `xenial` and stdout matches regexp: """ \d+ package(s)? can be updated. \d+ of these updates (is a|are) security update(s)?. """ + Then if `` in `bionic` and stdout matches regexp: + """ + \d+ update(s)? can be applied immediately. + \d+ of these updates (is a|are) standard security update(s)?. + """ Then if `` in `focal` and stdout matches regexp: """ - \d+ update(s)? can be installed immediately. - \d+ of these updates (is a|are) security update(s)?. + \d+ update(s)? can be applied immediately. """ When I attach `contract_token` with sudo Then stdout matches regexp: @@ -33,9 +37,10 @@ And stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +n/a +NIST-certified FIPS modules - fips-updates +yes +n/a +Uncertified security updates to FIPS modules + fips +yes +n/a +NIST-certified core packages + fips-updates +yes +n/a +NIST-certified core packages with priority security updates livepatch +yes +n/a +Canonical Livepatch service """ And stderr matches regexp: @@ -59,14 +64,12 @@ +https:\/\/ubuntu.com\/esm - UA (Infra:|Infrastructure) Extended Security Maintenance \(ESM\) is enabled. - - \d+ update(s)? can be installed immediately. - \d+ of these updates (is|are) (fixed|provided) through UA (Infra:|Infrastructure) ESM. - \d+ of these updates (is a|are) security update(s)?. + \d+ update(s)? can be applied immediately. + \d+ of these updates (is|are) (a|an)? UA Infra: ESM security update(s)?. + \d+ of these updates (is a|are) standard security update(s)?. To see these additional updates run: apt list --upgradable """ - Then if `` in `xenial or bionic` and stdout matches regexp: + Then if `` in `bionic` and stdout matches regexp: """ \* Introducing Extended Security Maintenance for Applications. +Receive updates to over 30,000 software packages with your @@ -74,23 +77,36 @@ +https:\/\/ubuntu.com\/esm + \d+ update(s)? can be applied immediately. + \d+ of these updates (is a|are) standard security update(s)?. + To see these additional updates run: apt list --upgradable + """ + Then if `` in `xenial` and stdout matches regexp: + """ + \* Introducing Extended Security Maintenance for Applications. + +Receive updates to over 30,000 software packages with your + +Ubuntu Advantage subscription. Free for personal use. + + +https:\/\/ubuntu.com\/16-04 + UA Infra: Extended Security Maintenance \(ESM\) is enabled. \d+ package(s)? can be updated. + \d+ of these updates (is|are) fixed through UA Infra: ESM. \d+ of these updates (is a|are) security update(s)?. To see these additional updates run: apt list --upgradable """ When I update contract to use `effectiveTo` as `days=-20` And I run `python3 /usr/lib/ubuntu-advantage/ua_update_messaging.py` with sudo And I run `update-motd` with sudo - Then if `` in `xenial or bionic` and stdout matches regexp: + Then if `` in `xenial` and stdout matches regexp: """ \*Your UA Infra: ESM subscription has EXPIRED\* \d+ additional security update\(s\) could have been applied via UA Infra: ESM. - Renew your UA services at https:\/\/ubuntu.com\/esm + Renew your UA services at https:\/\/ubuntu.com\/advantage """ Then if `` in `xenial` and stdout matches regexp: @@ -104,16 +120,14 @@ Then if `` in `xenial` and stdout matches regexp: """ \*Your UA Infra: ESM subscription has EXPIRED\* - Enabling UA Infra: ESM service would provide security updates for following - packages: - libkrad0 - 1 esm-infra security update\(s\) NOT APPLIED. Renew your UA services at + Enabling UA Infra: ESM service would provide security updates for following packages: + .* + \d+ esm-infra security update\(s\) NOT APPLIED. Renew your UA services at https:\/\/ubuntu.com\/advantage """ Examples: ubuntu release packages | release | downrev_pkg | - | trusty | libgit2-0=0.19.0-2 | | xenial | libkrad0=1.13.2+dfsg-5 | | bionic | libkrad0=1.16-2build1 | | focal | hello=2.10-2ubuntu2 | @@ -134,9 +148,10 @@ And stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes + + """ And stderr matches regexp: @@ -146,7 +161,6 @@ Examples: ubuntu release livepatch status | release | fips_status |lp_status | lp_desc | - | trusty | n/a |n/a | Available with the HWE kernel | | xenial | disabled |enabled | Canonical Livepatch service | | bionic | disabled |enabled | Canonical Livepatch service | | focal | n/a |enabled | Canonical Livepatch service | @@ -167,9 +181,10 @@ And stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes + +Canonical Livepatch service """ And stderr matches regexp: @@ -179,7 +194,6 @@ Examples: ubuntu release livepatch status | release | lp_status | fips_status | - | trusty | disabled | n/a | | xenial | n/a | n/a | | bionic | n/a | disabled | | focal | n/a | n/a | @@ -200,9 +214,10 @@ And stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes +disabled +Center for Internet Security Audit Tools esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes + +Canonical Livepatch service """ And stderr matches regexp: @@ -212,7 +227,6 @@ Examples: ubuntu release livepatch status | release | lp_status | fips_status | - | trusty | disabled | n/a | | xenial | n/a | n/a | | bionic | n/a | n/a | | focal | n/a | n/a | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/aws-ids.yaml ubuntu-advantage-tools-27.1~20.10.1/features/aws-ids.yaml --- ubuntu-advantage-tools-27.0.2~20.10.1/features/aws-ids.yaml 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/aws-ids.yaml 2021-05-27 19:05:18.000000000 +0000 @@ -1,4 +1,3 @@ bionic: ami-0e11aa8c6a6d58146 focal: ami-0383ece2c0f6de239 -trusty: ami-0a64b4493bc9dc321 xenial: ami-09e110a448d322f4a diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/azure-ids.yaml ubuntu-advantage-tools-27.1~20.10.1/features/azure-ids.yaml --- ubuntu-advantage-tools-27.0.2~20.10.1/features/azure-ids.yaml 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/azure-ids.yaml 2021-05-27 19:05:18.000000000 +0000 @@ -1,4 +1,3 @@ bionic: "Canonical:0001-com-ubuntu-pro-bionic:pro-18_04-lts" focal: "Canonical:0001-com-ubuntu-pro-focal:pro-20_04-lts" -trusty: "Canonical:0001-com-ubuntu-pro-trusty:pro-14_04-lts" xenial: "Canonical:0001-com-ubuntu-pro-xenial:pro-16_04-lts" diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/cloud.py ubuntu-advantage-tools-27.1~20.10.1/features/cloud.py --- ubuntu-advantage-tools-27.0.2~20.10.1/features/cloud.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/cloud.py 2021-05-27 19:05:18.000000000 +0000 @@ -152,9 +152,7 @@ except Exception as e: print("--- Retrying instance.wait on {}".format(str(e))) - if series != "trusty": - self._check_cloudinit_status(inst) - + self._check_cloudinit_status(inst) return inst def get_instance_id( diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/environment.py ubuntu-advantage-tools-27.1~20.10.1/features/environment.py --- ubuntu-advantage-tools-27.0.2~20.10.1/features/environment.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/environment.py 2021-05-27 19:05:18.000000000 +0000 @@ -20,7 +20,7 @@ from features.util import emit_spinner_on_travis, lxc_get_property, build_debs -ALL_SUPPORTED_SERIES = ["bionic", "focal", "trusty", "xenial"] +ALL_SUPPORTED_SERIES = ["bionic", "focal", "xenial"] DAILY_PPA = "http://ppa.launchpad.net/ua-client/daily/ubuntu" DAILY_PPA_KEYID = "6E34E7116C0BC933" @@ -33,20 +33,28 @@ - chmod 755 /usr/bin/ua """ -USERDATA_APT_SOURCE_PPA_TRUSTY = """\ -apt_sources: # for trusty - - source: deb {ppa_url} $RELEASE main - keyid: {ppa_keyid} -packages: [{packages}] """ +Package: * +Pin: release o=LP-PPA-ua-client-daily +Pin-Priority: 1001 +""" +UACLIENT_PPA_PIN = ( + "UGFja2FnZTogKgpQaW46IHJlbGVhc2Ugbz1MUC1QUE" + "EtdWEtY2xpZW50LWRhaWx5ClBpbi1Qcmlvcml0eTogMTAwMQo=" +) USERDATA_APT_SOURCE_PPA = """\ +write_files: + - encoding: b64 + content: {preference_pin} + owner: root:root + path: /etc/apt/preferences.d/uaclient + permissions: 0644 apt: sources: ua-tools-ppa: source: deb {ppa_url} $RELEASE main keyid: {ppa_keyid} -packages: [{packages}] """ PROCESS_LOG_TMPL = """\ @@ -128,6 +136,7 @@ "artifact_dir", "ppa", "ppa_keyid", + "userdata_file", ] redact_options = [ "aws_access_key_id", @@ -170,6 +179,7 @@ artifact_dir: str = None, ppa: str = DAILY_PPA, ppa_keyid: str = DAILY_PPA_KEYID, + userdata_file: str = None, cmdline_tags: "List" = [] ) -> None: # First, store the values we've detected @@ -195,6 +205,7 @@ self.artifact_dir = artifact_dir self.ppa = ppa self.ppa_keyid = ppa_keyid + self.userdata_file = userdata_file self.filter_series = set( [ tag.split(".")[1] @@ -658,8 +669,13 @@ ) user_data = "" + ud_file = context.config.userdata_file + if ud_file and os.path.exists(ud_file): + with open(ud_file, "r") as stream: + user_data = stream.read() if "pro" in context.config.machine_type: - user_data = USERDATA_BLOCK_AUTO_ATTACH_IMG + if not user_data: + user_data = USERDATA_BLOCK_AUTO_ATTACH_IMG if not deb_paths: if not user_data: user_data = "#cloud-config\n" @@ -667,24 +683,10 @@ ppa_keyid = context.config.ppa_keyid if context.config.ppa.startswith("ppa:"): ppa = ppa.replace("ppa:", "http://ppa.launchpad.net/") + "/ubuntu" - if series == "trusty": - packages = ["ubuntu-advantage-tools"] - - if "pro" in context.config.machine_type: - packages.append("ubuntu-advantage-pro") - - user_data += USERDATA_APT_SOURCE_PPA_TRUSTY.format( - ppa_url=ppa, ppa_keyid=ppa_keyid, packages=", ".join(packages) - ) - else: - packages = ["openssh-server", "ubuntu-advantage-tools"] - if "pro" in context.config.machine_type: - packages.append("ubuntu-advantage-pro") - - user_data += USERDATA_APT_SOURCE_PPA.format( - ppa_url=ppa, ppa_keyid=ppa_keyid, packages=", ".join(packages) - ) + user_data += USERDATA_APT_SOURCE_PPA.format( + ppa_url=ppa, ppa_keyid=ppa_keyid, preference_pin=UACLIENT_PPA_PIN + ) inst = context.config.cloud_manager.launch( instance_name=build_container_name, series=series, user_data=user_data ) @@ -694,6 +696,7 @@ build_container_name, series=series, config=context.config, + machine_type=context.config.machine_type, deb_paths=deb_paths, ) @@ -710,6 +713,7 @@ container_name: str, series: str, config: UAClientBehaveConfig, + machine_type: str, deb_paths: "Optional[List[str]]" = None, ) -> None: """Install ubuntu-advantage-tools into the specified container @@ -740,10 +744,18 @@ deb_files.append("/tmp/" + deb_name) inst.push_file(deb_file, "/tmp/" + deb_name) - if series == "trusty": - cmds.append(["sudo", "dpkg", "-i"] + deb_files) - else: - cmds.append(["sudo", "apt-get", "install", "-y"] + deb_files) + cmds.append( + ["sudo", "apt-get", "install", "-y", "--allow-downgrades"] + + deb_files + ) + else: + ua_pkg = ["ubuntu-advantage-tools"] + if "pro" in machine_type: + ua_pkg.append("ubuntu-advantage-pro") + + cmds.append( + ["sudo", "apt-get", "install", "-y", "--allow-downgrades"] + ua_pkg + ) if "pro" in config.machine_type: features = "features:\n disable_auto_attach: true\n" diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/staging_commands.feature ubuntu-advantage-tools-27.1~20.10.1/features/staging_commands.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/staging_commands.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/staging_commands.feature 2021-05-27 19:05:18.000000000 +0000 @@ -94,12 +94,18 @@ esm-infra(-no)? \d+.* """ + When I verify that running `ua enable esm-apps` `with sudo` exits `1` + Then stdout matches regexp + """ + One moment, checking your subscription first + UA Apps: ESM is already enabled. + See: sudo ua status + """ Examples: ubuntu release | release | apps-pkg | | bionic | bundler | | focal | ant | - | trusty | ant | | xenial | jq | @series.xenial @@ -260,6 +266,12 @@ """ +yes disabled """ + When I verify that running `ua enable fips --assume-yes` `with sudo` exits `1` + Then stdout matches regexp: + """ + Cannot enable FIPS because FIPS Updates was once enabled. + """ + And I verify that files exist matching `/var/lib/ubuntu-advantage/services-once-enabled` Examples: ubuntu release | release | fips-name | fips-service |fips-apt-source | @@ -340,43 +352,6 @@ @series.xenial @series.bionic - Scenario Outline: Attached enable of cis service in a ubuntu machine - Given a `` machine with ubuntu-advantage-tools installed - When I attach `contract_token_staging` with sudo - And I verify that running `ua enable cis --beta` `with sudo` exits `0` - Then I will see the following on stdout: - """ - One moment, checking your subscription first - Updating package lists - Installing CIS Audit packages - CIS Audit enabled - """ - When I run `apt-cache policy usg-cisbenchmark` as non-root - Then stdout does not match regexp: - """ - .*Installed: \(none\) - """ - And stdout matches regexp: - """ - \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu /main amd64 Packages - """ - When I run `apt-cache policy usg-common` as non-root - Then stdout does not match regexp: - """ - .*Installed: \(none\) - """ - And stdout matches regexp: - """ - \s* 500 https://esm.staging.ubuntu.com/cis/ubuntu /main amd64 Packages - """ - - Examples: not entitled services - | release | - | bionic | - | xenial | - - @series.xenial - @series.bionic @uses.config.machine_type.lxd.vm Scenario Outline: Attached enable fips-updates on fips enabled vm Given a `` machine with ubuntu-advantage-tools installed diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/steps/steps.py ubuntu-advantage-tools-27.1~20.10.1/features/steps/steps.py --- ubuntu-advantage-tools-27.0.2~20.10.1/features/steps/steps.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/steps/steps.py 2021-05-27 19:05:18.000000000 +0000 @@ -74,16 +74,6 @@ context.instance ) - if series == "trusty": - # On trusty, the distro-info package is not directly - # installed when we install the ubuntu-advantage-client - # deb. This is fixing that problem on trusty - when_i_run_command( - context=context, - command="apt-get install -f -y", - user_spec="with sudo", - ) - def cleanup_instance() -> None: if not context.config.destroy_instances: print( @@ -264,18 +254,7 @@ @when("I reboot the `{series}` machine") def when_i_reboot_the_machine(context, series): - if series == "trusty": - # TODO(LP: #1899299: LTS upgrade T->X pickled ds breaks Paths.run_dir) - # When Fix is SRUd to Xenial, we can drop the trusty clause - logging.warning( - "LP: #1899299: Not raising cloud-init-errors across Trusty reboot" - ) - context.instance.shutdown(wait=True) - context.instance.start(wait=False) - # Trusty -> Xenial upgrades would raise a Paths no run_dir attr failure - context.instance.wait() - else: - context.instance.restart(wait=True) + context.instance.restart(wait=True) @then("I will see the following on stdout") diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/ubuntu_pro.feature ubuntu-advantage-tools-27.1~20.10.1/features/ubuntu_pro.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/ubuntu_pro.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/ubuntu_pro.feature 2021-05-27 19:05:18.000000000 +0000 @@ -19,10 +19,11 @@ Then stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `ua status --all` as non-root @@ -33,8 +34,8 @@ cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `apt-cache policy` with sudo @@ -89,7 +90,7 @@ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | | xenial | disabled | disabled | disabled | libkrad0 | jq | | bionic | disabled | n/a | disabled | libkrad0 | bundler | - | focal | n/a | n/a | n/a | hello | ant | + | focal | n/a | n/a | disabled | hello | ant | @series.xenial @series.bionic @@ -110,10 +111,11 @@ Then stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `ua status --all` as non-root @@ -124,8 +126,8 @@ cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `apt-cache policy` with sudo @@ -180,7 +182,7 @@ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | | xenial | n/a | disabled | disabled | libkrad0 | jq | | bionic | disabled | n/a | disabled | libkrad0 | bundler | - | focal | n/a | n/a | n/a | hello | ant | + | focal | n/a | n/a | disabled | hello | ant | @series.xenial @series.bionic @@ -201,10 +203,11 @@ Then stdout matches regexp: """ SERVICE ENTITLED STATUS DESCRIPTION + cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes + +NIST-certified FIPS modules - fips-updates +yes + +Uncertified security updates to FIPS modules + fips +yes + +NIST-certified core packages + fips-updates +yes + +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `ua status --all` as non-root @@ -215,8 +218,8 @@ cis +yes + +Center for Internet Security Audit Tools esm-apps +yes +enabled +UA Apps: Extended Security Maintenance \(ESM\) esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +n/a +NIST-certified FIPS modules - fips-updates +yes +n/a +Uncertified security updates to FIPS modules + fips +yes +n/a +NIST-certified core packages + fips-updates +yes +n/a +NIST-certified core packages with priority security updates livepatch +yes +enabled +Canonical Livepatch service """ When I run `apt-cache policy` with sudo @@ -271,114 +274,4 @@ | release | fips-s | cc-eal-s | cis-s | infra-pkg | apps-pkg | | xenial | n/a | disabled | disabled | libkrad0 | jq | | bionic | n/a | n/a | disabled | libkrad0 | bundler | - | focal | n/a | n/a | n/a | hello | ant | - - @series.trusty - @uses.config.machine_type.aws.pro - Scenario Outline: Attached refresh in a Trusty Ubuntu PRO machine - Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: - """ - contract_url: 'https://contracts.canonical.com' - data_dir: /var/lib/ubuntu-advantage - log_level: debug - log_file: /var/log/ubuntu-advantage.log - """ - And I run `apt-cache policy` with sudo - Then apt-cache policy for the following url has permission `-32768` - """ - https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - When I run `ua auto-attach` with sudo - And I run `ua status --all` as non-root - Then stdout matches regexp: - """ - SERVICE ENTITLED STATUS DESCRIPTION - cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages - cis +yes +n/a +Center for Internet Security Audit Tools - esm-apps +yes +n/a +UA Apps: Extended Security Maintenance \(ESM\) - esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +n/a +NIST-certified FIPS modules - fips-updates +yes +n/a +Uncertified security updates to FIPS modules - livepatch +yes +n/a +Available with the HWE kernel - """ - When I run `apt-cache policy` with sudo - Then apt-cache policy for the following url has permission `500` - """ - https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - And apt-cache policy for the following url has permission `500` - """ - https://esm.ubuntu.com/ubuntu/ -infra-security/main amd64 Packages - """ - And I verify that running `apt update` `with sudo` exits `0` - When I run `apt install -y /-infra-security` with sudo, retrying exit [100] - And I run `apt-cache policy ` as non-root - Then stdout matches regexp: - """ - \s*500 https://esm.ubuntu.com/ubuntu/ -infra-security/main amd64 Packages - \s*500 https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - And stdout matches regexp: - """ - Installed: .*[~+]esm - """ - - Examples: ubuntu release - | release | infra-pkg | - | trusty | libgit2-dbg | - - @series.trusty - @uses.config.machine_type.azure.pro - Scenario Outline: Attached refresh in a Trusty Ubuntu PRO machine - Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: - """ - contract_url: 'https://contracts.canonical.com' - data_dir: /var/lib/ubuntu-advantage - log_level: debug - log_file: /var/log/ubuntu-advantage.log - """ - And I run `apt-cache policy` with sudo - Then apt-cache policy for the following url has permission `-32768` - """ - https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - When I run `ua auto-attach` with sudo - And I run `ua status --all` as non-root - Then stdout matches regexp: - """ - SERVICE ENTITLED STATUS DESCRIPTION - cc-eal +yes +n/a +Common Criteria EAL2 Provisioning Packages - cis +yes +n/a +Center for Internet Security Audit Tools - esm-apps +yes +n/a +UA Apps: Extended Security Maintenance \(ESM\) - esm-infra +yes +enabled +UA Infra: Extended Security Maintenance \(ESM\) - fips +yes +n/a +NIST-certified FIPS modules - fips-updates +yes +n/a +Uncertified security updates to FIPS modules - livepatch +yes +disabled +Canonical Livepatch service - """ - When I run `apt-cache policy` with sudo - Then apt-cache policy for the following url has permission `500` - """ - https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - And apt-cache policy for the following url has permission `500` - """ - https://esm.ubuntu.com/ubuntu/ -infra-security/main amd64 Packages - """ - And I verify that running `apt update` `with sudo` exits `0` - When I run `apt install -y /-infra-security` with sudo, retrying exit [100] - And I run `apt-cache policy ` as non-root - Then stdout matches regexp: - """ - \s*500 https://esm.ubuntu.com/ubuntu/ -infra-security/main amd64 Packages - \s*500 https://esm.ubuntu.com/ubuntu/ -infra-updates/main amd64 Packages - """ - And stdout matches regexp: - """ - Installed: .*[~+]esm - """ - - Examples: ubuntu release - | release | infra-pkg | - | trusty | libgit2-dbg | + | focal | n/a | n/a | disabled | hello | ant | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/ubuntu_upgrade.feature ubuntu-advantage-tools-27.1~20.10.1/features/ubuntu_upgrade.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/ubuntu_upgrade.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/ubuntu_upgrade.feature 2021-05-27 19:05:18.000000000 +0000 @@ -34,7 +34,6 @@ | release | next_release | devel_release | | focal | groovy | --devel-release | - @series.trusty @series.xenial @series.bionic @upgrade @@ -42,6 +41,8 @@ Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo And I run `apt-get dist-upgrade --assume-yes` with sudo + # Some packages upgrade may require a reboot + And I reboot the `` machine And I create the file `/etc/update-manager/release-upgrades.d/ua-test.cfg` with the following """ [Sources] @@ -66,6 +67,5 @@ Examples: ubuntu release | release | next_release | devel_release | - | trusty | xenial | | | xenial | bionic | | | bionic | focal | --devel-release | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/unattached_commands.feature ubuntu-advantage-tools-27.1~20.10.1/features/unattached_commands.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/unattached_commands.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/unattached_commands.feature 2021-05-27 19:05:18.000000000 +0000 @@ -19,28 +19,24 @@ | release | data | | bionic | lxd | | focal | lxd | - | trusty | nocloudnet | | xenial | lxd | + | groovy | lxd | + | hirsute | lxd | - @series.trusty @series.xenial Scenario Outline: Disabled unattached APT policy apt-hook for infra and apps Given a `` machine with ubuntu-advantage-tools installed When I run `apt update` with sudo When I run `apt-cache policy` with sudo - Then if `` in `trusty` and stdout matches regexp: - """ - -32768 -infra-security/main amd64 Packages - """ - Then if `` in `xenial` and stdout matches regexp: + Then stdout matches regexp: """ -32768 -infra-updates/main amd64 Packages """ - Then if `` in `trusty or xenial` and stdout does not match regexp: + And stdout does not match regexp: """ -32768 -apps-updates/main amd64 Packages """ - Then if `` in `trusty or xenial` and stdout does not match regexp: + And stdout does not match regexp: """ -32768 -apps-security/main amd64 Packages """ @@ -52,19 +48,11 @@ And I run `dpkg-reconfigure ubuntu-advantage-tools` with sudo And I run `apt-get update` with sudo When I run `apt-cache policy` with sudo - Then if `` in `trusty` and stdout does not match regexp: - """ - -32768 -apps-updates/main amd64 Packages - """ - Then if `` in `trusty` and stdout does not match regexp: - """ - -32768 -apps-security/main amd64 Packages - """ - Then if `` in `xenial` and stdout matches regexp: + Then stdout matches regexp: """ -32768 -apps-updates/main amd64 Packages """ - Then if `` in `xenial` and stdout matches regexp: + And stdout matches regexp: """ -32768 -apps-security/main amd64 Packages """ @@ -75,13 +63,13 @@ """ And I run `python3 /usr/lib/ubuntu-advantage/ua_update_messaging.py` with sudo And I run `run-parts /etc/update-motd.d/` with sudo - Then if `` in `xenial` and stdout matches regexp: + Then stdout matches regexp: """ \* Introducing Extended Security Maintenance for Applications. +Receive updates to over 30,000 software packages with your +Ubuntu Advantage subscription. Free for personal use. - +https:\/\/ubuntu.com\/esm + +https:\/\/ubuntu.com\/16-04 UA Infra: Extended Security Maintenance \(ESM\) is not enabled. """ @@ -96,10 +84,16 @@ """ apt-esm-json-hook """ + When I create the file `/etc/apt/sources.list.d/empty-release-origin.list` with the following + """ + deb [ allow-insecure=yes ] https://packages.irods.org/apt xenial main + """ + Then I verify that running `apt-get update` `with sudo` exits `0` + When I run `python3 /usr/lib/ubuntu-advantage/ua_update_messaging.py` with sudo + Then I verify that running `/usr/lib/ubuntu-advantage/apt-esm-hook process-templates` `with sudo` exits `0` Examples: ubuntu release | release | esm-infra-url | esm-apps-url | - | trusty | https://esm.ubuntu.com/ubuntu/ | NOTTESTED | | xenial | https://esm.ubuntu.com/infra/ubuntu | https://esm.ubuntu.com/apps/ubuntu | @series.all @@ -123,10 +117,12 @@ | bionic | refresh | | focal | detach | | focal | refresh | - | trusty | detach | - | trusty | refresh | | xenial | detach | | xenial | refresh | + | groovy | detach | + | groovy | refresh | + | hirsute | detach | + | hirsute | refresh | @series.all Scenario Outline: Unattached command known and unknown services in a ubuntu machine @@ -154,14 +150,18 @@ | focal | disable | livepatch | | focal | enable | unknown | | focal | disable | unknown | - | trusty | enable | livepatch | - | trusty | disable | livepatch | - | trusty | enable | unknown | - | trusty | disable | unknown | | xenial | enable | livepatch | | xenial | disable | livepatch | | xenial | enable | unknown | | xenial | disable | unknown | + | groovy | enable | livepatch | + | groovy | disable | livepatch | + | groovy | enable | unknown | + | groovy | disable | unknown | + | hirsute | enable | livepatch | + | hirsute | disable | livepatch | + | hirsute | enable | unknown | + | hirsute | disable | unknown | @series.all Scenario Outline: Help command on an unattached machine @@ -173,7 +173,7 @@ esm-infra Available: - yes + Help: esm-infra provides access to a private ppa which includes available high @@ -187,7 +187,7 @@ When I run `ua help esm-infra --format json` with sudo Then I will see the following on stdout: """ - {"name": "esm-infra", "available": "yes", "help": "esm-infra provides access to a private ppa which includes available high\nand critical CVE fixes for Ubuntu LTS packages in the Ubuntu Main\nrepository between the end of the standard Ubuntu LTS security\nmaintenance and its end of life. It is enabled by default with\nExtended Security Maintenance (ESM) for UA Apps and UA Infra.\nYou can find our more about the esm service at\nhttps://ubuntu.com/security/esm\n"} + {"name": "esm-infra", "available": "", "help": "esm-infra provides access to a private ppa which includes available high\nand critical CVE fixes for Ubuntu LTS packages in the Ubuntu Main\nrepository between the end of the standard Ubuntu LTS security\nmaintenance and its end of life. It is enabled by default with\nExtended Security Maintenance (ESM) for UA Apps and UA Infra.\nYou can find our more about the esm service at\nhttps://ubuntu.com/security/esm\n"} """ When I verify that running `ua help invalid-service` `with sudo` exits `1` Then I will see the following on stderr: @@ -196,11 +196,42 @@ """ Examples: ubuntu release + | release | infra-status | + | bionic | yes | + | focal | yes | + | xenial | yes | + | groovy | no | + | hirsute | no | + + + @series.all + Scenario Outline: Useful SSL failure message when there aren't any ca-certs + Given a `` machine with ubuntu-advantage-tools installed + When I run `apt remove ca-certificates -y` with sudo + When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1` + Then I will see the following on stderr: + """ + Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json + Cannot verify certificate of server + Please install "ca-certificates" and try again. + """ + When I run `apt install ca-certificates -y` with sudo + When I run `mv /etc/ssl/certs /etc/ssl/wronglocation` with sudo + When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1` + Then I will see the following on stderr: + """ + Failed to access URL: https://ubuntu.com/security/cves/CVE-1800-123456.json + Cannot verify certificate of server + Please check your openssl configuration. + """ + Examples: ubuntu release | release | + | xenial | | bionic | | focal | - | trusty | - | xenial | + | groovy | + | hirsute | + @series.focal Scenario Outline: Fix command on an unattached machine @@ -256,6 +287,7 @@ | release | | focal | + @uses.config.contract_token @series.xenial Scenario Outline: Fix command on an unattached machine Given a `` machine with ubuntu-advantage-tools installed @@ -268,10 +300,18 @@ https://ubuntu.com/security/CVE-2020-11728 1 affected package is installed: awl \(1/1\) awl: - Ubuntu security engineers are investigating this issue. + Sorry, no fix is available. 1 package is still affected: awl .*✘.* USN-4539-1 is not resolved. """ + When I run `ua fix CVE-2020-15180` as non-root + Then stdout matches regexp: + """ + CVE-2020-15180: MariaDB vulnerabilities + https://ubuntu.com/security/CVE-2020-15180 + No affected packages are installed. + .*✔.* CVE-2020-15180 does not affect your system. + """ When I run `ua fix CVE-2020-28196` as non-root Then stdout matches regexp: """ @@ -291,7 +331,7 @@ https://ubuntu.com/security/CVE-2017-9233 3 affected packages are installed: expat, matanza, swish-e \(1/3, 2/3\) matanza, swish-e: - Ubuntu security engineers are investigating this issue. + Sorry, no fix is available. """ And stderr matches regexp: """ @@ -304,123 +344,6 @@ | release | | xenial | - @uses.config.contract_token - @series.trusty - Scenario Outline: Fix command on an unattached machine - Given a `` machine with ubuntu-advantage-tools installed - When I run `ua fix USN-4539-1` as non-root - Then stdout matches regexp: - """ - USN-4539-1: AWL vulnerability - Found CVEs: - https://ubuntu.com/security/CVE-2020-11728 - No affected packages are installed. - .*✔.* USN-4539-1 does not affect your system. - """ - When I run `ua fix CVE-2020-15180` as non-root - Then stdout matches regexp: - """ - CVE-2020-15180: MariaDB vulnerabilities - https://ubuntu.com/security/CVE-2020-15180 - No affected packages are installed. - .*✔.* CVE-2020-15180 does not affect your system. - """ - When I run `ua fix CVE-2020-28196` as non-root - Then stdout matches regexp: - """ - CVE-2020-28196: Kerberos vulnerability - https://ubuntu.com/security/CVE-2020-28196 - 1 affected package is installed: krb5 - \(1/1\) krb5: - A fix is available in UA Infra. - Package fixes cannot be installed. - To install them, run this command as root \(try using sudo\) - 1 package is still affected: krb5 - .*✘.* CVE-2020-28196 is not resolved. - """ - When I fix `USN-4747-2` by attaching to a subscription with `contract_token` - Then stdout matches regexp: - """ - USN-4747-2: GNU Screen vulnerability - Found CVEs: - https://ubuntu.com/security/CVE-2021-26937 - 1 affected package is installed: screen - \(1/1\) screen: - A fix is available in UA Infra. - The update is not installed because this system is not attached to a - subscription. - - Choose: \[S\]ubscribe at ubuntu.com \[A\]ttach existing token \[C\]ancel - > Enter your token \(from https://ubuntu.com/advantage\) to attach this system: - > .*\{ ua attach .*\}.* - Updating package lists - UA Infra: ESM enabled - """ - And stdout matches regexp: - """ - .*\{ apt update && apt install --only-upgrade -y screen \}.* - .*✔.* USN-4747-2 is resolved. - """ - When I run `apt-get install -y screen=4.1.0~20120320gitdb59704-9 --force-yes` with sudo - And I run `ua disable esm-infra` with sudo - And I fix `USN-4747-2` by enabling required service - Then stdout matches regexp: - """ - USN-4747-2: GNU Screen vulnerability - Found CVEs: - https://ubuntu.com/security/CVE-2021-26937 - 1 affected package is installed: screen - \(1/1\) screen: - A fix is available in UA Infra. - The update is not installed because this system does not have - esm-infra enabled. - - Choose: \[E\]nable esm-infra \[C\]ancel - > .*\{ ua enable esm-infra \}.* - One moment, checking your subscription first - Updating package lists - UA Infra: ESM enabled - """ - And stdout matches regexp: - """ - .*\{ apt update && apt install --only-upgrade -y screen \}.* - .*✔.* USN-4747-2 is resolved. - """ - When I run `apt-get install -y screen=4.1.0~20120320gitdb59704-9 --force-yes` with sudo - And I update contract to use `effectiveTo` as `1999-12-01T00:00:00Z` - And I fix `USN-4747-2` by updating expired token - Then stdout matches regexp: - """ - USN-4747-2: GNU Screen vulnerability - Found CVEs: - https://ubuntu.com/security/CVE-2021-26937 - 1 affected package is installed: screen - \(1/1\) screen: - A fix is available in UA Infra. - The update is not installed because this system is attached to an - expired subscription. - - Choose: \[R\]enew your subscription \(at https://ubuntu.com/advantage\) \[C\]ancel - > Enter your new token to renew UA subscription on this system: - > .*\{ ua detach \}.* - Detach will disable the following service: - esm-infra - Updating package lists - This machine is now detached. - .*\{ ua attach .* \}.* - Updating package lists - UA Infra: ESM enabled - """ - And stdout matches regexp: - """ - .*\{ apt update && apt install --only-upgrade -y screen \}.* - .*✔.* USN-4747-2 is resolved. - """ - - Examples: ubuntu release - | release | - | trusty | - @series.bionic Scenario: Fix command on an unattached machine Given a `bionic` machine with ubuntu-advantage-tools installed diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/features/unattached_status.feature ubuntu-advantage-tools-27.1~20.10.1/features/unattached_status.feature --- ubuntu-advantage-tools-27.0.2~20.10.1/features/unattached_status.feature 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/features/unattached_status.feature 2021-05-27 19:05:18.000000000 +0000 @@ -7,10 +7,11 @@ Then stdout matches regexp: """ SERVICE AVAILABLE DESCRIPTION - esm-infra yes UA Infra: Extended Security Maintenance \(ESM\) - fips +NIST-certified FIPS modules - fips-updates +Uncertified security updates to FIPS modules - livepatch yes Canonical Livepatch service + cis +Center for Internet Security Audit Tools + esm-infra +UA Infra: Extended Security Maintenance \(ESM\) + fips +NIST-certified core packages + fips-updates +NIST-certified core packages with priority security updates + livepatch +Canonical Livepatch service This machine is not attached to a UA subscription. See https://ubuntu.com/advantage @@ -22,10 +23,10 @@ cc-eal +Common Criteria EAL2 Provisioning Packages cis +Center for Internet Security Audit Tools esm-apps +UA Apps: Extended Security Maintenance \(ESM\) - esm-infra yes UA Infra: Extended Security Maintenance \(ESM\) - fips +NIST-certified FIPS modules - fips-updates +Uncertified security updates to FIPS modules - livepatch yes Canonical Livepatch service + esm-infra +UA Infra: Extended Security Maintenance \(ESM\) + fips +NIST-certified core packages + fips-updates +NIST-certified core packages with priority security updates + livepatch +Canonical Livepatch service This machine is not attached to a UA subscription. See https://ubuntu.com/advantage @@ -34,10 +35,11 @@ Then stdout matches regexp: """ SERVICE AVAILABLE DESCRIPTION - esm-infra yes UA Infra: Extended Security Maintenance \(ESM\) - fips +NIST-certified FIPS modules - fips-updates +Uncertified security updates to FIPS modules - livepatch yes Canonical Livepatch service + cis +Center for Internet Security Audit Tools + esm-infra +UA Infra: Extended Security Maintenance \(ESM\) + fips +NIST-certified core packages + fips-updates +NIST-certified core packages with priority security updates + livepatch +Canonical Livepatch service This machine is not attached to a UA subscription. See https://ubuntu.com/advantage @@ -49,10 +51,10 @@ cc-eal +Common Criteria EAL2 Provisioning Packages cis +Center for Internet Security Audit Tools esm-apps +UA Apps: Extended Security Maintenance \(ESM\) - esm-infra yes UA Infra: Extended Security Maintenance \(ESM\) - fips +NIST-certified FIPS modules - fips-updates +Uncertified security updates to FIPS modules - livepatch yes Canonical Livepatch service + esm-infra +UA Infra: Extended Security Maintenance \(ESM\) + fips +NIST-certified core packages + fips-updates +NIST-certified core packages with priority security updates + livepatch +Canonical Livepatch service This machine is not attached to a UA subscription. See https://ubuntu.com/advantage @@ -69,18 +71,19 @@ cc-eal +Common Criteria EAL2 Provisioning Packages cis +Center for Internet Security Audit Tools esm-apps +UA Apps: Extended Security Maintenance \(ESM\) - esm-infra yes UA Infra: Extended Security Maintenance \(ESM\) - fips +NIST-certified FIPS modules - fips-updates +Uncertified security updates to FIPS modules - livepatch yes Canonical Livepatch service + esm-infra +UA Infra: Extended Security Maintenance \(ESM\) + fips +NIST-certified core packages + fips-updates +NIST-certified core packages with priority security updates + livepatch +Canonical Livepatch service This machine is not attached to a UA subscription. See https://ubuntu.com/advantage """ Examples: ubuntu release - | release | esm-apps | cc-eal | cis | fips | fips-update | - | bionic | yes | no | yes | yes | yes | - | focal | yes | no | no | no | no | - | trusty | no | no | no | no | no | - | xenial | yes | yes | yes | yes | yes | + | release | esm-apps | cc-eal | cis | fips | fips-update | infra | livepatch | + | bionic | yes | no | yes | yes | yes | yes | yes | + | focal | yes | no | yes | no | no | yes | yes | + | xenial | yes | yes | yes | yes | yes | yes | yes | + | groovy | no | no | no | no | no | no | no | + | hirsute | no | no | no | no | no | no | no | diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/.gitignore ubuntu-advantage-tools-27.1~20.10.1/.gitignore --- ubuntu-advantage-tools-27.0.2~20.10.1/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/.gitignore 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,29 @@ +artifacts/* +*debhelper* +debian/ubuntu-advantage-tools/ +debian/ubuntu-advantage-tools.substvars +dev/entitlement-creds.json.* +*.pyc +*.pyo +__pycache__ +.tox +.pybuild +.coverage +.cache +*.egg-info/ +.mypy_cache/ + +# Ignore artifacts generated by bddeb +*-1~bddeb* +ubuntu-advantage-tools_*.orig.tar.gz +ubuntu-advantage-tools_all.deb +ubuntu_advantage_tools.dsc + +# test/jenkins artifacts +pytest_results.xml +reports/ + +# apt-hook build artifacts +apt-hook/hook +apt-hook/ubuntu-advantage.pot +apt-hook/json-hook-src/json-hook diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/Jenkinsfile ubuntu-advantage-tools-27.1~20.10.1/Jenkinsfile --- ubuntu-advantage-tools-27.0.2~20.10.1/Jenkinsfile 2021-05-11 10:20:05.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/Jenkinsfile 2021-05-27 19:05:18.000000000 +0000 @@ -34,6 +34,7 @@ python3 -m venv $TMPDIR . $TMPDIR/bin/activate pip install tox # for tox supporting --parallel--safe-build + pip install tox-pip-version # To freeze pip version on some tests ''' } } @@ -67,32 +68,13 @@ set +x . $TMPDIR/bin/activate tox --parallel--safe-build -e py3 + tox --parallel--safe-build -e py3-xenial + tox --parallel--safe-build -e py3-bionic ''' } } stage ('Package builds') { parallel { - stage ('Package build: 14.04') { - environment { - BUILD_SERIES = "trusty" - SERIES_VERSION = "14.04" - PKG_VERSION = sh(returnStdout: true, script: "dpkg-parsechangelog --show-field Version").trim() - NEW_PKG_VERSION = "${PKG_VERSION}~${SERIES_VERSION}~${JOB_SUFFIX}" - ARTIFACT_DIR = "${TMPDIR}${BUILD_SERIES}" - } - steps { - sh ''' - set -x - mkdir ${ARTIFACT_DIR} - cp debian/changelog ${WORKSPACE}/debian/changelog-${SERIES_VERSION} - sed -i "s/${PKG_VERSION}/${NEW_PKG_VERSION}/" ${WORKSPACE}/debian/changelog-${SERIES_VERSION} - dpkg-source -l${WORKSPACE}/debian/changelog-${SERIES_VERSION} -b . - sbuild --resolve-alternatives --nolog --verbose --dist=${BUILD_SERIES} --no-run-lintian --append-to-version=~${SERIES_VERSION} ../ubuntu-advantage-tools*${NEW_PKG_VERSION}*dsc - cp ./ubuntu-advantage-tools*${SERIES_VERSION}*.deb ${ARTIFACT_DIR}/ubuntu-advantage-tools-${BUILD_SERIES}.deb - cp ./ubuntu-advantage-pro*${SERIES_VERSION}*.deb ${ARTIFACT_DIR}/ubuntu-advantage-pro-${BUILD_SERIES}.deb - ''' - } - } stage ('Package build: 16.04') { environment { BUILD_SERIES = "xenial" @@ -160,42 +142,42 @@ } stage ('Integration Tests') { parallel { - stage("lxc 14.04") { + stage("lxc 16.04") { environment { - UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}trusty/" - UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-14.04" + UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}xenial/" + UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-16.04" } steps { sh ''' set +x . $TMPDIR/bin/activate - tox --parallel--safe-build -e behave-lxd-14.04 + tox --parallel--safe-build -e behave-lxd-16.04 ''' } } - stage("lxc 16.04") { + stage("lxc 18.04") { environment { - UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}xenial/" - UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-16.04" + UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}bionic/" + UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-18.04" } steps { sh ''' set +x . $TMPDIR/bin/activate - tox --parallel--safe-build -e behave-lxd-16.04 + tox --parallel--safe-build -e behave-lxd-18.04 ''' } } - stage("lxc 18.04") { + stage("lxc 20.04") { environment { - UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}bionic/" - UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-18.04" + UACLIENT_BEHAVE_DEBS_PATH = "${TMPDIR}focal/" + UACLIENT_BEHAVE_ARTIFACT_DIR = "artifacts/behave-lxd-20.04" } steps { sh ''' set +x . $TMPDIR/bin/activate - tox --parallel--safe-build -e behave-lxd-18.04 + tox --parallel--safe-build -e behave-lxd-20.04 ''' } } diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/lib/reboot_cmds.py ubuntu-advantage-tools-27.1~20.10.1/lib/reboot_cmds.py --- ubuntu-advantage-tools-27.0.2~20.10.1/lib/reboot_cmds.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/lib/reboot_cmds.py 2021-05-27 19:05:18.000000000 +0000 @@ -78,7 +78,7 @@ try: entitlement.install_packages(cleanup_on_failure=False) except UserFacingError as e: - logging.exception(e) + logging.error(e.msg) logging.warning( "Failed to install packages at boot: {}".format( ", ".join(entitlement.packages) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/lib/ua_update_messaging.py ubuntu-advantage-tools-27.1~20.10.1/lib/ua_update_messaging.py --- ubuntu-advantage-tools-27.0.2~20.10.1/lib/ua_update_messaging.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/lib/ua_update_messaging.py 2021-05-27 19:05:18.000000000 +0000 @@ -23,7 +23,7 @@ from uaclient import entitlements from uaclient import defaults from uaclient.status import ( - MESSAGE_ANNOUNCE_ESM, + MESSAGE_ANNOUNCE_ESM_TMPL, MESSAGE_CONTRACT_EXPIRED_APT_NO_PKGS_TMPL, MESSAGE_CONTRACT_EXPIRED_APT_PKGS_TMPL, MESSAGE_CONTRACT_EXPIRED_GRACE_PERIOD_TMPL, @@ -95,6 +95,12 @@ util.remove_file(tmpl_file.replace(".tmpl", "")) +def _remove_msg_templates(msg_dir: "str", msg_template_names: "List[str]"): + # Purge all template out output messages for this service + for name in msg_template_names: + _write_template_or_remove("", os.path.join(msg_dir, name)) + + def _write_esm_service_msg_templates( cfg: config.UAConfig, ent: entitlements.base.UAEntitlement, @@ -104,14 +110,35 @@ no_pkgs_file: str, motd_pkgs_file: str, motd_no_pkgs_file: str, - no_warranty_file: str, ): + """Write any related template content for an ESM service. + + If no content is applicable for the current service state, remove + all service-related template files. + :param cfg: UAConfig instance for this environment. + :param ent: entitlements.base.UAEntitlement, + :param expiry_status: Current ContractExpiryStatus enum for attached VM. + :param remaining_days: Int remaining days on contrat, negative when + expired. + :param *_file: template file names to write. + """ pkgs_msg = no_pkgs_msg = motd_pkgs_msg = motd_no_pkgs_msg = "" - no_warranty_msg = "" tmpl_prefix = ent.name.upper().replace("-", "_") tmpl_pkg_count_var = "{{{}_PKG_COUNT}}".format(tmpl_prefix) tmpl_pkg_names_var = "{{{}_PACKAGES}}".format(tmpl_prefix) + + platform_info = util.get_platform_info() + is_active_esm = util.is_active_esm(platform_info["series"]) + if is_active_esm and ent.name == "esm-infra": + release = platform_info["release"] + ua_esm_url = defaults.EOL_UA_URL_TMPL.format( + hyphenatedrelease=release.replace(".", "-") + ) + eol_release = "for Ubuntu {release} ".format(release=release) + else: + eol_release = "" + ua_esm_url = defaults.BASE_ESM_URL if ent.application_status()[0] == ApplicationStatus.ENABLED: if expiry_status == ContractExpiryStatus.ACTIVE_EXPIRED_SOON: pkgs_msg = MESSAGE_CONTRACT_EXPIRED_SOON_TMPL.format( @@ -134,8 +161,6 @@ # Same cautionary message when in grace period motd_pkgs_msg = motd_no_pkgs_msg = no_pkgs_msg = pkgs_msg elif expiry_status == ContractExpiryStatus.EXPIRED: - if util.is_active_esm(util.get_platform_info()["series"]): - no_warranty_msg = MESSAGE_UBUNTU_NO_WARRANTY pkgs_msg = MESSAGE_CONTRACT_EXPIRED_APT_PKGS_TMPL.format( pkg_num=tmpl_pkg_count_var, pkg_names=tmpl_pkg_names_var, @@ -144,29 +169,27 @@ url=defaults.BASE_UA_URL, ) no_pkgs_msg = MESSAGE_CONTRACT_EXPIRED_APT_NO_PKGS_TMPL.format( - title=ent.title, url=defaults.BASE_ESM_URL + title=ent.title, url=defaults.BASE_UA_URL ) motd_no_pkgs_msg = no_pkgs_msg motd_pkgs_msg = MESSAGE_CONTRACT_EXPIRED_MOTD_PKGS_TMPL.format( title=ent.title, pkg_num=tmpl_pkg_count_var, - url=defaults.BASE_ESM_URL, + url=defaults.BASE_UA_URL, ) elif expiry_status != ContractExpiryStatus.EXPIRED: # Service not enabled pkgs_msg = MESSAGE_DISABLED_APT_PKGS_TMPL.format( title=ent.title, pkg_num=tmpl_pkg_count_var, pkg_names=tmpl_pkg_names_var, - url=defaults.BASE_ESM_URL, + eol_release=eol_release, + url=ua_esm_url, ) no_pkgs_msg = MESSAGE_DISABLED_MOTD_NO_PKGS_TMPL.format( - title=ent.title, url=defaults.BASE_ESM_URL + title=ent.title, url=ua_esm_url ) msg_dir = os.path.join(cfg.data_dir, "messages") - _write_template_or_remove( - no_warranty_msg, os.path.join(msg_dir, no_warranty_file) - ) _write_template_or_remove(no_pkgs_msg, os.path.join(msg_dir, no_pkgs_file)) _write_template_or_remove(pkgs_msg, os.path.join(msg_dir, pkgs_file)) _write_template_or_remove( @@ -192,38 +215,42 @@ motd_infra_no_pkg_file = ExternalMessage.MOTD_INFRA_NO_PKGS.value motd_infra_pkg_file = ExternalMessage.MOTD_INFRA_PKGS.value no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value + msg_dir = os.path.join(cfg.data_dir, "messages") apps_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-apps"] apps_inst = apps_cls(cfg) config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta" ) - apps_not_beta = bool(config_allow_beta or not apps_cls.is_beta) + apps_valid = bool(config_allow_beta or not apps_cls.is_beta) infra_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME["esm-infra"] infra_inst = infra_cls(cfg) expiry_status, remaining_days = get_contract_expiry_status(cfg) - if series != "trusty" and apps_not_beta: - _write_esm_service_msg_templates( - cfg, - apps_inst, - expiry_status, - remaining_days, - apps_pkg_file, - apps_no_pkg_file, - motd_apps_pkg_file, - motd_apps_no_pkg_file, - no_warranty_file, + enabled_status = ApplicationStatus.ENABLED + msg_esm_apps = False + msg_esm_infra = False + if util.is_active_esm(series): + no_warranty_msg = "" + if expiry_status in ( + ContractExpiryStatus.EXPIRED, + ContractExpiryStatus.NONE, + ): + no_warranty_msg = MESSAGE_UBUNTU_NO_WARRANTY + if infra_inst.application_status()[0] != enabled_status: + msg_esm_infra = True + no_warranty_msg = MESSAGE_UBUNTU_NO_WARRANTY + elif remaining_days <= defaults.CONTRACT_EXPIRY_PENDING_DAYS: + msg_esm_infra = True + _write_template_or_remove( + no_warranty_msg, os.path.join(msg_dir, no_warranty_file) ) + if not msg_esm_infra and series != "trusty": + # write_apt_and_motd_templates is only called if util.is_lts(series) + msg_esm_apps = apps_valid - # We only have esm-infra apt alerts for esm distros. - # However, if we have expired credentials, we will - # produce esm-infra message showing that the contract is - # expiring/expired. - infra_status, _ = infra_inst.application_status() - is_infra_enabled = infra_status == ApplicationStatus.ENABLED - if is_infra_enabled or util.is_active_esm(series): + if msg_esm_infra: _write_esm_service_msg_templates( cfg, infra_inst, @@ -233,7 +260,38 @@ infra_no_pkg_file, motd_infra_pkg_file, motd_infra_no_pkg_file, - no_warranty_file, + ) + else: + _remove_msg_templates( + msg_dir=os.path.join(cfg.data_dir, "messages"), + msg_template_names=[ + infra_pkg_file, + infra_no_pkg_file, + motd_infra_pkg_file, + motd_infra_no_pkg_file, + ], + ) + + if msg_esm_apps: + _write_esm_service_msg_templates( + cfg, + apps_inst, + expiry_status, + remaining_days, + apps_pkg_file, + apps_no_pkg_file, + motd_apps_pkg_file, + motd_apps_no_pkg_file, + ) + else: + _remove_msg_templates( + msg_dir=os.path.join(cfg.data_dir, "messages"), + msg_template_names=[ + apps_pkg_file, + apps_no_pkg_file, + motd_apps_pkg_file, + motd_apps_no_pkg_file, + ], ) @@ -256,8 +314,19 @@ msg_dir = os.path.join(cfg.data_dir, "messages") esm_news_file = os.path.join(msg_dir, ExternalMessage.ESM_ANNOUNCE.value) + platform_info = util.get_platform_info() + is_active_esm = util.is_active_esm(platform_info["series"]) + if is_active_esm: + ua_esm_url = defaults.EOL_UA_URL_TMPL.format( + hyphenatedrelease=platform_info["release"].replace(".", "-") + ) + else: + ua_esm_url = defaults.BASE_ESM_URL if all([series != "trusty", apps_not_beta, apps_not_enabled]): - util.write_file(esm_news_file, "\n" + MESSAGE_ANNOUNCE_ESM) + util.write_file( + esm_news_file, + "\n" + MESSAGE_ANNOUNCE_ESM_TMPL.format(url=ua_esm_url), + ) else: util.remove_file(esm_news_file) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/RELEASES.md ubuntu-advantage-tools-27.1~20.10.1/RELEASES.md --- ubuntu-advantage-tools-27.0.2~20.10.1/RELEASES.md 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/RELEASES.md 2021-05-27 19:05:18.000000000 +0000 @@ -6,53 +6,93 @@ | Build target | Version Format | | -------- | -------- | -| Devel series upstream release | XX.YY | -| Devel series bugfix release | XX.YY.Z~ubuntu1| -| Stable series release | XX.YY~ubuntu1~18.04.1| -| [Daily Build Recipe](https://code.launchpad.net/~canonical-server/+recipe/ua-client-daily) | XX.YY+-g~ubuntu1~18.04.1 | +| Devel series upstream release | `XX.YY` | +| Devel series bugfix release | `XX.YY.Z~ubuntu1`| +| Stable series release | `XX.YY~ubuntu1~18.04.1`| +| [Daily Build Recipe](https://code.launchpad.net/~canonical-server/+recipe/ua-client-daily) | `XX.YY+-g~ubuntu1~18.04.1` | | Ubuntu PRO series release | binary copies of Daily PPA | +| -------- | -------- | ## Supported upgrade use-cases based on version formats | Upgrade path | Version diffs | -| LTS to LTS | 20.3~ubuntu1~14.04.1 -> 20.3~ubuntu1~16.04.1 | -| LTS to Daily PPA | 20.3~ubuntu1~14.04.1 -> 20.3+202004011202~ubuntu1~14.04.1 | -| Ubuntu PRO to latest -updates | 20.3+202004011202~ubuntu1~14.04.1 -> 20.4~ubuntu1~14.04.1 | -| Ubuntu PRO to Daily PPA | 20.3+202004011202~ubuntu1~14.04.1 -> 20.4+202004021202~ubuntu1~14.04.1 | - +| -------- | -------- | +| LTS to LTS | `20.3~ubuntu1~14.04.1 -> 20.3~ubuntu1~16.04.1` | +| LTS to Daily PPA | `20.3~ubuntu1~14.04.1 -> 20.3+202004011202~ubuntu1~14.04.1` | +| Ubuntu PRO to latest -updates | `20.3+202004011202~ubuntu1~14.04.1` -> `20.4~ubuntu1~14.04.1` | +| Ubuntu PRO to Daily PPA | `20.3+202004011202~ubuntu1~14.04.1` -> `20.4+202004021202~ubuntu1~14.04.1` | ## Devel Release Process -Below is the procedure used to release ubuntu-advantage-client to an Ubuntu series: +Below is the procedure used to release ubuntu-advantage-client to an Ubuntu series. It makes the following assumptions: +* The current ubuntu devel release is impish +* We are releasing version 27.1 +* The previous release was 27.0.2 +* The commits for 27.0.2 and 27.1 on https://github.com/canonical/ubuntu-advantage-client are tagged with `27.0.2` and `27.1` respectively. + * These tags should be on the correct commits on the `release-27` branch + * `27.1`, which is the tag of the release we are making, has not yet been uploaded to `pkg/ubuntu/devel` 1. git-ubuntu clone ubuntu-advantage-tools; cd ubuntu-advantage-tools 2. git remote add upstream git@github.com:canonical/ubuntu-advantage-client.git 3. git fetch upstream - 4. git checkout upstream/release-24 - 5. git tag -a 24.3 - 6. git rebase --onto pkg/ubuntu/devel 24.2 24.3 - 7. git checkout -b pkg-upload-24.3 - 8. debuild -S - 9. dput ppa:chad.smith/ua-client-uploads ./ubuntu-advantage-tools_24.3_source.changes - 10. create a release tarfile/changes file using uss-tableflip's build-package script + 4. git rebase --onto pkg/ubuntu/devel 27.0.2 27.1 + 5. git checkout -B upload-27.1-impish # to create a new local branch name based on your detached branch in step 4 + 6. git push `` upload-27.1-impish + 7. Create a PPA for upload reviewers at `https://launchpad.net/~/+activate-ppa` + 8. Push to your test PPA so upload reviewers can install easily test packages easily during review + ```bash + debuild -S + dput ppa:/ua-client-uploads ./ubuntu-advantage-tools_*_source.changes ``` - ./tools/make-release --series --ppa - # follow printed procedure for dput to test PPA, and tag the released commitish -``` - 11. Create a merge proposal such as [24.2 PR](https://code.launchpad.net/~chad.smith/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/385073), [24.3 PR](https://code.launchpad.net/~chad.smith/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/389745) - 11. For SRUs: Create an "upload a new version bug" in https://bugs.launchpad.net/ubuntu/+source/ubuntu-advantage-tools/+bugs - 12. Describe the need, provide testing PPA and describe test instructions - 13. Ping appropriate maintainer about upload and verification - 14. Validate tests, accept upload and ship it - -Previous release bugs: -| ------- | -| [20.3 Focal](https://bugs.launchpad.net/ubuntu/+source/ubuntu-advantage-tools/+bug/1869980) | - + 9. Create a merge proposal for 27.1 which targets `ubuntu/devel` + * For an example, see the [27.0.2 merge proposal](https://code.launchpad.net/~chad.smith/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/+merge/402459) + 10. Add 2 review slots for `canonical-server` and `canonical-server-core-reviewers` + +## SRU Release Process +Below is the procedure used to SRU ubuntu-advantage-client to a stable Ubuntu series. It makes the following assumptions: +* The procedure for the Devel Release Process has just completed +* A local git branch representing the MP most recently released to the Ubuntu devel release is `release-27.1-impish` + + 1. Create an "upload a new version bug" in https://bugs.launchpad.net/ubuntu/+source/ubuntu-advantage-tools/+bugs + * Describe the need, provide testing PPA and describe test instructions + * For examples, see [27.0](https://bugs.launchpad.net/ubuntu/+source/ubuntu-advantage-tools/+bug/1926361) or [20.3](https://bugs.launchpad.net/ubuntu/+source/ubuntu-advantage-tools/+bug/1869980) + 2. Create a PR for each target series based off your local `release-${UA_VERSION}-impish` branch: + ```bash + UA_VERSION=27.1 + SRU_BUG=TODO_FROM_STEP_11 + rm -rf ../out + for release in xenial bionic focal groovy hirsute do + git checkout release-${UA_VERSION}-impish -B upload-${UA_VERSION}-$release + case "${release}" in + xenial) version=${UA_VERSION}~16.04.1;; + bionic) version=${UA_VERSION}~18.04.1;; + focal) version=${UA_VERSION}~20.04.1;; + groovy) version=${UA_VERSION}~20.10.1;; + hirsute) version=${UA_VERSION}~21.04.1;; + esac + dch -v ${version} -D ${release} Backport new upstream release: (LP: #${SRU_BUG}) to $release + git commit -m 'changelog backport to ${release}' debian/changelog + git push upload-${UA_VERSION}-$release + build-package # to create release-specific dsc files used to upload in step 16 + ``` + 3. Create merge proposals for each SRU target release @ `https://code.launchpad.net/~/ubuntu/+source/ubuntu-advantage-tools/+git/ubuntu-advantage-tools/`. Make sure each PR targets your `release-${UA_VERSION}-impish` branch. + 4. Add both `canonical-server` and `canonical-server-core-reviewers` as review slots on the MP. + 5. Upload each release (including impish) to `ppa:ua-client/staging` + ```bash + for changes_file in ../out/*changes; do + dput ppa:ua-client/staging $changes_file + done + ``` + 6. Ping ~Server channel member with upload rights for a review of your MR + 7. Address MR review comments or file issues for future until MR accepted and uploaded to the [proper upload release queue](https://launchpad.net/ubuntu/hirsute/+queue?queue_state=1&queue_text=ubuntu-advantage-tools) + 8. Ping [appropriate daily SRU vanguard for acceptance of ua-tools into -proposed](https://wiki.ubuntu.com/StableReleaseUpdates#Publishing)FreeNode #ubuntu-release + 9. Once [ubuntu-advantage-tools shows up in the pending_sru page](https://people.canonical.com/~ubuntu-archive/pending-sru.html), perform the [Ubuntu-advantage-client SRU verification steps](https://wiki.ubuntu.com/UbuntuAdvantageToolsUpdates) +10. When SRU verification is complete, mark any SRU-related bugs with 'verification-done' tags as each bug, task, or release verified and completes successfully +11. 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). It is now time to ping the [current SRU vanguard](https://wiki.ubuntu.com/StableReleaseUpdates#Publishing) for acceptance of ubuntu-advantage-tools into -updates ## Ubuntu PRO Release Process -Manually perform a binary package copy from Daily PPA to Premium PPA and notify image creators +Below is the procedure used to release ubuntu-advantage-tools to Ubuntu PRO images: 1. [Open Daily PPA copy-package operation](https://code.launchpad.net/~ua-client/+archive/ubuntu/daily/+copy-packages) 2. Check Trusty, Xenial, Bionic package diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/sru/release-27/test_trusty_upgrade_to_xenial.sh ubuntu-advantage-tools-27.1~20.10.1/sru/release-27/test_trusty_upgrade_to_xenial.sh --- ubuntu-advantage-tools-27.0.2~20.10.1/sru/release-27/test_trusty_upgrade_to_xenial.sh 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/sru/release-27/test_trusty_upgrade_to_xenial.sh 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,47 @@ +#!/bin/sh + +# Test if upgrading from trusty machine already attached to xenial will keep +# services still enabled after the do-release-upgrade operation. Currently, +# this will not happen because the latest version of uaclient in trusty does +# not enable do-release-upgrade to run a post script that handles the services +# migration when performing an upgrade. +# This script is adding a solution for that problem by manually adding the necessary +# configs for do-release-upgrade (This configurations are done for release 26 forward) +set -x + +series=trusty +name=$series-upgrade-lxd + +function fix_upgrade_for_esm_infra() { + # This fix is better documented here: + # https://github.com/canonical/ubuntu-advantage-client/issues/1590 + multipass exec $name -- sh -c "echo 'Pockets=security,updates,proposed,backports,infra-security,infra-updates,apps-security,apps-updates' >> allow.cfg" + multipass exec $name -- sh -c "echo '[Distro]\nPostInstallScripts=./xorg_fix_proprietary.py, /usr/lib/ubuntu-advantage/upgrade_lts_contract.py' >> allow.cfg" +} + + +multipass delete $name +multipass purge +multipass launch $series --name $name + +multipass exec $name -- sudo apt-get upgrade -y +multipass exec $name -- sudo apt-get dist-upgrade -y +multipass exec $name -- ua version +multipass exec $name -- sudo ua attach $UACLIENT_BEHAVE_CONTRACT_TOKEN +multipass exec $name -- sudo ua status +multipass exec $name -- sudo sh -c "cat </etc/apt/sources.list.d/ubuntu-$series-proposed.list +deb http://archive.ubuntu.com/ubuntu/ $series-proposed restricted main multiverse universe" +multipass exec $name -- sudo apt-get update +multipass exec $name -- sudo mkdir -p /etc/update-manager/release-upgrades.d +multipass exec $name -- sh -c "echo '[Sources]\nAllowThirdParty=yes' > allow.cfg" + +fix_upgrade_for_esm_infra + +multipass exec $name -- sudo mv allow.cfg /etc/update-manager/release-upgrades.d +multipass exec $name -- sudo do-release-upgrade --frontend DistUpgradeViewNonInteractive +multipass exec $name -- sudo ua status +multipass exec $name -- sudo reboot + +multipass exec $name -- sudo reboot +sleep 60 +multipass exec $name -- sudo ua status --wait diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/sru/release-27/test_xenial_upgrade.sh ubuntu-advantage-tools-27.1~20.10.1/sru/release-27/test_xenial_upgrade.sh --- ubuntu-advantage-tools-27.0.2~20.10.1/sru/release-27/test_xenial_upgrade.sh 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/sru/release-27/test_xenial_upgrade.sh 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,226 @@ +#!/usr/bin/bash + +set -x +set -e +name=test-xenial-ua-upgrade +series=xenial + +function h1() { + set +x + echo "" + echo "" + echo "" + echo "############################################################################" + echo "## $1" + echo "############################################################################" + echo "" + set -x +} +function h2() { + set +x + echo "" + echo "" + echo "-> $1" + echo "----------------------------------------------------------------------------" + echo "" + set -x +} + + +function setup() { + tool=$1 + + h2 "Make sure we're up to date" + $tool exec $name -- sudo apt update + $tool exec $name -- sudo apt upgrade -y + $tool exec $name -- sudo apt install ubuntu-advantage-tools -y + + h2 "Initial State" + $tool exec $name -- apt-cache policy ubuntu-advantage-tools + $tool exec $name -- sudo ubuntu-advantage status + $tool exec $name -- dpkg-query -L ubuntu-advantage-tools +} +function setup_container() { + h1 "Setting up fresh container" + lxc delete --force $name || true + lxc launch ubuntu-daily:xenial $name + sleep 10 + + setup lxc +} +function setup_vm() { + h1 "Setting up fresh vm" + multipass delete -p $name || true + multipass launch -n $name xenial + sleep 10 + + setup multipass +} +function teardown_container() { + lxc delete --force $name || true +} +function teardown_vm() { + multipass delete -p $name || true +} + +function install_new_ua() { + tool=$1 + h2 "Set up to use daily ppa" + $tool exec $name -- sudo sh -c "cat </etc/apt/sources.list.d/ubuntu-$series-proposed.list + deb http://archive.ubuntu.com/ubuntu/ $series-proposed restricted main multiverse universe" + $tool exec $name -- sudo apt update + + h2 "Actually install - verify there are no errors" + $tool exec $name -- sudo apt install ubuntu-advantage-tools -y +} + +function attach_ua() { + tool=$1 + set +x + echo "+ $tool exec $name -- sudo ua attach \$UACLIENT_BEHAVE_CONTRACT_TOKEN" + $tool exec $name -- sudo ua attach $UACLIENT_BEHAVE_CONTRACT_TOKEN + set -x +} + + + + +function test_upgrade_in_container() { + setup_container + + h1 "Upgrade to UA 27 while unattached" + + install_new_ua lxc + + h2 "Check for leftover files from old version - verify nothing unexpected is left behind" + lxc exec $name -- ls -l /etc/update-motd.d/99-esm /usr/share/keyrings/ubuntu-esm-keyring.gpg /usr/share/keyrings/ubuntu-fips-keyring.gpg /usr/share/man/man1/ubuntu-advantage.1.gz /usr/share/doc/ubuntu-advantage-tools/copyright /usr/share/doc/ubuntu-advantage-tools/changelog.gz /usr/bin/ubuntu-advantage || true + + h2 "New Status - verify esm-infra available but not enabled; esm-apps not visible" + lxc exec $name -- ua status + + h2 "Attach - verify esm-infra automatically enabled; esm-apps not visible" + attach_ua lxc + + h2 "Detaching before destruction" + lxc exec $name -- ua detach --assume-yes + + teardown_container +} + + +function test_upgrade_with_livepatch_in_vm() { + + setup_vm + + h1 "Upgrade to UA 27 while old version has livepatch enabled" + + h2 "Enable livepatch on old version" + set +x + echo "+ multipass exec $name -- ubuntu-advantage enable-livepatch \$LIVEPATCH_TOKEN" + multipass exec $name -- sudo ubuntu-advantage enable-livepatch $LIVEPATCH_TOKEN + set -x + + h2 "Status before - old UA and livepatch say enabled" + multipass exec $name -- sudo canonical-livepatch status + multipass exec $name -- sudo ubuntu-advantage status + + install_new_ua multipass + + h2 "Status after upgrade - livepatch still enabled but new UA doesn't report it" + multipass exec $name -- sudo canonical-livepatch status + multipass exec $name -- sudo ua status + + h2 "Attach - verify that livepatch is disabled and re-enabled" + attach_ua multipass + + h2 "Status after attach - both livepatch and UA should say enabled" + multipass exec $name -- sudo canonical-livepatch status + multipass exec $name -- sudo ua status + + h2 "Detaching before destruction" + multipass exec $name -- sudo ua detach --assume-yes + + teardown_vm +} + +function test_upgrade_with_fips_in_vm() { + + setup_vm + + h1 "Upgrade to UA 27 while old version has fips enabled" + + h2 "Manual fips check says disabled (file doesn't exist)" + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled || true + + h2 "Enable fips on old version" + set +x + echo "+ multipass exec $name -- ubuntu-advantage enable-fips \$FIPS_CREDS" + multipass exec $name -- sudo ubuntu-advantage enable-fips $FIPS_CREDS + set -x + + h2 "Reboot to finish fips activation" + multipass exec $name -- sudo reboot || true + sleep 20 + + h2 "Status before upgrade - old UA says fips is enabled, manual check agrees" + multipass exec $name -- sudo ubuntu-advantage status + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled + + h2 "Source added by old client is present" + multipass exec $name -- sudo ls /etc/apt/sources.list.d + multipass exec $name -- sudo grep -o private-ppa.launchpad.net/ubuntu-advantage/fips/ubuntu /etc/apt/sources.list.d/ubuntu-fips-xenial.list + + install_new_ua multipass + + h2 "Status after upgrade - new UA won't say anything is enabled, but a manual check still says fips is enabled" + multipass exec $name -- sudo ua status + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled + + h2 "Source file added by old client is renamed but contents left unchanged" + multipass exec $name -- sudo ls /etc/apt/sources.list.d + multipass exec $name -- sudo grep -o private-ppa.launchpad.net/ubuntu-advantage/fips/ubuntu /etc/apt/sources.list.d/ubuntu-fips.list + + h2 "Attach - only esm-infra will be auto-enabled" + attach_ua multipass + + h2 "Status after attach - new UA will say fips is disabled, livepatch is n/a, and there is a notice to enable fips" + multipass exec $name -- sudo ua status + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled + + h2 "Enable fips on new UA - This will re-install fips packages and ask to reboot again" + multipass exec $name -- sudo ua enable fips --assume-yes + + + h2 "Status after enabled but before reboot - UA says fips enabled, notice to enable fips is gone" + multipass exec $name -- sudo ua status + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled + + h2 "Source added by old client is replaced with new source" + set +x + echo "multipass exec $name -- sudo grep \$FIPS_CREDS /etc/apt/sources.list.d/ubuntu-fips.list && echo \"FAIL: found oldclient FIPS creds\" || echo \"SUCCESS: Migrated to new client creds\"" + multipass exec $name -- sudo grep $FIPS_CREDS /etc/apt/sources.list.d/ubuntu-fips.list && echo "FAIL: found oldclient FIPS creds" || echo "SUCCESS: Migrated to new client creds" + set -x + multipass exec $name -- sudo ls /etc/apt/sources.list.d + multipass exec $name -- sudo cat /etc/apt/sources.list.d/ubuntu-fips.list + multipass exec $name -- sudo apt update + + h2 "Check to make sure we have a valid ubuntu-*-fips metapackage installed" + multipass exec $name -- sudo grep "install" /var/log/ubuntu-advantage.log + + h2 "Reboot to finish second fips activation" + multipass exec $name -- sudo reboot || true + sleep 20 + + h2 "Status after reboot - new UA will say fips is enabled, manual check agrees" + multipass exec $name -- sudo ua status + multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled + + h2 "Detaching before destruction" + multipass exec $name -- sudo ua detach --assume-yes + + teardown_vm +} + +test_upgrade_in_container +test_upgrade_with_livepatch_in_vm +test_upgrade_with_fips_in_vm diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tools/constraints-trusty.txt ubuntu-advantage-tools-27.1~20.10.1/tools/constraints-trusty.txt --- ubuntu-advantage-tools-27.0.2~20.10.1/tools/constraints-trusty.txt 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tools/constraints-trusty.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -flake8==2.1.0 -pep8==1.4.6 -py==1.4.20 -pytest==2.5.1 -pyyaml==3.10 - -# pytest-cov isn't available in trusty; the package build tests don't require -# it, but including it here allows us to keep a consistent test command in -# tox.ini; we need the version constraint to work with pytest 2.5.1 -pytest-cov<1.7 diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tools/README.md ubuntu-advantage-tools-27.1~20.10.1/tools/README.md --- ubuntu-advantage-tools-27.0.2~20.10.1/tools/README.md 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tools/README.md 2021-05-27 19:05:18.000000000 +0000 @@ -6,5 +6,4 @@ ## Files - constraints-bionic: Bionic versions of packages used by Tox -- constraints-trusty: Trusty versions of packages used by Tox - constraints-xenial: Xenial versions of packages used by Tox diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tools/test_xenial_upgrade.sh ubuntu-advantage-tools-27.1~20.10.1/tools/test_xenial_upgrade.sh --- ubuntu-advantage-tools-27.0.2~20.10.1/tools/test_xenial_upgrade.sh 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tools/test_xenial_upgrade.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,224 +0,0 @@ -#!/usr/bin/bash - -set -x -set -e -name=test-xenial-ua-upgrade - -function h1() { - set +x - echo "" - echo "" - echo "" - echo "############################################################################" - echo "## $1" - echo "############################################################################" - echo "" - set -x -} -function h2() { - set +x - echo "" - echo "" - echo "-> $1" - echo "----------------------------------------------------------------------------" - echo "" - set -x -} - - -function setup() { - tool=$1 - - h2 "Make sure we're up to date" - $tool exec $name -- sudo apt update - $tool exec $name -- sudo apt upgrade -y - $tool exec $name -- sudo apt install ubuntu-advantage-tools -y - - h2 "Initial State" - $tool exec $name -- apt-cache policy ubuntu-advantage-tools - $tool exec $name -- sudo ubuntu-advantage status - $tool exec $name -- dpkg-query -L ubuntu-advantage-tools -} -function setup_container() { - h1 "Setting up fresh container" - lxc delete --force $name || true - lxc launch ubuntu-daily:xenial $name - sleep 10 - - setup lxc -} -function setup_vm() { - h1 "Setting up fresh vm" - multipass delete -p $name || true - multipass launch -n $name xenial - sleep 10 - - setup multipass -} -function teardown_container() { - lxc delete --force $name || true -} -function teardown_vm() { - multipass delete -p $name || true -} - -function install_new_ua() { - tool=$1 - h2 "Set up to use daily ppa" - $tool exec $name -- sudo add-apt-repository ppa:ua-client/daily -y - $tool exec $name -- sudo apt update - - h2 "Actually install - verify there are no errors" - $tool exec $name -- sudo apt install ubuntu-advantage-tools -y -} - -function attach_ua() { - tool=$1 - set +x - echo "+ $tool exec $name -- sudo ua attach \$UACLIENT_BEHAVE_CONTRACT_TOKEN" - $tool exec $name -- sudo ua attach $UACLIENT_BEHAVE_CONTRACT_TOKEN - set -x -} - - - - -function test_upgrade_in_container() { - setup_container - - h1 "Upgrade to UA 27 while unattached" - - install_new_ua lxc - - h2 "Check for leftover files from old version - verify nothing unexpected is left behind" - lxc exec $name -- ls -l /etc/update-motd.d/99-esm /usr/share/keyrings/ubuntu-esm-keyring.gpg /usr/share/keyrings/ubuntu-fips-keyring.gpg /usr/share/man/man1/ubuntu-advantage.1.gz /usr/share/doc/ubuntu-advantage-tools/copyright /usr/share/doc/ubuntu-advantage-tools/changelog.gz /usr/bin/ubuntu-advantage || true - - h2 "New Status - verify esm-infra available but not enabled; esm-apps not visible" - lxc exec $name -- ua status - - h2 "Attach - verify esm-infra automatically enabled; esm-apps not visible" - attach_ua lxc - - h2 "Detaching before destruction" - lxc exec $name -- ua detach --assume-yes - - teardown_container -} - - -function test_upgrade_with_livepatch_in_vm() { - - setup_vm - - h1 "Upgrade to UA 27 while old version has livepatch enabled" - - h2 "Enable livepatch on old version" - set +x - echo "+ multipass exec $name -- ubuntu-advantage enable-livepatch \$LIVEPATCH_TOKEN" - multipass exec $name -- sudo ubuntu-advantage enable-livepatch $LIVEPATCH_TOKEN - set -x - - h2 "Status before - old UA and livepatch say enabled" - multipass exec $name -- sudo canonical-livepatch status - multipass exec $name -- sudo ubuntu-advantage status - - install_new_ua multipass - - h2 "Status after upgrade - livepatch still enabled but new UA doesn't report it" - multipass exec $name -- sudo canonical-livepatch status - multipass exec $name -- sudo ua status - - h2 "Attach - verify that livepatch is disabled and re-enabled" - attach_ua multipass - - h2 "Status after attach - both livepatch and UA should say enabled" - multipass exec $name -- sudo canonical-livepatch status - multipass exec $name -- sudo ua status - - h2 "Detaching before destruction" - multipass exec $name -- sudo ua detach --assume-yes - - teardown_vm -} - -function test_upgrade_with_fips_in_vm() { - - setup_vm - - h1 "Upgrade to UA 27 while old version has fips enabled" - - h2 "Manual fips check says disabled (file doesn't exist)" - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled || true - - h2 "Enable fips on old version" - set +x - echo "+ multipass exec $name -- ubuntu-advantage enable-fips \$FIPS_CREDS" - multipass exec $name -- sudo ubuntu-advantage enable-fips $FIPS_CREDS - set -x - - h2 "Reboot to finish fips activation" - multipass exec $name -- sudo reboot || true - sleep 20 - - h2 "Status before upgrade - old UA says fips is enabled, manual check agrees" - multipass exec $name -- sudo ubuntu-advantage status - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled - - h2 "Source added by old client is present" - multipass exec $name -- sudo ls /etc/apt/sources.list.d - multipass exec $name -- sudo grep -o private-ppa.launchpad.net/ubuntu-advantage/fips/ubuntu /etc/apt/sources.list.d/ubuntu-fips-xenial.list - - install_new_ua multipass - - h2 "Status after upgrade - new UA won't say anything is enabled, but a manual check still says fips is enabled" - multipass exec $name -- sudo ua status - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled - - h2 "Source file added by old client is renamed but contents left unchanged" - multipass exec $name -- sudo ls /etc/apt/sources.list.d - multipass exec $name -- sudo grep -o private-ppa.launchpad.net/ubuntu-advantage/fips/ubuntu /etc/apt/sources.list.d/ubuntu-fips.list - - h2 "Attach - only esm-infra will be auto-enabled" - attach_ua multipass - - h2 "Status after attach - new UA will say fips is disabled, livepatch is n/a, and there is a notice to enable fips" - multipass exec $name -- sudo ua status - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled - - h2 "Enable fips on new UA - This will re-install fips packages and ask to reboot again" - multipass exec $name -- sudo ua enable fips --assume-yes - - - h2 "Status after enabled but before reboot - UA says fips enabled, notice to enable fips is gone" - multipass exec $name -- sudo ua status - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled - - h2 "Source added by old client is replaced with new source" - set +x - echo "multipass exec $name -- sudo grep \$FIPS_CREDS /etc/apt/sources.list.d/ubuntu-fips.list && echo \"FAIL: found oldclient FIPS creds\" || echo \"SUCCESS: Migrated to new client creds\"" - multipass exec $name -- sudo grep $FIPS_CREDS /etc/apt/sources.list.d/ubuntu-fips.list && echo "FAIL: found oldclient FIPS creds" || echo "SUCCESS: Migrated to new client creds" - set -x - multipass exec $name -- sudo ls /etc/apt/sources.list.d - multipass exec $name -- sudo cat /etc/apt/sources.list.d/ubuntu-fips.list - multipass exec $name -- sudo apt update - - h2 "Check to make sure we have a valid ubuntu-*-fips metapackage installed" - multipass exec $name -- sudo grep "install" /var/log/ubuntu-advantage.log - - h2 "Reboot to finish second fips activation" - multipass exec $name -- sudo reboot || true - sleep 20 - - h2 "Status after reboot - new UA will say fips is enabled, manual check agrees" - multipass exec $name -- sudo ua status - multipass exec $name -- sudo cat /proc/sys/crypto/fips_enabled - - h2 "Detaching before destruction" - multipass exec $name -- sudo ua detach --assume-yes - - teardown_vm -} - -test_upgrade_in_container -test_upgrade_with_livepatch_in_vm -test_upgrade_with_fips_in_vm diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tools/tox-lxd-runner ubuntu-advantage-tools-27.1~20.10.1/tools/tox-lxd-runner --- ubuntu-advantage-tools-27.0.2~20.10.1/tools/tox-lxd-runner 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tools/tox-lxd-runner 2021-05-27 19:05:18.000000000 +0000 @@ -16,7 +16,7 @@ if (($# != 2)); then echo "Usage: $0 " - echo "Example: $0 trusty 'tox -e flake8-trusty'" + echo "Example: $0 xenial 'tox -e flake8-xenial'" exit 1 fi diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tools/ua.bash ubuntu-advantage-tools-27.1~20.10.1/tools/ua.bash --- ubuntu-advantage-tools-27.0.2~20.10.1/tools/ua.bash 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tools/ua.bash 2021-05-27 19:05:18.000000000 +0000 @@ -1,6 +1,9 @@ # bash completion for ubuntu-advantage-tools -SERVICES="cc-eal cis-audit esm-infra fips fips-updates livepatch" +SERVICES=$(python3 -c " +from uaclient.entitlements import valid_services +print(*valid_services(), sep=' ') +") _ua_complete() { diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/tox.ini ubuntu-advantage-tools-27.1~20.10.1/tox.ini --- ubuntu-advantage-tools-27.0.2~20.10.1/tox.ini 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/tox.ini 2021-05-27 19:05:18.000000000 +0000 @@ -20,7 +20,6 @@ deps = -rrequirements.txt -rtest-requirements.txt - trusty: -ctools/constraints-trusty.txt xenial: -ctools/constraints-xenial.txt bionic: -ctools/constraints-bionic.txt mypy: mypy @@ -44,45 +43,39 @@ py3: py.test --junitxml=pytest_results.xml {posargs:--cov uaclient uaclient} flake8: flake8 uaclient lib setup.py flake8-bionic: flake8 features - mypy: mypy --python-version 3.4 uaclient/ mypy: mypy --python-version 3.5 uaclient/ mypy: mypy --python-version 3.6 uaclient/ features/ lib/ mypy-focal: mypy --python-version 3.7 uaclient/ features/ lib/ black: black --check --diff uaclient/ features/ lib/ setup.py - behave-lxd-14.04: behave -v {posargs} --tags="series.trusty,series.all" --tags="~upgrade" - behave-lxd-16.04: behave -v {posargs} --tags="series.xenial,series.all" --tags="~upgrade" - behave-lxd-18.04: behave -v {posargs} --tags="series.bionic,series.all" --tags="~upgrade" - behave-lxd-20.04: behave -v {posargs} --tags="series.focal,series.all" --tags="~upgrade" - behave-vm-16.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.xenial,series.all" - behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all" - behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all" - behave-upgrade-14.04: behave -v {posargs} features/ubuntu_upgrade.feature --tags="series.trusty" + behave-lxd-16.04: behave -v {posargs} --tags="series.xenial,series.lts,series.all" --tags="~upgrade" + behave-lxd-18.04: behave -v {posargs} --tags="series.bionic,series.lts,series.all" --tags="~upgrade" + behave-lxd-20.04: behave -v {posargs} --tags="series.focal,series.lts,series.all" --tags="~upgrade" + behave-lxd-20.10: behave -v {posargs} --tags="series.groovy,series.all" --tags="~upgrade" + behave-lxd-21.04: behave -v {posargs} --tags="series.hirsute,series.all" --tags="~upgrade" + behave-vm-16.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.xenial,series.all,series.lts" + behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all,series.lts" + behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all,series.lts" behave-upgrade-16.04: behave -v {posargs} features/ubuntu_upgrade.feature --tags="series.xenial" behave-upgrade-18.04: behave -v {posargs} features/ubuntu_upgrade.feature --tags="series.bionic" behave-upgrade-20.04: behave -v {posargs} features/ubuntu_upgrade.feature --tags="series.focal" - behave-awsgeneric-14.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.trusty,series.all" --tags="~upgrade" - behave-awsgeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.xenial,series.all" --tags="~upgrade" - behave-awsgeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.bionic,series.all" --tags="~upgrade" - behave-awsgeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.focal,series.all" --tags="~upgrade" - behave-awspro-14.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.trusty,series.all" - behave-awspro-16.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.xenial,series.all" - behave-awspro-18.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.bionic,series.all" - behave-awspro-20.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.focal,series.all" - behave-azuregeneric-14.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.trusty,series.all" --tags="~upgrade" - behave-azuregeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.xenial,series.all" --tags="~upgrade" - behave-azuregeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.bionic,series.all" --tags="~upgrade" - behave-azuregeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.focal,series.all" --tags="~upgrade" - behave-gcpgeneric-14.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.trusty,series.all" --tags="~upgrade" - behave-gcpgeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.xenial,series.all" --tags="~upgrade" - behave-gcpgeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.bionic,series.all" --tags="~upgrade" - behave-gcpgeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.focal,series.all" --tags="~upgrade" - behave-gcppro-16.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.xenial,series.all" --tags="~upgrade" - behave-gcppro-18.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.bionic,series.all" --tags="~upgrade" - behave-gcppro-20.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.focal,series.all" --tags="~upgrade" - behave-azurepro-14.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.trusty,series.all" - behave-azurepro-16.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.xenial,series.all" - behave-azurepro-18.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.bionic,series.all" - behave-azurepro-20.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.focal,series.all" + behave-awsgeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.xenial,series.lts,series.all" --tags="~upgrade" + behave-awsgeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.bionic,series.lts,series.all" --tags="~upgrade" + behave-awsgeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.aws.generic" --tags="series.focal,series.lts,series.all" --tags="~upgrade" + behave-awspro-16.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.xenial,series.lts" + behave-awspro-18.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.bionic,series.lts" + behave-awspro-20.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.focal,series.lts" + behave-azuregeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.xenial,series.lts,series.all" --tags="~upgrade" + behave-azuregeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.bionic,series.lts,series.all" --tags="~upgrade" + behave-azuregeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.azure.generic" --tags="series.focal,series.lts,series.all" --tags="~upgrade" + behave-gcpgeneric-16.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.xenial,series.lts,series.all" --tags="~upgrade" + behave-gcpgeneric-18.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.bionic,series.lts,series.all" --tags="~upgrade" + behave-gcpgeneric-20.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.generic" --tags="series.focal,series.lts,series.all" --tags="~upgrade" + behave-gcppro-16.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.xenial,series.lts" --tags="~upgrade" + behave-gcppro-18.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.bionic,series.lts" --tags="~upgrade" + behave-gcppro-20.04: behave -v {posargs} --tags="uses.config.machine_type.gcp.pro" --tags="series.focal,series.lts" --tags="~upgrade" + behave-azurepro-16.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.xenial,series.lts" + behave-azurepro-18.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.bionic,series.lts" + behave-azurepro-20.04: behave -v {posargs} features/ubuntu_pro.feature --tags="series.focal,series.lts" [flake8] # E251: Older versions of flake8 et al don't permit the diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/cli.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/cli.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/cli.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/cli.py 2021-05-27 19:05:18.000000000 +0000 @@ -296,7 +296,7 @@ action="store", nargs="?", help="a service to view help output for. One of: {}".format( - entitlements.RELEASED_ENTITLEMENTS_STR + ", ".join(entitlements.valid_services()) ), ) @@ -337,7 +337,7 @@ nargs="+", help=( "the name(s) of the Ubuntu Advantage services to enable." - " One of: {}".format(entitlements.RELEASED_ENTITLEMENTS_STR) + " One of: {}".format(", ".join(entitlements.valid_services())) ), ) parser.add_argument( @@ -367,7 +367,7 @@ nargs="+", help=( "the name(s) of the Ubuntu Advantage services to disable" - " One of: {}".format(entitlements.RELEASED_ENTITLEMENTS_STR) + " One of: {}".format(", ".join(entitlements.valid_services())) ), ) parser.add_argument( @@ -496,7 +496,11 @@ ret &= _perform_disable(entitlement, cfg, assume_yes=args.assume_yes) if entitlements_not_found: - valid_names = "Try " + entitlements.ALL_ENTITLEMENTS_STR + "." + valid_names = ( + "Try " + + ", ".join(entitlements.valid_services(allow_beta=True)) + + "." + ) service_msg = "\n".join( textwrap.wrap(valid_names, width=80, break_long_words=False) ) @@ -511,51 +515,6 @@ return 0 if ret else 1 -def _perform_enable( - entitlement_name: str, - cfg: config.UAConfig, - *, - assume_yes: bool = False, - silent_if_inapplicable: bool = False, - allow_beta: bool = False -) -> bool: - """Perform the enable action on a named entitlement. - - (This helper excludes any messaging, so that different enablement code - paths can message themselves.) - - :param entitlement_name: the name of the entitlement to enable - :param cfg: the UAConfig to pass to the entitlement - :param assume_yes: - Assume a yes response for any prompts during service enable - :param silent_if_inapplicable: - don't output messages when determining if an entitlement can be - enabled on this system - :param allow_beta: Allow enabling beta services - - @return: True on success, False otherwise - """ - ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[entitlement_name] - config_allow_beta = util.is_config_value_true( - config=cfg.cfg, path_to_value="features.allow_beta" - ) - allow_beta |= config_allow_beta - if not allow_beta and ent_cls.is_beta: - tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL - raise exceptions.BetaServiceError( - tmpl.format( - operation="enable", - name=entitlement_name, - service_msg="Beta services enabled with --beta param", - ) - ) - - entitlement = ent_cls(cfg, assume_yes=assume_yes) - ret = entitlement.enable(silent_if_inapplicable=silent_if_inapplicable) - cfg.status() # Update the status cache - return ret - - @assert_root @assert_attached(ua_status.MESSAGE_ENABLE_FAILURE_UNATTACHED_TMPL) @assert_lock_file("ua enable") @@ -575,26 +534,37 @@ entitlements_found, entitlements_not_found = get_valid_entitlement_names( names ) + valid_services_names = entitlements.valid_services(allow_beta=args.beta) ret = True - for entitlement in entitlements_found: + for ent_name in entitlements_found: try: - ret &= _perform_enable( - entitlement, - cfg, - assume_yes=args.assume_yes, - allow_beta=args.beta, + ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[ent_name] + entitlement = ent_cls( + cfg, assume_yes=args.assume_yes, allow_beta=args.beta ) - except exceptions.BetaServiceError: - entitlements_not_found.append(entitlement) + ent_ret = entitlement.enable() + cfg.status() # Update the status cache + + if not ent_ret: + can_enable, reason = entitlement.can_enable( + silent=True, allow_disable=False + ) + + if ( + not can_enable + and reason == ua_status.CanEnableFailureReason.IS_BETA + ): + # if we failed because ent is in beta and there was no + # allow_beta flag/config, pretend it doesn't exist + entitlements_not_found.append(ent_name) + + ret &= ent_ret except exceptions.UserFacingError as e: print(e) if entitlements_not_found: - if args.beta: - valid_names = entitlements.ALL_ENTITLEMENTS_STR - else: - valid_names = entitlements.RELEASED_ENTITLEMENTS_STR + valid_names = ", ".join(valid_services_names) service_msg = "\n".join( textwrap.wrap( "Try " + valid_names + ".", width=80, break_long_words=False @@ -677,14 +647,19 @@ cfg.status() # Persist updated status in the event of partial attach config.update_ua_messages(cfg) return 1 + contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][ "name" ] - print( - ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format( - contract_name=contract_name + + if contract_name: + print( + ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format( + contract_name=contract_name + ) ) - ) + else: + print(ua_status.MESSAGE_ATTACH_SUCCESS_NO_CONTRACT_NAME) config.update_ua_messages(cfg) action_status(args=None, cfg=cfg) @@ -1004,26 +979,36 @@ return func(*args, **kwargs) except KeyboardInterrupt: with util.disable_log_to_console(): - logging.exception("KeyboardInterrupt") + logging.error("KeyboardInterrupt") print("Interrupt received; exiting.", file=sys.stderr) if _CLEAR_LOCK_FILE: _CLEAR_LOCK_FILE("lock") sys.exit(1) except util.UrlError as exc: - with util.disable_log_to_console(): - msg_args = {"url": exc.url, "error": exc} - if exc.url: - msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_WITH_URL_TMPL - else: - msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_TMPL - logging.exception(msg_tmpl.format(**msg_args)) - print(ua_status.MESSAGE_CONNECTIVITY_ERROR, file=sys.stderr) + if "CERTIFICATE_VERIFY_FAILED" in str(exc): + tmpl = ua_status.MESSAGE_SSL_VERIFICATION_ERROR_CA_CERTIFICATES + if util.is_installed("ca-certificates"): + tmpl = ( + ua_status.MESSAGE_SSL_VERIFICATION_ERROR_OPENSSL_CONFIG + ) + print(tmpl.format(url=exc.url), file=sys.stderr) + else: + with util.disable_log_to_console(): + msg_args = {"url": exc.url, "error": exc} + if exc.url: + msg_tmpl = ( + ua_status.LOG_CONNECTIVITY_ERROR_WITH_URL_TMPL + ) + else: + msg_tmpl = ua_status.LOG_CONNECTIVITY_ERROR_TMPL + logging.exception(msg_tmpl.format(**msg_args)) + print(ua_status.MESSAGE_CONNECTIVITY_ERROR, file=sys.stderr) if _CLEAR_LOCK_FILE: _CLEAR_LOCK_FILE("lock") sys.exit(1) except exceptions.UserFacingError as exc: with util.disable_log_to_console(): - logging.exception(exc.msg) + logging.error(exc.msg) print("{}".format(exc.msg), file=sys.stderr) if _CLEAR_LOCK_FILE: if not isinstance(exc, exceptions.LockHeldError): diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/clouds/aws.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/clouds/aws.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/clouds/aws.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/clouds/aws.py 2021-05-27 19:05:18.000000000 +0000 @@ -10,22 +10,51 @@ from uaclient import util +IMDS_V2_TOKEN_URL = "http://169.254.169.254/latest/api/token" IMDS_URL = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7" SYS_HYPERVISOR_PRODUCT_UUID = "/sys/hypervisor/uuid" DMI_PRODUCT_SERIAL = "/sys/class/dmi/id/product_serial" DMI_PRODUCT_UUID = "/sys/class/dmi/id/product_uuid" +AWS_TOKEN_TTL_SECONDS = "21600" +AWS_TOKEN_PUT_HEADER = "X-aws-ec2-metadata-token" +AWS_TOKEN_REQ_HEADER = AWS_TOKEN_PUT_HEADER + "-ttl-seconds" + class UAAutoAttachAWSInstance(AutoAttachCloudInstance): + _api_token = None + # mypy does not handle @property around inner decorators # https://github.com/python/mypy/issues/1362 @property # type: ignore @util.retry(HTTPError, retry_sleeps=[1, 2, 5]) def identity_doc(self) -> "Dict[str, Any]": - response, _headers = util.readurl(IMDS_URL) + headers = self._get_imds_v2_token_headers() + response, _headers = util.readurl(IMDS_URL, headers=headers) return {"pkcs7": response} + @util.retry(HTTPError, retry_sleeps=[1, 2, 5]) + def _get_imds_v2_token_headers(self): + if self._api_token == "IMDSv1": + return None + elif self._api_token: + return {AWS_TOKEN_PUT_HEADER: self._api_token} + try: + response, _headers = util.readurl( + IMDS_V2_TOKEN_URL, + method="PUT", + headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, + ) + except HTTPError as e: + if e.code == 404: + self._api_token = "IMDSv1" + return None + else: + raise + self._api_token = response + return {AWS_TOKEN_PUT_HEADER: self._api_token} + @property def cloud_type(self) -> str: return "aws" diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/clouds/tests/test_aws.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/clouds/tests/test_aws.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/clouds/tests/test_aws.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/clouds/tests/test_aws.py 2021-05-27 19:05:18.000000000 +0000 @@ -5,7 +5,12 @@ import pytest -from uaclient.clouds.aws import UAAutoAttachAWSInstance +from uaclient.clouds.aws import ( + AWS_TOKEN_PUT_HEADER, + AWS_TOKEN_REQ_HEADER, + AWS_TOKEN_TTL_SECONDS, + UAAutoAttachAWSInstance, +) M_PATH = "uaclient.clouds.aws." @@ -16,13 +21,95 @@ assert "aws" == instance.cloud_type @mock.patch(M_PATH + "util.readurl") + def test__get_imds_v2_token_headers_none_on_404(self, readurl): + """A 404 on private AWS regions indicates lack IMDSv2 support.""" + readurl.side_effect = HTTPError( + "http://me", 404, "No IMDSv2 support", None, BytesIO() + ) + instance = UAAutoAttachAWSInstance() + assert None is instance._get_imds_v2_token_headers() + assert "IMDSv1" == instance._api_token + # No retries on 404. It is a permanent indication of no IMDSv2 support. + instance._get_imds_v2_token_headers() + assert 1 == readurl.call_count + + @mock.patch(M_PATH + "util.readurl") + def test__get_imds_v2_token_headers_caches_response(self, readurl): + """Return API token headers for IMDSv2 access. Response is cached.""" + instance = UAAutoAttachAWSInstance() + url = "http://169.254.169.254/latest/api/token" + readurl.return_value = "somebase64token==", {"header": "stuff"} + assert { + AWS_TOKEN_PUT_HEADER: "somebase64token==" + } == instance._get_imds_v2_token_headers() + instance._get_imds_v2_token_headers() + assert "somebase64token==" == instance._api_token + assert [ + mock.call( + url, + method="PUT", + headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, + ) + ] == readurl.call_args_list + + @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) + @pytest.mark.parametrize("fail_count,exception", ((3, False), (4, True))) + @mock.patch(M_PATH + "util.time.sleep") + @mock.patch(M_PATH + "util.readurl") + def test_retry_backoff_on__get_imds_v2_token_headers_caches_response( + self, readurl, sleep, fail_count, exception, caplog_text + ): + """Retry backoff before failing _get_imds_v2_token_headers.""" + + def fake_someurlerrors(url, method=None, headers=None): + if readurl.call_count <= fail_count: + raise HTTPError( + "http://me", + 700 + readurl.call_count, + "funky error msg", + None, + BytesIO(), + ) + return "base64token==", {"header": "stuff"} + + readurl.side_effect = fake_someurlerrors + instance = UAAutoAttachAWSInstance() + if exception: + with pytest.raises(HTTPError) as excinfo: + instance._get_imds_v2_token_headers() + assert 704 == excinfo.value.code + else: + assert { + AWS_TOKEN_PUT_HEADER: "base64token==" + } == instance._get_imds_v2_token_headers() + + expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)] + assert expected_sleep_calls == sleep.call_args_list + expected_logs = [ + "HTTP Error 701: funky error msg Retrying 3 more times.", + "HTTP Error 702: funky error msg Retrying 2 more times.", + "HTTP Error 703: funky error msg Retrying 1 more times.", + ] + logs = caplog_text() + for log in expected_logs: + assert log in logs + + @mock.patch(M_PATH + "util.readurl") def test_identity_doc_from_aws_url_pkcs7(self, readurl): """Return pkcs7 content from IMDS as AWS' identity doc""" readurl.return_value = "pkcs7WOOT!==", {"header": "stuff"} instance = UAAutoAttachAWSInstance() assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc url = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7" - assert [mock.call(url)] == readurl.call_args_list + token_url = "http://169.254.169.254/latest/api/token" + assert [ + mock.call( + token_url, + method="PUT", + headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, + ), + mock.call(url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}), + ] == readurl.call_args_list @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) @pytest.mark.parametrize("fail_count,exception", ((3, False), (4, True))) @@ -33,8 +120,11 @@ ): """Retry backoff is attempted before failing to get AWS.identity_doc""" - def fake_someurlerrors(url): - if readurl.call_count <= fail_count: + def fake_someurlerrors(url, method=None, headers=None): + # due to _get_imds_v2_token_headers + if "latest/api/token" in url: + return "base64token==", {"header": "stuff"} + if readurl.call_count <= fail_count + 1: raise HTTPError( "http://me", 700 + readurl.call_count, @@ -49,16 +139,16 @@ if exception: with pytest.raises(HTTPError) as excinfo: instance.identity_doc - assert 704 == excinfo.value.code + assert 705 == excinfo.value.code else: assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)] assert expected_sleep_calls == sleep.call_args_list expected_logs = [ - "HTTP Error 701: funky error msg Retrying 3 more times.", - "HTTP Error 702: funky error msg Retrying 2 more times.", - "HTTP Error 703: funky error msg Retrying 1 more times.", + "HTTP Error 702: funky error msg Retrying 3 more times.", + "HTTP Error 703: funky error msg Retrying 2 more times.", + "HTTP Error 704: funky error msg Retrying 1 more times.", ] logs = caplog_text() for log in expected_logs: diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/config.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/config.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/config.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/config.py 2021-05-27 19:05:18.000000000 +0000 @@ -68,6 +68,7 @@ "status-cache": DataPath("status.json", False), "notices": DataPath("notices.json", False), "marker-reboot-cmds": DataPath("marker-reboot-cmds-required", False), + "services-once-enabled": DataPath("services-once-enabled", False), } # type: Dict[str, DataPath] _entitlements = None # caching to avoid repetitive file reads diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/contract.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/contract.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/contract.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/contract.py 2021-05-27 19:05:18.000000000 +0000 @@ -262,7 +262,7 @@ except exceptions.UserFacingError: delta_error = True with util.disable_log_to_console(): - logging.exception( + logging.error( "Failed to process contract delta for {name}:" " {delta}".format(name=name, delta=new_entitlement) ) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/defaults.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/defaults.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/defaults.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/defaults.py 2021-05-27 19:05:18.000000000 +0000 @@ -14,6 +14,7 @@ BASE_CONTRACT_URL = "https://contracts.canonical.com" BASE_SECURITY_URL = "https://ubuntu.com/security" BASE_UA_URL = "https://ubuntu.com/advantage" +EOL_UA_URL_TMPL = "https://ubuntu.com/{hyphenatedrelease}" BASE_ESM_URL = "https://ubuntu.com/esm" PRINT_WRAP_WIDTH = 80 CONTRACT_EXPIRY_GRACE_PERIOD_DAYS = 14 diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/base.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/base.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/base.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/base.py 2021-05-27 19:05:18.000000000 +0000 @@ -5,8 +5,18 @@ import re import yaml +from uaclient.util import is_config_value_true + try: - from typing import Any, Callable, Dict, List, Optional, Tuple # noqa: F401 + from typing import ( # noqa: F401 + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Union, + ) StaticAffordance = Tuple[str, Callable[[], Any], bool] except ImportError: @@ -21,6 +31,7 @@ ApplicabilityStatus, ContractStatus, UserFacingStatus, + CanEnableFailureReason, MESSAGE_INCOMPATIBLE_SERVICE_STOPS_ENABLE, ) from uaclient.defaults import DEFAULT_HELP_FILE @@ -97,8 +108,19 @@ """ return self._incompatible_services + # Any custom messages to emit to the console or callables which are + # handled at pre_enable, pre_disable, pre_install or post_enable stages + @property + def messaging( + self + ) -> "Dict[str, List[Union[str, Tuple[Callable, Dict]]]]": + return {} + def __init__( - self, cfg: "Optional[config.UAConfig]" = None, assume_yes: bool = False + self, + cfg: "Optional[config.UAConfig]" = None, + assume_yes: bool = False, + allow_beta: bool = False, ) -> None: """Setup UAEntitlement instance @@ -108,8 +130,21 @@ cfg = config.UAConfig() self.cfg = cfg self.assume_yes = assume_yes + self.allow_beta = allow_beta + self._valid_service = None + + @property + def valid_service(self): + """Check if the service is marked as valid (non-beta)""" + if self._valid_service is None: + self._valid_service = ( + not self.is_beta + or self.allow_beta + or is_config_value_true(self.cfg.cfg, "features.allow_beta") + ) + + return self._valid_service - @abc.abstractmethod def enable(self, *, silent_if_inapplicable: bool = False) -> bool: """Enable specific entitlement. @@ -119,6 +154,38 @@ @return: True on success, False otherwise. """ + msg_ops = self.messaging.get("pre_enable", []) + if not util.handle_message_operations(msg_ops): + return False + can_enable, _ = self.can_enable(silent=silent_if_inapplicable) + if not can_enable: + return False + + ret = self._perform_enable( + silent_if_inapplicable=silent_if_inapplicable + ) + if not ret: + return False + + msg_ops = self.messaging.get("post_enable", []) + if not util.handle_message_operations(msg_ops): + return False + + return True + + @abc.abstractmethod + def _perform_enable(self, *, silent_if_inapplicable: bool = False) -> bool: + """ + Enable specific entitlement. This should be implemented by subclasses. + This method does the actual enablement, and does not check can_enable + or handle pre_enable or post_enable messaging. + + :param silent_if_inapplicable: + Don't emit any messages until after it has been determined that + this entitlement is applicable to the current machine. + + @return: True on success, False otherwise. + """ pass def can_disable(self, silent: bool = False) -> bool: @@ -138,12 +205,21 @@ return False return True - def can_enable(self, silent: bool = False) -> bool: + def can_enable( + self, silent: bool = False, allow_disable: bool = True + ) -> "Tuple[bool, Optional[CanEnableFailureReason]]": """ Report whether or not enabling is possible for the entitlement. :param silent: if True, suppress output + :param allow_disable: if True, allow disabling incompatible services + + :return: + tuple of (bool, CanEnableFailureReason). + (True, None) if can enable + (False, Reason) if can't enable """ + if self.is_access_expired(): logging.debug( "Updating contract on service '%s' expiry", self.name @@ -152,7 +228,7 @@ if not self.contract_status() == ContractStatus.ENTITLED: if not silent: print(status.MESSAGE_UNENTITLED_TMPL.format(title=self.title)) - return False + return (False, CanEnableFailureReason.NOT_ENTITLED) application_status, _ = self.application_status() if application_status != status.ApplicationStatus.DISABLED: @@ -162,19 +238,29 @@ title=self.title ) ) - return False + return (False, CanEnableFailureReason.ALREADY_ENABLED) + + if not self.valid_service: + return (False, CanEnableFailureReason.IS_BETA) + applicability_status, details = self.applicability_status() if applicability_status == status.ApplicabilityStatus.INAPPLICABLE: if not silent: print(details) - return False + return (False, CanEnableFailureReason.INAPPLICABLE) if self.incompatible_services: - return self.handle_incompatible_services() + handle_incompat_ret = self.handle_incompatible_services( + silent=silent, allow_disable=allow_disable + ) + if not handle_incompat_ret: + return (False, CanEnableFailureReason.INCOMPATIBLE_SERVICE) - return True + return (True, None) - def handle_incompatible_services(self) -> bool: + def handle_incompatible_services( + self, silent: bool = False, allow_disable: bool = True + ) -> bool: """ Prompt user when incompatible services are found during enable. @@ -189,9 +275,11 @@ features: block_disable_on_enable: true - We can also use the --allow-disable flag during enable to - automatically disable any incompatible service during - enable. + We can also control this behavior by calling this method with + allow_disable as False + + :param silent: if True, suppress output + :param allow_disable: if True, allow disabling incompatible services """ from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME @@ -221,8 +309,9 @@ incompatible_service=ent.title, ) - if cfg_block_disable_on_enable: - logging.info(e_msg) + if cfg_block_disable_on_enable or not allow_disable: + if not silent: + logging.info(e_msg) return False if not util.prompt_for_confirmation( @@ -444,11 +533,15 @@ if not resourceToken: resourceToken = deltas.get("resourceToken") delta_obligations = delta_entitlement.get("obligations", {}) - can_enable = self.can_enable(silent=True) - enableByDefault = bool( + enable_by_default = bool( delta_obligations.get("enableByDefault") and resourceToken ) - if can_enable and enableByDefault: + + if enable_by_default: + self.allow_beta = True + + can_enable, _ = self.can_enable(silent=True) + if can_enable and enable_by_default: if allow_enable: msg = status.MESSAGE_ENABLE_BY_DEFAULT_TMPL.format( name=self.name diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/cis.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/cis.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/cis.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/cis.py 2021-05-27 19:05:18.000000000 +0000 @@ -1,5 +1,13 @@ from uaclient.entitlements import repo +try: + from typing import Callable, Dict, List, Tuple, Union # noqa +except ImportError: + # typing isn't available on trusty, so ignore its absence + pass + +CIS_DOCS_URL = "https://security-certs.docs.ubuntu.com/en/cis" + class CISEntitlement(repo.RepoEntitlement): @@ -8,5 +16,14 @@ title = "CIS Audit" description = "Center for Internet Security Audit Tools" repo_key_file = "ubuntu-advantage-cis.gpg" - is_beta = True apt_noninteractive = True + + @property + def messaging( + self, + ) -> "Dict[str, List[Union[str, Tuple[Callable, Dict]]]]": + return { + "post_enable": [ + "Visit {} to learn how to use CIS".format(CIS_DOCS_URL) + ] + } diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/esm.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/esm.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/esm.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/esm.py 2021-05-27 19:05:18.000000000 +0000 @@ -12,8 +12,8 @@ class ESMBaseEntitlement(repo.RepoEntitlement): help_doc_url = "https://ubuntu.com/security/esm" - def enable(self, *, silent_if_inapplicable: bool = False) -> bool: - enable_performed = super().enable( + def _perform_enable(self, *, silent_if_inapplicable: bool = False) -> bool: + enable_performed = super()._perform_enable( silent_if_inapplicable=silent_if_inapplicable ) if enable_performed: @@ -41,10 +41,8 @@ series = util.get_platform_info()["series"] if series == "trusty": return None - config_allow_beta = util.is_config_value_true( - config=self.cfg.cfg, path_to_value="features.allow_beta" - ) - if config_allow_beta or self.is_beta is False: + + if self.valid_service: if util.is_lts(series): return "never" return None @@ -55,10 +53,8 @@ series = util.get_platform_info()["series"] if series == "trusty": return False - config_allow_beta = util.is_config_value_true( - config=self.cfg.cfg, path_to_value="features.allow_beta" - ) - if config_allow_beta or self.is_beta is False: + + if self.valid_service: return util.is_lts(series) return False diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/fips.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/fips.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/fips.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/fips.py 2021-05-27 19:05:18.000000000 +0000 @@ -241,7 +241,7 @@ name = "fips" title = "FIPS" - description = "NIST-certified FIPS modules" + description = "NIST-certified core packages" origin = "UbuntuFIPS" fips_pro_package_holds = [ @@ -272,6 +272,13 @@ fips_update.application_status()[0] == enabled_status ) + services_once_enabled = ( + self.cfg.read_cache("services-once-enabled") or {} + ) + fips_updates_once_enabled = services_once_enabled.get( + fips_update.name, False + ) + return static_affordances + ( ( "Cannot enable {} when {} is enabled.".format( @@ -280,6 +287,13 @@ lambda: is_fips_update_enabled, False, ), + ( + "Cannot enable {} because {} was once enabled.".format( + self.title, fips_update.title + ), + lambda: fips_updates_once_enabled, + False, + ), ) @property @@ -328,10 +342,13 @@ ) super().setup_apt_config() - def enable(self, *, silent_if_inapplicable: bool = False) -> bool: - if super().enable(silent_if_inapplicable=silent_if_inapplicable): + def _perform_enable(self, *, silent_if_inapplicable: bool = False) -> bool: + if super()._perform_enable( + silent_if_inapplicable=silent_if_inapplicable + ): self.cfg.remove_notice("", status.MESSAGE_FIPS_INSTALL_OUT_OF_DATE) return True + return False @@ -340,7 +357,7 @@ name = "fips-updates" title = "FIPS Updates" origin = "UbuntuFIPSUpdates" - description = "Uncertified security updates to FIPS modules" + description = "NIST-certified core packages with priority security updates" @property def messaging( @@ -366,3 +383,17 @@ ) ], } + + def enable(self, *, silent_if_inapplicable: bool = False) -> bool: + if super().enable(silent_if_inapplicable=silent_if_inapplicable): + services_once_enabled = ( + self.cfg.read_cache("services-once-enabled") or {} + ) + services_once_enabled.update({self.name: True}) + self.cfg.write_cache( + key="services-once-enabled", content=services_once_enabled + ) + + return True + + return False diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/__init__.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/__init__.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/__init__.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/__init__.py 2021-05-27 19:05:18.000000000 +0000 @@ -5,8 +5,11 @@ from uaclient.entitlements import fips from uaclient.entitlements.livepatch import LivepatchEntitlement +from uaclient.config import UAConfig +from uaclient.util import is_config_value_true + try: - from typing import cast, Dict, List, Type # noqa: F401 + from typing import cast, Dict, List, Type, Optional # noqa: F401 except ImportError: # typing isn't available on trusty, so ignore its absence def cast(_, x): # type: ignore @@ -29,11 +32,22 @@ ) # type: Dict[str, Type[UAEntitlement]] -ALL_ENTITLEMENTS_STR = ", ".join(sorted(ENTITLEMENT_CLASS_BY_NAME.keys())) -RELEASED_ENTITLEMENTS_STR = ", ".join( - [ - k - for k, cls in sorted(ENTITLEMENT_CLASS_BY_NAME.items()) - if not cls.is_beta - ] -) # type: str +def valid_services(allow_beta: bool = False) -> "List[str]": + """Return a list of valid (non-beta) services. + + @param allow_beta: if we should allow beta services to be marked as valid + """ + cfg = UAConfig() + allow_beta_cfg = is_config_value_true(cfg.cfg, "features.allow_beta") + allow_beta |= allow_beta_cfg + + if allow_beta: + return sorted(ENTITLEMENT_CLASS_BY_NAME.keys()) + + return sorted( + [ + ent_name + for ent_name, ent_cls in ENTITLEMENT_CLASS_BY_NAME.items() + if not ent_cls.is_beta + ] + ) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/livepatch.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/livepatch.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/livepatch.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/livepatch.py 2021-05-27 19:05:18.000000000 +0000 @@ -1,10 +1,12 @@ import logging +import re from uaclient.entitlements import base from uaclient import apt, exceptions, status from uaclient import util from uaclient.status import ApplicationStatus + SNAP_CMD = "/usr/bin/snap" SNAP_INSTALL_RETRIES = [0.5, 1.0, 5.0] LIVEPATCH_RETRIES = [0.5, 1.0] @@ -65,7 +67,7 @@ ), ) - def enable(self, *, silent_if_inapplicable: bool = False) -> bool: + def _perform_enable(self, *, silent_if_inapplicable: bool = False) -> bool: """Enable specific entitlement. :param silent_if_inapplicable: @@ -74,8 +76,6 @@ @return: True on success, False otherwise. """ - if not self.can_enable(silent=silent_if_inapplicable): - return False if not util.which("/snap/bin/canonical-livepatch"): if not util.which(SNAP_CMD): print("Installing snapd") @@ -100,9 +100,19 @@ "/usr/bin/snap is present but snapd is not installed;" " cannot enable {}".format(self.title) ) - util.subp( - [SNAP_CMD, "wait", "system", "seed.loaded"], capture=True - ) + + try: + util.subp( + [SNAP_CMD, "wait", "system", "seed.loaded"], capture=True + ) + except util.ProcessExecutionError as e: + if re.search(r"unknown command .*wait", str(e).lower()): + logging.warning( + status.MESSAGE_SNAPD_DOES_NOT_HAVE_WAIT_CMD + ) + else: + raise + print("Installing canonical-livepatch snap") try: util.subp( diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/repo.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/repo.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/repo.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/repo.py 2021-05-27 19:05:18.000000000 +0000 @@ -52,14 +52,6 @@ def disable_apt_auth_only(self) -> bool: return False # Set True on ESM to only remove apt auth - # Any custom messages to emit to the console or callables which are - # handled at pre_enable, pre_disable, pre_install or post_enable stages - @property - def messaging( - self - ) -> "Dict[str, List[Union[str, Tuple[Callable, Dict]]]]": - return {} - @property def packages(self) -> "List[str]": """debs to install on enablement""" @@ -94,7 +86,7 @@ ) ) - def enable(self, *, silent_if_inapplicable: bool = False) -> bool: + def _perform_enable(self, *, silent_if_inapplicable: bool = False) -> bool: """Enable specific entitlement. :param silent_if_inapplicable: @@ -104,28 +96,19 @@ @return: True on success, False otherwise. @raises: UserFacingError on failure to install suggested packages """ - msg_ops = self.messaging.get("pre_enable", []) - if not handle_message_operations(msg_ops): - return False - if not self.can_enable(silent=silent_if_inapplicable): - return False self.setup_apt_config() if self.packages: msg_ops = self.messaging.get("pre_install", []) - if not handle_message_operations(msg_ops): + if not util.handle_message_operations(msg_ops): return False self.install_packages() print(status.MESSAGE_ENABLED_TMPL.format(title=self.title)) - msg_ops = self.messaging.get("post_enable", []) - if not handle_message_operations(msg_ops): - return False - self.check_for_reboot_msg(operation="install") return True def disable(self, silent=False): msg_ops = self.messaging.get("pre_disable", []) - if not handle_message_operations(msg_ops): + if not util.handle_message_operations(msg_ops): return False if not self.can_disable(silent): return False @@ -133,7 +116,7 @@ self.remove_packages() self._cleanup() msg_ops = self.messaging.get("post_disable", []) - if not handle_message_operations(msg_ops): + if not util.handle_message_operations(msg_ops): return False self.check_for_reboot_msg(operation="disable operation") return True @@ -423,25 +406,3 @@ apt.run_apt_command( ["apt-get", "update"], status.MESSAGE_APT_UPDATE_FAILED ) - - -def handle_message_operations( - msg_ops: "List[Union[str, Tuple[Callable, Dict]]]" -) -> bool: - """Emit messages to the console for user interaction - - :param msg_op: A list of strings or tuples. Any string items are printed. - Any tuples will contain a callable and a dict of args to pass to the - callable. Callables are expected to return True on success and - False upon failure. - - :return: True upon success, False on failure. - """ - for msg_op in msg_ops: - if isinstance(msg_op, str): - print(msg_op) - else: # Then we are a callable and dict of args - functor, args = msg_op - if not functor(**args): - return False - return True diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/conftest.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/conftest.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/conftest.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/conftest.py 2021-05-27 19:05:18.000000000 +0000 @@ -97,24 +97,32 @@ directives: "Dict[str, Any]" = None, obligations: "Dict[str, Any]" = None, entitled: bool = True, + allow_beta: bool = False, assume_yes: "Optional[bool]" = None, suites: "List[str]" = None, - additional_packages: "List[str]" = None + additional_packages: "List[str]" = None, + services_once_enabled: "Dict[str, bool]" = None, + cfg: "Optional[config.UAConfig]" = None ): - cfg = config.UAConfig(cfg={"data_dir": tmpdir.strpath}) - cfg.write_cache( - "machine-token", - machine_token( - cls.name, - affordances=affordances, - directives=directives, - obligations=obligations, - entitled=entitled, - suites=suites, - additional_packages=additional_packages, - ), - ) - args = {} + if not cfg: + cfg = config.UAConfig(cfg={"data_dir": tmpdir.strpath}) + cfg.write_cache( + "machine-token", + machine_token( + cls.name, + affordances=affordances, + directives=directives, + obligations=obligations, + entitled=entitled, + suites=suites, + additional_packages=additional_packages, + ), + ) + + if services_once_enabled: + cfg.write_cache("services-once-enabled", services_once_enabled) + + args = {"allow_beta": allow_beta} if assume_yes is not None: args["assume_yes"] = assume_yes return cls(cfg, **args) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_base.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_base.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_base.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_base.py 2021-05-27 19:05:18.000000000 +0000 @@ -11,7 +11,7 @@ from uaclient.status import ContractStatus try: - from typing import Tuple # noqa + from typing import Dict, Optional, Tuple # noqa except ImportError: pass @@ -29,8 +29,9 @@ enable=None, applicability_status=None, application_status=None, + allow_beta=False, ): - super().__init__(cfg) + super().__init__(cfg, allow_beta=allow_beta) self._disable = disable self._enable = enable self._applicability_status = applicability_status @@ -43,7 +44,7 @@ ) return self._disable - def enable(self, silent_if_inapplicable: bool = False): + def _perform_enable(self, silent_if_inapplicable: bool = False): return self._enable def applicability_status(self): @@ -59,7 +60,9 @@ *, entitled: bool, applicability_status: "Tuple[status.ApplicabilityStatus, str]" = None, - application_status: "Tuple[status.ApplicationStatus, str]" = None + application_status: "Tuple[status.ApplicationStatus, str]" = None, + feature_overrides: "Optional[Dict[str, str]]" = None, + allow_beta: bool = False ) -> ConcreteTestEntitlement: cfg = config.UAConfig(cfg={"data_dir": tmpdir.strpath}) machineToken = { @@ -76,10 +79,15 @@ }, } cfg.write_cache("machine-token", machineToken) + + if feature_overrides: + cfg.cfg.update({"features": feature_overrides}) + return ConcreteTestEntitlement( cfg, applicability_status=applicability_status, application_status=application_status, + allow_beta=allow_beta, ) return factory @@ -92,8 +100,8 @@ base.UAEntitlement() expected_msg = ( "Can't instantiate abstract class UAEntitlement with abstract" - " methods application_status, description, disable, enable, name," - " title" + " methods _perform_enable, application_status, description," + " disable, name, title" ) assert expected_msg == str(excinfo.value) @@ -151,7 +159,9 @@ kwargs = {} if silent is not None: kwargs["silent"] = silent - assert not entitlement.can_enable(**kwargs) + can_enable, reason = entitlement.can_enable(**kwargs) + assert not can_enable + assert reason == status.CanEnableFailureReason.NOT_ENTITLED expected_stdout = ( "This subscription is not entitled to Test Concrete Entitlement\n" @@ -176,7 +186,7 @@ ent = concrete_entitlement_factory(entitled=False) with mock.patch.object(ent, "is_access_expired", return_value=True): - assert not ent.can_enable() + assert not ent.can_enable()[0] assert [ mock.call(ent.cfg) @@ -199,7 +209,9 @@ kwargs = {} if silent is not None: kwargs["silent"] = silent - assert not entitlement.can_enable(**kwargs) + can_enable, reason = entitlement.can_enable(**kwargs) + assert not can_enable + assert reason == status.CanEnableFailureReason.ALREADY_ENABLED expected_stdout = ( "Test Concrete Entitlement is already enabled.\n" @@ -227,7 +239,9 @@ kwargs = {} if silent is not None: kwargs["silent"] = silent - assert not entitlement.can_enable(**kwargs) + can_enable, reason = entitlement.can_enable(**kwargs) + assert not can_enable + assert reason == status.CanEnableFailureReason.INAPPLICABLE expected_stdout = "msg\n" if silent: @@ -249,11 +263,38 @@ kwargs = {} if silent is not None: kwargs["silent"] = silent - assert entitlement.can_enable(**kwargs) + can_enable, reason = entitlement.can_enable(**kwargs) + assert can_enable + assert reason is None stdout, _ = capsys.readouterr() assert "" == stdout + @pytest.mark.parametrize("is_beta", (True, False)) + @pytest.mark.parametrize("allow_beta", (True, False)) + @pytest.mark.parametrize("allow_beta_cfg", (True, False)) + def test_can_enable_on_beta_service( + self, allow_beta_cfg, allow_beta, is_beta, concrete_entitlement_factory + ): + + feature_overrides = {"allow_beta": allow_beta_cfg} + entitlement = concrete_entitlement_factory( + entitled=True, + applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""), + application_status=(status.ApplicationStatus.DISABLED, ""), + feature_overrides=feature_overrides, + allow_beta=allow_beta, + ) + entitlement.is_beta = is_beta + can_enable, reason = entitlement.can_enable() + + if not is_beta or allow_beta or allow_beta_cfg: + assert can_enable + assert reason is None + else: + assert not can_enable + assert reason == status.CanEnableFailureReason.IS_BETA + def test_contract_status_entitled(self, concrete_entitlement_factory): """The contract_status returns ENTITLED when entitlement enabled.""" entitlement = concrete_entitlement_factory(entitled=True) @@ -282,8 +323,17 @@ assert 0 == m_can_disable.call_count @pytest.mark.parametrize( - "block_disable_on_enable,assume_yes", - ((True, True), (False, False), (True, False), (False, True)), + "block_disable_on_enable,allow_disable,assume_yes", + ( + (True, True, True), + (True, False, True), + (False, True, False), + (False, False, False), + (True, True, False), + (True, False, False), + (False, True, True), + (False, False, True), + ), ) @mock.patch("uaclient.util.is_config_value_true") @mock.patch("uaclient.util.prompt_for_confirmation") @@ -292,6 +342,7 @@ m_prompt, m_is_config_value_true, block_disable_on_enable, + allow_disable, assume_yes, concrete_entitlement_factory, ): @@ -320,15 +371,21 @@ with mock.patch.object( ent, "ENTITLEMENT_CLASS_BY_NAME", {"test": m_entitlement_cls} ): - ret = base_ent.can_enable() + ret, reason = base_ent.can_enable(allow_disable=allow_disable) + + expected_prompt_call = 1 + if block_disable_on_enable or not allow_disable: + expected_prompt_call = 0 - expected_prompt_call = 0 if block_disable_on_enable else 1 expected_ret = False - if assume_yes and not block_disable_on_enable: + expected_reason = status.CanEnableFailureReason.INCOMPATIBLE_SERVICE + if assume_yes and not block_disable_on_enable and allow_disable: expected_ret = True + expected_reason = None expected_disable_call = 1 if expected_ret else 0 assert ret == expected_ret + assert reason == expected_reason assert m_prompt.call_count == expected_prompt_call assert m_is_config_value_true.call_count == 1 assert m_entitlement_obj.disable.call_count == expected_disable_call @@ -431,6 +488,45 @@ mock.ANY, ) == entitlement.application_status() + @pytest.mark.parametrize( + "orig_access,delta", + ( + ( + { + "resourceToken": "test", + "entitlement": { + "entitled": True, + "obligations": {"enableByDefault": False}, + }, + }, + { + "entitlement": { + "entitled": True, + "obligations": {"enableByDefault": True}, + } + }, + ), + ), + ) + def test_process_contract_deltas_enable_beta_if_enabled_by_default_turned( + self, concrete_entitlement_factory, orig_access, delta + ): + """Disable when deltas transition from active to unentitled.""" + entitlement = concrete_entitlement_factory( + entitled=True, + applicability_status=(status.ApplicabilityStatus.APPLICABLE, ""), + application_status=(status.ApplicationStatus.DISABLED, ""), + ) + entitlement.is_beta = True + assert not entitlement.allow_beta + with mock.patch.object(entitlement, "enable") as m_enable: + entitlement.process_contract_deltas( + orig_access, delta, allow_enable=True + ) + assert 1 == m_enable.call_count + + assert entitlement.allow_beta + class TestUaEntitlementUserFacingStatus: def test_inapplicable_when_not_applicable( diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_cc.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_cc.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_cc.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_cc.py 2021-05-27 19:05:18.000000000 +0000 @@ -92,12 +92,12 @@ m_platform_info.return_value = PLATFORM_INFO_SUPPORTED cfg = config.UAConfig(cfg={"data_dir": tmpdir.strpath}) cfg.write_cache("machine-token", CC_MACHINE_TOKEN) - entitlement = CommonCriteriaEntitlement(cfg) + entitlement = CommonCriteriaEntitlement(cfg, allow_beta=True) uf_status, uf_status_details = entitlement.user_facing_status() assert status.UserFacingStatus.INACTIVE == uf_status details = "{} is not configured".format(entitlement.title) assert details == uf_status_details - assert True is entitlement.can_enable() + assert (True, None) == entitlement.can_enable() assert ("", "") == capsys.readouterr() @@ -145,7 +145,7 @@ m_platform_info.side_effect = fake_platform cfg = config.UAConfig(cfg={"data_dir": tmpdir.strpath}) cfg.write_cache("machine-token", CC_MACHINE_TOKEN) - entitlement = CommonCriteriaEntitlement(cfg) + entitlement = CommonCriteriaEntitlement(cfg, allow_beta=True) with mock.patch("uaclient.apt.add_auth_apt_repo") as m_add_apt: with mock.patch("uaclient.apt.add_ppa_pinning") as m_add_pin: diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_cis.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_cis.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_cis.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_cis.py 2021-05-27 19:05:18.000000000 +0000 @@ -6,7 +6,7 @@ from uaclient import apt from uaclient import status -from uaclient.entitlements.cis import CISEntitlement +from uaclient.entitlements.cis import CISEntitlement, CIS_DOCS_URL M_REPOPATH = "uaclient.entitlements.repo." @@ -14,7 +14,9 @@ @pytest.fixture def entitlement(entitlement_factory): - return entitlement_factory(CISEntitlement, additional_packages=["pkg1"]) + return entitlement_factory( + CISEntitlement, allow_beta=True, additional_packages=["pkg1"] + ) class TestCISEntitlementCanEnable: @@ -105,5 +107,6 @@ "Updating package lists\n" "Installing CIS Audit packages\n" "CIS Audit enabled\n" + "Visit {} to learn how to use CIS\n".format(CIS_DOCS_URL) ) assert (expected_stdout, "") == capsys.readouterr() diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_entitlements.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_entitlements.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_entitlements.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_entitlements.py 2021-05-27 19:05:18.000000000 +0000 @@ -0,0 +1,28 @@ +"""Tests related to uaclient.entitlement.__init__ module.""" +import mock +import pytest + +from uaclient import entitlements + + +class TestValidServices: + @pytest.mark.parametrize("allow_beta", ((True), (False))) + @pytest.mark.parametrize("is_beta", ((True), (False))) + @mock.patch("uaclient.entitlements.is_config_value_true") + def test_valid_services(self, m_is_config_value, is_beta, allow_beta): + m_is_config_value.return_value = allow_beta + m_cls_1 = mock.MagicMock() + type(m_cls_1).is_beta = mock.PropertyMock(return_value=False) + + m_cls_2 = mock.MagicMock() + type(m_cls_2).is_beta = mock.PropertyMock(return_value=is_beta) + ents_dict = {"ent1": m_cls_1, "ent2": m_cls_2} + + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", ents_dict + ): + expected_services = ["ent1"] + if allow_beta or not is_beta: + expected_services.append("ent2") + + assert expected_services == entitlements.valid_services() diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_esm.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_esm.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_esm.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_esm.py 2021-05-27 19:05:18.000000000 +0000 @@ -70,8 +70,10 @@ ) @mock.patch("uaclient.util.is_lts") @mock.patch("uaclient.entitlements.esm.util.get_platform_info") + @mock.patch("uaclient.entitlements.UAConfig") def test_esm_apps_repo_pin_priority_never_on_on_lts( self, + m_cfg, m_get_platform_info, m_is_lts, series, @@ -94,10 +96,12 @@ cfg = FakeConfig.for_attached_machine() if cfg_allow_beta: cfg.override_features({"allow_beta": cfg_allow_beta}) + m_cfg.return_value = cfg inst = ESMAppsEntitlement(cfg) - inst.is_beta = is_beta - assert repo_pin_priority == inst.repo_pin_priority + with mock.patch.object(ESMAppsEntitlement, "is_beta", is_beta): + assert repo_pin_priority == inst.repo_pin_priority + is_lts_calls = [] if series != "trusty": if cfg_allow_beta or not is_beta: @@ -146,8 +150,10 @@ ) @mock.patch("uaclient.util.is_lts") @mock.patch("uaclient.entitlements.esm.util.get_platform_info") + @mock.patch("uaclient.entitlements.UAConfig") def test_esm_apps_disable_apt_auth_only_is_true_on_lts( self, + m_cfg, m_get_platform_info, m_is_lts, series, @@ -162,10 +168,13 @@ cfg = FakeConfig.for_attached_machine() if cfg_allow_beta: cfg.override_features({"allow_beta": cfg_allow_beta}) + m_cfg.return_value = cfg inst = ESMAppsEntitlement(cfg) - inst.is_beta = is_beta - assert disable_apt_auth_only is inst.disable_apt_auth_only + with mock.patch.object(ESMAppsEntitlement, "is_beta", is_beta): + print(is_beta, cfg_allow_beta) + print(inst.valid_service) + assert disable_apt_auth_only is inst.disable_apt_auth_only is_lts_calls = [] if series != "trusty": if cfg_allow_beta or not is_beta: @@ -224,7 +233,7 @@ mock.patch.object(type(entitlement), "packages", m_packages) ) - m_can_enable.return_value = True + m_can_enable.return_value = (True, None) assert True is entitlement.enable() @@ -325,7 +334,7 @@ mock.patch("uaclient.apt.os.unlink") ) - m_can_enable.return_value = True + m_can_enable.return_value = (True, None) with pytest.raises(exceptions.UserFacingError) as excinfo: entitlement.enable() diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_fips.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_fips.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_fips.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_fips.py 2021-05-27 19:05:18.000000000 +0000 @@ -122,7 +122,7 @@ "application_status", return_value=(status.ApplicationStatus.DISABLED, ""), ): - assert True is entitlement.can_enable() + assert (True, None) == entitlement.can_enable() assert ("", "") == capsys.readouterr() @@ -144,7 +144,7 @@ mock.patch.object(entitlement, "can_enable") ) stack.enter_context( - mock.patch(M_REPOPATH + "handle_message_operations") + mock.patch("uaclient.util.handle_message_operations") ) stack.enter_context( mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) @@ -157,7 +157,7 @@ mock.patch.object(type(entitlement), "packages", m_packages) ) - m_can_enable.return_value = True + m_can_enable.return_value = (True, None) assert True is entitlement.enable() @@ -233,7 +233,7 @@ (False, []), ], ) - @mock.patch("uaclient.entitlements.repo.RepoEntitlement.enable") + @mock.patch("uaclient.entitlements.repo.RepoEntitlement._perform_enable") @mock.patch("uaclient.config.UAConfig.remove_notice") def test_enable_removes_out_of_date_notice_on_success( self, @@ -245,7 +245,7 @@ ): m_repo_enable.return_value = repo_enable_return_value fips_entitlement = entitlement_factory(FIPSEntitlement) - assert repo_enable_return_value is fips_entitlement.enable() + assert repo_enable_return_value is fips_entitlement._perform_enable() assert expected_remove_notice_calls == m_remove_notice.call_args_list @mock.patch( @@ -255,8 +255,10 @@ self, m_platform_info, entitlement ): """When can_enable is false enable returns false and noops.""" - with mock.patch.object(entitlement, "can_enable", return_value=False): - with mock.patch(M_REPOPATH + "handle_message_operations"): + with mock.patch.object( + entitlement, "can_enable", return_value=(False, None) + ): + with mock.patch("uaclient.util.handle_message_operations"): assert False is entitlement.enable() assert 0 == m_platform_info.call_count @@ -270,8 +272,10 @@ """When directives do not contain suites returns false.""" entitlement = fips_entitlement_factory(suites=[]) - with mock.patch.object(entitlement, "can_enable", return_value=True): - with mock.patch(M_REPOPATH + "handle_message_operations"): + with mock.patch.object( + entitlement, "can_enable", return_value=(True, None) + ): + with mock.patch("uaclient.util.handle_message_operations"): with pytest.raises(exceptions.UserFacingError) as excinfo: entitlement.enable() error_msg = "Empty {} apt suites directive from {}".format( @@ -291,9 +295,13 @@ m_add_pinning = stack.enter_context( mock.patch("uaclient.apt.add_ppa_pinning") ) - stack.enter_context(mock.patch.object(entitlement, "can_enable")) stack.enter_context( - mock.patch(M_REPOPATH + "handle_message_operations") + mock.patch.object( + entitlement, "can_enable", return_value=(True, None) + ) + ) + stack.enter_context( + mock.patch("uaclient.util.handle_message_operations") ) m_remove_apt_config = stack.enter_context( mock.patch.object(entitlement, "remove_apt_config") @@ -330,10 +338,12 @@ mock.patch("uaclient.util.subp", side_effect=fake_subp) ) stack.enter_context( - mock.patch.object(entitlement, "can_enable", return_value=True) + mock.patch.object( + entitlement, "can_enable", return_value=(True, None) + ) ) stack.enter_context( - mock.patch(M_REPOPATH + "handle_message_operations") + mock.patch("uaclient.util.handle_message_operations") ) stack.enter_context( mock.patch.object( @@ -361,7 +371,7 @@ @mock.patch("uaclient.util.get_platform_info") @mock.patch("uaclient.util.is_config_value_true", return_value=False) @mock.patch("uaclient.util.prompt_for_confirmation", return_value=False) - @mock.patch("uaclient.entitlements.repo.handle_message_operations") + @mock.patch("uaclient.util.handle_message_operations") @mock.patch("uaclient.util.is_container", return_value=False) def test_enable_fails_when_livepatch_service_is_enabled( self, @@ -398,7 +408,7 @@ ) assert expected_msg.strip() in fake_stdout.getvalue().strip() - @mock.patch("uaclient.entitlements.repo.handle_message_operations") + @mock.patch("uaclient.util.handle_message_operations") @mock.patch( M_LIVEPATCH_PATH + "application_status", return_value=((status.ApplicationStatus.DISABLED, "")), @@ -433,9 +443,40 @@ expected_msg = "Cannot enable FIPS when FIPS Updates is enabled." assert expected_msg.strip() == fake_stdout.getvalue().strip() + @mock.patch("uaclient.util.handle_message_operations") + @mock.patch( + M_LIVEPATCH_PATH + "application_status", + return_value=((status.ApplicationStatus.DISABLED, "")), + ) + @mock.patch("uaclient.util.is_container", return_value=False) + def test_enable_fails_when_fips_updates_service_once_enabled( + self, + m_is_container, + m_livepatch, + m_handle_message_op, + entitlement_factory, + ): + m_handle_message_op.return_value = True + fips_entitlement = entitlement_factory( + FIPSEntitlement, services_once_enabled={"fips-updates": True} + ) + + with mock.patch.object( + fips_entitlement, "_allow_fips_on_cloud_instance" + ) as m_allow_fips_on_cloud: + m_allow_fips_on_cloud.return_value = True + fake_stdout = io.StringIO() + with contextlib.redirect_stdout(fake_stdout): + fips_entitlement.enable() + + expected_msg = ( + "Cannot enable FIPS because FIPS Updates was once enabled." + ) + assert expected_msg.strip() == fake_stdout.getvalue().strip() + @mock.patch("uaclient.util.get_platform_info") @mock.patch("uaclient.entitlements.fips.get_cloud_type") - @mock.patch("uaclient.entitlements.repo.handle_message_operations") + @mock.patch("uaclient.util.handle_message_operations") @mock.patch("uaclient.util.is_container", return_value=False) def test_enable_fails_when_on_xenial_cloud_instance( self, @@ -465,7 +506,7 @@ @mock.patch("uaclient.util.get_platform_info") @mock.patch("uaclient.util.is_config_value_true", return_value=False) @mock.patch("uaclient.entitlements.fips.get_cloud_type") - @mock.patch("uaclient.entitlements.repo.handle_message_operations") + @mock.patch("uaclient.util.handle_message_operations") @mock.patch("uaclient.util.is_container", return_value=False) def test_enable_fails_when_on_gcp_instance_with_default_fips( self, @@ -624,7 +665,7 @@ assert exc_info.value.msg.strip() == expected_msg -@mock.patch(M_REPOPATH + "handle_message_operations", return_value=True) +@mock.patch("uaclient.util.handle_message_operations", return_value=True) @mock.patch("uaclient.util.should_reboot", return_value=True) @mock.patch( "uaclient.util.get_platform_info", return_value={"series": "xenial"} @@ -936,3 +977,34 @@ ] else: assert packages == additional_packages + + +class TestFIPSUpdatesEntitlementEnable: + @pytest.mark.parametrize("enable_ret", ((True), (False))) + @mock.patch("uaclient.entitlements.fips.FIPSCommonEntitlement.enable") + def test_fips_updates_enable_write_service_once_enable_file( + self, m_enable, enable_ret, entitlement_factory + ): + m_enable.return_value = enable_ret + m_write_cache = mock.MagicMock() + m_read_cache = mock.MagicMock() + m_read_cache.return_value = {} + + cfg = mock.MagicMock() + cfg.read_cache = m_read_cache + cfg.write_cache = m_write_cache + + fips_updates_ent = entitlement_factory(FIPSUpdatesEntitlement, cfg=cfg) + assert fips_updates_ent.enable() == enable_ret + + if enable_ret: + assert 1 == m_read_cache.call_count + assert 1 == m_write_cache.call_count + assert [ + mock.call( + key="services-once-enabled", content={"fips-updates": True} + ) + ] == m_write_cache.call_args_list + else: + assert not m_read_cache.call_count + assert not m_write_cache.call_count diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_livepatch.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_livepatch.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_livepatch.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_livepatch.py 2021-05-27 19:05:18.000000000 +0000 @@ -202,7 +202,7 @@ with mock.patch("uaclient.util.is_container") as m_container: m_platform.return_value = supported_kernel m_container.return_value = False - assert entitlement.can_enable() + assert (True, None) == entitlement.can_enable() assert ("", "") == capsys.readouterr() assert [mock.call()] == m_container.call_args_list @@ -215,7 +215,10 @@ with mock.patch("uaclient.util.get_platform_info") as m_platform: m_platform.return_value = unsupported_min_kernel entitlement = LivepatchEntitlement(entitlement.cfg) - assert not entitlement.can_enable() + assert ( + False, + status.CanEnableFailureReason.INAPPLICABLE, + ) == entitlement.can_enable() msg = ( "Livepatch is not available for kernel 4.2.9-00-generic.\n" "Minimum kernel version required: 4.4.\n" @@ -231,7 +234,10 @@ with mock.patch("uaclient.util.get_platform_info") as m_platform: m_platform.return_value = unsupported_kernel entitlement = LivepatchEntitlement(entitlement.cfg) - assert not entitlement.can_enable() + assert ( + False, + status.CanEnableFailureReason.INAPPLICABLE, + ) == entitlement.can_enable() msg = ( "Livepatch is not available for kernel 4.4.0-140-notgeneric.\n" "Supported flavors are: generic, lowlatency.\n" @@ -264,9 +270,12 @@ m_platform.return_value = unsupported_kernel entitlement = LivepatchEntitlement(entitlement.cfg) if meets_min_version: - assert entitlement.can_enable() + assert (True, None) == entitlement.can_enable() else: - assert not entitlement.can_enable() + assert ( + False, + status.CanEnableFailureReason.INAPPLICABLE, + ) == entitlement.can_enable() if meets_min_version: msg = "" else: @@ -286,7 +295,10 @@ unsupported_kernel["arch"] = "ppc64le" with mock.patch("uaclient.util.get_platform_info") as m_platform: m_platform.return_value = unsupported_kernel - assert not entitlement.can_enable() + assert ( + False, + status.CanEnableFailureReason.INAPPLICABLE, + ) == entitlement.can_enable() msg = ( "Livepatch is not available for platform ppc64le.\n" "Supported platforms are: x86_64.\n" @@ -303,7 +315,10 @@ m_platform.return_value = unsupported_min_kernel m_is_container.return_value = True entitlement = LivepatchEntitlement(entitlement.cfg) - assert not entitlement.can_enable() + assert ( + False, + status.CanEnableFailureReason.INAPPLICABLE, + ) == entitlement.can_enable() msg = "Cannot install Livepatch on a container.\n" assert (msg, "") == capsys.readouterr() @@ -452,7 +467,9 @@ ), ] - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=False) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(False, None) + ) def test_enable_false_when_can_enable_false( self, m_can_enable, caplog_text, capsys, entitlement ): @@ -463,7 +480,9 @@ assert [mock.call(silent=mock.ANY)] == m_can_enable.call_args_list @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=False) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(False, None) + ) def test_enable_passes_silent_if_inapplicable_through( self, m_can_enable, caplog_text, entitlement, silent_if_inapplicable ): @@ -483,7 +502,9 @@ @mock.patch("uaclient.apt.run_apt_command") @mock.patch("uaclient.util.which", return_value=False) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=True) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) + ) def test_enable_installs_snapd_and_livepatch_snap_when_absent( self, m_can_enable, @@ -538,7 +559,9 @@ "uaclient.util.which", side_effect=lambda cmd: cmd == "/usr/bin/snap" ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=True) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) + ) def test_enable_installs_only_livepatch_snap_when_absent_but_snapd_present( self, m_can_enable, @@ -575,7 +598,9 @@ "uaclient.util.which", side_effect=lambda cmd: cmd == "/usr/bin/snap" ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=True) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) + ) def test_enable_bails_if_snap_cmd_exists_but_snapd_pkg_not_installed( self, m_can_enable, m_app_status, m_which, m_subp, capsys, entitlement ): @@ -597,7 +622,9 @@ @mock.patch("uaclient.util.subp") @mock.patch("uaclient.util.which", return_value="/found/livepatch") @mock.patch(M_PATH + "LivepatchEntitlement.application_status") - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=True) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) + ) def test_enable_does_not_install_livepatch_snap_when_present( self, m_can_enable, @@ -619,7 +646,9 @@ @mock.patch("uaclient.util.subp") @mock.patch("uaclient.util.which", return_value="/found/livepatch") @mock.patch(M_PATH + "LivepatchEntitlement.application_status") - @mock.patch(M_PATH + "LivepatchEntitlement.can_enable", return_value=True) + @mock.patch( + M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) + ) def test_enable_does_not_disable_inactive_livepatch_snap_when_present( self, m_can_enable, @@ -658,7 +687,7 @@ ("FIPSUpdatesEntitlement", "FIPS Updates"), ), ) - @mock.patch("uaclient.entitlements.repo.handle_message_operations") + @mock.patch("uaclient.util.handle_message_operations") @mock.patch("uaclient.util.is_container", return_value=False) def test_enable_fails_when_blocking_service_is_enabled( self, @@ -686,6 +715,90 @@ ) assert expected_msg.strip() == fake_stdout.getvalue().strip() + @pytest.mark.parametrize("caplog_text", [logging.WARN], indirect=True) + @mock.patch("uaclient.util.which") + @mock.patch("uaclient.apt.get_installed_packages") + @mock.patch("uaclient.util.subp") + def test_enable_alerts_user_that_snapd_does_not_wait_command( + self, + m_subp, + m_installed_pkgs, + m_which, + entitlement, + capsys, + caplog_text, + ): + m_which.side_effect = [False, True] + m_installed_pkgs.return_value = ["snapd"] + stderr_msg = ( + "error: Unknown command `wait'. Please specify one command of: " + "abort, ack, buy, change, changes, connect, create-user, disable," + " disconnect, download, enable, find, help, install, interfaces, " + "known, list, login, logout, refresh, remove, run or try" + ) + + m_subp.side_effect = [ + ProcessExecutionError( + cmd="snapd wait system seed.loaded", + exit_code=-1, + stdout="", + stderr=stderr_msg, + ), + True, + ] + + fake_stdout = io.StringIO() + + with mock.patch.object(entitlement, "can_enable") as m_can_enable: + m_can_enable.return_value = (True, None) + with mock.patch.object( + entitlement, "setup_livepatch_config" + ) as m_setup_livepatch: + with contextlib.redirect_stdout(fake_stdout): + entitlement.enable() + + assert 1 == m_can_enable.call_count + assert 1 == m_setup_livepatch.call_count + + assert ( + "Installing canonical-livepatch snap" + in fake_stdout.getvalue().strip() + ) + + for msg in status.MESSAGE_SNAPD_DOES_NOT_HAVE_WAIT_CMD.split("\n"): + assert msg in caplog_text() + + @mock.patch("uaclient.util.which") + @mock.patch("uaclient.apt.get_installed_packages") + @mock.patch("uaclient.util.subp") + def test_enable_raise_exception_for_unexpected_error_on_snapd_wait( + self, m_subp, m_installed_pkgs, m_which, entitlement + ): + m_which.side_effect = [False, True] + m_installed_pkgs.return_value = ["snapd"] + stderr_msg = "test error" + + m_subp.side_effect = ProcessExecutionError( + cmd="snapd wait system seed.loaded", + exit_code=-1, + stdout="", + stderr=stderr_msg, + ) + + with mock.patch.object(entitlement, "can_enable") as m_can_enable: + m_can_enable.return_value = (True, None) + with mock.patch.object( + entitlement, "setup_livepatch_config" + ) as m_setup_livepatch: + with pytest.raises(ProcessExecutionError) as excinfo: + entitlement.enable() + + assert 1 == m_can_enable.call_count + assert 0 == m_setup_livepatch.call_count + + expected_msg = "test error" + assert expected_msg in str(excinfo) + class TestLivepatchApplicationStatus: @pytest.mark.parametrize("which_result", ((True), (False))) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_repo.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_repo.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/entitlements/tests/test_repo.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/entitlements/tests/test_repo.py 2021-05-27 19:05:18.000000000 +0000 @@ -6,10 +6,7 @@ from uaclient import apt from uaclient import config -from uaclient.entitlements.repo import ( - RepoEntitlement, - handle_message_operations, -) +from uaclient.entitlements.repo import RepoEntitlement from uaclient.entitlements.tests.conftest import machine_token from uaclient import exceptions from uaclient import status @@ -393,7 +390,9 @@ class TestRepoEnable: @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) - @mock.patch.object(RepoTestEntitlement, "can_enable", return_value=False) + @mock.patch.object( + RepoTestEntitlement, "can_enable", return_value=(False, None) + ) def test_enable_passes_silent_if_inapplicable_through( self, m_can_enable, caplog_text, tmpdir, silent_if_inapplicable ): @@ -416,7 +415,9 @@ (["msg1", (lambda: True, {}), "msg2"], "msg1\nmsg2\n", 1), ), ) - @mock.patch.object(RepoTestEntitlement, "can_enable", return_value=False) + @mock.patch.object( + RepoTestEntitlement, "can_enable", return_value=(False, None) + ) def test_enable_can_exit_on_pre_enable_messaging_hooks( self, m_can_enable, @@ -497,7 +498,9 @@ @mock.patch(M_PATH + "apt.add_auth_apt_repo") @mock.patch(M_PATH + "os.path.exists", return_value=True) @mock.patch(M_PATH + "util.get_platform_info") - @mock.patch.object(RepoTestEntitlement, "can_enable", return_value=True) + @mock.patch.object( + RepoTestEntitlement, "can_enable", return_value=(True, None) + ) def test_enable_calls_adds_apt_repo_and_calls_apt_update( self, m_can_enable, @@ -614,7 +617,7 @@ packages = ["fake_pkg", "and_another"] with mock.patch.object(entitlement, "setup_apt_config"): with mock.patch.object( - entitlement, "can_enable", return_value=True + entitlement, "can_enable", return_value=(True, None) ): with mock.patch.object( type(entitlement), "packages", packages @@ -1037,6 +1040,6 @@ def test_handle_message_operations_for_strings_and_callables( self, msg_ops, retval, output, capsys ): - assert retval is handle_message_operations(msg_ops) + assert retval is util.handle_message_operations(msg_ops) out, _err = capsys.readouterr() assert output == out diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/security.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/security.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/security.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/security.py 2021-05-27 19:05:18.000000000 +0000 @@ -741,7 +741,6 @@ width=PRINT_WRAP_WIDTH, subsequent_indent=" ", ) - return "{}\n{}".format(msg_header, pkg_status.status_message) @@ -767,11 +766,7 @@ if ent_status == status.UserFacingStatus.ACTIVE: return False - config_allow_beta = util.is_config_value_true( - config=cfg.cfg, path_to_value="features.allow_beta" - ) - - return all([ent.is_beta, not config_allow_beta]) + return ent.valid_service return False diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/status.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/status.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/status.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/status.py 2021-05-27 19:05:18.000000000 +0000 @@ -2,7 +2,7 @@ import sys import textwrap -from uaclient.defaults import BASE_ESM_URL, BASE_UA_URL, PRINT_WRAP_WIDTH +from uaclient.defaults import BASE_UA_URL, PRINT_WRAP_WIDTH try: from typing import Any, Dict, List, Optional, Tuple, Union # noqa: F401 @@ -100,6 +100,19 @@ UNAVAILABLE = "—" +@enum.unique +class CanEnableFailureReason(enum.Enum): + """ + An enum representing the reasons an entitlement can't be enabled. + """ + + NOT_ENTITLED = object() + ALREADY_ENABLED = object() + INAPPLICABLE = object() + IS_BETA = object() + INCOMPATIBLE_SERVICE = object() + + ESSENTIAL = "essential" STANDARD = "standard" ADVANCED = "advanced" @@ -186,6 +199,14 @@ LOG_CONNECTIVITY_ERROR_WITH_URL_TMPL = ( MESSAGE_CONNECTIVITY_ERROR + " Failed to access URL: {url}. {error}" ) +MESSAGE_SSL_VERIFICATION_ERROR_CA_CERTIFICATES = """\ +Failed to access URL: {url} +Cannot verify certificate of server +Please install "ca-certificates" and try again.""" +MESSAGE_SSL_VERIFICATION_ERROR_OPENSSL_CONFIG = """\ +Failed to access URL: {url} +Cannot verify certificate of server +Please check your openssl configuration.""" MESSAGE_NONROOT_USER = "This command must be run as root (try using sudo)." MESSAGE_ALREADY_DISABLED_TMPL = """\ {title} is not currently enabled\nSee: sudo ua status""" @@ -251,6 +272,11 @@ MESSAGE_LIVEPATCH_LTS_REBOOT_REQUIRED = ( "Livepatch support requires a system reboot across LTS upgrade." ) +MESSAGE_SNAPD_DOES_NOT_HAVE_WAIT_CMD = ( + "snapd does not have wait command.\n" + "Enabling Livepatch can fail under this scenario\n" + "Please, upgrade snapd if Livepatch enable fails and try again." +) MESSAGE_FIPS_INSTALL_OUT_OF_DATE = ( "This FIPS install is out of date, run: sudo ua enable fips" ) @@ -266,15 +292,14 @@ """ PROMPT_FIPS_PRE_ENABLE = ( """\ -Installation of additional packages are required to make this system FIPS -compliant. +This will install the FIPS core packages. """ + PROMPT_YES_NO ) PROMPT_FIPS_UPDATES_PRE_ENABLE = ( """\ -This system will NOT be considered FIPS certified, but will include security -and bug fixes to the FIPS packages. +This will install the FIPS core packages and will include priority updates +with security fixes. """ + PROMPT_YES_NO ) @@ -332,6 +357,9 @@ MESSAGE_ATTACH_SUCCESS_TMPL = """\ This machine is now attached to '{contract_name}' """ +MESSAGE_ATTACH_SUCCESS_NO_CONTRACT_NAME = """\ +This machine is now successfully attached' +""" MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL = """\ Cannot {operation} unknown service '{name}'. @@ -362,7 +390,7 @@ MESSAGE_INCOMPATIBLE_SERVICE = """\ {service_being_enabled} cannot be enabled with {incompatible_service}. Disable {incompatible_service} and proceed to enable {service_being_enabled}? \ -(y/N)""" +(y/N) """ MESSAGE_INCOMPATIBLE_SERVICE_STOPS_ENABLE = """\ Cannot enable {service_being_enabled} when {incompatible_service} is enabled. @@ -388,15 +416,13 @@ To install them, run this command as root (try using sudo)""" # MOTD and APT command messaging -MESSAGE_ANNOUNCE_ESM = """\ +MESSAGE_ANNOUNCE_ESM_TMPL = """\ * Introducing Extended Security Maintenance for Applications. Receive updates to over 30,000 software packages with your Ubuntu Advantage subscription. Free for personal use. {url} -""".format( - url=BASE_ESM_URL -) +""" MESSAGE_CONTRACT_EXPIRED_SOON_TMPL = """\ CAUTION: Your {title} service will expire in {remaining_days} days. @@ -406,7 +432,6 @@ MESSAGE_CONTRACT_EXPIRED_GRACE_PERIOD_TMPL = """\ CAUTION: Your {title} service expired on {expired_date}. - Renew UA subscription at {url} to ensure continued security coverage for your applications. Your grace period will expire in {remaining_days} days. @@ -422,8 +447,7 @@ MESSAGE_CONTRACT_EXPIRED_APT_PKGS_TMPL = """\ *Your {title} subscription has EXPIRED* -Enabling {title} service would provide security updates for following -packages: +Enabling {title} service would provide security updates for following packages: {pkg_names} {pkg_num} {name} security update(s) NOT APPLIED. Renew your UA services at {url} @@ -431,7 +455,6 @@ MESSAGE_DISABLED_MOTD_NO_PKGS_TMPL = """\ Enable {title} to receive additional future security updates. - See {url} or run: sudo ua status """ @@ -444,10 +467,10 @@ MESSAGE_DISABLED_APT_PKGS_TMPL = """\ -*The following packages could receive security updates with - {title} service enabled: +*The following packages could receive security updates \ +with {title} service enabled: {pkg_names} -Learn more about {title} service at {url} +Learn more about {title} service {eol_release}at {url} """ MESSAGE_UBUNTU_NO_WARRANTY = """\ @@ -548,12 +571,19 @@ ) ) content.append("\nEnable services with: ua enable ") - pairs = [ - ("Account", status["account"]), - ("Subscription", status["subscription"]), - ] + pairs = [] + + if status["account"]: + pairs.append(("Account", status["account"])) + + if status["subscription"]: + pairs.append(("Subscription", status["subscription"])) + if status["origin"] != "free": pairs.append(("Valid until", str(status["expires"]))) pairs.append(("Technical support level", colorize(tech_support_level))) - content.extend(get_section_column_content(column_data=pairs)) + + if pairs: + content.extend(get_section_column_content(column_data=pairs)) + return "\n".join(content) diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_disable.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_disable.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_disable.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_disable.py 2021-05-27 19:05:18.000000000 +0000 @@ -10,7 +10,7 @@ ALL_SERVICE_MSG = "\n".join( textwrap.wrap( - "Try " + entitlements.ALL_ENTITLEMENTS_STR + ".", + "Try " + ", ".join(entitlements.valid_services(allow_beta=True)) + ".", width=80, break_long_words=False, ) @@ -24,7 +24,7 @@ Arguments: service the name(s) of the Ubuntu Advantage services to disable One - of: esm-infra, fips, fips-updates, livepatch + of: cis, esm-infra, fips, fips-updates, livepatch Flags: -h, --help show this help message and exit @@ -98,9 +98,11 @@ assert len(entitlements_cls) == m_cfg.status.call_count @pytest.mark.parametrize("assume_yes", (True, False)) - @mock.patch("uaclient.cli.entitlements") + @mock.patch( + "uaclient.entitlements.is_config_value_true", return_value=False + ) def test_entitlements_not_found_disabled_and_enabled( - self, m_entitlements, _m_getuid, assume_yes, tmpdir + self, _m_is_config_value_true, _m_getuid, assume_yes, tmpdir ): expected_error_tmpl = status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL num_calls = 2 @@ -117,12 +119,7 @@ m_ent3_obj = m_ent3_cls.return_value m_ent3_obj.disable.return_value = True - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "ent2": m_ent2_cls, - "ent3": m_ent3_cls, - } - m_entitlements.ALL_ENTITLEMENTS_STR = "ent2, ent3" - m_entitlements.RELEASED_ENTITLEMENTS_STR = "ent2, ent3" + m_ents_dict = {"ent2": m_ent2_cls, "ent3": m_ent3_cls} m_cfg = mock.Mock() m_cfg.check_lock_info.return_value = (-1, "") @@ -131,8 +128,11 @@ args_mock.service = ["ent1", "ent2", "ent3"] args_mock.assume_yes = assume_yes - with pytest.raises(exceptions.UserFacingError) as err: - action_disable(args_mock, m_cfg) + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + with pytest.raises(exceptions.UserFacingError) as err: + action_disable(args_mock, m_cfg) assert ( expected_error_tmpl.format( diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_enable.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_enable.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_enable.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_enable.py 2021-05-27 19:05:18.000000000 +0000 @@ -5,7 +5,7 @@ import pytest -from uaclient.cli import _perform_enable, action_enable, main +from uaclient.cli import action_enable, main from uaclient import entitlements from uaclient import exceptions from uaclient import status @@ -18,7 +18,7 @@ Arguments: service the name(s) of the Ubuntu Advantage services to enable. One - of: esm-infra, fips, fips-updates, livepatch + of: cis, esm-infra, fips, fips-updates, livepatch Flags: -h, --help show this help message and exit @@ -116,7 +116,11 @@ action_enable(args, cfg) service_msg = "\n".join( textwrap.wrap( - "Try " + entitlements.ALL_ENTITLEMENTS_STR + ".", + ( + "Try " + + ", ".join(entitlements.valid_services(allow_beta=True)) + + "." + ), width=80, break_long_words=False, ) @@ -128,30 +132,24 @@ == err.value.msg ) - @pytest.mark.parametrize("beta_flag, beta_count", ((False, 1), (True, 0))) @pytest.mark.parametrize("assume_yes", (True, False)) @mock.patch("uaclient.contract.get_available_resources", return_value={}) - @mock.patch("uaclient.cli.entitlements") + @mock.patch("uaclient.entitlements.valid_services") def test_assume_yes_passed_to_service_init( self, - m_entitlements, + m_valid_services, _m_get_available_resources, m_request_updated_contract, m_getuid, assume_yes, - beta_flag, - beta_count, FakeConfig, ): """assume-yes parameter is passed to entitlement instantiation.""" m_getuid.return_value = 0 m_entitlement_cls = mock.MagicMock() - m_ent_is_beta = mock.PropertyMock(return_value=False) - type(m_entitlement_cls).is_beta = m_ent_is_beta - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "testitlement": m_entitlement_cls - } + m_ents_dict = {"testitlement": m_entitlement_cls} + m_valid_services.return_value = list(m_ents_dict.keys()) m_entitlement_obj = m_entitlement_cls.return_value m_entitlement_obj.enable.return_value = True @@ -159,26 +157,25 @@ args = mock.MagicMock() args.service = ["testitlement"] args.assume_yes = assume_yes - args.beta = beta_flag - action_enable(args, cfg) + args.beta = False + + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + action_enable(args, cfg) + assert [ - mock.call(cfg, assume_yes=assume_yes) + mock.call(cfg, assume_yes=assume_yes, allow_beta=False) ] == m_entitlement_cls.call_args_list - assert beta_count == m_ent_is_beta.call_count - @pytest.mark.parametrize("beta_flag, beta_count", ((False, 1), (True, 0))) @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) @mock.patch("uaclient.contract.get_available_resources", return_value={}) - @mock.patch("uaclient.cli.entitlements") def test_entitlements_not_found_disabled_and_enabled( self, - m_entitlements, _m_get_available_resources, _m_request_updated_contract, m_getuid, silent_if_inapplicable, - beta_flag, - beta_count, FakeConfig, ): m_getuid.return_value = 0 @@ -186,77 +183,86 @@ m_ent1_cls = mock.Mock() m_ent1_obj = m_ent1_cls.return_value + m_ent1_obj.can_enable.return_value = (True, None) m_ent1_obj.enable.return_value = False m_ent2_cls = mock.Mock() - m_ent2_is_beta = mock.PropertyMock(return_value=False) + m_ent2_is_beta = mock.PropertyMock(return_value=True) type(m_ent2_cls).is_beta = m_ent2_is_beta m_ent2_obj = m_ent2_cls.return_value + m_ent2_obj.can_enable.return_value = ( + False, + status.CanEnableFailureReason.IS_BETA, + ) m_ent2_obj.enable.return_value = False m_ent3_cls = mock.Mock() m_ent3_is_beta = mock.PropertyMock(return_value=False) type(m_ent3_cls).is_beta = m_ent3_is_beta m_ent3_obj = m_ent3_cls.return_value + m_ent3_obj.can_enable.return_value = (True, None) m_ent3_obj.enable.return_value = True - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "ent2": m_ent2_cls, - "ent3": m_ent3_cls, - } - m_entitlements.ALL_ENTITLEMENTS_STR = "ent2, ent3" - m_entitlements.RELEASED_ENTITLEMENTS_STR = "ent2, ent3" + m_ents_dict = {"ent2": m_ent2_cls, "ent3": m_ent3_cls} cfg = FakeConfig.for_attached_machine() assume_yes = False args_mock = mock.Mock() args_mock.service = ["ent1", "ent2", "ent3"] args_mock.assume_yes = assume_yes - args_mock.beta = beta_flag + args_mock.beta = False expected_msg = "One moment, checking your subscription first\n" - with pytest.raises(exceptions.UserFacingError) as err: - fake_stdout = io.StringIO() - with contextlib.redirect_stdout(fake_stdout): - action_enable(args_mock, cfg) - - assert ( - expected_error_tmpl.format( - operation="enable", - name="ent1", - service_msg="Try " + m_entitlements.ALL_ENTITLEMENTS_STR + ".", + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + with pytest.raises(exceptions.UserFacingError) as err: + fake_stdout = io.StringIO() + with contextlib.redirect_stdout(fake_stdout): + action_enable(args_mock, cfg) + + assert ( + expected_error_tmpl.format( + operation="enable", + name="ent1, ent2", + service_msg=( + "Try " + + ", ".join( + entitlements.valid_services(allow_beta=False) + ) + + "." + ), + ) + == err.value.msg ) - == err.value.msg - ) - assert expected_msg == fake_stdout.getvalue() + assert expected_msg == fake_stdout.getvalue() for m_ent_cls in [m_ent2_cls, m_ent3_cls]: assert [ - mock.call(cfg, assume_yes=assume_yes) + mock.call(cfg, assume_yes=assume_yes, allow_beta=False) ] == m_ent_cls.call_args_list - expected_enable_call = mock.call(silent_if_inapplicable=False) + expected_enable_call = mock.call() for m_ent in [m_ent2_obj, m_ent3_obj]: assert [expected_enable_call] == m_ent.enable.call_args_list assert 0 == m_ent1_obj.call_count - assert beta_count == m_ent2_is_beta.call_count - assert beta_count == m_ent3_is_beta.call_count - @pytest.mark.parametrize("beta_flag, beta_count", ((False, 1), (True, 0))) + @pytest.mark.parametrize("beta_flag", ((False), (True))) @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) @mock.patch("uaclient.contract.get_available_resources", return_value={}) - @mock.patch("uaclient.cli.entitlements") + @mock.patch( + "uaclient.entitlements.is_config_value_true", return_value=False + ) def test_entitlements_not_found_and_beta( self, - m_entitlements, + _m_is_config_value_true, _m_get_available_resources, _m_request_updated_contract, m_getuid, silent_if_inapplicable, beta_flag, - beta_count, FakeConfig, ): m_getuid.return_value = 0 @@ -264,24 +270,30 @@ m_ent1_cls = mock.Mock() m_ent1_obj = m_ent1_cls.return_value + m_ent1_obj.can_enable.return_value = (True, None) m_ent1_obj.enable.return_value = False m_ent2_cls = mock.Mock() m_ent2_is_beta = mock.PropertyMock(return_value=True) - type(m_ent2_cls).is_beta = m_ent2_is_beta + type(m_ent2_cls)._is_beta = m_ent2_is_beta m_ent2_obj = m_ent2_cls.return_value + if beta_flag: + m_ent2_obj.can_enable.return_value = (True, None) + else: + m_ent2_obj.can_enable.return_value = ( + False, + status.CanEnableFailureReason.IS_BETA, + ) m_ent2_obj.enable.return_value = False m_ent3_cls = mock.Mock() m_ent3_is_beta = mock.PropertyMock(return_value=False) - type(m_ent3_cls).is_beta = m_ent3_is_beta + type(m_ent3_cls)._is_beta = m_ent3_is_beta m_ent3_obj = m_ent3_cls.return_value + m_ent3_obj.can_enable.return_value = (True, None) m_ent3_obj.enable.return_value = True - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "ent2": m_ent2_cls, - "ent3": m_ent3_cls, - } + m_ents_dict = {"ent2": m_ent2_cls, "ent3": m_ent3_cls} cfg = FakeConfig.for_attached_machine() assume_yes = False @@ -291,50 +303,95 @@ args_mock.beta = beta_flag expected_msg = "One moment, checking your subscription first\n" - m_entitlements.RELEASED_ENTITLEMENTS_STR = "ent1, ent3" - m_entitlements.ALL_ENTITLEMENTS_STR = "ent1, ent2, ent3" not_found_name = "ent1" mock_ent_list = [m_ent3_cls] mock_obj_list = [m_ent3_obj] - if not beta_flag: - not_found_name += ", ent2" - ent_str = "Try " + m_entitlements.RELEASED_ENTITLEMENTS_STR + "." - else: - ent_str = "Try " + m_entitlements.ALL_ENTITLEMENTS_STR + "." - mock_ent_list.append(m_ent2_cls) - mock_obj_list.append(m_ent3_obj) - service_msg = "\n".join( - textwrap.wrap(ent_str, width=80, break_long_words=False) - ) - - with pytest.raises(exceptions.UserFacingError) as err: - fake_stdout = io.StringIO() - with contextlib.redirect_stdout(fake_stdout): - action_enable(args_mock, cfg) + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + service_names = entitlements.valid_services(allow_beta=beta_flag) + if not beta_flag: + not_found_name += ", ent2" + ent_str = "Try " + ", ".join(service_names) + "." + else: + ent_str = "Try " + ", ".join(service_names) + "." + mock_ent_list.append(m_ent2_cls) + mock_obj_list.append(m_ent3_obj) + service_msg = "\n".join( + textwrap.wrap(ent_str, width=80, break_long_words=False) + ) - assert ( - expected_error_tmpl.format( - operation="enable", - name=not_found_name, - service_msg=service_msg, + with pytest.raises(exceptions.UserFacingError) as err: + fake_stdout = io.StringIO() + with contextlib.redirect_stdout(fake_stdout): + action_enable(args_mock, cfg) + + assert ( + expected_error_tmpl.format( + operation="enable", + name=not_found_name, + service_msg=service_msg, + ) + == err.value.msg ) - == err.value.msg - ) - assert expected_msg == fake_stdout.getvalue() + assert expected_msg == fake_stdout.getvalue() for m_ent_cls in mock_ent_list: assert [ - mock.call(cfg, assume_yes=assume_yes) + mock.call(cfg, assume_yes=assume_yes, allow_beta=beta_flag) ] == m_ent_cls.call_args_list - expected_enable_call = mock.call(silent_if_inapplicable=False) + expected_enable_call = mock.call() for m_ent in mock_obj_list: assert [expected_enable_call] == m_ent.enable.call_args_list assert 0 == m_ent1_obj.call_count - assert beta_count == m_ent2_is_beta.call_count - assert beta_count == m_ent3_is_beta.call_count + + @mock.patch("uaclient.contract.get_available_resources", return_value={}) + @mock.patch( + "uaclient.entitlements.is_config_value_true", return_value=False + ) + def test_dont_print_any_result_when_enable_fails( + self, + _m_is_config_value_true, + _m_get_available_resources, + _m_request_updated_contract, + m_getuid, + FakeConfig, + ): + # When enable fails, the reason is printed by can_enable or + # enable. Their output is tested in their respective unit tests. + m_getuid.return_value = 0 + m_entitlement_cls = mock.Mock() + type(m_entitlement_cls).is_beta = mock.PropertyMock(return_value=False) + m_entitlement_obj = m_entitlement_cls.return_value + m_entitlement_obj.can_enable.return_value = ( + False, + status.CanEnableFailureReason.ALREADY_ENABLED, + ) + m_entitlement_obj.enable.return_value = False + + m_ents_dict = {"ent1": m_entitlement_cls} + + cfg = FakeConfig.for_attached_machine() + args_mock = mock.Mock() + args_mock.service = ["ent1"] + args_mock.assume_yes = False + args_mock.beta = False + + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + fake_stdout = io.StringIO() + with contextlib.redirect_stdout(fake_stdout): + action_enable(args_mock, cfg) + + print(fake_stdout.getvalue()) + assert ( + "One moment, checking your subscription first\n" + == fake_stdout.getvalue() + ) @pytest.mark.parametrize( "service, beta", @@ -357,10 +414,12 @@ action_enable(args, cfg) assert expected_msg == fake_stdout.getvalue() + + service_names = entitlements.valid_services(allow_beta=beta) if beta: - ent_str = "Try " + entitlements.ALL_ENTITLEMENTS_STR + "." + ent_str = "Try " + ", ".join(service_names) + "." else: - ent_str = "Try " + entitlements.RELEASED_ENTITLEMENTS_STR + "." + ent_str = "Try " + ", ".join(service_names) + "." service_msg = "\n".join( textwrap.wrap(ent_str, width=80, break_long_words=False) ) @@ -373,132 +432,48 @@ == err.value.msg ) - -class TestPerformEnable: - @mock.patch("uaclient.cli.entitlements") - def test_missing_entitlement_raises_keyerror(self, m_entitlements): - """We raise a KeyError on missing entitlements - - (This isn't a problem because any callers of _perform_enable should - already have rejected invalid names.) - """ - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = {} - - with pytest.raises(KeyError): - _perform_enable("entitlement", mock.Mock()) - - @pytest.mark.parametrize( - "allow_beta, beta_call_count", ((True, 0), (False, 1)) - ) - @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) + @pytest.mark.parametrize("allow_beta", ((True), (False))) @mock.patch("uaclient.contract.get_available_resources", return_value={}) - @mock.patch("uaclient.cli.entitlements") + @mock.patch( + "uaclient.entitlements.is_config_value_true", return_value=False + ) def test_entitlement_instantiated_and_enabled( self, - m_entitlements, + _m_is_config_value_true, _m_get_available_resources, - silent_if_inapplicable, + _m_request_updated_contract, + m_getuid, allow_beta, - beta_call_count, + FakeConfig, ): + m_getuid.return_value = 0 m_entitlement_cls = mock.Mock() - m_cfg = mock.Mock() - m_user_cfg = mock.PropertyMock(return_value={}) - type(m_cfg).cfg = m_user_cfg - m_is_beta = mock.PropertyMock(return_value=allow_beta) - type(m_entitlement_cls).is_beta = m_is_beta - - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "testitlement": m_entitlement_cls - } - - kwargs = {"allow_beta": allow_beta} - if silent_if_inapplicable is not None: - kwargs["silent_if_inapplicable"] = silent_if_inapplicable - ret = _perform_enable("testitlement", m_cfg, **kwargs) - - assert [ - mock.call(m_cfg, assume_yes=False) - ] == m_entitlement_cls.call_args_list - - m_entitlement = m_entitlement_cls.return_value - if silent_if_inapplicable: - expected_enable_call = mock.call(silent_if_inapplicable=True) - else: - expected_enable_call = mock.call(silent_if_inapplicable=False) - assert [expected_enable_call] == m_entitlement.enable.call_args_list - assert ret == m_entitlement.enable.return_value - - assert 1 == m_cfg.status.call_count - assert 1 == m_user_cfg.call_count - assert beta_call_count == m_is_beta.call_count + m_entitlement_obj = m_entitlement_cls.return_value + m_entitlement_obj.can_enable.return_value = (True, None) + m_entitlement_obj.enable.return_value = True - @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) - @mock.patch("uaclient.cli.entitlements") - def test_beta_entitlement_not_enabled( - self, m_entitlements, silent_if_inapplicable - ): - m_entitlement_cls = mock.Mock() - m_cfg = mock.Mock() - m_user_cfg = mock.PropertyMock(return_value={}) - type(m_cfg).cfg = m_user_cfg - m_is_beta = mock.PropertyMock(return_value=True) - type(m_entitlement_cls).is_beta = m_is_beta - - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - "testitlement": m_entitlement_cls - } - - kwargs = {"allow_beta": False} - if silent_if_inapplicable is not None: - kwargs["silent_if_inapplicable"] = silent_if_inapplicable + cfg = FakeConfig.for_attached_machine() + cfg.status = mock.Mock() - with pytest.raises(exceptions.BetaServiceError): - _perform_enable("testitlement", m_cfg, **kwargs) + m_ents_dict = {"testitlement": m_entitlement_cls} - assert 1 == m_is_beta.call_count - assert 1 == m_user_cfg.call_count + args = mock.MagicMock() + args.assume_yes = False + args.beta = allow_beta + args.service = ["testitlement"] - @pytest.mark.parametrize("silent_if_inapplicable", (True, False, None)) - @mock.patch("uaclient.contract.get_available_resources", return_value={}) - @mock.patch("uaclient.cli.entitlements") - def test_beta_entitlement_instantiated_and_enabled_with_config_override( - self, - m_entitlements, - _m_get_available_resources, - silent_if_inapplicable, - ): - ent_name = "testitlement" - cfg_dict = {"features": {"allow_beta": True}} - m_entitlement_cls = mock.Mock() - m_cfg = mock.Mock() - m_cfg_dict = mock.PropertyMock(return_value=cfg_dict) - type(m_cfg).cfg = m_cfg_dict - - m_is_beta = mock.PropertyMock(return_value=True) - type(m_entitlement_cls).is_beta = m_is_beta - - m_entitlements.ENTITLEMENT_CLASS_BY_NAME = { - ent_name: m_entitlement_cls - } - - kwargs = {"allow_beta": False} - if silent_if_inapplicable is not None: - kwargs["silent_if_inapplicable"] = silent_if_inapplicable - ret = _perform_enable(ent_name, m_cfg, **kwargs) + with mock.patch.object( + entitlements, "ENTITLEMENT_CLASS_BY_NAME", m_ents_dict + ): + ret = action_enable(args, cfg) assert [ - mock.call(m_cfg, assume_yes=False) + mock.call(cfg, assume_yes=False, allow_beta=allow_beta) ] == m_entitlement_cls.call_args_list m_entitlement = m_entitlement_cls.return_value - if silent_if_inapplicable: - expected_enable_call = mock.call(silent_if_inapplicable=True) - else: - expected_enable_call = mock.call(silent_if_inapplicable=False) + expected_enable_call = mock.call() assert [expected_enable_call] == m_entitlement.enable.call_args_list - assert ret == m_entitlement.enable.return_value + assert ret == 0 - assert 1 == m_cfg.status.call_count - assert 0 == m_is_beta.call_count - assert 1 == m_cfg_dict.call_count + assert 1 == cfg.status.call_count diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli.py 2021-05-27 19:05:18.000000000 +0000 @@ -49,9 +49,9 @@ (https://ubuntu.com/security/esm) - esm-infra: UA Infra: Extended Security Maintenance (ESM) (https://ubuntu.com/security/esm) - - fips-updates: Uncertified security updates to FIPS modules + - fips-updates: NIST-certified core packages with priority security updates (https://ubuntu.com/security/certifications#fips) - - fips: NIST-certified FIPS modules + - fips: NIST-certified core packages (https://ubuntu.com/security/certifications#fips) - livepatch: Canonical Livepatch service (https://ubuntu.com/security/livepatch) @@ -61,11 +61,13 @@ SERVICES_WRAPPED_HELP = textwrap.dedent( """ Client to manage Ubuntu Advantage services on a machine. + - cis: Center for Internet Security Audit Tools + (https://ubuntu.com/security/certifications#cis) - esm-infra: UA Infra: Extended Security Maintenance (ESM) (https://ubuntu.com/security/esm) - - fips-updates: Uncertified security updates to FIPS modules + - fips-updates: NIST-certified core packages with priority security updates (https://ubuntu.com/security/certifications#fips) - - fips: NIST-certified FIPS modules + - fips: NIST-certified core packages (https://ubuntu.com/security/certifications#fips) - livepatch: Canonical Livepatch service (https://ubuntu.com/security/livepatch) @@ -437,21 +439,6 @@ "exception,expected_error_msg,expected_log", ( ( - LockHeldError( - pid="123", - lock_request="ua reboot-cmds", - lock_holder="ua auto-attach", - ), - "Unable to perform: ua reboot-cmds.\nOperation in progress:" - " ua auto-attach (pid:123)\n", - "LockHeldError", - ), - ( - KeyboardInterrupt, - "Interrupt received; exiting.\n", - "KeyboardInterrupt", - ), - ( TypeError("'NoneType' object is not subscriptable"), status.MESSAGE_UNEXPECTED_ERROR + "\n", "Unhandled exception, please file a bug", @@ -492,10 +479,60 @@ assert expected_log in error_log @pytest.mark.parametrize( + "exception,expected_error_msg,expected_log", + ( + ( + KeyboardInterrupt, + "Interrupt received; exiting.\n", + "KeyboardInterrupt", + ), + ), + ) + @mock.patch(M_PATH_UACONFIG + "delete_cache_key") + @mock.patch("uaclient.cli.setup_logging") + @mock.patch("uaclient.cli.get_parser") + def test_interrupt_errors_handled_gracefully( + self, + m_get_parser, + _m_setup_logging, + m_delete_cache_key, + capsys, + logging_sandbox, + caplog_text, + exception, + expected_error_msg, + expected_log, + ): + m_args = m_get_parser.return_value.parse_args.return_value + m_args.action.side_effect = exception + + with pytest.raises(SystemExit) as excinfo: + with mock.patch("sys.argv", ["/usr/bin/ua", "subcmd"]): + main() + assert 0 == m_delete_cache_key.call_count + + exc = excinfo.value + assert 1 == exc.code + + out, err = capsys.readouterr() + assert "" == out + assert expected_error_msg == err + error_log = caplog_text() + assert expected_log in error_log + + @pytest.mark.parametrize( "exception,expected_exit_code", [ (UserFacingError("You need to know about this."), 1), (AlreadyAttachedError(mock.MagicMock()), 0), + ( + LockHeldError( + pid="123", + lock_request="ua reboot-cmds", + lock_holder="ua auto-attach", + ), + 1, + ), ], ) @mock.patch("uaclient.cli.setup_logging") @@ -533,7 +570,7 @@ [line.strip() for line in error_log.splitlines()] ) assert expected_msg in error_log - assert "Traceback (most recent call last):" in error_log + assert "Traceback (most recent call last):" not in error_log @pytest.mark.parametrize( "error_url,expected_log", diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_status.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_status.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_cli_status.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_cli_status.py 2021-05-27 19:05:18.000000000 +0000 @@ -34,9 +34,9 @@ (ESM) esm-infra no {dash} UA Infra: Extended Security Maintenance\ (ESM) -fips no {dash} NIST-certified FIPS modules -fips-updates no {dash} Uncertified security updates to FIPS\ - modules +fips no {dash} NIST-certified core packages +fips-updates no {dash} NIST-certified core packages with\ + priority security updates livepatch no {dash} Canonical Livepatch service {notices} Enable services with: ua enable @@ -50,11 +50,12 @@ # Omit beta services from status ATTACHED_STATUS_NOBETA = """\ SERVICE ENTITLED STATUS DESCRIPTION +cis no {dash} Center for Internet Security Audit Tools esm-infra no {dash} UA Infra: Extended Security Maintenance\ (ESM) -fips no {dash} NIST-certified FIPS modules -fips-updates no {dash} Uncertified security updates to FIPS\ - modules +fips no {dash} NIST-certified core packages +fips-updates no {dash} NIST-certified core packages with\ + priority security updates livepatch no {dash} Canonical Livepatch service {notices} Enable services with: ua enable @@ -65,7 +66,7 @@ Technical support level: n/a """ -BETA_SVC_NAMES = ["cc-eal", "cis", "esm-apps"] +BETA_SVC_NAMES = ["cc-eal", "esm-apps"] SERVICES_JSON_ALL = [ { @@ -101,7 +102,7 @@ "statusDetails": "", }, { - "description": "NIST-certified FIPS modules", + "description": "NIST-certified core packages", "description_override": None, "entitled": "no", "name": "fips", @@ -109,7 +110,9 @@ "statusDetails": "", }, { - "description": "Uncertified security updates to FIPS modules", + "description": ( + "NIST-certified core packages with priority security updates" + ), "description_override": None, "entitled": "no", "name": "fips-updates", diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_status.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_status.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_status.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_status.py 2021-05-27 19:05:18.000000000 +0000 @@ -6,16 +6,21 @@ from uaclient.status import format_tabular, TxtColor, colorize_commands -@pytest.fixture -def status_dict_attached(): +@pytest.fixture(params=[True, False]) +def status_dict_attached(request): status = config.DEFAULT_STATUS.copy() # The following are required so we don't get an "unattached" error status["attached"] = True - status["account"] = "account" - status["subscription"] = "subscription" status["expires"] = "expires" + if request.param: + status["account"] = "account" + status["subscription"] = "subscription" + else: + status["account"] = "" + status["subscription"] = "" + return status @@ -139,16 +144,8 @@ @pytest.mark.parametrize( "origin,expected_headers", [ - ("free", ("Account", "Subscription")), - ( - "not-free", - ( - "Account", - "Subscription", - "Valid until", - "Technical support level", - ), - ), + ("free", ()), + ("not-free", ("Valid until", "Technical support level")), ], ) def test_correct_header_keys_included( @@ -156,6 +153,11 @@ ): status_dict_attached["origin"] = origin + if status_dict_attached["subscription"]: + expected_headers = ("Subscription",) + expected_headers + if status_dict_attached["account"]: + expected_headers = ("Account",) + expected_headers + tabular_output = format_tabular(status_dict_attached) headers = [ diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_ua_update_messaging.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_ua_update_messaging.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_ua_update_messaging.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_ua_update_messaging.py 2021-05-27 19:05:18.000000000 +0000 @@ -10,7 +10,7 @@ CONTRACT_EXPIRY_GRACE_PERIOD_DAYS, ) from uaclient.status import ( - MESSAGE_ANNOUNCE_ESM, + MESSAGE_ANNOUNCE_ESM_TMPL, MESSAGE_CONTRACT_EXPIRED_APT_NO_PKGS_TMPL, MESSAGE_CONTRACT_EXPIRED_APT_PKGS_TMPL, MESSAGE_CONTRACT_EXPIRED_GRACE_PERIOD_TMPL, @@ -65,43 +65,120 @@ class TestWriteAPTAndMOTDTemplates: @pytest.mark.parametrize( - "series,is_active_esm,esm_apps_beta,esm_infra_enabled,cfg_allow_beta", + "is_active_esm,contract_expiry_status,infra_enabled, no_warranty", ( - ("xenial", True, True, True, None), - ("xenial", True, True, False, None), - ("xenial", True, False, True, None), - ("xenial", False, True, False, None), - ("xenial", True, True, True, True), - ("xenial", True, True, False, True), + (True, ContractExpiryStatus.EXPIRED, True, True), + (True, ContractExpiryStatus.NONE, False, True), + (True, ContractExpiryStatus.ACTIVE, True, False), + (True, ContractExpiryStatus.EXPIRED_GRACE_PERIOD, False, True), + (True, ContractExpiryStatus.ACTIVE_EXPIRED_SOON, False, True), + (True, ContractExpiryStatus.ACTIVE, False, True), + (True, ContractExpiryStatus.EXPIRED_GRACE_PERIOD, True, False), + (True, ContractExpiryStatus.ACTIVE_EXPIRED_SOON, True, False), ), ) @mock.patch(M_PATH + "entitlements") @mock.patch(M_PATH + "_write_esm_service_msg_templates") @mock.patch(M_PATH + "util.is_active_esm") @mock.patch(M_PATH + "get_contract_expiry_status") - def test_write_apps_and_infra_services( + def test_write_apps_or_infra_services_emits_no_warranty( self, get_contract_expiry_status, util_is_active_esm, write_esm_service_templates, entitlements, + is_active_esm, + contract_expiry_status, + infra_enabled, + no_warranty, + FakeConfig, + ): + util_is_active_esm.return_value = is_active_esm + if infra_enabled: + infra_status = ApplicationStatus.ENABLED + else: + infra_status = ApplicationStatus.DISABLED + infra_cls = mock.MagicMock() + infra_obj = infra_cls.return_value + infra_obj.application_status.return_value = (infra_status, "") + entitlements.ENTITLEMENT_CLASS_BY_NAME = { + "esm-apps": mock.MagicMock(), + "esm-infra": infra_cls, + } + get_contract_expiry_status.return_value = ( + contract_expiry_status, + -12355, # unused in this test + ) + cfg = FakeConfig.for_attached_machine() + msg_dir = os.path.join(cfg.data_dir, "messages") + os.makedirs(msg_dir) + + write_apt_and_motd_templates(cfg, "xenial") + assert [mock.call("xenial")] == util_is_active_esm.call_args_list + no_warranty_file = os.path.join(msg_dir, "ubuntu-no-warranty") + if no_warranty: + assert MESSAGE_UBUNTU_NO_WARRANTY == util.load_file( + no_warranty_file + ) + else: + assert False is os.path.exists(no_warranty_file) + + @pytest.mark.parametrize( + "series,is_active_esm,contract_days,infra_enabled," + "esm_apps_beta,cfg_allow_beta,show_infra,show_apps", + ( + # Enabled infra, active contract, beta apps: show none + ("xenial", True, 21, True, True, None, False, False), + # Not enabled infra, beta apps: show infra msg + ("xenial", True, -21, False, True, None, True, False), + # Any *expired contract, infra enabled, beta apps: show infra msg + ("xenial", True, 20, True, True, None, True, False), + ("xenial", True, 0, True, True, None, True, False), + ("xenial", True, -1, True, True, None, True, False), + ("xenial", True, -21, True, True, None, True, False), + # Infra enabled, any *expired contract, allow_beta: show infra msg + ("xenial", True, 20, True, True, True, True, False), + # Infra enabled, active contract, allow_beta: show apps msg + ("xenial", True, 21, True, True, True, False, True), + # Infra enabled, active contract, apps not beta: show apps msg + ("xenial", True, 21, True, False, None, False, True), + ), + ) + @mock.patch(M_PATH + "entitlements") + @mock.patch(M_PATH + "_remove_msg_templates") + @mock.patch(M_PATH + "_write_esm_service_msg_templates") + @mock.patch(M_PATH + "util.is_active_esm") + @mock.patch(M_PATH + "get_contract_expiry_status") + def test_write_apps_or_infra_services_mutually_exclusive( + self, + get_contract_expiry_status, + util_is_active_esm, + write_esm_service_templates, + remove_msg_templates, + entitlements, series, is_active_esm, + contract_days, + infra_enabled, esm_apps_beta, - esm_infra_enabled, cfg_allow_beta, + show_infra, + show_apps, FakeConfig, ): - """Write both Infra and Apps when not-beta service.""" + """Write Infra or Apps when Apps not-beta service. + + Messaging is mutually exclusive, if Infra templates are emitted, don't + write Apps. + """ get_contract_expiry_status.return_value = ( ContractExpiryStatus.ACTIVE, - 21, + contract_days, ) - - infra_status = ApplicationStatus.DISABLED - if esm_infra_enabled: + if infra_enabled: infra_status = ApplicationStatus.ENABLED - + else: + infra_status = ApplicationStatus.DISABLED util_is_active_esm.return_value = is_active_esm infra_cls = mock.MagicMock() infra_obj = infra_cls.return_value @@ -116,73 +193,149 @@ "esm-infra": infra_cls, } cfg = FakeConfig.for_attached_machine() + os.makedirs(os.path.join(cfg.data_dir, "messages")) if cfg_allow_beta: cfg.override_features({"allow_beta": cfg_allow_beta}) - if cfg_allow_beta or not esm_apps_beta: - write_calls = [ + write_calls = [] + remove_calls = [] + if show_infra: + write_calls.append( mock.call( cfg, mock.ANY, ContractExpiryStatus.ACTIVE, - 21, - ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value, - ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value, - ExternalMessage.MOTD_APPS_PKGS.value, - ExternalMessage.MOTD_APPS_NO_PKGS.value, - ExternalMessage.UBUNTU_NO_WARRANTY.value, + contract_days, + ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value, + ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value, + ExternalMessage.MOTD_INFRA_PKGS.value, + ExternalMessage.MOTD_INFRA_NO_PKGS.value, ) - ] + ) else: - write_calls = [] - if esm_infra_enabled or is_active_esm: + remove_calls.append( + mock.call( + msg_dir=os.path.join(cfg.data_dir, "messages"), + msg_template_names=[ + ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value, + ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value, + ExternalMessage.MOTD_INFRA_PKGS.value, + ExternalMessage.MOTD_INFRA_NO_PKGS.value, + ], + ) + ) + if show_apps: write_calls.append( mock.call( cfg, mock.ANY, ContractExpiryStatus.ACTIVE, - 21, - ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value, - ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value, - ExternalMessage.MOTD_INFRA_PKGS.value, - ExternalMessage.MOTD_INFRA_NO_PKGS.value, - ExternalMessage.UBUNTU_NO_WARRANTY.value, + contract_days, + ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value, + ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value, + ExternalMessage.MOTD_APPS_PKGS.value, + ExternalMessage.MOTD_APPS_NO_PKGS.value, + ) + ) + else: + remove_calls.append( + mock.call( + msg_dir=os.path.join(cfg.data_dir, "messages"), + msg_template_names=[ + ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value, + ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value, + ExternalMessage.MOTD_APPS_PKGS.value, + ExternalMessage.MOTD_APPS_NO_PKGS.value, + ], ) ) write_apt_and_motd_templates(cfg, series) assert [mock.call(cfg)] == get_contract_expiry_status.call_args_list + assert remove_calls == remove_msg_templates.call_args_list assert write_calls == write_esm_service_templates.call_args_list class Test_WriteESMServiceAPTMsgTemplates: @pytest.mark.parametrize( - "contract_expiry, expect_messages", + "service_name,contract_expiry,expect_messages,platform_info," + "eol_release,url", ( - (ContractExpiryStatus.ACTIVE, True), - (ContractExpiryStatus.EXPIRED, False), + ( + "esm-apps", + ContractExpiryStatus.ACTIVE, + True, + {"series": "xenial", "release": "16.04"}, + "", + BASE_ESM_URL, + ), + ( + "esm-infra", + ContractExpiryStatus.ACTIVE, + True, + {"series": "xenial", "release": "16.04"}, + "for Ubuntu 16.04 ", + "https://ubuntu.com/16-04", + ), + ( + "esm-apps", + ContractExpiryStatus.ACTIVE, + True, + {"series": "xenial", "release": "16.04"}, + "", + BASE_ESM_URL, + ), + ( + "esm-apps", + ContractExpiryStatus.EXPIRED, + False, + {"series": "xenial", "release": "16.04"}, + "", + BASE_ESM_URL, + ), ), ) + @mock.patch(M_PATH + "util.get_platform_info") + @mock.patch(M_PATH + "util.is_active_esm", return_value=True) @mock.patch( M_PATH + "entitlements.repo.RepoEntitlement.application_status" ) def test_apt_templates_written_for_disabled_services( - self, app_status, contract_expiry, expect_messages, FakeConfig, tmpdir + self, + app_status, + util_is_active_esm, + get_platform_info, + service_name, + contract_expiry, + expect_messages, + platform_info, + eol_release, + url, + FakeConfig, + tmpdir, ): """Disabled service messages are omitted if contract expired. This represents customer chosen disabling of service on an attached machine. So, they've chosen to disable expired services. """ + if service_name == "esm-infra": + title = "UA Infra: ESM" + pkg_count_var = "{ESM_INFRA_PKG_COUNT}" + pkg_names_var = "{ESM_INFRA_PACKAGES}" + else: + title = "UA Apps: ESM" + pkg_count_var = "{ESM_APPS_PKG_COUNT}" + pkg_names_var = "{ESM_APPS_PACKAGES}" + get_platform_info.return_value = platform_info m_entitlement_cls = mock.MagicMock() m_ent_obj = m_entitlement_cls.return_value disabled_status = ApplicationStatus.DISABLED, "" m_ent_obj.application_status.return_value = disabled_status - type(m_ent_obj).name = mock.PropertyMock(return_value="esm-apps") - type(m_ent_obj).title = mock.PropertyMock(return_value="UA Apps: ESM") + type(m_ent_obj).name = mock.PropertyMock(return_value=service_name) + type(m_ent_obj).title = mock.PropertyMock(return_value=title) pkgs_file = tmpdir.join("pkgs-msg") no_pkgs_file = tmpdir.join("no-pkgs-msg") motd_pkgs_file = tmpdir.join("motd-pkgs-msg") motd_no_pkgs_file = tmpdir.join("motd-no-pkgs-msg") - no_warranty_file = tmpdir.join("ubuntu-no-warranty") _write_esm_service_msg_templates( FakeConfig.for_attached_machine(), m_ent_obj, @@ -192,22 +345,20 @@ no_pkgs_file.strpath, motd_pkgs_file.strpath, motd_no_pkgs_file.strpath, - no_warranty_file.strpath, ) if expect_messages: assert ( MESSAGE_DISABLED_APT_PKGS_TMPL.format( - title="UA Apps: ESM", - pkg_num="{ESM_APPS_PKG_COUNT}", - pkg_names="{ESM_APPS_PACKAGES}", - url=BASE_ESM_URL, + title=title, + pkg_num=pkg_count_var, + pkg_names=pkg_names_var, + eol_release=eol_release, + url=url, ) == pkgs_file.read() ) assert ( - MESSAGE_DISABLED_MOTD_NO_PKGS_TMPL.format( - title="UA Apps: ESM", url=BASE_ESM_URL - ) + MESSAGE_DISABLED_MOTD_NO_PKGS_TMPL.format(title=title, url=url) == no_pkgs_file.read() ) else: @@ -215,15 +366,41 @@ assert False is os.path.exists(no_pkgs_file.strpath) @pytest.mark.parametrize( - "contract_status, remaining_days, is_active_esm", + "contract_status, remaining_days, is_active_esm, platform_info", ( - (ContractExpiryStatus.ACTIVE, 21, True), - (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, 10, True), - (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, -5, True), - (ContractExpiryStatus.EXPIRED, -20, True), - (ContractExpiryStatus.EXPIRED, -20, False), + ( + ContractExpiryStatus.ACTIVE, + 21, + True, + {"series": "xenial", "release": "16.04"}, + ), + ( + ContractExpiryStatus.ACTIVE_EXPIRED_SOON, + 10, + True, + {"series": "xenial", "release": "16.04"}, + ), + ( + ContractExpiryStatus.EXPIRED_GRACE_PERIOD, + -5, + True, + {"series": "xenial", "release": "16.04"}, + ), + ( + ContractExpiryStatus.EXPIRED, + -20, + True, + {"series": "xenial", "release": "16.04"}, + ), + ( + ContractExpiryStatus.EXPIRED, + -20, + False, + {"series": "xenial", "release": "16.04"}, + ), ), ) + @mock.patch(M_PATH + "util.get_platform_info") @mock.patch(M_PATH + "util.is_active_esm") @mock.patch( M_PATH + "entitlements.repo.RepoEntitlement.application_status" @@ -232,12 +409,15 @@ self, app_status, util_is_active_esm, + get_platform_info, contract_status, remaining_days, is_active_esm, + platform_info, FakeConfig, tmpdir, ): + get_platform_info.return_value = platform_info util_is_active_esm.return_value = is_active_esm m_entitlement_cls = mock.MagicMock() m_ent_obj = m_entitlement_cls.return_value @@ -249,7 +429,6 @@ no_pkgs_tmpl = tmpdir.join("no-pkgs-msg.tmpl") motd_pkgs_tmpl = tmpdir.join("motd-pkgs-msg.tmpl") motd_no_pkgs_tmpl = tmpdir.join("motd-no-pkgs-msg.tmpl") - no_warranty_file = tmpdir.join("ubuntu-no-warranty") pkgs_tmpl.write("oldcache") no_pkgs_tmpl.write("oldcache") motd_pkgs_tmpl.write("oldcache") @@ -276,7 +455,6 @@ no_pkgs_tmpl.strpath, motd_pkgs_tmpl.strpath, motd_no_pkgs_tmpl.strpath, - no_warranty_file.strpath, ) if contract_status == ContractExpiryStatus.ACTIVE: # Old messages removed on ACTIVE status @@ -313,52 +491,111 @@ url=BASE_UA_URL, ) no_pkgs_msg = MESSAGE_CONTRACT_EXPIRED_APT_NO_PKGS_TMPL.format( - title="UA Apps: ESM", url=BASE_ESM_URL + title="UA Apps: ESM", url=BASE_UA_URL ) - if is_active_esm: - assert MESSAGE_UBUNTU_NO_WARRANTY == no_warranty_file.read() - else: - assert False is os.path.exists(no_warranty_file.strpath) assert pkgs_msg == pkgs_tmpl.read() assert no_pkgs_msg == no_pkgs_tmpl.read() class TestWriteESMAnnouncementMessage: @pytest.mark.parametrize( - "series,is_beta,cfg_allow_beta,apps_enabled,expected", + "series,release,is_active_esm,is_beta,cfg_allow_beta," + "apps_enabled,expected", ( # No ESM announcement when trusty - ("trusty", False, True, False, None), + ("trusty", "14.04", True, False, True, False, None), # ESMApps.is_beta == True no Announcement - ("xenial", True, None, False, None), + ("xenial", "16.04", True, True, None, False, None), # Once release begins ESM and ESMApps.is_beta is false announce - ("xenial", False, None, False, "\n" + MESSAGE_ANNOUNCE_ESM), + ( + "xenial", + "16.04", + True, + False, + None, + False, + "\n" + + MESSAGE_ANNOUNCE_ESM_TMPL.format( + url="https://ubuntu.com/16-04" + ), + ), # allow_beta uaclient.config overrides is_beta and days_until_esm - ("xenial", True, True, False, "\n" + MESSAGE_ANNOUNCE_ESM), + ( + "xenial", + "16.04", + True, + True, + True, + False, + "\n" + + MESSAGE_ANNOUNCE_ESM_TMPL.format( + url="https://ubuntu.com/16-04" + ), + ), # when esm-apps already enabled don't show - ("xenial", False, True, True, None), - ("bionic", False, None, False, "\n" + MESSAGE_ANNOUNCE_ESM), - ("focal", False, None, False, "\n" + MESSAGE_ANNOUNCE_ESM), + ("xenial", "16.04", True, False, True, True, None), + ( + "bionic", + "18.04", + False, + False, + None, + False, + "\n" + + MESSAGE_ANNOUNCE_ESM_TMPL.format( + url="https://ubuntu.com/esm" + ), + ), + # Once Bionic transitions to ESM support, emit 18-04 messaging + ( + "bionic", + "18.04", + True, + False, + None, + False, + "\n" + + MESSAGE_ANNOUNCE_ESM_TMPL.format( + url="https://ubuntu.com/18-04" + ), + ), + ( + "focal", + "20.04", + False, + False, + None, + False, + "\n" + + MESSAGE_ANNOUNCE_ESM_TMPL.format( + url="https://ubuntu.com/esm" + ), + ), ), ) @mock.patch( M_PATH + "entitlements.repo.RepoEntitlement.application_status" ) @mock.patch(M_PATH + "entitlements") + @mock.patch(M_PATH + "util.is_active_esm") @mock.patch(M_PATH + "util.get_platform_info") def test_message_based_on_beta_status_and_count_until_active_esm( self, get_platform_info, + util_is_active_esm, entitlements, esm_application_status, series, + release, + is_active_esm, is_beta, cfg_allow_beta, apps_enabled, expected, FakeConfig, ): - get_platform_info.return_value = {"series": series} + get_platform_info.return_value = {"series": series, "release": release} + util.is_active_esm.return_value = is_active_esm cfg = FakeConfig.for_attached_machine() msg_dir = os.path.join(cfg.data_dir, "messages") diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_util.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_util.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/tests/test_util.py 2021-05-11 10:20:05.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/tests/test_util.py 2021-05-27 19:05:18.000000000 +0000 @@ -588,21 +588,46 @@ class TestReadurl: @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) @pytest.mark.parametrize( - "headers,data,response,expected_logs", + "headers,data,method,url,response,expected_logs", ( ( {}, None, None, + "http://some_url", + None, [ "URL [GET]: http://some_url, headers: {}, data: None", "URL [GET] response: http://some_url, headers: {}," " data: response\n", ], ), + # AWS PRO redacts IMDSv2 token + ( + { + "X-aws-ec2-metadata-token-ttl-seconds": "21600", + "X-aws-ec2-metadata-token": "SEKRET", + }, + None, + "PUT", + "http://169.254.169.254/latest/api/token", + b"SECRET==", + [ + "URL [PUT]: http://169.254.169.254/latest/api/token," + " headers: {'X-aws-ec2-metadata-token': ''," + " 'X-aws-ec2-metadata-token-ttl-seconds': '21600'}", + "URL [PUT] response:" + " http://169.254.169.254/latest/api/token, headers:" + " {'X-aws-ec2-metadata-token': ''," + " 'X-aws-ec2-metadata-token-ttl-seconds': '21600'}," + " data: \n", + ], + ), ( {"key1": "Bearcat", "Authorization": "Bearer SEKRET"}, b"{'token': 'HIDEME', 'tokenInfo': 'SHOWME'}", + None, + "http://some_url", b"{'machineToken': 'HIDEME', 'machineTokenInfo': 'SHOWME'}", [ "URL [POST]: http://some_url, headers: {'Authorization':" @@ -618,7 +643,15 @@ ) @mock.patch("uaclient.util.request.urlopen") def test_readurl_redacts_call_and_response( - self, urlopen, headers, data, response, expected_logs, caplog_text + self, + urlopen, + headers, + data, + method, + url, + response, + expected_logs, + caplog_text, ): """Log and redact sensitive data from logs for url interactions.""" @@ -635,7 +668,7 @@ urlopen.return_value = FakeHTTPResponse( headers=headers, content=response ) - util.readurl("http://some_url", headers=headers, data=data) + util.readurl(url, method=method, headers=headers, data=data) logs = caplog_text() for log in expected_logs: assert log in logs diff -Nru ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/util.py ubuntu-advantage-tools-27.1~20.10.1/uaclient/util.py --- ubuntu-advantage-tools-27.0.2~20.10.1/uaclient/util.py 2021-05-11 10:19:18.000000000 +0000 +++ ubuntu-advantage-tools-27.1~20.10.1/uaclient/util.py 2021-05-27 19:05:18.000000000 +0000 @@ -23,6 +23,7 @@ try: from typing import ( # noqa: F401 Any, + Callable, Dict, List, Mapping, @@ -703,6 +704,8 @@ r"(\'attach\', \')[^\']+", r"(\'machineToken\': \')[^\']+", r"(\'token\': \')[^\']+", + r"(\'X-aws-ec2-metadata-token\': \')[^\']+", + r"(.*\[PUT\] response.*api/token,.*data: ).*", ] @@ -719,3 +722,33 @@ def should_reboot() -> bool: """Check if the system needs to be rebooted.""" return os.path.exists(REBOOT_FILE_CHECK_PATH) + + +def is_installed(package_name: str) -> bool: + try: + out, _ = subp(["dpkg", "-l", package_name]) + return "ii {} ".format(package_name) in out + except ProcessExecutionError: + return False + + +def handle_message_operations( + msg_ops: "List[Union[str, Tuple[Callable, Dict]]]" +) -> bool: + """Emit messages to the console for user interaction + + :param msg_op: A list of strings or tuples. Any string items are printed. + Any tuples will contain a callable and a dict of args to pass to the + callable. Callables are expected to return True on success and + False upon failure. + + :return: True upon success, False on failure. + """ + for msg_op in msg_ops: + if isinstance(msg_op, str): + print(msg_op) + else: # Then we are a callable and dict of args + functor, args = msg_op + if not functor(**args): + return False + return True