diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/20apt-esm-hook.conf ubuntu-advantage-tools-27.14.4~16.04/apt-hook/20apt-esm-hook.conf --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/20apt-esm-hook.conf 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/20apt-esm-hook.conf 2023-04-05 15:14:00.000000000 +0000 @@ -2,10 +2,6 @@ "[ ! -e /run/systemd/system ] || [ $(id -u) -ne 0 ] || systemctl start --no-block apt-news.service esm-cache.service || true"; }; -APT::Update::Post-Invoke-Stats { - "[ ! -f /usr/lib/ubuntu-advantage/apt-esm-hook ] || /usr/lib/ubuntu-advantage/apt-esm-hook || true"; -}; - binary::apt::AptCli::Hooks::Upgrade { "[ ! -f /usr/lib/ubuntu-advantage/apt-esm-json-hook ] || /usr/lib/ubuntu-advantage/apt-esm-json-hook || true"; }; diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-counts.cc ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-counts.cc --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-counts.cc 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-counts.cc 2023-04-05 15:14:00.000000000 +0000 @@ -52,11 +52,24 @@ } // set up esm cache + // make sure the configuration is isolated, apart from Acquire + for (const Configuration::Item *ConfigItem = _config->Tree(0); ConfigItem != NULL; ConfigItem = ConfigItem->Next) { + if (ConfigItem->FullTag(0) != "Acquire") { + _config->Clear(ConfigItem->FullTag(0)); + } + } + _config->Set("Dir", "/var/lib/ubuntu-advantage/apt-esm/"); _config->Set("Dir::State::status", "/var/lib/ubuntu-advantage/apt-esm/var/lib/dpkg/status"); + + if (!pkgInitConfig(*_config)) { + return false; + } + if (!pkgInitSystem(*_config, _system)) { return false; } + pkgCacheFile esm_cachefile; pkgCache *esm_cache = esm_cachefile.GetPkgCache(); if (esm_cache == NULL) { diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-templates.cc ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-templates.cc --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-templates.cc 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-templates.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,273 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "esm-templates.hh" - - -struct result { - int enabled_esms_i; - int disabled_esms_i; - std::vector esm_i_packages; - - int enabled_esms_a; - int disabled_esms_a; - std::vector esm_a_packages; -}; - -// Check if we have an ESM upgrade for the specified package -static void check_esm_upgrade(pkgCache::PkgIterator pkg, pkgPolicy *policy, result &res) -{ - pkgCache::VerIterator cur = pkg.CurrentVer(); - - if (cur.end()) - return; - - // Search all versions >= cur (list in decreasing order) - for (pkgCache::VerIterator ver = pkg.VersionList(); !ver.end() && ver->ID != cur->ID; ver++) - { - for (pkgCache::VerFileIterator pf = ver.FileList(); !pf.end(); pf++) - { - 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()); - - // Pin-Priority: never unauthenticated APT repos == -32768 - if (policy->GetPriority(pf.File()) == -32768) - { - res.disabled_esms_i++; - } - else - { - res.enabled_esms_i++; - } - } - } - 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()); - - // Pin-Priority: never unauthenticated APT repos == -32768 - if (policy->GetPriority(pf.File()) == -32768) - { - res.disabled_esms_a++; - } - else - { - res.enabled_esms_a++; - } - } - } - } - } -} - -// Calculate the update count -static int get_update_count(result &res) -{ - int count = 0; - if (!pkgInitConfig(*_config)) - return -1; - - if (!pkgInitSystem(*_config, _system)) - return -1; - - pkgCacheFile cachefile; - - pkgCache *cache = cachefile.GetPkgCache(); - pkgPolicy *policy = cachefile.GetPolicy(); - - if (cache == NULL || policy == NULL) - return -1; - - for (pkgCache::PkgIterator pkg = cache->PkgBegin(); !pkg.end(); pkg++) - { - check_esm_upgrade(pkg, policy, res); - } - return count; -} - - -static void process_template_file( - std::string template_file_name, - std::string static_file_name, - std::string esm_a_pkgs_count, - std::string esm_a_pkgs, - std::string esm_i_pkgs_count, - std::string esm_i_pkgs -) { - std::ifstream message_tmpl_file(template_file_name.c_str()); - if (message_tmpl_file.is_open()) { - // This line loads the whole file contents into a string - std::string message_tmpl((std::istreambuf_iterator(message_tmpl_file)), (std::istreambuf_iterator())); - - message_tmpl_file.close(); - - // Process all template variables - std::array tmpl_var_names = { - ESM_APPS_PKGS_COUNT_TEMPLATE_VAR, - ESM_APPS_PACKAGES_TEMPLATE_VAR, - ESM_INFRA_PKGS_COUNT_TEMPLATE_VAR, - ESM_INFRA_PACKAGES_TEMPLATE_VAR - }; - std::array tmpl_var_vals = { - esm_a_pkgs_count, - esm_a_pkgs, - esm_i_pkgs_count, - esm_i_pkgs - }; - for (uint i = 0; i < tmpl_var_names.size(); i++) { - size_t pos = message_tmpl.find(tmpl_var_names[i]); - if (pos != std::string::npos) { - message_tmpl.replace(pos, tmpl_var_names[i].size(), tmpl_var_vals[i]); - } - } - - std::ofstream message_static_file(static_file_name.c_str()); - if (message_static_file.is_open()) { - message_static_file << message_tmpl; - message_static_file.close(); - } - } else { - remove(static_file_name.c_str()); - } -} - -void process_all_templates() { - int bytes_written; - int length; - - // Iterate over apt cache looking for esm packages - result res = {0, 0, std::vector(), 0, 0, std::vector()}; - get_update_count(res); - if (_error->PendingError()) - { - _error->DumpErrors(); - return; - } - - // Compute all strings necessary to fill in templates - std::string space_separated_esm_i_packages = ""; - if (res.esm_i_packages.size() > 0) { - for (uint i = 0; i < res.esm_i_packages.size() - 1; i++) { - space_separated_esm_i_packages.append(res.esm_i_packages[i]); - space_separated_esm_i_packages.append(" "); - } - space_separated_esm_i_packages.append(res.esm_i_packages[res.esm_i_packages.size() - 1]); - } - std::string space_separated_esm_a_packages = ""; - if (res.esm_a_packages.size() > 0) { - for (uint i = 0; i < res.esm_a_packages.size() - 1; i++) { - space_separated_esm_a_packages.append(res.esm_a_packages[i]); - space_separated_esm_a_packages.append(" "); - } - space_separated_esm_a_packages.append(res.esm_a_packages[res.esm_a_packages.size() - 1]); - } - - std::array static_file_names = { - APT_PRE_INVOKE_APPS_PKGS_STATIC_PATH, - MOTD_APPS_PKGS_STATIC_PATH, - APT_PRE_INVOKE_INFRA_PKGS_STATIC_PATH, - MOTD_INFRA_PKGS_STATIC_PATH, - }; - std::array apt_static_files = { - APT_PRE_INVOKE_APPS_PKGS_STATIC_PATH, - APT_PRE_INVOKE_INFRA_PKGS_STATIC_PATH, - }; - std::array motd_static_files = { - MOTD_APPS_PKGS_STATIC_PATH, - MOTD_INFRA_PKGS_STATIC_PATH, - }; - - // Decide which templates to use (nopkg or pkg variants) - std::vector template_file_names; - if (res.esm_a_packages.size() > 0) { - template_file_names.push_back(APT_PRE_INVOKE_APPS_PKGS_TEMPLATE_PATH); - template_file_names.push_back(MOTD_APPS_PKGS_TEMPLATE_PATH); - } else { - template_file_names.push_back(APT_PRE_INVOKE_APPS_NO_PKGS_TEMPLATE_PATH); - template_file_names.push_back(MOTD_APPS_NO_PKGS_TEMPLATE_PATH); - } - if (res.esm_i_packages.size() > 0) { - template_file_names.push_back(APT_PRE_INVOKE_INFRA_PKGS_TEMPLATE_PATH); - template_file_names.push_back(MOTD_INFRA_PKGS_TEMPLATE_PATH); - } else { - template_file_names.push_back(APT_PRE_INVOKE_INFRA_NO_PKGS_TEMPLATE_PATH); - template_file_names.push_back(MOTD_INFRA_NO_PKGS_TEMPLATE_PATH); - } - // Insert values into selected templates and render to separate file - for (uint i = 0; i < template_file_names.size(); i++) { - process_template_file( - template_file_names[i], - static_file_names[i], - std::to_string(res.esm_a_packages.size()), - space_separated_esm_a_packages, - std::to_string(res.esm_i_packages.size()), - space_separated_esm_i_packages - ); - } - - // combine rendered files so that there is one apt file and one motd file - // first apt - std::ofstream apt_pre_invoke_msg; - apt_pre_invoke_msg.open(APT_PRE_INVOKE_MESSAGE_STATIC_PATH); - for (uint i = 0; i < apt_static_files.size(); i++) { - std::ifstream message_file(apt_static_files[i]); - if (message_file.is_open()) { - apt_pre_invoke_msg << message_file.rdbuf(); - message_file.close(); - }; - } - bytes_written = apt_pre_invoke_msg.tellp(); - if (bytes_written > 0) { - // Then we wrote some content add trailing newline - apt_pre_invoke_msg << std::endl; - } - apt_pre_invoke_msg.close(); - if (bytes_written == 0) { - // We added nothing. Remove the file - remove(APT_PRE_INVOKE_MESSAGE_STATIC_PATH); - } - - // then motd - std::ofstream motd_msg; - motd_msg.open(MOTD_ESM_SERVICE_STATUS_MESSAGE_STATIC_PATH); - for (uint i = 0; i < motd_static_files.size(); i++) { - std::ifstream message_file(motd_static_files[i]); - if (message_file.is_open()) { - 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(); - } - message_file.close(); - }; - } - bytes_written = motd_msg.tellp(); - if (bytes_written > 0) { - // Then we wrote some content add trailing newline - motd_msg << std::endl; - } - motd_msg.close(); - if (bytes_written == 0) { - // We added nothing. Remove the file - remove(MOTD_ESM_SERVICE_STATUS_MESSAGE_STATIC_PATH); - } -} diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-templates.hh ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-templates.hh --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/esm-templates.hh 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/esm-templates.hh 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -#include -#include - -#define MOTD_ESM_SERVICE_STATUS_MESSAGE_STATIC_PATH "/var/lib/ubuntu-advantage/messages/motd-esm-service-status" -#define MOTD_APPS_NO_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/motd-no-packages-apps.tmpl" -#define MOTD_INFRA_NO_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/motd-no-packages-infra.tmpl" -#define MOTD_APPS_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/motd-packages-apps.tmpl" -#define MOTD_INFRA_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/motd-packages-infra.tmpl" -#define MOTD_APPS_PKGS_STATIC_PATH "/var/lib/ubuntu-advantage/messages/motd-packages-apps" -#define MOTD_INFRA_PKGS_STATIC_PATH "/var/lib/ubuntu-advantage/messages/motd-packages-infra" -#define APT_PRE_INVOKE_APPS_NO_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-no-packages-apps.tmpl" -#define APT_PRE_INVOKE_INFRA_NO_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-no-packages-infra.tmpl" -#define APT_PRE_INVOKE_APPS_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-packages-apps.tmpl" -#define APT_PRE_INVOKE_APPS_PKGS_STATIC_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-packages-apps" -#define APT_PRE_INVOKE_INFRA_PKGS_TEMPLATE_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-packages-infra.tmpl" -#define APT_PRE_INVOKE_INFRA_PKGS_STATIC_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-packages-infra" -#define APT_PRE_INVOKE_MESSAGE_STATIC_PATH "/var/lib/ubuntu-advantage/messages/apt-pre-invoke-esm-service-status" - -#define ESM_APPS_PKGS_COUNT_TEMPLATE_VAR "{ESM_APPS_PKG_COUNT}" -#define ESM_APPS_PACKAGES_TEMPLATE_VAR "{ESM_APPS_PACKAGES}" -#define ESM_INFRA_PKGS_COUNT_TEMPLATE_VAR "{ESM_INFRA_PKG_COUNT}" -#define ESM_INFRA_PACKAGES_TEMPLATE_VAR "{ESM_INFRA_PACKAGES}" - -void process_all_templates(); diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/hook.cc ubuntu-advantage-tools-27.14.4~16.04/apt-hook/hook.cc --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/hook.cc 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/hook.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2018-2019 Canonical Ltd - * Author: Julian Andres Klode - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * -*/ - -#include -#include - -#include "esm-templates.hh" - -int main(int argc, char *argv[]) -{ - (void) argc; // unused - (void) argv; // unused - - setlocale(LC_ALL, ""); - textdomain("ubuntu-advantage"); - - process_all_templates(); - - return 0; -} diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/json-hook.cc ubuntu-advantage-tools-27.14.4~16.04/apt-hook/json-hook.cc --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/json-hook.cc 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/json-hook.cc 2023-04-05 15:14:00.000000000 +0000 @@ -206,11 +206,11 @@ CloudID ret = NONE; if (cloud_id_file.is_open()) { std::string cloud_id_str((std::istreambuf_iterator(cloud_id_file)), (std::istreambuf_iterator())); - if (cloud_id_str == "aws") { + if (cloud_id_str.find("aws") == 0) { ret = AWS; - } else if (cloud_id_str == "azure") { + } else if (cloud_id_str.find("azure") == 0) { ret = AZURE; - } else if (cloud_id_str == "gce") { + } else if (cloud_id_str.find("gce") == 0) { ret = GCE; } cloud_id_file.close(); @@ -239,7 +239,6 @@ ESMContext get_esm_context() { CloudID cloud_id = get_cloud_id(); bool is_x = is_xenial(); - bool non_azure_cloud = cloud_id != NONE && cloud_id != AZURE; ESMContext ret; ret.context = ""; diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/Makefile ubuntu-advantage-tools-27.14.4~16.04/apt-hook/Makefile --- ubuntu-advantage-tools-27.13.6~16.04.1/apt-hook/Makefile 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/apt-hook/Makefile 2023-04-05 15:14:00.000000000 +0000 @@ -1,12 +1,6 @@ all: build -build: hook ubuntu-advantage.pot json-hook - -ubuntu-advantage.pot: hook.cc - xgettext hook.cc -o ubuntu-advantage.pot - -hook: hook.cc - $(CXX) -Wall -Wextra -pedantic -std=c++11 $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -g -o hook hook.cc esm-templates.cc -lapt-pkg $(LDLIBS) +build: json-hook json-hook: json-hook.cc $(CXX) -Wall -Wextra -pedantic -std=c++11 $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -g -o json-hook json-hook-main.cc json-hook.cc esm-counts.cc -ljson-c -lapt-pkg $(LDLIBS) @@ -18,11 +12,10 @@ install-conf: install -D -m 644 20apt-esm-hook.conf $(DESTDIR)/etc/apt/apt.conf.d/20apt-esm-hook.conf -install: hook json-hook - install -D -m 755 hook $(DESTDIR)/usr/lib/ubuntu-advantage/apt-esm-hook +install: json-hook install -D -m 755 json-hook $(DESTDIR)/usr/lib/ubuntu-advantage/apt-esm-json-hook clean: - rm -f hook json-hook json-hook-test ubuntu-advantage.pot + rm -f json-hook json-hook-test .PHONY: test diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/CONTRIBUTING.md ubuntu-advantage-tools-27.14.4~16.04/CONTRIBUTING.md --- ubuntu-advantage-tools-27.13.6~16.04.1/CONTRIBUTING.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/CONTRIBUTING.md 2023-04-05 15:14:00.000000000 +0000 @@ -29,3 +29,7 @@ ### Explanation * [How auto-attach works](./dev-docs/explanations/how_auto_attach_works.md) + +### Documentation + +* [Documentation guide](./dev-docs/devdocs_styleguide.md) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/changelog ubuntu-advantage-tools-27.14.4~16.04/debian/changelog --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/changelog 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/changelog 2023-04-06 13:50:05.000000000 +0000 @@ -1,8 +1,77 @@ -ubuntu-advantage-tools (27.13.6~16.04.1) xenial; urgency=medium +ubuntu-advantage-tools (27.14.4~16.04) xenial; urgency=medium - * Backport new upstream release: (LP: #2008814) to xenial + * Backport new upstream release: (LP: #2011477) to xenial - -- Renan Rodrigo Tue, 28 Feb 2023 16:17:34 -0300 + -- Renan Rodrigo Thu, 06 Apr 2023 10:50:05 -0300 + +ubuntu-advantage-tools (27.14.4) lunar; urgency=medium + + * timer: disable update_contract_info job (LP: #2015302) + * livepatch: prevent livepatch from auto-enabling and subsequently failing + on non-amd64 systems (LP: #2015241) + + -- Renan Rodrigo Tue, 04 Apr 2023 17:56:07 -0300 + +ubuntu-advantage-tools (27.14.3) lunar; urgency=medium + + * livepatch: prevent livepatch from auto-enabling and subsequently failing + on interim releases (LP: #2013409) + + -- Grant Orndorff Fri, 31 Mar 2023 10:13:44 -0400 + +ubuntu-advantage-tools (27.14.2~23.04.1) lunar; urgency=medium + + * status: + - always use dpkg instead of lscpu for fetching architecture + information (LP: #2012735) + + -- Lucas Moura Tue, 28 Mar 2023 11:46:11 -0300 + +ubuntu-advantage-tools (27.14.1~23.04.1) lunar; urgency=medium + + * New upstream release 27.14.1 + - apt: fix a configuration leak in the apt.get_pkg_candidate_version + function (LP: #2012642) + + -- Renan Rodrigo Thu, 23 Mar 2023 13:41:05 -0300 + +ubuntu-advantage-tools (27.14~23.04.1) lunar; urgency=medium + + * d/ubuntu-advantage-tools.{postinst,postrm,preinst}: + - migrate certain settings out of uaclient.conf to a new file managed by + the pro config subcommand (LP: #2004280) + * d/ubuntu-advantage-tools.postinst: + - refactor PREVIOUS_PKG_VER as a global variable + - simplify how we add notices + * New upstream release 27.14 (LP: #2011477) + - api: new u.unattended_upgrades.status.v1 endpoint for querying status of + unattended upgrades + - apt: + + remove legacy apt-hook + + deliver json apt-hook for interim releases + + fix cloud identification logic in json apt-hook + + make all calls to esm-cache isolated from system configuration + (LP: #2008280) + + only set up the esm cache on supported systems (LP: #2004018) + - fix: + + format the output to be more readable (LP: #1926182) + + add option to attach during a fix without a token + + verify if fixed version can be installed before trying (LP: #2006705) + - livepatch: show warning if current kernel is not supported + - locks: alert user about corrupted lock files (LP: #1996931) + - logging: logs are now formatted as jsonlines + - motd: remove esm-apps announcement + - notices: new representation on disk as separate files (LP: #1987738) + - realtime: remove ubuntu-realtime package on disablement + - status: + + removed contract info update check network call + + no longer includes warnings about notices when non-root (LP: #2006138) + + unattached status sends virt type to contract server for better + resource availability calculation + - timer jobs: add daily job to check for contract updates + - yaml: always import distro-provided pyyaml (LP: #2007234, LP: #2007241) + + -- Grant Orndorff Mon, 13 Mar 2023 15:28:33 -0400 ubuntu-advantage-tools (27.13.6~23.04.1) lunar; urgency=medium diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/rules ubuntu-advantage-tools-27.14.4~16.04/debian/rules --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/rules 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/rules 2023-04-05 15:14:00.000000000 +0000 @@ -66,11 +66,7 @@ # We install the conf file even on non-LTS version to avoid issues on upgrade scenarios make -C apt-hook DESTDIR=$(CURDIR)/debian/ubuntu-advantage-tools install-conf - -# Hooks will only be delivered on LTS instances -ifeq (LTS,$(findstring LTS,$(VERSION))) make -C apt-hook DESTDIR=$(CURDIR)/debian/ubuntu-advantage-tools install -endif # We want to guarantee that we are not shipping any conftest files find $(CURDIR)/debian/ubuntu-advantage-tools -type f -name conftest.py -delete diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.maintscript ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.maintscript --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.maintscript 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.maintscript 2023-04-05 15:14:00.000000000 +0000 @@ -3,3 +3,4 @@ rm_conffile /etc/update-motd.d/80-livepatch 19.1~ ubuntu-advantage-tools rm_conffile /etc/cron.daily/ubuntu-advantage-tools 19.1~ ubuntu-advantage-tools rm_conffile /etc/init/ua-auto-attach.conf 20.2~ ubuntu-advantage-tools +rm_conffile /etc/update-motd.d/88-esm-announce 27.14~ ubuntu-advantage-tools diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.postinst ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.postinst --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.postinst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.postinst 2023-04-05 15:14:00.000000000 +0000 @@ -57,6 +57,20 @@ TMP_CANDIDATE_CACHE_PATH="/tmp/ubuntu-advantage/candidate-version" +NOTICES_DIR="/var/lib/ubuntu-advantage/notices" +TEMP_NOTICES_DIR="/run/ubuntu-advantage/notices" + +add_notice() { + notice=$1 + mkdir -p $NOTICES_DIR + touch $NOTICES_DIR/$notice +} +add_temp_notice() { + notice=$1 + mkdir -p $TEMP_NOTICES_DIR + touch $TEMP_NOTICES_DIR/$notice +} + # Rename apt config files for ua services removing ubuntu release names redact_ubuntu_release_from_ua_apt_filenames() { DIR=$1 @@ -140,27 +154,15 @@ mark_reboot_for_fips_pro() { FIPS_HOLDS=$(apt-mark showholds | grep -E 'fips|libssl1|openssh-client|openssh-server|linux-fips|openssl|strongswan' || exit 0) if [ "$FIPS_HOLDS" ]; then - mark_reboot_cmds_as_needed FIPS_REBOOT_REQUIRED_MSG + mark_reboot_cmds_as_needed + add_temp_notice 20-fips_reboot_required fi } - -add_notice() { - msg_name=$1 - /usr/bin/python3 -c " -from uaclient.config import UAConfig -from uaclient.messages import ${msg_name} -cfg = UAConfig(root_mode=True) -cfg.notice_file.add(label='', description=${msg_name}) -" -} - mark_reboot_cmds_as_needed() { - msg_name=$1 if [ ! -f "$REBOOT_CMD_MARKER_FILE" ]; then touch $REBOOT_CMD_MARKER_FILE fi - add_notice "$msg_name" } patch_status_json_0_1_for_non_root() { @@ -195,7 +197,7 @@ if echo "$cloud_id" | grep -E -q "^(azure|aws)"; then if echo "$fips_installed" | grep -E -q "installed"; then - add_notice NOTICE_WRONG_FIPS_METAPACKAGE_ON_CLOUD + add_notice 25-wrong_fips_metapackage_on_cloud fi fi } @@ -209,7 +211,6 @@ # then we will assume that the user would want the # ua-timer to be disabled as well. In that case, we will # disable the ua-timer here. - PREVIOUS_PKG_VER=$1 # We should only perform this check on UA version that have the # ua-messaging.timer: 27.0 until 27.2. This will also guarantee @@ -236,7 +237,6 @@ } remove_old_systemd_units() { - PREVIOUS_PKG_VER=$1 # These are the commands that are run when the package is purged. # Since we actually want to remove this service from now on # we have replicated that behavior here @@ -282,12 +282,11 @@ from uaclient.files import MachineTokenFile machine_token_file = MachineTokenFile() content = machine_token_file.read() -machine_token_file.write(content=content) +machine_token_file.write(content) " } migrate_ubuntu_pro_beta_banner() { - PREVIOUS_PKG_VER=$1 # This only shipped in 27.11.2~ if dpkg --compare-versions "$PREVIOUS_PKG_VER" ge "27.11.2~" \ && dpkg --compare-versions "$PREVIOUS_PKG_VER" lt "27.11.3~"; then @@ -302,6 +301,25 @@ fi fi } +migrate_old_notices(){ + notices_file=/var/lib/ubuntu-advantage/notices.json + # only run if notices.json is present + if [ ! -f $notices_file ];then + return + fi + + # This migration will happen for pro client versions <27.14 that still use notices.json. + # Notices are generally short lived, so the chances of an upgrade happening while a + # notice is in place is very small. + # Despite that we do a simple migration here of the most important notices: reboot required notices. + # All notices with "reboot" in them can be safely transformed into a generic reboot required message. + # The new message won't include the exact reason for the reboot, but the recommended action is the same. + if grep -q -i "reboot" $notices_file; then + add_temp_notice 10-reboot_required + fi + rm -f $notices_file +} + cleanup_candidate_version_stamp_permissions() { if [ -f $TMP_CANDIDATE_CACHE_PATH ]; then @@ -315,6 +333,32 @@ fi } +cleanup_old_motd_files() { + rm -rf $UA_MESSAGES_DIR/motd* +} + +migrate_user_config_post() { + # LP: #2004280 + preinst_file="/etc/ubuntu-advantage/uaclient.conf.preinst-backup" + bkp_file="/etc/ubuntu-advantage/uaclient.conf.dpkg-bak" + + if [ -f /etc/ubuntu-advantage/uaclient.conf.preinst-backup ]; then + # This script modifies the preinst-backup version of the file in-place + /usr/bin/python3 /usr/lib/ubuntu-advantage/migrate_user_config.py + + if cmp --silent $preinst_file $bkp_file; then + # This should only happen if we failed to perform the migration. + # Therefore, there is no need to keep the backup file around + rm -f $bkp_file + fi + + # Overwrite uaclient.conf with the now-migrated version from preinst + mv $preinst_file /etc/ubuntu-advantage/uaclient.conf + # just in case this temp file was left behind + rm -f /etc/ubuntu-advantage/uaclient.conf.preinst-backup-migrated-temp + fi +} + case "$1" in configure) PREVIOUS_PKG_VER=$2 @@ -343,7 +387,7 @@ # Repo for FIPS packages changed from old client if [ -f $FIPS_APT_SOURCE_FILE ]; then if grep -q $OLD_CLIENT_FIPS_PPA $FIPS_APT_SOURCE_FILE; then - add_notice FIPS_INSTALL_OUT_OF_DATE + add_notice 22-fips_install_out_of_date fi fi @@ -369,7 +413,8 @@ if [ "$VERSION_ID" = "16.04" ]; then if echo "$PREVIOUS_PKG_VER" | grep -q "14.04"; then - mark_reboot_cmds_as_needed LIVEPATCH_LTS_REBOOT_REQUIRED + mark_reboot_cmds_as_needed + add_temp_notice 30-lp_lts_reboot_required fi if dpkg --compare-versions "$PREVIOUS_PKG_VER" lt "27.13~"; then # Clean any unauthenticated ESM infra files previously inserted @@ -385,8 +430,8 @@ mark_reboot_for_fips_pro rm_old_license_check_marker - disable_new_timer_if_old_timer_already_disabled $PREVIOUS_PKG_VER - remove_old_systemd_units $PREVIOUS_PKG_VER + disable_new_timer_if_old_timer_already_disabled + remove_old_systemd_units /usr/lib/ubuntu-advantage/cloud-id-shim.sh || true # On old version of ubuntu-advantange-tools, we don't have a public @@ -395,27 +440,25 @@ if [ -f $MACHINE_TOKEN_FILE ] && [ ! -f $PUBLIC_MACHINE_TOKEN_FILE ]; then create_public_machine_token_file fi - migrate_ubuntu_pro_beta_banner $PREVIOUS_PKG_VER + migrate_ubuntu_pro_beta_banner cleanup_candidate_version_stamp_permissions if dpkg --compare-versions "$PREVIOUS_PKG_VER" lt "27.13~"; then cleanup_apt_news_flag_file fi - - # LP: #2003977 - # The version gate is open ended, and should be closed when the apt_news - # configuration is moved away from a conffile. - if dpkg --compare-versions "$2" ge 27.11.3~; then - if [ -f /var/lib/ubuntu-advantage/preinst-detected-apt-news-disabled ]; then - # The preinst has left us a note to tell us that apt-news was - # previously disabled and should be re-disabled, so disable it - # and discard the note. - pro config set apt_news=false - rm /var/lib/ubuntu-advantage/preinst-detected-apt-news-disabled - fi - # Remove the conffile backup if it exists now that an error unwind is - # no longer possible - rm -f /etc/ubuntu-advantage/uaclient.conf.preinst-remove + if dpkg --compare-versions "$PREVIOUS_PKG_VER" lt "27.14~"; then + cleanup_old_motd_files + migrate_old_notices + migrate_user_config_post + fi + + if grep -q "ua_config:" /etc/ubuntu-advantage/uaclient.conf; then + echo "Warning: uaclient.conf contains old ua_config field." >&2 + echo " Please do the following:" >&2 + echo " 1. Run 'pro config set field=value' for each field/value pair" >&2 + echo " present under ua_config in /etc/ubuntu-advantage/uaclient.conf" >&2 + echo " 2. Delete ua_config and all sub-fields in" >&2 + echo " /etc/ubuntu-advantage/uaclient.conf" >&2 fi ;; esac diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.postrm ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.postrm --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.postrm 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.postrm 2023-04-05 15:14:00.000000000 +0000 @@ -31,12 +31,9 @@ remove_gpg_files ;; abort-install|abort-upgrade) - # LP: #2003977 - # The version gate is open ended, and should be closed when the - # apt_news configuration is moved away from a conffile. - if dpkg --compare-versions "$2" ge 27.11.3~; then - rm -f /var/lib/ubuntu-advantage/preinst-detected-apt-news-disabled - [ -f /etc/ubuntu-advantage/uaclient.conf.preinst-remove ] && mv /etc/ubuntu-advantage/uaclient.conf.preinst-remove /etc/ubuntu-advantage/uaclient.conf + # LP: #2004280 + if dpkg --compare-versions "$2" lt "27.14~"; then + [ -f /etc/ubuntu-advantage/uaclient.conf.preinst-backup ] && mv /etc/ubuntu-advantage/uaclient.conf.preinst-backup /etc/ubuntu-advantage/uaclient.conf fi ;; esac diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.preinst ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.preinst --- ubuntu-advantage-tools-27.13.6~16.04.1/debian/ubuntu-advantage-tools.preinst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/debian/ubuntu-advantage-tools.preinst 2023-04-05 15:14:00.000000000 +0000 @@ -2,100 +2,34 @@ set -e -remove_old_config_fields() { - PREVIOUS_PKG_VER="$1" - if dpkg --compare-versions "$PREVIOUS_PKG_VER" le "27.8"; then - if grep -q "^license_check_log_file:" /etc/ubuntu-advantage/uaclient.conf; then - sed -i '/^license_check_log_file:.*$/d' /etc/ubuntu-advantage/uaclient.conf || true - fi + +migrate_user_config_pre() { + if [ ! -f /etc/ubuntu-advantage/uaclient.conf ]; then + return fi -} -restore_previous_conffile() { - previous_pkg_version=$1 - # Back up existing conffile in case of an error unwind - cp -a /etc/ubuntu-advantage/uaclient.conf /etc/ubuntu-advantage/uaclient.conf.preinst-remove - - if dpkg --compare-versions "$previous_pkg_version" ge 27.13.1~; then - # Restore the default conffile that shipped with 27.13.X - cat > /etc/ubuntu-advantage/uaclient.conf < /etc/ubuntu-advantage/uaclient.conf < /etc/ubuntu-advantage/uaclient.conf < ../ubuntu-advantage-tools_*.dsc # emulating different architectures in sbuild-launchpad-chroot -sbuild-launchpad-chroot create --architecture="riscv64" "--name=focal-riscv64" "--series=focal +sbuild-launchpad-chroot create --architecture="riscv64" "--name=focal-riscv64" "--series=focal" ``` > **Note** diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/dev-docs/howtoguides/detach_pro_instances.md ubuntu-advantage-tools-27.14.4~16.04/dev-docs/howtoguides/detach_pro_instances.md --- ubuntu-advantage-tools-27.13.6~16.04.1/dev-docs/howtoguides/detach_pro_instances.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/dev-docs/howtoguides/detach_pro_instances.md 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,25 @@ +# How to permanently detach Pro instances + +## TL;DR + +1. Modify the client configuration file, normally `/etc/ubuntu-advantage/uaclient.conf`, +to contain: + + ```yaml + features: + disable_auto_attach: true + ``` + +2. Perform a `sudo pro detach --assume-yes`. + +## Explanation + +On Pro instances, a `pro detach` won't permanently detach them as, +the instance will be reauto-attached on the next boot (on non GCE instances) +or immediately (on GCE instances due to the daemon). + +The config in step 1 will prevent the [daemon](../../systemd/ubuntu-advantage.service) +and the a [service](../../systemd/ua-auto-attach.service) at next boot to auto-reattach. + +If you want to allow the instance to reauto attach by itself, then remove or set to false +`disable_auto_attach` in the configuration file. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/dev-docs/howtoguides/testing.md ubuntu-advantage-tools-27.14.4~16.04/dev-docs/howtoguides/testing.md --- ubuntu-advantage-tools-27.13.6~16.04.1/dev-docs/howtoguides/testing.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/dev-docs/howtoguides/testing.md 2023-04-05 15:14:00.000000000 +0000 @@ -16,6 +16,11 @@ tox ``` +> **Note** +> There are a number of `autouse` mocks in our unit tests. These are intended to prevent accidental side effects on the host system from running the unit tests, as well as prevent leaks of the system environment into the unit tests. +> One such `autouse` mock tells the unit tests that they are run as root (unless the mock is overriden for a particular test). +> These `autouse` mocks have helped, but may not be preventing all side effects or environment leakage. + The client also includes built-in dep8 tests. These are run as follows: ```shell diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/conf.py ubuntu-advantage-tools-27.14.4~16.04/docs/conf.py --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/conf.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/conf.py 2023-04-05 15:14:00.000000000 +0000 @@ -14,18 +14,20 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +import datetime # -- Project information ----------------------------------------------------- project = "Ubuntu Pro Client" -copyright = "2022, Canonical Ltd." - +author = "Canonical Group Ltd" +copyright = "%s, %s" % (datetime.date.today().year, author) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. + extensions = [ "myst_parser", "sphinx_copybutton", @@ -33,26 +35,62 @@ ] # Add any paths that contain templates here, relative to this directory. + templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. + exclude_patterns = [] # It seems we need to request creation of automatic anchors for our headings. # Setting to 2 because that's what we need now. # If referencing any heading of lesser importance, adjust here. -myst_heading_anchors = 2 + +myst_heading_anchors = 3 # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# +# a list of builtin themes: +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + html_theme = "furo" html_logo = "_static/circle_of_friends.png" +html_theme_options = { + "light_css_variables": { + "color-sidebar-background-border": "none", + "font-stack": "Ubuntu, -apple-system, Segoe UI, Roboto, Oxygen, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif", + "font-stack--monospace": "Ubuntu Mono variable, Ubuntu Mono, Consolas, Monaco, Courier, monospace", + "color-foreground-primary": "#111", + "color-foreground-secondary": "var(--color-foreground-primary)", + "color-foreground-muted": "#333", + "color-background-secondary": "#FFF", + "color-background-hover": "#f2f2f2", + "color-brand-primary": "#111", + "color-brand-content": "#06C", + "color-inline-code-background": "rgba(0,0,0,.03)", + "color-sidebar-link-text": "#111", + "color-sidebar-item-background--current": "#ebebeb", + "color-sidebar-item-background--hover": "#f2f2f2", + "sidebar-item-line-height": "1.3rem", + "color-link-underline": "var(--color-background-primary)", + "color-link-underline--hover": "var(--color-background-primary)", + }, + "dark_css_variables": { + "color-foreground-secondary": "var(--color-foreground-primary)", + "color-foreground-muted": "#CDCDCD", + "color-background-secondary": "var(--color-background-primary)", + "color-background-hover": "#666", + "color-brand-primary": "#fff", + "color-brand-content": "#06C", + "color-sidebar-link-text": "#f7f7f7", + "color-sidebar-item-background--current": "#666", + "color-sidebar-item-background--hover": "#333", + }, +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -62,6 +100,7 @@ html_css_files = [ "css/logo.css", "css/github_issue_links.css", + "css/custom.css" ] html_js_files = [ "js/github_issue_links.js", diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/apt_messages.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/apt_messages.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/apt_messages.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/apt_messages.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,131 +1,155 @@ -# Ubuntu Pro related APT messages +# Ubuntu Pro-related APT messages -When running some APT commands, you might see Ubuntu Pro related messages on +When running some APT commands, you might see Ubuntu Pro-related messages in the output of those commands. Currently, we deliver those messages when -running either `apt-get upgrade` or `apt-get dist-upgrade` commands. The scenarios +running either `apt upgrade` or `apt dist-upgrade`. The scenarios where we deliver those messages are: -* **ESM series with esm-infra service disabled**: When you are running a machine - with an ESM series, like Xenial, we advertise the `esm-infra` service if packages could - be upgraded by enabling the service: - - ``` - Reading package lists... Done - Building dependency tree - Reading state information... Done - Calculating upgrade... Done - The following package was automatically installed and is no longer required: - libfreetype6 - Use 'apt autoremove' to remove it. - Get more security updates through Ubuntu Pro with 'esm-infra' enabled - libpam0g libpam-modules openssl ntfs-3g git-man libsystemd0 squashfs-tools git openssh-sftp-server udev libpam-runtime isc-dhcp-common libx11-6 libudev1 apport python3-apport systemd-sysv liblz4-1 libpam-systemd systemd libpam-modules-bin openssh-server libx11-data openssh-client libxml2 curl isc-dhcp-client python3-problem-report libcurl3-gnutls libssl1.0.0 - Learn more about Ubuntu Pro for 16.04 at https://ubuntu.com/16-04 - ``` - - Note that the ESM message is located in the middle of the `apt-get` command output. Additionally, - if there are no packages to upgrade at the moment, we would instead deliver: - - ``` - Receive additional future security updates with Ubuntu Pro. - Learn more about Ubuntu Pro for 16.04 at https://ubuntu.com/16-04 - ``` - - ```{note} - If the user is using a LTS series instead, we will advertise `esm-apps`. - ``` - -* **esm package count**: If both ESM services are enabled on the system, - we deliver a package count related to each service near the end of the `apt-get` command: - - ``` - 1 standard LTS security update, 29 esm-infra security updates and 8 esm-apps security updates - ``` - - We only deliver that message if the service is enabled and we did upgrade packages related - to it. For example, if we had no `esm-infra` package upgrades, the message would be: - - ``` - 1 standard LTS security update and 8 esm-apps security updates - ``` - -* **expired contract**: If we detect that your contract is expired, we will deliver the following - message advertising `esm-infra` in the middle of the `apt` command: - - ``` - *Your Ubuntu Pro subscription has EXPIRED* - Get more security updates through Ubuntu Pro with 'esm-infra' enabled - libpam0g libpam-modules openssl ntfs-3g git-man libsystemd0 squashfs-tools git openssh-sftp-server udev libpam-runtime isc-dhcp-common libx11-6 libudev1 apport python3-apport systemd-sysv liblz4-1 libpam-systemd systemd libpam-modules-bin openssh-server libx11-data openssh-client libxml2 curl isc-dhcp-client python3-problem-report libcurl3-gnutls libssl1.0.0 - Renew your service at https://ubuntu.com/pro - ``` - - Note that if we don't have any package to upgrade related to `esm-infra`, we would deliver instead - the message: +## ESM series with esm-infra service disabled - ``` - *Your Ubuntu Pro subscription has EXPIRED* - Renew your service at https://ubuntu.com/pro - ``` - -* **contract is about to expire**: Similarly, if we detect that your contract is about to expire, - we deliver the following message in the middle of the `apt-get` command: - - ``` - CAUTION: Your Ubuntu Pro subscription will expire in 2 days. - Renew your subscription at https://ubuntu.com/pro to ensure continued security - coverage for your applications. - ``` - -* **contract expired, but in grace period**: Additionally, if we detect that the contract is - expired, but still in the grace period, the following message will be seen in the middle - of the `apt-get` command: - - ``` - CAUTION: Your Ubuntu Pro subscription expired on 10 Sep 2021. - Renew your subscription at https://ubuntu.com/pro to ensure continued security - coverage for your applications. - Your grace period will expire in 8 days. - ``` +When you run `apt upgrade` on an ESM release, like Xenial, we advertise +the `esm-infra` service if packages could be upgraded by enabling the service: -```{note} -For contract expired messages, we only advertise `esm-infra`. +``` +Reading package lists... Done +Building dependency tree +Reading state information... Done +Calculating upgrade... Done +The following package was automatically installed and is no longer required: + libfreetype6 + Use 'apt autoremove' to remove it. +The following security updates require Ubuntu Pro with 'esm-infra' enabled: + libpam0g libpam-modules openssl ntfs-3g git-man libsystemd0 squashfs-tools git openssh-sftp-server udev libpam-runtime isc-dhcp-common libx11-6 libudev1 apport python3-apport systemd-sysv liblz4-1 libpam-systemd systemd libpam-modules-bin openssh-server libx11-data openssh-client libxml2 curl isc-dhcp-client python3-problem-report libcurl3-gnutls libssl1.0.0 +Learn more about Ubuntu Pro for 16.04 at https://ubuntu.com/16-04 +``` + +## LTS series with esm-apps service disabled + +When you are running `apt upgraded` on a LTS release, like Focal, we advertise +the `esm-apps` service if packages could be upgraded by enabling the service: + +``` +Reading package lists... Done +Building dependency tree +Reading state information... Done +Calculating upgrade... Done +The following package was automatically installed and is no longer required: + libfreetype6 +Use 'apt autoremove' to remove it. +Get more security updates through Ubuntu Pro with 'esm-apps' enabled: + adminer editorconfig ansible +Learn more about Ubuntu Pro at https://ubuntu.com/pro +0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. ``` +## ESM package count -## How are the APT messages generated +If both ESM services are enabled on the system, we deliver a package count +related to each service near the end of the `apt` command: -We have two distinct `apt` hooks that allow us to deliver those messages when you -are running `apt-get upgrade` or `apt-get dist-upgrade`, they are: +``` +1 standard LTS security update, 29 esm-infra security updates and 8 esm-apps security updates +``` + +We only deliver this message if the service is enabled *and* we upgraded +packages related to it. For example, if we had no `esm-infra` package upgrades, +the message would be: + +``` +1 standard LTS security update and 8 esm-apps security updates +``` -* **apt-esm-hook**: Responsible for delivering the contract expired and ESM services - advertising. However, the messaging here is created by two distinct steps: +## Expired contract - 1. Our [update_messages](what_are_the_timer_jobs.md) timer job create templates for - the APT messages this hook will deliver. We cannot create the full message on the - timer job, because we need the accurate package names and count. That information - can only be obtained when running the `apt-get` command. +If we detect that your contract is expired, we will deliver the following +message advertising `esm-infra` in the middle of the `apt upgrade` command: + +``` +# +# *Your Ubuntu Pro subscription has EXPIRED* +# 10 additional security update(s) require Ubuntu Pro with '{service}' enabled. +# Renew your service at https://ubuntu.com/pro +# +``` + +If we don't have any `esm-infra`-related packages to upgrade, we would show the +following message instead: + +``` +# +# *Your Ubuntu Pro subscription has EXPIRED* +# Renew your service at https://ubuntu.com/pro +# +``` + +## Contract is about to expire + +Similarly, if we detect that your contract is about to expire, we deliver the +following message in the middle of the `apt` command: + +``` +# +# CAUTION: Your Ubuntu Pro subscription will expire in 2 days. +# Renew your subscription at https://ubuntu.com/pro to ensure continued +# security coverage for your applications. +# +``` + +## Contract has expired, but still in grace period + +Additionally, if we detect that the contract has expired, but is still in the +grace period, the following message will be seen in the middle of the `apt` +command output: + +``` +# +# CAUTION: Your Ubuntu Pro subscription expired on 10 Sep 2021. +# Renew your subscription at https://ubuntu.com/pro to ensure continued +# security coverage for your applications. +# Your grace period will expire in 11 days. +# +``` - ```{note} - These templates will only be produced if some conditions are met. For example, - we only produce expired contract templates if the contracts are indeed expired. - ``` +## How are the APT messages generated? - 2. When you run either `apt-get upgrade` or `apt-get dist-upgrade`, the hook searches - for these templates and if they exist, they are populated with the right `apt` - content and delivered to the user. +We have two distinct `apt` hooks that allow us to deliver these messages when +you run `apt upgrade` or `apt dist-upgrade`. They are: -* **apt-esm-json-hook**: The json hook is responsible for delivering the package count - message we mentioned on the `esm package count` item. This hook is used because - to inject that message on the exact place we want, we need to use a specific apt` - [json hook](https://salsa.debian.org/apt-team/apt/-/blob/main/doc/json-hooks-protocol.md) - to communicate with. +### `apt-esm-hook` +Responsible for populating templates with accurate package counts (i.e. the package +count we see on the Expired contract messages). +However, the messaging here is created by two distinct steps: + +1. Our [update_messages](what_are_the_timer_jobs.md) timer job creates + templates for the APT messages this hook will deliver. We cannot create the + full message on the timer job, because we need the accurate package names + and count. This information can only be obtained when running the `apt` + command. + + ```{note} + These templates will only be produced if certain conditions are met. For + example, we only produce "expired contract" templates if the contracts are + indeed expired. + ``` + +2. When you run either `apt upgrade` or `apt dist-upgrade`, the hook + searches for these templates and if they exist, they are populated with the + correct `apt` content and delivered to the user. + +### `apt-esm-json-hook` + +The JSON hook is responsible for delivering the rest of the message we have presented here. +This hook is used to inject the message in the exact place we want, so we need to use a specific `apt` +[JSON hook](https://salsa.debian.org/apt-team/apt/-/blob/main/doc/json-hooks-protocol.md) +to communicate with it. ```{note} Those hooks are only delivered on LTS releases. This is because the hooks will not deliver useful messages on non-LTS due to lack of support for ESM services. ``` -## How are APT configured to deliver those messages +## How are APT configured to deliver those messages? We currently ship the package the `20apt-esm-hook.conf` configuration that configures both the basic apt hooks to call our `apt-esm-hook` binary, and also diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/errors_explained.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/errors_explained.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/errors_explained.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/errors_explained.md 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,101 @@ +# Errors you may encounter and their meaning + +If you encounter an error or warning message from Pro Client that you don't understand and cannot find it in this document, please click "Have a question?" at the top of the page and let us know so that we can add it. + +## User Configuration Migration in version 27.14 + +Version 27.14 of Ubuntu Pro Client changed how some user configuration settings are stored on disk. It moved several settings out of `/etc/ubuntu-advantage/uaclient.conf` and into a file managed solely by the `pro config {set,unset,show}` subcommands. + +Most settings should've gotten automatically migrated to the new file when Pro Client upgraded. If something failed you may see one of the following messages: + +### Migration error 1 +**Error message:** +``` +Warning: Failed to load /etc/ubuntu-advantage/uaclient.conf + No automatic migration will occur. + You may need to use "pro config set" to re-set your settings. +``` + +**Where you'll see it:** + +During an `apt upgrade` or `apt install ubuntu-advantage-tools` + +**What does it mean:** + +This means that `/etc/ubuntu-advantage/uaclient.conf` was unable to be read or unable to be parsed as yaml during the migration. + +**What you can do about it:** + +Check the contents of `/etc/ubuntu-advantage/uaclient.conf`. +1. Ensure it is valid yaml +2. For any setting that is nested under `ua_config`: + - If you modified the value in the past: run `pro config set field_name=your_custom_value` + - delete the setting from `/etc/ubuntu-advantage/uaclient.conf` + - delete the `ua_config:` line from `/etc/ubuntu-advantage/uaclient.conf` + +### Migration error 2 +**Error message:** +``` +Warning: Failed to migrate user_config from /etc/ubuntu-advantage/uaclient.conf + Please run the following to keep your custom settings: + pro config set example=example +``` + +**Where you'll see it:** + +During an `apt upgrade` or `apt install ubuntu-advantage-tools` + +**What does it mean:** + +This means that `/var/lib/ubuntu-advantage/user-config.json` was unable to be written or a json serialization error occurred. + +**What you can do about it:** + +Run each of the `pro config set` commands recommended in the warning message. + +### Migration error 3 +**Error message:** +``` +Warning: Failed to migrate /etc/ubuntu-advantage/uaclient.conf + Please add following to uaclient.conf to keep your config: + example: example +``` + +**Where you'll see it:** + +During an `apt upgrade` or `apt install ubuntu-advantage-tools` + +**What does it mean:** + +This means that `/etc/ubuntu-advantage/uaclient.conf` was unable to be written or a yaml serialization error occurred. + +**What you can do about it:** + +Ensure that the settings listed in the warning output make it into your new uaclient.conf. + +### Warnings in versions >=27.14~ + +**Error message:** +``` +legacy "ua_config" found in uaclient.conf +``` +or +``` +Warning: uaclient.conf contains old ua_config field. +``` + +**Where you'll see it:** + +In `/var/log/ubuntu-advantage.log` after using the `pro` cli or during an `apt upgrade` to a newer version of ubuntu-advantage-tools. + +**What does it mean:** + +This means that there are still settings nested under `ua_config` in `/etc/ubuntu-advantage/uaclient.conf`. These will still be honored, but support may be removed in the future. + +**What you can do about it:** + +Check the contents of `/etc/ubuntu-advantage/uaclient.conf`. +For any setting that is nested under `ua_config`: +- If you modified the value in the past: run `pro config set field_name=your_custom_value` +- delete the setting from `/etc/ubuntu-advantage/uaclient.conf` +- delete the `ua_config:` line from `/etc/ubuntu-advantage/uaclient.conf` diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/how_to_interpret_the_security_status_command.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/how_to_interpret_the_security_status_command.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/how_to_interpret_the_security_status_command.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/how_to_interpret_the_security_status_command.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,10 +1,10 @@ -# How to interpret the security-status command output +# What does `security-status` do? -The `security-status` command is used to get an overview -of the packages installed in your machine. +The `security-status` command is used to get an overview of the packages +installed on your machine. -If you run the `pro security-status --format yaml` command on your -machine, you are expected to see the output following this structure: +If you run the `pro security-status --format yaml` command on your machine, you +should expect to see an output that follows this structure: ``` _schema_version: '0.1' @@ -41,31 +41,44 @@ Patched: true ``` -Let's understand what each key mean on the output of the `pro security-status` command: +Let's understand what each key means in the output of the `pro security-status` +command: -* **`summary`**: The summary of the system related to Ubuntu Pro and - the different package sources in the system: +## `summary` - * **`num_installed_packages`**: The total number of installed packages in the system. - * **`num_esm_apps_packages`**: The number of packages installed from `esm-apps`. - * **`num_esm_apps_updates`**: The number of `esm-apps` package updates available to the system. - * **`num_esm_infra_packages`**: The number of packages installed from `esm-infra`. - * **`num_esm_infra_updates`**: The number of `esm-infra` package updates available to the system. - * **`num_main_packages`**: The number of packages installed from the `main` archive component. - * **`num_multiverse_packages**: The number of packages installed from the `multiverse` archive - component. - * **`num_restricted_packages`**: The number of packages installed from the `restricted` archive - component. - * **`num_third_party_packages`** : The number of packages installed from `third party` sources. - * **`num_universe_packages`**: The number of packages installed from the `universe` archive - component. - * **`num_unknown_packages`**: The number of packages installed from sources not known to `apt` - (installed locally through dpkg or packages without a remote reference). - * **`num_standard_security_updates`**: The number of standard security updates available to the system. - - It is worth mentioning here that the `_updates` fields are presenting the number of **security** - updates for **installed** packages. For example, let's assume your machine has a universe package that - has a security update from `esm-infra`. The count will be displayed as: +This provides a summary of the system related to Ubuntu Pro and the different +package sources in the system: + +* **`num_installed_packages`**: The total number of installed packages on the + system. +* **`num_esm_apps_packages`**: The number of packages installed from `esm-apps`. +* **`num_esm_apps_updates`**: The number of `esm-apps` package updates available + to the system. +* **`num_esm_infra_packages`**: The number of packages installed from + `esm-infra`. +* **`num_esm_infra_updates`**: The number of `esm-infra` package updates + available to the system. +* **`num_main_packages`**: The number of packages installed from the `main` + archive component. +* **`num_multiverse_packages`**: The number of packages installed from the + `multiverse` archive component. +* **`num_restricted_packages`**: The number of packages installed from the + `restricted` archive component. +* **`num_third_party_packages`** : The number of packages installed from + `third party` sources. +* **`num_universe_packages`**: The number of packages installed from the + `universe` archive component. +* **`num_unknown_packages`**: The number of packages installed from sources not + known to `apt` (e.g., those installed locally through `dpkg` or packages + without a remote reference). +* **`num_standard_security_updates`**: The number of standard security updates + available to the system. + +```{note} + It is worth mentioning here that the `_updates` fields are presenting the + number of **security** updates for **installed** packages. For example, let's + assume your machine has a universe package that has a security update from + `esm-infra`. The count will be displayed as: ``` num_esm_infra_packages: 0 @@ -80,30 +93,38 @@ num_esm_infra_updates: 0 num_universe_packages: 0 ``` +``` - * **`ua`**: An object representing the state of Ubuntu Pro on the system: - * **`attached`**: If the system is attached to an Ubuntu Pro subscription. - * **`enabled_services`**: A list of services that are enabled on the system. If unattached, this - will always be an empty list. - * **`entitled_services`**: A list of services that are entitled on your Ubuntu Pro subscription. If - unattached, this will always be an empty list. - -* **`packages`**: A list of security updates for packages installed in the system. - Every entry on the list will follow this structure: - - * **`origin`**: The host were the update comes from. - * **`package`**: The name of the package. - * **`service_name`**: The service that provides that package update. It can be either: `esm-infra`, - `esm-apps` or `standard-security`. - * **`status`**: The status for this update. It will be one of: - * **"upgrade_available"**: The package can be upgraded right now. - * **"pending_attach"**: The package needs an Ubuntu Pro subscription attached to be upgraded. - * **"pending_enable"**: The machine is attached to an Ubuntu Pro subscription, but the service required to - provide the upgrade is not enabled. - * **"upgrade_unavailable"**: The machine is attached, but the contract is not entitled to - the service which provides the upgrade. - * **`version`**: The update version. - * **`download_size`**: The number of bytes that would be downloaded in order to install the update. - -* **`livepatch`**: Livepatch related information. Currently, the only information -presented is **`fixed_cves`** - a list of CVEs that were fixed by Livepatches applied to the kernel. +* **`ua`**: An object representing the state of Ubuntu Pro on the system: + * **`attached`**: If the system is attached to an Ubuntu Pro subscription. + * **`enabled_services`**: A list of services that are enabled on the system. + If unattached, this will always be an empty list. + * **`entitled_services`**: A list of services that are entitled on your + Ubuntu Pro subscription. If unattached, this will always be an empty list. + +## `packages` + +This provides a list of security updates for packages installed on the system. +Every entry on the list will follow this structure: + +* **`origin`**: The host where the update comes from. +* **`package`**: The name of the package. +* **`service_name`**: The service that provides the package update. It can be + one of: `esm-infra`, `esm-apps` or `standard-security`. +* **`status`**: The status for this update. It will be one of: + * **"upgrade_available"**: The package can be upgraded right now. + * **"pending_attach"**: The package needs an Ubuntu Pro subscription attached + to be upgraded. + * **"pending_enable"**: The machine is attached to an Ubuntu Pro subscription, + but the service required to provide the upgrade is not enabled. + * **"upgrade_unavailable"**: The machine is attached, but the contract is not + entitled to the service which provides the upgrade. +* **`version`**: The update version. +* **`download_size`**: The number of bytes that would be downloaded in order to + install the update. + +## `livepatch` + +This displays Livepatch-related information. Currently, the only information +presented is **`fixed_cves`**. This represents a list of CVEs that were fixed +by Livepatches applied to the kernel. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/motd_messages.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/motd_messages.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/motd_messages.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/motd_messages.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,112 +1,111 @@ -# Ubuntu Pro related messages on MOTD +# Ubuntu Pro-related MOTD messages -When Ubuntu Pro Client (`pro`) is installed on the system, it delivers custom messages on [MOTD](https://wiki.debian.org/motd). -Those messages are generated directly by two different sources: +When the Ubuntu Pro Client (`pro`) is installed on the system, it delivers +custom messages on ["Message of the Day" (MOTD)](https://wiki.debian.org/motd). +Those messages are generated directly by two different sources. -* **python script**: The [update-notifier](https://wiki.ubuntu.com/UpdateNotifier) deliver a script - called `apt_check.py`. Considering Ubuntu Pro related information, this script is responsible for: +## Python-scripted MOTD + +The [update-notifier](https://wiki.ubuntu.com/UpdateNotifier) delivers a script +called `apt_check.py`. With regards to Ubuntu Pro, this script is responsible +for: - * inform the user about the status of one of the ESM services, `esm-apps` if the machine is a - LTS series or `esm-infra` if the series is on ESM mode. - * showing the number of `esm-infra` or `esm-apps` packages that can be upgraded in the machine - - For example, this is the output of the `apt_check.py` script on a LTS machine when both of - those services are enabled: - - ``` - Expanded Security Maintenance for Applications is enabled. - - 11 updates can be applied immediately. - 5 of these updates are ESM Apps security updates. - 1 of these updates is a ESM Infra security update. - 5 of these updates are standard security updates. - To see these additional updates run: apt list --upgradable - ``` - - Note that if we were running this on a ESM series, we would instead see `esm-infra` being - advertised: - - ``` - Expanded Security Maintenance Infrastructure is enabled. - - 11 updates can be applied immediately. - 5 of these updates are ESM Apps security updates. - 1 of these updates is a ESM Infra security update. - 5 of these updates are standard security updates. - To see these additional updates run: apt list --upgradable - ``` +* Informing the user about the status of one of the ESM services; `esm-apps` if + the machine is an LTS series, or `esm-infra` if the series is in ESM mode. +* Showing the number of `esm-infra` or `esm-apps` packages that can be upgraded + on the machine. + +For example, here is the output of the `apt_check.py` script on a LTS machine +when both of those services are enabled: + +``` +Expanded Security Maintenance for Applications is enabled. + +11 updates can be applied immediately. +5 of these updates are ESM Apps security updates. +1 of these updates is a ESM Infra security update. +5 of these updates are standard security updates. +To see these additional updates run: apt list --upgradable +``` + +However, if we were running this on an ESM series, we would instead see +`esm-infra` being advertised: + +``` +Expanded Security Maintenance Infrastructure is enabled. + +11 updates can be applied immediately. +5 of these updates are ESM Apps security updates. +1 of these updates is a ESM Infra security update. +5 of these updates are standard security updates. +To see these additional updates run: apt list --upgradable +``` - Now considering the scenario were one of those services is not enabled. For example, if - `esm-apps` was not enabled, the output will be: +Now let's consider a scenario where one of these services is not enabled. For +example, if `esm-apps` was disabled, the output will be: - ``` - Expanded Security Maintenance for Applications is not enabled. +``` +Expanded Security Maintenance for Applications is not enabled. - 6 updates can be applied immediately. - 1 of these updates is a ESM Infra security update. - 5 of these updates are standard security updates. - To see these additional updates run: apt list --upgradable +6 updates can be applied immediately. +1 of these updates is a ESM Infra security update. +5 of these updates are standard security updates. +To see these additional updates run: apt list --upgradable - 5 additional security updates can be applied with ESM Apps - Learn more about enabling ESM Apps for Ubuntu 16.04 at - https://ubuntu.com/16-04 - ``` - - In the end of the output we can see the number of packages that could - be upgraded if that service was enabled. Note that we would deliver the same information - for `esm-infra` if the service was disabled and the series running on the machine is on ESM - state. - -* **Ubuntu Pro timer jobs**: One of the timer jobs Ubuntu Pro has is used to insert additional messages into MOTD. - Those messages will be always delivered before or after the content created by the python - script delivered by `update-notifier`. Those additional messages are generated when `pro` detects - some conditions on the machine. They are: - - * **subscription expired**: When the Ubuntu Pro subscription is expired, `pro` will deliver the following - message after the `update-notifier` message: - - ``` - *Your Ubuntu Pro subscription has EXPIRED* - 2 additional security update(s) require Ubuntu Pro with 'esm-infra' enabled. - Renew your service at https://ubuntu.com/pro - ``` - - * **subscription about to expire**: When the Ubuntu Pro subscription is about to expire, we deliver the - following message after the `update-notifier` message: - - ``` - CAUTION: Your Ubuntu Pro subscription will expire in 2 days. - Renew your subscription at https://ubuntu.com/pro to ensure continued security - coverage for your applications. - ``` - - * **subscription expired but within grace period**: When the Ubuntu Pro subscription is expired, but is - still within the grace period, we deliver the following message after the `update-notifier` - script: - - ``` - CAUTION: Your Ubuntu Pro subscription expired on 10 Sep 2021. - Renew your subscription at https://ubuntu.com/pro to ensure continued security - coverage for your applications. - Your grace period will expire in 9 days. - ``` - - * **advertising esm-apps service**: When we detect that `esm-apps` is supported and not enabled - in the system, we advertise it using the following message that is delivered before the - `update-notifier` message: - - ``` - * Introducing Expanded Security Maintenance for Applications. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription. Free for personal use - - https://ubuntu.com/16-04 - ``` - - Note that we could also advertise the `esm-infra` service instead. This will happen - if you use an ESM release. Additionally, the the url we use to advertise the service is different - based on the series that is running on the machine. - - Additionally, all of those Ubuntu Pro custom messages are delivered into - `/var/lib/ubuntu-advantage/messages`. We also add custom scripts into `/etc/update-motd.d` to - check if those messages exist and if they do, insert them on the full MOTD message. +5 additional security updates can be applied with ESM Apps +Learn more about enabling ESM Apps for Ubuntu 16.04 at +https://ubuntu.com/16-04 +``` + +At the end of the output we can see the number of packages that *could* be +upgraded if that service was enabled. Note that we would deliver the same +information for `esm-infra` if the service was disabled and the series running +on the machine is in ESM state. + +## MOTD through Ubuntu Pro timer jobs + +One of the timer jobs Ubuntu Pro uses can insert additional messages into MOTD. +These messages will be always delivered before or after the content created by +the Python script delivered by `update-notifier`. These additional messages are +generated when `pro` detects that certain conditions on the machine have been +met. They are: + +### Subscription expired + +When the Ubuntu Pro subscription is expired, `pro` will deliver the following +message after the `update-notifier` message: + +``` +*Your Ubuntu Pro subscription has EXPIRED* +2 additional security update(s) require Ubuntu Pro with 'esm-infra' enabled. +Renew your service at https://ubuntu.com/pro +``` + +### Subscription about to expire + +When the Ubuntu Pro subscription is about to expire, we deliver the following +message after the `update-notifier` message: + +``` +CAUTION: Your Ubuntu Pro subscription will expire in 2 days. +Renew your subscription at https://ubuntu.com/pro to ensure continued security +coverage for your applications. +``` + +### Subscription expired but within grace period + +When the Ubuntu Pro subscription has expired, but is still within the grace +period, we deliver the following message after the `update-notifier` script: + +``` +CAUTION: Your Ubuntu Pro subscription expired on 10 Sep 2021. +Renew your subscription at https://ubuntu.com/pro to ensure continued security +coverage for your applications. +Your grace period will expire in 9 days. +``` + +### How are these messages updated and inserted into MOTD? + +1. The contract status is checked periodically in the background when the machine is attached to an Ubuntu Pro contract. +2. If one of the above messages applies to the contract that the machine is attached to, then the message is stored in `/var/lib/ubuntu-advantage/messages/motd-contract-status`. +3. At MOTD generation time, the script located at `/etc/update-motd.d/91-contract-ua-esm-status` checks if `/var/lib/ubuntu-advantage/messages/motd-contract-status` exists and if it does, inserts the message into the full MOTD. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/status_columns.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/status_columns.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/status_columns.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/status_columns.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,7 +1,11 @@ -# Status output explanation +# The `pro status` output explained -When running `pro status` we can observe two different types of outputs, attached vs unattached. -When unattached, users will see the status table containing only three columns: +When running `pro status` we can observe two different types of outputs, which +depend on whether the Ubuntu Pro subscription is attached or unattached. + +## Pro subscription unattached +When unattached, users will see the following status table containing only +three columns: ``` SERVICE AVAILABLE DESCRIPTION @@ -15,12 +19,16 @@ Where: -* **SERVICE**: is the name of service being offered -* **AVAILABLE**: if that service is available on that machine. To verify if a service is available, we - check the machine kernel version, architecture and Ubuntu release it is running. +* **SERVICE**: Is the name of service being offered +* **AVAILABLE**: Shows if that service is available on that machine. To verify + if a service is available, we check the machine kernel version, architecture, + Ubuntu release being used and the machine type (i.e lxd for LXD containers) * **DESCRIPTION**: A short description of the service. -However, if we run the same command when attached, we have an output with 4 columns: +## With Pro subscription attached + +However, if we run the same command when attached, we have an output with 4 +columns: ``` SERVICE ENTITLED STATUS DESCRIPTION @@ -29,46 +37,67 @@ 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 -``` - -Here we can notice that the column **AVAILABLE** no longer applies and we have new columns: - -* **ENTITLED**: If the user subscription allow that service to be enabled -* **STATUS**: The state of that service on the machine. - -It is possible that a service appears as available when running status unattached, but turns -out as not entitled. This happens because even if the service can be enabled on the machine, -if your Ubuntu Pro subscription doesn't allow you to do that, `pro` cannot enable it. - -Additionally, the **STATUS** column allows for three possible states: +``` -* **enabled**: service is enabled in the machine -* **disabled**: service is not currently running -* **n/a**: This means non-applicable. This can happen if the service cannot be enabled on the machine - due to a non-contract restriction. For example, we cannot enable `livepatch` on a container. +You may notice that the column **AVAILABLE** is no longer shown, and instead we +see the following new columns: -### Notices -Notices are information regarding the Ubuntu Pro status which either require some kind of action from the user, or may impact the experience with Ubuntu Pro. +* **ENTITLED**: Shows if the user subscription allows that service to be + enabled. +* **STATUS**: Reports the state of that service on the machine. + +It is possible that a service could appear as "available" when the Pro status is +unattached, but then shows as "not entitled" if the subscription is later +attached. This happens because even if the service is available, if your Ubuntu +Pro subscription doesn't allow you access to a service, `pro` cannot enable it. + +The **STATUS** column allows for three possible states: + +* **enabled**: The service is enabled on the machine. +* **disabled**: The service is not currently running. +* **n/a**: This means "not applicable". This will show if the service cannot be + enabled on the machine due to a non-contract restriction. For example, we + cannot enable `livepatch` on a container. + +## Notices + +"Notices" are information regarding the Ubuntu Pro status which either require +some kind of action from the user, or may impact the experience with Ubuntu Pro. + +For example, let's say FIPS was just enabled, but the system wasn't rebooted +yet (which is required for booting into the FIPS Kernel). The output of +`pro status` in this case will contain: -For example, let's say FIPS was just enabled, but the system wasn't rebooted yet (which is needed for booting into the FIPS Kernel), the output of `pro status` will contain: ``` NOTICES FIPS support requires system reboot to complete configuration. ``` + After the system is rebooted, the notice will go away. -Notices can always be resolved, and the way to resolve it should be explicit in the notice itself. +Notices can always be resolved, and the instructions on how to resolve it will +be explicitly stated in the notice itself. + +## Features + +"Features" are extra configuration values that can be set and unset in +`uaclient.conf`. Most of these are meant for development/testing purposes, but +some can be used in application flows. For example, to always have beta services +with the same flow as the non-beta (for `enable`, `status`, etc.), +`uaclient.conf` may have: -### Features -Features are extra configuration values that can be set/unset in `uaclient.conf`. Most of those are meant for development/testing purposes, but some can be used in application flows. For example, to always have beta services with the same flow as the non-beta (for enable, status, etc), `uaclient.conf` may have: ``` features: allow_beta: true ``` + In this case, the output of `pro status` will contain: + ``` FEATURES allow_beta: True ``` -Keep in mind that any feature defined like this will be listed, even if it is invalid or typed the wrong way. Those appear on status for information/debugging purposes. +It's important to keep in mind that any feature defined like this will be +listed, even if it is invalid or typed the wrong way. Those appear in `status` +output for informational and debugging purposes. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_are_the_timer_jobs.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_are_the_timer_jobs.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_are_the_timer_jobs.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_are_the_timer_jobs.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,19 +1,26 @@ -# Timer jobs +# Timer jobs explained -Ubuntu Pro Client (`pro`) sets up a systemd timer to run jobs that need to be executed recurrently. Everytime the -timer runs, it decides which jobs need to be executed based on their intervals. When a job runs -successfully, its next run is determined by the interval defined for that job. +Ubuntu Pro Client (`pro`) sets up a `systemd` timer to run jobs that need to be +carried out periodically. Every time the timer runs, it decides which jobs need +to be performed based on their intervals. When a job runs successfully, its next +run is determined by the interval defined for that job. ## Current jobs -The jobs that `pro` runs periodically are: +The jobs that `pro` runs periodically are `metering` and `update_messaging`. -| Job | Description | Interval | -| --- | ----------- | -------- | -| update_messaging | Update MOTD and APT messages | 6 hours | -| metering | (Only when attached to Ubuntu Pro services) Pings Canonical servers for contract metering | 4 hours | - -- The `update_messaging` job makes sure that the MOTD and APT messages match the -available/enabled services on the system, showing information about available -packages or security updates. -- The `metering` will inform Canonical on which services are enabled on the machine. +### The `update_messaging` job + +`update_messaging` updates the MOTD and APT messages every 6 hours. It ensures +that the MOTD and APT messages displayed on the system match those that are +available/enabled. It finds updated information about available packages or +security updates and shows these to the user. + +### The `metering` job + +`metering` pings the Canonical servers for contract metering every 4 hours. It +informs Canonical which services are enabled on the machine. + +```{note} +The `metering` job only runs when attached to an Ubuntu Pro subscription. +``` diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_are_ubuntu_pro_cloud_instances.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_are_ubuntu_pro_cloud_instances.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_are_ubuntu_pro_cloud_instances.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,15 +1,28 @@ -# What is a Public Cloud Ubuntu Pro machine? +# About Public Cloud Ubuntu Pro images -Ubuntu Pro images are published to [AWS](https://ubuntu.com/aws/pro), [Azure](https://ubuntu.com/azure/pro) and [GCP](https://ubuntu.com/gcp/pro) which come with Ubuntu -Pro support and services built in. On first boot, Ubuntu Pro images will automatically attach -to an Ubuntu Pro support contract and enable necessary Ubuntu Pro services so -that no extra setup is required to ensure a secure and supported Ubuntu machine. - -There are two primary flavors of Ubuntu Pro images in clouds: - -* Ubuntu Pro: Ubuntu LTS images with attached Ubuntu Pro support with kernel Livepatch and -ESM security access already enabled. Ubuntu Pro images are entitled to enable any additional Ubuntu Pro -services (like [`fips`](../howtoguides/enable_fips.md) or [`usg`](../howtoguides/enable_cis.md)). -* Ubuntu Pro FIPS: Specialized Ubuntu Pro images for 16.04, 18.04 and 20.04 which come pre-enabled -with the cloud-optimized FIPS-certified kernel and all additional SSL and security hardening -enabled. These images are available as [AWS Ubuntu Pro FIPS](https://ubuntu.com/aws/fips), [Azure Ubuntu Pro FIPS](https://ubuntu.com/azure/fips) and GCP Ubuntu Pro FIPS. +Ubuntu Pro images are published to [AWS](https://ubuntu.com/aws/pro), +[Azure](https://ubuntu.com/azure/pro) and [GCP](https://ubuntu.com/gcp/pro). +All of these come with Ubuntu Pro support and services built in. + +On first boot, Ubuntu Pro images will automatically attach to an Ubuntu Pro +support contract and enable all necessary Ubuntu Pro services so that no extra +setup is required to ensure a secure and supported Ubuntu machine. + +There are two primary flavors of Ubuntu Pro images in clouds: *Ubuntu Pro*, and +*Ubuntu Pro FIPS*. + +## Ubuntu Pro + +These Ubuntu LTS images are provided already attached to Ubuntu Pro support, +with kernel Livepatch and ESM security access enabled. Ubuntu Pro images are +entitled to enable any additional Ubuntu Pro services (like +[`fips`](../howtoguides/enable_fips.md) or +[`usg`](../howtoguides/enable_cis.md)). + +## Ubuntu Pro FIPS + +These specialized Ubuntu Pro images for 16.04, 18.04 and 20.04 come pre-enabled +with the cloud-optimized FIPS-certified kernel, as well as all additional SSL +and security hardening enabled. These images are available as +[AWS Ubuntu Pro FIPS](https://ubuntu.com/aws/fips), +[Azure Ubuntu Pro FIPS](https://ubuntu.com/azure/fips) and GCP Ubuntu Pro FIPS. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_is_the_daemon.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_is_the_daemon.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_is_the_daemon.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_is_the_daemon.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,8 +1,12 @@ -# What is the Pro Upgrade Daemon? +# What is the Pro upgrade daemon? -Ubuntu Pro Client sets up a daemon on supported platforms (currently GCP only) to detect if an Ubuntu Pro license is purchased for the machine. If a Pro license is detected, then the machine is automatically attached. +Ubuntu Pro Client sets up a daemon on supported platforms (currently GCP +only) to detect if an Ubuntu Pro license has been purchased for the machine. +If a Pro license is detected, then the machine is automatically attached. -If you are uninterested in Ubuntu Pro services, you can safely stop and disable the daemon using systemctl: +If you are not interested in Ubuntu Pro services and don't want your machine to +be automatically attached to your subscription, you can safely stop and disable +the daemon using `systemctl`: ``` sudo systemctl stop ubuntu-advantage.service diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_is_the_ubuntu_advantage_pro_package.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,4 +1,8 @@ -# What is the ubuntu-advantage-pro package? +# What is the `ubuntu-advantage-pro` package? -The ubuntu-advantage-pro package is used by [Public Cloud Ubuntu Pro](what_are_ubuntu_pro_cloud_instances.md) machines to automate machine attach on boot. -Therefore, the only thing that `ubuntu-advantage-pro` does is ship a systemd unit that runs an auto-attach command on first boot. +The `ubuntu-advantage-pro` package is used by +[Public Cloud Ubuntu Pro](what_are_ubuntu_pro_cloud_instances.md) machines to +automate the process of attaching a machine on boot. + +Therefore, the only thing that `ubuntu-advantage-pro` does is ship a `systemd` +unit that runs an auto-attach command on first boot. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_refresh_does.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_refresh_does.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/what_refresh_does.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/what_refresh_does.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,13 +1,25 @@ -# What refresh does +# What `pro refresh` does -When you run the `pro refresh` command on your machine, three distinct stages are performed: +When you run the `pro refresh` command on your machine, three distinct stages +are performed. -* **contract**: The contract information on the machine is refreshed. If we find any deltas - between the old contract and the new one, we process that delta and apply the changes - on the machine. If you need only this stage during refresh, run `pro refresh contract`. +## Contract -* **config**: If there is any config change made on `/etc/ubuntu-advantage/uaclient.conf`, those - changes will now be applied to the machine. If you need only this stage during refresh, run `pro refresh config`. +The contract information on the machine is refreshed. If we find any deltas +between the old contract and the new one, we process that delta and apply the +changes to the machine. -* **MOTD and APT messages**: Process new MOTD and APT messages and refresh the machine to use - them. If you need only this stage during refresh, run `pro refresh messages`. +If you need *only* this stage during refresh, run `pro refresh contract`. + +## Configuration + +If there is any config change made to `/etc/ubuntu-advantage/uaclient.conf`, +those changes will now be applied to the machine. + +If you need *only* this stage during refresh, run `pro refresh config`. + +## MOTD and APT messages + +Processes new MOTD and APT messages, and refreshes the machine to use them. + +If you need *only* this stage during refresh, run `pro refresh messages`. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/why_trusty_is_no_longer_supported.md ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/why_trusty_is_no_longer_supported.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations/why_trusty_is_no_longer_supported.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations/why_trusty_is_no_longer_supported.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,7 +1,10 @@ -# Why trusty is no longer supported +# Why is 14.04 (Trusty) no longer supported? -On 14.04 (Trusty), the Ubuntu Pro Client package is not receiving upstream updates -beyond version 19.6 plus any critical CVE maintenance related to this version. Version 19.6 already -has full-featured support of the applicable Ubuntu Pro service offerings `esm-infra` and `livepatch`. In the -event that a CVE is discovered that affects the Ubuntu Pro Client version on Trusty, a fix and backport will be -provided for those specific issues. +On 14.04 (Trusty), the Ubuntu Pro Client package is not receiving upstream +updates beyond version 19.6, plus any critical CVE maintenance related to this +version. + +Version 19.6 already has full-featured support of the applicable Ubuntu Pro +service offerings `esm-infra` and `livepatch`. In the event that a CVE is +discovered that affects the Ubuntu Pro Client version on Trusty, a fix and +backport will be provided for those specific issues. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations.rst ubuntu-advantage-tools-27.14.4~16.04/docs/explanations.rst --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/explanations.rst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/explanations.rst 2023-04-05 15:14:00.000000000 +0000 @@ -5,11 +5,50 @@ understanding of how the Ubuntu Pro Client (``pro``) works. They enable you to expand your knowledge and become better at using and configuring ``pro``. -Explanation -=========== +Messaging +========= + +Here you'll find details about Ubuntu Pro Client-related APT and MOTD messages +-- what they are, when they are used and how they work. .. toctree:: :maxdepth: 1 - :glob: - explanations/* + Pro-related APT messages + Pro-related MOTD messages + +Commands +======== + +Some of the commands in ``pro`` do more than you think. Here we'll show you a +selection of some of the commands -- what they do, and how they work. + +.. toctree:: + :maxdepth: 1 + + explanations/how_to_interpret_the_security_status_command.md + explanations/status_columns.md + explanations/what_refresh_does.md + +Public Cloud Ubuntu Pro +======================= + +Here we talk about Ubuntu Pro images for AWS, Azure and GCP, and the related +tooling: the ``ubuntu-advantage-pro`` package. + +.. toctree:: + :maxdepth: 1 + + explanations/what_are_ubuntu_pro_cloud_instances.md + explanations/what_is_the_ubuntu_advantage_pro_package.md + +Other Pro features explained +============================ + +.. toctree:: + :maxdepth: 1 + + explanations/what_are_the_timer_jobs.md + explanations/what_is_the_daemon.md + explanations/why_trusty_is_no_longer_supported.md + explanations/errors_explained.md diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/disable_enable_apt_news.md ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/disable_enable_apt_news.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/disable_enable_apt_news.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/disable_enable_apt_news.md 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,45 @@ +# How to disable or re-enable APT News + +APT News is a feature that allows for timely package-related news to be displayed during an `apt upgrade`. It is distinct from [Ubuntu Pro 'available update' messages](../explanations/apt_messages.md) that are also displayed during an `apt upgrade`. APT News messages are fetched from [https://motd.ubuntu.com/aptnews.json](https://motd.ubuntu.com/aptnews.json) at most once per day. + +By default, APT News is turned on. In this How-to-guide, we show how to turn off and on the APT News feature for a particular machine. + +## Step 1: Check the current configuration of APT News + +``` +pro config show apt_news +``` + +The default value is `True`, so if you haven't yet modified this setting, you will see: +``` +apt_news True +``` + +## Step 2: Disable APT News + +``` +pro config set apt_news=false +``` + +This should also clear any current APT News you may be seeing on your system during `apt upgrade`. + +You can double-check that the setting was successful by running the following again: + +``` +pro config show apt_news +``` + +You should now see: +``` +apt_news False +``` + +## Step 3: (Optional) Re-enable APT News + +If you change your mind and want APT News to start appearing again in `apt upgrade`, run the following: + +``` +pro config set apt_news=true +``` + +And verify the setting worked with `pro config show apt_news`. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/enable_realtime_kernel.md ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/enable_realtime_kernel.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/enable_realtime_kernel.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/enable_realtime_kernel.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,7 +1,7 @@ # How to enable Real-time kernel ```{caution} -Real-Time Kernel is only supported on 22.04. For more information, please see +Real-time kernel is only supported on 22.04. For more information, please see https://ubuntu.com/realtime-kernel ``` @@ -14,7 +14,7 @@ ``` You'll need to acknowledge a warning, and then you should see output like the -following, indicating that the Real-Time Kernel package has been installed. +following, indicating that the Real-time kernel package has been installed. ``` One moment, checking your subscription first @@ -39,7 +39,7 @@ The --access-only flag is introduced in version 27.11 ``` -If you would like to enable access to the Real-Time Kernel APT repository but +If you would like to enable access to the Real-time kernel APT repository but not install the kernel right away, use the `--access-only` flag while enabling. ```console diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/get_rid_of_corrupt_lock.md ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/get_rid_of_corrupt_lock.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/get_rid_of_corrupt_lock.md 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/get_rid_of_corrupt_lock.md 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,29 @@ +# How to get rid of a corrupt lock + +Some pro commands (`attach`, `enable`, `detach` and `disable`) will potentially change the +internal state of your system. Since those commands can run in parallel, we have a lock file +mechanism to guarantee that only one of these commands can run at the same time. The lock follow +this pattern: + +``` +PROCESS_PID:LOCK_HOLDER_NAME +``` + +Where: + +*PROCESS_PID*: The PID of the process that is running the pro command +*LOCK_HOLDER_NAME*: The name of the command that is using the lock (i.e. `pro disable`) + +If the lock file doesn't follow that pattern, we say that it is corrupted. That might happen if we +have any type of disk failures in the system. Once we detect a corrupted lock file, any of +the mentioned pro commands will generate the following output: + +``` +There is a corrupted lock file in the system. To continue, please remove it +from the system by running: + +$ sudo rm /var/lib/ubuntu-advantage/lock +``` + +You can follow the instructions presented on the output to get rid of the corrupted lock. +After that, running the command should generate a correct lock file and continue as expected. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/pro_in_airgapped.md ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/pro_in_airgapped.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides/pro_in_airgapped.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides/pro_in_airgapped.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -# Get started with `pro` in airgapped environment -Want to use `pro` in an offline (=airgapped/internetless) environment? This how-to guide will help you understand how to use the power of Ubuntu Pro in an internetless environment. - -We will show you how to use your existing token in the machine without Internet access and get the same resources as if your machine had network access. - -What you’ll learn -* How to use the airgapped functionality -* How to configure the Pro client to use airgapped -* How to configure resources in the internetless environment - -What you’ll need -* Preliminary Ubuntu Pro knowledge (refer [here](https://canonical-ubuntu-pro-client.readthedocs-hosted.com/) for more info) -* Two Ubuntu machines (one in the airgapped & one in the non-airgapped environment) running 16.04 LTS, 18.04 LTS, 20.04 LTS or 22.04 LTS -* `sudo` access on both machines -* Existing Ubuntu One account -* Ubuntu Pro client version 27.11.2 or newer on the airgapped machine - -## Before we start -1. Check your machines are up-to-date: \ -`sudo apt update && sudo apt upgrade` -2. Check the Ubuntu Pro client is installed on the airgapped machine: \ -`pro --version` -3. Get the token from the [Ubuntu Pro dashboard](https://ubuntu.com/pro/dashboard) you plan to use in the airgapped environment. In this guide, we’ll refer to this token as `[YOUR_CONTRACT_TOKEN]` - - -## Installation -Run these commands on both machines: -```bash -sudo add-apt-repository ppa:yellow/ua-airgapped && sudo apt update -sudo apt install ua-airgapped contracts-airgapped get-resource-tokens -``` - -These three commands serve the following purposes: -1. `ua-airgapped` creates the configuration for running the server locally -2. `contracts-airgapped` runs the server for the Ubuntu Pro client -3. `get-resource-tokens` fetches resource tokens for setting up mirrors of Canonical repositories (as we cannot access Canonical-hosted resources in an offline environment) - - -## Get configuration to set up mirrors -The Ubuntu Pro client cannot access Canonical-hosted services in an offline environment, so we will mirror these repositories locally. - -First, find out the repositories’ URLs of all the resources your contract is entitled to. Run the following in a non-airgapped environment: \ -`echo '[YOUR_CONTRACT_TOKEN]:' | ua-airgapped | grep 'aptURL:'` - -Then, get the resource tokens to access these resources in a non-airgapped environment: \ -`get-resource-tokens [YOUR_CONTRACT_TOKEN]` - -Stdout from `get-resource-tokens` gives pairs of services and their resource tokens (one token per service). The repository URL is specified as `aptURL` from the first command and, together with the resource token, they are required to set up a mirror, be it `apt-mirror`, `aptly`, Landscape or another software. - -You can check the resource token's correctness. For example, let’s say we want to set a mirror for `esm-infra`. It has `aptURL: https://esm.ubuntu.com`. If the resource token is `[ESM_INFRA_RESOURCE_TOKEN]`, then the command on the non-airgapped machine will be: -```bash -/usr/lib/apt/apt-helper download-file https://bearer:[ESM_INFRA_RESOURCE_TOKEN]@esm.ubuntu.com/ubuntu/ /tmp/check-esm-resource-token.txt -``` -Response must be `200`. `401 Unauthorized` shows that the resource token or the URL is wrong. Please note that the specifics of setting up mirrors may be different depending on the product, but the bearer authentication will be identical in all cases, so refer to the command above to distinguish between airgapped and mirror problems. - -## Set up mirrors -If you have experience in setting up mirrors, this section is optional for you. However, if you want to get started with the simplest option of setting up mirrors and have no prior experience, keep reading. - -Suppose you want to set up mirrors for esm-infra. Then, the steps to set up a mirror would be: -1. Install `apt-mirror`: \ -`sudo apt install apt-mirror` -2. Fetch the resource token for `esm-infra` (as per the section above). -3. Change the `/etc/apt/mirror.list` file to the following contents: - ``` - set nthreads 20 - set _tilde 0 - - deb https://bearer:[ESM_INFRA_RESOURCE_TOKEN]@esm.ubuntu.com/infra/ubuntu/ jammy-infra-updates main - deb https://bearer:[ESM_INFRA_RESOURCE_TOKEN]@esm.ubuntu.com/infra/ubuntu/ jammy-infra-security main - - clean http://archive.ubuntu.com/ubuntu - ``` -4. Run `apt-mirror`: \ -`sudo -u apt-mirror apt-mirror` -5. Serve the `esm-infra` mirror on port `9090` (will be `[ESM_INFRA_MIRROR_URL]` in the next section): \ -`python3 -m http.server --directory /var/spool/apt-mirror/mirror/esm.ubuntu.com/infra/ 9090` - - -## Get configuration to run the server -The prerequisites for this step are: -1. Mirrors of needed resources are set up on the airgapped machine -2. Mirrors of needed resources are served on some port on the airgapped machine - -We need to create such a configuration for our server so that it points to the local mirror server on the airgapped machine, not the Canonical one. For example, if we want to use `esm-infra` in the airgapped environment, create the `02-overridden.yml` file on the non-airgapped machine with the following contents: -```yaml -[YOUR_CONTRACT_TOKEN]: - esm-infra: - directives: - aptURL: [ESM_INFRA_MIRROR_URL] -``` -Run `ua-airgapped` for the new config in non-airgapped environment: `cat 02-overridden.yml | ua-airgapped > 02-server-ready.yml` - -`02-server-ready.yml` is our final configuration to run the server. - -## Running the server and final tweaks -Run the following commands in the airgapped environment: -1. Run the server: `contracts-airgapped --input=./02-server-ready.yml` -2. Change the `contract_url` setting in `uaclient.conf` to point to the server: `http://1.2.3.4:8484`. Here `1.2.3.4` is the IP address of the airgapped machine. -3. Run `sudo pro refresh` to check everything works fine. -4. Finally, attach your token: `sudo pro attach [YOUR_CONTRACT_TOKEN]`. - -## Congratulations! -That’s been a long run, but you’ve made it! Now you are all set to run all the `pro` commands in the airgapped environment as you are used to. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides.rst ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides.rst --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/howtoguides.rst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/howtoguides.rst 2023-04-05 15:14:00.000000000 +0000 @@ -34,6 +34,7 @@ .. toctree:: :maxdepth: 1 + Disable or re-enable APT News Configure a proxy Configure a timer job @@ -82,19 +83,19 @@ :maxdepth: 1 Check Ubuntu Pro Client version - -Create a ``pro`` Golden Image -============================= + +``lock`` +----------- .. toctree:: :maxdepth: 1 - Create a customised Cloud Ubuntu Pro image - -Use ``pro`` in an airgapped environment -======================================= + Get rid of corrupted locks + +Create a ``pro`` Golden Image +============================= .. toctree:: :maxdepth: 1 - Use Ubuntu Pro in an airgapped environment + Create a customised Cloud Ubuntu Pro image diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/index.rst ubuntu-advantage-tools-27.14.4~16.04/docs/index.rst --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/index.rst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/index.rst 2023-04-05 15:14:00.000000000 +0000 @@ -55,9 +55,8 @@ Getting help ============ -Having trouble? We would like to help! For help on a specific page in this -documentation, click on the "Have a question?" link at the top of that page. -You can also... +Ubuntu Pro is a new product, and we're keen to know about your experience of +using it! - **Have questions?** You might find the answers `in our FAQ`_. diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/api.md ubuntu-advantage-tools-27.14.4~16.04/docs/references/api.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/api.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/references/api.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,10 +1,16 @@ -# Pro API Reference Guide +# The Ubuntu Pro API reference guide +The Ubuntu Pro Client has a Python-based API to be consumed by users who want +to integrate the Client's functionality with their software. -The Pro Client has a Python-based API to be consumed by users who want to integrate the client's functionality to their software. -The functions and objects are available through the `uaclient.api` module, and all of the available endpoints return an object with specific data for the calls. - -Besides importing the Python code directly, consumers who are not writing Python may use the CLI to call the same functionality, using the `pro api` command. This command will always return a JSON with a standard structure, as can be seen below: +The functions and objects are available through the `uaclient.api` module, and +all of the available endpoints return an object with specific data for the +calls. + +Besides importing the Python code directly, consumers who are not writing +Python may use the command line interface (CLI) to call the same functionality, +using the `pro api` command. This command will always return a JSON with a +standard structure, as can be seen below: ```json { @@ -25,6 +31,51 @@ } ``` +## Version dependencies +The package name of Ubuntu Pro Client is `ubuntu-advantage-tools`. + +The documentation for each endpoint below states the first version to include that endpoint. + +If you depend on these endpoints, we recommend using a standard dependency version requirement to ensure that a sufficiently up-to-date version of the Pro Client is installed before trying to use it. + +```{important} +The `~` at the end of the version is important to ensure that dpkg version comparison works as expected. +``` + +For example, if your software is packaged as a deb, then you can add the following to your `Depends` list in your `control` file to ensure the installed version is at least 27.11~: +``` +ubuntu-advantage-tools (>= 27.11~) +``` + +### Runtime version detection +If you need to detect the current version at runtime, the most reliable way is to query `dpkg`. +``` +dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools +``` + +If you need to compare versions at runtime, make sure you use the `dpkg` version comparison algorithm. +For example, the following will exit 0 if the currently installed version of Pro Client is at least 27.11~: +``` +dpkg --compare-versions "$(dpkg-query --showformat='${Version}' --show ubuntu-advantage-tools)" ge "27.11~" +``` + +### Runtime feature detection +As an alternative to version detection, you can use feature detection. This is easier to do when importing the API in python than it is when using the `pro api` subcommand. + +In python, try to import the desired endpoint. If an `ImportError` is raised, then the currently installed version of Ubuntu Pro Client doesn't support that endpoint. + +For example: +```python +try: + from uaclient.api.u.pro.version.v1 import version + pro_client_api_supported = True +except ImportError: + pro_client_api_supported = False +``` + +You could do something similar by catching certain errors when using the `pro api` subcommand, but there are more cases that could indicate an old version, and it generally isn't recommended. + +## Available endpoints The currently available endpoints are: - [u.pro.version.v1](#uproversionv1) - [u.pro.attach.magic.initiate.v1](#uproattachmagicinitiatev1) @@ -40,13 +91,19 @@ - [u.security.package_manifest.v1](#usecuritypackage_manifestv1) ## u.pro.version.v1 -Shows the installed Client version. + +Introduced in Ubuntu Pro Client Version: `27.11~` + +Shows the installed Pro Client version. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.version.v1 import version @@ -54,39 +111,48 @@ ``` #### Expected return object: + `uaclient.api.u.pro.version.v1.VersionResult` |Field Name|Type|Description| |-|-|-| -|installed_version|str|The current installed version| +|`installed_version`|*str*|The current installed version| -### Raised Exceptions -- `VersionError`: raised if the client cannot determine the version +### Raised exceptions +- `VersionError`: Raised if the Client cannot determine the version. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.version.v1 ``` #### Expected attributes in JSON structure + ```json { "installed_version":"" } ``` - ## u.pro.attach.magic.initiate.v1 + +Introduced in Ubuntu Pro Client Version: `27.11~` + Initiates the Magic Attach flow, retrieving the User Code to confirm the operation and the Token used to proceed. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.magic.initiate.v1 import initiate @@ -94,29 +160,35 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.magic.initiate.v1.MagicAttachInitiateResult` |Field Name|Type|Description| |-|-|-| -|user_code|str|Code the user will see in the UI when confirming the Magic Attach| -|token|str|Magic token used by the tooling to continue the operation| -|expires|str|Timestamp of the Magic Attach process expiration| -|expires_in|int|Seconds before the Magic Attach process expires| - - -### Raised Exceptions - -- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server -- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction -- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment +|`user_code`|*str*|Code the user will see in the UI when confirming the Magic Attach| +|`token`|*str*|Magic Token used by the tooling to continue the operation| +|`expires`|*str*|Timestamp of the Magic Attach process expiration| +|`expires_in`|*int*|Seconds before the Magic Attach process expires| + +### Raised exceptions + +- `ConnectivityError`: Raised if it is not possible to connect to the Contracts + Server. +- `ContractAPIError`: Raised if there is an unexpected error in the Contracts + Server interaction. +- `MagicAttachUnavailable`: Raised if the Magic Attach service is busy or + unavailable at the moment. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.attach.magic.initiate.v1 ``` #### Expected attributes in JSON structure + ```json { "user_code":"", @@ -126,16 +198,20 @@ } ``` +## u.pro.attach.magic.wait.v1 +Introduced in Ubuntu Pro Client Version: `27.11~` -## u.pro.attach.magic.wait.v1 Polls the contract server waiting for the user to confirm the Magic Attach. ### Args -- `magic_token`: The token provided by the initiate endpoint + +- `magic_token`: The Token provided by the initiate endpoint. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.magic.wait.v1 import MagicAttachWaitOptions, wait @@ -144,33 +220,38 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.magic.wait.v1.MagicAttachWaitResult` |Field Name|Type|Description| |-|-|-| -|user_code|str|Code the user will see in the UI when confirming the Magic Attach| -|token|str|Magic token used by the tooling to continue the operation| -|expires|str|Timestamp of the Magic Attach process expiration| -|expires_in|int|Seconds before the Magic Attach process expires| -|contract_id|str|ID of the contract the machine will be attached to| -|contract_token|str|The contract token to attach the machine| - - -### Raised Exceptions - -- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server -- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction -- `MagicAttachTokenError`: raised when an invalid/expired token is sent -- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment - +|`user_code`|*str*|Code the user will see in the UI when confirming the Magic Attach| +|`token`|*str*|Magic Token used by the tooling to continue the operation| +|`expires`|*str*|Timestamp of the Magic Attach process expiration| +|`expires_in`|*int*|Seconds before the Magic Attach process expires| +|`contract_id`|*str*|ID of the contract the machine will be attached to| +|`contract_token`|*str*|The contract Token to attach the machine| + +### Raised exceptions + +- `ConnectivityError`: Raised if it is not possible to connect to the Contracts + Server. +- `ContractAPIError`: Raised if there is an unexpected error in the Contracts + Server interaction. +- `MagicAttachTokenError`: Raised when an invalid/expired Token is sent. +- `MagicAttachUnavailable`: Raised if the Magic Attach service is busy or + unavailable at the moment. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.attach.magic.wait.v1 --args magic_token= ``` #### Expected attributes in JSON structure + ```json { "user_code":"", @@ -182,15 +263,20 @@ } ``` - ## u.pro.attach.magic.revoke.v1 -Revokes a magic attach token. + +Introduced in Ubuntu Pro Client Version: `27.11~` + +Revokes a Magic Attach Token. ### Args -- `magic_token`: The token provided by the initiate endpoint + +- `magic_token`: The Token provided by the initiate endpoint. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.magic.revoke.v1 import MagicAttachRevokeOptions, revoke @@ -199,40 +285,51 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.magic.wait.v1.MagicAttachRevokeResult` No data present in the result. +### Raised exceptions -### Raised Exceptions - -- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server -- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction -- `MagicAttachTokenAlreadyActivated`: raised when trying to revoke a token which was already activated through the UI -- `MagicAttachTokenError`: raised when an invalid/expired token is sent -- `MagicAttachUnavailable`: raised if the Magic Attach service is busy or unavailable at the moment - +- `ConnectivityError`: Raised if it is not possible to connect to the Contracts + Server. +- `ContractAPIError`: Raised if there is an unexpected error in the Contracts + Server interaction. +- `MagicAttachTokenAlreadyActivated`: Raised when trying to revoke a Token + which was already activated through the UI. +- `MagicAttachTokenError`: Raised when an invalid/expired Token is sent. +- `MagicAttachUnavailable`: Raised if the Magic Attach service is busy or + unavailable at the moment. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.attach.magic.revoke.v1 --args magic_token= ``` #### Expected attributes in JSON structure + ```json {} ``` - ## u.pro.attach.auto.should_auto_attach.v1 + +Introduced in Ubuntu Pro Client Version: `27.11~` + Checks if a given system should run auto-attach on boot. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.auto.should_auto_attach.v1 import should_auto_attach @@ -240,40 +337,53 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.auto.should_auto_attach.v1.ShouldAutoAttachResult` |Field Name|Type|Description| |-|-|-| -|should_auto_attach|bool|True if the system should run auto-attach on boot| +|`should_auto_attach`|*bool*|True if the system should run auto-attach on boot| + +### Raised exceptions -### Raised Exceptions No exceptions raised by this endpoint. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.attach.auto.should_auto_attach.v1 ``` #### Expected attributes in JSON structure + ```json { "should_auto_attach": false } ``` - ## u.pro.attach.auto.full_auto_attach.v1 + +Introduced in Ubuntu Pro Client Version: `27.11~` + Runs the whole auto-attach process on the system. ### Args -- `enable`: optional list of services to enable after auto-attaching -- `enable_beta`: optional list of beta services to enable after auto-attaching -> If none of the lists are set, the services will be enabled based on the contract definitions. +- `enable`: Optional list of services to enable after auto-attaching. +- `enable_beta`: Optional list of beta services to enable after auto-attaching. + +```{note} +If none of the lists are set, the services will be enabled based on the +contract definitions. +``` ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.auto.full_auto_attach.v1 import full_auto_attach, FullAutoAttachOptions @@ -282,46 +392,70 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.auto.full_auto_attach.v1.FullAutoAttachResult` No data present in the result. -### Raised Exceptions - -- `AlreadyAttachedError`: raised if running on a machine which is already attached to a Pro subscription -- `AutoAttachDisabledError`: raised if `disable_auto_attach: true` in uaclient.conf -- `ConnectivityError`: raised if it is not possible to connect to the Contracts Server -- `ContractAPIError`: raised if there is an unexpected error in the Contracts Server interaction -- `EntitlementsNotEnabledError`: raised if the client fails to enable any of the entitlements - (whether present in any of the lists or listed in the contract) -- `LockHeldError`: raised if another Client process is holding the lock on the machine -- `NonAutoAttachImageError`: raised if the cloud where the system is running does not support auto-attach -- `UserFacingError`: raised if: - - the client is unable to determine on which cloud the system is running - - the image where the client is running does not support auto-attach +### Raised exceptions +- `AlreadyAttachedError`: Raised if running on a machine which is already + attached to a Pro subscription. +- `AutoAttachDisabledError`: Raised if `disable_auto_attach: true` in + `uaclient.conf`. +- `ConnectivityError`: Raised if it is not possible to connect to the Contracts + Server. +- `ContractAPIError`: Raised if there is an unexpected error in the Contracts + Server interaction. +- `EntitlementsNotEnabledError`: Raised if the Client fails to enable any of + the entitlements (whether present in any of the lists or listed in the + contract). +- `LockHeldError`: Raised if another Client process is holding the lock on the + machine. +- `NonAutoAttachImageError`: Raised if the cloud where the system is running + does not support auto-attach. +- `UserFacingError`: Raised if: + - The Client is unable to determine which cloud the system is running on. + - The image where the Client is running does not support auto-attach. ### CLI interaction + #### Calling from the CLI: -This endpoint currently has no CLI support. Only the Python-based version is available. +This endpoint currently has no CLI support. Only the Python-based version is +available. ## u.pro.attach.auto.configure_retry_service.v1 -Configures options for the retry auto attach functionality and create file that will activate the retry auto attach functionality if `ubuntu-advantage.service` runs. -Note that this does not start `ubuntu-advantage.service`. This makes it useful for calling during the boot process `Before: ubuntu-advantage.service` so that when `ubuntu-advantage.service` starts, its ConditionPathExists check passes and executes the retry auto attach function. +Introduced in Ubuntu Pro Client Version: `27.12~` + +Configures options for the retry auto-attach functionality, and creates files +that will activate the retry auto-attach functionality if +`ubuntu-advantage.service` runs. -If you call this function outside of the boot process and would like the retry auto attach functionality to actually start, you'll need to call something like `systemctl start ubuntu-advantage.service`. +Note that this does not start `ubuntu-advantage.service`. This makes it useful +for calling during the boot process `Before: ubuntu-advantage.service` so that +when `ubuntu-advantage.service` starts, its `ConditionPathExists` check passes +and activates the retry auto-attach function. +If you call this function outside of the boot process and would like the retry +auto-attach functionality to actually start, you'll need to call something +like `systemctl start ubuntu-advantage.service`. ### Args -- `enable`: optional list of services to enable after auto-attaching -- `enable_beta`: optional list of beta services to enable after auto-attaching -> If none of the lists are set, the services will be enabled based on the contract definitions. +- `enable`: Optional list of services to enable after auto-attaching. +- `enable_beta`: Optional list of beta services to enable after auto-attaching. + +```{note} +If none of the lists are set, the services will be enabled based on the +contract definitions. +``` ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.attach.auto.configure_retry_service.v1 import configure_retry_service, ConfigureRetryServiceOptions @@ -330,26 +464,36 @@ ``` #### Expected return object: + `uaclient.api.u.pro.attach.auto.configure_retry_service.v1.ConfigureRetryServiceResult` No data present in the result. -### Raised Exceptions +### Raised exceptions + No exceptions raised by this endpoint. ### CLI interaction + #### Calling from the CLI: -This endpoint currently has no CLI support. Only the Python-based version is available. +This endpoint currently has no CLI support. Only the Python-based version is +available. ## u.pro.security.status.livepatch_cves.v1 -Lists Livepatch patches for the current running kernel. + +Introduced in Ubuntu Pro Client Version: `27.12~` + +Lists Livepatch patches for the currently-running kernel. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.security.status.livepatch_cves.v1 import livepatch_cves @@ -357,30 +501,33 @@ ``` #### Expected return object: + `uaclient.api.u.pro.security.status.livepatch_cves.v1.LivepatchCVEsResult` |Field Name|Type|Description| |-|-|-| -|fixed_cves|list(LivepatchCVEObject)|List of Livepatch patches for the given system| +|`fixed_cves`|*list(LivepatchCVEObject)*|List of Livepatch patches for the given system| `uaclient.api.u.pro.security.status.livepatch_cves.v1.LivepatchCVEObject` |Field Name|Type|Description| |-|-|-| -|name|str|Name (ID) of the CVE| -|patched|bool|Livepatch has patched the CVE| +|`name`|*str*|Name (ID) of the CVE| +|`patched`|*bool*|Livepatch has patched the CVE| +### Raised exceptions -### Raised Exceptions No exceptions raised by this endpoint. - ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.security.status.livepatch_cves.v1 ``` #### Expected attributes in JSON structure + ```json { "fixed_cves":[ @@ -396,19 +543,25 @@ } ``` - ## u.pro.security.status.reboot_required.v1 -Informs if the system should be rebooted or not. -Possible outputs are: -- yes: the system should be rebooted -- no: there is no need to reboot the system -- yes-kernel-livepatches-applied: there are livepatch patches applied to the current kernel, but a reboot is required for an update to take place. This reboot can wait until the next maintenance window. + +Introduced in Ubuntu Pro Client Version: `27.12~` + +Informs if the system should be rebooted or not. Possible outputs are: +- `yes`: The system should be rebooted. +- `no`: There is no need to reboot the system. +- `yes-kernel-livepatches-applied`: There are Livepatch patches applied to the + current kernel, but a reboot is required for an update to take place. This + reboot can wait until the next maintenance window. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.security.status.reboot_required.v1 import reboot_required @@ -416,38 +569,47 @@ ``` #### Expected return object: + `uaclient.api.u.pro.security.status.reboot_required.v1.RebootRequiredResult` |Field Name|Type|Description| |-|-|-| -|reboot_required|str|One of the descriptive strings indicating if the system should be rebooted| +|`reboot_required`|*str*|One of the descriptive strings indicating if the system should be rebooted| -### Raised Exceptions -No exceptions raised by this endpoint. +### Raised exceptions +No exceptions raised by this endpoint. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.security.status.reboot_required.v1 ``` #### Expected attributes in JSON structure + ```json { "reboot_required": "yes|no|yes-kernel-livepatches-applied" } ``` - ## u.pro.packages.summary.v1 + +Introduced in Ubuntu Pro Client Version: `27.12~` + Shows a summary of installed packages in the system, categorized by origin. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.packages.summary.v1 import summary @@ -455,37 +617,40 @@ ``` #### Expected return object: + `uaclient.api.u.pro.packages.summary.v1.PackageSummaryResult` |Field Name|Type|Description| |-|-|-| -|summary|PackageSummary|Summary of all installed packages| +|`summary`|*PackageSummary*|Summary of all installed packages| `uaclient.api.u.pro.packages.summary.v1.PackageSummary` |Field Name|Type|Description| |-|-|-| -|num_installed_packages|int|Total count of installed packages| -|num_esm_apps_packages|int|Count of packages installed from esm-apps| -|num_esm_infra_packages|int|Count of packages installed from esm-infra| -|num_main_packages|int|Count of packages installed from main| -|num_multiverse_packages|int|Count of packages installed from multiverse| -|num_restricted_packages|int|Count of packages installed from restricted| -|num_third_party_packages|int|Count of packages installed from third party sources| -|num_universe_packages|int|Count of packages installed from universe| -|num_unknown_packages|int|Count of packages installed from unknown sources| +|`num_installed_packages`|*int*|Total count of installed packages| +|`num_esm_apps_packages`|*int*|Count of packages installed from `esm-apps`| +|`num_esm_infra_packages`|*int*|Count of packages installed from `esm-infra`| +|`num_main_packages`|*int*|Count of packages installed from `main`| +|`num_multiverse_packages`|*int*|Count of packages installed from `multiverse`| +|`num_restricted_packages`|*int*|Count of packages installed from `restricted`| +|`num_third_party_packages`|*int*|Count of packages installed from third party sources| +|`num_universe_packages`|*int*|Count of packages installed from `universe`| +|`num_unknown_packages`|*int*|Count of packages installed from unknown sources| +### Raised exceptions -### Raised Exceptions No exceptions raised by this endpoint. - ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.packages.summary.v1 ``` #### Expected attributes in JSON structure + ```json { "summary":{ @@ -502,15 +667,21 @@ } ``` - ## u.pro.packages.updates.v1 -Shows available updates for packages in a system, categorized by where they can be obtained. + +Introduced in Ubuntu Pro Client Version: `27.12~` + +Shows available updates for packages in a system, categorized by where they +can be obtained. ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.pro.packages.updates.v1 import updates @@ -518,44 +689,48 @@ ``` #### Expected return object: + `uaclient.api.u.pro.packages.updates.v1.PackageUpdatesResult` |Field Name|Type|Description| |-|-|-| -|summary|UpdateSummary|Summary of all available updates| -|updates|list(UpdateInfo)|Detailed list of all available updates| +|`summary`|*UpdateSummary*|Summary of all available updates| +|`updates`|*list(UpdateInfo)*|Detailed list of all available updates| `uaclient.api.u.pro.packages.updates.v1.UpdateSummary` |Field Name|Type|Description| |-|-|-| -|num_updates|int|Total count of available updates| -|num_esm_apps_updates|int|Count of available updates from esm-apps| -|num_esm_infra_updates|int|Count of available updates from esm-infra| -|num_standard_security_updates|int|Count of available updates from the -security pocket| -|num_standard_updates|int|Count of available updates from the -updates pocket| +|`num_updates`|*int*|Total count of available updates| +|`num_esm_apps_updates`|*int*|Count of available updates from `esm-apps`| +|`num_esm_infra_updates`|*int*|Count of available updates from `esm-infra`| +|`num_standard_security_updates`|*int*|Count of available updates from the `-security` pocket| +|`num_standard_updates`|*int*|Count of available updates from the `-updates` pocket| `uaclient.api.u.pro.packages.updates.v1.UpdateInfo` |Field Name|Type|Description| |-|-|-| -|download_size|int|Download size for the update in bytes| -|origin|str|Where the update is downloaded from| -|package|str|Name of the package to be updated| -|provided_by|str|Service which provides the update| -|status|str|Whether this update is ready for download or not| -|version|str|Version of the update| +|`download_size`|*int*|Download size for the update in bytes| +|`origin`|*str*|Where the update is downloaded from| +|`package`|*str*|Name of the package to be updated| +|`provided_by`|*str*|Service which provides the update| +|`status`|*str*|Whether this update is ready for download or not| +|`version`|*str*|Version of the update| -### Raised Exceptions -No exceptions raised by this endpoint. +### Raised exceptions +No exceptions raised by this endpoint. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.pro.packages.updates.v1 ``` #### Expected attributes in JSON structure + ```json { "summary":{ @@ -578,16 +753,21 @@ } ``` - ## u.security.package_manifest.v1 -Returns the status of installed packages (apt and snap), formatted as a -manifest file (i.e. `package_name\tversion`) + +Introduced in Ubuntu Pro Client Version: `27.12~` + +Returns the status of installed packages (`apt` and `snap`), formatted as a +manifest file (i.e. `package_name\tversion`). ### Args + This endpoint takes no arguments. ### Python API interaction + #### Calling from Python code + ```python from uaclient.api.u.security.package_manifest.v1 import package_manifest @@ -595,23 +775,27 @@ ``` #### Expected return object: + `uaclient.api.u.security.package_manifest.v1.PackageManifestResult` |Field Name|Type|Description| |-|-|-| -|manifest_data|str|Manifest of apt and snap packages installed on the system| +|`manifest_data`|*str*|Manifest of `apt` and `snap` packages installed on the system| -### Raised Exceptions -No exceptions raised by this endpoint. +### Raised exceptions +No exceptions raised by this endpoint. ### CLI interaction + #### Calling from the CLI: + ```bash pro api u.security.package_manifest.v1 ``` #### Expected attributes in JSON structure + ```json { "package_manifest":"package1\t1.0\npackage2\t2.3\n" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/network_requirements.md ubuntu-advantage-tools-27.14.4~16.04/docs/references/network_requirements.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/network_requirements.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/references/network_requirements.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,18 +1,35 @@ -# Ubuntu Pro Client Network Requirements +# Ubuntu Pro Client network requirements -Using the Ubuntu Pro Client to enable support services will rely on network access to obtain updated service -credentials, add APT repositories to install deb packages and install [snap packages](https://snapcraft.io/about) when -Livepatch is enabled. Also see the Proxy Configuration explanation to inform Ubuntu Pro Client of HTTP(S)/APT proxies. +Using the Ubuntu Pro Client to enable support services will rely on network +access to: -Ensure the managed system has access to the following port:urls if in a network-limited environment: +- Obtain updated service credentials +- Add APT repositories to install `deb` packages +- Install [`snap` packages](https://snapcraft.io/about) when Livepatch is + enabled. -* 443:https://contracts.canonical.com/ - HTTP PUTs, GETs and POSTs for Ubuntu Pro Client interaction -* 443:https://esm.ubuntu.com/\* - APT repository access for most services +```{seealso} -Enabling kernel Livepatch require additional network egress: +You can also refer to our [Proxy Configuration guide](/../howtoguides/configure_proxies.md) +to learn how to inform Ubuntu Pro Client of HTTP(S)/APT proxies. +``` -* snap endpoints required in order to install and run snaps as defined in [snap forum network-requirements post](https://forum.snapcraft.io/t/network-requirements/5147) -* 443:api.snapcraft.io -* 443:dashboard.snapcraft.io -* 443:login.ubuntu.com -* 443:\*.snapcraftcontent.com - Download CDNs +## Network-limited + +Ensure the managed system has access to the following port:urls if in a +network-limited environment: + +* `443:https://contracts.canonical.com/`: HTTP PUTs, GETs and POSTs for Ubuntu + Pro Client interaction. +* `443:https://esm.ubuntu.com/\*`: APT repository access for most services. + +## Enable kernel Livepatch + +Enabling kernel Livepatch requires additional network egress: + +* `snap` endpoints required in order to install and run snaps as defined in + [snap forum network-requirements post](https://forum.snapcraft.io/t/network-requirements/5147) +* `443:api.snapcraft.io` +* `443:dashboard.snapcraft.io` +* `443:login.ubuntu.com` +* `443:\*.snapcraftcontent.com` - Download CDNs diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/ppas.md ubuntu-advantage-tools-27.14.4~16.04/docs/references/ppas.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/ppas.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/references/ppas.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,9 +1,30 @@ -# Available PPAs with different version of Ubuntu Pro Client -There are 3 PPAs with different release channels of the Ubuntu Pro Client: +# Ubuntu Pro Client PPAs -1. Stable: This contains stable builds only which have been verified for release into Ubuntu stable releases or Ubuntu Pro images. - - add with `sudo add-apt-repository ppa:ua-client/stable` -2. Staging: This contains builds under validation for release to stable Ubuntu releases and images - - add with `sudo add-apt-repository ppa:ua-client/staging` -3. Daily: This PPA is updated every day with the latest changes. - - add with `sudo add-apt-repository ppa:ua-client/daily` +There are three Personal Package Archives (PPAs) with different release +channels in the Ubuntu Pro Client: *Stable*, *Staging*, and *Daily*. + +## Stable + +This contains stable builds only, which have been verified for release into +Ubuntu stable releases or Ubuntu Pro images. You can add it with: + +``` +sudo add-apt-repository ppa:ua-client/stable +``` + +## Staging + +This contains builds *under validation* for release to stable Ubuntu releases +and images. You can add it with: + +``` +sudo add-apt-repository ppa:ua-client/staging +``` + +## Daily + +This PPA is updated every day with the latest changes. You can add it with: + +``` +sudo add-apt-repository ppa:ua-client/daily +``` diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/support_matrix.md ubuntu-advantage-tools-27.14.4~16.04/docs/references/support_matrix.md --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/references/support_matrix.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/references/support_matrix.md 2023-04-05 15:14:00.000000000 +0000 @@ -1,19 +1,19 @@ -# Support Matrix for the client +# Support matrix for the Ubuntu Pro Client -Ubuntu Pro services are only available on Ubuntu Long Term Support (LTS) releases. +Ubuntu Pro services are only available on Ubuntu Long Term Support (LTS) +releases. -On interim Ubuntu releases, `pro status` will report most of the services as 'n/a' and disallow enabling those services. +On interim Ubuntu releases, `pro status` will report most of the services as +"n/a" (not applicable), and disallow enabling those services. -Below is a list of platforms and releases ubuntu-advantage-tools supports +Here is a list of platforms and releases that `ubuntu-advantage-tools` supports. -| Ubuntu Release | Build Architectures | Support Level | -| -------------- | -------------------------------------------------- | -------------------------- | -| Trusty | amd64, arm64, armhf, i386, powerpc, ppc64el | Last release 19.6 | -| Xenial | amd64, arm64, armhf, i386, powerpc, ppc64el, s390x | Active SRU of all features | -| Bionic | amd64, arm64, armhf, i386, ppc64el, s390x | Active SRU of all features | -| Focal | amd64, arm64, armhf, ppc64el, riscv64, s390x | Active SRU of all features | -| Groovy | amd64, arm64, armhf, ppc64el, riscv64, s390x | Last release 27.1 | -| Hirsute | amd64, arm64, armhf, ppc64el, riscv64, s390x | Last release 27.5 | -| Impish | amd64, arm64, armhf, ppc64el, riscv64, s390x | Last release 27.9 | -| Jammy | amd64, arm64, armhf, ppc64el, riscv64, s390x | Active SRU of all features | -| Kinetic | amd64, arm64, armhf, ppc64el, riscv64, s390x | Active SRU of all features | +| Ubuntu Release | Build Architectures | Initial Release | Final Release | +| -------------- | -------------------------------------------------- | --------------- | -------------------------- | +| Trusty | amd64, arm64, armhf, i386, powerpc, ppc64el | None | 19.6 | +| Xenial | amd64, arm64, armhf, i386, powerpc, ppc64el, s390x | None | Active SRU of all features | +| Bionic | amd64, arm64, armhf, i386, ppc64el, s390x | 17 | Active SRU of all features | +| Focal | amd64, arm64, armhf, ppc64el, riscv64, s390x | 20.3 | Active SRU of all features | +| Jammy | amd64, arm64, armhf, ppc64el, riscv64, s390x | 27.7 | Active SRU of all features | +| Kinetic | amd64, arm64, armhf, ppc64el, riscv64, s390x | 27.11.2 | Active SRU of all features | +| Lunar | amd64, arm64, armhf, ppc64el, riscv64, s390x | TBD | Active SRU of all features | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/references.rst ubuntu-advantage-tools-27.14.4~16.04/docs/references.rst --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/references.rst 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/references.rst 2023-04-05 15:14:00.000000000 +0000 @@ -1,9 +1,9 @@ Ubuntu Pro Client reference *************************** -Our reference section contains support information for the Ubuntu Pro Client. -This includes details on the network requirements, API definitions, support -matrices and so on. +Our reference section contains technical information for the Ubuntu Pro Client. +This includes details of the network requirements, API definitions, and other +related tools. Reference ========= @@ -12,4 +12,7 @@ :maxdepth: 1 :glob: - references/* + API reference guide + Network requirements + Personal Package Archives (PPAs) + Support matrix diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/css/custom.css ubuntu-advantage-tools-27.14.4~16.04/docs/_static/css/custom.css --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/css/custom.css 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/_static/css/custom.css 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,240 @@ +/** Fix the font weight (300 for normal, 400 for slightly bold) **/ +/** Should be 100 for all headers, 400 for normal text **/ + +h1, h2, h3, h4, h5, h6, .sidebar-tree .current-page>.reference, button, input, optgroup, select, textarea, th.head { + font-weight: 200; +} + +.toc-title { + font-weight: 400; +} + +div.page, li.scroll-current>.reference, dl.glossary dt, dl.simple dt, dl:not([class]) dt { + font-weight: 300; + line-height: 1.5; + font-size: var(--font-size--normal); +} + + +/** Side bars (side-bar tree = left, toc-tree = right) **/ +div.sidebar-tree { + font-weight: 200; + line-height: 1.5; + font-size: var(--font-size--normal); +} + +div.toc-tree { + font-weight: 200; + font-size: var(--font-size--medium); + line-height: 1.5; +} + +.sidebar-tree .toctree-l1>.reference, .toc-tree li.scroll-current>.reference { + font-weight: 400; +} + +/** List styling **/ +ol, ul { + margin-bottom: 1.5rem; + margin-left: 1rem; + margin-top: 0; + padding-left: 1rem; +} + +/** Table styling **/ + +th.head { + text-transform: uppercase; + font-size: var(--font-size--small); +} + +table.docutils { + border: 0; + box-shadow: none; + width:100%; +} + +table.docutils td, table.docutils th, table.docutils td:last-child, table.docutils th:last-child, table.docutils td:first-child, table.docutils th:first-child { + border-right: none; + border-left: none; +} + +/* center align table cells with ":-:" */ +td.text-center { + text-align: center; +} + +/** No rounded corners **/ + +.admonition, code.literal, .sphinx-tabs-tab, .sphinx-tabs-panel, .highlight { + border-radius: 0; +} + +/** code blocks and literals **/ +code.docutils.literal.notranslate, .highlight pre, pre.literal-block { + font-size: var(--font-size--medium); +} + + +/** Admonition styling **/ + +.admonition { + font-size: var(--font-size--medium); + box-shadow: none; +} + +/** Styling for links **/ +/* unvisited link */ +a:link { + color: #06c; + text-decoration: none; +} + +/* visited link */ +a:visited { + color: #7d42b8; + text-decoration: none; +} + +/* mouse over link */ +a:hover { + text-decoration: underline; +} + +/* selected link */ +a:active { + text-decoration: underline; +} + +a.sidebar-brand.centered { + text-decoration: none; +} + +/** Color for the "copy link" symbol next to headings **/ + +a.headerlink { + color: var(--color-brand-primary); +} + +/** Line to the left of the current navigation entry **/ + +.sidebar-tree li.current-page { + border-left: 2px solid var(--color-brand-primary); +} + +/** Some tweaks for issue #16 **/ + +[role="tablist"] { + border-bottom: 1px solid var(--color-sidebar-item-background--hover); +} + +.sphinx-tabs-tab[aria-selected="true"] { + border: 0; + border-bottom: 2px solid var(--color-brand-primary); + background-color: var(--color-sidebar-item-background--current); + font-weight:300; +} + +.sphinx-tabs-tab{ + color: var(--color-brand-primary); + font-weight:300; +} + +.sphinx-tabs-panel { + border: 0; + border-bottom: 1px solid var(--color-sidebar-item-background--hover); + background: var(--color-background-primary); +} + +button.sphinx-tabs-tab:hover { + background-color: var(--color-sidebar-item-background--hover); +} + +/** Custom classes to fix scrolling in tables by decreasing the + font size or breaking certain columns. + Specify the classes in the Markdown file with, for example: + ```{rst-class} break-col-4 min-width-4-8 + ``` +**/ + +table.dec-font-size { + font-size: smaller; +} +table.break-col-1 td.text-left:first-child { + word-break: break-word; +} +table.break-col-4 td.text-left:nth-child(4) { + word-break: break-word; +} +table.min-width-1-15 td.text-left:first-child { + min-width: 15em; +} +table.min-width-4-8 td.text-left:nth-child(4) { + min-width: 8em; +} + +/** Underline for abbreviations **/ + +abbr[title] { + text-decoration: underline solid #cdcdcd; +} + +/** Use the same style for right-details as for left-details **/ +.bottom-of-page .right-details { + font-size: var(--font-size--small); + display: block; +} + +/** Version switcher */ +button.version_select { + color: var(--color-foreground-primary); + background-color: var(--color-toc-background); + padding: 5px 10px; + border: none; +} + +.version_select:hover, .version_select:focus { + background-color: var(--color-sidebar-item-background--hover); +} + +.version_dropdown { + position: relative; + display: inline-block; + text-align: right; + font-size: var(--sidebar-item-font-size); +} + +.available_versions { + display: none; + position: absolute; + right: 0px; + background-color: var(--color-toc-background); + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 11; +} + +.available_versions a { + color: var(--color-foreground-primary); + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.available_versions a:hover {background-color: var(--color-sidebar-item-background--current)} + +.show {display:block;} + +/** Fix for nested numbered list - the nested list is lettered **/ +ol.arabic ol.arabic { + list-style: lower-alpha; +} + +/** Make expandable sections look like links **/ +details summary { + color: var(--color-link); +} + +/** Context links at the bottom of the page **/ +footer { + font-size: var(--font-size--medium); +} diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/css/github_issue_links.css ubuntu-advantage-tools-27.14.4~16.04/docs/_static/css/github_issue_links.css --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/css/github_issue_links.css 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/_static/css/github_issue_links.css 2023-04-05 15:14:00.000000000 +0000 @@ -4,4 +4,21 @@ .github-issue-link { font-size: var(--font-size--small); font-weight: bold; + background-color: #DD4814; + padding: 13px 23px; + text-decoration: none; +} +.github-issue-link:link { + color: #FFFFFF; +} +.github-issue-link:visited { + color: #FFFFFF +} +.muted-link.github-issue-link:hover { + color: #FFFFFF; + text-decoration: underline; +} +.github-issue-link:active { + color: #FFFFFF; + text-decoration: underline; } diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/js/github_issue_links.js ubuntu-advantage-tools-27.14.4~16.04/docs/_static/js/github_issue_links.js --- ubuntu-advantage-tools-27.13.6~16.04.1/docs/_static/js/github_issue_links.js 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/docs/_static/js/github_issue_links.js 2023-04-05 15:14:00.000000000 +0000 @@ -4,7 +4,7 @@ link.classList.add("github-issue-link"); link.text = "Have a question?"; link.href = ( - "https://github.com/canonical/ubuntu-advantage-client/issues/new?" + "https://github.com/canonical/ubuntu-pro-client/issues/new?" + "title=docs%3A+TYPE+YOUR+QUESTION+HERE" + "&body=*Please describe the question or issue you're facing with " + `"${document.title}"` diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/api_packages.feature ubuntu-advantage-tools-27.14.4~16.04/features/api_packages.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/api_packages.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/api_packages.feature 2023-04-05 15:14:00.000000000 +0000 @@ -21,16 +21,18 @@ # Install some outdated package And I run `apt install = -y --allow-downgrades` with sudo # See the update there - When I run `pro api u.pro.packages.updates.v1` as non-root + When I store candidate version of package `` + And I regexify `candidate` stored var + And I run `pro api u.pro.packages.updates.v1` as non-root Then stdout matches regexp: """ - {"download_size": \d+, "origin": ".+", "package": "", "provided_by": "", "status": "upgrade_available", "version": ""} + {"download_size": \d+, "origin": ".+", "package": "", "provided_by": "", "status": "upgrade_available", "version": "$behave_var{stored_var candidate}"} """ Examples: ubuntu release - | release | package | outdated_version | candidate_version | provided_by | - | xenial | libcurl3-gnutls | 7.47.0-1ubuntu2 | 7.47.0-1ubuntu2.19\+esm6 | esm-infra | - | bionic | libcurl4 | 7.58.0-2ubuntu3 | 7.58.0-2ubuntu3.22 | standard-security | - | focal | libcurl4 | 7.68.0-1ubuntu2 | 7.68.0-1ubuntu2.15 | standard-security | - | jammy | libcurl4 | 7.81.0-1 | 7.81.0-1ubuntu1.7 | standard-security | - | kinetic | libcurl4 | 7.85.0-1 | 7.85.0-1ubuntu0.2 | standard-security | + | release | package | outdated_version | provided_by | + | xenial | libcurl3-gnutls | 7.47.0-1ubuntu2 | esm-infra | + | bionic | libcurl4 | 7.58.0-2ubuntu3 | standard-security | + | focal | libcurl4 | 7.68.0-1ubuntu2 | standard-security | + | jammy | libcurl4 | 7.81.0-1 | standard-security | + | kinetic | libcurl4 | 7.85.0-1 | standard-security | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/api_security.feature ubuntu-advantage-tools-27.14.4~16.04/features/api_security.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/api_security.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/api_security.feature 2023-04-05 15:14:00.000000000 +0000 @@ -29,7 +29,17 @@ """ When I run `apt update` with sudo And I run `apt upgrade -y` with sudo - And I run `apt install jq bzip2 libopenscap8 -y` with sudo + And I run `apt install jq bzip2 -y` with sudo + # Install the oscap version 1.3.7 which solved the epoch error message issue + And I run `apt-get install -y cmake libdbus-1-dev libdbus-glib-1-dev libcurl4-openssl-dev libgcrypt20-dev libselinux1-dev libxslt1-dev libgconf2-dev libacl1-dev libblkid-dev libcap-dev libxml2-dev libldap2-dev libpcre3-dev swig libxml-parser-perl libxml-xpath-perl libperl-dev libbz2-dev g++ libapt-pkg-dev libyaml-dev libxmlsec1-dev libxmlsec1-openssl` with sudo + And I run `wget https://github.com/OpenSCAP/openscap/releases/download/1.3.7/openscap-1.3.7.tar.gz` as non-root + And I run `tar xzf openscap-1.3.7.tar.gz` as non-root + And I run shell command `mkdir -p openscap-1.3.7/build` as non-root + And I run shell command `cd openscap-1.3.7/build/ && cmake ..` with sudo + And I run shell command `cd openscap-1.3.7/build/ && make` with sudo + And I run shell command `cd openscap-1.3.7/build/ && make install` with sudo + # Installs its shared libs in /usr/local/lib/ + And I run `ldconfig` with sudo And I run shell command `pro api u.security.package_manifest.v1 | jq -r '.data.attributes.manifest_data' > manifest` as non-root And I run shell command `wget https://security-metadata.canonical.com/oval/oci.com.ubuntu..usn.oval.xml.bz2` as non-root And I run `bunzip2 oci.com.ubuntu..usn.oval.xml.bz2` as non-root diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/api_unattended_upgrades.feature ubuntu-advantage-tools-27.14.4~16.04/features/api_unattended_upgrades.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/api_unattended_upgrades.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/api_unattended_upgrades.feature 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,207 @@ +Feature: api.u.unattended_upgrades.status.v1 + + @series.all + @uses.config.machine_type.lxd.container + Scenario Outline: v1 unattended upgrades status + Given a `` machine with ubuntu-advantage-tools installed + When I run `pro api u.unattended_upgrades.status.v1` as non-root + Then stdout matches regexp: + """ + {"_schema_version": "v1", "data": {"attributes": {"apt_periodic_job_enabled": true, "package_lists_refresh_frequency_days": 1, "systemd_apt_timer_enabled": true, "unattended_upgrades_allowed_origins": \["\$\{distro_id\}:\$\{distro_codename\}", "\$\{distro_id\}:\$\{distro_codename\}\-security", "\$\{distro_id\}ESMApps:\$\{distro_codename\}\-apps-security", "\$\{distro_id\}ESM:\$\{distro_codename\}\-infra-security"\], "unattended_upgrades_disabled_reason": null, "unattended_upgrades_frequency_days": 1, "unattended_upgrades_last_run": null, "unattended_upgrades_running": true}, "meta": {"environment_vars": \[\], "raw_config": {"APT::Periodic::Enable": "1", "APT::Periodic::Unattended-Upgrade": "1", "APT::Periodic::Update-Package-Lists": "1", "Unattended-Upgrade::Allowed-Origins": \["\$\{distro_id\}:\$\{distro_codename\}", "\$\{distro_id\}:\$\{distro_codename\}\-security", "\$\{distro_id\}ESMApps:\$\{distro_codename\}\-apps-security", "\$\{distro_id\}ESM:\$\{distro_codename\}\-infra-security"\][,]?\s*}}, "type": "UnattendedUpgradesStatus"}, "errors": \[\], "result": "success", "version": ".*", "warnings": \[\]} + """ + When I create the file `/etc/apt/apt.conf.d/99test` with the following: + """ + APT::Periodic::Enable "0"; + """ + And I run `apt-get update` with sudo + And I run `apt-get install jq -y` with sudo + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.apt_periodic_job_enabled` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_running` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.msg` as non-root + Then I will see the following on stdout: + """ + "APT::Periodic::Enable is turned off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.code` as non-root + Then I will see the following on stdout: + """ + "unattended-upgrades-cfg-value-turned-off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"APT::Periodic::Enable\"'` as non-root + Then I will see the following on stdout: + """ + "0" + """ + When I create the file `/etc/apt/apt.conf.d/99test` with the following: + """ + APT::Periodic::Update-Package-Lists "0"; + """ + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.apt_periodic_job_enabled` as non-root + Then I will see the following on stdout: + """ + true + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.package_lists_refresh_frequency_days` as non-root + Then I will see the following on stdout: + """ + 0 + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_running` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.msg` as non-root + Then I will see the following on stdout: + """ + "APT::Periodic::Update-Package-Lists is turned off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.code` as non-root + Then I will see the following on stdout: + """ + "unattended-upgrades-cfg-value-turned-off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"APT::Periodic::Update-Package-Lists\"'` as non-root + Then I will see the following on stdout: + """ + "0" + """ + When I create the file `/etc/apt/apt.conf.d/99test` with the following: + """ + APT::Periodic::Unattended-Upgrade "0"; + """ + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_frequency_days` as non-root + Then I will see the following on stdout: + """ + 0 + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.package_lists_refresh_frequency_days` as non-root + Then I will see the following on stdout: + """ + 1 + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_running` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.msg` as non-root + Then I will see the following on stdout: + """ + "APT::Periodic::Unattended-Upgrade is turned off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.code` as non-root + Then I will see the following on stdout: + """ + "unattended-upgrades-cfg-value-turned-off" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"APT::Periodic::Unattended-Upgrade\"'` as non-root + Then I will see the following on stdout: + """ + "0" + """ + When I run `systemctl stop apt-daily.timer` with sudo + And I run `rm /etc/apt/apt.conf.d/99test` with sudo + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.systemd_apt_timer_enabled` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_running` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.msg` as non-root + Then I will see the following on stdout: + """ + "apt-daily.timer jobs are not running" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.code` as non-root + Then I will see the following on stdout: + """ + "unattended-upgrades-systemd-job-disabled" + """ + When I create the file `/etc/apt/apt.conf.d/50unattended-upgrades` with the following: + """ + APT::Periodic::Unattended-Upgrade "1"; + """ + And I run `systemctl start apt-daily.timer` with sudo + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_frequency_days` as non-root + Then I will see the following on stdout: + """ + 1 + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.systemd_apt_timer_enabled` as non-root + Then I will see the following on stdout: + """ + true + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_allowed_origins` as non-root + Then I will see the following on stdout: + """ + [] + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_running` as non-root + Then I will see the following on stdout: + """ + false + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.msg` as non-root + Then I will see the following on stdout: + """ + "Unattended-Upgrade::Allowed-Origins is empty" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_disabled_reason.code` as non-root + Then I will see the following on stdout: + """ + "unattended-upgrades-cfg-list-value-empty" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"Unattended-Upgrade::Allowed-Origins\"'` as non-root + Then I will see the following on stdout: + """ + null + """ + When I run `/usr/lib/apt/apt.systemd.daily update` with sudo + And I run `/usr/lib/apt/apt.systemd.daily install` with sudo + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq .data.attributes.unattended_upgrades_last_run` as non-root + Then stdout matches regexp: + """ + "(?!null).*" + """ + When I create the file `/etc/apt/apt.conf.d/99test` with the following: + """ + Unattended-Upgrade::Mail "mail"; + Unattended-Upgrade::Package-Blacklist { + "vim"; + }; + """ + And I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"Unattended-Upgrade::Mail\"'` as non-root + Then I will see the following on stdout: + """ + "mail" + """ + When I run shell command `pro api u.unattended_upgrades.status.v1 | jq '.data.meta.raw_config.\"Unattended-Upgrade::Package-Blacklist\"'` as non-root + Then I will see the following on stdout: + """ + [ + "vim" + ] + """ + + Examples: ubuntu release + | release | extra_field | + | xenial | | + | bionic | "Unattended-Upgrade::DevRelease": "false" | + | focal | "Unattended-Upgrade::DevRelease": "auto" | + | jammy | "Unattended-Upgrade::DevRelease": "auto" | + | kinetic | "Unattended-Upgrade::DevRelease": "auto" | + | lunar | "Unattended-Upgrade::DevRelease": "auto" | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/apt_messages.feature ubuntu-advantage-tools-27.14.4~16.04/features/apt_messages.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/apt_messages.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/apt_messages.feature 2023-04-05 15:14:00.000000000 +0000 @@ -216,12 +216,14 @@ | focal | hello | | jammy | hello | - @series.lts + @series.all @uses.config.machine_type.lxd.container Scenario Outline: APT News Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo - When I run `apt-get -o APT::Get::Always-Include-Phased-Updates=true upgrade -y` with sudo + # On interim releases we will not enable any service, so we need a manual apt-get update + When I run `apt-get update` with sudo + When I run `DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::Always-Include-Phased-Updates=true upgrade -y` with sudo When I run `apt-get autoremove -y` with sudo When I run `pro detach --assume-yes` with sudo @@ -246,12 +248,7 @@ ] } """ - # test is too fast and systemd doesn't like triggering motd-news.service - # (during pro refresh messages) too frequently - # So there are "wait"s before each pro refresh messages call - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -293,7 +290,6 @@ # apt update stamp will prevent a apt_news refresh When I run `apt-get update` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -306,11 +302,9 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # manual refresh gets new message - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -325,13 +319,12 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # creates /run/ubuntu-advantage and /var/lib/ubuntu-advantage/messages if not there When I run `rm -rf /run/ubuntu-advantage` with sudo When I run `rm -rf /var/lib/ubuntu-advantage/messages` with sudo When I run `rm /var/lib/apt/periodic/update-success-stamp` with sudo When I run `apt-get update` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -346,7 +339,7 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # more than 3 lines ignored When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -364,9 +357,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -376,7 +367,7 @@ Calculating upgrade... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # more than 77 chars ignored When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -391,9 +382,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -403,7 +392,7 @@ Calculating upgrade... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # end is respected When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -419,9 +408,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -445,9 +432,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -460,7 +445,7 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # begin >30 days ago ignored, even if end is set to future When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -476,9 +461,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -488,7 +471,7 @@ Calculating upgrade... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # begin in future When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -503,9 +486,7 @@ ] } """ - When I wait `1` seconds When I run `pro refresh messages` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -515,7 +496,7 @@ Calculating upgrade... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - + # local apt news overrides for contract expiry notices When I create the file `/var/www/html/aptnews.json` on the `apt-news-server` machine with the following: """ @@ -532,25 +513,15 @@ """ When I attach `contract_token` with sudo When I run `apt upgrade -y` with sudo - When I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today +2}" - } - } - } - """ - And I append the following on uaclient config: + When I set the machine token overlay to the following yaml """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today +2} """ # test that apt update will trigger hook to update apt_news for local override When I run shell command `rm -f /var/lib/apt/periodic/update-success-stamp` with sudo When I run `apt-get update` with sudo - When I run shell command `rm -f /var/lib/ubuntu-advantage/messages/apt-pre*` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout """ @@ -565,17 +536,12 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today -3}" - } - } - } + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today -3} """ - When I wait `1` seconds When I run `pro refresh messages` with sudo When I run `apt upgrade` with sudo Then stdout matches regexp: @@ -592,17 +558,12 @@ # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. """ - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today -20}" - } - } - } + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today -20} """ - When I wait `1` seconds When I run `pro refresh messages` with sudo When I run `apt upgrade` with sudo Then I will see the following on stdout @@ -648,6 +609,7 @@ | bionic | | focal | | jammy | + | kinetic | @series.xenial @series.bionic @@ -655,7 +617,8 @@ Scenario Outline: AWS URLs Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo - When I run `pro refresh messages` with sudo + When I run `apt-get install ansible -y` with sudo + When I run `apt-get update` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: """ @@ -672,7 +635,8 @@ Scenario Outline: Azure URLs Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo - When I run `pro refresh messages` with sudo + When I run `apt-get install ansible -y` with sudo + When I run `apt-get update` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: """ @@ -689,7 +653,8 @@ Scenario Outline: GCP URLs Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo - When I run `pro refresh messages` with sudo + When I run `apt-get install ansible -y` with sudo + When I run `apt-get update` with sudo When I run `apt upgrade --dry-run` with sudo Then stdout matches regexp: """ @@ -699,3 +664,53 @@ | release | msg | | xenial | Learn more about Ubuntu Pro for 16\.04 at https:\/\/ubuntu\.com\/16-04 | | bionic | Learn more about Ubuntu Pro on GCP at https:\/\/ubuntu\.com\/gcp\/pro | + + @series.kinetic + @uses.config.machine_type.lxd.container + Scenario Outline: APT Hook do not advertises esm-apps on upgrade for interim releases + Given a `` machine with ubuntu-advantage-tools installed + When I run `apt-get update` with sudo + When I run `apt-get -o APT::Get::Always-Include-Phased-Updates=true upgrade -y` with sudo + When I run `apt-get -y autoremove` with sudo + When I run `apt-get install hello -y` with sudo + When I run `pro config set apt_news=false` with sudo + When I run `pro refresh messages` with sudo + When I run `apt upgrade` with sudo + Then stdout does not match regexp: + """ + Get more security updates through Ubuntu Pro with 'esm-apps' enabled: + """ + When I run `apt-get upgrade` with sudo + Then I will see the following on stdout: + """ + Reading package lists... + Building dependency tree... + Reading state information... + Calculating upgrade... + 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. + """ + When I attach `contract_token` with sudo + When I run `apt upgrade --dry-run` with sudo + Then stdout matches regexp: + """ + Reading package lists... + Building dependency tree... + Reading state information... + Calculating upgrade... + 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded\. + """ + When I run `apt-get upgrade -y` with sudo + When I run `pro detach --assume-yes` with sudo + When I run `pro refresh messages` with sudo + When I run `apt upgrade` with sudo + Then stdout matches regexp: + """ + Reading package lists... + Building dependency tree... + Reading state information... + Calculating upgrade... + 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded\. + """ + Examples: ubuntu release + | release | + | kinetic | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/attached_commands.feature ubuntu-advantage-tools-27.14.4~16.04/features/attached_commands.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/attached_commands.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/attached_commands.feature 2023-04-05 15:14:00.000000000 +0000 @@ -249,7 +249,7 @@ esm-infra +yes +Expanded Security Maintenance for Infrastructure fips + +NIST-certified core packages fips-updates + +NIST-certified core packages with priority security updates - livepatch +(yes|no) +Canonical Livepatch service + livepatch +(yes|no) +(Canonical Livepatch service|Current kernel is not supported) realtime-kernel + +Ubuntu kernel with PREEMPT_RT patches integrated ros + +Security Updates for the Robot Operating System ros-updates + +All Updates for the Robot Operating System @@ -766,7 +766,7 @@ When I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo And I run `cat /var/lib/ubuntu-advantage/jobs-status.json` with sudo Then stdout matches regexp: - """" + """ "update_messaging": """ When I run `pro config show` with sudo @@ -779,50 +779,27 @@ And I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo And I run `cat /var/lib/ubuntu-advantage/jobs-status.json` with sudo Then stdout matches regexp: - """" + """ "update_messaging": null """ When I delete the file `/var/lib/ubuntu-advantage/jobs-status.json` - And I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + And I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - contract_url: https://contracts.canonical.com - data_dir: /var/lib/ubuntu-advantage - log_file: /var/log/ubuntu-advantage.log - log_level: debug - security_url: https://ubuntu.com/security - ua_config: - apt_http_proxy: null - apt_https_proxy: null - http_proxy: null - https_proxy: null - update_messaging_timer: 14400 - metering_timer: 0 + { "metering_timer": 0 } """ And I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo And I run `cat /var/lib/ubuntu-advantage/jobs-status.json` with sudo Then stdout matches regexp: - """" + """ "metering": null """ When I delete the file `/var/lib/ubuntu-advantage/jobs-status.json` - And I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + And I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - contract_url: https://contracts.canonical.com - data_dir: /var/lib/ubuntu-advantage - log_file: /var/log/ubuntu-advantage.log - log_level: debug - security_url: https://ubuntu.com/security - ua_config: - apt_http_proxy: null - apt_https_proxy: null - http_proxy: null - https_proxy: null - update_messaging_timer: -10 - metering_timer: notanumber + { "metering_timer": "notanumber", "update_messaging_timer": -10 } """ And I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo Then I verify that running `grep "Invalid value for update_messaging interval found in config." /var/log/ubuntu-advantage-timer.log` `with sudo` exits `0` - And I verify that running `grep "Invalid value for metering interval found in config." /var/log/ubuntu-advantage-timer.log` `with sudo` exits `0` And I verify that the timer interval for `update_messaging` is `21600` And I verify that the timer interval for `metering` is `14400` When I create the file `/var/lib/ubuntu-advantage/jobs-status.json` with the following: @@ -832,15 +809,15 @@ And I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo And I run `cat /var/lib/ubuntu-advantage/jobs-status.json` with sudo Then stdout does not match regexp: - """" + """ "update_status" """ And stdout matches regexp: - """" + """ "metering" """ And stdout matches regexp: - """" + """ "update_messaging" """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/attached_enable.feature ubuntu-advantage-tools-27.14.4~16.04/features/attached_enable.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/attached_enable.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/attached_enable.feature 2023-04-05 15:14:00.000000000 +0000 @@ -79,27 +79,14 @@ Scenario Outline: Empty series affordance means no series, null means all series Given a `` machine with ubuntu-advantage-tools installed When I attach `contract_token` with sudo and options `--no-auto-enable` - When I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "esm-infra", - "affordances": { - "series": [] - } - } - ] - } - } - } + When I set the machine token overlay to the following yaml """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: esm-infra + affordances: + series: [] """ When I verify that running `pro enable esm-infra` `with sudo` exits `1` Then stdout matches regexp: @@ -344,25 +331,13 @@ @uses.config.machine_type.lxd.container Scenario Outline: Attached enable not entitled service in a ubuntu machine Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "esm-apps", - "entitled": false - } - ] - } - } - } - """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: esm-apps + entitled: false """ When I attach `contract_token` with sudo Then I verify that running `pro enable esm-apps` `as non-root` exits `1` @@ -1133,76 +1108,46 @@ @uses.config.machine_type.aws.generic Scenario: Cloud overrides for a generic aws Focal instance Given a `focal` machine with ubuntu-advantage-tools installed - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "fips", - "entitled": true, - "affordances": { - "architectures": [ - "amd64", - "ppc64el", - "ppc64le", - "s390x", - "x86_64" - ], - "series": [ - "xenial", - "bionic", - "focal" - ] - }, - "directives": { - "additionalPackages": [ - "ubuntu-fips" - ], - "aptKey": "E23341B2A1467EDBF07057D6C1997C40EDE22758", - "aptURL": "https://esm.ubuntu.com/fips", - "suites": [ - "xenial", - "bionic", - "focal" - ] - }, - "obligations": { - "enableByDefault": false - }, - "overrides": [ - { - "selector": { - "series": "focal" - }, - "directives": { - "additionalPackages": [ - "some-package-focal" - ] - } - }, - { - "selector": { - "cloud": "aws" - }, - "directives": { - "additionalPackages": [ - "some-package-aws" - ] - } - } - ] - } - ] - } - } - } - """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: fips + entitled: true + affordances: + architectures: + - amd64 + - ppc64el + - ppc64le + - s390x + - x86_64 + series: + - xenial + - bionic + - focal + directives: + additionalPackages: + - ubuntu-fips + aptKey: E23341B2A1467EDBF07057D6C1997C40EDE22758 + aptURL: https://esm.ubuntu.com/fips + suites: + - xenial + - bionic + - focal + obligations: + enableByDefault: false + overrides: + - selector: + series: focal + directives: + additionalPackages: + - some-package-focal + - selector: + cloud: aws + directives: + additionalPackages: + - some-package-aws """ And I attach `contract_token` with sudo And I verify that running `pro enable fips --assume-yes` `with sudo` exits `1` @@ -1284,3 +1229,29 @@ | xenial | jq | | bionic | bundler | | focal | ant | + + @series.lts + @uses.config.machine_type.lxd.container + Scenario Outline: Attached enable with corrupt lock + Given a `` machine with ubuntu-advantage-tools installed + When I attach `contract_token` with sudo + And I run `pro disable esm-infra --assume-yes` with sudo + And I create the file `/var/lib/ubuntu-advantage/lock` with the following: + """ + corrupted + """ + Then I verify that running `pro enable esm-infra --assume-yes` `with sudo` exits `1` + And stderr matches regexp: + """ + There is a corrupted lock file in the system. To continue, please remove it + from the system by running: + + \$ sudo rm /var/lib/ubuntu-advantage/lock + """ + + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | jammy | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/attach_validtoken.feature ubuntu-advantage-tools-27.14.4~16.04/features/attach_validtoken.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/attach_validtoken.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/attach_validtoken.feature 2023-04-05 15:14:00.000000000 +0000 @@ -198,25 +198,13 @@ @uses.config.machine_type.aws.generic Scenario Outline: Attach command in an generic AWS Ubuntu VM Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "esm-apps", - "entitled": false - } - ] - } - } - } - """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: esm-apps + entitled: false """ And I attach `contract_token` with sudo Then stdout matches regexp: @@ -247,25 +235,13 @@ @uses.config.machine_type.azure.generic Scenario Outline: Attach command in an generic Azure Ubuntu VM Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "esm-apps", - "entitled": false - } - ] - } - } - } + When I set the machine token overlay to the following yaml """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: esm-apps + entitled: false """ And I attach `contract_token` with sudo Then stdout matches regexp: @@ -296,25 +272,13 @@ @uses.config.machine_type.gcp.generic Scenario Outline: Attach command in an generic GCP Ubuntu VM Given a `` machine with ubuntu-advantage-tools installed - When I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "resourceEntitlements": [ - { - "type": "esm-apps", - "entitled": false - } - ] - } - } - } + When I set the machine token overlay to the following yaml """ - And I append the following on uaclient config: - """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + resourceEntitlements: + - type: esm-apps + entitled: false """ And I attach `contract_token` with sudo Then stdout matches regexp: @@ -386,22 +350,15 @@ """ esm-infra +yes +enabled +Expanded Security Maintenance for Infrastructure """ - When I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "2000-01-02T03:04:05Z" - } - } - } + When I set the machine token overlay to the following yaml """ - And I append the following on uaclient config: + machineTokenInfo: + contractInfo: + effectiveTo: 2000-01-02T03:04:05Z """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" - """ - When I run `pro status` with sudo + And I delete the file `/var/lib/ubuntu-advantage/jobs-status.json` + And I run `python3 /usr/lib/ubuntu-advantage/timer.py` with sudo + And I run `pro status` with sudo Then stdout matches regexp: """ A change has been detected in your contract. @@ -412,7 +369,8 @@ """ Successfully refreshed your subscription. """ - When I run `sed -i '/^.*machine_token_overlay:/d' /etc/ubuntu-advantage/uaclient.conf` with sudo + # remove machine token overlay + When I change config key `features` to use value `{}` And I run `pro status` with sudo Then stdout does not match regexp: """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/config.feature ubuntu-advantage-tools-27.14.4~16.04/features/config.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/config.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/config.feature 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,55 @@ +Feature: pro config sub-command + + @series.xenial + @series.jammy + @series.kinetic + @uses.config.machine_type.lxd.container + Scenario Outline: old ua_config in uaclient.conf is still supported + Given a `` machine with ubuntu-advantage-tools installed + When I run `pro config show` with sudo + Then I will see the following on stdout: + """ + http_proxy None + https_proxy None + apt_http_proxy None + apt_https_proxy None + ua_apt_http_proxy None + ua_apt_https_proxy None + global_apt_http_proxy None + global_apt_https_proxy None + update_messaging_timer 21600 + metering_timer 14400 + apt_news True + apt_news_url https://motd.ubuntu.com/aptnews.json + """ + Then I will see the following on stderr: + """ + """ + When I append the following on uaclient config: + """ + ua_config: {apt_news: false} + """ + When I run `pro config show` with sudo + Then I will see the following on stdout: + """ + http_proxy None + https_proxy None + apt_http_proxy None + apt_https_proxy None + ua_apt_http_proxy None + ua_apt_https_proxy None + global_apt_http_proxy None + global_apt_https_proxy None + update_messaging_timer 21600 + metering_timer 14400 + apt_news False + apt_news_url https://motd.ubuntu.com/aptnews.json + """ + Then I will see the following on stderr: + """ + """ + Examples: ubuntu release + | release | + | xenial | + | jammy | + | kinetic | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/daemon.feature ubuntu-advantage-tools-27.14.4~16.04/features/daemon.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/daemon.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/daemon.feature 2023-04-05 15:14:00.000000000 +0000 @@ -86,10 +86,9 @@ """ # verify it stays on when configured to do so - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - poll_for_pro_license: true + { "poll_for_pro_license": true } """ # Turn on memory accounting When I run `sed -i s/#DefaultMemoryAccounting=no/DefaultMemoryAccounting=yes/ /etc/systemd/system.conf` with sudo diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/environment.py ubuntu-advantage-tools-27.14.4~16.04/features/environment.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/environment.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/environment.py 2023-04-05 15:14:00.000000000 +0000 @@ -390,11 +390,8 @@ def before_scenario(context: Context, scenario: Scenario): - """ - In this function, we launch a container, install ubuntu-advantage-tools and - then capture an image. This image is then reused by each scenario, reducing - test execution time. - """ + context.stored_vars = {} + reason = _should_skip_tags(context, scenario.effective_tags) if reason: scenario.skip(reason=reason) @@ -419,10 +416,19 @@ return # before_step doesn't execute early enough to modify the step - # so we perform the version step surgery here + # so we perform step text surgery here + # Also, logging capture is not set up when before_scenario is called, + # so if you call logging.info here, then the root logger gets configured + # and it messes up all the future behave logging capture machinery. + # See https://github.com/behave/behave/blob/v1.2.6/behave/model.py#L700 + # But we want to log the replacement we're making, so we use the behave + # logger and warning log_level to make sure it gets included. + logger = logging.getLogger("behave.before_scenario.process_template_vars") for step in scenario.steps: if step.text: - step.text = process_template_vars(context, step.text) + step.text = process_template_vars( + context, step.text, logger_fn=logger.warn + ) FAILURE_FILES = ( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/fix.feature ubuntu-advantage-tools-27.14.4~16.04/features/fix.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/fix.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/fix.feature 2023-04-05 15:14:00.000000000 +0000 @@ -6,6 +6,7 @@ Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo When I run `apt remove ca-certificates -y` with sudo + When I run `rm -f /etc/ssl/certs/ca-certificates.crt` with sudo When I verify that running `ua fix CVE-1800-123456` `as non-root` exits `1` Then stderr matches regexp: """ @@ -64,30 +65,36 @@ """ USN-4539-1: AWL vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2020-11728 + - https://ubuntu.com/security/CVE-2020-11728 + 1 affected source package is installed: awl \(1/1\) awl: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y libawl-php \}.* + .*✔.* USN-4539-1 is resolved. """ When I run `pro fix CVE-2020-28196` as non-root Then stdout matches regexp: """ CVE-2020-28196: Kerberos vulnerability - https://ubuntu.com/security/CVE-2020-28196 + - https://ubuntu.com/security/CVE-2020-28196 + 1 affected source package is installed: krb5 \(1/1\) krb5: A fix is available in Ubuntu standard updates. The update is already installed. + .*✔.* CVE-2020-28196 is resolved. """ When I run `pro fix CVE-2022-24959` as non-root Then stdout matches regexp: """ CVE-2022-24959: Linux kernel vulnerabilities - https://ubuntu.com/security/CVE-2022-24959 + - https://ubuntu.com/security/CVE-2022-24959 + No affected source packages are installed. + .*✔.* CVE-2022-24959 does not affect your system. """ @@ -108,10 +115,12 @@ """ USN-4539-1: AWL vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2020-11728 + - https://ubuntu.com/security/CVE-2020-11728 + 1 affected source package is installed: awl \(1/1\) awl: Ubuntu security engineers are investigating this issue. + 1 package is still affected: awl .*✘.* USN-4539-1 is not resolved. """ @@ -119,19 +128,23 @@ Then stdout matches regexp: """ CVE-2020-15180: MariaDB vulnerabilities - https://ubuntu.com/security/CVE-2020-15180 + - https://ubuntu.com/security/CVE-2020-15180 + No affected source packages are installed. + .*✔.* CVE-2020-15180 does not affect your system. """ When I run `pro fix CVE-2020-28196` as non-root Then stdout matches regexp: """ CVE-2020-28196: Kerberos vulnerability - https://ubuntu.com/security/CVE-2020-28196 + - https://ubuntu.com/security/CVE-2020-28196 + 1 affected source package is installed: krb5 \(1/1\) krb5: A fix is available in Ubuntu standard updates. The update is already installed. + .*✔.* CVE-2020-28196 is resolved. """ When I run `DEBIAN_FRONTEND=noninteractive apt-get install -y expat=2.1.0-7 swish-e matanza ghostscript` with sudo @@ -141,13 +154,15 @@ .*WARNING: The option --dry-run is being used. No packages will be installed when running this command..* CVE-2017-9233: Coin3D vulnerability - https://ubuntu.com/security/CVE-2017-9233 + - https://ubuntu.com/security/CVE-2017-9233 + 3 affected source packages are installed: expat, matanza, swish-e \(1/3, 2/3\) matanza, swish-e: Ubuntu security engineers are investigating this issue. \(3/3\) expat: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y expat \}.* + 2 packages are still affected: matanza, swish-e .*✘.* CVE-2017-9233 is not resolved. """ @@ -155,13 +170,15 @@ Then stdout matches regexp: """ CVE-2017-9233: Coin3D vulnerability - https://ubuntu.com/security/CVE-2017-9233 + - https://ubuntu.com/security/CVE-2017-9233 + 3 affected source packages are installed: expat, matanza, swish-e \(1/3, 2/3\) matanza, swish-e: Ubuntu security engineers are investigating this issue. \(3/3\) expat: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y expat \}.* + 2 packages are still affected: matanza, swish-e .*✘.* CVE-2017-9233 is not resolved. """ @@ -172,19 +189,23 @@ No packages will be installed when running this command..* USN-5079-2: curl vulnerabilities Found CVEs: - https://ubuntu.com/security/CVE-2021-22946 - https://ubuntu.com/security/CVE-2021-22947 + - https://ubuntu.com/security/CVE-2021-22946 + - https://ubuntu.com/security/CVE-2021-22947 + 1 affected source package is installed: curl \(1/1\) curl: A fix is available in Ubuntu Pro: ESM Infra. + .*The machine is not attached to an Ubuntu Pro subscription. To proceed with the fix, a prompt would ask for a valid Ubuntu Pro token. \{ pro attach TOKEN \}.* + .*Ubuntu Pro service: esm-infra is not enabled. To proceed with the fix, a prompt would ask permission to automatically enable this service. \{ pro enable esm-infra \}.* .*\{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls \}.* + .*✔.* USN-5079-2 is resolved. """ When I fix `USN-5079-2` by attaching to a subscription with `contract_token_staging_expired` @@ -192,8 +213,9 @@ """ USN-5079-2: curl vulnerabilities Found CVEs: - https://ubuntu.com/security/CVE-2021-22946 - https://ubuntu.com/security/CVE-2021-22947 + - https://ubuntu.com/security/CVE-2021-22946 + - https://ubuntu.com/security/CVE-2021-22947 + 1 affected source package is installed: curl \(1/1\) curl: A fix is available in Ubuntu Pro: ESM Infra. @@ -206,6 +228,7 @@ Attach denied: Contract ".*" expired on .* Visit https://ubuntu.com/pro to manage contract tokens. + 1 package is still affected: curl .*✘.* USN-5079-2 is not resolved. """ @@ -214,8 +237,9 @@ """ USN-5079-2: curl vulnerabilities Found CVEs: - https://ubuntu.com/security/CVE-2021-22946 - https://ubuntu.com/security/CVE-2021-22947 + - https://ubuntu.com/security/CVE-2021-22946 + - https://ubuntu.com/security/CVE-2021-22947 + 1 affected source package is installed: curl \(1/1\) curl: A fix is available in Ubuntu Pro: ESM Infra. @@ -233,6 +257,7 @@ And stdout matches regexp: """ .*\{ apt update && apt install --only-upgrade -y curl libcurl3-gnutls \}.* + .*✔.* USN-5079-2 is resolved. """ When I verify that running `pro fix USN-5051-2` `with sudo` exits `2` @@ -240,11 +265,13 @@ """ USN-5051-2: OpenSSL vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2021-3712 + - https://ubuntu.com/security/CVE-2021-3712 + 1 affected source package is installed: openssl \(1/1\) openssl: A fix is available in Ubuntu Pro: ESM Infra. .*\{ apt update && apt install --only-upgrade -y libssl1.0.0 openssl \}.* + A reboot is required to complete fix operation. .*✘.* USN-5051-2 is not resolved. """ @@ -257,15 +284,18 @@ No packages will be installed when running this command..* USN-5378-4: Gzip vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2022-1271 + - https://ubuntu.com/security/CVE-2022-1271 + 2 affected source packages are installed: gzip, xz-utils \(1/2, 2/2\) gzip, xz-utils: A fix is available in Ubuntu Pro: ESM Infra. + .*Ubuntu Pro service: esm-infra is not enabled. To proceed with the fix, a prompt would ask permission to automatically enable this service. \{ pro enable esm-infra \}.* .*\{ apt update && apt install --only-upgrade -y gzip liblzma5 xz-utils \}.* + .*✔.* USN-5378-4 is resolved. """ When I run `pro fix USN-5378-4` `with sudo` and stdin `E` @@ -273,7 +303,8 @@ """ USN-5378-4: Gzip vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2022-1271 + - https://ubuntu.com/security/CVE-2022-1271 + 2 affected source packages are installed: gzip, xz-utils \(1/2, 2/2\) gzip, xz-utils: A fix is available in Ubuntu Pro: ESM Infra. @@ -286,6 +317,7 @@ Updating package lists Ubuntu Pro: ESM Infra enabled .*\{ apt update && apt install --only-upgrade -y gzip liblzma5 xz-utils \}.* + .*✔.* USN-5378-4 is resolved. """ @@ -328,10 +360,12 @@ No packages will be installed when running this command..* USN-4539-1: AWL vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2020-11728 + - https://ubuntu.com/security/CVE-2020-11728 + 1 affected source package is installed: awl \(1/1\) awl: Ubuntu security engineers are investigating this issue. + 1 package is still affected: awl .*✘.* USN-4539-1 is not resolved. """ @@ -340,10 +374,12 @@ """ USN-4539-1: AWL vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2020-11728 + - https://ubuntu.com/security/CVE-2020-11728 + 1 affected source package is installed: awl \(1/1\) awl: Ubuntu security engineers are investigating this issue. + 1 package is still affected: awl .*✘.* USN-4539-1 is not resolved. """ @@ -351,11 +387,13 @@ Then stdout matches regexp: """ CVE-2020-28196: Kerberos vulnerability - https://ubuntu.com/security/CVE-2020-28196 + - https://ubuntu.com/security/CVE-2020-28196 + 1 affected source package is installed: krb5 \(1/1\) krb5: A fix is available in Ubuntu standard updates. The update is already installed. + .*✔.* CVE-2020-28196 is resolved. """ When I run `apt-get install xterm=330-1ubuntu2 -y` with sudo @@ -363,12 +401,14 @@ Then stdout matches regexp: """ CVE-2021-27135: xterm vulnerability - https://ubuntu.com/security/CVE-2021-27135 + - https://ubuntu.com/security/CVE-2021-27135 + 1 affected source package is installed: xterm \(1/1\) xterm: A fix is available in Ubuntu standard updates. Package fixes cannot be installed. To install them, run this command as root \(try using sudo\) + 1 package is still affected: xterm .*✘.* CVE-2021-27135 is not resolved. """ @@ -378,33 +418,39 @@ .*WARNING: The option --dry-run is being used. No packages will be installed when running this command..* CVE-2021-27135: xterm vulnerability - https://ubuntu.com/security/CVE-2021-27135 + - https://ubuntu.com/security/CVE-2021-27135 + 1 affected source package is installed: xterm \(1/1\) xterm: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y xterm \}.* + .*✔.* CVE-2021-27135 is resolved. """ When I run `pro fix CVE-2021-27135` with sudo Then stdout matches regexp: """ CVE-2021-27135: xterm vulnerability - https://ubuntu.com/security/CVE-2021-27135 + - https://ubuntu.com/security/CVE-2021-27135 + 1 affected source package is installed: xterm \(1/1\) xterm: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y xterm \}.* + .*✔.* CVE-2021-27135 is resolved. """ When I run `pro fix CVE-2021-27135` with sudo Then stdout matches regexp: """ CVE-2021-27135: xterm vulnerability - https://ubuntu.com/security/CVE-2021-27135 + - https://ubuntu.com/security/CVE-2021-27135 + 1 affected source package is installed: xterm \(1/1\) xterm: A fix is available in Ubuntu standard updates. The update is already installed. + .*✔.* CVE-2021-27135 is resolved. """ When I run `apt-get install libbz2-1.0=1.0.6-8.1 -y --allow-downgrades` with sudo @@ -414,10 +460,36 @@ """ USN-4038-3: bzip2 regression Found Launchpad bugs: - https://launchpad.net/bugs/1834494 + - https://launchpad.net/bugs/1834494 + 1 affected source package is installed: bzip2 \(1/1\) bzip2: A fix is available in Ubuntu standard updates. .*\{ apt update && apt install --only-upgrade -y bzip2 libbz2-1.0 \}.* + .*✔.* USN-4038-3 is resolved. """ + + @series.bionic + @uses.config.machine_type.lxd.container + Scenario: Fix command on a machine without security/updates source lists + Given a `bionic` machine with ubuntu-advantage-tools installed + When I run `sed -i "/bionic-updates/d" /etc/apt/sources.list` with sudo + And I run `sed -i "/bionic-security/d" /etc/apt/sources.list` with sudo + And I run `apt-get update` with sudo + And I run `wget -O pkg.deb https://launchpad.net/ubuntu/+source/openssl/1.1.1-1ubuntu2.1~18.04.14/+build/22454675/+files/openssl_1.1.1-1ubuntu2.1~18.04.14_amd64.deb` as non-root + And I run `dpkg -i pkg.deb` with sudo + And I verify that running `pro fix CVE-2023-0286` `as non-root` exits `1` + Then stdout matches regexp: + """ + CVE-2023-0286: OpenSSL vulnerabilities + - https://ubuntu.com/security/CVE-2023-0286 + + 2 affected source packages are installed: openssl, openssl1.0 + \(1/2, 2/2\) openssl, openssl1.0: + A fix is available in Ubuntu standard updates. + - Cannot install package openssl version 1.1.1-1ubuntu2.1~18.04.21 + + 1 package is still affected: openssl + .*✘.* CVE-2023-0286 is not resolved. + """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/livepatch.feature ubuntu-advantage-tools-27.14.4~16.04/features/livepatch.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/livepatch.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/livepatch.feature 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,103 @@ +@uses.config.contract_token +Feature: Livepatch + + @series.focal + @uses.config.machine_type.lxd.vm + Scenario Outline: Attached livepatch status shows warning when on unsupported kernel + Given a `` machine with ubuntu-advantage-tools installed + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +Current kernel is not supported + """ + Then stdout matches regexp: + """ + Supported livepatch kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels + """ + When I attach `contract_token` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +warning +Current kernel is not supported + """ + Then stdout matches regexp: + """ + NOTICES + The current kernel \(5.4.0-(\d+)-kvm, amd64\) is not supported by livepatch. + Supported kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels + Either switch to a supported kernel or `pro disable livepatch` to dismiss this warning. + + """ + When I run `pro disable livepatch` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +disabled +Current kernel is not supported + """ + Then stdout does not match regexp: + """ + NOTICES + The current kernel \(5.4.0-(\d+)-kvm, amd64\) is not supported by livepatch. + Supported kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels + Either switch to a supported kernel or `pro disable livepatch` to dismiss this warning. + + """ + When I run `apt-get install linux-generic -y` with sudo + When I run `DEBIAN_FRONTEND=noninteractive apt-get remove linux-image*-kvm -y` with sudo + When I run `update-grub` with sudo + When I reboot the machine + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +disabled +Canonical Livepatch service + """ + When I run `pro enable livepatch` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +enabled +Canonical Livepatch service + """ + When I run `pro detach --assume-yes` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + livepatch +yes +Canonical Livepatch service + """ + Then stdout does not match regexp: + """ + Supported livepatch kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels + """ + Examples: ubuntu release + | release | + | focal | + + @series.kinetic + @series.lunar + @uses.config.machine_type.lxd.vm + Scenario Outline: Livepatch is not enabled by default and can't be enabled on interim releases + Given a `` machine with ubuntu-advantage-tools installed + When I run `pro status --all` with sudo + Then stdout matches regexp: + """ + livepatch +no +Current kernel is not supported + """ + When I attach `contract_token` with sudo + When I run `pro status --all` with sudo + Then stdout matches regexp: + """ + livepatch +yes +n/a +Canonical Livepatch service + """ + When I verify that running `pro enable livepatch` `with sudo` exits `1` + Then stdout contains substring: + """ + Livepatch is not available for Ubuntu . + """ + When I run `pro status --all` with sudo + Then stdout matches regexp: + """ + livepatch +yes +n/a +Canonical Livepatch service + """ + Examples: ubuntu release + | release | pretty_name | + | kinetic | 22.10 (Kinetic Kudu) | + | lunar | 23.04 (Lunar Lobster) | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/logs.feature ubuntu-advantage-tools-27.14.4~16.04/features/logs.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/logs.feature 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/logs.feature 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,30 @@ +Feature: Logs in Json Array Formatter + + @series.all + @uses.config.machine_type.lxd.container + Scenario Outline: The log file can be successfully parsed as json array + Given a `` machine with ubuntu-advantage-tools installed + When I run `apt update` with sudo + And I run `apt install jq -y` with sudo + And I verify that running `pro status` `with sudo` exits `0` + And I verify that running `pro enable test_entitlement` `with sudo` exits `1` + And I run shell command `tail /var/log/ubuntu-advantage.log | jq -r .` as non-root + Then I will see the following on stderr + """ + """ + When I attach `contract_token` with sudo + And I verify that running `pro refresh` `with sudo` exits `0` + And I verify that running `pro status` `with sudo` exits `0` + And I verify that running `pro enable test_entitlement` `with sudo` exits `1` + And I run shell command `tail /var/log/ubuntu-advantage.log | jq -r .` as non-root + Then I will see the following on stderr + """ + """ + Examples: ubuntu release + | release | + | xenial | + | bionic | + | focal | + | kinetic | + | jammy | + | lunar | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/motd_messages.feature ubuntu-advantage-tools-27.14.4~16.04/features/motd_messages.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/motd_messages.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/motd_messages.feature 2023-04-05 15:14:00.000000000 +0000 @@ -3,105 +3,7 @@ @series.xenial @series.bionic @uses.config.machine_type.lxd.container - Scenario Outline: MOTD Announce Message - Given a `` machine with ubuntu-advantage-tools installed - When I run `apt-get install -y update-motd` with sudo - When I run `pro refresh messages` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: - """ - - \* Introducing Expanded Security Maintenance for Applications\. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription\. Free for personal use\. - - - - [\w\d]+ - """ - When I attach `contract_token` with sudo - And I run `update-motd` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout does not match regexp: - """ - \* Introducing Expanded Security Maintenance for Applications\. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription\. Free for personal use\. - - - """ - Examples: ubuntu release - | release | url | - | xenial | https:\/\/ubuntu.com\/16-04 | - | bionic | https:\/\/ubuntu.com\/pro | - - @series.xenial - @series.bionic - @uses.config.machine_type.aws.generic - Scenario Outline: AWS URLs - Given a `` machine with ubuntu-advantage-tools installed - When I run `apt-get install -y update-motd` with sudo - When I run `pro refresh messages` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: - """ - \* Introducing Expanded Security Maintenance for Applications\. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription\. Free for personal use\. - - - """ - Examples: ubuntu release - | release | url | - | xenial | https:\/\/ubuntu.com\/16-04 | - | bionic | https:\/\/ubuntu.com\/aws\/pro | - - @series.xenial - @series.bionic - @uses.config.machine_type.azure.generic - Scenario Outline: Azure URLs - Given a `` machine with ubuntu-advantage-tools installed - When I run `apt-get install -y update-motd` with sudo - When I run `pro refresh messages` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: - """ - \* Introducing Expanded Security Maintenance for Applications\. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription\. Free for personal use\. - - - """ - Examples: ubuntu release - | release | url | - | xenial | https:\/\/ubuntu.com\/16-04\/azure | - | bionic | https:\/\/ubuntu.com\/azure\/pro | - - @series.xenial - @series.bionic - @uses.config.machine_type.gcp.generic - Scenario Outline: GCP URLs - Given a `` machine with ubuntu-advantage-tools installed - When I run `apt-get install -y update-motd` with sudo - When I run `pro refresh messages` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout matches regexp: - """ - \* Introducing Expanded Security Maintenance for Applications\. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription\. Free for personal use\. - - - """ - Examples: ubuntu release - | release | url | - | xenial | https:\/\/ubuntu.com\/16-04 | - | bionic | https:\/\/ubuntu.com\/gcp\/pro | - - @series.xenial - @series.bionic - @uses.config.machine_type.lxd.container - Scenario Outline: MOTD Contract Expiration Notices After Contract Update + Scenario Outline: Contract update prevents contract expiration messages Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo When I attach `contract_token` with sudo @@ -119,7 +21,6 @@ [\w\d.]+ """ When I update contract to use `effectiveTo` as `$behave_var{today -3}` - When I wait `1` seconds When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout does not match regexp: @@ -134,7 +35,6 @@ [\w\d.]+ """ When I update contract to use `effectiveTo` as `$behave_var{today -20}` - When I wait `1` seconds When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout does not match regexp: @@ -147,19 +47,6 @@ [\w\d.]+ """ - When I run `apt-get upgrade -y` with sudo - When I wait `1` seconds - When I run `pro refresh messages` with sudo - And I run `run-parts /etc/update-motd.d/` with sudo - Then stdout does not match regexp: - """ - [\w\d.]+ - - \*Your Ubuntu Pro subscription has EXPIRED\* - Renew your service at https:\/\/ubuntu.com\/pro - - [\w\d.]+ - """ Examples: ubuntu release | release | service | | xenial | esm-infra | @@ -169,27 +56,17 @@ @series.xenial @series.bionic @uses.config.machine_type.lxd.container - Scenario Outline: MOTD Contract Expiration Notices with contract not updated + Scenario Outline: Contract Expiration Messages Given a `` machine with ubuntu-advantage-tools installed When I run `apt-get update` with sudo And I run `apt-get install ansible -y` with sudo And I attach `contract_token` with sudo - And I create the file `/tmp/machine-token-overlay.json` with the following: - """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today +2}" - } - } - } - """ - And I append the following on uaclient config: + And I set the machine token overlay to the following yaml """ - features: - machine_token_overlay: "/tmp/machine-token-overlay.json" + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today +2} """ - And I wait `1` seconds And I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout matches regexp: @@ -202,17 +79,12 @@ [\w\d.]+ """ - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today -3}" - } - } - } + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today -3} """ - When I wait `1` seconds When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout matches regexp: @@ -226,17 +98,12 @@ [\w\d.]+ """ - When I create the file `/tmp/machine-token-overlay.json` with the following: + When I set the machine token overlay to the following yaml """ - { - "machineTokenInfo": { - "contractInfo": { - "effectiveTo": "$behave_var{today -20}" - } - } - } + machineTokenInfo: + contractInfo: + effectiveTo: $behave_var{today -20} """ - When I wait `1` seconds When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout matches regexp: @@ -250,7 +117,6 @@ [\w\d.]+ """ When I run `apt-get upgrade -y` with sudo - When I wait `1` seconds When I run `pro refresh messages` with sudo And I run `run-parts /etc/update-motd.d/` with sudo Then stdout matches regexp: diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/proxy_config.feature ubuntu-advantage-tools-27.14.4~16.04/features/proxy_config.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/proxy_config.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/proxy_config.feature 2023-04-05 15:14:00.000000000 +0000 @@ -110,11 +110,12 @@ And I run `pro config unset ua_apt_https_proxy` with sudo And I run `pro refresh config` with sudo Then I verify that no files exist matching `/etc/apt/apt.conf.d/90ubuntu-advantage-aptproxy` - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - ua_apt_http_proxy: "invalidurl" - ua_apt_https_proxy: "invalidurls" + { + "ua_apt_http_proxy": "invalidurl", + "ua_apt_https_proxy": "invalidurls" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -212,11 +213,12 @@ Setting Livepatch proxy Successfully processed your pro configuration. """ - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - http_proxy: "" - https_proxy: "" + { + "http_proxy": "", + "https_proxy": "" + } """ And I run `pro refresh config` with sudo Then I will see the following on stdout: @@ -226,21 +228,23 @@ Successfully processed your pro configuration. """ - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - http_proxy: "invalidurl" - https_proxy: "invalidurls" + { + "http_proxy": "invalidurl", + "https_proxy": "invalidurls" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: """ \"invalidurl\" is not a valid url. Not setting as proxy. """ - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - https_proxy: "https://localhost:12345" + { + "https_proxy": "https://localhost:12345" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -413,15 +417,12 @@ dns_v4_first on\nacl all src 0.0.0.0\/0\nhttp_access allow all """ And I run `systemctl restart squid.service` `with sudo` on the `proxy` machine - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` 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 - ua_config: - http_proxy: http://$behave_var{machine-ip proxy}:3128 - https_proxy: http://$behave_var{machine-ip proxy}:3128 + { + "http_proxy": "http://$behave_var{machine-ip proxy}:3128", + "https_proxy": "http://$behave_var{machine-ip proxy}:3128" + } """ And I verify `/var/log/squid/access.log` is empty on `proxy` machine # We need this for the route command @@ -438,15 +439,12 @@ esm-infra +yes +disabled +Expanded Security Maintenance for Infrastructure """ When I run `truncate -s 0 /var/log/squid/access.log` `with sudo` on the `proxy` machine - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` 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 - ua_config: - ua_apt_http_proxy: http://$behave_var{machine-ip proxy}:3128 - ua_apt_https_proxy: http://$behave_var{machine-ip proxy}:3128 + { + "ua_apt_http_proxy": "http://$behave_var{machine-ip proxy}:3128", + "ua_apt_https_proxy": "http://$behave_var{machine-ip proxy}:3128" + } """ And I verify `/var/log/squid/access.log` is empty on `proxy` machine Then I verify that no files exist matching `/etc/apt/apt.conf.d/90ubuntu-advantage-aptproxy` @@ -496,11 +494,12 @@ And I run `pro config unset ua_apt_https_proxy` with sudo And I run `pro refresh config` with sudo Then I verify that no files exist matching `/etc/apt/apt.conf.d/90ubuntu-advantage-aptproxy` - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - ua_apt_http_proxy: "invalidurl" - ua_apt_https_proxy: "invalidurls" + { + "ua_apt_http_proxy": "invalidurl", + "ua_apt_https_proxy": "invalidurls" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -512,10 +511,11 @@ """ \"http://host:port\" is not a valid url. Not setting as proxy """ - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - ua_apt_https_proxy: "https://localhost:12345" + { + "ua_apt_https_proxy": "https://localhost:12345" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -564,15 +564,12 @@ dns_v4_first on\nauth_param basic program \/usr\/lib\/squid\/basic_ncsa_auth \/etc\/squid\/passwordfile\nacl topsecret proxy_auth REQUIRED\nhttp_access allow topsecret """ And I run `systemctl restart squid.service` `with sudo` on the `proxy` machine - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` 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 - ua_config: - http_proxy: http://someuser:somepassword@$behave_var{machine-ip proxy}:3128 - https_proxy: http://someuser:somepassword@$behave_var{machine-ip proxy}:3128 + { + "http_proxy": "http://someuser:somepassword@$behave_var{machine-ip proxy}:3128", + "https_proxy": "http://someuser:somepassword@$behave_var{machine-ip proxy}:3128" + } """ And I verify `/var/log/squid/access.log` is empty on `proxy` machine And I attach `contract_token` with sudo @@ -582,15 +579,12 @@ .*CONNECT contracts.canonical.com.* """ When I run `truncate -s 0 /var/log/squid/access.log` `with sudo` on the `proxy` machine - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` 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 - ua_config: - ua_apt_http_proxy: http://someuser:somepassword@$behave_var{machine-ip proxy}:3128 - ua_apt_https_proxy: http://someuser:somepassword@$behave_var{machine-ip proxy}:3128 + { + "ua_apt_http_proxy": "http://someuser:somepassword@$behave_var{machine-ip proxy}:3128" + "ua_apt_https_proxy": "http://someuser:somepassword@$behave_var{machine-ip proxy}:3128" + } """ And I verify `/var/log/squid/access.log` is empty on `proxy` machine And I run `pro refresh config` with sudo @@ -612,10 +606,11 @@ """ .*GET.*security.ubuntu.com.* """ - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - ua_apt_https_proxy: http://wronguser:wrongpassword@$behave_var{machine-ip proxy}:3128 + { + "ua_apt_https_proxy": "http://wronguser:wrongpassword@$behave_var{machine-ip proxy}:3128" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -745,11 +740,12 @@ And I run `pro config unset global_apt_https_proxy` with sudo And I run `pro refresh config` with sudo Then I verify that no files exist matching `/etc/apt/apt.conf.d/90ubuntu-advantage-aptproxy` - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - global_apt_http_proxy: "invalidurl" - global_https_proxy: "invalidurls" + { + "global_apt_http_proxy": "invalidurl", + "global_https_proxy": "invalidurls" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -970,10 +966,11 @@ """ When I run `pro config unset ua_apt_http_proxy` with sudo And I run `pro config unset ua_apt_https_proxy` with sudo - And I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - apt_http_proxy: http://$behave_var{machine-ip proxy}:3128 + { + "apt_http_proxy": "http://$behave_var{machine-ip proxy}:3128" + } """ And I verify that running `pro refresh config` `with sudo` exits `0` Then stdout matches regexp: @@ -994,11 +991,12 @@ And I run `pro config unset global_apt_https_proxy` with sudo And I run `pro config unset ua_apt_http_proxy` with sudo And I run `pro config unset ua_apt_https_proxy` with sudo - And I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - global_apt_http_proxy: http://$behave_var{machine-ip proxy}:3128 - ua_apt_http_proxy: http://$behave_var{machine-ip proxy}:3128 + { + "global_apt_http_proxy": "http://$behave_var{machine-ip proxy}:3128", + "ua_apt_http_proxy": "http://$behave_var{machine-ip proxy}:3128" + } """ And I verify that running `pro refresh config` `with sudo` exits `1` Then stderr matches regexp: @@ -1132,11 +1130,12 @@ And I run `pro config unset apt_https_proxy` with sudo And I run `pro refresh config` with sudo Then I verify that no files exist matching `/etc/apt/apt.conf.d/90ubuntu-advantage-aptproxy` - When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: + When I create the file `/var/lib/ubuntu-advantage/user-config.json` with the following: """ - ua_config: - apt_http_proxy: http://$behave_var{machine-ip proxy}:3128 - apt_https_proxy: http://$behave_var{machine-ip proxy}:3128 + { + "apt_http_proxy": "http://$behave_var{machine-ip proxy}:3128" + "apt_https_proxy": "http://$behave_var{machine-ip proxy}:3128" + } """ When I run `pro refresh config` with sudo Then stdout matches regexp: diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/realtime_kernel.feature ubuntu-advantage-tools-27.14.4~16.04/features/realtime_kernel.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/realtime_kernel.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/realtime_kernel.feature 2023-04-05 15:14:00.000000000 +0000 @@ -5,7 +5,7 @@ @uses.config.machine_type.lxd.container Scenario Outline: Enable Real-time kernel service in a container Given a `` machine with ubuntu-advantage-tools installed - When I attach `contract_token` with sudo + When I attach `contract_token` with sudo and options `--no-auto-enable` Then I verify that running `pro enable realtime-kernel` `as non-root` exits `1` And I will see the following on stderr: """ @@ -25,7 +25,7 @@ @uses.config.machine_type.lxd.vm Scenario Outline: Enable Real-time kernel service on unsupported release Given a `` machine with ubuntu-advantage-tools installed - When I attach `contract_token` with sudo + When I attach `contract_token` with sudo and options `--no-auto-enable` Then I verify that running `pro enable realtime-kernel` `as non-root` exits `1` And I will see the following on stderr: """ @@ -47,7 +47,7 @@ @uses.config.machine_type.lxd.vm Scenario Outline: Enable Real-time kernel service Given a `` machine with ubuntu-advantage-tools installed - When I attach `contract_token` with sudo + When I attach `contract_token` with sudo and options `--no-auto-enable` Then I verify that running `pro enable realtime-kernel` `as non-root` exits `1` And I will see the following on stderr: """ @@ -92,8 +92,22 @@ When I run `pro disable realtime-kernel` `with sudo` and stdin `y` Then stdout matches regexp: """ - This will disable Ubuntu Pro updates to the Real-time kernel on this machine. - The Real-time kernel will remain installed. + This will remove the boot order preference for the Real-time kernel and + disable updates to the Real-time kernel. + + This will NOT fully remove the kernel from your system. + + After this operation is complete you must: + - Ensure a different kernel is installed and configured to boot + - Reboot into that kernel + - Fully remove the realtime kernel packages from your system + - This might look something like `apt remove linux\*realtime`, + but you must ensure this is correct before running it. + """ + When I run `apt-cache policy ubuntu-realtime` as non-root + Then stdout contains substring + """ + Installed: (none) """ Examples: ubuntu release @@ -104,7 +118,7 @@ @uses.config.machine_type.lxd.vm Scenario Outline: Enable Real-time kernel service access-only Given a `` machine with ubuntu-advantage-tools installed - When I attach `contract_token` with sudo + When I attach `contract_token` with sudo and options `--no-auto-enable` When I run `pro enable realtime-kernel --beta --access-only` with sudo Then stdout matches regexp: """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/attach.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/attach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/attach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/attach.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,11 +1,9 @@ -import logging -import time - from behave import when from features.steps.contract import change_contract_endpoint_to_staging from features.steps.shell import ( then_i_verify_that_running_cmd_with_spec_exits_with_codes, + when_i_retry_run_command, when_i_run_command, ) @@ -32,22 +30,10 @@ ): change_contract_endpoint_to_staging(context, user_spec) cmd = "pro attach {} {}".format(token, options).strip() - when_i_run_command(context, cmd, user_spec, verify_return=False) - if verify_return: - retries = [5, 5, 10] # Sleep times to wait between retries - while context.process.returncode != 0: - try: - time.sleep(retries.pop(0)) - except IndexError: # no more timeouts - logging.warning("Exhausted retries waiting for exit code: 0") - break - logging.info( - "--- Retrying on exit {exit_code}: {cmd}".format( - exit_code=context.process.returncode, cmd=cmd - ) - ) - when_i_run_command(context, cmd, user_spec, verify_return=False) + when_i_retry_run_command(context, cmd, user_spec, ERROR_CODE) + else: + when_i_run_command(context, cmd, user_spec, verify_return=False) @when("I attempt to attach `{token_type}` {user_spec}") diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/contract.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/contract.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/contract.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/contract.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,8 +1,16 @@ +import json + +import yaml from behave import then, when from hamcrest import assert_that, equal_to, not_ +from features.steps.files import ( + change_config_key_to_use_value, + when_i_create_file_with_content, +) from features.steps.shell import when_i_run_command from features.util import SUT, process_template_vars +from uaclient import util from uaclient.defaults import ( DEFAULT_CONFIG_FILE, DEFAULT_PRIVATE_MACHINE_TOKEN_PATH, @@ -108,3 +116,18 @@ contract_field=key.split(".")[-1], new_value=saved_value, ) + + +@when("I set the machine token overlay to the following yaml") +def when_i_set_the_machine_token_overlay(context): + json_text = json.dumps( + yaml.safe_load(context.text), cls=util.DatetimeAwareJSONEncoder + ) + when_i_create_file_with_content( + context, "/tmp/machine-token-overlay.json", text=json_text + ) + change_config_key_to_use_value( + context, + "features", + "{ machine_token_overlay: /tmp/machine-token-overlay.json}", + ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/files.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/files.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/files.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/files.py 2023-04-05 15:14:00.000000000 +0000 @@ -121,13 +121,14 @@ ) -@when("I change config key `{key}` to use value `{value}`") -def change_contract_key_to_use_value(context, key, value): - value = process_template_vars(context, value) +@when("I change config key `{key}` to use value `{yaml_value}`") +def change_config_key_to_use_value(context, key, yaml_value): + yaml_value = process_template_vars(context, yaml_value) content = _get_file_contents(context, DEFAULT_CONFIG_FILE) cfg = yaml.safe_load(content) - cfg[key] = value + val = yaml.safe_load(yaml_value) + cfg[key] = val new_content = yaml.dump(cfg) when_i_create_file_with_content( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/misc.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/misc.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/misc.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/misc.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,4 +1,5 @@ import json +import re import time from behave import then, when @@ -84,3 +85,11 @@ user_spec="with sudo", ) assert_that(context.process.stdout.strip(), equal_to("empty")) + + +@when("I regexify `{var_name}` stored var") +def regixify_stored_var(context, var_name): + val = context.stored_vars.get(var_name) + + if val: + context.stored_vars[var_name] = re.escape(val) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/packages.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/packages.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/packages.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/packages.py 2023-04-05 15:14:00.000000000 +0000 @@ -134,3 +134,21 @@ when_i_run_command( context, "apt-get install -y pro-dummy-thirdparty", "with sudo" ) + + +@when("I store candidate version of package `{package}`") +def store_candidate_version(context, package): + when_i_run_command( + context, + "apt-cache policy {}".format(package), + "as non-root", + ) + + candidate_version_match = re.search( + "Candidate:(?P.*)", context.process.stdout + ) + + if candidate_version_match: + context.stored_vars["candidate"] = candidate_version_match.group( + 1 + ).strip() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/shell.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/shell.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/shell.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/shell.py 2023-04-05 15:14:00.000000000 +0000 @@ -13,20 +13,21 @@ def when_i_retry_run_command(context, command, user_spec, exit_codes): when_i_run_command(context, command, user_spec, verify_return=False) retries = [5, 5, 10] # Sleep times to wait between retries - while str(context.process.returncode) in exit_codes.split(","): - try: - time.sleep(retries.pop(0)) - except IndexError: # no more timeouts - logging.warning( - "Exhausted retries waiting for exit codes: %s", exit_codes - ) - break + while len(retries) > 0 and str( + context.process.returncode + ) in exit_codes.split(","): + time.sleep(retries.pop(0)) logging.info( "--- Retrying on exit {exit_code}: {command}".format( exit_code=context.process.returncode, command=command ) ) when_i_run_command(context, command, user_spec, verify_return=False) + logging.warning( + "Exhausted retries waiting for exit codes: %s. Final exit code: %d", + exit_codes, + context.process.returncode, + ) assert_that(context.process.returncode, equal_to(0)) @@ -43,10 +44,6 @@ ): command = process_template_vars(context, command) - if "" in command and "proxy" in context.machines: - command = command.replace( - "", context.machines["proxy"].instance.ip - ) prefix = get_command_prefix_for_user_spec(user_spec) full_cmd = prefix + shlex.split(command) @@ -126,9 +123,9 @@ def get_command_prefix_for_user_spec(user_spec): prefix = [] - if user_spec == "with sudo": + if user_spec.lstrip() == "with sudo": prefix = ["sudo"] - elif user_spec != "as non-root": + elif user_spec.lstrip() != "as non-root": raise Exception( "The two acceptable values for user_spec are: 'with sudo'," " 'as non-root'" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/ubuntu_advantage_tools.py ubuntu-advantage-tools-27.14.4~16.04/features/steps/ubuntu_advantage_tools.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/steps/ubuntu_advantage_tools.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/steps/ubuntu_advantage_tools.py 2023-04-05 15:14:00.000000000 +0000 @@ -2,7 +2,7 @@ import os import re -from behave import when +from behave import then, when from features.steps.files import when_i_create_file_with_content from features.steps.packages import when_i_apt_install @@ -269,3 +269,22 @@ when_i_run_command( context, "apt-get install ubuntu-advantage-pro", "with sudo" ) + + +APT_POLICY_IS = "the apt-cache policy of ubuntu-advantage-tools is" + + +@then(APT_POLICY_IS) +def then_i_apt_cache_policy_is(context): + pass + + +@when("I check the apt-cache policy of ubuntu-advantage-tools") +def when_i_check_apt_cache_policy(context): + when_i_run_command(context, "apt-get update", "with sudo") + when_i_run_command( + context, "apt-cache policy ubuntu-advantage-tools", "with sudo" + ) + for step in context.scenario.steps: + if step.name == APT_POLICY_IS: + step.text = context.process.stdout diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/ubuntu_pro.feature ubuntu-advantage-tools-27.14.4~16.04/features/ubuntu_pro.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/ubuntu_pro.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/ubuntu_pro.feature 2023-04-05 15:14:00.000000000 +0000 @@ -11,6 +11,7 @@ dns_v4_first on\nacl all src 0.0.0.0\/0\nhttp_access allow all """ And I run `systemctl restart squid.service` `with sudo` on the `proxy` machine + # This also tests that legacy `ua_config` settings still work When I create the file `/etc/ubuntu-advantage/uaclient.conf` with the following: """ contract_url: 'https://contracts.canonical.com' @@ -625,14 +626,18 @@ Scenario Outline: Auto-attach no-op when cloud-init has ubuntu_advantage on userdata Given a `` machine with ubuntu-advantage-tools installed adding this cloud-init user_data: # This user_data should not do anything, just guarantee that the ua-auto-attach service - # # does nothing + # does nothing """ ubuntu_advantage: + features: + disable_auto_attach: true """ When I run `cloud-init query userdata` with sudo Then stdout matches regexp: """ ubuntu_advantage: + features: + disable_auto_attach: true """ # On GCP, this service will auto-attach the machine automatically after we override # the uaclient.conf file. To guarantee that we are not auto-attaching on reboot @@ -661,6 +666,11 @@ """ Skipping auto-attach and deferring to cloud-init to setup and configure auto-attach """ + When I run `cloud-init status` with sudo + Then stdout matches regexp: + """ + status: done + """ Examples: ubuntu release | release | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/ubuntu_pro_fips.feature ubuntu-advantage-tools-27.14.4~16.04/features/ubuntu_pro_fips.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/ubuntu_pro_fips.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/ubuntu_pro_fips.feature 2023-04-05 15:14:00.000000000 +0000 @@ -40,6 +40,7 @@ """ 1 """ + When I run `systemctl daemon-reload` with sudo When I run `systemctl start ua-auto-attach.service` with sudo And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3` Then stdout matches regexp: @@ -85,6 +86,9 @@ Then stdout matches regexp: """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-security/main amd64 Packages + """ + Then stdout matches regexp: + """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-updates/main amd64 Packages """ And stdout matches regexp: @@ -256,6 +260,7 @@ """ 1 """ + When I run `systemctl daemon-reload` with sudo When I run `systemctl start ua-auto-attach.service` with sudo And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3` Then stdout matches regexp: @@ -301,6 +306,9 @@ Then stdout matches regexp: """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-security/main amd64 Packages + """ + Then stdout matches regexp: + """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-updates/main amd64 Packages """ And stdout matches regexp: @@ -525,6 +533,7 @@ """ 1 """ + When I run `systemctl daemon-reload` with sudo When I run `systemctl start ua-auto-attach.service` with sudo And I verify that running `systemctl status ua-auto-attach.service` `as non-root` exits `0,3` Then stdout matches regexp: @@ -570,6 +579,9 @@ Then stdout matches regexp: """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-security/main amd64 Packages + """ + Then stdout matches regexp: + """ \s*500 https://esm.ubuntu.com/infra/ubuntu -infra-updates/main amd64 Packages """ And stdout matches regexp: diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/unattached_commands.feature ubuntu-advantage-tools-27.14.4~16.04/features/unattached_commands.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/unattached_commands.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/unattached_commands.feature 2023-04-05 15:14:00.000000000 +0000 @@ -349,3 +349,78 @@ | bionic | | focal | | jammy | + + @series.jammy + @series.kinetic + @series.lunar + @uses.config.machine_type.lxd.container + # Services fail, degraded systemctl, but no crashes. + Scenario Outline: services fail gracefully when yaml is broken/absent + Given a `` machine with ubuntu-advantage-tools installed + When I run `apt update` with sudo + And I run `rm -rf /usr/lib/python3/dist-packages/yaml` with sudo + And I verify that running `pro status` `with sudo` exits `1` + Then stderr matches regexp: + """ + Couldn't import the YAML module. + Make sure the 'python3-yaml' package is installed correctly + and \/usr\/lib\/python3\/dist-packages is in yout PYTHONPATH\. + """ + When I verify that running `python3 /usr/lib/ubuntu-advantage/esm_cache.py` `with sudo` exits `1` + Then stderr matches regexp: + """ + Couldn't import the YAML module. + Make sure the 'python3-yaml' package is installed correctly + and \/usr\/lib\/python3\/dist-packages is in yout PYTHONPATH\. + """ + When I verify that running `systemctl start apt-news.service` `with sudo` exits `1` + And I verify that running `systemctl start esm-cache.service` `with sudo` exits `1` + And I run `systemctl --failed` with sudo + Then stdout matches regexp: + """ + apt-news.service + """ + And stdout matches regexp: + """ + esm-cache.service + """ + When I run `apt install python3-pip -y` with sudo + And I run `pip3 install pyyaml==3.10 ` with sudo + And I run `ls /usr/local/lib//dist-packages/` with sudo + Then stdout matches regexp: + """ + yaml + """ + And I verify that running `pro status` `with sudo` exits `1` + Then stderr matches regexp: + """ + Error while trying to parse a yaml file using 'yaml' from + """ + # Test the specific script which triggered LP #2007241 + When I verify that running `python3 /usr/lib/ubuntu-advantage/esm_cache.py` `with sudo` exits `1` + Then stderr matches regexp: + """ + Error while trying to parse a yaml file using 'yaml' from + """ + When I verify that running `systemctl start apt-news.service` `with sudo` exits `1` + And I verify that running `systemctl start esm-cache.service` `with sudo` exits `1` + And I run `systemctl --failed` with sudo + Then stdout matches regexp: + """ + apt-news.service + """ + And stdout matches regexp: + """ + esm-cache.service + """ + When I run `ls /var/crash` with sudo + Then I will see the following on stdout + """ + """ + + Examples: ubuntu release + | release | python_version | suffix | + | jammy | python3.10 | | + | kinetic | python3.10 | | + # Lunar has a BIG error message explaining why this is a clear user error... + | lunar | python3.11 | --break-system-packages | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/unattached_status.feature ubuntu-advantage-tools-27.14.4~16.04/features/unattached_status.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/unattached_status.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/unattached_status.feature 2023-04-05 15:14:00.000000000 +0000 @@ -57,7 +57,7 @@ esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates - livepatch +yes +Canonical Livepatch service + livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System @@ -75,7 +75,7 @@ esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates - livepatch +yes +Canonical Livepatch service + livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) realtime-kernel +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System @@ -99,7 +99,7 @@ esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates - livepatch +yes +Canonical Livepatch service + livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/util.py ubuntu-advantage-tools-27.14.4~16.04/features/util.py --- ubuntu-advantage-tools-27.13.6~16.04.1/features/util.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/util.py 2023-04-05 15:14:00.000000000 +0000 @@ -7,7 +7,7 @@ import subprocess import tempfile from enum import Enum -from typing import Iterable, List, Optional +from typing import Callable, Iterable, List, Optional import yaml @@ -139,6 +139,8 @@ break if exclude: continue + if os.path.isdir(fname): + continue with open(fname) as f: new_file_content += f.read() @@ -274,15 +276,20 @@ } -def _replace_and_log(s, old, new): - logging.debug('replacing "{}" with "{}"'.format(old, new)) +def _replace_and_log(s, old, new, logger_fn): + logger_fn('replacing "{}" with "{}"'.format(old, new)) return s.replace(old, new) -def process_template_vars(context, template: str) -> str: +def process_template_vars( + context, template: str, logger_fn: Optional[Callable] = None +) -> str: + if logger_fn is None: + logger_fn = logging.info + processed_template = template - for match in re.finditer(r"\$behave_var{(.*)}", template): + for match in re.finditer(r"\$behave_var{([\w\s\-\+]*)}", template): args = match.group(1).split() function_name = args[0] if function_name == "version": @@ -291,6 +298,7 @@ processed_template, match.group(0), context.config.check_version, + logger_fn, ) elif function_name == "machine-ip": if args[1] in context.machines: @@ -298,10 +306,14 @@ processed_template, match.group(0), context.machines[args[1]].instance.ip, + logger_fn, ) elif function_name == "cloud": processed_template = _replace_and_log( - processed_template, match.group(0), context.config.cloud + processed_template, + match.group(0), + context.config.cloud, + logger_fn, ) elif function_name == "today": dt = datetime.datetime.utcnow() @@ -310,13 +322,25 @@ dt = dt + datetime.timedelta(days=offset) dt_str = dt.strftime("%Y-%m-%dT00:00:00Z") processed_template = _replace_and_log( - processed_template, match.group(0), dt_str + processed_template, + match.group(0), + dt_str, + logger_fn, ) elif function_name == "contract_token_staging": processed_template = _replace_and_log( processed_template, match.group(0), context.config.contract_token_staging, + logger_fn, ) + elif function_name == "stored_var": + if context.stored_vars.get(args[1]): + processed_template = _replace_and_log( + processed_template, + match.group(0), + context.stored_vars.get(args[1]), + logger_fn, + ) return processed_template diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/features/_version.feature ubuntu-advantage-tools-27.14.4~16.04/features/_version.feature --- ubuntu-advantage-tools-27.13.6~16.04.1/features/_version.feature 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/features/_version.feature 2023-04-05 15:14:00.000000000 +0000 @@ -25,6 +25,14 @@ """ $behave_var{version} """ + # The following doesn't actually assert anything. It merely ensures that the output of + # apt-cache policy ubuntu-advantage-tools on the test machine is included in our test output. + # This is useful to manually verify the package is installed from the correct source e.g. -proposed. + When I check the apt-cache policy of ubuntu-advantage-tools + Then the apt-cache policy of ubuntu-advantage-tools is + """ + THIS GETS REPLACED AT RUNTIME VIA A HACK IN steps/ubuntu_advantage_tools.py + """ Examples: version | release | | xenial | @@ -50,6 +58,14 @@ """ $behave_var{version} """ + # The following doesn't actually assert anything. It merely ensures that the output of + # apt-cache policy ubuntu-advantage-tools on the test machine is included in our test output. + # This is useful to manually verify the package is installed from the correct source e.g. -proposed. + When I check the apt-cache policy of ubuntu-advantage-tools + Then the apt-cache policy of ubuntu-advantage-tools is + """ + THIS GETS REPLACED AT RUNTIME VIA A HACK IN steps/ubuntu_advantage_tools.py + """ Examples: version | release | | xenial | diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/.github/workflows/ci-base.yaml ubuntu-advantage-tools-27.14.4~16.04/.github/workflows/ci-base.yaml --- ubuntu-advantage-tools-27.13.6~16.04.1/.github/workflows/ci-base.yaml 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/.github/workflows/ci-base.yaml 2023-04-05 15:14:00.000000000 +0000 @@ -23,33 +23,21 @@ uses: actions/checkout@v3 - name: Formatting run: tox -e black -e isort + - name: Style + run: tox -e flake8 - name: Mypy run: tox -e mypy - name: Version Consistency run: python3 ./tools/check-versions-are-consistent.py unit-tests: - name: Matrix - strategy: - matrix: - testenv: - - {os: ubuntu-18.04, pyver: py35, deadsnake: python3.5} - - {os: ubuntu-18.04, pyver: py36} - - {os: ubuntu-20.04, pyver: py38} - - {os: ubuntu-22.04, pyver: py310} - runs-on: ${{ matrix.testenv.os }} + name: Unit Tests + runs-on: ubuntu-22.04 steps: - name: Install dependencies run: | sudo DEBIAN_FRONTEND=noninteractive apt-get -qy update sudo DEBIAN_FRONTEND=noninteractive apt-get -qy install tox - - name: Install older Python from deadsnakes PPA - if: matrix.testenv.deadsnake != '' - run: | - sudo add-apt-repository --yes ppa:deadsnakes/ppa - sudo DEBIAN_FRONTEND=noninteractive apt-get -qy install "${{ matrix.testenv.deadsnake }}" "${{ matrix.testenv.deadsnake }}-dev" - name: Git checkout uses: actions/checkout@v3 - - name: Flake8 - run: tox -e "${{ matrix.testenv.pyver }}-flake8" - name: Unit - run: tox -e "${{ matrix.testenv.pyver }}-test" + run: tox -e test diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/apt_news.py ubuntu-advantage-tools-27.14.4~16.04/lib/apt_news.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/apt_news.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/apt_news.py 2023-04-05 15:14:00.000000000 +0000 @@ -22,7 +22,7 @@ if __name__ == "__main__": - cfg = UAConfig(root_mode=True) + cfg = UAConfig() setup_logging( logging.INFO, logging.DEBUG, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/auto_attach.py ubuntu-advantage-tools-27.14.4~16.04/lib/auto_attach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/auto_attach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/auto_attach.py 2023-04-05 15:14:00.000000000 +0000 @@ -103,7 +103,7 @@ if __name__ == "__main__": - cfg = UAConfig(root_mode=True) + cfg = UAConfig() setup_logging( logging.INFO, logging.DEBUG, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/daemon.py ubuntu-advantage-tools-27.14.4~16.04/lib/daemon.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/daemon.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/daemon.py 2023-04-05 15:14:00.000000000 +0000 @@ -15,7 +15,7 @@ def main() -> int: - cfg = UAConfig(root_mode=True) + cfg = UAConfig() setup_logging( logging.INFO, logging.DEBUG, log_file=cfg.daemon_log_file, logger=LOG ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/esm_cache.py ubuntu-advantage-tools-27.14.4~16.04/lib/esm_cache.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/esm_cache.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/esm_cache.py 2023-04-05 15:14:00.000000000 +0000 @@ -18,7 +18,7 @@ if __name__ == "__main__": - cfg = UAConfig(root_mode=True) + cfg = UAConfig() setup_logging( logging.INFO, logging.DEBUG, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/migrate_user_config.py ubuntu-advantage-tools-27.14.4~16.04/lib/migrate_user_config.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/migrate_user_config.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/migrate_user_config.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,162 @@ +""" +This is called in postinst when upgrading from a version <27.14 +This removes the "ua_config" sub-document from uaclient.conf and moves it to +it's own json file in /var/lib/ubuntu-advantage/user-config.json. +The fields under "ua_config" are intended to be set/unset by the user via the +`pro config set` command, so we don't want them covered as a "conffile". +It writes back a uaclient.conf that will match the new default uaclient.conf +as long as no defaults have changed. +We print warning messages if errors occur with instructions to help the user +resolve them. +""" + +import json +import os +import sys + +from uaclient import defaults, messages +from uaclient.yaml import safe_dump, safe_load + +UACLIENT_CONF_BACKUP_PATH = ( + "/etc/ubuntu-advantage/uaclient.conf.preinst-backup" +) +UACLIENT_CONF_MIGRATED_TEMP_PATH = ( + "/etc/ubuntu-advantage/uaclient.conf.preinst-backup-migrated-temp" +) + +USER_CONFIG_DEFAULTS = { + "http_proxy": None, + "https_proxy": None, + "apt_http_proxy": None, + "apt_https_proxy": None, + "ua_apt_http_proxy": None, + "ua_apt_https_proxy": None, + "global_apt_http_proxy": None, + "global_apt_https_proxy": None, + "update_messaging_timer": 21600, + "metering_timer": 14400, + "apt_news": True, + "apt_news_url": "https://motd.ubuntu.com/aptnews.json", +} + + +def load_pre_upgrade_conf(): + # Step 1: Load pre-upgrade uaclient.conf backed up in preinst + try: + with open(UACLIENT_CONF_BACKUP_PATH, "r") as uaclient_conf_file: + old_uaclient_conf = safe_load(uaclient_conf_file) + except Exception: + print( + messages.USER_CONFIG_MIGRATION_WARNING_UACLIENT_CONF_LOAD, + file=sys.stderr, + ) + return None + return old_uaclient_conf + + +def create_new_user_config_file(old_uaclient_conf): + # Step 2: Create new user_config.json + old_user_config = old_uaclient_conf.get("ua_config", {}) + if not isinstance(old_user_config, dict): + # invalid and could not have been working, just ignore by treating + # as empty + old_user_config = {} + + new_user_config = {} + # only keep a setting if it was changed + for field in USER_CONFIG_DEFAULTS.keys(): + old_val = old_user_config.get(field) + if old_val is not None and old_val != USER_CONFIG_DEFAULTS.get(field): + new_user_config[field] = old_val + + try: + with open( + defaults.DEFAULT_USER_CONFIG_JSON_FILE, "w" + ) as user_config_file: + json.dump(new_user_config, user_config_file) + except Exception: + if len(new_user_config) > 0: + print( + messages.USER_CONFIG_MIGRATION_WARNING_NEW_USER_CONFIG_WRITE, + file=sys.stderr, + ) + for field in sorted(new_user_config.keys()): + print( + " pro config set {}={}".format( + field, new_user_config[field] + ), + file=sys.stderr, + ) + + +def create_new_uaclient_conffile(old_uaclient_conf): + # Step 3: Create new uaclient.conf + # The goal here is to end up with a minimal diff compared to the default + # uaclient.conf. + # If nothing has changed in any of the uaclient.conf fields, then the + # result should exactly match the new default uaclient.conf. + new_uaclient_conf = { + "contract_url": old_uaclient_conf.get( + "contract_url", defaults.BASE_CONTRACT_URL + ), + "log_level": old_uaclient_conf.get("log_level", "debug"), + } + + # these are only present if they were added by the user + for field in ("features", "settings_overrides"): + if field in old_uaclient_conf: + new_uaclient_conf[field] = old_uaclient_conf.get(field) + + # the rest only if they're different from defaults + for field in ( + "data_dir", + "log_file", + "security_url", + "timer_log_file", + "daemon_log_file", + ): + old_val = old_uaclient_conf.get(field) + if old_val is not None and old_val != defaults.CONFIG_DEFAULTS.get( + field + ): + new_uaclient_conf[field] = old_val + + try: + print(messages.USER_CONFIG_MIGRATION_MIGRATING, file=sys.stderr) + new_uaclient_conf_str = safe_dump( + new_uaclient_conf, + default_flow_style=False, + ) + # don't open until after successful yaml serialization + # this way, if there is an error in yaml serialization we won't + # accidentally truncate the file + with open(UACLIENT_CONF_MIGRATED_TEMP_PATH, "w") as uaclient_conf_file: + uaclient_conf_file.write(new_uaclient_conf_str) + + # write to a temp file and rename atomically to avoid partial writes + os.rename(UACLIENT_CONF_MIGRATED_TEMP_PATH, UACLIENT_CONF_BACKUP_PATH) + except Exception: + print( + messages.USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE, + file=sys.stderr, + ) + for field in sorted(new_uaclient_conf.keys()): + print( + " {}: {}".format( + field, repr(new_uaclient_conf[field]) + ), + file=sys.stderr, + ) + + +def main(): + old_uaclient_conf = load_pre_upgrade_conf() + if old_uaclient_conf is None: + # something went wrong, we can't continue migration + return + create_new_user_config_file(old_uaclient_conf) + create_new_uaclient_conffile(old_uaclient_conf) + + +if __name__ == "__main__": + main() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/reboot_cmds.py ubuntu-advantage-tools-27.14.4~16.04/lib/reboot_cmds.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/reboot_cmds.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/reboot_cmds.py 2023-04-05 15:14:00.000000000 +0000 @@ -21,6 +21,8 @@ from uaclient import config, contract, exceptions, lock, messages from uaclient.cli import setup_logging from uaclient.entitlements.fips import FIPSEntitlement +from uaclient.files import notices +from uaclient.files.notices import Notice from uaclient.system import subp # Retry sleep backoff algorithm if lock is held. @@ -29,7 +31,7 @@ MAX_RETRIES_ON_LOCK_HELD = 7 -def run_command(cmd, cfg): +def run_command(cmd, cfg: config.UAConfig): try: out, _ = subp(cmd.split(), capture=True) logging.debug("Successfully executed cmd: {}".format(cmd)) @@ -49,7 +51,7 @@ sys.exit(1) -def fix_pro_pkg_holds(cfg): +def fix_pro_pkg_holds(cfg: config.UAConfig): status_cache = cfg.read_cache("status-cache") if not status_cache: return @@ -82,13 +84,9 @@ ) ) sys.exit(1) - cfg.notice_file.remove( - "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg - ) - cfg.notice_file.remove("", messages.FIPS_REBOOT_REQUIRED_MSG) -def refresh_contract(cfg): +def refresh_contract(cfg: config.UAConfig): try: contract.request_updated_contract(cfg) except exceptions.UrlError as exc: @@ -97,13 +95,12 @@ sys.exit(1) -def process_remaining_deltas(cfg): +def process_remaining_deltas(cfg: config.UAConfig): cmd = "/usr/bin/python3 /usr/lib/ubuntu-advantage/upgrade_lts_contract.py" run_command(cmd=cmd, cfg=cfg) - cfg.notice_file.remove("", messages.LIVEPATCH_LTS_REBOOT_REQUIRED) -def process_reboot_operations(cfg): +def process_reboot_operations(cfg: config.UAConfig): reboot_cmd_marker_file = cfg.data_path("marker-reboot-cmds") @@ -124,16 +121,18 @@ process_remaining_deltas(cfg) cfg.delete_cache_key("marker-reboot-cmds") - cfg.notice_file.remove("", messages.REBOOT_SCRIPT_FAILED) + notices.remove(Notice.REBOOT_SCRIPT_FAILED) logging.debug("Successfully ran all commands on reboot.") except Exception as e: msg = "Failed running commands on reboot." msg += str(e) logging.error(msg) - cfg.notice_file.add("", messages.REBOOT_SCRIPT_FAILED) + notices.add( + Notice.REBOOT_SCRIPT_FAILED, + ) -def main(cfg): +def main(cfg: config.UAConfig): """Retry running process_reboot_operations on LockHeldError :raises: LockHeldError when lock still held by auto-attach after retries. @@ -153,6 +152,6 @@ if __name__ == "__main__": - cfg = config.UAConfig(root_mode=True) + cfg = config.UAConfig() setup_logging(logging.INFO, logging.DEBUG, log_file=cfg.log_file) main(cfg=cfg) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/timer.py ubuntu-advantage-tools-27.14.4~16.04/lib/timer.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/timer.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/timer.py 2023-04-06 13:50:05.000000000 +0000 @@ -15,11 +15,13 @@ timer_jobs_state_file, ) from uaclient.jobs.metering import metering_enabled_resources -from uaclient.jobs.update_messaging import update_apt_and_motd_messages +from uaclient.jobs.update_contract_info import update_contract_info +from uaclient.jobs.update_messaging import update_motd_messages LOG = logging.getLogger(__name__) UPDATE_MESSAGING_INTERVAL = 21600 # 6 hours METERING_INTERVAL = 14400 # 4 hours +UPDATE_CONTRACT_INFO_INTERVAL = 86400 # 24 hours class TimedJob: @@ -108,9 +110,14 @@ metering_job = MeteringTimedJob(metering_enabled_resources, METERING_INTERVAL) update_message_job = TimedJob( "update_messaging", - update_apt_and_motd_messages, + update_motd_messages, UPDATE_MESSAGING_INTERVAL, ) +update_contract_info_job = TimedJob( + "update_contract_info", + update_contract_info, + UPDATE_CONTRACT_INFO_INTERVAL, +) def run_job( @@ -158,7 +165,7 @@ # We do this for the first run of the timer job, where the file # doesn't exist jobs_status_obj = AllTimerJobsState( - metering=None, update_messaging=None + metering=None, update_messaging=None, update_contract_info=None ) jobs_status_obj.metering = run_job( @@ -171,7 +178,7 @@ if __name__ == "__main__": - cfg = UAConfig(root_mode=True) + cfg = UAConfig() current_time = datetime.now(timezone.utc) # The ua-timer logger should log everything to its file diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/lib/upgrade_lts_contract.py ubuntu-advantage-tools-27.14.4~16.04/lib/upgrade_lts_contract.py --- ubuntu-advantage-tools-27.13.6~16.04.1/lib/upgrade_lts_contract.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/lib/upgrade_lts_contract.py 2023-04-05 15:14:00.000000000 +0000 @@ -55,7 +55,7 @@ def process_contract_delta_after_apt_lock() -> None: logging.debug("Check whether to upgrade-lts-contract") - cfg = UAConfig(root_mode=True) + cfg = UAConfig() if not cfg.is_attached: logging.debug("Skipping upgrade-lts-contract. Machine is unattached") return @@ -85,11 +85,10 @@ sys.exit(1) past_entitlements = UAConfig( - series=past_release, root_mode=True + series=past_release, ).machine_token_file.entitlements new_entitlements = UAConfig( series=current_release, - root_mode=True, ).machine_token_file.entitlements retry_count = 0 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/README.md ubuntu-advantage-tools-27.14.4~16.04/README.md --- ubuntu-advantage-tools-27.13.6~16.04.1/README.md 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/README.md 2023-04-05 15:14:00.000000000 +0000 @@ -7,13 +7,16 @@ ###### Clean and Consistent CLI for your Ubuntu Pro Systems -![Latest Upstream Version](https://img.shields.io/github/v/tag/canonical/ubuntu-advantage-client.svg?label=Latest%20Upstream%20Version&logo=github&logoColor=white&color=33ce57) -![CI](https://github.com/canonical/ubuntu-advantage-client/actions/workflows/ci-base.yaml/badge.svg?branch=main) +[![Latest Upstream Version](https://img.shields.io/github/v/tag/canonical/ubuntu-advantage-client.svg?label=Latest%20Upstream%20Version&logo=github&logoColor=white&color=33ce57)](https://github.com/canonical/ubuntu-pro-client/tags) +[![CI](https://github.com/canonical/ubuntu-advantage-client/actions/workflows/ci-base.yaml/badge.svg?branch=main)](https://github.com/canonical/ubuntu-pro-client/actions) +[![Documentation Status](https://readthedocs.com/projects/canonical-ubuntu-pro-client/badge/?version=latest)](https://canonical-ubuntu-pro-client.readthedocs-hosted.com/en/latest/?badge=latest)
-![Released Xenial Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/xenial?label=Xenial&logo=ubuntu&logoColor=white) -![Released Bionic Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/bionic?label=Bionic&logo=ubuntu&logoColor=white) -![Released Focal Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/focal?label=Focal&logo=ubuntu&logoColor=white) -![Released Jammy Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/jammy?label=Jammy&logo=ubuntu&logoColor=white) +[![Released Xenial Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/xenial?label=Xenial&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/xenial/+source/ubuntu-advantage-tools) +[![Released Bionic Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/bionic?label=Bionic&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/bionic/+source/ubuntu-advantage-tools) +[![Released Focal Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/focal?label=Focal&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/focal/+source/ubuntu-advantage-tools) +[![Released Jammy Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/jammy?label=Jammy&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/jammy/+source/ubuntu-advantage-tools) +[![Released Kinetic Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/kinetic?label=Kinetic&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/kinetic/+source/ubuntu-advantage-tools) +[![Released Lunar Version](https://img.shields.io/ubuntu/v/ubuntu-advantage-tools/lunar?label=Lunar&logo=ubuntu&logoColor=white)](https://launchpad.net/ubuntu/lunar/+source/ubuntu-advantage-tools) The Ubuntu Pro Client (`pro`) is the official tool to enable Canonical offerings on your system. @@ -33,50 +36,7 @@ `pro` is already installed on every Ubuntu system. Try it out by running `pro help`! -## Documentation - -### Tutorials - -* [Getting started with Ubuntu Pro Client](./docs/tutorials/basic_commands.md) -* [Create a FIPS compliant Ubuntu Docker image](./docs/tutorials/create_a_fips_docker_image.md) -* [Fix vulnerabilities by CVE or USN using `pro fix`](./docs/tutorials/fix_scenarios.md) -* [Create a Custom Ubuntu Pro Cloud Image with FIPS Updates](./docs/tutorials/create_a_fips_updates_pro_cloud_image.md) - -### How To Guides - -* [How to get an Ubuntu Pro token and attach to a subscription](./docs/howtoguides/get_token_and_attach.md) -* [How to Configure Proxies](./docs/howtoguides/configure_proxies.md) -* [How to Enable Ubuntu Pro Services in a Dockerfile](./docs/howtoguides/enable_in_dockerfile.md) -* [How to Create a custom Golden Image based on Public Cloud Ubuntu Pro images](./docs/howtoguides/create_pro_golden_image.md) -* [How to Manually update MOTD and APT messages](./docs/howtoguides/update_motd_messages.md) -* [How to enable CIS](./docs/howtoguides/enable_cis.md) -* [How to enable CC EAL](./docs/howtoguides/enable_cc.md) -* [How to enable ESM Infra](./docs/howtoguides/enable_esm_infra.md) -* [How to enable FIPS](./docs/howtoguides/enable_fips.md) -* [How to enable Livepatch](./docs/howtoguides/enable_livepatch.md) -* [How to configure a timer job](./docs/howtoguides/configuring_timer_jobs.md) -* [How to attach with a configuration file](./docs/howtoguides/how_to_attach_with_config_file.md) -* [How to collect Ubuntu Pro Client logs](./docs/howtoguides/how_to_collect_logs.md) -* [How to simulate attach operation](./docs/howtoguides/how_to_simulate_attach.md) -* [How to run `pro fix` in dry-run mode](./docs/howtoguides/how_to_run_fix_in_dry_run_mode.md) - -### Reference - -* [Ubuntu Release and Architecture Support Matrix](./docs/references/support_matrix.md) -* [Network Requirements](./docs/references/network_requirements.md) -* [API Reference Guide](./docs/references/api.md) -* [PPAs with different versions of `pro`](./docs/references/ppas.md) - -### Explanation - -* [What is the daemon for? (And how to disable it)](./docs/explanations/what_is_the_daemon.md) -* [What are Public Cloud Ubuntu Pro instances?](./docs/explanations/what_are_ubuntu_pro_cloud_instances.md) -* [What is the ubuntu-advantage-pro package?](./docs/explanations/what_is_the_ubuntu_advantage_pro_package.md) -* [What are the timer jobs?](./docs/explanations/what_are_the_timer_jobs.md) -* [What are the Ubuntu Pro related MOTD messages?](./docs/explanations/motd_messages.md) -* [What are the Ubuntu Pro related APT messages?](./docs/explanations/apt_messages.md) -* [How to interpret the security-status command](./docs/explanations/how_to_interpret_the_security_status_command.md) -* [Why Trusty (14.04) is no longer supported](./docs/explanations/why_trusty_is_no_longer_supported.md) +Or [check out the docs](https://canonical-ubuntu-pro-client.readthedocs-hosted.com/en/latest/). ## Project and community diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/sru/release-27.14/test-migrate-user-config.sh ubuntu-advantage-tools-27.14.4~16.04/sru/release-27.14/test-migrate-user-config.sh --- ubuntu-advantage-tools-27.13.6~16.04.1/sru/release-27.14/test-migrate-user-config.sh 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/sru/release-27.14/test-migrate-user-config.sh 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,288 @@ +#!/bin/bash +set -e + +# TODO remove TESTING sections for -proposed verification +# TESTING: +local_deb=$1 + +function install_old_version { + name=$1 + series=$2 + old_version=$3 + PACKAGE=ubuntu-advantage-tools + ARCH=amd64 + echo -e "\n-------------------------------------------" + echo "** installing $old_version" + echo "-------------------------------------------" + package_url=$(curl -s https://launchpad.net/ubuntu/$series/$ARCH/$PACKAGE/$old_version | grep -o "http://launchpadlibrarian.net/.*/${PACKAGE}_${old_version}_${ARCH}.deb") + lxc exec $name -- wget -nv -O ua.deb $package_url + lxc exec $name -- dpkg -i ./ua.deb + lxc exec $name -- apt-cache policy ubuntu-advantage-tools + echo "-------------------------------------------" +} + +function upgrade_to_proposed { + name=$1 + verify=$2 + + # TESTING: + echo -e "\n-------------------------------------------" + echo "** upgrading to 27.14 from local - VERIFY $verify" + echo "-------------------------------------------" + lxc file push $local_deb $name/tmp/uanew.deb + lxc exec $name -- dpkg -i /tmp/uanew.deb + lxc exec $name -- apt-cache policy ubuntu-advantage-tools + echo "-------------------------------------------" + return + # END TESTING + + echo -e "\n-------------------------------------------" + echo "** upgrading to 27.14 from proposed - VERIFY $verify" + echo "-------------------------------------------" + lxc exec $name -- sh -c "echo \"deb http://archive.ubuntu.com/ubuntu $series-proposed main\" | tee /etc/apt/sources.list.d/proposed.list" + lxc exec $name -- apt-get update > /dev/null + lxc exec $name -- apt-get install ubuntu-advantage-tools + lxc exec $name -- apt-cache policy ubuntu-advantage-tools + echo "-------------------------------------------" +} + + +function test_normal_upgrade { + series=$1 + old_version=$2 + echo -e "\n\n###########################################" + echo "## $series: $old_version -> 27.14: no changes to uaclient.conf" + echo "###########################################" + name=$(echo $series-$old_version | tr .~ -) + + echo -e "\n-------------------------------------------" + echo "** launching container" + echo "-------------------------------------------" + lxc launch -q ubuntu-daily:$series $name + sleep 5 + lxc exec $name -- apt-get update > /dev/null + lxc exec $name -- apt-get install debsums -y > /dev/null + echo "-------------------------------------------" + + install_old_version $name $series $old_version + + upgrade_to_proposed $name "NO CONFFILE PROMPT" + + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show no uaclient.conf.dpkg-bak" + echo "-------------------------------------------" + lxc exec $name -- sh -c "ls -al /etc/ubuntu-advantage/uaclient.conf.dpkg-bak || true" + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show user config" + echo "-------------------------------------------" + lxc exec $name -- sh -c "ls -al /var/lib/ubuntu-advantage/user-config.json || true" + lxc exec $name -- pro config show + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** debsums - VERIFY ALL OK" + echo "-------------------------------------------" + lxc exec $name -- debsums -e ubuntu-advantage-tools + echo "-------------------------------------------" + + lxc delete --force $name + echo "###########################################" +} + +function test_apt_news_false_upgrade { + series=$1 + old_version=$2 + echo -e "\n\n###########################################" + echo "## $series: $old_version -> 27.14: ua_config changes preserved in new user-config" + echo "###########################################" + name=$(echo $series-$old_version | tr .~ -) + + echo -e "\n-------------------------------------------" + echo "** launching container" + echo "-------------------------------------------" + lxc launch -q ubuntu-daily:$series $name + sleep 5 + lxc exec $name -- apt-get update > /dev/null + lxc exec $name -- apt-get install debsums -y > /dev/null + echo "-------------------------------------------" + + install_old_version $name $series $old_version + + echo -e "\n-------------------------------------------" + echo "** pro config set apt_news=false" + echo "-------------------------------------------" + lxc exec $name -- pro config set apt_news=false + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + + upgrade_to_proposed $name "NO CONFFILE PROMPT" + + echo -e "\n-------------------------------------------" + echo "** Backup file is gone after successful migration" + echo "-------------------------------------------" + lxc exec $name -- ls -la /etc/ubuntu-advantage/ + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf.dpkg-bak" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf.dpkg-bak + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show user config" + echo "-------------------------------------------" + lxc exec $name -- cat /var/lib/ubuntu-advantage/user-config.json + echo + lxc exec $name -- pro config show + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** debsums - VERIFY ALL OK" + echo "-------------------------------------------" + lxc exec $name -- debsums -e ubuntu-advantage-tools + echo "-------------------------------------------" + + lxc delete --force $name + echo "###########################################" +} + +function test_uaclient_conf_changes_upgrade { + series=$1 + old_version=$2 + echo -e "\n\n###########################################" + echo "## $series: $old_version -> 27.14: preserve uaclient.conf changes" + echo "###########################################" + name=$(echo $series-$old_version | tr .~ -) + + echo -e "\n-------------------------------------------" + echo "** launching container" + echo "-------------------------------------------" + lxc launch -q ubuntu-daily:$series $name + sleep 5 + lxc exec $name -- apt-get update > /dev/null + echo "-------------------------------------------" + + install_old_version $name $series $old_version + + echo -e "\n-------------------------------------------" + echo "** make changes to uaclient.conf root" + echo "-------------------------------------------" + lxc exec $name -- sed -i "s/debug/warning/" /etc/ubuntu-advantage/uaclient.conf + lxc exec $name -- sh -c "echo 'features:' >> /etc/ubuntu-advantage/uaclient.conf" + lxc exec $name -- sh -c "echo ' allow_beta: on' >> /etc/ubuntu-advantage/uaclient.conf" + lxc exec $name -- sh -c "echo settings_overrides: {} >> /etc/ubuntu-advantage/uaclient.conf" + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show user config" + echo "-------------------------------------------" + lxc exec $name -- pro config show + echo "-------------------------------------------" + + upgrade_to_proposed $name "NO CONFFILE PROMPT" + + echo -e "\n-------------------------------------------" + echo "** Backup file is gone after successful migration" + echo "-------------------------------------------" + lxc exec $name -- ls -la /etc/ubuntu-advantage/ + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf.dpkg-bak" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf.dpkg-bak + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show user config" + echo "-------------------------------------------" + lxc exec $name -- cat /var/lib/ubuntu-advantage/user-config.json + echo + lxc exec $name -- pro config show + echo "-------------------------------------------" + + lxc delete --force $name + echo "###########################################" +} + +function test_migration_failure { + series=$1 + old_version=$2 + echo -e "\n\n###########################################" + echo "## $series: $old_version -> 27.14: migration failure" + echo "###########################################" + name=$(echo $series-$old_version | tr .~ -) + + echo -e "\n-------------------------------------------" + echo "** launching container" + echo "-------------------------------------------" + lxc launch -q ubuntu-daily:$series $name + sleep 5 + lxc exec $name -- apt-get update > /dev/null + echo "-------------------------------------------" + + install_old_version $name $series $old_version + + echo -e "\n-------------------------------------------" + echo "** mess up uaclient.conf to be invalid yaml" + echo "-------------------------------------------" + lxc exec $name -- sh -c "echo {{{ >> /etc/ubuntu-advantage/uaclient.conf" + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + + upgrade_to_proposed $name "WARNING MESSAGE PRESENT AND NO CONFFILE PROMPT" + + echo -e "\n-------------------------------------------" + echo "** Backup file is gone" + echo "-------------------------------------------" + lxc exec $name -- ls -la /etc/ubuntu-advantage/ + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show uaclient.conf - backup was restored" + echo "-------------------------------------------" + lxc exec $name -- cat /etc/ubuntu-advantage/uaclient.conf + echo "-------------------------------------------" + echo -e "\n-------------------------------------------" + echo "** Show user config" + echo "-------------------------------------------" + lxc exec $name -- sh -c "ls -al /var/lib/ubuntu-advantage/user-config.json || true" + echo "-------------------------------------------" + + lxc delete --force $name + echo "###########################################" +} + +# xenial +test_normal_upgrade xenial 27.11.3~16.04.1 +test_normal_upgrade xenial 27.13.1~16.04.1 +test_apt_news_false_upgrade xenial 27.11.3~16.04.1 +test_apt_news_false_upgrade xenial 27.13.1~16.04.1 +test_uaclient_conf_changes_upgrade xenial 27.11.3~16.04.1 +test_uaclient_conf_changes_upgrade xenial 27.13.1~16.04.1 +test_migration_failure xenial 27.11.3~16.04.1 +test_migration_failure xenial 27.13.1~16.04.1 + +# TODO: repeat for each release diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/systemd/apt-news.service ubuntu-advantage-tools-27.14.4~16.04/systemd/apt-news.service --- ubuntu-advantage-tools-27.13.6~16.04.1/systemd/apt-news.service 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/systemd/apt-news.service 2023-04-05 15:14:00.000000000 +0000 @@ -1,3 +1,13 @@ +# APT News is hosted at https://motd.ubuntu.com/aptnews.json and can include +# timely information related to apt updates available to your system. +# This service runs in the background during an `apt update` to download the +# latest news and set it to appear in the output of the next `apt upgrade`. +# The script won't do anything if you've run: `pro config set apt_news=false`. +# The script will limit network requests to at most once per 24 hours. +# You can also host your own aptnews.json and configure your system to use it +# with the command: +# `pro config set apt_news_url=https://yourhostname/path/to/aptnews.json` + [Unit] Description=Update APT News diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/tools/run-integration-tests.py ubuntu-advantage-tools-27.14.4~16.04/tools/run-integration-tests.py --- ubuntu-advantage-tools-27.13.6~16.04.1/tools/run-integration-tests.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/tools/run-integration-tests.py 2023-04-05 15:14:00.000000000 +0000 @@ -35,7 +35,7 @@ "gcppro": ["xenial", "bionic", "focal", "jammy"], "gcppro-fips": ["bionic", "focal"], "lxd": ["xenial", "bionic", "focal", "jammy", "kinetic", "lunar"], - "vm": ["xenial", "bionic", "focal", "jammy"], + "vm": ["xenial", "bionic", "focal", "jammy", "kinetic", "lunar"], "upgrade": ["xenial", "bionic", "focal", "jammy", "kinetic"], } diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/tools/test-in-lxd.sh ubuntu-advantage-tools-27.14.4~16.04/tools/test-in-lxd.sh --- ubuntu-advantage-tools-27.13.6~16.04.1/tools/test-in-lxd.sh 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/tools/test-in-lxd.sh 2023-04-05 15:14:00.000000000 +0000 @@ -1,19 +1,36 @@ #!/usr/bin/bash -series=$1 +# +# test-in-lxd.sh [series] +# +# Create an LXD container or vm with the current ubuntu-advantages-tool package +# built from ../ -set -x +set -eux -build_out=$(./tools/build.sh $series) -hash=$(echo $build_out | jq -r .state_hash) -deb=$(echo $build_out | jq -r .debs[] | grep tools) +VM=${VM:-0} +SHELL_BEFORE=${SHELL_BEFORE:-0} + +series=${1:-jammy} +build_out=$(./tools/build.sh "$series") +hash=$(echo "$build_out" | jq -r .state_hash) +deb=$(echo "$build_out" | jq -r '.debs[]' | grep tools) name=ua-$series-$hash -lxc delete $name --force -lxc launch ubuntu-daily:$series $name +flags= +if [ "$VM" -ne 0 ]; then + flags="--vm" +fi + +lxc delete "$name" --force || true +lxc launch "ubuntu-daily:${series}" "$name" $flags sleep 5 -lxc file push $deb $name/tmp/ua.deb +if [[ "$VM" -ne 0 ]]; then + echo "vms take a while before the agent is ready" + sleep 30 +fi +lxc file push "$deb" "${name}/tmp/ua.deb" -if [ -n "$SHELL_BEFORE" ]; then +if [[ "$SHELL_BEFORE" -ne 0 ]]; then set +x echo echo @@ -21,8 +38,8 @@ echo "After you exit the shell we'll upgrade pro and bring you right back." echo set -x - lxc exec $name bash + lxc exec "$name" bash fi -lxc exec $name -- dpkg -i /tmp/ua.deb -lxc exec $name bash +lxc exec "$name" -- dpkg -i /tmp/ua.deb +lxc shell "$name" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/tools/test-in-multipass.sh ubuntu-advantage-tools-27.14.4~16.04/tools/test-in-multipass.sh --- ubuntu-advantage-tools-27.13.6~16.04.1/tools/test-in-multipass.sh 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/tools/test-in-multipass.sh 2023-04-05 15:14:00.000000000 +0000 @@ -1,22 +1,28 @@ #!/usr/bin/bash -series=$1 +# +# test-in-multipass.sh [series] +# +# Create an Multipass instance with the current ubuntu-advantages-tool package +# built from ../ +set -eux -set -x +SHELL_BEFORE=${SHELL_BEFORE:-0} -build_out=$(./tools/build.sh $series) -hash=$(echo $build_out | jq -r .state_hash) -deb=$(echo $build_out | jq -r .debs[] | grep tools) +series=${1:-jammy} +build_out=$(./tools/build.sh "$series") +hash=$(echo "$build_out" | jq -r .state_hash) +deb=$(echo "$build_out" | jq -r '.debs[]' | grep tools) name=ua-$series-$hash -multipass delete $name --purge -multipass launch $series --name $name +multipass delete "$name" --purge || true +multipass launch "$series" --name "$name" sleep 30 # Snaps won't access /tmp -cp $deb ~/ua.deb -multipass transfer ~/ua.deb $name:/tmp/ua.deb +cp "$deb" ~/ua.deb +multipass transfer ~/ua.deb "${name}:/tmp/ua.deb" rm -f ~/ua.deb -if [ -n "$SHELL_BEFORE" ]; then +if [[ "$SHELL_BEFORE" -ne 0 ]]; then set +x echo echo @@ -24,8 +30,8 @@ echo "After you exit the shell we'll upgrade pro and bring you right back." echo set -x - multipass exec $name bash + multipass exec "$name" bash fi -multipass exec $name -- sudo dpkg -i /tmp/ua.deb -multipass shell $name +multipass exec "$name" -- sudo dpkg -i /tmp/ua.deb +multipass shell "$name" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/tox.ini ubuntu-advantage-tools-27.14.4~16.04/tox.ini --- ubuntu-advantage-tools-27.13.6~16.04.1/tox.ini 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/tox.ini 2023-04-05 15:14:00.000000000 +0000 @@ -75,6 +75,8 @@ behave-vm-18.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.bionic,series.all,series.lts" --tags="~upgrade" behave-vm-20.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.focal,series.all,series.lts" --tags="~upgrade" --tags="~docker" behave-vm-22.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.jammy,series.all,series.lts" --tags="~upgrade" + behave-vm-22.10: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.kinetic,series.all" --tags="~upgrade" + behave-vm-23.04: behave -v {posargs} --tags="uses.config.machine_type.lxd.vm" --tags="series.lunar,series.all" --tags="~upgrade" behave-upgrade-16.04: behave -v {posargs} --tags="upgrade" --tags="series.xenial,series.all" behave-upgrade-18.04: behave -v {posargs} --tags="upgrade" --tags="series.bionic,series.all" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/actions.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/actions.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/actions.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/actions.py 2023-04-05 15:14:00.000000000 +0000 @@ -8,8 +8,8 @@ config, contract, entitlements, - event_logger, exceptions, + livepatch, messages, ) from uaclient import status as ua_status @@ -21,11 +21,9 @@ DEFAULT_CONFIG_FILE, DEFAULT_LOG_PREFIX, ) -from uaclient.entitlements.livepatch import LIVEPATCH_CMD from uaclient.files.state_files import timer_jobs_state_file LOG = logging.getLogger("pro.actions") -event = event_logger.get_event_logger() UA_SERVICES = ( @@ -48,7 +46,7 @@ :raise ContractAPIError: On unexpected errors when talking to the contract server. """ - from uaclient.jobs.update_messaging import update_apt_and_motd_messages + from uaclient.jobs.update_messaging import update_motd_messages try: contract.request_updated_contract( @@ -57,19 +55,19 @@ except exceptions.UrlError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) - update_apt_and_motd_messages(cfg) + update_motd_messages(cfg) raise exc except exceptions.UserFacingError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) - update_apt_and_motd_messages(cfg) + update_motd_messages(cfg) raise exc current_iid = identity.get_instance_id() if current_iid: cfg.write_cache("instance-id", current_iid) - update_apt_and_motd_messages(cfg) + update_motd_messages(cfg) def auto_attach( @@ -182,7 +180,7 @@ "pro status --format json", "{}/ua-status.json".format(output_dir) ) _write_command_output_to_file( - "{} status".format(LIVEPATCH_CMD), + "{} status".format(livepatch.LIVEPATCH_CMD), "{}/livepatch-status.txt".format(output_dir), ) _write_command_output_to_file( @@ -224,7 +222,7 @@ logging.warning("Failed to load file: %s\n%s", f, str(e)) continue content = util.redact_sensitive_logs(content) - if os.getuid() == 0: + if util.we_are_currently_root(): # if root, overwrite the original with redacted content system.write_file(f, content) system.write_file( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/api.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/api.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/api.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/api.py 2023-04-05 15:14:00.000000000 +0000 @@ -28,6 +28,7 @@ "u.pro.security.status.reboot_required.v1", "u.pro.version.v1", "u.security.package_manifest.v1", + "u.unattended_upgrades.status.v1", ] diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/data_types.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/data_types.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/data_types.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/data_types.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,4 +1,4 @@ -from typing import Dict, List, Union # noqa: F401 +from typing import Any, Dict, List, Union # noqa: F401 from uaclient.data_types import DataObject, Field, StringDataValue, data_list from uaclient.util import get_pro_environment @@ -6,7 +6,7 @@ class AdditionalInfo: - meta = {} # type: Dict[str, str] + meta = {} # type: Dict[str, Any] warnings = [] # type: List[ErrorWarningObject] diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/exceptions.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/exceptions.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/exceptions.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/exceptions.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,6 +1,7 @@ from typing import List, Tuple from uaclient import messages +from uaclient.api.errors import APIError from uaclient.exceptions import ( AlreadyAttachedError, ConnectivityError, @@ -47,3 +48,9 @@ messages.AUTO_ATTACH_DISABLED_ERROR.msg, messages.AUTO_ATTACH_DISABLED_ERROR.name, ) + + +class UnattendedUpgradesError(APIError): + def __init__(self, msg): + self.msg = msg + self.msg_code = "unable-to-determine-unattended-upgrade-status" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/tests/test_api_u_pro_attach_auto_full_auto_attach_v1.py 2023-04-05 15:14:00.000000000 +0000 @@ -157,6 +157,8 @@ assert ret == expected_ret +@mock.patch("uaclient.files.notices.add") +@mock.patch("uaclient.files.notices.remove") class TestFullAutoAttachV1: @mock.patch( "uaclient.actions.enable_entitlement_by_name", @@ -168,9 +170,11 @@ _auto_attach, _get_cloud_instance, m_enable_ent_by_name, + _notice_remove, + _notice_add, FakeConfig, ): - cfg = FakeConfig(root_mode=True) + cfg = FakeConfig() def enable_ent_side_effect(cfg, name, assume_yes, allow_beta): if name != "wrong": @@ -199,9 +203,11 @@ _auto_attach, _get_cloud_instance, enable_ent_by_name, + _notice_remove, + _notice_add, FakeConfig, ): - cfg = FakeConfig(root_mode=True) + cfg = FakeConfig() options = FullAutoAttachOptions( enable=["esm-infra", "fips"], enable_beta=["esm-apps", "ros"], @@ -217,7 +223,9 @@ exceptions.LockHeldError("request", "holder", 10), ], ) - def test_lock_held(self, _m_spinlock_enter, FakeConfig): + def test_lock_held( + self, _m_spinlock_enter, _notice_remove, _notice_read, FakeConfig + ): with pytest.raises(exceptions.LockHeldError): _full_auto_attach(FullAutoAttachOptions, FakeConfig()) @@ -345,6 +353,8 @@ m_get_cloud_instance, m_auto_attach, m_enable_services_by_name, + _notice_remove, + _notice_add, options, is_attached, is_disabled, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/tests/test_api_u_unattended_upgrades_status_v1.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/tests/test_api_u_unattended_upgrades_status_v1.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/tests/test_api_u_unattended_upgrades_status_v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/tests/test_api_u_unattended_upgrades_status_v1.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,150 @@ +import datetime + +import mock +import pytest + +import uaclient.api.u.unattended_upgrades.status.v1 as api +from uaclient.messages import ( + UNATTENDED_UPGRADES_CFG_LIST_VALUE_EMPTY, + UNATTENDED_UPGRADES_CFG_VALUE_TURNED_OFF, + UNATTENDED_UPGRADES_SYSTEMD_JOB_DISABLED, +) + +M_PATH = "uaclient.api.u.unattended_upgrades.status.v1" + + +class TestUnattendedUpgradesGetAptDailyJob: + @pytest.mark.parametrize( + "job_state_return,expected_return", + ( + ((True, True), True), + ((False, True), False), + ((True, False), False), + ((False, False), False), + ), + ) + @mock.patch(M_PATH + ".get_systemd_job_state") + def test_get_apt_daily_job_status( + self, m_systemd_job_state, job_state_return, expected_return + ): + m_systemd_job_state.side_effect = job_state_return + assert expected_return is api._get_apt_daily_job_status() + + +class TestIsUnattendedUpgradesRunning: + @pytest.mark.parametrize( + "apt_timer_enabled,unattended_upgrades_cfg,expected_ret,expected_msg", + ( + (False, {}, False, UNATTENDED_UPGRADES_SYSTEMD_JOB_DISABLED), + ( + True, + {"test": []}, + False, + UNATTENDED_UPGRADES_CFG_LIST_VALUE_EMPTY.format( + cfg_name="test" + ), + ), + ( + True, + {"test": ["foo"], "test2": "0"}, + False, + UNATTENDED_UPGRADES_CFG_VALUE_TURNED_OFF.format( + cfg_name="test2" + ), + ), + ( + True, + {"test": ["foo"], "test2": "1"}, + True, + None, + ), + ), + ) + def test_is_unattended_upgrades_running( + self, + apt_timer_enabled, + unattended_upgrades_cfg, + expected_ret, + expected_msg, + ): + assert ( + expected_ret, + expected_msg, + ) == api._is_unattended_upgrades_running( + apt_timer_enabled, unattended_upgrades_cfg + ) + + +class TestUnattendedUpgradesLastRun: + @mock.patch("os.path.getctime") + def test_unattended_upgrades_last_run_when_file_not_present( + self, + m_getctime, + ): + m_getctime.side_effect = FileNotFoundError() + assert None is api._get_unattended_upgrades_last_run() + + +class TestUnattendedUpgradesStatusV1: + @mock.patch(M_PATH + ".get_apt_config_keys") + @mock.patch(M_PATH + ".get_apt_config_values") + @mock.patch(M_PATH + "._is_unattended_upgrades_running") + @mock.patch(M_PATH + "._get_apt_daily_job_status") + @mock.patch(M_PATH + "._get_unattended_upgrades_last_run") + def test_unattended_upgrades_status_v1( + self, + m_last_run, + m_apt_job_status, + m_is_running, + m_apt_cfg_values, + m_apt_cfg_keys, + FakeConfig, + ): + expected_datetime = datetime.datetime(2023, 2, 23, 15, 0, 0, 102490) + + m_is_running.return_value = (True, "") + m_apt_job_status.return_value = True + m_last_run.return_value = expected_datetime + m_apt_cfg_values.return_value = { + "APT::Periodic::Enable": "", + "APT::Periodic::Update-Package-Lists": "1", + "APT::Periodic::Unattended-Upgrade": "1", + "Unattended-Upgrade::Allowed-Origins": ["test"], + "Unattended-Upgrade::Mail": "mail", + } + m_apt_cfg_keys.return_value = ["Unattended-Upgrade::Mail"] + + actual_return = api._status(FakeConfig()) + assert True is actual_return.apt_periodic_job_enabled + assert True is actual_return.systemd_apt_timer_enabled + assert 1 == actual_return.package_lists_refresh_frequency_days + assert 1 == actual_return.unattended_upgrades_frequency_days + assert ["test"] == actual_return.unattended_upgrades_allowed_origins + assert True is actual_return.unattended_upgrades_running + assert expected_datetime == actual_return.unattended_upgrades_last_run + assert None is actual_return.unattended_upgrades_disabled_reason + assert ( + "1" + == actual_return.meta["raw_config"][ + "APT::Periodic::Unattended-Upgrade" + ] + ) + assert "1" == actual_return.meta["raw_config"]["APT::Periodic::Enable"] + assert ( + "1" + == actual_return.meta["raw_config"][ + "APT::Periodic::Update-Package-Lists" + ] + ) + assert ["test"] == actual_return.meta["raw_config"][ + "Unattended-Upgrade::Allowed-Origins" + ] + assert ( + "mail" + == actual_return.meta["raw_config"]["Unattended-Upgrade::Mail"] + ) + + assert m_is_running.call_count == 1 + assert m_apt_job_status.call_count == 1 + assert m_apt_cfg_values.call_count == 1 + assert m_last_run.call_count == 1 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/u/pro/attach/auto/full_auto_attach/v1.py 2023-04-05 15:14:00.000000000 +0000 @@ -63,7 +63,7 @@ def full_auto_attach(options: FullAutoAttachOptions) -> FullAutoAttachResult: - return _full_auto_attach(options, UAConfig(root_mode=True)) + return _full_auto_attach(options, UAConfig()) def _full_auto_attach( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/u/unattended_upgrades/status/v1.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/u/unattended_upgrades/status/v1.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/api/u/unattended_upgrades/status/v1.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/api/u/unattended_upgrades/status/v1.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,215 @@ +import datetime +import os +from typing import Dict, List, Optional, Tuple, Union + +from uaclient import exceptions, messages +from uaclient.api.api import APIEndpoint +from uaclient.api.data_types import AdditionalInfo +from uaclient.api.exceptions import UnattendedUpgradesError +from uaclient.apt import get_apt_config_keys, get_apt_config_values +from uaclient.config import UAConfig +from uaclient.data_types import ( + BoolDataValue, + DataObject, + DatetimeDataValue, + Field, + IntDataValue, + StringDataValue, +) +from uaclient.system import get_systemd_job_state + +UNATTENDED_UPGRADES_CONFIG_KEYS = [ + "APT::Periodic::Enable", + "APT::Periodic::Update-Package-Lists", + "APT::Periodic::Unattended-Upgrade", + "Unattended-Upgrade::Allowed-Origins", +] + +UNATTENDED_UPGRADES_STAMP_PATH = "/var/lib/apt/periodic/upgrade-stamp" + + +class UnattendedUpgradesDisabledReason(DataObject): + fields = [ + Field("msg", StringDataValue), + Field("code", StringDataValue), + ] + + def __init__(self, msg: str, code: str): + self.msg = msg + self.code = code + + +class UnattendedUpgradesStatusResult(DataObject, AdditionalInfo): + fields = [ + Field("systemd_apt_timer_enabled", BoolDataValue), + Field("apt_periodic_job_enabled", BoolDataValue), + Field("package_lists_refresh_frequency_days", IntDataValue), + Field("unattended_upgrades_frequency_days", IntDataValue), + Field("unattended_upgrades_allowed_origins", StringDataValue), + Field("unattended_upgrades_running", BoolDataValue), + Field( + "unattended_upgrades_disabled_reason", + UnattendedUpgradesDisabledReason, + required=False, + ), + Field( + "unattended_upgrades_last_run", DatetimeDataValue, required=False + ), + ] + + def __init__( + self, + *, + systemd_apt_timer_enabled: bool, + apt_periodic_job_enabled: bool, + package_lists_refresh_frequency_days: int, + unattended_upgrades_frequency_days: int, + unattended_upgrades_allowed_origins: List[str], + unattended_upgrades_running: bool, + unattended_upgrades_disabled_reason: Optional[ + UnattendedUpgradesDisabledReason + ], + unattended_upgrades_last_run: Optional[datetime.datetime] + ): + self.systemd_apt_timer_enabled = systemd_apt_timer_enabled + self.apt_periodic_job_enabled = apt_periodic_job_enabled + self.package_lists_refresh_frequency_days = ( + package_lists_refresh_frequency_days + ) + self.unattended_upgrades_frequency_days = ( + unattended_upgrades_frequency_days + ) + self.unattended_upgrades_allowed_origins = ( + unattended_upgrades_allowed_origins + ) + self.unattended_upgrades_running = unattended_upgrades_running + self.unattended_upgrades_disabled_reason = ( + unattended_upgrades_disabled_reason + ) + self.unattended_upgrades_last_run = unattended_upgrades_last_run + + +def _get_apt_daily_job_status() -> bool: + try: + apt_daily_job_enabled = get_systemd_job_state("apt-daily.timer") + apt_daily_upgrade_job_enabled = get_systemd_job_state( + "apt-daily-upgrade.timer" + ) + systemd_apt_timer_enabled = ( + apt_daily_job_enabled and apt_daily_upgrade_job_enabled + ) + except exceptions.ProcessExecutionError as e: + raise UnattendedUpgradesError(msg=str(e)) + + return systemd_apt_timer_enabled + + +def _is_unattended_upgrades_running( + systemd_apt_timer_enabled: bool, + unattended_upgrades_cfg: Dict[str, Union[str, List[str]]], +) -> Tuple[bool, Optional[messages.NamedMessage]]: + if not systemd_apt_timer_enabled: + return (False, messages.UNATTENDED_UPGRADES_SYSTEMD_JOB_DISABLED) + + for key, value in unattended_upgrades_cfg.items(): + if not value: + return ( + False, + messages.UNATTENDED_UPGRADES_CFG_LIST_VALUE_EMPTY.format( + cfg_name=key + ), + ) + if isinstance(value, str) and value == "0": + return ( + False, + messages.UNATTENDED_UPGRADES_CFG_VALUE_TURNED_OFF.format( + cfg_name=key + ), + ) + + return (True, None) + + +def _get_unattended_upgrades_last_run() -> Optional[datetime.datetime]: + try: + creation_epoch = os.path.getctime(UNATTENDED_UPGRADES_STAMP_PATH) + except FileNotFoundError: + return None + + return datetime.datetime.fromtimestamp(creation_epoch) + + +def status() -> UnattendedUpgradesStatusResult: + return _status(UAConfig()) + + +def _status(cfg: UAConfig) -> UnattendedUpgradesStatusResult: + systemd_apt_timer_enabled = _get_apt_daily_job_status() + unattended_upgrades_last_run = _get_unattended_upgrades_last_run() + + unattended_upgrades_cfg = get_apt_config_values( + set( + UNATTENDED_UPGRADES_CONFIG_KEYS + + get_apt_config_keys("Unattended-Upgrade") + ) + ) + + # If that key is not present on the APT Config, we assume it + # that the config is "enabled", as by default this configuration + # will not be present in APT + unattended_upgrades_cfg["APT::Periodic::Enable"] = ( + unattended_upgrades_cfg["APT::Periodic::Enable"] or "1" + ) + + ( + unattended_upgrades_running, + disabled_reason, + ) = _is_unattended_upgrades_running( + systemd_apt_timer_enabled, unattended_upgrades_cfg + ) + + if disabled_reason: + unattended_upgrades_disabled_reason = UnattendedUpgradesDisabledReason( + msg=disabled_reason.msg, + code=disabled_reason.name, + ) + else: + unattended_upgrades_disabled_reason = None + + unattended_upgrades_result = UnattendedUpgradesStatusResult( + systemd_apt_timer_enabled=systemd_apt_timer_enabled, + apt_periodic_job_enabled=str( + unattended_upgrades_cfg.get("APT::Periodic::Enable", "") + ) + == "1", + package_lists_refresh_frequency_days=int( + unattended_upgrades_cfg.get( # type: ignore + "APT::Periodic::Update-Package-Lists", 0 + ) + ), + unattended_upgrades_frequency_days=int( + unattended_upgrades_cfg.get( # type: ignore + "APT::Periodic::Unattended-Upgrade", 0 + ) + ), + unattended_upgrades_allowed_origins=list( + unattended_upgrades_cfg.get("Unattended-Upgrade::Allowed-Origins") + or [] + ), + unattended_upgrades_disabled_reason=( + unattended_upgrades_disabled_reason + ), + unattended_upgrades_running=unattended_upgrades_running, + unattended_upgrades_last_run=unattended_upgrades_last_run, + ) + unattended_upgrades_result.meta = {"raw_config": unattended_upgrades_cfg} + + return unattended_upgrades_result + + +endpoint = APIEndpoint( + version="v1", + name="UnattendedUpgradesStatus", + fn=_status, + options_cls=None, +) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/apt.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/apt.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/apt.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/apt.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,3 +1,4 @@ +import copy import datetime import enum import glob @@ -8,7 +9,10 @@ import sys import tempfile from functools import lru_cache -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, Iterable, List, NamedTuple, Optional, Union + +import apt # type: ignore +import apt_pkg # type: ignore from uaclient import event_logger, exceptions, gpg, messages, system from uaclient.defaults import ESM_APT_ROOTDIR @@ -211,6 +215,91 @@ ) +class PreserveAptCfg: + def __init__(self, apt_func): + self.apt_func = apt_func + self.current_apt_cfg = {} # Dict[str, Any] + + def __enter__(self): + cfg = apt_pkg.config + self.current_apt_cfg = { + key: copy.deepcopy(cfg.get(key)) for key in cfg.keys() + } + + return self.apt_func() + + def __exit__(self, type, value, traceback): + cfg = apt_pkg.config + # We need to restore the apt cache configuration after creating our + # cache, otherwise we may break people interacting with the + # library after importing our modules. + for key in self.current_apt_cfg.keys(): + cfg.set(key, self.current_apt_cfg[key]) + apt_pkg.init_system() + + +def get_apt_cache(): + for key in apt_pkg.config.keys(): + apt_pkg.config.clear(key) + apt_pkg.init() + cache = apt.Cache() + return cache + + +def get_esm_cache(): + try: + # Take care to initialize the cache with only the + # Acquire configuration preserved + for key in apt_pkg.config.keys(): + if not re.search("^Acquire", key): + apt_pkg.config.clear(key) + apt_pkg.config.set("Dir", ESM_APT_ROOTDIR) + apt_pkg.init() + # If the rootdir folder doesn't contain any apt source info, the + # cache will be empty + cache = apt.Cache(rootdir=ESM_APT_ROOTDIR) + except Exception: + cache = {} + + return cache + + +def get_pkg_candidate_version(pkg: str) -> Optional[str]: + with PreserveAptCfg(get_apt_cache) as cache: + try: + package = cache[pkg] + except Exception: + return None + + if not package.candidate: + return None + + pkg_candidate = getattr(package.candidate, "version") + + if not pkg_candidate: + return None + + with PreserveAptCfg(get_esm_cache) as esm_cache: + if esm_cache: + try: + esm_package = esm_cache[pkg] + except Exception: + return pkg_candidate + + if not esm_package.candidate: + return pkg_candidate + + esm_pkg_candidate = getattr(esm_package.candidate, "version") + + if not esm_pkg_candidate: + return pkg_candidate + + if compare_versions(esm_pkg_candidate, pkg_candidate, "ge"): + return esm_pkg_candidate + + return pkg_candidate + + def get_apt_cache_policy_for_package( package: str, error_msg: Optional[str] = None, @@ -602,37 +691,50 @@ if not system.is_current_series_lts(): return - import apt # type: ignore - import apt_pkg # type: ignore - + from uaclient.actions import status from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.entitlements.esm import ( ESMAppsEntitlement, ESMInfraEntitlement, ) + apps_available = False + infra_available = False + + current_status = cfg.read_cache("status-cache") + if current_status is None: + current_status = status(cfg)[0] + + for service in current_status.get("services", []): + if service.get("name", "") == "esm-apps": + apps_available = service.get("available", "no") == "yes" + if service.get("name", "") == "esm-infra": + infra_available = service.get("available", "no") == "yes" + apps = ESMAppsEntitlement(cfg) # Always setup ESM-Apps - if apps.application_status()[0] == ApplicationStatus.DISABLED: + if ( + apps_available + and apps.application_status()[0] == ApplicationStatus.DISABLED + ): apps.setup_local_esm_repo() + else: + apps.disable_local_esm_repo() # Only setup ESM-Infra for EOSS systems if system.is_current_series_active_esm(): infra = ESMInfraEntitlement(cfg) - if infra.application_status()[0] == ApplicationStatus.DISABLED: + if ( + infra_available + and infra.application_status()[0] == ApplicationStatus.DISABLED + ): infra.setup_local_esm_repo() + else: + infra.disable_local_esm_repo() # Read the cache and update it - # Take care to initialize the cache with only the - # Acquire configuration preserved - for key in apt_pkg.config.keys(): - if "Acquire" not in key: - apt_pkg.config.clear(key) - apt_pkg.config.set("Dir", ESM_APT_ROOTDIR) - apt_pkg.init_config() - - cache = apt.Cache(rootdir=ESM_APT_ROOTDIR) + cache = get_esm_cache() try: cache.update() @@ -640,3 +742,57 @@ # in tests - down the rabbit hole, not worth it except apt.cache.FetchFailedException: logging.warning("Failed to fetch the ESM Apt Cache") + + +def remove_packages(package_names: List[str], error_message: str): + run_apt_command( + [ + "apt-get", + "remove", + "--assume-yes", + '-o Dpkg::Options::="--force-confdef"', + '-o Dpkg::Options::="--force-confold"', + ] + + list(package_names), + error_message, + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) + + +def _get_apt_config(): + # We need to clear the config values in case another module + # has already initiated it + for key in apt_pkg.config.keys(): + apt_pkg.config.clear(key) + + apt_pkg.init_config() + return apt_pkg.config + + +def get_apt_config_keys(base_key): + with PreserveAptCfg(_get_apt_config) as apt_cfg: + apt_cfg_keys = apt_cfg.list(base_key) + + return apt_cfg_keys + + +def get_apt_config_values( + cfg_names: Iterable[str], +) -> Dict[str, Union[str, List[str]]]: + """ + Get all APT configuration values for the given config names. If + one of the config names is not present on the APT config, that + config name will have a value of None + """ + apt_cfg_dict = {} # type: Dict[str, Union[str, List[str]]] + + with PreserveAptCfg(_get_apt_config) as apt_cfg: + for cfg_name in cfg_names: + cfg_value = apt_cfg.get(cfg_name) + + if not str(cfg_value): + cfg_value = apt_cfg.value_list(cfg_name) or None + + apt_cfg_dict[cfg_name] = cfg_value + + return apt_cfg_dict diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/cli.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/cli.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/cli.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/cli.py 2023-04-05 15:14:00.000000000 +0000 @@ -3,7 +3,6 @@ import argparse import json import logging -import os import pathlib import re import sys @@ -14,8 +13,6 @@ from functools import wraps from typing import List, Optional, Tuple # noqa -import yaml - from uaclient import ( actions, apt, @@ -28,10 +25,9 @@ event_logger, exceptions, lock, - messages, - security, - security_status, ) +from uaclient import log as pro_log +from uaclient import messages, security, security_status from uaclient import status as ua_status from uaclient import util, version from uaclient.api.api import call_api @@ -53,7 +49,7 @@ ) from uaclient.apt import AptProxyScope, setup_apt_proxy from uaclient.data_types import AttachActionsConfigFile, IncorrectTypeError -from uaclient.defaults import DEFAULT_LOG_FORMAT, PRINT_WRAP_WIDTH +from uaclient.defaults import PRINT_WRAP_WIDTH from uaclient.entitlements import ( create_enable_entitlements_not_found_message, entitlements_disable_order, @@ -65,11 +61,11 @@ CanEnableFailure, CanEnableFailureReason, ) -from uaclient.files import state_files -from uaclient.jobs.update_messaging import ( - refresh_motd, - update_apt_and_motd_messages, -) +from uaclient.files import notices, state_files +from uaclient.files.notices import Notice +from uaclient.jobs.update_messaging import refresh_motd, update_motd_messages +from uaclient.log import JsonArrayFormatter +from uaclient.yaml import safe_dump, safe_load NAME = "pro" @@ -156,8 +152,7 @@ @staticmethod def _get_service_descriptions() -> Tuple[List[str], List[str]]: - root_mode = os.getuid() == 0 - cfg = config.UAConfig(root_mode=root_mode) + cfg = config.UAConfig() service_info_tmpl = " - {name}: {description}{url}" non_beta_services_desc = [] @@ -216,7 +211,7 @@ @wraps(f) def new_f(*args, **kwargs): - if os.getuid() != 0: + if not util.we_are_currently_root(): raise exceptions.NonRootUserError() else: return f(*args, **kwargs) @@ -328,9 +323,11 @@ return parser -def config_show_parser(parser): +def config_show_parser(parser, parent_command: str): """Build or extend an arg parser for 'config show' subcommand.""" - parser.usage = USAGE_TMPL.format(name=NAME, command="show [key]") + parser.usage = USAGE_TMPL.format( + name=NAME, command="{} show [key]".format(parent_command) + ) parser.prog = "show" parser.description = "Show customisable configuration settings" parser.add_argument( @@ -341,10 +338,12 @@ return parser -def config_set_parser(parser): +def config_set_parser(parser, parent_command: str): """Build or extend an arg parser for 'config set' subcommand.""" - parser.usage = USAGE_TMPL.format(name=NAME, command="set =") - parser.prog = "set" + parser.usage = USAGE_TMPL.format( + name=NAME, command="{} set =".format(parent_command) + ) + parser.prog = "aset" parser.description = "Set and apply Ubuntu Pro configuration settings" parser._optionals.title = "Flags" parser.add_argument( @@ -359,9 +358,11 @@ return parser -def config_unset_parser(parser): +def config_unset_parser(parser, parent_command: str): """Build or extend an arg parser for 'config unset' subcommand.""" - parser.usage = USAGE_TMPL.format(name=NAME, command="unset ") + parser.usage = USAGE_TMPL.format( + name=NAME, command="{} unset ".format(parent_command) + ) parser.prog = "unset" parser.description = "Unset Ubuntu Pro configuration setting" parser.add_argument( @@ -378,8 +379,11 @@ def config_parser(parser): """Build or extend an arg parser for config subcommand.""" - parser.usage = USAGE_TMPL.format(name=NAME, command="config ") - parser.prog = "config" + command = "config" + parser.usage = USAGE_TMPL.format( + name=NAME, command="{} ".format(command) + ) + parser.prog = command parser.description = "Manage Ubuntu Pro configuration" parser._optionals.title = "Flags" subparsers = parser.add_subparsers( @@ -389,19 +393,19 @@ "show", help="show all Ubuntu Pro configuration setting(s)" ) parser_show.set_defaults(action=action_config_show) - config_show_parser(parser_show) + config_show_parser(parser_show, parent_command=command) parser_set = subparsers.add_parser( "set", help="set Ubuntu Pro configuration setting" ) parser_set.set_defaults(action=action_config_set) - config_set_parser(parser_set) + config_set_parser(parser_set, parent_command=command) parser_unset = subparsers.add_parser( "unset", help="unset Ubuntu Pro configuration setting" ) parser_unset.set_defaults(action=action_config_unset) - config_unset_parser(parser_unset) + config_unset_parser(parser_unset, parent_command=command) return parser @@ -600,7 +604,7 @@ ) else: print( - yaml.safe_dump( + safe_dump( security_status.security_status_dict(cfg), default_flow_style=False, ) @@ -996,7 +1000,7 @@ @return: 0 on success, 1 otherwise """ - from uaclient.entitlements.livepatch import configure_livepatch_proxy + from uaclient.livepatch import configure_livepatch_proxy from uaclient.snap import configure_snap_proxy parser = get_parser(cfg=cfg) @@ -1123,7 +1127,7 @@ @return: 0 on success, 1 otherwise """ from uaclient.apt import AptProxyScope - from uaclient.entitlements.livepatch import unconfigure_livepatch_proxy + from uaclient.livepatch import unconfigure_livepatch_proxy from uaclient.snap import unconfigure_snap_proxy if args.key not in config.UA_CONFIGURABLE_KEYS: @@ -1366,7 +1370,7 @@ cfg.delete_cache() cfg.machine_token_file.delete() - update_apt_and_motd_messages(cfg) + update_motd_messages(cfg) event.info(messages.DETACH_SUCCESS) return 0 @@ -1426,16 +1430,12 @@ value=args.format, ) - event.info("Initiating attach operation...") + event.info(messages.CLI_MAGIC_ATTACH_INIT) initiate_resp = _initiate(cfg=cfg) - - event.info("\nPlease sign in to your Ubuntu Pro account at this link:") - event.info("https://ubuntu.com/pro/attach") event.info( - "And provide the following code: {}{}{}".format( - messages.TxtColor.BOLD, - initiate_resp.user_code, - messages.TxtColor.ENDC, + "\n" + + messages.CLI_MAGIC_ATTACH_SIGN_IN.format( + user_code=initiate_resp.user_code ) ) @@ -1444,7 +1444,7 @@ try: wait_resp = _wait(options=wait_options, cfg=cfg) except exceptions.MagicAttachTokenError as e: - event.info("Failed to perform magic-attach") + event.info(messages.CLI_MAGIC_ATTACH_FAILED) revoke_options = MagicAttachRevokeOptions( magic_token=initiate_resp.token @@ -1452,7 +1452,7 @@ _revoke(options=revoke_options, cfg=cfg) raise e - event.info("\nAttaching the machine...") + event.info("\n" + messages.CLI_MAGIC_ATTACH_PROCESSING) return wait_resp.contract_token @@ -1474,7 +1474,7 @@ else: try: attach_config = AttachActionsConfigFile.from_dict( - yaml.safe_load(args.attach_config) + safe_load(args.attach_config) ) except IncorrectTypeError as e: raise exceptions.AttachInvalidConfigFileError( @@ -1665,29 +1665,12 @@ return parser -def action_status(args, *, cfg): +def action_status(args, *, cfg: config.UAConfig): if not cfg: cfg = config.UAConfig() show_all = args.all if args else False token = args.simulate_with_token if args else None active_value = ua_status.UserFacingConfigStatus.ACTIVE.value - if cfg.is_attached: - try: - if contract.is_contract_changed(cfg): - cfg.notice_file.try_add( - "", messages.NOTICE_REFRESH_CONTRACT_WARNING - ) - else: - cfg.notice_file.try_remove( - "", messages.NOTICE_REFRESH_CONTRACT_WARNING - ) - except Exception as e: - with util.disable_log_to_console(): - err_msg = messages.UPDATE_CHECK_CONTRACT_FAILURE.format( - reason=str(e) - ) - logging.warning(err_msg) - event.warning(err_msg) status, ret = actions.status( cfg, simulate_with_token=token, show_all=show_all ) @@ -1758,7 +1741,7 @@ # functions should raise UserFacingError exceptions, which are # covered by the main_error_handler decorator try: - update_apt_and_motd_messages(cfg) + update_motd_messages(cfg) refresh_motd() if cfg.apt_news: apt_news.update_apt_news(cfg) @@ -1778,7 +1761,7 @@ if args.target is None or args.target == "contract": _action_refresh_contract(args, cfg) - cfg.notice_file.remove("", messages.NOTICE_REFRESH_CONTRACT_WARNING) + notices.remove(Notice.CONTRACT_REFRESH_WARNING) if args.target is None or args.target == "messages": _action_refresh_messages(args, cfg) @@ -1854,11 +1837,11 @@ cfg = config.UAConfig() log_file = cfg.log_file console_formatter = util.LogFormatter() - log_formatter = logging.Formatter(DEFAULT_LOG_FORMAT) if logger is None: # Then we configure the root logger logger = logging.getLogger() logger.setLevel(log_level) + logger.addFilter(pro_log.RedactionFilter()) # Clear all handlers, so they are replaced for this logger logger.handlers = [] @@ -1871,7 +1854,7 @@ logger.addHandler(console_handler) # Setup file logging - if os.getuid() == 0: + if util.we_are_currently_root(): # Setup readable-by-root-only debug file logging if running as root log_file_path = pathlib.Path(log_file) @@ -1880,8 +1863,8 @@ log_file_path.chmod(0o644) file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(JsonArrayFormatter()) file_handler.setLevel(log_level) - file_handler.setFormatter(log_formatter) file_handler.set_name("ua-file") logger.addHandler(file_handler) @@ -1977,8 +1960,7 @@ def main(sys_argv=None): if not sys_argv: sys_argv = sys.argv - is_root = os.getuid() == 0 - cfg = config.UAConfig(root_mode=is_root) + cfg = config.UAConfig() parser = get_parser(cfg=cfg) cli_arguments = sys_argv[1:] if not cli_arguments: @@ -1996,9 +1978,7 @@ console_level = logging.DEBUG if args.debug else logging.INFO setup_logging(console_level, log_level, cfg.log_file) - logging.debug( - util.redact_sensitive_logs("Executed with sys.argv: %r" % sys_argv) - ) + logging.debug("Executed with sys.argv: %r" % sys_argv) with util.disable_log_to_console(): cfg.warn_about_invalid_keys() @@ -2009,9 +1989,7 @@ ] if pro_environment: logging.debug( - util.redact_sensitive_logs( - "Executed with environment variables: %r" % pro_environment - ) + "Executed with environment variables: %r" % pro_environment ) return_value = args.action(args, cfg=cfg) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/config.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/config.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/config.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/config.py 2023-04-05 15:14:00.000000000 +0000 @@ -6,8 +6,6 @@ from functools import lru_cache, wraps from typing import Any, Callable, Dict, Optional, Tuple, TypeVar -import yaml - from uaclient import ( apt, event_logger, @@ -21,12 +19,16 @@ from uaclient.defaults import ( APT_NEWS_URL, BASE_CONTRACT_URL, + BASE_LIVEPATCH_URL, BASE_SECURITY_URL, CONFIG_DEFAULTS, CONFIG_FIELD_ENVVAR_ALLOWLIST, DEFAULT_CONFIG_FILE, + DEFAULT_DATA_DIR, ) -from uaclient.files import NoticeFile +from uaclient.files import notices, state_files +from uaclient.files.notices import Notice +from uaclient.yaml import safe_load LOG = logging.getLogger(__name__) @@ -65,6 +67,7 @@ "timer_log_file", "daemon_log_file", "ua_config", + "livepatch_url", ) # A data path is a filename, an attribute ("private") indicating whether it @@ -90,7 +93,6 @@ "machine-access-cis": DataPath("machine-access-cis.json", True, False), "lock": DataPath("lock", False, False), "status-cache": DataPath("status.json", False, False), - "notices": DataPath("notices.json", False, False), "marker-reboot-cmds": DataPath( "marker-reboot-cmds-required", False, False ), @@ -109,8 +111,8 @@ def __init__( self, cfg: Optional[Dict[str, Any]] = None, + user_config: Optional[state_files.UserConfigData] = None, series: Optional[str] = None, - root_mode: bool = False, ) -> None: """""" if cfg: @@ -121,30 +123,43 @@ self.cfg_path = get_config_path() self.cfg, self.invalid_keys = parse_config(self.cfg_path) + if user_config: + self.user_config = user_config + else: + try: + self.user_config = ( + state_files.user_config_file.read() + or state_files.UserConfigData() + ) + except Exception as e: + with util.disable_log_to_console(): + logging.warning("Error loading user config: {}".format(e)) + logging.warning("Using default config values") + self.user_config = state_files.UserConfigData() + + # support old ua_config values in uaclient.conf as user-config.json + # value overrides + if "ua_config" in self.cfg: + self.user_config = state_files.UserConfigData.from_dict( + {**self.user_config.to_dict(), **self.cfg["ua_config"]}, + optional_type_errors_become_null=True, + ) + self.series = series - self.root_mode = root_mode self._machine_token_file = ( None ) # type: Optional[files.MachineTokenFile] - self._notice_file = None # type: Optional[NoticeFile] @property def machine_token_file(self): if not self._machine_token_file: self._machine_token_file = files.MachineTokenFile( self.data_dir, - self.root_mode, self.features.get("machine_token_overlay"), ) return self._machine_token_file @property - def notice_file(self): - if not self._notice_file: - self._notice_file = NoticeFile(self.data_dir, self.root_mode) - return self._notice_file - - @property def contract_url(self) -> str: return self.cfg.get("contract_url", BASE_CONTRACT_URL) @@ -153,57 +168,53 @@ return self.cfg.get("security_url", BASE_SECURITY_URL) @property + def livepatch_url(self) -> str: + return self.cfg.get("livepatch_url", BASE_LIVEPATCH_URL) + + @property def http_proxy(self) -> Optional[str]: - return self.cfg.get("ua_config", {}).get("http_proxy") + return self.user_config.http_proxy @http_proxy.setter def http_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["http_proxy"] = value - self.write_cfg() + self.user_config.http_proxy = value + state_files.user_config_file.write(self.user_config) @property def https_proxy(self) -> Optional[str]: - return self.cfg.get("ua_config", {}).get("https_proxy") + return self.user_config.https_proxy @https_proxy.setter def https_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["https_proxy"] = value - self.write_cfg() + self.user_config.https_proxy = value + state_files.user_config_file.write(self.user_config) @property def ua_apt_https_proxy(self) -> Optional[str]: - return self.cfg.get("ua_config", {}).get("ua_apt_https_proxy") + return self.user_config.ua_apt_https_proxy @ua_apt_https_proxy.setter def ua_apt_https_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["ua_apt_https_proxy"] = value - self.write_cfg() + self.user_config.ua_apt_https_proxy = value + state_files.user_config_file.write(self.user_config) @property def ua_apt_http_proxy(self) -> Optional[str]: - return self.cfg.get("ua_config", {}).get("ua_apt_http_proxy") + return self.user_config.ua_apt_http_proxy @ua_apt_http_proxy.setter def ua_apt_http_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["ua_apt_http_proxy"] = value - self.write_cfg() + self.user_config.ua_apt_http_proxy = value + state_files.user_config_file.write(self.user_config) @property # type: ignore @str_cache def global_apt_http_proxy(self) -> Optional[str]: - global_val = self.cfg.get("ua_config", {}).get("global_apt_http_proxy") + global_val = self.user_config.global_apt_http_proxy if global_val: return global_val - old_apt_val = self.cfg.get("ua_config", {}).get("apt_http_proxy") + old_apt_val = self.user_config.apt_http_proxy if old_apt_val: event.info(messages.WARNING_DEPRECATED_APT_HTTP) return old_apt_val @@ -211,23 +222,19 @@ @global_apt_http_proxy.setter def global_apt_http_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["global_apt_http_proxy"] = value - self.cfg["ua_config"]["apt_http_proxy"] = None + self.user_config.global_apt_http_proxy = value + self.user_config.apt_http_proxy = None UAConfig.global_apt_http_proxy.fget.cache_clear() # type: ignore - self.write_cfg() + state_files.user_config_file.write(self.user_config) @property # type: ignore @str_cache def global_apt_https_proxy(self) -> Optional[str]: - global_val = self.cfg.get("ua_config", {}).get( - "global_apt_https_proxy" - ) + global_val = self.user_config.global_apt_https_proxy if global_val: return global_val - old_apt_val = self.cfg.get("ua_config", {}).get("apt_https_proxy") + old_apt_val = self.user_config.apt_https_proxy if old_apt_val: event.info(messages.WARNING_DEPRECATED_APT_HTTPS) return old_apt_val @@ -235,85 +242,87 @@ @global_apt_https_proxy.setter def global_apt_https_proxy(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["global_apt_https_proxy"] = value - self.cfg["ua_config"]["apt_https_proxy"] = None + self.user_config.global_apt_https_proxy = value + self.user_config.apt_https_proxy = None UAConfig.global_apt_https_proxy.fget.cache_clear() # type: ignore - self.write_cfg() + state_files.user_config_file.write(self.user_config) @property - def update_messaging_timer(self) -> Optional[int]: - return self.cfg.get("ua_config", {}).get("update_messaging_timer") + def update_messaging_timer(self) -> int: + val = self.user_config.update_messaging_timer + if val is None: + return 21600 + return val @update_messaging_timer.setter def update_messaging_timer(self, value: int): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["update_messaging_timer"] = value - self.write_cfg() + self.user_config.update_messaging_timer = value + state_files.user_config_file.write(self.user_config) @property - def metering_timer(self) -> "Optional[int]": - return self.cfg.get("ua_config", {}).get("metering_timer") + def metering_timer(self) -> int: + val = self.user_config.metering_timer + if val is None: + return 14400 + return val @metering_timer.setter def metering_timer(self, value: int): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["metering_timer"] = value - self.write_cfg() + self.user_config.metering_timer = value + state_files.user_config_file.write(self.user_config) @property def poll_for_pro_license(self) -> bool: # TODO: when polling is supported # 1. change default here to True # 2. add this field to UA_CONFIGURABLE_KEYS - return self.cfg.get("ua_config", {}).get("poll_for_pro_license", False) + val = self.user_config.poll_for_pro_license + if val is None: + return False + return val @poll_for_pro_license.setter def poll_for_pro_license(self, value: bool): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["poll_for_pro_license"] = value - self.write_cfg() + self.user_config.poll_for_pro_license = value + state_files.user_config_file.write(self.user_config) @property def polling_error_retry_delay(self) -> int: # TODO: when polling is supported # 1. add this field to UA_CONFIGURABLE_KEYS - return self.cfg.get("ua_config", {}).get( - "polling_error_retry_delay", 600 - ) + val = self.user_config.polling_error_retry_delay + if val is None: + return 600 + return val @polling_error_retry_delay.setter def polling_error_retry_delay(self, value: int): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["polling_error_retry_delay"] = value - self.write_cfg() + self.user_config.polling_error_retry_delay = value + state_files.user_config_file.write(self.user_config) @property def apt_news(self) -> bool: - return self.cfg.get("ua_config", {}).get("apt_news", True) + val = self.user_config.apt_news + if val is None: + return True + return val @apt_news.setter def apt_news(self, value: bool): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["apt_news"] = value - self.write_cfg() + self.user_config.apt_news = value + state_files.user_config_file.write(self.user_config) @property def apt_news_url(self) -> str: - return self.cfg.get("ua_config", {}).get("apt_news_url", APT_NEWS_URL) + val = self.user_config.apt_news_url + if val is None: + return APT_NEWS_URL + return val @apt_news_url.setter def apt_news_url(self, value: str): - if "ua_config" not in self.cfg: - self.cfg["ua_config"] = {} - self.cfg["ua_config"]["apt_news_url"] = value - self.write_cfg() + self.user_config.apt_news_url = value + state_files.user_config_file.write(self.user_config) def check_lock_info(self) -> Tuple[int, str]: """Return lock info if config lock file is present the lock is active. @@ -331,12 +340,19 @@ if not os.path.exists(lock_path): return no_lock lock_content = system.load_file(lock_path) - [lock_pid, lock_holder] = lock_content.split(":") + + try: + [lock_pid, lock_holder] = lock_content.split(":") + except ValueError: + raise exceptions.InvalidLockFile( + os.path.join(self.data_dir, "lock") + ) + try: system.subp(["ps", lock_pid]) return (int(lock_pid), lock_holder) except exceptions.ProcessExecutionError: - if os.getuid() != 0: + if not util.we_are_currently_root(): logging.debug( "Found stale lock file previously held by %s:%s", lock_pid, @@ -353,7 +369,7 @@ @property def data_dir(self): - return self.cfg["data_dir"] + return self.cfg.get("data_dir", DEFAULT_DATA_DIR) @property def log_level(self): @@ -361,7 +377,7 @@ try: return getattr(logging, log_level.upper()) except AttributeError: - return getattr(logging, CONFIG_DEFAULTS["log_level"]) + return logging.DEBUG @property def log_file(self) -> str: @@ -406,7 +422,7 @@ def data_path(self, key: Optional[str] = None) -> str: """Return the file path in the data directory represented by the key""" - data_dir = self.cfg["data_dir"] + data_dir = self.data_dir if not key: return os.path.join(data_dir, PRIVATE_SUBDIR) if key in self.data_paths: @@ -439,7 +455,7 @@ if key.startswith("machine-access"): self._machine_token_file = None elif key == "lock": - self.notice_file.remove("", "Operation in progress.*") + notices.remove(Notice.OPERATION_IN_PROGRESS) cache_path = self.data_path(key) self._perform_delete(cache_path) @@ -477,9 +493,9 @@ self._machine_token_file = None elif key == "lock": if ":" in content: - self.notice_file.add( - "", - "Operation in progress: {}".format(content.split(":")[1]), + notices.add( + Notice.OPERATION_IN_PROGRESS, + operation=content.split(":")[1], ) if not isinstance(content, str): content = json.dumps(content, cls=util.DatetimeAwareJSONEncoder) @@ -564,10 +580,11 @@ ): services_with_proxies.append("snap") - from uaclient.entitlements import livepatch + from uaclient import livepatch from uaclient.entitlements.entitlement_status import ApplicationStatus + from uaclient.entitlements.livepatch import LivepatchEntitlement - livepatch_ent = livepatch.LivepatchEntitlement() + livepatch_ent = LivepatchEntitlement() livepatch_status, _ = livepatch_ent.application_status() if livepatch_status == ApplicationStatus.ENABLED: @@ -595,40 +612,26 @@ ) ) - def write_cfg(self, config_path=None): - """Write config values back to config_path or DEFAULT_CONFIG_FILE.""" - if not config_path: - config_path = DEFAULT_CONFIG_FILE - content = messages.UACLIENT_CONF_HEADER - cfg_dict = copy.deepcopy(self.cfg) - if "log_level" not in cfg_dict: - cfg_dict["log_level"] = CONFIG_DEFAULTS["log_level"] - # Ensure defaults are present in uaclient.conf if absent - for attr in ( - "contract_url", - "security_url", - "data_dir", - "log_file", - "timer_log_file", - "daemon_log_file", - ): - cfg_dict[attr] = getattr(self, attr) - - # Each UA_CONFIGURABLE_KEY needs to have a property on UAConfig - # which reads the proper key value or returns a default - cfg_dict["ua_config"] = { - key: getattr(self, key, None) for key in UA_CONFIGURABLE_KEYS - } - - content += yaml.dump(cfg_dict, default_flow_style=False) - system.write_file(config_path, content) - def warn_about_invalid_keys(self): if self.invalid_keys is not None: for invalid_key in sorted(self.invalid_keys): logging.warning( "Ignoring invalid uaclient.conf key: %s", invalid_key ) + if "ua_config" in self.cfg: + # this one is still technically supported but we want people to + # migrate so it gets a special warning + logging.warning('legacy "ua_config" found in uaclient.conf') + logging.warning("Please do the following:") + logging.warning( + " 1. run `pro config set field=value` for each" + ' field/value pair present under "ua_config" in' + " /etc/ubuntu-advantage/uaclient.conf" + ) + logging.warning( + ' 2. Delete "ua_config" and all sub-fields in' + " /etc/ubuntu-advantage/uaclient.conf" + ) def get_config_path() -> str: @@ -666,7 +669,7 @@ LOG.debug("Using client configuration file at %s", config_path) if os.path.exists(config_path): - cfg.update(yaml.safe_load(system.load_file(config_path))) + cfg.update(safe_load(system.load_file(config_path))) env_keys = {} for key, value in os.environ.items(): key = key.lower() @@ -683,7 +686,7 @@ # with it if value.endswith("yaml"): if os.path.exists(value): - value = yaml.safe_load(system.load_file(value)) + value = safe_load(system.load_file(value)) else: raise exceptions.UserFacingError( "Could not find yaml file: {}".format(value) @@ -696,7 +699,8 @@ elif key in CONFIG_FIELD_ENVVAR_ALLOWLIST: env_keys[field_name] = value cfg.update(env_keys) - cfg["data_dir"] = os.path.expanduser(cfg["data_dir"]) + if "data_dir" in cfg: + cfg["data_dir"] = os.path.expanduser(cfg["data_dir"]) for key in ("contract_url", "security_url"): if not util.is_service_url(cfg[key]): raise exceptions.UserFacingError( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/conftest.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/conftest.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/conftest.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/conftest.py 2023-04-05 15:14:00.000000000 +0000 @@ -2,18 +2,28 @@ import io import logging import sys +from enum import Enum from typing import Any, Dict import mock import pytest -from uaclient import event_logger -from uaclient.config import UAConfig - # We are doing this because we are sure that python3-apt comes with the distro, # but it cannot be installed in a virtual environment to be properly tested. +# Those need to be mocked here, before importing our modules, so the pytest +# virtualenv doesn't cry because it can't find the modules +m_apt_pkg = mock.MagicMock() sys.modules["apt"] = mock.MagicMock() -sys.modules["apt_pkg"] = mock.MagicMock() +sys.modules["apt_pkg"] = m_apt_pkg + +# Useless try/except to make flake8 happy \_("/)_/ +try: + from uaclient import event_logger + from uaclient.config import UAConfig + from uaclient.files.notices import NoticeFileDetails + from uaclient.files.state_files import UserConfigData +except ImportError: + raise @pytest.yield_fixture(scope="session", autouse=True) @@ -46,6 +56,19 @@ yield original +@pytest.yield_fixture(scope="session", autouse=True) +def util_we_are_currently_root(): + """ + A fixture that mocks util.we_are_currently_root for all tests. + Default to true as most tests need it to be true. + """ + from uaclient.util import we_are_currently_root + + original = we_are_currently_root + with mock.patch("uaclient.util.we_are_currently_root", return_value=True): + yield original + + @pytest.fixture def caplog_text(request): """ @@ -118,11 +141,13 @@ self, cfg_overrides={}, features_override=None, - root_mode: bool = True, ) -> None: if not cfg_overrides.get("data_dir"): cfg_overrides.update({"data_dir": tmpdir.strpath}) - super().__init__(cfg_overrides, root_mode=root_mode) + super().__init__( + cfg_overrides, + user_config=UserConfigData(), + ) @classmethod def for_attached_machine( @@ -131,7 +156,6 @@ machine_token: Dict[str, Any] = None, status_cache: Dict[str, Any] = None, effective_to: datetime.datetime = None, - root_mode: bool = True, ): if not machine_token: machine_token = { @@ -199,7 +223,7 @@ if not status_cache: status_cache = {"attached": True} - config = cls(root_mode=root_mode) + config = cls() config.machine_token_file._machine_token = machine_token config.write_cache("status-cache", status_cache) return config @@ -217,3 +241,29 @@ event.reset() return event + + +class FakeNotice(NoticeFileDetails, Enum): + a = NoticeFileDetails("01", "a", True, "notice_a") + a2 = NoticeFileDetails("03", "a2", True, "notice_a2") + b = NoticeFileDetails("02", "b2", False, "notice_b") + + +@pytest.yield_fixture(autouse=True) +def mock_notices_dir(tmpdir_factory): + perm_dir = tmpdir_factory.mktemp("notices") + temp_dir = tmpdir_factory.mktemp("temp_notices") + with mock.patch( + "uaclient.defaults.NOTICES_PERMANENT_DIRECTORY", + perm_dir.strpath, + ): + with mock.patch( + "uaclient.defaults.NOTICES_TEMPORARY_DIRECTORY", + temp_dir.strpath, + ): + yield + + +@pytest.fixture +def apt_pkg(): + return m_apt_pkg diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/contract_data_types.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/contract_data_types.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/contract_data_types.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/contract_data_types.py 2023-04-05 15:14:00.000000000 +0000 @@ -3,6 +3,7 @@ from uaclient.data_types import ( BoolDataValue, DataObject, + DatetimeDataValue, Field, IntDataValue, StringDataValue, @@ -67,7 +68,7 @@ fields = [ Field("name", StringDataValue, False), Field("id", StringDataValue, False), - Field("createdAt", StringDataValue, False), + Field("createdAt", DatetimeDataValue, False), Field("type", StringDataValue, False), Field("userRoleOnAccount", StringDataValue, False), Field("externalAccountIDs", data_list(ExternalID), False), @@ -238,12 +239,12 @@ fields = [ Field("name", StringDataValue, False), Field("id", StringDataValue, False), - Field("createdAt", StringDataValue, False), + Field("createdAt", DatetimeDataValue, False), Field("createdBy", StringDataValue, False), Field("resourceEntitlements", data_list(Entitlement), False), Field("specificResourceEntitlements", data_list(Entitlement), False), - Field("effectiveFrom", StringDataValue, False), - Field("effectiveTo", StringDataValue, False), + Field("effectiveFrom", DatetimeDataValue, False), + Field("effectiveTo", DatetimeDataValue, False), Field("products", data_list(StringDataValue), False), Field("origin", StringDataValue, False), ] @@ -278,7 +279,7 @@ Field("machineId", StringDataValue, False), Field("accountInfo", AccountInfo, False), Field("contractInfo", ContractInfo, False), - Field("expires", StringDataValue, False), + Field("expires", DatetimeDataValue, False), ] def __init__( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/contract.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/contract.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/contract.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/contract.py 2023-04-05 15:14:00.000000000 +0000 @@ -358,6 +358,7 @@ "architecture": platform["arch"], "series": platform["series"], "kernel": platform["kernel"], + "virt": platform["virt"], } def _get_activity_info(self, machine_id: Optional[str] = None): @@ -652,7 +653,7 @@ .get("effectiveTo", None) ) new_expiry = ( - util.parse_rfc3339_date(resp_expiry) + resp_expiry if resp_expiry else cfg.machine_token_file.contract_expiry_datetime ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/__init__.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/__init__.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/__init__.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/__init__.py 2023-04-05 15:14:00.000000000 +0000 @@ -3,9 +3,12 @@ import sys from subprocess import TimeoutExpired -from uaclient import exceptions, system +from uaclient import exceptions +from uaclient import log as pro_log +from uaclient import system from uaclient.config import UAConfig -from uaclient.defaults import DEFAULT_DATA_DIR, DEFAULT_LOG_FORMAT +from uaclient.defaults import DEFAULT_DATA_DIR +from uaclient.log import JsonArrayFormatter LOG = logging.getLogger("pro.daemon") @@ -42,6 +45,7 @@ logger.setLevel(log_level) logger.handlers = [] + logger.addFilter(pro_log.RedactionFilter()) console_handler = logging.StreamHandler(sys.stderr) console_handler.setFormatter(logging.Formatter("%(message)s")) @@ -50,7 +54,7 @@ logger.addHandler(console_handler) file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(JsonArrayFormatter()) file_handler.setLevel(log_level) - file_handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) file_handler.set_name("ua-file") logger.addHandler(file_handler) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/retry_auto_attach.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/retry_auto_attach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/retry_auto_attach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/retry_auto_attach.py 2023-04-05 15:14:00.000000000 +0000 @@ -10,7 +10,7 @@ ) from uaclient.config import UAConfig from uaclient.daemon import AUTO_ATTACH_STATUS_MOTD_FILE -from uaclient.files import state_files +from uaclient.files import notices, state_files LOG = logging.getLogger("pro.daemon.retry_auto_attach") @@ -79,7 +79,12 @@ state_files.retry_auto_attach_state_file.delete() state_files.retry_auto_attach_options_file.delete() system.ensure_file_absent(AUTO_ATTACH_STATUS_MOTD_FILE) - cfg.notice_file.remove("", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX) + notices.remove( + notices.Notice.AUTO_ATTACH_RETRY_FULL_NOTICE, + ) + notices.remove( + notices.Notice.AUTO_ATTACH_RETRY_TOTAL_FAILURE, + ) def retry_auto_attach(cfg: UAConfig) -> None: @@ -129,10 +134,12 @@ cfg=cfg, lock_holder="pro.daemon.retry_auto_attach.notice_updates", ): - cfg.notice_file.remove( - "", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX + notices.add( + notices.Notice.AUTO_ATTACH_RETRY_FULL_NOTICE, + num_attempts=offset + index + 1, + reason=msg_reason, + next_run_datestring=next_attempt.isoformat(), ) - cfg.notice_file.add("", auto_attach_status_msg) except exceptions.LockHeldError: pass @@ -183,4 +190,8 @@ system.write_file( AUTO_ATTACH_STATUS_MOTD_FILE, auto_attach_status_msg + "\n\n" ) - cfg.notice_file.add("", auto_attach_status_msg) + notices.add( + notices.Notice.AUTO_ATTACH_RETRY_TOTAL_FAILURE, + num_attempts=len(RETRY_INTERVALS) + 1, + reason=msg_reason, + ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/tests/test_poll_for_pro_license.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/daemon/tests/test_poll_for_pro_license.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/daemon/tests/test_poll_for_pro_license.py 2023-04-05 15:14:00.000000000 +0000 @@ -62,7 +62,6 @@ err = Exception() m_auto_attach.side_effect = err cfg = FakeConfig() - cfg.notice_file.add = mock.MagicMock() cloud = mock.MagicMock() attempt_auto_attach(cfg, cloud) @@ -260,9 +259,7 @@ cfg = FakeConfig.for_attached_machine() else: cfg = FakeConfig() - cfg.cfg.update( - {"ua_config": {"poll_for_pro_license": cfg_poll_for_pro_licenses}} - ) + cfg.user_config.poll_for_pro_license = cfg_poll_for_pro_licenses m_is_config_value_true.return_value = is_config_value_true m_is_current_series_lts.return_value = is_current_series_lts @@ -414,14 +411,8 @@ FakeConfig, ): cfg = FakeConfig() - cfg.cfg.update( - { - "ua_config": { - "poll_for_pro_license": True, - "polling_error_retry_delay": 123, - } - } - ) + cfg.user_config.poll_for_pro_license = True + cfg.user_config.polling_error_retry_delay = 123 m_is_config_value_true.return_value = False m_is_current_series_lts.return_value = True diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/data_types.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/data_types.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/data_types.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/data_types.py 2023-04-05 15:14:00.000000000 +0000 @@ -3,51 +3,56 @@ from enum import Enum from typing import Any, List, Optional, Type, TypeVar, Union -from uaclient import exceptions, util - -INCORRECT_TYPE_ERROR_MESSAGE = ( - "Expected value with type {expected_type} but got type: {got_type}" -) -INCORRECT_LIST_ELEMENT_TYPE_ERROR_MESSAGE = ( - "Got value with incorrect type at index {index}: {nested_msg}" -) -INCORRECT_FIELD_TYPE_ERROR_MESSAGE = ( - 'Got value with incorrect type for field "{key}": {nested_msg}' -) -INCORRECT_ENUM_VALUE_ERROR_MESSAGE = ( - "Value provided was not found in {enum_class}'s allowed: value: {values}" -) +from uaclient import exceptions, messages, util class IncorrectTypeError(exceptions.UserFacingError): def __init__(self, expected_type: str, got_type: str): - super().__init__( - INCORRECT_TYPE_ERROR_MESSAGE.format( - expected_type=expected_type, got_type=got_type - ) + msg = messages.INCORRECT_TYPE_ERROR_MESSAGE.format( + expected_type=expected_type, got_type=got_type ) + super().__init__(msg.msg, msg.name) + self.expected_type = expected_type + self.got_type = got_type class IncorrectListElementTypeError(IncorrectTypeError): def __init__(self, err: IncorrectTypeError, at_index: int): - self.msg = INCORRECT_LIST_ELEMENT_TYPE_ERROR_MESSAGE.format( + msg = messages.INCORRECT_LIST_ELEMENT_TYPE_ERROR_MESSAGE.format( index=at_index, nested_msg=err.msg ) + self.msg = msg.msg + self.msg_code = msg.name + self.additional_info = None + self.expected_type = err.expected_type + self.got_type = err.got_type class IncorrectFieldTypeError(IncorrectTypeError): def __init__(self, err: IncorrectTypeError, key: str): - self.msg = INCORRECT_FIELD_TYPE_ERROR_MESSAGE.format( + msg = messages.INCORRECT_FIELD_TYPE_ERROR_MESSAGE.format( key=key, nested_msg=err.msg ) + self.msg = msg.msg + self.msg_code = msg.name + self.additional_info = None self.key = key + self.expected_type = err.expected_type + self.got_type = err.got_type class IncorrectEnumValueError(IncorrectTypeError): - def __init__(self, values: List[str], enum_class: Any): - self.msg = INCORRECT_ENUM_VALUE_ERROR_MESSAGE.format( + def __init__(self, values: List[Union[str, int]], enum_class: Any): + msg = messages.INCORRECT_ENUM_VALUE_ERROR_MESSAGE.format( values=values, enum_class=repr(enum_class) ) + self.msg = msg.msg + self.msg_code = msg.name + self.additional_info = None + self.expected_type = "one of: {}".format( + ", ".join([str(v) for v in values]) + ) + self.got_type = "" class DataValue: @@ -183,11 +188,19 @@ """ def __init__( - self, key: str, data_cls: Type[DataValue], required: bool = True + self, + key: str, + data_cls: Type[DataValue], + required: bool = True, + dict_key: Optional[str] = None, ): self.key = key self.data_cls = data_cls self.required = required + if dict_key is not None: + self.dict_key = dict_key + else: + self.dict_key = self.key T = TypeVar("T", bound="DataObject") @@ -246,7 +259,7 @@ new_val = val if new_val is not None or keep_none: - d[field.key] = new_val + d[field.dict_key] = new_val return d def to_json(self, keep_null: bool = True) -> str: @@ -257,16 +270,18 @@ ) @classmethod - def from_dict(cls: Type[T], d: dict) -> T: + def from_dict( + cls: Type[T], d: dict, optional_type_errors_become_null: bool = False + ) -> T: kwargs = {} for field in cls.fields: try: - val = d[field.key] + val = d[field.dict_key] except KeyError: if field.required: raise IncorrectFieldTypeError( IncorrectTypeError(field.data_cls.__name__, "null"), - field.key, + field.dict_key, ) else: val = None @@ -274,7 +289,21 @@ try: val = field.data_cls.from_value(val) except IncorrectTypeError as e: - raise IncorrectFieldTypeError(e, field.key) + if not field.required and optional_type_errors_become_null: + # SC-1428: we should warn here, but this currently runs + # before setup_logging() in the case of + # user-config.json. + # + # logging.warning( + # "{} is wrong type (expected {} but got {}) but " + # "considered optional - treating as null".format( + # field.key, e.expected_type, e.got_type + # ) + # ) + val = None + else: + raise IncorrectFieldTypeError(e, field.dict_key) + kwargs[field.key] = val return cls(**kwargs) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/defaults.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/defaults.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/defaults.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/defaults.py 2023-04-05 15:14:00.000000000 +0000 @@ -7,6 +7,7 @@ UAC_ETC_PATH = "/etc/ubuntu-advantage/" UAC_RUN_PATH = "/run/ubuntu-advantage/" +UAC_TMP_PATH = "/tmp/ubuntu-advantage/" DEFAULT_DATA_DIR = "/var/lib/ubuntu-advantage" MACHINE_TOKEN_FILE = "machine-token.json" PRIVATE_SUBDIR = "/private" @@ -18,15 +19,18 @@ CANDIDATE_CACHE_PATH = UAC_RUN_PATH + "candidate-version" DEFAULT_CONFIG_FILE = UAC_ETC_PATH + "uaclient.conf" DEFAULT_HELP_FILE = UAC_ETC_PATH + "help_data.yaml" +DEFAULT_USER_CONFIG_JSON_FILE = DEFAULT_DATA_DIR + "/user-config.json" DEFAULT_UPGRADE_CONTRACT_FLAG_FILE = UAC_ETC_PATH + "request-update-contract" BASE_CONTRACT_URL = "https://contracts.canonical.com" BASE_SECURITY_URL = "https://ubuntu.com/security" +BASE_LIVEPATCH_URL = "https://livepatch.canonical.com" BASE_UA_URL = "https://ubuntu.com/pro" EOL_UA_URL_TMPL = "https://ubuntu.com/{hyphenatedrelease}" BASE_ESM_URL = "https://ubuntu.com/esm" APT_NEWS_URL = "https://motd.ubuntu.com/aptnews.json" CLOUD_BUILD_INFO = "/etc/cloud/build.info" ESM_APT_ROOTDIR = DEFAULT_DATA_DIR + "/apt-esm/" +PRO_ATTACH_URL = BASE_UA_URL + "/attach" DOCUMENTATION_URL = ( "https://discourse.ubuntu.com/t/ubuntu-advantage-client/21788" @@ -46,7 +50,7 @@ "contract_url": BASE_CONTRACT_URL, "security_url": BASE_SECURITY_URL, "data_dir": DEFAULT_DATA_DIR, - "log_level": "INFO", + "log_level": "debug", "log_file": "/var/log/ubuntu-advantage.log", "timer_log_file": "/var/log/ubuntu-advantage-timer.log", "daemon_log_file": "/var/log/ubuntu-advantage-daemon.log", @@ -63,3 +67,5 @@ ROOT_READABLE_MODE = 0o600 WORLD_READABLE_MODE = 0o644 +NOTICES_PERMANENT_DIRECTORY = DEFAULT_DATA_DIR + "/notices/" +NOTICES_TEMPORARY_DIRECTORY = UAC_RUN_PATH + "notices/" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/base.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/base.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/base.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/base.py 2023-04-05 15:14:00.000000000 +0000 @@ -5,8 +5,6 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Type, Union -import yaml - from uaclient import config, contract, event_logger, messages, system, util from uaclient.defaults import DEFAULT_HELP_FILE from uaclient.entitlements.entitlement_status import ( @@ -21,6 +19,7 @@ ) from uaclient.types import MessagingOperationsDict, StaticAffordance from uaclient.util import is_config_value_true +from uaclient.yaml import safe_load event = event_logger.get_event_logger() @@ -61,6 +60,11 @@ # List of services that depend on this service _dependent_services = () # type: Tuple[Type[UAEntitlement], ...] + affordance_check_arch = True + affordance_check_series = True + affordance_check_kernel_min_version = True + affordance_check_kernel_flavor = True + @property @abc.abstractmethod def name(self) -> str: @@ -108,7 +112,7 @@ if os.path.exists(DEFAULT_HELP_FILE): with open(DEFAULT_HELP_FILE, "r") as f: - help_dict = yaml.safe_load(f) + help_dict = safe_load(f) self._help_info = help_dict.get(self.name, {}).get("help", "") @@ -171,8 +175,7 @@ @param config: Parsed configuration dictionary """ if not cfg: - root_mode = os.getuid() == 0 - cfg = config.UAConfig(root_mode=root_mode) + cfg = config.UAConfig() self.cfg = cfg self.assume_yes = assume_yes self.allow_beta = allow_beta @@ -192,105 +195,6 @@ return self._valid_service - # using Union instead of Optional here to signal that it may expand to - # support additional reason types in the future. - def enable( - self, - silent: bool = False, - ) -> Tuple[bool, Union[None, CanEnableFailure]]: - """Enable specific entitlement. - - @return: tuple of (success, optional reason) - (True, None) on success. - (False, reason) otherwise. reason is only non-None if it is a - populated CanEnableFailure reason. This may expand to - include other types of reasons in the future. - """ - - msg_ops = self.messaging.get("pre_can_enable", []) - if not util.handle_message_operations(msg_ops): - return False, None - - can_enable, fail = self.can_enable() - if not can_enable: - if fail is None: - # this shouldn't happen, but if it does we shouldn't continue - return False, None - elif fail.reason == CanEnableFailureReason.INCOMPATIBLE_SERVICE: - # Try to disable those services before proceeding with enable - incompat_ret, error = self.handle_incompatible_services() - if not incompat_ret: - fail.message = error - return False, fail - elif ( - fail.reason - == CanEnableFailureReason.INACTIVE_REQUIRED_SERVICES - ): - # Try to enable those services before proceeding with enable - req_ret, error = self._enable_required_services() - if not req_ret: - fail.message = error - return False, fail - else: - # every other reason means we can't continue - return False, fail - - msg_ops = self.messaging.get("pre_enable", []) - if not util.handle_message_operations(msg_ops): - return False, None - - ret = self._perform_enable(silent=silent) - if not ret: - return False, None - - msg_ops = self.messaging.get("post_enable", []) - if not util.handle_message_operations(msg_ops): - return False, None - - return True, None - - @abc.abstractmethod - def _perform_enable(self, silent: 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. - - @return: True on success, False otherwise. - """ - pass - - def can_disable( - self, ignore_dependent_services: bool = False - ) -> Tuple[bool, Optional[CanDisableFailure]]: - """Report whether or not disabling is possible for the entitlement. - - :return: - (True, None) if can disable - (False, CanDisableFailure) if can't disable - """ - application_status, _ = self.application_status() - - if application_status == ApplicationStatus.DISABLED: - return ( - False, - CanDisableFailure( - CanDisableFailureReason.ALREADY_DISABLED, - message=messages.ALREADY_DISABLED.format(title=self.title), - ), - ) - - if self.dependent_services and not ignore_dependent_services: - if self.detect_dependent_services(): - return ( - False, - CanDisableFailure( - CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES - ), - ) - - return True, None - def can_enable(self) -> Tuple[bool, Optional[CanEnableFailure]]: """ Report whether or not enabling is possible for the entitlement. @@ -368,6 +272,74 @@ return (True, None) + # using Union instead of Optional here to signal that it may expand to + # support additional reason types in the future. + def enable( + self, + silent: bool = False, + ) -> Tuple[bool, Union[None, CanEnableFailure]]: + """Enable specific entitlement. + + @return: tuple of (success, optional reason) + (True, None) on success. + (False, reason) otherwise. reason is only non-None if it is a + populated CanEnableFailure reason. This may expand to + include other types of reasons in the future. + """ + + msg_ops = self.messaging.get("pre_can_enable", []) + if not util.handle_message_operations(msg_ops): + return False, None + + can_enable, fail = self.can_enable() + if not can_enable: + if fail is None: + # this shouldn't happen, but if it does we shouldn't continue + return False, None + elif fail.reason == CanEnableFailureReason.INCOMPATIBLE_SERVICE: + # Try to disable those services before proceeding with enable + incompat_ret, error = self.handle_incompatible_services() + if not incompat_ret: + fail.message = error + return False, fail + elif ( + fail.reason + == CanEnableFailureReason.INACTIVE_REQUIRED_SERVICES + ): + # Try to enable those services before proceeding with enable + req_ret, error = self._enable_required_services() + if not req_ret: + fail.message = error + return False, fail + else: + # every other reason means we can't continue + return False, fail + + msg_ops = self.messaging.get("pre_enable", []) + if not util.handle_message_operations(msg_ops): + return False, None + + ret = self._perform_enable(silent=silent) + if not ret: + return False, None + + msg_ops = self.messaging.get("post_enable", []) + if not util.handle_message_operations(msg_ops): + return False, None + + return True, None + + @abc.abstractmethod + def _perform_enable(self, silent: 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. + + @return: True on success, False otherwise. + """ + pass + def detect_dependent_services(self) -> bool: """ Check for depedent services. @@ -522,100 +494,82 @@ return True, None - def applicability_status( - self, - ) -> Tuple[ApplicabilityStatus, Optional[messages.NamedMessage]]: - """Check all contract affordances to vet current platform - - Affordances are a list of support constraints for the entitlement. - Examples include a list of supported series, architectures for kernel - revisions. + def can_disable( + self, ignore_dependent_services: bool = False + ) -> Tuple[bool, Optional[CanDisableFailure]]: + """Report whether or not disabling is possible for the entitlement. :return: - tuple of (ApplicabilityStatus, NamedMessage). APPLICABLE if - platform passes all defined affordances, INAPPLICABLE if it doesn't - meet all of the provided constraints. + (True, None) if can disable + (False, CanDisableFailure) if can't disable """ - entitlement_cfg = self.cfg.machine_token_file.entitlements.get( - self.name - ) - if not entitlement_cfg: - return ( - ApplicabilityStatus.APPLICABLE, - messages.NO_ENTITLEMENT_AFFORDANCES_CHECKED, - ) - for error_message, functor, expected_result in self.static_affordances: - if functor() != expected_result: - return ApplicabilityStatus.INAPPLICABLE, error_message - affordances = entitlement_cfg["entitlement"].get("affordances", {}) - platform = system.get_platform_info() - affordance_arches = affordances.get("architectures", None) - if ( - affordance_arches is not None - and platform["arch"] not in affordance_arches - ): - deduplicated_arches = util.deduplicate_arches(affordance_arches) - return ( - ApplicabilityStatus.INAPPLICABLE, - messages.INAPPLICABLE_ARCH.format( - title=self.title, - arch=platform["arch"], - supported_arches=", ".join(deduplicated_arches), - ), - ) - affordance_series = affordances.get("series", None) - if ( - affordance_series is not None - and platform["series"] not in affordance_series - ): + application_status, _ = self.application_status() + + if application_status == ApplicationStatus.DISABLED: return ( - ApplicabilityStatus.INAPPLICABLE, - messages.INAPPLICABLE_SERIES.format( - title=self.title, series=platform["version"] + False, + CanDisableFailure( + CanDisableFailureReason.ALREADY_DISABLED, + message=messages.ALREADY_DISABLED.format(title=self.title), ), ) - kernel_info = system.get_kernel_info() - affordance_kernels = affordances.get("kernelFlavors", None) - affordance_min_kernel = affordances.get("minKernelVersion") - if affordance_kernels is not None: - if kernel_info.flavor not in affordance_kernels: + + if self.dependent_services and not ignore_dependent_services: + if self.detect_dependent_services(): return ( - ApplicabilityStatus.INAPPLICABLE, - messages.INAPPLICABLE_KERNEL.format( - title=self.title, - kernel=kernel_info.uname_release, - supported_kernels=", ".join(affordance_kernels), + False, + CanDisableFailure( + CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES ), ) - if ( - affordance_min_kernel - and kernel_info.major is not None - and kernel_info.minor is not None - ): - invalid_msg = messages.INAPPLICABLE_KERNEL_VER.format( - title=self.title, - kernel=kernel_info.uname_release, - min_kernel=affordance_min_kernel, - ) - try: - kernel_major, kernel_minor = affordance_min_kernel.split(".") - min_kern_major = int(kernel_major) - min_kern_minor = int(kernel_minor) - except ValueError: - logging.warning( - "Could not parse minKernelVersion: %s", - affordance_min_kernel, - ) - return (ApplicabilityStatus.INAPPLICABLE, invalid_msg) - if kernel_info.major < min_kern_major: - return ApplicabilityStatus.INAPPLICABLE, invalid_msg + return True, None + + def disable( + self, silent: bool = False + ) -> Tuple[bool, Optional[CanDisableFailure]]: + """Disable specific entitlement + + @param silent: Boolean set True to silence print/log of messages + + @return: tuple of (success, optional reason) + (True, None) on success. + (False, reason) otherwise. reason is only non-None if it is a + populated CanDisableFailure reason. This may expand to + include other types of reasons in the future. + """ + msg_ops = self.messaging.get("pre_disable", []) + if not util.handle_message_operations(msg_ops): + return False, None + + can_disable, fail = self.can_disable() + if not can_disable: + if fail is None: + # this shouldn't happen, but if it does we shouldn't continue + return False, None elif ( - kernel_info.major == min_kern_major - and kernel_info.minor < min_kern_minor + fail.reason + == CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES ): - return ApplicabilityStatus.INAPPLICABLE, invalid_msg - return ApplicabilityStatus.APPLICABLE, None + ret, msg = self._disable_dependent_services(silent=silent) + if not ret: + fail.message = msg + return False, fail + else: + # every other reason means we can't continue + return False, fail + + if not self._perform_disable(silent=silent): + return False, None + + msg_ops = self.messaging.get("post_disable", []) + if not util.handle_message_operations(msg_ops): + return False, None + + self._check_for_reboot_msg( + operation="disable operation", silent=silent + ) + return True, None @abc.abstractmethod def _perform_disable(self, silent: bool = False) -> bool: @@ -706,51 +660,106 @@ ) ) - def disable( - self, silent: bool = False - ) -> Tuple[bool, Optional[CanDisableFailure]]: - """Disable specific entitlement + def applicability_status( + self, + ) -> Tuple[ApplicabilityStatus, Optional[messages.NamedMessage]]: + """Check all contract affordances to vet current platform - @param silent: Boolean set True to silence print/log of messages + Affordances are a list of support constraints for the entitlement. + Examples include a list of supported series, architectures for kernel + revisions. - @return: tuple of (success, optional reason) - (True, None) on success. - (False, reason) otherwise. reason is only non-None if it is a - populated CanDisableFailure reason. This may expand to - include other types of reasons in the future. + :return: + tuple of (ApplicabilityStatus, NamedMessage). APPLICABLE if + platform passes all defined affordances, INAPPLICABLE if it doesn't + meet all of the provided constraints. """ - msg_ops = self.messaging.get("pre_disable", []) - if not util.handle_message_operations(msg_ops): - return False, None + entitlement_cfg = self.cfg.machine_token_file.entitlements.get( + self.name + ) + if not entitlement_cfg: + return ( + ApplicabilityStatus.APPLICABLE, + messages.NO_ENTITLEMENT_AFFORDANCES_CHECKED, + ) + for error_message, functor, expected_result in self.static_affordances: + if functor() != expected_result: + return ApplicabilityStatus.INAPPLICABLE, error_message + affordances = entitlement_cfg["entitlement"].get("affordances", {}) + platform = system.get_platform_info() + affordance_arches = affordances.get("architectures", None) + if ( + self.affordance_check_arch + and affordance_arches is not None + and platform["arch"] not in affordance_arches + ): + deduplicated_arches = util.deduplicate_arches(affordance_arches) + return ( + ApplicabilityStatus.INAPPLICABLE, + messages.INAPPLICABLE_ARCH.format( + title=self.title, + arch=platform["arch"], + supported_arches=", ".join(deduplicated_arches), + ), + ) + affordance_series = affordances.get("series", None) + if ( + self.affordance_check_series + and affordance_series is not None + and platform["series"] not in affordance_series + ): + return ( + ApplicabilityStatus.INAPPLICABLE, + messages.INAPPLICABLE_SERIES.format( + title=self.title, series=platform["version"] + ), + ) + kernel_info = system.get_kernel_info() + affordance_kernels = affordances.get("kernelFlavors", None) + affordance_min_kernel = affordances.get("minKernelVersion", None) + if ( + self.affordance_check_kernel_flavor + and affordance_kernels is not None + ): + if kernel_info.flavor not in affordance_kernels: + return ( + ApplicabilityStatus.INAPPLICABLE, + messages.INAPPLICABLE_KERNEL.format( + title=self.title, + kernel=kernel_info.uname_release, + supported_kernels=", ".join(affordance_kernels), + ), + ) + if ( + self.affordance_check_kernel_min_version + and affordance_min_kernel + and kernel_info.major is not None + and kernel_info.minor is not None + ): + invalid_msg = messages.INAPPLICABLE_KERNEL_VER.format( + title=self.title, + kernel=kernel_info.uname_release, + min_kernel=affordance_min_kernel, + ) + try: + kernel_major, kernel_minor = affordance_min_kernel.split(".") + min_kern_major = int(kernel_major) + min_kern_minor = int(kernel_minor) + except ValueError: + logging.warning( + "Could not parse minKernelVersion: %s", + affordance_min_kernel, + ) + return (ApplicabilityStatus.INAPPLICABLE, invalid_msg) - can_disable, fail = self.can_disable() - if not can_disable: - if fail is None: - # this shouldn't happen, but if it does we shouldn't continue - return False, None + if kernel_info.major < min_kern_major: + return ApplicabilityStatus.INAPPLICABLE, invalid_msg elif ( - fail.reason - == CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES + kernel_info.major == min_kern_major + and kernel_info.minor < min_kern_minor ): - ret, msg = self._disable_dependent_services(silent=silent) - if not ret: - fail.message = msg - return False, fail - else: - # every other reason means we can't continue - return False, fail - - if not self._perform_disable(silent=silent): - return False, None - - msg_ops = self.messaging.get("post_disable", []) - if not util.handle_message_operations(msg_ops): - return False, None - - self._check_for_reboot_msg( - operation="disable operation", silent=silent - ) - return True, None + return ApplicabilityStatus.INAPPLICABLE, invalid_msg + return ApplicabilityStatus.APPLICABLE, None def contract_status(self) -> ContractStatus: """Return whether the user is entitled to the entitlement or not""" @@ -763,6 +772,68 @@ return ContractStatus.ENTITLED return ContractStatus.UNENTITLED + def user_facing_status( + self, + ) -> Tuple[UserFacingStatus, Optional[messages.NamedMessage]]: + """Return (user-facing status, details) for entitlement""" + applicability, details = self.applicability_status() + if applicability != ApplicabilityStatus.APPLICABLE: + return UserFacingStatus.INAPPLICABLE, details + entitlement_cfg = self.cfg.machine_token_file.entitlements.get( + self.name + ) + if not entitlement_cfg: + return ( + UserFacingStatus.UNAVAILABLE, + messages.SERVICE_NOT_ENTITLED.format(title=self.title), + ) + elif entitlement_cfg["entitlement"].get("entitled", False) is False: + return ( + UserFacingStatus.UNAVAILABLE, + messages.SERVICE_NOT_ENTITLED.format(title=self.title), + ) + + application_status, explanation = self.application_status() + + if application_status == ApplicationStatus.DISABLED: + return UserFacingStatus.INACTIVE, explanation + + warning, warn_msg = self.enabled_warning_status() + + if warning: + return UserFacingStatus.WARNING, warn_msg + + return UserFacingStatus.ACTIVE, explanation + + @abc.abstractmethod + def application_status( + self, + ) -> Tuple[ApplicationStatus, Optional[messages.NamedMessage]]: + """ + The current status of application of this entitlement + + :return: + A tuple of (ApplicationStatus, human-friendly reason) + """ + pass + + def enabled_warning_status( + self, + ) -> Tuple[bool, Optional[messages.NamedMessage]]: + """ + If the entitlment is enabled, are there any warnings? + The message is displayed as a Warning Notice in status output + + :return: + A tuple of (warning bool, human-friendly reason) + """ + return False, None + + def status_description_override( + self, + ) -> Optional[str]: + return None + def is_access_expired(self) -> bool: """Return entitlement access info as stale and needing refresh.""" entitlement_contract = self.cfg.machine_token_file.entitlements.get( @@ -885,43 +956,3 @@ return True return False - - def user_facing_status( - self, - ) -> Tuple[UserFacingStatus, Optional[messages.NamedMessage]]: - """Return (user-facing status, details) for entitlement""" - applicability, details = self.applicability_status() - if applicability != ApplicabilityStatus.APPLICABLE: - return UserFacingStatus.INAPPLICABLE, details - entitlement_cfg = self.cfg.machine_token_file.entitlements.get( - self.name - ) - if not entitlement_cfg: - return ( - UserFacingStatus.UNAVAILABLE, - messages.SERVICE_NOT_ENTITLED.format(title=self.title), - ) - elif entitlement_cfg["entitlement"].get("entitled", False) is False: - return ( - UserFacingStatus.UNAVAILABLE, - messages.SERVICE_NOT_ENTITLED.format(title=self.title), - ) - - application_status, explanation = self.application_status() - user_facing_status = { - ApplicationStatus.ENABLED: UserFacingStatus.ACTIVE, - ApplicationStatus.DISABLED: UserFacingStatus.INACTIVE, - }[application_status] - return user_facing_status, explanation - - @abc.abstractmethod - def application_status( - self, - ) -> Tuple[ApplicationStatus, Optional[messages.NamedMessage]]: - """ - The current status of application of this entitlement - - :return: - A tuple of (ApplicationStatus, human-friendly reason) - """ - pass diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/entitlement_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/entitlement_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/entitlement_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/entitlement_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -80,6 +80,7 @@ INACTIVE = "disabled" INAPPLICABLE = "n/a" UNAVAILABLE = "—" + WARNING = "warning" @enum.unique diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/esm.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/esm.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/esm.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/esm.py 2023-04-05 15:14:00.000000000 +0000 @@ -22,11 +22,11 @@ return (ROSEntitlement, ROSUpdatesEntitlement) def _perform_enable(self, silent: bool = False) -> bool: - from uaclient.jobs.update_messaging import update_apt_and_motd_messages + from uaclient.jobs.update_messaging import update_motd_messages enable_performed = super()._perform_enable(silent=silent) if enable_performed: - update_apt_and_motd_messages(self.cfg) + update_motd_messages(self.cfg) self.disable_local_esm_repo() return enable_performed @@ -78,11 +78,11 @@ def disable( self, silent=False ) -> Tuple[bool, Union[None, CanDisableFailure]]: - from uaclient.jobs.update_messaging import update_apt_and_motd_messages + from uaclient.jobs.update_messaging import update_motd_messages disable_performed, fail = super().disable(silent=silent) if disable_performed: - update_apt_and_motd_messages(self.cfg) + update_motd_messages(self.cfg) if system.is_current_series_lts(): self.setup_local_esm_repo() return disable_performed, fail @@ -98,11 +98,11 @@ def disable( self, silent=False ) -> Tuple[bool, Union[None, CanDisableFailure]]: - from uaclient.jobs.update_messaging import update_apt_and_motd_messages + from uaclient.jobs.update_messaging import update_motd_messages disable_performed, fail = super().disable(silent=silent) if disable_performed: - update_apt_and_motd_messages(self.cfg) + update_motd_messages(self.cfg) if system.is_current_series_active_esm(): self.setup_local_esm_repo() return disable_performed, fail diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/fips.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/fips.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/fips.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/fips.py 2023-04-05 15:14:00.000000000 +0000 @@ -8,6 +8,8 @@ from uaclient.entitlements import repo from uaclient.entitlements.base import IncompatibleService from uaclient.entitlements.entitlement_status import ApplicationStatus +from uaclient.files import notices +from uaclient.files.notices import Notice from uaclient.files.state_files import ( ServicesOnceEnabledData, services_once_enabled_file, @@ -198,12 +200,12 @@ ) ) if operation == "install": - self.cfg.notice_file.add( - "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg + notices.add( + Notice.FIPS_SYSTEM_REBOOT_REQUIRED, ) elif operation == "disable operation": - self.cfg.notice_file.add( - "", messages.FIPS_DISABLE_REBOOT_REQUIRED + notices.add( + Notice.FIPS_DISABLE_REBOOT_REQUIRED, ) def _allow_fips_on_cloud_instance( @@ -268,8 +270,8 @@ super_status, super_msg = super().application_status() if system.is_container() and not system.should_reboot(): - self.cfg.notice_file.try_remove( - "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg + notices.remove( + Notice.FIPS_SYSTEM_REBOOT_REQUIRED, ) return super_status, super_msg @@ -278,24 +280,18 @@ # We are now only removing the notice if there is no reboot # required information regarding the fips metapackage we install. if not system.should_reboot(set(self.packages)): - self.cfg.notice_file.try_remove( - "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg + notices.remove( + Notice.FIPS_SYSTEM_REBOOT_REQUIRED, ) - self.cfg.notice_file.try_remove( - "", messages.FIPS_REBOOT_REQUIRED_MSG - ) if system.load_file(self.FIPS_PROC_FILE).strip() == "1": - self.cfg.notice_file.try_remove( - "", messages.NOTICE_FIPS_MANUAL_DISABLE_URL + notices.remove( + Notice.FIPS_MANUAL_DISABLE_URL, ) return super_status, super_msg else: - self.cfg.notice_file.try_remove( - "", messages.FIPS_DISABLE_REBOOT_REQUIRED - ) - self.cfg.notice_file.try_add( - "", messages.NOTICE_FIPS_MANUAL_DISABLE_URL + notices.add( + Notice.FIPS_MANUAL_DISABLE_URL, ) return ( ApplicationStatus.DISABLED, @@ -303,10 +299,6 @@ file_name=self.FIPS_PROC_FILE ), ) - else: - self.cfg.notice_file.try_remove( - "", messages.FIPS_DISABLE_REBOOT_REQUIRED - ) if super_status != ApplicationStatus.ENABLED: return super_status, super_msg @@ -327,27 +319,18 @@ ) remove_packages = fips_metapackage.intersection(installed_packages) if remove_packages: - env = {"DEBIAN_FRONTEND": "noninteractive"} - apt_options = [ - '-o Dpkg::Options::="--force-confdef"', - '-o Dpkg::Options::="--force-confold"', - ] - apt.run_apt_command( - ["apt-get", "remove", "--assume-yes"] - + apt_options - + list(remove_packages), + apt.remove_packages( + list(fips_metapackage), messages.DISABLE_FAILED_TMPL.format(title=self.title), - env=env, ) def _perform_enable(self, silent: bool = False) -> bool: if super()._perform_enable(silent=silent): - self.cfg.notice_file.try_remove( - "", messages.NOTICE_WRONG_FIPS_METAPACKAGE_ON_CLOUD - ) - self.cfg.notice_file.try_remove( - "", messages.FIPS_REBOOT_REQUIRED_MSG + notices.remove( + Notice.WRONG_FIPS_METAPACKAGE_ON_CLOUD, ) + notices.remove(Notice.FIPS_REBOOT_REQUIRED) + notices.remove(Notice.FIPS_DISABLE_REBOOT_REQUIRED) return True return False @@ -472,8 +455,8 @@ "defaulting to generic FIPS package." ) if super()._perform_enable(silent=silent): - self.cfg.notice_file.try_remove( - "", messages.FIPS_INSTALL_OUT_OF_DATE + notices.remove( + Notice.FIPS_INSTALL_OUT_OF_DATE, ) return True @@ -535,8 +518,12 @@ def _perform_enable(self, silent: bool = False) -> bool: if super()._perform_enable(silent=silent): - self.cfg.notice_file.try_remove( - "", messages.FIPS_DISABLE_REBOOT_REQUIRED + services_once_enabled = ( + self.cfg.read_cache("services-once-enabled") or {} + ) + services_once_enabled.update({self.name: True}) + self.cfg.write_cache( + key="services-once-enabled", content=services_once_enabled ) services_once_enabled_file.write( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/livepatch.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/livepatch.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/livepatch.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/livepatch.py 2023-04-06 13:50:05.000000000 +0000 @@ -1,11 +1,12 @@ import logging import re -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Optional, Tuple from uaclient import ( apt, event_logger, exceptions, + livepatch, messages, snap, system, @@ -16,95 +17,28 @@ from uaclient.types import StaticAffordance LIVEPATCH_RETRIES = [0.5, 1.0] -HTTP_PROXY_OPTION = "http-proxy" -HTTPS_PROXY_OPTION = "https-proxy" ERROR_MSG_MAP = { "Unknown Auth-Token": "Invalid Auth-Token provided to livepatch.", "unsupported kernel": "Your running kernel is not supported by Livepatch.", } -LIVEPATCH_CMD = "/snap/bin/canonical-livepatch" - event = event_logger.get_event_logger() -def unconfigure_livepatch_proxy( - protocol_type: str, retry_sleeps: Optional[List[float]] = None -) -> None: - """ - Unset livepatch configuration settings for http and https proxies. - - :param protocol_type: String either http or https - :param retry_sleeps: Optional list of sleep lengths to apply between - retries. Specifying a list of [0.5, 1] tells subp to retry twice - on failure; sleeping half a second before the first retry and 1 second - before the second retry. - """ - if not system.which(LIVEPATCH_CMD): - return - system.subp( - [LIVEPATCH_CMD, "config", "{}-proxy=".format(protocol_type)], - retry_sleeps=retry_sleeps, - ) - - -def configure_livepatch_proxy( - http_proxy: Optional[str] = None, - https_proxy: Optional[str] = None, - retry_sleeps: Optional[List[float]] = None, -) -> None: - """ - Configure livepatch to use http and https proxies. - - :param http_proxy: http proxy to be used by livepatch. If None, it will - not be configured - :param https_proxy: https proxy to be used by livepatch. If None, it will - not be configured - :@param retry_sleeps: Optional list of sleep lengths to apply between - snap calls - """ - if http_proxy or https_proxy: - event.info( - messages.SETTING_SERVICE_PROXY.format( - service=LivepatchEntitlement.title - ) - ) - - if http_proxy: - system.subp( - [LIVEPATCH_CMD, "config", "http-proxy={}".format(http_proxy)], - retry_sleeps=retry_sleeps, - ) - - if https_proxy: - system.subp( - [LIVEPATCH_CMD, "config", "https-proxy={}".format(https_proxy)], - retry_sleeps=retry_sleeps, - ) - - -def get_config_option_value(key: str) -> Optional[str]: - """ - Gets the config value from livepatch. - :param protocol: can be any valid livepatch config option - :return: the value of the livepatch config option, or None if not set - """ - out, _ = system.subp([LIVEPATCH_CMD, "config"]) - match = re.search("^{}: (.*)$".format(key), out, re.MULTILINE) - value = match.group(1) if match else None - if value: - # remove quotes if present - value = re.sub(r"\"(.*)\"", r"\g<1>", value) - return value.strip() if value else None - - class LivepatchEntitlement(UAEntitlement): help_doc_url = "https://ubuntu.com/security/livepatch" name = "livepatch" title = "Livepatch" description = "Canonical Livepatch service" + affordance_check_kernel_min_version = False + affordance_check_kernel_flavor = False + # we do want to check series because livepatch errors on non-lts releases + affordance_check_series = True + # we still need to check arch because the livepatch-client is not built + # for all arches + affordance_check_arch = True @property def incompatible_services(self) -> Tuple[IncompatibleService, ...]: @@ -196,7 +130,7 @@ retry_sleeps=snap.SNAP_INSTALL_RETRIES, ) - if not system.which(LIVEPATCH_CMD): + if not livepatch.is_livepatch_installed(): event.info("Installing canonical-livepatch snap") try: system.subp( @@ -207,7 +141,7 @@ except exceptions.ProcessExecutionError as e: raise exceptions.ErrorInstallingLivepatch(error_msg=str(e)) - configure_livepatch_proxy(http_proxy, https_proxy) + livepatch.configure_livepatch_proxy(http_proxy, https_proxy) return self.setup_livepatch_config( process_directives=True, process_token=True @@ -250,13 +184,14 @@ self.title, ) try: - system.subp([LIVEPATCH_CMD, "disable"]) + system.subp([livepatch.LIVEPATCH_CMD, "disable"]) except exceptions.ProcessExecutionError as e: logging.error(str(e)) return False try: system.subp( - [LIVEPATCH_CMD, "enable", livepatch_token], capture=True + [livepatch.LIVEPATCH_CMD, "enable", livepatch_token], + capture=True, ) except exceptions.ProcessExecutionError as e: msg = "Unable to enable Livepatch: " @@ -276,9 +211,9 @@ @return: True on success, False otherwise. """ - if not system.which(LIVEPATCH_CMD): + if not livepatch.is_livepatch_installed(): return True - system.subp([LIVEPATCH_CMD, "disable"], capture=True) + system.subp([livepatch.LIVEPATCH_CMD, "disable"], capture=True) return True def application_status( @@ -286,12 +221,13 @@ ) -> Tuple[ApplicationStatus, Optional[messages.NamedMessage]]: status = (ApplicationStatus.ENABLED, None) - if not system.which(LIVEPATCH_CMD): + if not livepatch.is_livepatch_installed(): return (ApplicationStatus.DISABLED, messages.LIVEPATCH_NOT_ENABLED) try: system.subp( - [LIVEPATCH_CMD, "status"], retry_sleeps=LIVEPATCH_RETRIES + [livepatch.LIVEPATCH_CMD, "status"], + retry_sleeps=LIVEPATCH_RETRIES, ) except exceptions.ProcessExecutionError as e: # TODO(May want to parse INACTIVE/failure assessment) @@ -302,6 +238,30 @@ ) return status + def enabled_warning_status( + self, + ) -> Tuple[bool, Optional[messages.NamedMessage]]: + if livepatch.on_supported_kernel() is False: + kernel_info = system.get_kernel_info() + arch = system.get_dpkg_arch() + return ( + True, + messages.LIVEPATCH_KERNEL_NOT_SUPPORTED.format( + version=kernel_info.uname_release, arch=arch + ), + ) + # if on_supported_kernel returns None we default to no warning + # because there would be no way for a user to resolve the warning + return False, None + + def status_description_override(self): + if ( + livepatch.on_supported_kernel() is False + and not system.is_container() + ): + return messages.LIVEPATCH_KERNEL_NOT_SUPPORTED_DESCRIPTION + return None + def process_contract_deltas( self, orig_access: Dict[str, Any], @@ -366,7 +326,11 @@ ca_certs = directives.get("caCerts") if ca_certs: system.subp( - [LIVEPATCH_CMD, "config", "ca-certs={}".format(ca_certs)], + [ + livepatch.LIVEPATCH_CMD, + "config", + "ca-certs={}".format(ca_certs), + ], capture=True, ) remote_server = directives.get("remoteServer", "") @@ -375,7 +339,7 @@ if remote_server: system.subp( [ - LIVEPATCH_CMD, + livepatch.LIVEPATCH_CMD, "config", "remote-server={}".format(remote_server), ], diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/realtime.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/realtime.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/realtime.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/realtime.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,6 +1,6 @@ from typing import Optional, Tuple # noqa: F401 -from uaclient import event_logger, messages, system, util +from uaclient import apt, event_logger, messages, system, util from uaclient.entitlements import repo from uaclient.entitlements.base import IncompatibleService from uaclient.types import ( # noqa: F401 @@ -91,3 +91,13 @@ ) ], } + + def remove_packages(self) -> None: + packages = set(self.packages).intersection( + set(apt.get_installed_packages_names()) + ) + if packages: + apt.remove_packages( + list(packages), + messages.DISABLE_FAILED_TMPL.format(title=self.title), + ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/repo.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/repo.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/repo.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/repo.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,8 +1,8 @@ import abc import copy import logging -import os import re +from os.path import exists from typing import Any, Dict, List, Optional, Tuple, Union from uaclient import ( @@ -93,12 +93,8 @@ def _perform_disable(self, silent=False): if hasattr(self, "remove_packages"): self.remove_packages() - self._cleanup(silent=silent) - return True - - def _cleanup(self, silent: bool = False) -> None: - """Clean up the entitlement without checks or messaging""" self.remove_apt_config(silent=silent) + return True def application_status( self, @@ -264,7 +260,7 @@ ) except exceptions.UserFacingError: if cleanup_on_failure: - self._cleanup() + self.remove_apt_config() raise def setup_apt_config(self, silent: bool = False) -> None: @@ -363,9 +359,9 @@ ) prerequisite_pkgs = [] - if not os.path.exists(apt.APT_METHOD_HTTPS_FILE): + if not exists(apt.APT_METHOD_HTTPS_FILE): prerequisite_pkgs.append("apt-transport-https") - if not os.path.exists(apt.CA_CERTIFICATES_FILE): + if not exists(apt.CA_CERTIFICATES_FILE): prerequisite_pkgs.append("ca-certificates") if prerequisite_pkgs: diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_base.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_base.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_base.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_base.py 2023-04-05 15:14:00.000000000 +0000 @@ -77,7 +77,7 @@ def concrete_entitlement_factory(FakeConfig): def factory( *, - entitled: bool, + entitled: bool = True, applicability_status: Tuple[ApplicabilityStatus, str] = ( ApplicabilityStatus.APPLICABLE, "", @@ -153,84 +153,43 @@ entitlement = ConcreteTestEntitlement(cfg) assert "/some/path" == entitlement.cfg.data_dir - def test_can_disable_false_on_entitlement_inactive( - self, concrete_entitlement_factory - ): - """When status is INACTIVE, can_disable returns False.""" - entitlement = concrete_entitlement_factory( - entitled=True, - application_status=(ApplicationStatus.DISABLED, ""), - ) - - ret, fail = entitlement.can_disable() - assert not ret - - expected_msg = ( - "Test Concrete Entitlement is not currently enabled\n" - "See: sudo pro status" - ) - assert expected_msg == fail.message.msg - - def test_can_disable_false_on_dependent_service( - self, concrete_entitlement_factory - ): - """When status is INACTIVE, can_disable returns False.""" - m_ent_cls = mock.Mock() - type(m_ent_cls).name = mock.PropertyMock(return_value="test") - m_ent_obj = m_ent_cls.return_value - m_ent_obj.application_status.return_value = ( - ApplicationStatus.ENABLED, - None, - ) - - entitlement = concrete_entitlement_factory( - entitled=True, - application_status=(ApplicationStatus.ENABLED, ""), - dependent_services=(m_ent_cls,), - ) - - ret, fail = entitlement.can_disable() - assert not ret - assert fail.reason == CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES - assert fail.message is None - - @mock.patch("uaclient.entitlements.entitlement_factory") - def test_can_disable_when_ignoring_dependent_service( - self, m_ent_factory, concrete_entitlement_factory - ): - """When status is INACTIVE, can_disable returns False.""" - entitlement = concrete_entitlement_factory( - entitled=True, - application_status=(ApplicationStatus.ENABLED, ""), - dependent_services=("test",), - ) - - m_ent_cls = mock.Mock() - m_ent_obj = m_ent_cls.return_value - m_ent_obj.application_status.return_value = ( - ApplicationStatus.ENABLED, - None, - ) - m_ent_factory.return_value = m_ent_cls - - ret, fail = entitlement.can_disable(ignore_dependent_services=True) - assert ret is True - assert fail is None - def test_can_disable_true_on_entitlement_active( - self, capsys, concrete_entitlement_factory +class TestUaEntitlementNames: + @pytest.mark.parametrize( + "p_name,expected", + ( + ("pretty_name", ["testconcreteentitlement", "pretty_name"]), + ("testconcreteentitlement", ["testconcreteentitlement"]), + ), + ) + @mock.patch( + "uaclient.entitlements.base.UAEntitlement.presentation_name", + new_callable=mock.PropertyMock, + ) + def test_valid_names( + self, m_p_name, p_name, expected, concrete_entitlement_factory ): - """When entitlement is ENABLED, can_disable returns True.""" - entitlement = concrete_entitlement_factory( - entitled=True, - application_status=(ApplicationStatus.ENABLED, ""), - ) + m_p_name.return_value = p_name + entitlement = concrete_entitlement_factory(entitled=True) + assert expected == entitlement.valid_names - assert entitlement.can_disable() + def test_presentation_name(self, concrete_entitlement_factory): + entitlement = concrete_entitlement_factory(entitled=True) + assert "testconcreteentitlement" == entitlement.presentation_name + m_entitlements = { + "testconcreteentitlement": { + "entitlement": { + "affordances": {"presentedAs": "something_else"} + } + } + } + with mock.patch( + "uaclient.files.MachineTokenFile.entitlements", m_entitlements + ): + assert "something_else" == entitlement.presentation_name - stdout, _ = capsys.readouterr() - assert "" == stdout +class TestUaEntitlementCanEnable: def test_can_enable_false_on_unentitled( self, concrete_entitlement_factory ): @@ -369,33 +328,6 @@ assert reason.reason == CanEnableFailureReason.IS_BETA assert reason.message is None - def test_contract_status_entitled(self, concrete_entitlement_factory): - """The contract_status returns ENTITLED when entitlement enabled.""" - entitlement = concrete_entitlement_factory(entitled=True) - assert ContractStatus.ENTITLED == entitlement.contract_status() - - def test_contract_status_unentitled(self, concrete_entitlement_factory): - """The contract_status returns NONE when entitlement is unentitled.""" - entitlement = concrete_entitlement_factory(entitled=False) - assert ContractStatus.UNENTITLED == entitlement.contract_status() - - @pytest.mark.parametrize( - "orig_access,delta", - (({}, {}), ({}, {"entitlement": {"entitled": False}})), - ) - def test_process_contract_deltas_does_nothing_on_empty_orig_access( - self, concrete_entitlement_factory, orig_access, delta - ): - """When orig_acccess dict is empty perform no work.""" - entitlement = concrete_entitlement_factory( - entitled=True, - applicability_status=(ApplicabilityStatus.APPLICABLE, ""), - application_status=(ApplicationStatus.DISABLED, ""), - ) - with mock.patch.object(entitlement, "can_disable") as m_can_disable: - entitlement.process_contract_deltas(orig_access, delta) - assert 0 == m_can_disable.call_count - def test_can_enable_when_incompatible_service_found( self, concrete_entitlement_factory ): @@ -449,6 +381,8 @@ ) assert reason.message is None + +class TestUaEntitlementEnable: @pytest.mark.parametrize( "block_disable_on_enable,assume_yes", [(False, False), (False, True), (True, False), (True, True)], @@ -681,142 +615,126 @@ assert 1 == m_can_enable.call_count @pytest.mark.parametrize( - "orig_access,delta", + [ + "msg_ops_results", + "can_enable_call_count", + "perform_enable_call_count", + "expected_result", + ], ( - ({"entitlement": {"entitled": True}}, {}), # no deltas - ( - {"entitlement": {"entitled": False}}, - {"entitlement": {"entitled": True}}, - ), # transition to entitled - ( - {"entitlement": {"entitled": False}}, - { - "entitlement": { - "entitled": False, # overridden by series 'example' - "series": {"example": {"entitled": True}}, - } - }, - ), + ([False], 0, 0, (False, None)), + ([True, False], 1, 0, (False, None)), + ([True, True, False], 1, 1, (False, None)), + ([True, True, True], 1, 1, (True, None)), ), ) + @mock.patch.object( + ConcreteTestEntitlement, "_perform_enable", return_value=True + ) @mock.patch( - "uaclient.system.get_platform_info", return_value={"series": "example"} + "uaclient.entitlements.base.UAEntitlement.can_enable", + return_value=(True, None), ) - def test_process_contract_deltas_does_nothing_when_delta_remains_entitled( - self, m_platform_info, concrete_entitlement_factory, orig_access, delta + @mock.patch("uaclient.util.handle_message_operations") + def test_enable_when_messaging_hooks_fail( + self, + m_handle_messaging_hooks, + m_can_enable, + m_perform_enable, + msg_ops_results, + can_enable_call_count, + perform_enable_call_count, + expected_result, + concrete_entitlement_factory, ): - """If deltas do not represent transition to unentitled, do nothing.""" + m_handle_messaging_hooks.side_effect = msg_ops_results + entitlement = concrete_entitlement_factory() + assert expected_result == entitlement.enable() + assert can_enable_call_count == m_can_enable.call_count + assert perform_enable_call_count == m_perform_enable.call_count + + +class TestUaEntitlementCanDisable: + def test_can_disable_false_on_entitlement_inactive( + self, concrete_entitlement_factory + ): + """When status is INACTIVE, can_disable returns False.""" entitlement = concrete_entitlement_factory( entitled=True, - applicability_status=(ApplicabilityStatus.APPLICABLE, ""), application_status=(ApplicationStatus.DISABLED, ""), ) - entitlement.process_contract_deltas(orig_access, delta) - assert ( - ApplicationStatus.DISABLED, - mock.ANY, - ) == entitlement.application_status() - @pytest.mark.parametrize( - "orig_access,delta", - ( - ( - { - "entitlement": {"entitled": True} - }, # Full entitlement dropped - {"entitlement": {"entitled": util.DROPPED_KEY}}, - ), - ( - {"entitlement": {"entitled": True}}, - {"entitlement": {"entitled": False}}, - ), # transition to unentitled - ), - ) - def test_process_contract_deltas_clean_cache_on_inactive_unentitled( - self, concrete_entitlement_factory, orig_access, delta, caplog_text + ret, fail = entitlement.can_disable() + assert not ret + + expected_msg = ( + "Test Concrete Entitlement is not currently enabled\n" + "See: sudo pro status" + ) + assert expected_msg == fail.message.msg + + def test_can_disable_false_on_dependent_service( + self, concrete_entitlement_factory ): - """Only clear cache when deltas transition inactive to unentitled.""" + """When status is INACTIVE, can_disable returns False.""" + m_ent_cls = mock.Mock() + type(m_ent_cls).name = mock.PropertyMock(return_value="test") + m_ent_obj = m_ent_cls.return_value + m_ent_obj.application_status.return_value = ( + ApplicationStatus.ENABLED, + None, + ) + entitlement = concrete_entitlement_factory( entitled=True, - application_status=(ApplicationStatus.DISABLED, ""), + application_status=(ApplicationStatus.ENABLED, ""), + dependent_services=(m_ent_cls,), ) - entitlement.process_contract_deltas(orig_access, delta) - # If an entitlement is disabled, we don't need to tell the user - # anything about it becoming unentitled - # (FIXME: Something on bionic means that DEBUG log lines are being - # picked up by caplog_text(), so work around that here) - assert [] == [ - line for line in caplog_text().splitlines() if "DEBUG" not in line - ] - @pytest.mark.parametrize( - "orig_access,delta", - ( - ( - { - "entitlement": {"entitled": True} - }, # Full entitlement dropped - {"entitlement": {"entitled": util.DROPPED_KEY}}, - ), - ( - {"entitlement": {"entitled": True}}, - {"entitlement": {"entitled": False}}, - ), # transition to unentitled - ), - ) - def test_process_contract_deltas_disable_on_active_unentitled( - self, concrete_entitlement_factory, orig_access, delta + ret, fail = entitlement.can_disable() + assert not ret + assert fail.reason == CanDisableFailureReason.ACTIVE_DEPENDENT_SERVICES + assert fail.message is None + + @mock.patch("uaclient.entitlements.entitlement_factory") + def test_can_disable_when_ignoring_dependent_service( + self, m_ent_factory, concrete_entitlement_factory ): - """Disable when deltas transition from active to unentitled.""" + """When status is INACTIVE, can_disable returns False.""" entitlement = concrete_entitlement_factory( entitled=True, application_status=(ApplicationStatus.ENABLED, ""), + dependent_services=("test",), ) - entitlement.process_contract_deltas(orig_access, delta) - assert ( - ApplicationStatus.DISABLED, - 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 + m_ent_cls = mock.Mock() + m_ent_obj = m_ent_cls.return_value + m_ent_obj.application_status.return_value = ( + ApplicationStatus.ENABLED, + None, + ) + m_ent_factory.return_value = m_ent_cls + + ret, fail = entitlement.can_disable(ignore_dependent_services=True) + assert ret is True + assert fail is None + + def test_can_disable_true_on_entitlement_active( + self, capsys, concrete_entitlement_factory ): - """Disable when deltas transition from active to unentitled.""" + """When entitlement is ENABLED, can_disable returns True.""" entitlement = concrete_entitlement_factory( entitled=True, - applicability_status=(ApplicabilityStatus.APPLICABLE, ""), - application_status=(ApplicationStatus.DISABLED, ""), + application_status=(ApplicationStatus.ENABLED, ""), ) - 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 + assert entitlement.can_disable() + + stdout, _ = capsys.readouterr() + assert "" == stdout + +class TestUaEntitlementDisable: @mock.patch("uaclient.util.prompt_for_confirmation") def test_disable_when_dependent_service_found( self, m_prompt, concrete_entitlement_factory @@ -902,38 +820,17 @@ assert expected_msg == fail.message.msg assert 1 == m_can_disable.call_count - @pytest.mark.parametrize( - "p_name,expected", - ( - ("pretty_name", ["testconcreteentitlement", "pretty_name"]), - ("testconcreteentitlement", ["testconcreteentitlement"]), - ), - ) - @mock.patch( - "uaclient.entitlements.base.UAEntitlement.presentation_name", - new_callable=mock.PropertyMock, - ) - def test_valid_names( - self, m_p_name, p_name, expected, concrete_entitlement_factory - ): - m_p_name.return_value = p_name - entitlement = concrete_entitlement_factory(entitled=True) - assert expected == entitlement.valid_names - def test_presentation_name(self, concrete_entitlement_factory): +class TestUaEntitlementContractStatus: + def test_contract_status_entitled(self, concrete_entitlement_factory): + """The contract_status returns ENTITLED when entitlement enabled.""" entitlement = concrete_entitlement_factory(entitled=True) - assert "testconcreteentitlement" == entitlement.presentation_name - m_entitlements = { - "testconcreteentitlement": { - "entitlement": { - "affordances": {"presentedAs": "something_else"} - } - } - } - with mock.patch( - "uaclient.files.MachineTokenFile.entitlements", m_entitlements - ): - assert "something_else" == entitlement.presentation_name + assert ContractStatus.ENTITLED == entitlement.contract_status() + + def test_contract_status_unentitled(self, concrete_entitlement_factory): + """The contract_status returns NONE when entitlement is unentitled.""" + entitlement = concrete_entitlement_factory(entitled=False) + assert ContractStatus.UNENTITLED == entitlement.contract_status() class TestUaEntitlementUserFacingStatus: @@ -1008,3 +905,159 @@ user_facing_status, details = entitlement.user_facing_status() assert expected_uf_status == user_facing_status assert msg == details + + +class TestUaEntitlementProcessContractDeltas: + @pytest.mark.parametrize( + "orig_access,delta", + (({}, {}), ({}, {"entitlement": {"entitled": False}})), + ) + def test_process_contract_deltas_does_nothing_on_empty_orig_access( + self, concrete_entitlement_factory, orig_access, delta + ): + """When orig_acccess dict is empty perform no work.""" + entitlement = concrete_entitlement_factory( + entitled=True, + applicability_status=(ApplicabilityStatus.APPLICABLE, ""), + application_status=(ApplicationStatus.DISABLED, ""), + ) + with mock.patch.object(entitlement, "can_disable") as m_can_disable: + entitlement.process_contract_deltas(orig_access, delta) + assert 0 == m_can_disable.call_count + + @pytest.mark.parametrize( + "orig_access,delta", + ( + ({"entitlement": {"entitled": True}}, {}), # no deltas + ( + {"entitlement": {"entitled": False}}, + {"entitlement": {"entitled": True}}, + ), # transition to entitled + ( + {"entitlement": {"entitled": False}}, + { + "entitlement": { + "entitled": False, # overridden by series 'example' + "series": {"example": {"entitled": True}}, + } + }, + ), + ), + ) + @mock.patch( + "uaclient.system.get_platform_info", return_value={"series": "example"} + ) + def test_process_contract_deltas_does_nothing_when_delta_remains_entitled( + self, m_platform_info, concrete_entitlement_factory, orig_access, delta + ): + """If deltas do not represent transition to unentitled, do nothing.""" + entitlement = concrete_entitlement_factory( + entitled=True, + applicability_status=(ApplicabilityStatus.APPLICABLE, ""), + application_status=(ApplicationStatus.DISABLED, ""), + ) + entitlement.process_contract_deltas(orig_access, delta) + assert ( + ApplicationStatus.DISABLED, + mock.ANY, + ) == entitlement.application_status() + + @pytest.mark.parametrize( + "orig_access,delta", + ( + ( + { + "entitlement": {"entitled": True} + }, # Full entitlement dropped + {"entitlement": {"entitled": util.DROPPED_KEY}}, + ), + ( + {"entitlement": {"entitled": True}}, + {"entitlement": {"entitled": False}}, + ), # transition to unentitled + ), + ) + def test_process_contract_deltas_clean_cache_on_inactive_unentitled( + self, concrete_entitlement_factory, orig_access, delta, caplog_text + ): + """Only clear cache when deltas transition inactive to unentitled.""" + entitlement = concrete_entitlement_factory( + entitled=True, + application_status=(ApplicationStatus.DISABLED, ""), + ) + entitlement.process_contract_deltas(orig_access, delta) + # If an entitlement is disabled, we don't need to tell the user + # anything about it becoming unentitled + # (FIXME: Something on bionic means that DEBUG log lines are being + # picked up by caplog_text(), so work around that here) + assert [] == [ + line for line in caplog_text().splitlines() if "DEBUG" not in line + ] + + @pytest.mark.parametrize( + "orig_access,delta", + ( + ( + { + "entitlement": {"entitled": True} + }, # Full entitlement dropped + {"entitlement": {"entitled": util.DROPPED_KEY}}, + ), + ( + {"entitlement": {"entitled": True}}, + {"entitlement": {"entitled": False}}, + ), # transition to unentitled + ), + ) + def test_process_contract_deltas_disable_on_active_unentitled( + self, concrete_entitlement_factory, orig_access, delta + ): + """Disable when deltas transition from active to unentitled.""" + entitlement = concrete_entitlement_factory( + entitled=True, + application_status=(ApplicationStatus.ENABLED, ""), + ) + entitlement.process_contract_deltas(orig_access, delta) + assert ( + ApplicationStatus.DISABLED, + 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=(ApplicabilityStatus.APPLICABLE, ""), + application_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 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_cc.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_cc.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_cc.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_cc.py 2023-04-05 15:14:00.000000000 +0000 @@ -61,12 +61,10 @@ ), ), ) - @mock.patch(M_REPOPATH + "os.getuid", return_value=0) @mock.patch("uaclient.system.get_platform_info") def test_inapplicable_on_invalid_affordances( self, m_platform_info, - m_getuid, arch, series, version, @@ -166,9 +164,7 @@ 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: - with mock.patch( - M_REPOPATH + "os.path.exists", side_effect=exists - ): + with mock.patch(M_REPOPATH + "exists", side_effect=exists): assert (True, None) == entitlement.enable() add_apt_calls = [ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_cis.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_cis.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_cis.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_cis.py 2023-04-05 15:14:00.000000000 +0000 @@ -65,9 +65,7 @@ m_apt_policy.return_value = "fakeout" m_should_reboot.return_value = False - with mock.patch( - M_REPOPATH + "os.path.exists", mock.Mock(return_value=True) - ): + with mock.patch(M_REPOPATH + "exists", mock.Mock(return_value=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: assert entitlement.enable() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_esm.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_esm.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_esm.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_esm.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,10 +1,9 @@ -import contextlib import os.path import mock import pytest -from uaclient import apt, exceptions +from uaclient import apt from uaclient.entitlements.esm import ESMAppsEntitlement, ESMInfraEntitlement M_PATH = "uaclient.entitlements.esm.ESMInfraEntitlement." @@ -17,156 +16,7 @@ return entitlement_factory(request.param, suites=["xenial"]) -@mock.patch("uaclient.system.is_lts", return_value=True) -@mock.patch("uaclient.util.validate_proxy", side_effect=lambda x, y, z: y) -@mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") -@mock.patch("uaclient.apt.setup_apt_proxy") -class TestESMEntitlementEnable: - @pytest.mark.parametrize( - "esm_cls", [ESMAppsEntitlement, ESMInfraEntitlement] - ) - @pytest.mark.parametrize("apt_error", (True, False)) - def test_enable_configures_apt_sources_and_auth_files( - self, - m_setup_apt_proxy, - m_update_apt_and_motd_msgs, - m_validate_proxy, - _m_is_lts, - esm_cls, - apt_error, - entitlement_factory, - caplog_text, - ): - """When entitled, configure apt repo auth token, pinning and url. - When setup_apt_config fails, cleanup any apt artifacts. - """ - entitlement = entitlement_factory( - esm_cls, - cfg_extension={ - "ua_config": { # intentionally using apt_* - "apt_http_proxy": "apt_http_proxy_value", - "apt_https_proxy": "apt_https_proxy_value", - } - }, - suites=["xenial"], - ) - patched_packages = ["a", "b"] - - original_exists = os.path.exists - - def fake_exists(path): - if path in (apt.APT_METHOD_HTTPS_FILE, apt.CA_CERTIFICATES_FILE): - return True - return original_exists(path) - - def fake_subp(cmd, capture=None, retry_sleeps=None, env={}): - if cmd == ["apt-get", "update"]: - raise exceptions.ProcessExecutionError( - "Failure", stderr="Could not get lock /var/lib/dpkg/lock" - ) - return "", "" - - with contextlib.ExitStack() as stack: - m_add_apt = stack.enter_context( - mock.patch("uaclient.apt.add_auth_apt_repo") - ) - if apt_error: - m_subp = stack.enter_context( - mock.patch("uaclient.system.subp", side_effect=fake_subp) - ) - else: - m_subp = stack.enter_context( - mock.patch("uaclient.system.subp", return_value=("", "")) - ) - - m_can_enable = stack.enter_context( - mock.patch.object(entitlement, "can_enable") - ) - m_remove_apt_config = stack.enter_context( - mock.patch.object(entitlement, "remove_apt_config") - ) - m_disable_local_repo = stack.enter_context( - mock.patch.object(entitlement, "disable_local_esm_repo") - ) - stack.enter_context( - mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) - ) - stack.enter_context( - mock.patch( - M_REPOPATH + "os.path.exists", side_effect=fake_exists - ) - ) - # Note that this patch uses a PropertyMock and happens on the - # entitlement's type because packages is a property - m_packages = mock.PropertyMock(return_value=patched_packages) - stack.enter_context( - mock.patch.object(type(entitlement), "packages", m_packages) - ) - - m_can_enable.return_value = (True, None) - - if apt_error: - with pytest.raises(exceptions.UserFacingError) as excinfo: - entitlement.enable() - else: - assert (True, None) == entitlement.enable() - - add_apt_calls = [ - mock.call( - "/etc/apt/sources.list.d/ubuntu-{}.list".format( - entitlement.name - ), - "http://{}".format(entitlement.name.upper()), - "{}-token".format(entitlement.name), - ["xenial"], - entitlement.repo_key_file, - ) - ] - install_cmd = mock.call( - ["apt-get", "install", "--assume-yes"] + patched_packages, - capture=True, - retry_sleeps=apt.APT_RETRIES, - env={}, - ) - - subp_calls = [ - mock.call( - ["apt-get", "update"], - capture=True, - retry_sleeps=apt.APT_RETRIES, - env={}, - ), - ] - if not apt_error: - subp_calls.append(install_cmd) - - update_msgs_calls = [] if apt_error else [mock.call(entitlement.cfg)] - - assert [mock.call()] == m_can_enable.call_args_list - assert [ - mock.call( - http_proxy="apt_http_proxy_value", - https_proxy="apt_https_proxy_value", - proxy_scope=apt.AptProxyScope.GLOBAL, - ) - ] == m_setup_apt_proxy.call_args_list - assert add_apt_calls == m_add_apt.call_args_list - assert subp_calls == m_subp.call_args_list - assert update_msgs_calls == m_update_apt_and_motd_msgs.call_args_list - - if apt_error: - error_msg = "APT update failed. Another process is running APT." - assert error_msg == excinfo.value.msg - assert [ - mock.call(run_apt_update=False) - ] == m_remove_apt_config.call_args_list - assert [] == m_disable_local_repo.call_args_list - else: - assert [] == m_remove_apt_config.call_args_list - assert [mock.call()] == m_disable_local_repo.call_args_list - - -@mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") +@mock.patch("uaclient.jobs.update_messaging.update_motd_messages") @mock.patch( "uaclient.system.get_platform_info", return_value={"series": "xenial"} ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_fips.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_fips.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_fips.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_fips.py 2023-04-05 15:14:00.000000000 +0000 @@ -27,6 +27,7 @@ FIPSEntitlement, FIPSUpdatesEntitlement, ) +from uaclient.files.notices import Notice, NoticesManager M_PATH = "uaclient.entitlements.fips." M_LIVEPATCH_PATH = "uaclient.entitlements.livepatch.LivepatchEntitlement." @@ -270,6 +271,7 @@ entitlement, ): """When entitled, configure apt repo auth token, pinning and url.""" + notice_ent_cls = NoticesManager() patched_packages = ["a", "b"] expected_conditional_packages = [ "openssh-server", @@ -303,7 +305,13 @@ stack.enter_context( mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) ) - stack.enter_context(mock.patch(M_REPOPATH + "os.path.exists")) + stack.enter_context( + mock.patch( + "uaclient.entitlements.fips.system.should_reboot", + return_value=True, + ) + ) + stack.enter_context(mock.patch(M_REPOPATH + "exists")) stack.enter_context( mock.patch.object(fips, "services_once_enabled_file") ) @@ -400,20 +408,20 @@ assert apt_pinning_calls == m_add_pinning.call_args_list assert subp_calls == m_subp.call_args_list assert [ - ["", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg] - ] == entitlement.cfg.notice_file.read() + messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg, + ] == notice_ent_cls.list() @pytest.mark.parametrize( "fips_common_enable_return_value, expected_remove_notice_calls", [ - (True, [mock.call("", messages.FIPS_INSTALL_OUT_OF_DATE)]), + (True, [mock.call(Notice.FIPS_INSTALL_OUT_OF_DATE)]), (False, []), ], ) @mock.patch( "uaclient.entitlements.fips.FIPSCommonEntitlement._perform_enable" ) - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_enable_removes_out_of_date_notice_on_success( self, m_remove_notice, @@ -437,16 +445,18 @@ True, [ mock.call( - "", messages.NOTICE_WRONG_FIPS_METAPACKAGE_ON_CLOUD + Notice.WRONG_FIPS_METAPACKAGE_ON_CLOUD, + ), + mock.call( + Notice.FIPS_REBOOT_REQUIRED, ), - mock.call("", messages.FIPS_REBOOT_REQUIRED_MSG), ], ), (False, []), ], ) @mock.patch("uaclient.entitlements.repo.RepoEntitlement._perform_enable") - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_enable_removes_wrong_met_notice_on_success( self, m_remove_notice, @@ -517,7 +527,7 @@ stack.enter_context( mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) ) - stack.enter_context(mock.patch(M_REPOPATH + "os.path.exists")) + stack.enter_context(mock.patch(M_REPOPATH + "exists")) with pytest.raises(exceptions.UserFacingError) as excinfo: entitlement.enable() @@ -531,53 +541,6 @@ assert 0 == m_add_pinning.call_count assert 0 == m_remove_apt_config.call_count - @mock.patch("uaclient.apt.setup_apt_proxy") - def test_failure_to_install_removes_apt_auth( - self, _m_setup_apt_proxy, entitlement, tmpdir - ): - - authfile = tmpdir.join("90ubuntu-advantage") - authfile.write("") - - def fake_subp(cmd, *args, **kwargs): - if "install" in cmd: - raise exceptions.ProcessExecutionError(cmd) - return ("", "") - - with contextlib.ExitStack() as stack: - stack.enter_context( - mock.patch("uaclient.system.subp", side_effect=fake_subp) - ) - stack.enter_context( - mock.patch.object( - entitlement, "can_enable", return_value=(True, None) - ) - ) - stack.enter_context( - mock.patch("uaclient.util.handle_message_operations") - ) - stack.enter_context( - mock.patch.object( - entitlement, "setup_apt_config", return_value=True - ) - ) - m_remove_apt_config = stack.enter_context( - mock.patch.object( - entitlement, "remove_apt_config", return_value=True - ) - ) - stack.enter_context( - mock.patch(M_GETPLATFORM, return_value={"series": "xenial"}) - ) - stack.enter_context(mock.patch(M_REPOPATH + "os.path.exists")) - - with pytest.raises(exceptions.UserFacingError) as excinfo: - entitlement.enable() - error_msg = "Could not enable {}.".format(entitlement.title) - assert error_msg == excinfo.value.msg - - assert 1 == m_remove_apt_config.call_count - @mock.patch( "uaclient.entitlements.fips.get_cloud_type", return_value=("", None) ) @@ -916,6 +879,8 @@ entitlement, ): """When can_disable, disable removes apt config and packages.""" + notice_ent_cls = NoticesManager() + with mock.patch.object( entitlement, "can_disable", return_value=(True, None) ): @@ -929,8 +894,8 @@ assert [mock.call(silent=True)] == m_remove_apt_config.call_args_list assert [mock.call()] == m_remove_packages.call_args_list assert [ - ["", messages.FIPS_DISABLE_REBOOT_REQUIRED] - ] == entitlement.cfg.notice_file.read() + messages.FIPS_DISABLE_REBOOT_REQUIRED, + ] == notice_ent_cls.list() @mock.patch("uaclient.system.should_reboot") @@ -958,7 +923,7 @@ def test_non_root_does_not_fail( self, _m_should_reboot, super_application_status, FakeConfig ): - cfg = FakeConfig(root_mode=False) + cfg = FakeConfig() entitlement = FIPSUpdatesEntitlement(cfg) msg = "sure is some status here" with mock.patch( @@ -980,6 +945,7 @@ should_reboot, entitlement, ): + notice_ent_cls = NoticesManager() m_should_reboot.return_value = should_reboot orig_load_file = system.load_file @@ -996,18 +962,21 @@ return orig_exists(path) msg = messages.NamedMessage("test-code", "sure is some status here") - entitlement.cfg.notice_file.add( - "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg + notice_ent_cls.add( + Notice.FIPS_SYSTEM_REBOOT_REQUIRED, + messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg, ) if path_exists: - entitlement.cfg.notice_file.add( - "", messages.FIPS_REBOOT_REQUIRED_MSG + notice_ent_cls.add( + Notice.FIPS_REBOOT_REQUIRED, + messages.FIPS_REBOOT_REQUIRED_MSG, ) if proc_content == "0": - entitlement.cfg.notice_file.add( - "", messages.FIPS_DISABLE_REBOOT_REQUIRED + notice_ent_cls.add( + Notice.FIPS_DISABLE_REBOOT_REQUIRED, + messages.FIPS_DISABLE_REBOOT_REQUIRED, ) with mock.patch( @@ -1028,28 +997,37 @@ if path_exists and should_reboot and proc_content == "1": expected_msg = msg assert [ - ["", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg] - ] == entitlement.cfg.notice_file.read() + messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg, + messages.FIPS_REBOOT_REQUIRED_MSG, + ] == notice_ent_cls.list() elif path_exists and not should_reboot and proc_content == "1": expected_msg = msg - assert entitlement.cfg.notice_file.read() is None + # we do not delete the FIPS_REBOOT_REQUIRED notices + # deleting will happen after rebooting + assert notice_ent_cls.list() == [messages.FIPS_REBOOT_REQUIRED_MSG] elif path_exists and should_reboot and proc_content == "0": expected_msg = messages.FIPS_PROC_FILE_ERROR.format( file_name=entitlement.FIPS_PROC_FILE ) expected_status = ApplicationStatus.DISABLED assert [ - ["", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg], - ["", messages.NOTICE_FIPS_MANUAL_DISABLE_URL], - ] == entitlement.cfg.notice_file.read() + messages.FIPS_DISABLE_REBOOT_REQUIRED, + messages.NOTICE_FIPS_MANUAL_DISABLE_URL, + messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg, + messages.FIPS_REBOOT_REQUIRED_MSG, + ] == notice_ent_cls.list() elif path_exists and not should_reboot and proc_content == "0": expected_msg = messages.FIPS_PROC_FILE_ERROR.format( file_name=entitlement.FIPS_PROC_FILE ) expected_status = ApplicationStatus.DISABLED assert [ - ["", messages.NOTICE_FIPS_MANUAL_DISABLE_URL], - ] == entitlement.cfg.notice_file.read() + ( + Notice.FIPS_MANUAL_DISABLE_URL.value.label, + messages.NOTICE_FIPS_MANUAL_DISABLE_URL, + ) + == notice_ent_cls.list() + ] else: expected_msg = messages.FIPS_REBOOT_REQUIRED @@ -1082,7 +1060,7 @@ self, m_run_apt, entitlement ): m_run_apt.side_effect = exceptions.UserFacingError("error") - with mock.patch.object(entitlement, "_cleanup"): + with mock.patch.object(entitlement, "remove_apt_config"): with pytest.raises(exceptions.UserFacingError): entitlement.install_packages() @@ -1246,7 +1224,7 @@ class TestFIPSUpdatesEntitlementEnable: @pytest.mark.parametrize("enable_ret", ((True), (False))) - @mock.patch("uaclient.files.NoticeFile.try_remove") + @mock.patch("uaclient.entitlements.fips.notices.NoticesManager.remove") @mock.patch( "uaclient.entitlements.fips.FIPSCommonEntitlement._perform_enable" ) @@ -1265,7 +1243,6 @@ fips, "services_once_enabled_file", fake_file ) as m_services_once_enabled: cfg = mock.MagicMock() - cfg.notice_file.try_remove = m_remove_notice fips_updates_ent = entitlement_factory( FIPSUpdatesEntitlement, cfg=cfg ) @@ -1273,9 +1250,6 @@ if enable_ret: assert 1 == m_services_once_enabled.write.call_count - assert [ - mock.call("", messages.FIPS_DISABLE_REBOOT_REQUIRED) - ] == m_remove_notice.call_args_list else: assert not m_services_once_enabled.write.call_count diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_livepatch.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_livepatch.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_livepatch.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_livepatch.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,4 +1,4 @@ -"""Tests related to uaclient.entitlement.base module.""" +"""Tests related to uaclient.entitlement.livepatch module.""" import contextlib import copy @@ -10,7 +10,7 @@ import mock import pytest -from uaclient import apt, exceptions, messages, system +from uaclient import apt, exceptions, livepatch, messages, system from uaclient.entitlements.entitlement_status import ( ApplicabilityStatus, ApplicationStatus, @@ -19,12 +19,8 @@ UserFacingStatus, ) from uaclient.entitlements.livepatch import ( - LIVEPATCH_CMD, LivepatchEntitlement, - configure_livepatch_proxy, - get_config_option_value, process_config_directives, - unconfigure_livepatch_proxy, ) from uaclient.entitlements.tests.conftest import machine_token from uaclient.snap import SNAP_CMD @@ -68,166 +64,6 @@ return livepatch_entitlement_factory() -class TestConfigureLivepatchProxy: - @pytest.mark.parametrize( - "http_proxy,https_proxy,retry_sleeps", - ( - ("http_proxy", "https_proxy", [1, 2]), - ("http_proxy", "", None), - ("", "https_proxy", [1, 2]), - ("http_proxy", None, [1, 2]), - (None, "https_proxy", None), - (None, None, [1, 2]), - ), - ) - @mock.patch("uaclient.system.subp") - def test_configure_livepatch_proxy( - self, m_subp, http_proxy, https_proxy, retry_sleeps, capsys, event - ): - configure_livepatch_proxy(http_proxy, https_proxy, retry_sleeps) - expected_calls = [] - if http_proxy: - expected_calls.append( - mock.call( - [ - LIVEPATCH_CMD, - "config", - "http-proxy={}".format(http_proxy), - ], - retry_sleeps=retry_sleeps, - ) - ) - - if https_proxy: - expected_calls.append( - mock.call( - [ - LIVEPATCH_CMD, - "config", - "https-proxy={}".format(https_proxy), - ], - retry_sleeps=retry_sleeps, - ) - ) - - assert m_subp.call_args_list == expected_calls - - out, _ = capsys.readouterr() - if http_proxy or https_proxy: - assert out.strip() == messages.SETTING_SERVICE_PROXY.format( - service=LivepatchEntitlement.title - ) - - @pytest.mark.parametrize( - "key, subp_return_value, expected_ret", - [ - ("http-proxy", ("nonsense", ""), None), - ("http-proxy", ("", "nonsense"), None), - ( - "http-proxy", - ( - """\ -http-proxy: "" -https-proxy: "" -no-proxy: "" -remote-server: https://livepatch.canonical.com -ca-certs: "" -check-interval: 60 # minutes""", - "", - ), - None, - ), - ( - "http-proxy", - ( - """\ -http-proxy: one -https-proxy: two -no-proxy: "" -remote-server: https://livepatch.canonical.com -ca-certs: "" -check-interval: 60 # minutes""", - "", - ), - "one", - ), - ( - "https-proxy", - ( - """\ -http-proxy: one -https-proxy: two -no-proxy: "" -remote-server: https://livepatch.canonical.com -ca-certs: "" -check-interval: 60 # minutes""", - "", - ), - "two", - ), - ( - "nonexistentkey", - ( - """\ -http-proxy: one -https-proxy: two -no-proxy: "" -remote-server: https://livepatch.canonical.com -ca-certs: "" -check-interval: 60 # minutes""", - "", - ), - None, - ), - ], - ) - @mock.patch("uaclient.system.subp") - def test_get_config_option_value( - self, m_util_subp, key, subp_return_value, expected_ret - ): - m_util_subp.return_value = subp_return_value - ret = get_config_option_value(key) - assert ret == expected_ret - assert [ - mock.call([LIVEPATCH_CMD, "config"]) - ] == m_util_subp.call_args_list - - -class TestUnconfigureLivepatchProxy: - @pytest.mark.parametrize( - "livepatch_installed, protocol_type, retry_sleeps", - ( - (True, "http", None), - (True, "https", [1]), - (True, "http", []), - (False, "http", None), - ), - ) - @mock.patch("uaclient.system.which") - @mock.patch("uaclient.system.subp") - def test_unconfigure_livepatch_proxy( - self, subp, which, livepatch_installed, protocol_type, retry_sleeps - ): - if livepatch_installed: - which.return_value = LIVEPATCH_CMD - else: - which.return_value = None - kwargs = {"protocol_type": protocol_type} - if retry_sleeps is not None: - kwargs["retry_sleeps"] = retry_sleeps - assert None is unconfigure_livepatch_proxy(**kwargs) - if livepatch_installed: - expected_calls = [ - mock.call( - [LIVEPATCH_CMD, "config", protocol_type + "-proxy="], - retry_sleeps=retry_sleeps, - ) - ] - else: - expected_calls = [] - assert expected_calls == subp.call_args_list - - class TestLivepatchContractStatus: def test_contract_status_entitled(self, entitlement): """The contract_status returns ENTITLED when entitled is True.""" @@ -241,11 +77,18 @@ class TestLivepatchUserFacingStatus: @mock.patch( + "uaclient.livepatch.on_supported_kernel", + return_value=None, + ) + @mock.patch( "uaclient.entitlements.livepatch.system.is_container", - return_value=False, + return_value=True, ) def test_user_facing_status_inapplicable_on_inapplicable_status( - self, _m_is_container, livepatch_entitlement_factory + self, + _m_is_container, + _m_on_supported_kernel, + livepatch_entitlement_factory, ): """The user-facing details INAPPLICABLE applicability_status""" affordances = copy.deepcopy(DEFAULT_AFFORDANCES) @@ -259,10 +102,7 @@ m_platform_info.return_value = PLATFORM_INFO_SUPPORTED uf_status, details = entitlement.user_facing_status() assert uf_status == UserFacingStatus.INAPPLICABLE - expected_details = ( - "Livepatch is not available for Ubuntu 16.04 LTS" - " (Xenial Xerus)." - ) + expected_details = "Cannot install Livepatch on a container." assert expected_details == details.msg def test_user_facing_status_unavailable_on_unentitled(self, entitlement): @@ -298,7 +138,7 @@ process_config_directives(cfg) expected_subp = mock.call( [ - LIVEPATCH_CMD, + livepatch.LIVEPATCH_CMD, "config", livepatch_param_tmpl.format(directive_value), ], @@ -321,10 +161,12 @@ process_config_directives(cfg) expected_calls = [ mock.call( - [LIVEPATCH_CMD, "config", "ca-certs=value2"], capture=True + [livepatch.LIVEPATCH_CMD, "config", "ca-certs=value2"], + capture=True, ), mock.call( - [LIVEPATCH_CMD, "config", "remote-server=value1"], capture=True + [livepatch.LIVEPATCH_CMD, "config", "remote-server=value1"], + capture=True, ), ] assert expected_calls == m_subp.call_args_list @@ -403,169 +245,6 @@ assert ("", "") == capsys.readouterr() assert [mock.call()] == m_container.call_args_list - @mock.patch( - "uaclient.system.get_kernel_info", - return_value=system.KernelInfo( - uname_release="4.4.0-140-notgeneric", - proc_version_signature_version="", - major=4, - minor=4, - patch=0, - abi="140", - flavor="notgeneric", - ), - ) - @mock.patch( - "uaclient.system.get_platform_info", - return_value=PLATFORM_INFO_SUPPORTED, - ) - def test_can_enable_false_on_unsupported_kernel_flavor( - self, - _m_platform, - _m_kernel_info, - _m_is_container, - _m_livepatch_status, - _m_fips_status, - entitlement, - ): - """When on an unsupported kernel, can_enable returns False.""" - entitlement = LivepatchEntitlement(entitlement.cfg) - result, reason = entitlement.can_enable() - assert False is result - assert CanEnableFailureReason.INAPPLICABLE == reason.reason - msg = ( - "Livepatch is not available for kernel 4.4.0-140-notgeneric.\n" - "Supported flavors are: generic, lowlatency." - ) - assert msg == reason.message.msg - - @pytest.mark.parametrize( - "kernel_info,meets_min_version", - ( - ( - system.KernelInfo( - uname_release="3.5.0-00-generic", - proc_version_signature_version="", - major=3, - minor=5, - patch=0, - abi="00", - flavor="generic", - ), - False, - ), - ( - system.KernelInfo( - uname_release="4.2.9-00-generic", - proc_version_signature_version="", - major=4, - minor=2, - patch=9, - abi="00", - flavor="generic", - ), - False, - ), - ( - system.KernelInfo( - uname_release="4.3.0-00-generic", - proc_version_signature_version="", - major=4, - minor=3, - patch=0, - abi="00", - flavor="generic", - ), - False, - ), - ( - system.KernelInfo( - uname_release="4.4.0-00-generic", - proc_version_signature_version="", - major=4, - minor=4, - patch=0, - abi="00", - flavor="generic", - ), - True, - ), - ( - system.KernelInfo( - uname_release="4.10.0-00-generic", - proc_version_signature_version="", - major=4, - minor=10, - patch=0, - abi="00", - flavor="generic", - ), - True, - ), - ( - system.KernelInfo( - uname_release="5.0.0-00-generic", - proc_version_signature_version="", - major=5, - minor=0, - patch=0, - abi="00", - flavor="generic", - ), - True, - ), - ), - ) - @mock.patch("uaclient.system.get_kernel_info") - @mock.patch( - "uaclient.system.get_platform_info", - return_value=PLATFORM_INFO_SUPPORTED, - ) - def test_can_enable_false_on_unsupported_min_kernel_version( - self, - _m_platform, - m_kernel_info, - _m_is_container, - _m_livepatch_status, - _m_fips_status, - kernel_info, - meets_min_version, - entitlement, - ): - """When on an unsupported kernel version, can_enable returns False.""" - m_kernel_info.return_value = kernel_info - entitlement = LivepatchEntitlement(entitlement.cfg) - if meets_min_version: - assert (True, None) == entitlement.can_enable() - else: - result, reason = entitlement.can_enable() - assert False is result - assert CanEnableFailureReason.INAPPLICABLE == reason.reason - msg = ( - "Livepatch is not available for kernel {}.\n" - "Minimum kernel version required: 4.4.".format( - kernel_info.uname_release - ) - ) - assert msg == reason.message.msg - - def test_can_enable_false_on_unsupported_architecture( - self, _m_is_container, _m_livepatch_status, _m_fips_status, entitlement - ): - """When on an unsupported architecture, can_enable returns False.""" - unsupported_kernel = copy.deepcopy(dict(PLATFORM_INFO_SUPPORTED)) - unsupported_kernel["arch"] = "ppc64le" - with mock.patch("uaclient.system.get_platform_info") as m_platform: - m_platform.return_value = unsupported_kernel - result, reason = entitlement.can_enable() - assert False is result - assert CanEnableFailureReason.INAPPLICABLE == reason.reason - msg = ( - "Livepatch is not available for platform ppc64le.\n" - "Supported platforms are: amd64." - ) - assert msg == reason.message.msg - def test_can_enable_false_on_containers( self, m_is_container, _m_livepatch_status, _m_fips_status, entitlement ): @@ -687,7 +366,7 @@ @mock.patch(M_PATH + "snap.is_installed") @mock.patch("uaclient.util.validate_proxy", side_effect=lambda x, y, z: y) @mock.patch("uaclient.snap.configure_snap_proxy") -@mock.patch("uaclient.entitlements.livepatch.configure_livepatch_proxy") +@mock.patch("uaclient.livepatch.configure_livepatch_proxy") class TestLivepatchEntitlementEnable: mocks_apt_update = [mock.call()] @@ -715,14 +394,17 @@ mocks_config = [ mock.call( [ - LIVEPATCH_CMD, + livepatch.LIVEPATCH_CMD, "config", "remote-server=https://alt.livepatch.com", ], capture=True, ), - mock.call([LIVEPATCH_CMD, "disable"]), - mock.call([LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True), + mock.call([livepatch.LIVEPATCH_CMD, "disable"]), + mock.call( + [livepatch.LIVEPATCH_CMD, "enable", "livepatch-token"], + capture=True, + ), ] @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) @@ -732,7 +414,7 @@ @mock.patch("uaclient.contract.apply_contract_overrides") @mock.patch("uaclient.apt.run_apt_install_command") @mock.patch("uaclient.apt.run_apt_update_command") - @mock.patch("uaclient.system.which", return_value=False) + @mock.patch("uaclient.system.which", return_value=None) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") @mock.patch( M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) @@ -787,7 +469,10 @@ assert expected_log not in caplog_text() else: assert expected_log in caplog_text() - expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)] + expected_calls = [ + mock.call("/usr/bin/snap"), + mock.call(livepatch.LIVEPATCH_CMD), + ] assert expected_calls == m_which.call_args_list assert m_validate_proxy.call_count == 2 assert m_snap_proxy.call_count == 1 @@ -797,7 +482,8 @@ @mock.patch("uaclient.system.subp") @mock.patch("uaclient.contract.apply_contract_overrides") @mock.patch( - "uaclient.system.which", side_effect=lambda cmd: cmd == "/usr/bin/snap" + "uaclient.system.which", + side_effect=lambda cmd: cmd if cmd == "/usr/bin/snap" else None, ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") @mock.patch( @@ -836,7 +522,10 @@ "Canonical livepatch enabled.\n" ) assert (msg, "") == capsys.readouterr() - expected_calls = [mock.call("/usr/bin/snap"), mock.call(LIVEPATCH_CMD)] + expected_calls = [ + mock.call("/usr/bin/snap"), + mock.call(livepatch.LIVEPATCH_CMD), + ] assert expected_calls == m_which.call_args_list assert m_validate_proxy.call_count == 2 assert m_snap_proxy.call_count == 1 @@ -844,7 +533,8 @@ @mock.patch("uaclient.system.subp") @mock.patch( - "uaclient.system.which", side_effect=lambda cmd: cmd == "/usr/bin/snap" + "uaclient.system.which", + side_effect=lambda cmd: cmd if cmd == "/usr/bin/snap" else None, ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") @mock.patch( @@ -882,7 +572,9 @@ @mock.patch("uaclient.system.get_platform_info") @mock.patch("uaclient.system.subp") @mock.patch("uaclient.contract.apply_contract_overrides") - @mock.patch("uaclient.system.which", side_effect=[True, True]) + @mock.patch( + "uaclient.system.which", side_effect=["/path/to/exe", "/path/to/exe"] + ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") @mock.patch( M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) @@ -915,15 +607,16 @@ ), mock.call( [ - LIVEPATCH_CMD, + livepatch.LIVEPATCH_CMD, "config", "remote-server=https://alt.livepatch.com", ], capture=True, ), - mock.call([LIVEPATCH_CMD, "disable"]), + mock.call([livepatch.LIVEPATCH_CMD, "disable"]), mock.call( - [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True + [livepatch.LIVEPATCH_CMD, "enable", "livepatch-token"], + capture=True, ), ] assert subp_calls == m_subp.call_args_list @@ -935,7 +628,9 @@ @mock.patch("uaclient.system.get_platform_info") @mock.patch("uaclient.system.subp") @mock.patch("uaclient.contract.apply_contract_overrides") - @mock.patch("uaclient.system.which", side_effect=[True, True]) + @mock.patch( + "uaclient.system.which", side_effect=["/path/to/exe", "/path/to/exe"] + ) @mock.patch(M_PATH + "LivepatchEntitlement.application_status") @mock.patch( M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None) @@ -966,14 +661,15 @@ ), mock.call( [ - LIVEPATCH_CMD, + livepatch.LIVEPATCH_CMD, "config", "remote-server=https://alt.livepatch.com", ], capture=True, ), mock.call( - [LIVEPATCH_CMD, "enable", "livepatch-token"], capture=True + [livepatch.LIVEPATCH_CMD, "enable", "livepatch-token"], + capture=True, ), ] assert subp_no_livepatch_disable == m_subp.call_args_list @@ -1036,7 +732,7 @@ caplog_text, event, ): - m_which.side_effect = [True, False] + m_which.side_effect = ["/path/to/exe", None] m_is_installed.return_value = True stderr_msg = ( @@ -1131,7 +827,7 @@ m_is_installed, entitlement, ): - m_which.side_effect = [False, True] + m_which.side_effect = [None, "/path/to/exe"] m_is_installed.return_value = True stderr_msg = "test error" @@ -1168,7 +864,7 @@ class TestLivepatchApplicationStatus: - @pytest.mark.parametrize("which_result", ((True), (False))) + @pytest.mark.parametrize("which_result", (("/path/to/exe"), (None))) @pytest.mark.parametrize("subp_raise_exception", ((True), (False))) @mock.patch("uaclient.system.which") @mock.patch("uaclient.system.subp") @@ -1193,7 +889,7 @@ assert details is None @mock.patch("time.sleep") - @mock.patch("uaclient.system.which", return_value=True) + @mock.patch("uaclient.system.which", return_value="/path/to/exe") def test_status_command_retry_on_application_status( self, m_which, m_sleep, entitlement ): diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_repo.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_repo.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/entitlements/tests/test_repo.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/entitlements/tests/test_repo.py 2023-04-05 15:14:00.000000000 +0000 @@ -375,45 +375,6 @@ class TestRepoEnable: @pytest.mark.parametrize( - "pre_enable_msg, output, perform_enable_call_count", - ( - (["msg1", (lambda: False, {}), "msg2"], "msg1\n", 0), - (["msg1", (lambda: True, {}), "msg2"], "msg1\nmsg2\n", 1), - ), - ) - @mock.patch.object( - RepoTestEntitlement, - "_perform_enable", - return_value=False, - ) - @mock.patch.object( - RepoTestEntitlement, - "can_enable", - return_value=(True, None), - ) - def test_enable_can_exit_on_pre_enable_messaging_hooks( - self, - _m_can_enable, - m_perform_enable, - pre_enable_msg, - output, - perform_enable_call_count, - entitlement, - capsys, - event, - ): - with mock.patch( - M_PATH + "RepoEntitlement.messaging", - new_callable=mock.PropertyMock, - ) as m_messaging: - m_messaging.return_value = {"pre_enable": pre_enable_msg} - with mock.patch.object(type(entitlement), "packages", []): - entitlement.enable() - stdout, _ = capsys.readouterr() - assert output == stdout - assert perform_enable_call_count == m_perform_enable.call_count - - @pytest.mark.parametrize( "pre_disable_msg,post_disable_msg,output,retval", ( ( @@ -472,12 +433,11 @@ @pytest.mark.parametrize("should_reboot", (False, True)) @pytest.mark.parametrize("with_pre_install_msg", (False, True)) @pytest.mark.parametrize("packages", (["a"], [], None)) - @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.apt.setup_apt_proxy") @mock.patch(M_PATH + "system.should_reboot") @mock.patch(M_PATH + "system.subp", return_value=("", "")) @mock.patch(M_PATH + "apt.add_auth_apt_repo") - @mock.patch(M_PATH + "os.path.exists", return_value=True) + @mock.patch(M_PATH + "exists", return_value=True) @mock.patch(M_PATH + "system.get_platform_info") @mock.patch.object( RepoTestEntitlement, "can_enable", return_value=(True, None) @@ -491,7 +451,6 @@ m_subp, m_should_reboot, m_setup_apt_proxy, - _m_getuid, entitlement, capsys, caplog_text, @@ -619,25 +578,57 @@ class TestPerformEnable: @pytest.mark.parametrize( - "supports_access_only, access_only, expected_install_calls", - ( - (False, False, [mock.call()]), - (False, True, [mock.call()]), - (True, False, [mock.call()]), - (True, True, []), - ), + [ + "supports_access_only", + "access_only", + "expected_setup_apt_calls", + "expected_install_calls", + "expected_check_for_reboot_calls", + ], + [ + ( + False, + False, + [mock.call(silent=mock.ANY)], + [mock.call()], + [mock.call(operation="install")], + ), + ( + False, + True, + [mock.call(silent=mock.ANY)], + [mock.call()], + [mock.call(operation="install")], + ), + ( + True, + False, + [mock.call(silent=mock.ANY)], + [mock.call()], + [mock.call(operation="install")], + ), + ( + True, + True, + [mock.call(silent=mock.ANY)], + [], + [], + ), + ], ) @mock.patch(M_PATH + "RepoEntitlement._check_for_reboot_msg") @mock.patch(M_PATH + "RepoEntitlement.install_packages") @mock.patch(M_PATH + "RepoEntitlement.setup_apt_config") - def test_access_only_skips_install( + def test_perform_enable( self, - _m_setup_apt_config, + m_setup_apt_config, m_install_packages, - _m_check_for_reboot_msg, + m_check_for_reboot_msg, supports_access_only, access_only, + expected_setup_apt_calls, expected_install_calls, + expected_check_for_reboot_calls, entitlement_factory, ): with mock.patch.object( @@ -648,8 +639,15 @@ affordances={"series": ["xenial"]}, access_only=access_only, ) - entitlement._perform_enable() + assert entitlement._perform_enable(silent=True) is True + assert ( + m_setup_apt_config.call_args_list == expected_setup_apt_calls + ) assert m_install_packages.call_args_list == expected_install_calls + assert ( + m_check_for_reboot_msg.call_args_list + == expected_check_for_reboot_calls + ) class TestRemoveAptConfig: @@ -851,7 +849,7 @@ Presence is determined based on checking known files from those debs. It avoids a costly round-trip shelling out to call dpkg -l. """ - with mock.patch(M_PATH + "os.path.exists") as m_exists: + with mock.patch(M_PATH + "exists") as m_exists: m_exists.return_value = False entitlement.setup_apt_config() assert [ @@ -902,7 +900,7 @@ RepoTestEntitlementRepoWithPin, affordances={"series": ["xenial"]} ) entitlement.origin = "RepoTestOrigin" # don't error on origin = None - with mock.patch(M_PATH + "os.path.exists") as m_exists: + with mock.patch(M_PATH + "exists") as m_exists: m_exists.return_value = True # Skip prerequisite pkg installs entitlement.setup_apt_config() assert [ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/event_logger.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/event_logger.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/event_logger.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/event_logger.py 2023-04-05 15:14:00.000000000 +0000 @@ -11,7 +11,7 @@ import sys from typing import Any, Dict, List, Optional, Set, Union # noqa: F401 -import yaml +from uaclient.yaml import safe_dump JSON_SCHEMA_VERSION = "0.1" EventFieldErrorType = Optional[Union[str, Dict[str, str]]] @@ -210,7 +210,11 @@ "needs_reboot": self._needs_reboot, } - print(json.dumps(response, sort_keys=True)) + from uaclient.util import DatetimeAwareJSONEncoder + + print( + json.dumps(response, cls=DatetimeAwareJSONEncoder, sort_keys=True) + ) def _process_events_status(self): output = format_machine_readable_output(self._output_content) @@ -227,7 +231,7 @@ ) ) elif self._event_logger_mode == EventLoggerMode.YAML: - print(yaml.dump(output, default_flow_style=False)) + print(safe_dump(output, default_flow_style=False)) def process_events(self) -> None: """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/exceptions.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/exceptions.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/exceptions.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/exceptions.py 2023-04-05 15:14:00.000000000 +0000 @@ -469,3 +469,9 @@ def __init__(self, version): msg = messages.MISSING_SERIES_ON_OS_RELEASE.format(version=version) super().__init__(msg=msg.msg, msg_code=msg.name) + + +class InvalidLockFile(UserFacingError): + def __init__(self, lock_file_path): + msg = messages.INVALID_LOCK_FILE.format(lock_file_path=lock_file_path) + super().__init__(msg=msg.msg, msg_code=msg.name) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/data_types.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/data_types.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/data_types.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/data_types.py 2023-04-05 15:14:00.000000000 +0000 @@ -2,12 +2,12 @@ from enum import Enum from typing import Callable, Dict, Generic, Optional, Type, TypeVar -import yaml - from uaclient import exceptions from uaclient.data_types import DataObject from uaclient.files.files import UAFile from uaclient.util import DatetimeAwareJSONDecoder +from uaclient.yaml import parser as yaml_parser +from uaclient.yaml import safe_dump, safe_load class DataObjectFileFormat(Enum): @@ -25,11 +25,15 @@ ua_file: UAFile, file_format: DataObjectFileFormat = DataObjectFileFormat.JSON, preprocess_data: Optional[Callable[[Dict], Dict]] = None, + optional_type_errors_become_null: bool = False, ): self.data_object_cls = data_object_cls self.ua_file = ua_file self.file_format = file_format self.preprocess_data = preprocess_data + self.optional_type_errors_become_null = ( + optional_type_errors_become_null + ) def read(self) -> Optional[DOFType]: raw_data = self.ua_file.read() @@ -48,8 +52,8 @@ ) elif self.file_format == DataObjectFileFormat.YAML: try: - parsed_data = yaml.safe_load(raw_data) - except yaml.parser.ParserError: + parsed_data = safe_load(raw_data) + except yaml_parser.ParserError: raise exceptions.InvalidFileFormatError( self.ua_file.path, "yaml" ) @@ -60,14 +64,17 @@ if self.preprocess_data: parsed_data = self.preprocess_data(parsed_data) - return self.data_object_cls.from_dict(parsed_data) + return self.data_object_cls.from_dict( + parsed_data, + optional_type_errors_become_null=self.optional_type_errors_become_null, # noqa: E501 + ) def write(self, content: DOFType): if self.file_format == DataObjectFileFormat.JSON: str_content = content.to_json() elif self.file_format == DataObjectFileFormat.YAML: data = content.to_dict() - str_content = yaml.dump(data, default_flow_style=False) + str_content = safe_dump(data, default_flow_style=False) self.ua_file.write(str_content) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/files.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/files.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/files.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/files.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,9 +1,8 @@ import json import logging import os -import re from datetime import datetime -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional # noqa: F401 from uaclient import defaults, event_logger, exceptions, messages, system, util from uaclient.contract_data_types import PublicMachineTokenData @@ -64,11 +63,9 @@ def __init__( self, directory: str = defaults.DEFAULT_DATA_DIR, - root_mode: bool = True, machine_token_overlay_path: Optional[str] = None, ): file_name = defaults.MACHINE_TOKEN_FILE - self.is_root = root_mode self.private_file = UAFile( file_name, directory + defaults.PRIVATE_SUBDIR ) @@ -78,19 +75,23 @@ self._entitlements = None self._contract_expiry_datetime = None - def write(self, content: dict): + def write(self, private_content: dict): """Update the machine_token file for both pub/private files""" - if self.is_root: - content_str = json.dumps( - content, cls=util.DatetimeAwareJSONEncoder + if util.we_are_currently_root(): + private_content_str = json.dumps( + private_content, cls=util.DatetimeAwareJSONEncoder ) - self.private_file.write(content_str) - content = json.loads(content_str) # taking care of datetime type - content = PublicMachineTokenData.from_dict(content).to_dict(False) - content_str = json.dumps( - content, cls=util.DatetimeAwareJSONEncoder + self.private_file.write(private_content_str) + + # PublicMachineTokenData only has public fields defined and + # ignores all other (private) fields in from_dict + public_content = PublicMachineTokenData.from_dict( + private_content + ).to_dict(keep_none=False) + public_content_str = json.dumps( + public_content, cls=util.DatetimeAwareJSONEncoder ) - self.public_file.write(content_str) + self.public_file.write(public_content_str) self._machine_token = None self._entitlements = None @@ -100,7 +101,7 @@ def delete(self): """Delete both pub and private files""" - if self.is_root: + if util.we_are_currently_root(): self.public_file.delete() self.private_file.delete() @@ -111,7 +112,7 @@ raise exceptions.NonRootUserError() def read(self) -> Optional[dict]: - if self.is_root: + if util.we_are_currently_root(): file_handler = self.private_file else: file_handler = self.public_file @@ -126,7 +127,7 @@ @property def is_present(self): - if self.is_root: + if util.we_are_currently_root(): return self.public_file.is_present and self.private_file.is_present else: return self.public_file.is_present @@ -288,83 +289,3 @@ .get("id") ) return None - - -class NoticeFile: - def __init__( - self, - directory: str = defaults.DEFAULT_DATA_DIR, - root_mode: bool = True, - ): - file_name = "notices.json" - self.file = UAFile(file_name, directory, False) - self.is_root = root_mode - - def add(self, label: str, description: str): - """ - Adds a notice to the notices.json file. - Raises a NonRootUserError if the user is not root. - """ - if self.is_root: - notices = self.read() or [] - notice = [label, description] - if notice not in notices: - notices.append(notice) - self.write(notices) - else: - raise exceptions.NonRootUserError - - def try_add(self, label: str, description: str): - """ - Adds a notice to the notices.json file. - Logs a warning when adding as non-root - """ - try: - self.add(label, description) - except exceptions.NonRootUserError: - event.warning("Trying to add notice as non-root user") - - def remove(self, label_regex: str, descr_regex: str): - """ - Removes a notice to the notices.json file. - Raises a NonRootUserError if the user is not root. - """ - if self.is_root: - notices = [] - cached_notices = self.read() or [] - if cached_notices: - for notice_label, notice_descr in cached_notices: - if re.match(label_regex, notice_label): - if re.match(descr_regex, notice_descr): - continue - notices.append((notice_label, notice_descr)) - if notices: - self.write(notices) - elif os.path.exists(self.file.path): - self.file.delete() - else: - raise exceptions.NonRootUserError - - def try_remove(self, label_regex: str, descr_regex: str): - """ - Removes a notice to the notices.json file. - Logs a warning when removing as non-root - """ - try: - self.remove(label_regex, descr_regex) - except exceptions.NonRootUserError: - event.warning("Trying to remove notice as non-root user") - - def read(self): - content = self.file.read() - if not content: - return None - try: - return json.loads(content, cls=util.DatetimeAwareJSONDecoder) - except ValueError: - return content - - def write(self, content: Any): - if not isinstance(content, str): - content = json.dumps(content, cls=util.DatetimeAwareJSONEncoder) - self.file.write(content) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/__init__.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/__init__.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/__init__.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/__init__.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,10 +1,9 @@ from uaclient.files.data_types import DataObjectFile, DataObjectFileFormat -from uaclient.files.files import MachineTokenFile, NoticeFile, UAFile +from uaclient.files.files import MachineTokenFile, UAFile __all__ = [ "DataObjectFile", "DataObjectFileFormat", "MachineTokenFile", - "NoticeFile", "UAFile", ] diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/notices.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/notices.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/notices.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/notices.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,231 @@ +import logging +import os +from collections import namedtuple +from enum import Enum +from typing import List + +from uaclient import defaults, event_logger, messages, system, util + +LOG = logging.getLogger(__name__) +event = event_logger.get_event_logger() +NoticeFileDetails = namedtuple( + "NoticeFileDetails", ["order_id", "label", "is_permanent", "message"] +) + + +class Notice(NoticeFileDetails, Enum): + REBOOT_REQUIRED = NoticeFileDetails( + label="reboot_required", + order_id="10", + is_permanent=False, + message="System reboot required", + ) + ENABLE_REBOOT_REQUIRED = NoticeFileDetails( + label="enable_reboot_required", + order_id="11", + is_permanent=False, + message=messages.ENABLE_REBOOT_REQUIRED_TMPL, + ) + REBOOT_SCRIPT_FAILED = NoticeFileDetails( + label="reboot_script_failed", + order_id="12", + is_permanent=True, + message=messages.REBOOT_SCRIPT_FAILED, + ) + FIPS_REBOOT_REQUIRED = NoticeFileDetails( + label="fips_reboot_required", + order_id="20", + is_permanent=False, + message=messages.FIPS_REBOOT_REQUIRED_MSG, + ) + FIPS_SYSTEM_REBOOT_REQUIRED = NoticeFileDetails( + label="fips_system_reboot_required", + order_id="21", + is_permanent=False, + message=messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg, + ) + FIPS_INSTALL_OUT_OF_DATE = NoticeFileDetails( + label="fips_install_out_of_date", + order_id="22", + is_permanent=True, + message=messages.FIPS_INSTALL_OUT_OF_DATE, + ) + FIPS_DISABLE_REBOOT_REQUIRED = NoticeFileDetails( + label="fips_disable_reboot_required", + order_id="23", + is_permanent=False, + message=messages.FIPS_DISABLE_REBOOT_REQUIRED, + ) + FIPS_PROC_FILE_ERROR = NoticeFileDetails( + label="fips_proc_file_error", + order_id="24", + is_permanent=True, + message=messages.FIPS_PROC_FILE_ERROR, + ) + FIPS_MANUAL_DISABLE_URL = NoticeFileDetails( + label="fips_manual_disable_url", + order_id="25", + is_permanent=True, + message=messages.NOTICE_FIPS_MANUAL_DISABLE_URL, + ) + WRONG_FIPS_METAPACKAGE_ON_CLOUD = NoticeFileDetails( + label="wrong_fips_metapackage_on_cloud", + order_id="25", + is_permanent=True, + message=messages.NOTICE_WRONG_FIPS_METAPACKAGE_ON_CLOUD, + ) + LIVEPATCH_LTS_REBOOT_REQUIRED = NoticeFileDetails( + label="lp_lts_reboot_required", + order_id="30", + is_permanent=False, + message=messages.LIVEPATCH_LTS_REBOOT_REQUIRED, + ) + CONTRACT_REFRESH_WARNING = NoticeFileDetails( + label="contract_refresh_warning", + order_id="40", + is_permanent=True, + message=messages.NOTICE_REFRESH_CONTRACT_WARNING, + ) + OPERATION_IN_PROGRESS = NoticeFileDetails( + label="operation_in_progress", + order_id="60", + is_permanent=False, + message="Operation in progress: {operation}", + ) + AUTO_ATTACH_RETRY_FULL_NOTICE = NoticeFileDetails( + label="auto_attach_retry_full_notice", + order_id="70", + is_permanent=False, + message=messages.AUTO_ATTACH_RETRY_NOTICE, + ) + AUTO_ATTACH_RETRY_TOTAL_FAILURE = NoticeFileDetails( + label="auto_attach_total_failure", + order_id="71", + is_permanent=True, + message=messages.AUTO_ATTACH_RETRY_TOTAL_FAILURE_NOTICE, + ) + + +class NoticesManager: + def add( + self, + notice_details: Notice, + description: str, + ): + """Adds a notice file. If the notice is found, + it overwrites it. + + :param notice_details: Holds details concerning the notice file. + :param description: The content to be written to the notice file. + """ + if not util.we_are_currently_root(): + with util.disable_log_to_console(): + LOG.warning("Trying to add a notice as non-root user") + return + + directory = ( + defaults.NOTICES_PERMANENT_DIRECTORY + if notice_details.value.is_permanent + else defaults.NOTICES_TEMPORARY_DIRECTORY + ) + filename = "{}-{}".format( + notice_details.value.order_id, notice_details.value.label + ) + system.write_file( + os.path.join(directory, filename), + description, + ) + + def remove(self, notice_details: Notice): + """Deletes a notice file. + + :param notice_details: Holds details concerning the notice file. + """ + if not util.we_are_currently_root(): + with util.disable_log_to_console(): + LOG.warning("Trying to remove a notice as non-root user") + return + + directory = ( + defaults.NOTICES_PERMANENT_DIRECTORY + if notice_details.value.is_permanent + else defaults.NOTICES_TEMPORARY_DIRECTORY + ) + filename = "{}-{}".format( + notice_details.value.order_id, notice_details.value.label + ) + system.ensure_file_absent(os.path.join(directory, filename)) + + def list(self) -> List[str]: + """Gets all the notice files currently saved. + + :returns: List of notice file contents. + """ + notice_directories = ( + defaults.NOTICES_PERMANENT_DIRECTORY, + defaults.NOTICES_TEMPORARY_DIRECTORY, + ) + notices = [] + for notice_directory in notice_directories: + if not os.path.exists(notice_directory): + continue + notice_file_names = [ + file_name + for file_name in os.listdir(notice_directory) + if os.path.isfile(os.path.join(notice_directory, file_name)) + ] + for notice_file_name in notice_file_names: + notice_file_contents = system.load_file( + os.path.join(notice_directory, notice_file_name) + ) + if notice_file_contents: + notices.append(notice_file_contents) + else: + # if no contents of file, default to message + # defined in the enum + try: + order_id, label = notice_file_name.split("-") + notice = None + for n in Notice: + if n.order_id == order_id and n.label == label: + notice = n + if notice is None: + raise Exception() + notices.append(notice.value.message) + except Exception: + with util.disable_log_to_console(): + logging.warning( + "Something went wrong while processing" + " notice: {}.".format( + notice_file_name, + ) + ) + notices.sort() + return notices + + +_notice_cls = None + + +def get_notice(): + global _notice_cls + if _notice_cls is None: + _notice_cls = NoticesManager() + + return _notice_cls + + +def add(notice_details: Notice, **kwargs) -> None: + notice = get_notice() + description = notice_details.message.format(**kwargs) + notice.add(notice_details, description) + + +def remove(notice_details: Notice) -> None: + notice = get_notice() + notice.remove(notice_details) + + +def list() -> List[str]: + notice = get_notice() + return notice.list() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/state_files.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/state_files.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/state_files.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/state_files.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,5 +1,7 @@ +import datetime from typing import Any, Dict, List, Optional +from uaclient import defaults from uaclient.data_types import ( BoolDataValue, DataObject, @@ -9,7 +11,6 @@ StringDataValue, data_list, ) -from uaclient.defaults import MESSAGES_DIR from uaclient.files.data_types import DataObjectFile, DataObjectFileFormat from uaclient.files.files import UAFile @@ -113,15 +114,18 @@ fields = [ Field("metering", TimerJobState, required=False), Field("update_messaging", TimerJobState, required=False), + Field("update_contract_info", TimerJobState, required=False), ] def __init__( self, metering: Optional[TimerJobState], update_messaging: Optional[TimerJobState], + update_contract_info: Optional[TimerJobState], ): self.metering = metering self.update_messaging = update_messaging + self.update_contract_info = update_contract_info timer_jobs_state_file = DataObjectFile( @@ -131,4 +135,101 @@ ) -apt_news_contents_file = UAFile("apt-news", directory=MESSAGES_DIR) +apt_news_contents_file = UAFile("apt-news", directory=defaults.MESSAGES_DIR) + + +class LivepatchSupportCacheData(DataObject): + fields = [ + Field("version", StringDataValue), + Field("flavor", StringDataValue), + Field("arch", StringDataValue), + Field("codename", StringDataValue), + Field("supported", BoolDataValue, required=False), + Field("cached_at", DatetimeDataValue), + ] + + def __init__( + self, + version: str, + flavor: str, + arch: str, + codename: str, + supported: Optional[bool], + cached_at: datetime.datetime, + ): + self.version = version + self.flavor = flavor + self.arch = arch + self.codename = codename + self.supported = supported + self.cached_at = cached_at + + +livepatch_support_cache = DataObjectFile( + LivepatchSupportCacheData, + UAFile( + "livepatch-kernel-support-cache.json", + directory=defaults.UAC_TMP_PATH, + private=False, + ), + file_format=DataObjectFileFormat.JSON, +) + + +class UserConfigData(DataObject): + fields = [ + Field("apt_http_proxy", StringDataValue, required=False), + Field("apt_https_proxy", StringDataValue, required=False), + Field("global_apt_http_proxy", StringDataValue, required=False), + Field("global_apt_https_proxy", StringDataValue, required=False), + Field("ua_apt_http_proxy", StringDataValue, required=False), + Field("ua_apt_https_proxy", StringDataValue, required=False), + Field("http_proxy", StringDataValue, required=False), + Field("https_proxy", StringDataValue, required=False), + Field("apt_news", BoolDataValue, required=False), + Field("apt_news_url", StringDataValue, required=False), + Field("poll_for_pro_license", BoolDataValue, required=False), + Field("polling_error_retry_delay", IntDataValue, required=False), + Field("metering_timer", IntDataValue, required=False), + Field("update_messaging_timer", IntDataValue, required=False), + ] + + def __init__( + self, + apt_http_proxy: Optional[str] = None, + apt_https_proxy: Optional[str] = None, + global_apt_http_proxy: Optional[str] = None, + global_apt_https_proxy: Optional[str] = None, + ua_apt_http_proxy: Optional[str] = None, + ua_apt_https_proxy: Optional[str] = None, + http_proxy: Optional[str] = None, + https_proxy: Optional[str] = None, + apt_news: Optional[bool] = None, + apt_news_url: Optional[str] = None, + poll_for_pro_license: Optional[bool] = None, + polling_error_retry_delay: Optional[int] = None, + metering_timer: Optional[int] = None, + update_messaging_timer: Optional[int] = None, + ): + self.apt_http_proxy = apt_http_proxy + self.apt_https_proxy = apt_https_proxy + self.global_apt_http_proxy = global_apt_http_proxy + self.global_apt_https_proxy = global_apt_https_proxy + self.ua_apt_http_proxy = ua_apt_http_proxy + self.ua_apt_https_proxy = ua_apt_https_proxy + self.http_proxy = http_proxy + self.https_proxy = https_proxy + self.apt_news = apt_news + self.apt_news_url = apt_news_url + self.poll_for_pro_license = poll_for_pro_license + self.polling_error_retry_delay = polling_error_retry_delay + self.metering_timer = metering_timer + self.update_messaging_timer = update_messaging_timer + + +user_config_file = DataObjectFile( + UserConfigData, + UAFile("user-config.json", private=True), + DataObjectFileFormat.JSON, + optional_type_errors_become_null=True, +) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/tests/test_files.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/tests/test_files.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/tests/test_files.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/tests/test_files.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,5 +1,7 @@ import os +import mock + from uaclient import system from uaclient.files import MachineTokenFile, UAFile @@ -27,8 +29,10 @@ token_file.delete() assert token_file.machine_token is None - def test_public_file_filtering(self, tmpdir): + @mock.patch("uaclient.util.we_are_currently_root") + def test_public_file_filtering(self, m_we_are_currently_root, tmpdir): # root access of machine token file + m_we_are_currently_root.return_value = True token_file = MachineTokenFile( directory=tmpdir.strpath, ) @@ -40,9 +44,8 @@ root_token = token_file.machine_token assert token == root_token # non root access of machine token file - token_file = MachineTokenFile( - directory=tmpdir.strpath, root_mode=False - ) + m_we_are_currently_root.return_value = False + token_file = MachineTokenFile(directory=tmpdir.strpath) nonroot_token = token_file.machine_token assert root_token != nonroot_token machine_token = nonroot_token.get("machineToken", None) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/tests/test_notices.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/tests/test_notices.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/files/tests/test_notices.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/files/tests/test_notices.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,124 @@ +import os + +import mock +import pytest + +from uaclient import defaults +from uaclient.conftest import FakeNotice +from uaclient.files import notices +from uaclient.files.notices import NoticesManager + + +class TestNotices: + @pytest.mark.parametrize( + "label,content", + ( + ( + FakeNotice.a, + "notice_a", + ), + ), + ) + @mock.patch("uaclient.files.notices.system.write_file") + def test_add( + self, + sys_write_file, + label, + content, + ): + notice = NoticesManager() + notice.add(label, content) + assert [ + mock.call( + os.path.join(defaults.NOTICES_PERMANENT_DIRECTORY, "01-a"), + content, + ) + ] == sys_write_file.call_args_list + + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) + @mock.patch("uaclient.files.notices.system.write_file") + def test_add_non_root( + self, + m_sys_write_file, + m_we_are_currently_root, + caplog_text, + ): + notice = NoticesManager() + notice.add(FakeNotice.a, "content") + assert [] == m_sys_write_file.call_args_list + assert "Trying to add a notice as non-root user" in caplog_text() + + @pytest.mark.parametrize( + "label,content", + ( + ( + FakeNotice.a, + "notice_a", + ), + ), + ) + def test_add_duplicate_label( + self, + label, + content, + ): + notice = NoticesManager() + notice.add(label, content) + with mock.patch( + "uaclient.files.notices.system.write_file" + ) as sys_write_file: + notice.add(label, content) + assert 1 == sys_write_file.call_count + + @pytest.mark.parametrize( + "label,content", + ( + ( + FakeNotice.a, + "notice_a", + ), + ), + ) + @mock.patch("uaclient.files.notices.system.ensure_file_absent") + def test_remove( + self, + sys_file_absent, + label, + content, + ): + notice = NoticesManager() + notice.add(label, content) + notice.remove(label) + assert [ + mock.call( + os.path.join(defaults.NOTICES_PERMANENT_DIRECTORY, "01-a"), + ) + ] == sys_file_absent.call_args_list + + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) + @mock.patch("uaclient.files.notices.system.ensure_file_absent") + def test_remove_non_root( + self, + m_sys_file_absent, + m_we_are_currently_root, + caplog_text, + ): + notice = NoticesManager() + notice.remove(FakeNotice.a) + assert [] == m_sys_file_absent.call_args_list + assert "Trying to remove a notice as non-root user" in caplog_text() + + @mock.patch("uaclient.files.notices.NoticesManager.list") + @mock.patch("uaclient.files.notices.NoticesManager.remove") + @mock.patch("uaclient.files.notices.NoticesManager.add") + def test_notice_module( + self, notice_cls_add, notice_cls_remove, notice_cls_read + ): + notices.add(FakeNotice.a) + assert [ + mock.call(FakeNotice.a, "notice_a"), + ] == notice_cls_add.call_args_list + notices.remove(FakeNotice.a) + assert [mock.call(FakeNotice.a)] == notice_cls_remove.call_args_list + notices.list() + assert 1 == notice_cls_read.call_count diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/tests/test_update_contract_info.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/tests/test_update_contract_info.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/tests/test_update_contract_info.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/tests/test_update_contract_info.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,88 @@ +import logging + +import mock +import pytest + +from uaclient.files.notices import Notice +from uaclient.jobs.update_contract_info import update_contract_info + +M_PATH = "uaclient.jobs.update_contract_info." + + +@mock.patch(M_PATH + "contract.is_contract_changed", return_value=False) +class TestUpdateContractInfo: + @pytest.mark.parametrize( + "contract_changed,is_attached", + ( + (False, True), + (True, False), + (True, True), + (False, False), + ), + ) + @mock.patch(M_PATH + "notices", autospec=True) + def test_is_contract_changed( + self, + m_notices, + m_contract_changed, + contract_changed, + is_attached, + FakeConfig, + ): + m_contract_changed.return_value = contract_changed + if is_attached: + cfg = FakeConfig().for_attached_machine() + else: + cfg = FakeConfig() + + update_contract_info(cfg=cfg) + + if is_attached: + if contract_changed: + assert [ + mock.call( + Notice.CONTRACT_REFRESH_WARNING, + ) + ] == m_notices.add.call_args_list + else: + assert [ + mock.call( + Notice.CONTRACT_REFRESH_WARNING, + ) + ] not in m_notices.add.call_args_list + assert [ + mock.call(Notice.CONTRACT_REFRESH_WARNING) + ] in m_notices.remove.call_args_list + else: + assert m_contract_changed.call_count == 0 + + @pytest.mark.parametrize( + "contract_changed", + ( + False, + True, + Exception("Error checking contract info"), + ), + ) + @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) + @mock.patch(M_PATH + "notices", autospec=True) + def test_contract_failure( + self, + m_notices, + m_contract_changed, + contract_changed, + caplog_text, + FakeConfig, + ): + m_contract_changed.side_effect = (contract_changed,) + m_notices.add.side_effect = Exception("Error checking contract info") + m_notices.remove.side_effect = Exception( + "Error checking contract info" + ) + cfg = FakeConfig().for_attached_machine() + + assert False is update_contract_info(cfg=cfg) + assert ( + "Failed to check for change in machine contract." + " Reason: Error checking contract info\n" + ) in caplog_text() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/tests/test_update_messaging.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/tests/test_update_messaging.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/tests/test_update_messaging.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/tests/test_update_messaging.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,33 +1,18 @@ import datetime -import os import mock import pytest -from uaclient import system -from uaclient.defaults import BASE_UA_URL, CONTRACT_EXPIRY_GRACE_PERIOD_DAYS +from uaclient import messages +from uaclient.api.u.pro.packages.updates.v1 import ( + PackageUpdatesResult, + UpdateSummary, +) from uaclient.entitlements.entitlement_status import ApplicationStatus from uaclient.jobs.update_messaging import ( - AWS_PRO_URL, - AZURE_PRO_URL, - AZURE_XENIAL_URL, - GCP_PRO_URL, - XENIAL_ESM_URL, ContractExpiryStatus, - ExternalMessage, - _write_esm_service_msg_templates, - get_contextual_esm_info_url, get_contract_expiry_status, - update_apt_and_motd_messages, - write_apt_and_motd_templates, - write_esm_announcement_message, -) -from uaclient.messages import ( - ANNOUNCE_ESM_APPS_TMPL, - CONTRACT_EXPIRED_MOTD_GRACE_PERIOD_TMPL, - CONTRACT_EXPIRED_MOTD_NO_PKGS_TMPL, - CONTRACT_EXPIRED_MOTD_PKGS_TMPL, - CONTRACT_EXPIRED_MOTD_SOON_TMPL, + update_motd_messages, ) M_PATH = "uaclient.jobs.update_messaging." @@ -60,539 +45,348 @@ contract_remaining_days, ) == get_contract_expiry_status(cfg) - -class TestGetContextualESMInfoURL: - @pytest.mark.parametrize( - "cloud, series, expected", - ( - (None, "xenial", (XENIAL_ESM_URL, " for 16.04")), - (None, "bionic", (BASE_UA_URL, "")), - (None, "focal", (BASE_UA_URL, "")), - (None, "jammy", (BASE_UA_URL, "")), - ("aws", "xenial", (XENIAL_ESM_URL, " for 16.04")), - ("aws", "bionic", (AWS_PRO_URL, " on AWS")), - ("aws", "focal", (AWS_PRO_URL, " on AWS")), - ("aws", "jammy", (AWS_PRO_URL, " on AWS")), - ("azure", "xenial", (AZURE_XENIAL_URL, " for 16.04 on Azure")), - ("azure", "bionic", (AZURE_PRO_URL, " on Azure")), - ("azure", "focal", (AZURE_PRO_URL, " on Azure")), - ("azure", "jammy", (AZURE_PRO_URL, " on Azure")), - ("gce", "xenial", (XENIAL_ESM_URL, " for 16.04")), - ("gce", "bionic", (GCP_PRO_URL, " on GCP")), - ("gce", "focal", (GCP_PRO_URL, " on GCP")), - ("gce", "jammy", (GCP_PRO_URL, " on GCP")), - ("somethingelse", "xenial", (XENIAL_ESM_URL, " for 16.04")), - ("somethingelse", "bionic", (BASE_UA_URL, "")), - ("somethingelse", "focal", (BASE_UA_URL, "")), - ("somethingelse", "jammy", (BASE_UA_URL, "")), - ), - ) - @mock.patch("uaclient.jobs.update_messaging.system.get_platform_info") - @mock.patch("uaclient.jobs.update_messaging.identity.get_cloud_type") - def test_get_contextual_esm_info_url( - self, m_get_cloud_type, m_get_platform_info, cloud, series, expected - ): - m_get_cloud_type.return_value = (cloud, None) - m_get_platform_info.return_value = {"series": series} - assert get_contextual_esm_info_url.__wrapped__() == expected - - -class TestWriteAPTAndMOTDTemplates: @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), - ), + "expiry,is_updated", + (("2040-05-08T19:02:26Z", False), ("2042-05-08T19:02:26Z", True)), ) - @mock.patch(M_PATH + "entitlements.entitlement_factory") - @mock.patch(M_PATH + "_remove_msg_templates") - @mock.patch(M_PATH + "_write_esm_service_msg_templates") - @mock.patch(M_PATH + "system.is_active_esm") - @mock.patch(M_PATH + "get_contract_expiry_status") - def test_write_apps_or_infra_services_mutually_exclusive( + @mock.patch("uaclient.files.MachineTokenFile.write") + @mock.patch(M_PATH + "contract.UAContractClient.get_updated_contract_info") + def test_update_contract_expiry( self, - get_contract_expiry_status, - util_is_active_esm, - write_esm_service_templates, - remove_msg_templates, - m_entitlement_factory, - series, - is_active_esm, - contract_days, - infra_enabled, - esm_apps_beta, - cfg_allow_beta, - show_infra, - show_apps, - FakeConfig, + get_updated_contract_info, + machine_token_write, + expiry, + is_updated, ): - """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, - contract_days, - ) - 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 - infra_obj.application_status.return_value = (infra_status, "") - infra_obj.name = "esm-infra" - apps_cls = mock.MagicMock() - apps_cls.is_beta = esm_apps_beta - apps_obj = apps_cls.return_value - apps_obj.name = "esm-apps" - - def factory_side_effect(cfg, name): - if name == "esm-infra": - return infra_cls - if name == "esm-apps": - return apps_cls - - m_entitlement_factory.side_effect = factory_side_effect - - 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}) - write_calls = [] - remove_calls = [] - if show_infra: - write_calls.append( - mock.call( - cfg, - mock.ANY, - ContractExpiryStatus.ACTIVE, - 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: - 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, - 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, - ) - ) + get_updated_contract_info.return_value = { + "machineTokenInfo": {"contractInfo": {"effectiveTo": expiry}} + } + if is_updated: + 1 == machine_token_write.call_count 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 + 0 == machine_token_write.call_count -class Test_WriteESMServiceAPTMsgTemplates: +class TestUpdateMotdMessages: @pytest.mark.parametrize( - "contract_status, remaining_days, is_active_esm, platform_info", - ( + [ + "attached", + "contract_expiry_statuses", + "is_current_series_active_esm", + "infra_status", + "is_current_series_lts", + "apps_status", + "updates", + "expected", + "update_contract_expiry_calls", + "ensure_file_absent_calls", + "write_file_calls", + ], + [ ( - ContractExpiryStatus.ACTIVE, - 21, + # not attached + False, + [], + False, + None, + False, + None, + None, + False, + [], + [], + [], + ), + ( + # somehow attached but none contract status + True, + [(ContractExpiryStatus.NONE, None)], + False, + None, + False, + None, + None, True, - {"series": "xenial", "release": "16.04"}, + [], + [mock.call(mock.ANY)], + [], ), ( - ContractExpiryStatus.ACTIVE_EXPIRED_SOON, - 10, + # active contract + True, + [(ContractExpiryStatus.ACTIVE, None)], + False, + None, + False, + None, + None, True, - {"series": "xenial", "release": "16.04"}, + [], + [mock.call(mock.ANY)], + [], ), ( - ContractExpiryStatus.EXPIRED_GRACE_PERIOD, - -5, + # expiring soon contract, updated to be active + True, + [ + (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, None), + (ContractExpiryStatus.ACTIVE, None), + ], + False, + None, + False, + None, + None, True, - {"series": "xenial", "release": "16.04"}, + [mock.call(mock.ANY)], + [mock.call(mock.ANY)], + [], ), ( - ContractExpiryStatus.EXPIRED, - -20, + # expired grace period contract, updated to be active + True, + [ + (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, None), + (ContractExpiryStatus.ACTIVE, None), + ], + False, + None, + False, + None, + None, True, - {"series": "xenial", "release": "16.04"}, + [mock.call(mock.ANY)], + [mock.call(mock.ANY)], + [], ), ( - ContractExpiryStatus.EXPIRED, - -20, + # expired contract, updated to be active + True, + [ + (ContractExpiryStatus.EXPIRED, None), + (ContractExpiryStatus.ACTIVE, None), + ], False, - {"series": "xenial", "release": "16.04"}, + None, + False, + None, + None, + True, + [mock.call(mock.ANY)], + [mock.call(mock.ANY)], + [], ), - ), - ) - @mock.patch(M_PATH + "system.get_platform_info") - @mock.patch(M_PATH + "system.is_active_esm") - @mock.patch( - M_PATH + "entitlements.repo.RepoEntitlement.application_status" - ) - def test_apt_templates_written_for_enabled_services_by_contract_status( - 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 - disabled_status = ApplicationStatus.ENABLED, "" - 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="Ubuntu Pro: ESM Apps" - ) - pkgs_tmpl = tmpdir.join("pkgs-msg.tmpl") - 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") - pkgs_tmpl.write("oldcache") - no_pkgs_tmpl.write("oldcache") - motd_pkgs_tmpl.write("oldcache") - motd_no_pkgs_tmpl.write("oldcache") - no_pkgs_msg_file = no_pkgs_tmpl.strpath.replace(".tmpl", "") - pkgs_msg_file = pkgs_tmpl.strpath.replace(".tmpl", "") - system.write_file(no_pkgs_msg_file, "oldcache") - system.write_file(pkgs_msg_file, "oldcache") - - now = datetime.datetime.utcnow() - expire_date = now + datetime.timedelta(days=remaining_days) - cfg = FakeConfig.for_attached_machine() - m_token = cfg.machine_token - m_token["machineTokenInfo"]["contractInfo"][ - "effectiveTo" - ] = expire_date - - _write_esm_service_msg_templates( - cfg, - m_ent_obj, - contract_status, - remaining_days, - pkgs_tmpl.strpath, - no_pkgs_tmpl.strpath, - motd_pkgs_tmpl.strpath, - motd_no_pkgs_tmpl.strpath, - ) - if contract_status == ContractExpiryStatus.ACTIVE: - # Old messages removed on ACTIVE status - assert False is os.path.exists(pkgs_tmpl.strpath) - assert False is os.path.exists(no_pkgs_tmpl.strpath) - assert False is os.path.exists(motd_pkgs_tmpl.strpath) - assert False is os.path.exists(motd_no_pkgs_tmpl.strpath) - assert False is os.path.exists(pkgs_msg_file) - assert False is os.path.exists(no_pkgs_msg_file) - elif contract_status == ContractExpiryStatus.ACTIVE_EXPIRED_SOON: - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_SOON_TMPL.format( - remaining_days=remaining_days, - ) - assert motd_pkgs_msg == motd_pkgs_tmpl.read() - assert motd_pkgs_msg == motd_no_pkgs_tmpl.read() - elif contract_status == ContractExpiryStatus.EXPIRED_GRACE_PERIOD: - exp_dt = cfg.machine_token_file.contract_expiry_datetime - exp_dt = exp_dt.strftime("%d %b %Y") - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_GRACE_PERIOD_TMPL.format( - expired_date=exp_dt, - remaining_days=remaining_days - + CONTRACT_EXPIRY_GRACE_PERIOD_DAYS, - ) - assert motd_pkgs_msg == motd_pkgs_tmpl.read() - assert motd_pkgs_msg == motd_no_pkgs_tmpl.read() - elif contract_status == ContractExpiryStatus.EXPIRED: - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_PKGS_TMPL.format( - pkg_num="{ESM_APPS_PKG_COUNT}", - service="esm-apps", - ) - motd_no_pkgs_msg = CONTRACT_EXPIRED_MOTD_NO_PKGS_TMPL - assert motd_pkgs_msg == motd_pkgs_tmpl.read() - assert motd_no_pkgs_msg == motd_no_pkgs_tmpl.read() - - -class TestWriteESMAnnouncementMessage: - @pytest.mark.parametrize( - "series,release,is_active_esm,is_beta,cfg_allow_beta," - "apps_enabled,expected", - ( - # ESMApps.is_beta == True no Announcement - ("xenial", "16.04", True, True, None, False, None), - # Once release begins ESM and ESMApps.is_beta is false announce ( - "xenial", - "16.04", + # expiring soon for real True, + [ + (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, 3), + (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, 3), + ], False, None, False, - "\n" + ANNOUNCE_ESM_APPS_TMPL.format(url=XENIAL_ESM_URL), + None, + None, + True, + [mock.call(mock.ANY)], + [], + [ + mock.call( + mock.ANY, + messages.CONTRACT_EXPIRES_SOON_MOTD.format( + remaining_days=3 + ), + ) + ], ), - # allow_beta uaclient.config overrides is_beta and days_until_esm ( - "xenial", - "16.04", + # expired grace period for real + True, + [ + (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, -3), + (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, -3), + ], + False, + None, + False, + None, + None, True, + [mock.call(mock.ANY)], + [], + [ + mock.call( + mock.ANY, + messages.CONTRACT_EXPIRED_GRACE_PERIOD_MOTD.format( + remaining_days=11, expired_date="21 Dec 2012" + ), + ) + ], + ), + ( + # expired, eol release, esm-infra not enabled True, + [ + (ContractExpiryStatus.EXPIRED, 3), + (ContractExpiryStatus.EXPIRED, 3), + ], True, + (ApplicationStatus.DISABLED, None), False, - "\n" + ANNOUNCE_ESM_APPS_TMPL.format(url=XENIAL_ESM_URL), + None, + None, + True, + [mock.call(mock.ANY)], + [], + [mock.call(mock.ANY, messages.CONTRACT_EXPIRED_MOTD_NO_PKGS)], ), - # when esm-apps already enabled don't show - ("xenial", "16.04", True, False, True, True, None), ( - "bionic", - "18.04", + # expired, lts release, esm-apps not enabled + True, + [ + (ContractExpiryStatus.EXPIRED, 3), + (ContractExpiryStatus.EXPIRED, 3), + ], False, + None, + True, + (ApplicationStatus.DISABLED, None), + None, + True, + [mock.call(mock.ANY)], + [], + [mock.call(mock.ANY, messages.CONTRACT_EXPIRED_MOTD_NO_PKGS)], + ), + ( + # expired, interim release + True, + [ + (ContractExpiryStatus.EXPIRED, 3), + (ContractExpiryStatus.EXPIRED, 3), + ], False, None, False, - "\n" + ANNOUNCE_ESM_APPS_TMPL.format(url=BASE_UA_URL), + None, + None, + True, + [mock.call(mock.ANY)], + [], + [mock.call(mock.ANY, messages.CONTRACT_EXPIRED_MOTD_NO_PKGS)], ), ( - "focal", - "20.04", - False, + # expired, eol release, esm-infra enabled + True, + [ + (ContractExpiryStatus.EXPIRED, 3), + (ContractExpiryStatus.EXPIRED, 3), + ], + True, + (ApplicationStatus.ENABLED, None), False, None, + PackageUpdatesResult(UpdateSummary(0, 0, 4, 0, 0), []), + True, + [mock.call(mock.ANY)], + [], + [ + mock.call( + mock.ANY, + messages.CONTRACT_EXPIRED_MOTD_PKGS.format( + service="esm-infra", pkg_num=4 + ), + ) + ], + ), + ( + # expired, lts release, esm-apps enabled + True, + [ + (ContractExpiryStatus.EXPIRED, 3), + (ContractExpiryStatus.EXPIRED, 3), + ], False, - "\n" + ANNOUNCE_ESM_APPS_TMPL.format(url=BASE_UA_URL), + None, + True, + (ApplicationStatus.ENABLED, None), + PackageUpdatesResult(UpdateSummary(0, 5, 0, 0, 0), []), + True, + [mock.call(mock.ANY)], + [], + [ + mock.call( + mock.ANY, + messages.CONTRACT_EXPIRED_MOTD_PKGS.format( + service="esm-apps", pkg_num=5 + ), + ) + ], ), - ), + ], ) + @mock.patch(M_PATH + "api_u_pro_packages_updates_v1") + @mock.patch(M_PATH + "ESMAppsEntitlement.application_status") + @mock.patch(M_PATH + "system.is_current_series_lts") + @mock.patch(M_PATH + "ESMInfraEntitlement.application_status") + @mock.patch(M_PATH + "system.is_current_series_active_esm") @mock.patch( - M_PATH + "entitlements.repo.RepoEntitlement.application_status" + M_PATH + "UAConfig.machine_token_file", new_callable=mock.PropertyMock ) - @mock.patch(M_PATH + "entitlements.entitlement_factory") - @mock.patch(M_PATH + "system.is_active_esm") - @mock.patch(M_PATH + "system.get_platform_info") - def test_message_based_on_beta_status_and_count_until_active_esm( - self, - get_platform_info, - util_is_active_esm, - m_entitlement_factory, - esm_application_status, - series, - release, - is_active_esm, - is_beta, - cfg_allow_beta, - apps_enabled, - expected, - FakeConfig, - ): - get_contextual_esm_info_url.cache_clear() - get_platform_info.return_value = {"series": series, "release": release} - system.is_active_esm.return_value = is_active_esm - - cfg = FakeConfig.for_attached_machine() - msg_dir = os.path.join(cfg.data_dir, "messages") - os.makedirs(msg_dir) - esm_news_path = os.path.join(msg_dir, "motd-esm-announce") - - if cfg_allow_beta: - cfg.override_features({"allow_beta": cfg_allow_beta}) - - m_entitlement_cls = mock.MagicMock() - m_entitlement_cls.is_beta = is_beta - m_ent_obj = m_entitlement_cls.return_value - if apps_enabled: - status_return = ApplicationStatus.ENABLED, "" - else: - status_return = ApplicationStatus.DISABLED, "" - m_ent_obj.application_status.return_value = status_return - - m_entitlement_factory.return_value = m_entitlement_cls - - write_esm_announcement_message(cfg, series) - if expected is None: - assert False is os.path.exists(esm_news_path) - else: - assert expected == system.load_file(esm_news_path) - - -class TestUpdateAPTandMOTDMessages: - @pytest.mark.parametrize( - "series,is_lts,esm_active,cfg_allow_beta", - ( - ("xenial", True, True, True), - ("bionic", True, False, True), - ("bionic", True, False, None), - ("groovy", False, False, None), - ), + @mock.patch(M_PATH + "system.write_file") + @mock.patch(M_PATH + "system.ensure_file_absent") + @mock.patch(M_PATH + "update_contract_expiry") + @mock.patch(M_PATH + "get_contract_expiry_status") + @mock.patch( + M_PATH + "UAConfig.is_attached", new_callable=mock.PropertyMock ) - @mock.patch(M_PATH + "system.is_lts") - @mock.patch(M_PATH + "system.is_active_esm") - @mock.patch(M_PATH + "write_apt_and_motd_templates") - @mock.patch(M_PATH + "write_esm_announcement_message") - @mock.patch(M_PATH + "system.subp") - @mock.patch(M_PATH + "system.get_platform_info") - def test_motd_and_apt_templates_written_separately( + def test_update_motd_messages( self, - get_platform_info, - subp, - write_esm_announcement_message, - write_apt_and_motd_templates, - is_active_esm, - util_is_lts, - series, - is_lts, - esm_active, - cfg_allow_beta, + m_is_attached, + m_get_contract_expiry_status, + m_update_contract_expiry, + m_ensure_file_absent, + m_write_file, + m_machine_token_file, + m_is_current_series_active_esm, + m_infra_status, + m_is_current_series_lts, + m_apps_status, + m_api_updates_v1, + attached, + contract_expiry_statuses, + is_current_series_active_esm, + infra_status, + is_current_series_lts, + apps_status, + updates, + expected, + update_contract_expiry_calls, + ensure_file_absent_calls, + write_file_calls, FakeConfig, ): - """Update message templates for LTS releases with esm active. + m_is_attached.return_value = attached + m_get_contract_expiry_status.side_effect = contract_expiry_statuses + m_is_current_series_active_esm.return_value = ( + is_current_series_active_esm + ) + m_infra_status.return_value = infra_status + m_is_current_series_lts.return_value = is_current_series_lts + m_apps_status.return_value = apps_status + m_api_updates_v1.return_value = updates + + machine_token_file = mock.MagicMock() + machine_token_file.contract_expiry_datetime = datetime.datetime( + 2012, 12, 21 + ) + m_machine_token_file.return_value = machine_token_file - Assert cleanup of cached template and rendered message files when - non-LTS release. + assert expected == update_motd_messages(FakeConfig()) - Allow config allow_beta overrides. - """ - get_platform_info.return_value = {"series": series} - util_is_lts.return_value = is_lts - is_active_esm.return_value = esm_active - cfg = FakeConfig.for_attached_machine() - if cfg_allow_beta: - cfg.override_features({"allow_beta": cfg_allow_beta}) - msg_dir = os.path.join(cfg.data_dir, "messages") - if not is_lts: - # setup old msg files to assert they are removed - os.makedirs(msg_dir) - for msg_enum in ExternalMessage: - msg_path = os.path.join(msg_dir, msg_enum.value) - system.write_file(msg_path, "old") - system.write_file(msg_path.replace(".tmpl", ""), "old") - - update_apt_and_motd_messages(cfg) - os.path.exists(os.path.join(cfg.data_dir, "messages")) - - if is_lts: - write_apt_calls = [mock.call(cfg, series)] - esm_announce_calls = [mock.call(cfg, series)] - subp_calls = [ - mock.call( - [ - "/usr/lib/ubuntu-advantage/apt-esm-hook", - ] - ) - ] - else: - write_apt_calls = esm_announce_calls = [] - subp_calls = [] - # Cached msg templates removed on non-LTS - for msg_enum in ExternalMessage: - msg_path = os.path.join(msg_dir, msg_enum.value) - assert False is os.path.exists(msg_path) - assert False is os.path.exists(msg_path.replace(".tmpl", "")) assert ( - esm_announce_calls == write_esm_announcement_message.call_args_list + update_contract_expiry_calls + == m_update_contract_expiry.call_args_list ) - assert write_apt_calls == write_apt_and_motd_templates.call_args_list - assert subp_calls == subp.call_args_list - - @pytest.mark.parametrize( - "expiry_status,call_count", - ( - (ContractExpiryStatus.ACTIVE, 0), - (ContractExpiryStatus.ACTIVE_EXPIRED_SOON, 1), - (ContractExpiryStatus.EXPIRED_GRACE_PERIOD, 1), - ), - ) - @mock.patch(M_PATH + "update_contract_expiry") - @mock.patch(M_PATH + "get_contract_expiry_status") - @mock.patch(M_PATH + "system.is_lts") - @mock.patch(M_PATH + "system.is_active_esm") - @mock.patch(M_PATH + "write_apt_and_motd_templates") - @mock.patch(M_PATH + "write_esm_announcement_message") - @mock.patch(M_PATH + "system.subp") - @mock.patch(M_PATH + "system.get_platform_info") - def test_contract_expiry_motd_and_apt_messages( - self, - _get_platform_info, - _subp, - _write_esm_announcement_message, - _write_apt_and_motd_templates, - _is_active_esm, - _util_is_lts, - get_contract_expiry_status, - update_contract_expiry, - expiry_status, - call_count, - FakeConfig, - ): - get_contract_expiry_status.return_value = expiry_status, 0 - cfg = FakeConfig.for_attached_machine() - update_apt_and_motd_messages(cfg) - assert update_contract_expiry.call_count == call_count - - @pytest.mark.parametrize( - "expiry,is_updated", - (("2040-05-08T19:02:26Z", False), ("2042-05-08T19:02:26Z", True)), - ) - @mock.patch("uaclient.files.MachineTokenFile.write") - @mock.patch(M_PATH + "contract.UAContractClient.get_updated_contract_info") - def test_update_contract_expiry( - self, - get_updated_contract_info, - machine_token_write, - expiry, - is_updated, - ): - get_updated_contract_info.return_value = { - "machineTokenInfo": {"contractInfo": {"effectiveTo": expiry}} - } - if is_updated: - 1 == machine_token_write.call_count - else: - 0 == machine_token_write.call_count + assert ensure_file_absent_calls == m_ensure_file_absent.call_args_list + assert write_file_calls == m_write_file.call_args_list diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/update_contract_info.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/update_contract_info.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/update_contract_info.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/update_contract_info.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,29 @@ +import logging + +from uaclient import contract, messages, util +from uaclient.config import UAConfig +from uaclient.files import notices +from uaclient.files.notices import Notice + +LOG = logging.getLogger(__name__) + + +def update_contract_info(cfg: UAConfig) -> bool: + if cfg.is_attached: + try: + if contract.is_contract_changed(cfg): + notices.add( + Notice.CONTRACT_REFRESH_WARNING, + ) + else: + notices.remove( + Notice.CONTRACT_REFRESH_WARNING, + ) + except Exception as e: + with util.disable_log_to_console(): + err_msg = messages.UPDATE_CHECK_CONTRACT_FAILURE.format( + reason=str(e) + ) + LOG.warning(err_msg) + return False + return True diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/update_messaging.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/update_messaging.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/jobs/update_messaging.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/jobs/update_messaging.py 2023-04-05 15:14:00.000000000 +0000 @@ -9,26 +9,21 @@ import enum import logging import os -from functools import lru_cache from os.path import exists -from typing import List, Tuple +from typing import Tuple -from uaclient import config, contract, defaults, entitlements, system, util -from uaclient.clouds import identity -from uaclient.entitlements.entitlement_status import ApplicationStatus -from uaclient.messages import ( - ANNOUNCE_ESM_APPS_TMPL, - CONTRACT_EXPIRED_MOTD_GRACE_PERIOD_TMPL, - CONTRACT_EXPIRED_MOTD_NO_PKGS_TMPL, - CONTRACT_EXPIRED_MOTD_PKGS_TMPL, - CONTRACT_EXPIRED_MOTD_SOON_TMPL, +from uaclient import contract, defaults, messages, system +from uaclient.api.u.pro.packages.updates.v1 import ( + _updates as api_u_pro_packages_updates_v1, ) +from uaclient.config import UAConfig +from uaclient.entitlements import ESMAppsEntitlement, ESMInfraEntitlement +from uaclient.entitlements.entitlement_status import ApplicationStatus -XENIAL_ESM_URL = "https://ubuntu.com/16-04" -AZURE_PRO_URL = "https://ubuntu.com/azure/pro" -AZURE_XENIAL_URL = "https://ubuntu.com/16-04/azure" -AWS_PRO_URL = "https://ubuntu.com/aws/pro" -GCP_PRO_URL = "https://ubuntu.com/gcp/pro" +MOTD_CONTRACT_STATUS_FILE_NAME = "motd-contract-status" +UPDATE_NOTIFIER_MOTD_SCRIPT = ( + "/usr/lib/update-notifier/update-motd-updates-available" +) @enum.unique @@ -40,29 +35,35 @@ EXPIRED = 4 -# Type of message file used for external messaging (APT and MOTD) -@enum.unique -class ExternalMessage(enum.Enum): - MOTD_APPS_NO_PKGS = "motd-no-packages-apps.tmpl" - MOTD_INFRA_NO_PKGS = "motd-no-packages-infra.tmpl" - MOTD_APPS_PKGS = "motd-packages-apps.tmpl" - MOTD_INFRA_PKGS = "motd-packages-infra.tmpl" - APT_PRE_INVOKE_APPS_NO_PKGS = "apt-pre-invoke-no-packages-apps.tmpl" - APT_PRE_INVOKE_INFRA_NO_PKGS = "apt-pre-invoke-no-packages-infra.tmpl" - APT_PRE_INVOKE_APPS_PKGS = "apt-pre-invoke-packages-apps.tmpl" - APT_PRE_INVOKE_INFRA_PKGS = "apt-pre-invoke-packages-infra.tmpl" - APT_PRE_INVOKE_SERVICE_STATUS = "apt-pre-invoke-esm-service-status" - MOTD_ESM_SERVICE_STATUS = "motd-esm-service-status" - ESM_ANNOUNCE = "motd-esm-announce" - - -UPDATE_NOTIFIER_MOTD_SCRIPT = ( - "/usr/lib/update-notifier/update-motd-updates-available" -) +def update_contract_expiry(cfg: UAConfig): + orig_token = cfg.machine_token + machine_token = orig_token.get("machineToken", "") + contract_id = ( + orig_token.get("machineTokenInfo", {}) + .get("contractInfo", {}) + .get("id", None) + ) + contract_client = contract.UAContractClient(cfg) + resp = contract_client.get_updated_contract_info( + machine_token, contract_id + ) + resp_expiry = ( + resp.get("machineTokenInfo", {}) + .get("contractInfo", {}) + .get("effectiveTo", None) + ) + if ( + resp_expiry is not None + and resp_expiry != cfg.machine_token_file.contract_expiry_datetime + ): + orig_token["machineTokenInfo"]["contractInfo"][ + "effectiveTo" + ] = resp_expiry + cfg.machine_token_file.write(orig_token) def get_contract_expiry_status( - cfg: config.UAConfig, + cfg: UAConfig, ) -> Tuple[ContractExpiryStatus, int]: """Return a tuple [ContractExpiryStatus, num_days]""" if not cfg.is_attached: @@ -88,278 +89,90 @@ return ContractExpiryStatus.ACTIVE, remaining_days -@lru_cache(maxsize=None) -def get_contextual_esm_info_url() -> Tuple[str, str]: - cloud, _ = identity.get_cloud_type() - series = system.get_platform_info()["series"] - - is_aws = False - is_gcp = False - is_azure = False - non_azure_cloud = False - if cloud is not None: - is_aws = cloud.startswith("aws") - is_gcp = cloud.startswith("gce") - is_azure = cloud.startswith("azure") - non_azure_cloud = not is_azure - - is_xenial = series == "xenial" - - if cloud is None and not is_xenial: - return (defaults.BASE_UA_URL, "") - if (cloud is None or non_azure_cloud) and is_xenial: - return (XENIAL_ESM_URL, " for 16.04") - if is_azure and not is_xenial: - return (AZURE_PRO_URL, " on Azure") - if is_azure and is_xenial: - return (AZURE_XENIAL_URL, " for 16.04 on Azure") - if is_aws and not is_xenial: - return (AWS_PRO_URL, " on AWS") - if is_gcp and not is_xenial: - return (GCP_PRO_URL, " on GCP") - - # default case - return (defaults.BASE_UA_URL, "") - - -def _write_template_or_remove(msg: str, tmpl_file: str): - """Write a template to tmpl_file. - - When msg is empty, remove both tmpl_file and the generated msg. - """ - if msg: - system.write_file(tmpl_file, msg) - else: - system.ensure_file_absent(tmpl_file) - if tmpl_file.endswith(".tmpl"): - system.ensure_file_absent(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, - expiry_status: ContractExpiryStatus, - remaining_days: int, - pkgs_file: str, - no_pkgs_file: str, - motd_pkgs_file: str, - motd_no_pkgs_file: str, -): - """Write any related template content for an ESM service. +def update_motd_messages(cfg: UAConfig) -> bool: + """Emit human-readable status message used by motd. - If no content is applicable for the current service state, remove - all service-related template files. + Used by /etc/update.motd.d/91-contract-ua-esm-status :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 = "" - tmpl_prefix = ent.name.upper().replace("-", "_") - tmpl_pkg_count_var = "{{{}_PKG_COUNT}}".format(tmpl_prefix) - - if ent.application_status()[0] == ApplicationStatus.ENABLED: - if expiry_status == ContractExpiryStatus.ACTIVE_EXPIRED_SOON: - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_SOON_TMPL.format( - remaining_days=remaining_days, - ) - motd_no_pkgs_msg = motd_pkgs_msg - elif expiry_status == ContractExpiryStatus.EXPIRED_GRACE_PERIOD: - grace_period_remaining = ( - defaults.CONTRACT_EXPIRY_GRACE_PERIOD_DAYS + remaining_days - ) - exp_dt = cfg.machine_token_file.contract_expiry_datetime - if exp_dt is None: - exp_dt_str = "Unknown" - else: - exp_dt_str = exp_dt.strftime("%d %b %Y") - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_GRACE_PERIOD_TMPL.format( - expired_date=exp_dt_str, - remaining_days=grace_period_remaining, - ) - motd_no_pkgs_msg = motd_pkgs_msg - elif expiry_status == ContractExpiryStatus.EXPIRED: - motd_pkgs_msg = CONTRACT_EXPIRED_MOTD_PKGS_TMPL.format( - pkg_num=tmpl_pkg_count_var, - service=ent.name, - ) - motd_no_pkgs_msg = CONTRACT_EXPIRED_MOTD_NO_PKGS_TMPL - - msg_dir = os.path.join(cfg.data_dir, "messages") - _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( - motd_no_pkgs_msg, os.path.join(msg_dir, motd_no_pkgs_file) - ) - _write_template_or_remove( - motd_pkgs_msg, os.path.join(msg_dir, motd_pkgs_file) - ) - - -def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None: - """Write messaging templates about available esm packages. + if not cfg.is_attached: + return False - :param cfg: UAConfig instance for this environment. - :param series: string of Ubuntu release series: 'xenial'. - """ - apps_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value - apps_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value - infra_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value - infra_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value - motd_apps_no_pkg_file = ExternalMessage.MOTD_APPS_NO_PKGS.value - motd_apps_pkg_file = ExternalMessage.MOTD_APPS_PKGS.value - motd_infra_no_pkg_file = ExternalMessage.MOTD_INFRA_NO_PKGS.value - motd_infra_pkg_file = ExternalMessage.MOTD_INFRA_PKGS.value - msg_dir = os.path.join(cfg.data_dir, "messages") - - apps_cls = entitlements.entitlement_factory(cfg=cfg, 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" + logging.debug("Updating Ubuntu Pro messages for MOTD.") + motd_contract_status_msg_path = os.path.join( + cfg.data_dir, "messages", MOTD_CONTRACT_STATUS_FILE_NAME ) - apps_valid = bool(config_allow_beta or not apps_cls.is_beta) - infra_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-infra") - infra_inst = infra_cls(cfg) expiry_status, remaining_days = get_contract_expiry_status(cfg) + if expiry_status in ( + ContractExpiryStatus.ACTIVE_EXPIRED_SOON, + ContractExpiryStatus.EXPIRED_GRACE_PERIOD, + ContractExpiryStatus.EXPIRED, + ): + update_contract_expiry(cfg) + expiry_status, remaining_days = get_contract_expiry_status(cfg) - enabled_status = ApplicationStatus.ENABLED - msg_esm_apps = False - msg_esm_infra = False - if system.is_active_esm(series): - if infra_inst.application_status()[0] != enabled_status: - msg_esm_infra = True - elif remaining_days <= defaults.CONTRACT_EXPIRY_PENDING_DAYS: - msg_esm_infra = True - if not msg_esm_infra: - # write_apt_and_motd_templates is only called if system.is_lts(series) - msg_esm_apps = apps_valid - - if msg_esm_infra: - _write_esm_service_msg_templates( - cfg, - infra_inst, - expiry_status, - remaining_days, - infra_pkg_file, - infra_no_pkg_file, - motd_infra_pkg_file, - motd_infra_no_pkg_file, - ) - else: - _remove_msg_templates( - msg_dir=msg_dir, - 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=msg_dir, - msg_template_names=[ - apps_pkg_file, - apps_no_pkg_file, - motd_apps_pkg_file, - motd_apps_no_pkg_file, - ], - ) - - -def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None: - """Write human-readable messages if ESM is offered on this LTS release. - - Do not write ESM announcements if esm-apps is enabled or beta. - - :param cfg: UAConfig instance for this environment. - :param series: string of Ubuntu release series: 'xenial'. - """ - apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps") - apps_inst = apps_cls(cfg) - enabled_status = ApplicationStatus.ENABLED - apps_not_enabled = apps_inst.application_status()[0] != enabled_status - 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) - - msg_dir = os.path.join(cfg.data_dir, "messages") - esm_news_file = os.path.join(msg_dir, ExternalMessage.ESM_ANNOUNCE.value) - if apps_not_beta and apps_not_enabled: - url, _ = get_contextual_esm_info_url() - system.write_file( - esm_news_file, - "\n" + ANNOUNCE_ESM_APPS_TMPL.format(url=url), - ) - else: - system.ensure_file_absent(esm_news_file) - - -def update_apt_and_motd_messages(cfg: config.UAConfig) -> bool: - """Emit templates and human-readable status messages in msg_dir. - - These structured messages will be sourced by both /etc/update.motd.d - and APT UA-configured hooks. APT hook content will orginate from - apt-hook/hook.cc - - Call apt-esm-hook to render final human-readable - messages. - - :param cfg: UAConfig instance for this environment. - """ - logging.debug("Updating Ubuntu Pro messages for APT and MOTD.") - msg_dir = os.path.join(cfg.data_dir, "messages") - if not os.path.exists(msg_dir): - os.makedirs(msg_dir) - - series = system.get_platform_info()["series"] - if not system.is_lts(series): - # ESM is only on LTS releases. Remove all messages and templates. - for msg_enum in ExternalMessage: - msg_path = os.path.join(msg_dir, msg_enum.value) - system.ensure_file_absent(msg_path) - if msg_path.endswith(".tmpl"): - system.ensure_file_absent(msg_path.replace(".tmpl", "")) - return True - - expiry_status, _ = get_contract_expiry_status(cfg) - if expiry_status not in ( + if expiry_status in ( ContractExpiryStatus.ACTIVE, ContractExpiryStatus.NONE, ): - update_contract_expiry(cfg) - - # Announce ESM availabilty on active ESM LTS releases - write_esm_announcement_message(cfg, series) - write_apt_and_motd_templates(cfg, series) - # Now that we've setup/cleanedup templates render them with apt-hook - try: - system.subp(["/usr/lib/ubuntu-advantage/apt-esm-hook"]) - except Exception as exc: - logging.debug("failed to run apt-esm-hook: %s", str(exc)) + system.ensure_file_absent(motd_contract_status_msg_path) + elif expiry_status == ContractExpiryStatus.ACTIVE_EXPIRED_SOON: + system.write_file( + motd_contract_status_msg_path, + messages.CONTRACT_EXPIRES_SOON_MOTD.format( + remaining_days=remaining_days, + ), + ) + elif expiry_status == ContractExpiryStatus.EXPIRED_GRACE_PERIOD: + grace_period_remaining = ( + defaults.CONTRACT_EXPIRY_GRACE_PERIOD_DAYS + remaining_days + ) + exp_dt = cfg.machine_token_file.contract_expiry_datetime + if exp_dt is None: + exp_dt_str = "Unknown" + else: + exp_dt_str = exp_dt.strftime("%d %b %Y") + system.write_file( + motd_contract_status_msg_path, + messages.CONTRACT_EXPIRED_GRACE_PERIOD_MOTD.format( + expired_date=exp_dt_str, + remaining_days=grace_period_remaining, + ), + ) + elif expiry_status == ContractExpiryStatus.EXPIRED: + service = "n/a" + pkg_num = 0 + + if system.is_current_series_active_esm(): + esm_infra_status, _ = ESMInfraEntitlement(cfg).application_status() + if esm_infra_status == ApplicationStatus.ENABLED: + service = "esm-infra" + pkg_num = api_u_pro_packages_updates_v1( + cfg + ).summary.num_esm_infra_updates + elif system.is_current_series_lts(): + esm_apps_status, _ = ESMAppsEntitlement(cfg).application_status() + if esm_apps_status == ApplicationStatus.ENABLED: + service = "esm-apps" + pkg_num = api_u_pro_packages_updates_v1( + cfg + ).summary.num_esm_apps_updates + + if pkg_num == 0: + system.write_file( + motd_contract_status_msg_path, + messages.CONTRACT_EXPIRED_MOTD_NO_PKGS, + ) + else: + system.write_file( + motd_contract_status_msg_path, + messages.CONTRACT_EXPIRED_MOTD_PKGS.format( + pkg_num=pkg_num, + service=service, + ), + ) return True @@ -375,35 +188,3 @@ system.subp([UPDATE_NOTIFIER_MOTD_SCRIPT, "--force"]) except Exception as exc: logging.exception(exc) - - system.subp(["sudo", "systemctl", "restart", "motd-news.service"]) - - -def update_contract_expiry(cfg: config.UAConfig): - orig_token = cfg.machine_token - machine_token = orig_token.get("machineToken", "") - contract_id = ( - orig_token.get("machineTokenInfo", {}) - .get("contractInfo", {}) - .get("id", None) - ) - - contract_client = contract.UAContractClient(cfg) - resp = contract_client.get_updated_contract_info( - machine_token, contract_id - ) - resp_expiry = ( - resp.get("machineTokenInfo", {}) - .get("contractInfo", {}) - .get("effectiveTo", None) - ) - new_expiry = ( - util.parse_rfc3339_date(resp_expiry) - if resp_expiry - else cfg.machine_token_file.contract_expiry_datetime - ) - if cfg.machine_token_file.contract_expiry_datetime != new_expiry: - orig_token["machineTokenInfo"]["contractInfo"][ - "effectiveTo" - ] = new_expiry - cfg.machine_token_file.write(orig_token) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/livepatch.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/livepatch.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/livepatch.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/livepatch.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,397 @@ +import datetime +import json +import logging +import re +from functools import lru_cache +from typing import List, Optional, Tuple + +from uaclient import ( + event_logger, + exceptions, + messages, + serviceclient, + system, + util, +) +from uaclient.data_types import ( + BoolDataValue, + DataObject, + Field, + IncorrectTypeError, + StringDataValue, + data_list, +) +from uaclient.files import state_files + +HTTP_PROXY_OPTION = "http-proxy" +HTTPS_PROXY_OPTION = "https-proxy" + +LIVEPATCH_CMD = "/snap/bin/canonical-livepatch" + +LIVEPATCH_API_V1_KERNELS_SUPPORTED = "/v1/api/kernels/supported" + +event = event_logger.get_event_logger() + + +class LivepatchPatchFixStatus(DataObject): + fields = [ + Field("name", StringDataValue, required=False, dict_key="Name"), + Field("patched", BoolDataValue, required=False, dict_key="Patched"), + ] + + def __init__( + self, + name: Optional[str], + patched: Optional[bool], + ): + self.name = name + self.patched = patched + + +class LivepatchPatchStatus(DataObject): + fields = [ + Field("state", StringDataValue, required=False, dict_key="State"), + Field( + "fixes", + data_list(LivepatchPatchFixStatus), + required=False, + dict_key="Fixes", + ), + ] + + def __init__( + self, + state: Optional[str], + fixes: Optional[List[LivepatchPatchFixStatus]], + ): + self.state = state + self.fixes = fixes + + +class LivepatchStatusStatus(DataObject): + fields = [ + Field("kernel", StringDataValue, required=False, dict_key="Kernel"), + Field( + "livepatch", + LivepatchPatchStatus, + required=False, + dict_key="Livepatch", + ), + Field( + "supported", + StringDataValue, + required=False, + dict_key="Supported", + ), + ] + + def __init__( + self, + kernel: Optional[str], + livepatch: Optional[LivepatchPatchStatus], + supported: Optional[str], + ): + self.kernel = kernel + self.livepatch = livepatch + self.supported = supported + + +class LivepatchStatus(DataObject): + fields = [ + Field( + "status", + data_list(LivepatchStatusStatus), + required=False, + dict_key="Status", + ), + ] + + def __init__( + self, + status: Optional[List[LivepatchStatusStatus]], + ): + self.status = status + + +def status() -> Optional[LivepatchStatusStatus]: + if not is_livepatch_installed(): + logging.debug("canonical-livepatch is not installed") + return None + + try: + out, _ = system.subp([LIVEPATCH_CMD, "status", "--format", "json"]) + except exceptions.ProcessExecutionError: + with util.disable_log_to_console(): + logging.warning( + "canonical-livepatch returned error when checking status" + ) + return None + + try: + status_json = json.loads(out) + except json.JSONDecodeError: + with util.disable_log_to_console(): + logging.warning( + "canonical-livepatch status returned invalid json: {}".format( + out + ) + ) + return None + + try: + status_root = LivepatchStatus.from_dict(status_json) + except IncorrectTypeError: + with util.disable_log_to_console(): + logging.warning( + "canonical-livepatch status returned unexpected " + "structure: {}".format(out) + ) + return None + + if status_root.status is None or len(status_root.status) < 1: + logging.debug("canonical-livepatch has no status") + return None + + return status_root.status[0] + + +class UALivepatchClient(serviceclient.UAServiceClient): + + cfg_url_base_attr = "livepatch_url" + api_error_cls = exceptions.UrlError + + def is_kernel_supported( + self, version: str, flavor: str, arch: str, codename: str + ) -> Optional[bool]: + """ + :returns: True if supported + False if unsupported + None if API returns error or ambiguous response + """ + query_params = { + "kernel-version": version, + "flavour": flavor, + "architecture": arch, + "codename": codename, + } + headers = self.headers() + try: + result, _headers = self.request_url( + LIVEPATCH_API_V1_KERNELS_SUPPORTED, + query_params=query_params, + headers=headers, + ) + except Exception as e: + with util.disable_log_to_console(): + logging.warning( + "error while checking livepatch supported kernels API" + ) + logging.warning(e) + return None + + if not isinstance(result, dict): + logging.warning( + "livepatch api returned something that isn't a dict" + ) + return None + + return bool(result.get("Supported", False)) + + +def _on_supported_kernel_cli() -> Optional[bool]: + lp_status = status() + if lp_status is None: + return None + if lp_status.supported == "supported": + return True + if lp_status.supported == "unsupported": + return False + return None + + +def _on_supported_kernel_cache( + version: str, flavor: str, arch: str, codename: str +) -> Tuple[bool, Optional[bool]]: + """Check local cache of kernel support + + :return: (is_cache_valid, result) + """ + try: + cache_data = state_files.livepatch_support_cache.read() + except Exception: + cache_data = None + + if cache_data is not None: + one_week_ago = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(days=7) + if all( + [ + cache_data.cached_at > one_week_ago, # less than one week old + cache_data.version == version, + cache_data.flavor == flavor, + cache_data.arch == arch, + cache_data.codename == codename, + ] + ): + if cache_data.supported is None: + with util.disable_log_to_console(): + logging.warning( + "livepatch kernel support cache has None value" + ) + return (True, cache_data.supported) + return (False, None) + + +def _on_supported_kernel_api( + version: str, flavor: str, arch: str, codename: str +) -> Optional[bool]: + supported = UALivepatchClient().is_kernel_supported( + version=version, + flavor=flavor, + arch=arch, + codename=codename, + ) + + # cache response before returning + state_files.livepatch_support_cache.write( + state_files.LivepatchSupportCacheData( + version=version, + flavor=flavor, + arch=arch, + codename=codename, + supported=supported, + cached_at=datetime.datetime.now(datetime.timezone.utc), + ) + ) + + if supported is None: + with util.disable_log_to_console(): + logging.warning( + "livepatch kernel support API response was ambiguous" + ) + return supported + + +@lru_cache(maxsize=None) +def on_supported_kernel() -> Optional[bool]: + """ + Checks CLI, local cache, and API in that order for kernel support + If all checks fail to return an authoritative answer, we return None + """ + + # first check cli + cli_says = _on_supported_kernel_cli() + if cli_says is not None: + logging.debug("using livepatch cli for support") + return cli_says + + # gather required system info to query support + kernel_info = system.get_kernel_info() + if ( + kernel_info.flavor is None + or kernel_info.major is None + or kernel_info.minor is None + ): + logging.warning( + "unable to determine enough kernel information to " + "check livepatch support" + ) + return None + + arch = util.standardize_arch_name(system.get_dpkg_arch()) + codename = system.get_platform_info()["series"] + + lp_api_kernel_ver = "{major}.{minor}".format( + major=kernel_info.major, minor=kernel_info.minor + ) + + # second check cache + is_cache_valid, cache_says = _on_supported_kernel_cache( + lp_api_kernel_ver, kernel_info.flavor, arch, codename + ) + if is_cache_valid: + logging.debug("using livepatch support cache") + return cache_says + + # finally check api + logging.debug("using livepatch support api") + return _on_supported_kernel_api( + lp_api_kernel_ver, kernel_info.flavor, arch, codename + ) + + +def unconfigure_livepatch_proxy( + protocol_type: str, retry_sleeps: Optional[List[float]] = None +) -> None: + """ + Unset livepatch configuration settings for http and https proxies. + + :param protocol_type: String either http or https + :param retry_sleeps: Optional list of sleep lengths to apply between + retries. Specifying a list of [0.5, 1] tells subp to retry twice + on failure; sleeping half a second before the first retry and 1 second + before the second retry. + """ + if not is_livepatch_installed(): + return + system.subp( + [LIVEPATCH_CMD, "config", "{}-proxy=".format(protocol_type)], + retry_sleeps=retry_sleeps, + ) + + +def configure_livepatch_proxy( + http_proxy: Optional[str] = None, + https_proxy: Optional[str] = None, + retry_sleeps: Optional[List[float]] = None, +) -> None: + """ + Configure livepatch to use http and https proxies. + + :param http_proxy: http proxy to be used by livepatch. If None, it will + not be configured + :param https_proxy: https proxy to be used by livepatch. If None, it will + not be configured + :@param retry_sleeps: Optional list of sleep lengths to apply between + snap calls + """ + from uaclient.entitlements import LivepatchEntitlement + + if http_proxy or https_proxy: + event.info( + messages.SETTING_SERVICE_PROXY.format( + service=LivepatchEntitlement.title + ) + ) + + if http_proxy: + system.subp( + [LIVEPATCH_CMD, "config", "http-proxy={}".format(http_proxy)], + retry_sleeps=retry_sleeps, + ) + + if https_proxy: + system.subp( + [LIVEPATCH_CMD, "config", "https-proxy={}".format(https_proxy)], + retry_sleeps=retry_sleeps, + ) + + +def get_config_option_value(key: str) -> Optional[str]: + """ + Gets the config value from livepatch. + :param key: can be any valid livepatch config option + :return: the value of the livepatch config option, or None if not set + """ + out, _ = system.subp([LIVEPATCH_CMD, "config"]) + match = re.search("^{}: (.*)$".format(key), out, re.MULTILINE) + value = match.group(1) if match else None + if value: + # remove quotes if present + value = re.sub(r"\"(.*)\"", r"\g<1>", value) + return value.strip() if value else None + + +def is_livepatch_installed() -> bool: + return system.which(LIVEPATCH_CMD) is not None diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/lock.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/lock.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/lock.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/lock.py 2023-04-05 15:14:00.000000000 +0000 @@ -4,6 +4,8 @@ import time from uaclient import config, exceptions +from uaclient.files import notices +from uaclient.files.notices import Notice LOG = logging.getLogger("pro.lock") @@ -47,8 +49,10 @@ self.cfg.write_cache( "lock", "{}:{}".format(os.getpid(), self.lock_holder) ) - notice_msg = "Operation in progress: {}".format(self.lock_holder) - self.cfg.notice_file.add("", notice_msg) + notices.add( + Notice.OPERATION_IN_PROGRESS, + operation=self.lock_holder, + ) clear_lock_file = functools.partial(self.cfg.delete_cache_key, "lock") def __exit__(self, _exc_type, _exc_value, _traceback): diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/log.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/log.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/log.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/log.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,60 @@ +import json +import logging +from collections import OrderedDict +from typing import Any, Dict # noqa: F401 + +from uaclient import util + + +class RedactionFilter(logging.Filter): + """A logging filter to redact confidential info""" + + def filter(self, record: logging.LogRecord): + record.msg = util.redact_sensitive_logs(str(record.msg)) + return True + + +class JsonArrayFormatter(logging.Formatter): + """Json Array Formatter for our logging mechanism + Custom made for Pro logging needs + """ + + default_time_format = "%Y-%m-%dT%H:%M:%S" + default_msec_format = "%s.%03d" + required_fields = ( + "asctime", + "levelname", + "name", + "funcName", + "lineno", + "message", + ) + + def format(self, record: logging.LogRecord) -> str: + record.message = record.getMessage() + record.asctime = self.formatTime(record) + + extra_message_dict = {} # type: Dict[str, Any] + if record.exc_info: + extra_message_dict["exc_info"] = self.formatException( + record.exc_info + ) + if not extra_message_dict.get("exc_info") and record.exc_text: + extra_message_dict["exc_info"] = record.exc_text + if record.stack_info: + extra_message_dict["stack_info"] = self.formatStack( + record.stack_info + ) + extra = record.__dict__.get("extra") + if extra and isinstance(extra, dict): + extra_message_dict.update(extra) + + # is ordered to maintain order of fields in log output + local_log_record = OrderedDict() # type: Dict[str, Any] + # update the required fields in the order stated + for field in self.required_fields: + value = record.__dict__.get(field) + local_log_record[field] = value + + local_log_record["extra"] = extra_message_dict + return json.dumps(list(local_log_record.values())) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/messages.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/messages.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/messages.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/messages.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,6 +1,6 @@ from typing import Dict, Optional # noqa: F401 -from uaclient.defaults import BASE_UA_URL, DOCUMENTATION_URL +from uaclient.defaults import BASE_UA_URL, DOCUMENTATION_URL, PRO_ATTACH_URL class NamedMessage: @@ -37,11 +37,18 @@ name=self.name, msg=self.tmpl_msg.format(**msg_params) ) + def __repr__(self): + return "FormattedNamedMessage({}, {})".format( + self.name.__repr__(), + self.tmpl_msg.__repr__(), + ) + class TxtColor: OKGREEN = "\033[92m" DISABLEGREY = "\033[37m" INFOBLUE = "\033[94m" + WARNINGYELLOW = "\033[93m" FAIL = "\033[91m" BOLD = "\033[1m" ENDC = "\033[0m" @@ -59,6 +66,8 @@ ERROR_JSON_DECODING_IN_FILE = """\ Found error: {error} when reading json file: {file_path}""" +SECURITY_FIX_ATTACH_PROMPT = """\ +Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel""" SECURITY_FIX_NOT_FOUND_ISSUE = "Error: {issue_id} not found." SECURITY_FIX_RELEASE_STREAM = "A fix is available in {fix_stream}." SECURITY_UPDATE_NOT_INSTALLED = "The update is not yet installed." @@ -149,6 +158,19 @@ See: """ + BASE_UA_URL ) + +CLI_MAGIC_ATTACH_INIT = "Initiating attach operation..." +CLI_MAGIC_ATTACH_FAILED = "Failed to perform attach..." +CLI_MAGIC_ATTACH_SIGN_IN = """\ +Please sign in to your Ubuntu Pro account at this link: +{url} +And provide the following code: {bold}{{user_code}}{end_bold}""".format( + url=PRO_ATTACH_URL, + bold=TxtColor.BOLD, + end_bold=TxtColor.ENDC, +) +CLI_MAGIC_ATTACH_PROCESSING = "Attaching the machine..." + NO_ACTIVE_OPERATIONS = """No Ubuntu Pro operations are running""" REBOOT_SCRIPT_FAILED = ( "Failed running reboot_cmds script. See: /var/log/ubuntu-advantage.log" @@ -231,36 +253,32 @@ # BEGIN MOTD and APT command messaging -ANNOUNCE_ESM_APPS_TMPL = """\ - * Introducing Expanded Security Maintenance for Applications. - Receive updates to over 25,000 software packages with your - Ubuntu Pro subscription. Free for personal use. - - {url} -""" - -CONTRACT_EXPIRED_MOTD_SOON_TMPL = """\ +CONTRACT_EXPIRES_SOON_MOTD = """\ CAUTION: Your Ubuntu Pro subscription will expire in {remaining_days} days. Renew your subscription at https://ubuntu.com/pro to ensure continued security coverage for your applications. + """ -CONTRACT_EXPIRED_MOTD_GRACE_PERIOD_TMPL = """\ +CONTRACT_EXPIRED_GRACE_PERIOD_MOTD = """\ CAUTION: Your Ubuntu Pro subscription expired on {expired_date}. Renew your subscription at https://ubuntu.com/pro to ensure continued security coverage for your applications. Your grace period will expire in {remaining_days} days. + """ -CONTRACT_EXPIRED_MOTD_PKGS_TMPL = """\ +CONTRACT_EXPIRED_MOTD_PKGS = """\ *Your Ubuntu Pro subscription has EXPIRED* {pkg_num} additional security update(s) require Ubuntu Pro with '{service}' enabled. Renew your service at https://ubuntu.com/pro + """ # noqa: E501 -CONTRACT_EXPIRED_MOTD_NO_PKGS_TMPL = """\ +CONTRACT_EXPIRED_MOTD_NO_PKGS = """\ *Your Ubuntu Pro subscription has EXPIRED* Renew your service at https://ubuntu.com/pro + """ CONTRACT_EXPIRES_SOON_APT_NEWS = """\ @@ -302,13 +320,6 @@ */ """ -UACLIENT_CONF_HEADER = """\ -# Ubuntu Pro client config file. -# If you modify this file, run "pro refresh config" to ensure changes are -# picked up by Ubuntu Pro client. - -""" - SETTING_SERVICE_PROXY = "Setting {service} proxy" ERROR_USING_PROXY = ( 'Error trying to use "{proxy}" as proxy to reach "{test_url}": {error}' @@ -805,8 +816,18 @@ bold=TxtColor.BOLD, end_bold=TxtColor.ENDC ) REALTIME_PRE_DISABLE_PROMPT = """\ -This will disable Ubuntu Pro updates to the Real-time kernel on this machine. -The Real-time kernel will remain installed.\ +This will remove the boot order preference for the Real-time kernel and +disable updates to the Real-time kernel. + +This will NOT fully remove the kernel from your system. + +After this operation is complete you must: + - Ensure a different kernel is installed and configured to boot + - Reboot into that kernel + - Fully remove the realtime kernel packages from your system + - This might look something like `apt remove linux*realtime`, + but you must ensure this is correct before running it. + Are you sure? (y/N) """ REALTIME_ERROR_INSTALL_ON_CONTAINER = NamedMessage( @@ -938,11 +959,6 @@ KERNEL_PARSE_ERROR = "Failed to parse kernel: {kernel}" -LSCPU_ARCH_PARSE_ERROR = NamedMessage( - name="lscpu-arch-parse-error", - msg="Failed to parse architecture from output of lscpu", -) - WARN_NEW_VERSION_AVAILABLE = FormattedNamedMessage( name="new-version-available", msg="A new version of the client is available: {version}. \ @@ -1117,3 +1133,117 @@ The VERSION filed does not have version information: {version} and the VERSION_CODENAME information is not present""", ) + +INCORRECT_TYPE_ERROR_MESSAGE = FormattedNamedMessage( + "incorrect-type", + "Expected value with type {expected_type} but got type: {got_type}", +) +INCORRECT_LIST_ELEMENT_TYPE_ERROR_MESSAGE = FormattedNamedMessage( + "incorrect-list-element-type", + "Got value with incorrect type at index {index}: {nested_msg}", +) +INCORRECT_FIELD_TYPE_ERROR_MESSAGE = FormattedNamedMessage( + "incorrect-field-type", + 'Got value with incorrect type for field "{key}": {nested_msg}', +) +INCORRECT_ENUM_VALUE_ERROR_MESSAGE = FormattedNamedMessage( + "incorrect-enum-value", + "Value provided was not found in {enum_class}'s allowed: value: {values}", +) + +LIVEPATCH_KERNEL_NOT_SUPPORTED = FormattedNamedMessage( + name="livepatch-kernel-not-supported", + msg="""\ +The current kernel ({version}, {arch}) is not supported by livepatch. +Supported kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels +Either switch to a supported kernel or `pro disable livepatch` to dismiss this warning.""", # noqa: E501 +) +LIVEPATCH_KERNEL_NOT_SUPPORTED_DESCRIPTION = "Current kernel is not supported" +LIVEPATCH_KERNEL_NOT_SUPPORTED_UNATTACHED = "Supported livepatch kernels are listed here: https://ubuntu.com/security/livepatch/docs/kernels" # noqa: E501 + +ERROR_PARSING_VERSION_OS_RELEASE = FormattedNamedMessage( + "error-parsing-version-os-release", + """\ +Could not parse /etc/os-release VERSION: {orig_ver} (modified to {mod_ver})""", +) + +MISSING_SERIES_ON_OS_RELEASE = FormattedNamedMessage( + "missing-series-on-os-release", + """\ +Could not extract series information from /etc/os-release. +The VERSION filed does not have version information: {version} +and the VERSION_CODENAME information is not present""", +) + +INVALID_LOCK_FILE = FormattedNamedMessage( + "invalid-lock-file", + """\ +There is a corrupted lock file in the system. To continue, please remove it +from the system by running: + +$ sudo rm {lock_file_path}""", +) + +MISSING_YAML_MODULE = NamedMessage( + "missing-yaml-module", + """\ +Couldn't import the YAML module. +Make sure the 'python3-yaml' package is installed correctly +and /usr/lib/python3/dist-packages is in yout PYTHONPATH.""", +) + +BROKEN_YAML_MODULE = FormattedNamedMessage( + "broken-yaml-module", + "Error while trying to parse a yaml file using 'yaml' from {path}", +) + +FIX_CANNOT_INSTALL_PACKAGE = FormattedNamedMessage( + "fix-cannot-install-package", + "Cannot install package {package} version {version}" "", +) + +ERROR_PARSING_APT_SOURCE_FILES = FormattedNamedMessage( + name="error-parsing-apt-source-files", + msg="""\ +Error parsing APT source files: +{exception_str}""", +) + +ERROR_RUNNING_CMD = FormattedNamedMessage( + "error-running-cmd", + """\ +Error running cmd: {cmd} +{error_our} +""", +) + +UNATTENDED_UPGRADES_SYSTEMD_JOB_DISABLED = NamedMessage( + "unattended-upgrades-systemd-job-disabled", + "apt-daily.timer jobs are not running", +) + +UNATTENDED_UPGRADES_CFG_LIST_VALUE_EMPTY = FormattedNamedMessage( + "unattended-upgrades-cfg-list-value-empty", + "{cfg_name} is empty", +) + +UNATTENDED_UPGRADES_CFG_VALUE_TURNED_OFF = FormattedNamedMessage( + "unattended-upgrades-cfg-value-turned-off", + "{cfg_name} is turned off", +) + +USER_CONFIG_MIGRATION_MIGRATING = ( + "Migrating /etc/ubuntu-advantage/uaclient.conf" +) +USER_CONFIG_MIGRATION_WARNING_UACLIENT_CONF_LOAD = """\ +Warning: Failed to load /etc/ubuntu-advantage/uaclient.conf.preinst-backup + No automatic migration will occur. + You may need to use "pro config set" to re-set your settings.""" + +USER_CONFIG_MIGRATION_WARNING_NEW_USER_CONFIG_WRITE = """\ +Warning: Failed to migrate user_config from /etc/ubuntu-advantage/uaclient.conf + Please run the following to keep your custom settings:""" + +USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE = """\ +Warning: Failed to migrate /etc/ubuntu-advantage/uaclient.conf + Please add following to uaclient.conf to keep your config:""" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/security.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/security.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/security.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/security.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,7 +1,6 @@ import copy import enum import json -import os import socket import textwrap from collections import defaultdict @@ -9,6 +8,15 @@ from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple from uaclient import apt, exceptions, messages, serviceclient, system, util +from uaclient.api.u.pro.attach.magic.initiate.v1 import _initiate +from uaclient.api.u.pro.attach.magic.revoke.v1 import ( + MagicAttachRevokeOptions, + _revoke, +) +from uaclient.api.u.pro.attach.magic.wait.v1 import ( + MagicAttachWaitOptions, + _wait, +) from uaclient.clouds.identity import ( CLOUD_TYPE_TO_TITLE, PRO_CLOUDS, @@ -21,6 +29,8 @@ ApplicabilityStatus, UserFacingStatus, ) +from uaclient.files import notices +from uaclient.files.notices import Notice from uaclient.status import colorize_commands CVE_OR_USN_REGEX = ( @@ -48,6 +58,16 @@ ) +BinaryPackageFix = NamedTuple( + "BinaryPackageFix", + [ + ("source_pkg", str), + ("binary_pkg", str), + ("fixed_version", str), + ], +) + + @enum.unique class FixStatus(enum.Enum): """ @@ -92,7 +112,7 @@ headers=headers, method=method, query_params=query_params, - potentially_sensitive=False, + log_response_body=False, ) def get_cves( @@ -272,7 +292,7 @@ break lines = [ "{issue}: {title}".format(issue=self.id, title=title), - "https://ubuntu.com/security/{}".format(self.id), + " - https://ubuntu.com/security/{}".format(self.id), ] return "\n".join(lines) @@ -374,11 +394,11 @@ if self.cves_ids: lines.append("Found CVEs:") for cve in self.cves_ids: - lines.append("https://ubuntu.com/security/{}".format(cve)) + lines.append(" - https://ubuntu.com/security/{}".format(cve)) elif self.references: lines.append("Found Launchpad bugs:") for reference in self.references: - lines.append(reference) + lines.append(" - " + reference) return "\n".join(lines) @@ -746,12 +766,13 @@ count = len(affected_pkg_status) if count == 0: print( - messages.SECURITY_AFFECTED_PKGS.format( + "\n" + + messages.SECURITY_AFFECTED_PKGS.format( count="No", plural_str="s are" ) + "." ) - print(messages.SECURITY_ISSUE_UNAFFECTED.format(issue=issue_id)) + print("\n" + messages.SECURITY_ISSUE_UNAFFECTED.format(issue=issue_id)) return if count == 1: @@ -759,13 +780,21 @@ else: plural_str = "s are" msg = ( - messages.SECURITY_AFFECTED_PKGS.format( + "\n" + + messages.SECURITY_AFFECTED_PKGS.format( count=count, plural_str=plural_str ) + ": " + ", ".join(sorted(affected_pkg_status.keys())) ) - print(textwrap.fill(msg, width=PRINT_WRAP_WIDTH, subsequent_indent=" ")) + print( + textwrap.fill( + msg, + width=PRINT_WRAP_WIDTH, + subsequent_indent=" ", + replace_whitespace=False, + ) + ) def override_usn_release_package_status( @@ -873,7 +902,7 @@ def _handle_released_package_fixes( cfg: UAConfig, src_pocket_pkgs: Dict[str, List[Tuple[str, CVEPackageStatus]]], - binary_pocket_pkgs: Dict[str, List[str]], + binary_pocket_pkgs: Dict[str, List[BinaryPackageFix]], pkg_index: int, num_pkgs: int, dry_run: bool, @@ -888,7 +917,7 @@ all_already_installed = True upgrade_status = True unfixed_pkgs = set() - installed_pkgs = set() + installed_pkgs = set() # type: Set[str] if src_pocket_pkgs: for pocket in [ UBUNTU_STANDARD_UPDATES_POCKET, @@ -917,10 +946,29 @@ # installed. all_already_installed = False + upgrade_pkgs = [] + for binary_pkg in binary_pkgs: + candidate_version = apt.get_pkg_candidate_version( + binary_pkg.binary_pkg + ) + if candidate_version and apt.compare_versions( + binary_pkg.fixed_version, candidate_version, "le" + ): + upgrade_pkgs.append(binary_pkg.binary_pkg) + else: + print( + "- " + + messages.FIX_CANNOT_INSTALL_PACKAGE.format( + package=binary_pkg.binary_pkg, + version=binary_pkg.fixed_version, + ).msg + ) + unfixed_pkgs.add(binary_pkg.source_pkg) + pkg_index += len(pkg_src_group) upgrade_status &= upgrade_packages_and_attach( cfg=cfg, - upgrade_pkgs=binary_pkgs, + upgrade_pkgs=upgrade_pkgs, pocket=pocket, dry_run=dry_run, ) @@ -928,7 +976,9 @@ if not upgrade_status: unfixed_pkgs.update([src_pkg for src_pkg, _ in pkg_src_group]) else: - installed_pkgs.update(binary_pkgs) + installed_pkgs.update( + binary_pkg.binary_pkg for binary_pkg in binary_pkgs + ) return ReleasedPackagesInstallResult( fix_status=upgrade_status, @@ -1021,11 +1071,16 @@ raise exceptions.SecurityAPIMetadataError( msg, issue_id ) - fixed_pkg = usn_released_src[binary_pkg] - fixed_version = fixed_pkg["version"] # type: ignore + fixed_version = usn_released_src.get(binary_pkg, {}).get( + "version", "" + ) if not apt.compare_versions(fixed_version, version, "le"): binary_pocket_pkgs[pkg_status.pocket_source].append( - binary_pkg + BinaryPackageFix( + source_pkg=src_pkg, + binary_pkg=binary_pkg, + fixed_version=fixed_version, + ) ) released_pkgs_install_result = _handle_released_package_fixes( @@ -1039,8 +1094,12 @@ unfixed_pkgs += released_pkgs_install_result.unfixed_pkgs + print() if unfixed_pkgs: print(_format_unfixed_packages_msg(unfixed_pkgs)) + fix_message = messages.SECURITY_ISSUE_NOT_RESOLVED.format( + issue=issue_id + ) if released_pkgs_install_result.fix_status: # fix_status is True if either: @@ -1065,13 +1124,20 @@ operation="fix operation" ) print(reboot_msg) - cfg.notice_file.add("", reboot_msg) + notices.add( + Notice.ENABLE_REBOOT_REQUIRED, + operation="fix operation", + ) print( util.handle_unicode_characters( messages.SECURITY_ISSUE_NOT_RESOLVED.format(issue=issue_id) ) ) - return FixStatus.SYSTEM_VULNERABLE_UNTIL_REBOOT + return ( + FixStatus.SYSTEM_STILL_VULNERABLE + if unfixed_pkgs + else FixStatus.SYSTEM_VULNERABLE_UNTIL_REBOOT + ) else: # we successfully installed some packages, and the system # reboot-required flag is not set, so we're good @@ -1124,6 +1190,33 @@ return False +def _perform_magic_attach(cfg: UAConfig): + print(messages.CLI_MAGIC_ATTACH_INIT) + initiate_resp = _initiate(cfg=cfg) + print( + "\n" + + messages.CLI_MAGIC_ATTACH_SIGN_IN.format( + user_code=initiate_resp.user_code + ) + ) + + wait_options = MagicAttachWaitOptions(magic_token=initiate_resp.token) + + try: + wait_resp = _wait(options=wait_options, cfg=cfg) + except exceptions.MagicAttachTokenError as e: + print(messages.CLI_MAGIC_ATTACH_FAILED) + + revoke_options = MagicAttachRevokeOptions( + magic_token=initiate_resp.token + ) + _revoke(options=revoke_options, cfg=cfg) + raise e + + print("\n" + messages.CLI_MAGIC_ATTACH_PROCESSING) + return _run_ua_attach(cfg, wait_resp.contract_token) + + def _prompt_for_attach(cfg: UAConfig) -> bool: """Prompt for attach to a subscription or token. @@ -1132,16 +1225,14 @@ _inform_ubuntu_pro_existence_if_applicable() print(messages.SECURITY_UPDATE_NOT_INSTALLED_SUBSCRIPTION) choice = util.prompt_choices( - "Choose: [S]ubscribe at ubuntu.com [A]ttach existing token [C]ancel", + messages.SECURITY_FIX_ATTACH_PROMPT, valid_choices=["s", "a", "c"], ) if choice == "c": return False if choice == "s": - print(messages.PROMPT_UA_SUBSCRIPTION_URL) - # TODO(GH: #1413: magic subscription attach) - input("Hit [Enter] when subscription is complete.") - if choice in ("a", "s"): + return _perform_magic_attach(cfg) + if choice == "a": print(messages.PROMPT_ENTER_TOKEN) token = input("> ") return _run_ua_attach(cfg, token) @@ -1186,7 +1277,7 @@ def _check_attached(cfg: UAConfig, dry_run: bool) -> bool: """Verify if machine is attached to an Ubuntu Pro subscription.""" if dry_run: - print(messages.SECURITY_DRY_RUN_UA_NOT_ATTACHED) + print("\n" + messages.SECURITY_DRY_RUN_UA_NOT_ATTACHED) return True return _prompt_for_attach(cfg) @@ -1209,7 +1300,8 @@ if applicability_status == ApplicabilityStatus.APPLICABLE: if dry_run: print( - messages.SECURITY_DRY_RUN_UA_SERVICE_NOT_ENABLED.format( + "\n" + + messages.SECURITY_DRY_RUN_UA_SERVICE_NOT_ENABLED.format( service=ent.name ) ) @@ -1303,7 +1395,7 @@ # If we are running on --dry-run mode, we don't need to be root # to understand what will happen with the system - if os.getuid() != 0 and not dry_run: + if not util.we_are_currently_root() and not dry_run: print(messages.SECURITY_APT_NON_ROOT) return False @@ -1336,11 +1428,16 @@ ) if not dry_run: - apt.run_apt_update_command() - apt.run_apt_command( - cmd=["apt-get", "install", "--only-upgrade", "-y"] + upgrade_pkgs, - error_msg=messages.APT_INSTALL_FAILED.msg, - env={"DEBIAN_FRONTEND": "noninteractive"}, - ) + try: + apt.run_apt_update_command() + apt.run_apt_command( + cmd=["apt-get", "install", "--only-upgrade", "-y"] + + upgrade_pkgs, + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) + except Exception as e: + msg = getattr(e, "msg", str(e)) + print(msg.strip()) + return False return True diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/security_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/security_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/security_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/security_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,25 +1,21 @@ -import json -import logging import re import textwrap from collections import defaultdict from enum import Enum from functools import lru_cache from random import choice -from typing import Any, DefaultDict, Dict, List, Tuple # noqa: F401 +from typing import Any, DefaultDict, Dict, List, Tuple, Union # noqa: F401 import apt # type: ignore -from uaclient import messages +from uaclient import livepatch, messages +from uaclient.apt import PreserveAptCfg, get_apt_cache, get_esm_cache from uaclient.config import UAConfig -from uaclient.defaults import ESM_APT_ROOTDIR from uaclient.entitlements import ESMAppsEntitlement, ESMInfraEntitlement from uaclient.entitlements.entitlement_status import ( ApplicabilityStatus, ApplicationStatus, ) -from uaclient.entitlements.livepatch import LIVEPATCH_CMD -from uaclient.exceptions import ProcessExecutionError from uaclient.status import status from uaclient.system import ( REBOOT_PKGS_FILE_PATH, @@ -30,8 +26,6 @@ is_supported, load_file, should_reboot, - subp, - which, ) ESM_SERVICES = ("esm-infra", "esm-apps") @@ -63,29 +57,19 @@ } -@lru_cache(maxsize=None) -def get_esm_cache(): - try: - # If the rootdir folder doesn't contain any apt source info, the - # cache will be empty - cache = apt.Cache(rootdir=ESM_APT_ROOTDIR) - except Exception: - cache = {} - - return cache - - def get_installed_packages_by_origin() -> DefaultDict[ "str", List[apt.package.Package] ]: result = defaultdict(list) - cache = apt.Cache() - installed_packages = [package for package in cache if package.is_installed] - result["all"] = installed_packages + with PreserveAptCfg(get_apt_cache) as cache: + installed_packages = [ + package for package in cache if package.is_installed + ] + result["all"] = installed_packages - for package in installed_packages: - result[get_origin_for_package(package)].append(package) + for package in installed_packages: + result[get_origin_for_package(package)].append(package) return result @@ -153,37 +137,12 @@ # but has be advertised about esm packages. Since those # sources live in a private folder, we need a different apt cache # to access them. - esm_cache = get_esm_cache() - - for package in packages: - if package.is_installed: - for version in package.versions: - if version > package.installed: - counted_as_security = False - for origin in version.origins: - service = get_origin_information_to_service_map().get( - (origin.origin, origin.archive) - ) - if service: - result[service].append((version, origin.site)) - counted_as_security = True - break # No need to loop through all the origins - # Also no need to report backports at least for now... - expected_origin = version.origins[0] - if ( - not counted_as_security - and "backports" not in expected_origin.archive - ): - result["standard-updates"].append( - (version, expected_origin.site) - ) - - # This loop should be only used if the user does not have esm - # (infra or apps) enabled, and it is shorter than the previous one - if package.name in esm_cache: - esm_package = esm_cache[package.name] - for version in esm_package.versions: + with PreserveAptCfg(get_esm_cache) as esm_cache: + for package in packages: + if package.is_installed: + for version in package.versions: if version > package.installed: + counted_as_security = False for origin in version.origins: service = ( get_origin_information_to_service_map().get( @@ -192,7 +151,35 @@ ) if service: result[service].append((version, origin.site)) + counted_as_security = True + # No need to loop through all the origins break + # Also no need to report backports at least for now... + expected_origin = version.origins[0] + if ( + not counted_as_security + and "backports" not in expected_origin.archive + ): + result["standard-updates"].append( + (version, expected_origin.site) + ) + + # This loop should be only used if the user does not have esm + # (infra or apps) enabled, and it is shorter than the + # previous one + if package.name in esm_cache: + esm_package = esm_cache[package.name] + for version in esm_package.versions: + if version > package.installed: + for origin in version.origins: + service = get_origin_information_to_service_map().get( # noqa + (origin.origin, origin.archive) + ) + if service: + result[service].append( + (version, origin.site) + ) + break return result @@ -220,37 +207,21 @@ # Yeah Any is bad, but so is python<3.8 without TypedDict def get_livepatch_fixed_cves() -> List[Dict[str, Any]]: - try: - out, _err = subp([LIVEPATCH_CMD, "status", "--format", "json"]) - except ProcessExecutionError: - return [] - - try: - livepatch_output = json.loads(out) - except json.JSONDecodeError: - msg = "Could not parse Livepatch Status JSON: {}".format(out) - logging.debug(msg) - return [] - - status_list = livepatch_output.get("Status") - if status_list: - # Livepatch guarantees this list has only one element - lp_kernel_version = status_list[0].get("Kernel") - our_kernel_version = get_kernel_info().proc_version_signature_version - if ( - our_kernel_version is not None - and our_kernel_version == lp_kernel_version - ): - livepatch_status = status_list[0].get("Livepatch", {}) - if livepatch_status.get("State", "") == "applied": - fixes = livepatch_status.get("Fixes", []) - return [ - { - "name": fix.get("Name", ""), - "patched": fix.get("Patched", ""), - } - for fix in fixes - ] + lp_status = livepatch.status() + our_kernel_version = get_kernel_info().proc_version_signature_version + if ( + lp_status is not None + and our_kernel_version is not None + and our_kernel_version == lp_status.kernel + and lp_status.livepatch is not None + and lp_status.livepatch.state == "applied" + and lp_status.livepatch.fixes is not None + and len(lp_status.livepatch.fixes) > 0 + ): + return [ + {"name": fix.name or "", "patched": fix.patched or False} + for fix in lp_status.livepatch.fixes + ] return [] @@ -282,42 +253,18 @@ if num_kernel_pkgs != num_pkgs: return RebootStatus.REBOOT_REQUIRED - if not which("canonical-livepatch"): + if not livepatch.is_livepatch_installed(): return RebootStatus.REBOOT_REQUIRED - try: - livepatch_status, _ = subp( - [LIVEPATCH_CMD, "status", "--format", "json"] - ) - except ProcessExecutionError: - # If we can't query the Livepatch status, then return a - # normal reboot state - logging.debug("Could not query Livepatch Status") - return RebootStatus.REBOOT_REQUIRED - - try: - livepatch_status_dict = json.loads(livepatch_status) - except json.JSONDecodeError: - # If we cannot parse the json file, we will return a - # normal reboot state - msg = "Could not parse Livepatch Status JSON: {}".format( - livepatch_status - ) - logging.debug(msg) - return RebootStatus.REBOOT_REQUIRED - - patch_statuses = livepatch_status_dict.get("Status", [{}]) - - kernel_info = get_kernel_info() - kernel_name = kernel_info.proc_version_signature_version - - patch_state = "" - for patch_status in patch_statuses: - livepatch_kernel = patch_status.get("Kernel") - if livepatch_kernel and livepatch_kernel == kernel_name: - patch_state = patch_status.get("Livepatch", {}).get("State", "") - - if patch_state == "applied": + our_kernel_version = get_kernel_info().proc_version_signature_version + lp_status = livepatch.status() + if ( + lp_status is not None + and our_kernel_version is not None + and our_kernel_version == lp_status.kernel + and lp_status.livepatch is not None + and lp_status.livepatch.state == "applied" + ): return RebootStatus.REBOOT_REQUIRED_LIVEPATCH_APPLIED # Any other Livepatch status will not be considered here to modify the diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/serviceclient.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/serviceclient.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/serviceclient.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/serviceclient.py 2023-04-05 15:14:00.000000000 +0000 @@ -30,8 +30,7 @@ def __init__(self, cfg: Optional[config.UAConfig] = None) -> None: if not cfg: - root_mode = os.getuid() == 0 - self.cfg = config.UAConfig(root_mode=root_mode) + self.cfg = config.UAConfig() else: self.cfg = cfg @@ -49,14 +48,16 @@ headers=None, method=None, query_params=None, - potentially_sensitive: bool = True, + log_response_body: bool = True, timeout: Optional[int] = None, ): path = path.lstrip("/") if not headers: headers = self.headers() if headers.get("content-type") == "application/json" and data: - data = json.dumps(data).encode("utf-8") + data = json.dumps(data, cls=util.DatetimeAwareJSONEncoder).encode( + "utf-8" + ) url = urljoin(getattr(self.cfg, self.cfg_url_base_attr), path) fake_response, fake_headers = self._get_fake_responses(url) if fake_response: @@ -75,7 +76,7 @@ headers=headers, method=method, timeout=timeout_to_use, - potentially_sensitive=potentially_sensitive, + log_response_body=log_response_body, ) except error.URLError as e: body = None diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/status.py 2023-04-05 15:14:00.000000000 +0000 @@ -7,8 +7,15 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from uaclient import event_logger, exceptions, messages, system, util, version -from uaclient.config import UAConfig +from uaclient import ( + event_logger, + exceptions, + livepatch, + messages, + util, + version, +) +from uaclient.config import UA_CONFIGURABLE_KEYS, UAConfig from uaclient.contract import get_available_resources, get_contract_information from uaclient.defaults import ATTACH_FAIL_DATE_FORMAT, PRINT_WRAP_WIDTH from uaclient.entitlements import entitlement_factory @@ -18,6 +25,8 @@ UserFacingConfigStatus, UserFacingStatus, ) +from uaclient.files import notices +from uaclient.files.notices import Notice from uaclient.messages import TxtColor event = event_logger.get_event_logger() @@ -46,6 +55,9 @@ + UserFacingStatus.UNAVAILABLE.value + TxtColor.ENDC ), + UserFacingStatus.WARNING.value: ( + TxtColor.WARNINGYELLOW + UserFacingStatus.WARNING.value + TxtColor.ENDC + ), ContractStatus.ENTITLED.value: ( TxtColor.OKGREEN + ContractStatus.ENTITLED.value + TxtColor.ENDC ), @@ -114,8 +126,9 @@ def _attached_service_status(ent, inapplicable_resources) -> Dict[str, Any]: + warning = None status_details = "" - description_override = None + description_override = ent.status_description_override() contract_status = ent.contract_status() available = "no" if ent.name in inapplicable_resources else "yes" @@ -127,7 +140,12 @@ description_override = inapplicable_resources[ent.name] else: ent_status, details = ent.user_facing_status() - if details: + if ent_status == UserFacingStatus.WARNING: + warning = { + "code": details.name, + "message": details.msg, + } + elif details: status_details = details.msg if ent_status == UserFacingStatus.INAPPLICABLE: @@ -144,12 +162,14 @@ "description_override": description_override, "available": available, "blocked_by": blocked_by, + "warning": warning, } def _attached_status(cfg: UAConfig) -> Dict[str, Any]: """Return configuration of attached status as a dictionary.""" - cfg.notice_file.try_remove("", messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX) + notices.remove(Notice.AUTO_ATTACH_RETRY_FULL_NOTICE) + notices.remove(Notice.AUTO_ATTACH_RETRY_TOTAL_FAILURE) response = copy.deepcopy(DEFAULT_STATUS) machineTokenInfo = cfg.machine_token["machineTokenInfo"] @@ -160,7 +180,7 @@ "machine_id": machineTokenInfo["machineId"], "attached": True, "origin": contractInfo.get("origin"), - "notices": cfg.notice_file.read() or [], + "notices": notices.list() or [], "contract": { "id": contractInfo["id"], "name": contractInfo["name"], @@ -233,6 +253,7 @@ ent_cls = entitlement_factory( cfg=cfg, name=resource.get("name", "") ) + except exceptions.EntitlementNotFoundError: LOG.debug( messages.AVAILABILITY_FROM_UNKNOWN_SERVICE.format( @@ -241,10 +262,22 @@ ) continue + # FIXME: we need a better generic unattached availability status + # that takes into account local information. + if ( + ent_cls.name == "livepatch" + and livepatch.on_supported_kernel() is False + ): + lp = ent_cls(cfg) + descr_override = lp.status_description_override() + else: + descr_override = None + response["services"].append( { "name": resource.get("presentedAs", resource["name"]), "description": ent_cls.description, + "description_override": descr_override, "available": available, } ) @@ -302,7 +335,7 @@ status_val = userStatus.INACTIVE.value status_desc = messages.NO_ACTIVE_OPERATIONS (lock_pid, lock_holder) = cfg.check_lock_info() - notices = cfg.notice_file.read() or [] + notices_list = notices.list() or [] if lock_pid > 0: status_val = userStatus.ACTIVE.value status_desc = messages.LOCK_HELD.format( @@ -311,21 +344,25 @@ elif os.path.exists(cfg.data_path("marker-reboot-cmds")): status_val = userStatus.REBOOTREQUIRED.value operation = "configuration changes" - for label, description in notices: - if label == "Reboot required": - operation = description - break status_desc = messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation=operation ) - return { + ret = { "execution_status": status_val, "execution_details": status_desc, - "notices": notices, + "notices": notices_list, "config_path": cfg.cfg_path, "config": cfg.cfg, "features": cfg.features, } + # LP: #2004280 maintain backwards compatibility + ua_config = {} + for key in UA_CONFIGURABLE_KEYS: + if hasattr(cfg, key): + ua_config[key] = getattr(cfg, key) + ret["config"]["ua_config"] = ua_config + + return ret def status(cfg: UAConfig, show_all: bool = False) -> Dict[str, Any]: @@ -344,18 +381,9 @@ response.update(_get_config_status(cfg)) - if cfg.root_mode: + if util.we_are_currently_root(): cfg.write_cache("status-cache", response) - # Try to remove fix reboot notices if not applicable - if not system.should_reboot(): - cfg.notice_file.remove( - "", - messages.ENABLE_REBOOT_REQUIRED_TMPL.format( - operation="fix operation" - ), - ) - response = _handle_beta_resources(cfg, show_all, response) if not show_all: @@ -432,7 +460,7 @@ now = datetime.now(timezone.utc) if contract_info.get("effectiveTo"): response["expires"] = contract_info.get("effectiveTo") - expiration_datetime = util.parse_rfc3339_date(response["expires"]) + expiration_datetime = response["expires"] delta = expiration_datetime - now if delta.total_seconds() <= 0: message = messages.ATTACH_FORBIDDEN_EXPIRED.format( @@ -444,7 +472,7 @@ ret = 1 if contract_info.get("effectiveFrom"): response["effective"] = contract_info.get("effectiveFrom") - effective_datetime = util.parse_rfc3339_date(response["effective"]) + effective_datetime = response["effective"] delta = now - effective_datetime if delta.total_seconds() <= 0: message = messages.ATTACH_FORBIDDEN_NOT_YET.format( @@ -553,7 +581,7 @@ Content lines will be center-aligned based on max value length of first column. """ - content = [""] + content = [] if header: content.append(header) template_length = max([len(pair[0]) for pair in column_data]) @@ -601,23 +629,36 @@ ) ] for service in status["services"]: - content.append(STATUS_UNATTACHED_TMPL.format(**service)) - - if status.get("notices"): - content.extend( - get_section_column_content( - status.get("notices") or [], header="NOTICES" + descr_override = service.get("description_override") + description = ( + descr_override if descr_override else service["description"] + ) + content.append( + STATUS_UNATTACHED_TMPL.format( + name=service["name"], + available=service["available"], + description=description, ) ) + notices = status.get("notices") + if notices: + content.append("NOTICES") + content.extend(notices) + if status.get("features"): content.append("\nFEATURES") for key, value in sorted(status["features"].items()): content.append("{}: {}".format(key, value)) content.extend(["", messages.UNATTACHED.msg]) + if livepatch.on_supported_kernel() is False: + content.extend( + ["", messages.LIVEPATCH_KERNEL_NOT_SUPPORTED_UNATTACHED] + ) return "\n".join(content) + service_warnings = [] content = [STATUS_HEADER] for service_status in status["services"]: entitled = service_status["entitled"] @@ -631,15 +672,22 @@ "status": colorize(service_status["status"]), "description": description, } + warning = service_status.get("warning", None) + if warning is not None: + warning_message = warning.get("message", None) + if warning_message is not None: + service_warnings.append(warning_message) content.append(STATUS_TMPL.format(**fmt_args)) tech_support_level = status["contract"]["tech_support_level"] - if status.get("notices"): - content.extend( - get_section_column_content( - status.get("notices") or [], header="NOTICES" - ) - ) + if status.get("notices") or len(service_warnings) > 0: + content.append("") + content.append("NOTICES") + notices = status.get("notices") + if notices: + content.extend(notices) + if len(service_warnings) > 0: + content.extend(service_warnings) if status.get("features"): content.append("\nFEATURES") @@ -662,6 +710,7 @@ pairs.append(("Technical support level", colorize(tech_support_level))) if pairs: + content.append("") content.extend(get_section_column_content(column_data=pairs)) return "\n".join(content) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/system.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/system.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/system.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/system.py 2023-04-05 15:14:00.000000000 +0000 @@ -93,29 +93,21 @@ @lru_cache(maxsize=None) -def get_lscpu_arch() -> str: - """used for livepatch""" - out, _err = subp(["lscpu"]) - for line in out.splitlines(): - if line.strip().startswith("Architecture:"): - arch = line.split(":")[1].strip() - if arch: - return arch - else: - break - error_msg = messages.LSCPU_ARCH_PARSE_ERROR - raise exceptions.UserFacingError( - msg=error_msg.msg, msg_code=error_msg.name - ) - - -@lru_cache(maxsize=None) def get_dpkg_arch() -> str: out, _err = subp(["dpkg", "--print-architecture"]) return out.strip() @lru_cache(maxsize=None) +def get_virt_type() -> str: + try: + out, _ = subp(["systemd-detect-virt"]) + return out.strip() + except exceptions.ProcessExecutionError: + return "" + + +@lru_cache(maxsize=None) def get_machine_id(cfg) -> str: """ Get system's unique machine-id or create our own in data_dir. @@ -181,8 +173,10 @@ "series": series.lower(), "kernel": get_kernel_info().uname_release, "arch": get_dpkg_arch(), + "virt": get_virt_type(), } ) + return platform_info @@ -503,13 +497,13 @@ break except exceptions.ProcessExecutionError as e: if capture: - logging.debug(util.redact_sensitive_logs(str(e))) + logging.debug(str(e)) msg = "Stderr: {}\nStdout: {}".format(e.stderr, e.stdout) - logging.warning(util.redact_sensitive_logs(msg)) + logging.warning(msg) if not retry_sleeps: raise retry_msg = " Retrying %d more times." % len(retry_sleeps) - logging.debug(util.redact_sensitive_logs(str(e) + retry_msg)) + logging.debug(str(e) + retry_msg) time.sleep(retry_sleeps.pop(0)) return out, err @@ -518,3 +512,25 @@ if os.path.exists(folder_path): logging.debug("Removing folder: %s", folder_path) rmtree(folder_path) + + +def get_systemd_job_state(job_name: str) -> bool: + """ + Get if the systemd job is active in the system. Note that any status + different from "active" will make this function return False. + Additionally, if the system doesn't exist we will also return False + here. + + @param job_name: Name of the systemd job to look at + + @return: A Boolean specifying if the job is active or not + """ + try: + out, _ = subp(["systemctl", "is-active", job_name]) + except exceptions.ProcessExecutionError as e: + out = e.stdout + + if not out: + return False + + return out.strip() == "active" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/testing/fakes.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/testing/fakes.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/testing/fakes.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/testing/fakes.py 2023-04-05 15:14:00.000000000 +0000 @@ -51,3 +51,9 @@ ret = self.content[self.cursor : size] self.cursor += size return ret + + def __enter__(self): + return self + + def __exit__(self, _exc_type, _exc_value, _traceback): + pass diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_actions.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_actions.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_actions.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_actions.py 2023-04-05 15:14:00.000000000 +0000 @@ -44,7 +44,7 @@ ], ) @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid") - @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") + @mock.patch("uaclient.jobs.update_messaging.update_motd_messages") @mock.patch("uaclient.status.status") @mock.patch(M_PATH + "contract.request_updated_contract") @mock.patch(M_PATH + "config.UAConfig.write_cache") @@ -130,7 +130,7 @@ @mock.patch("uaclient.actions._write_command_output_to_file") class TestCollectLogs: @pytest.mark.parametrize("caplog_text", [logging.WARNING], indirect=True) - @mock.patch("os.getuid") + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) @mock.patch("uaclient.system.write_file") @mock.patch("uaclient.system.load_file") @mock.patch("uaclient.actions._get_state_files") @@ -141,13 +141,12 @@ m_get_state_files, m_load_file, m_write_file, - m_getuid, + m_we_are_currently_root, m_write_cmd, caplog_text, ): m_get_state_files.return_value = ["a", "b"] m_load_file.side_effect = [UnicodeError("test"), "test"] - m_getuid.return_value = 1 m_glob.return_value = [] with mock.patch("os.path.isfile", return_value=True): diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_apt.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_apt.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_apt.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_apt.py 2023-04-05 15:14:00.000000000 +0000 @@ -18,8 +18,8 @@ APT_KEYS_DIR, APT_PROXY_CONF_FILE, APT_RETRIES, - ESM_APT_ROOTDIR, KEYRINGS_DIR, + PreserveAptCfg, add_apt_auth_conf_entry, add_auth_apt_repo, add_ppa_pinning, @@ -29,6 +29,7 @@ find_apt_list_files, get_apt_cache_policy, get_apt_cache_time, + get_apt_config_values, get_installed_packages_names, is_installed, remove_apt_list_files, @@ -1060,7 +1061,7 @@ assert expected_result is compare_versions(ver1, ver2, relation) -class TestAptCacheTime: +class TestAptCache: @pytest.mark.parametrize( "file_exists,expected", ((True, 1.23), (False, None)) ) @@ -1073,7 +1074,7 @@ @pytest.mark.parametrize( "is_lts,cache_call_list", - ((True, [mock.call(rootdir=ESM_APT_ROOTDIR)]), (False, [])), + ((True, [mock.call()]), (False, [])), ) @pytest.mark.parametrize( "apps_status", (ApplicationStatus.ENABLED, ApplicationStatus.DISABLED) @@ -1082,18 +1083,22 @@ "infra_status", (ApplicationStatus.ENABLED, ApplicationStatus.DISABLED) ) @pytest.mark.parametrize("is_esm", (True, False)) + @pytest.mark.parametrize("can_enable_infra", ("yes", "no")) + @pytest.mark.parametrize("can_enable_apps", ("yes", "no")) @mock.patch("uaclient.entitlements.esm.ESMAppsEntitlement") @mock.patch("uaclient.entitlements.esm.ESMInfraEntitlement") @mock.patch("uaclient.apt.system.is_current_series_lts") @mock.patch("uaclient.apt.system.is_current_series_active_esm") - @mock.patch("apt.Cache") + @mock.patch("uaclient.apt.get_esm_cache") + @mock.patch("uaclient.actions.status") @mock.patch("apt_pkg.config") @mock.patch("apt_pkg.init_config") def test_update_esm_caches_based_on_lts( self, _m_apt_pkg_init_config, _m_apt_pkg_config, - m_cache, + m_status, + m_esm_cache, m_is_esm, m_is_lts, m_infra_entitlement, @@ -1103,8 +1108,20 @@ apps_status, infra_status, is_esm, + can_enable_infra, + can_enable_apps, FakeConfig, ): + m_status.return_value = ( + { + "services": [ + {"name": "esm-apps", "available": can_enable_apps}, + {"name": "esm-infra", "available": can_enable_infra}, + ] + }, + 0, + ) + m_is_esm.return_value = is_esm m_is_lts.return_value = is_lts @@ -1126,18 +1143,103 @@ infra_setup_repo_count = 0 apps_setup_repo_count = 0 + infra_disable_repo_count = 0 + apps_disable_repo_count = 0 + status_count = 0 + status_cache_args_list = [] if is_lts: - if apps_status == ApplicationStatus.DISABLED: + status_count = 1 + status_cache_args_list = [mock.call("status-cache")] + if ( + apps_status == ApplicationStatus.DISABLED + and can_enable_apps == "yes" + ): apps_setup_repo_count = 1 - if infra_status == ApplicationStatus.DISABLED and is_esm: - infra_setup_repo_count = 1 + else: + apps_disable_repo_count = 1 - update_esm_caches(FakeConfig()) + if ( + infra_status == ApplicationStatus.DISABLED + and is_esm + and can_enable_infra == "yes" + ): + infra_setup_repo_count = 1 + elif is_esm: + infra_disable_repo_count = 1 - assert m_cache.call_args_list == cache_call_list + cfg = FakeConfig() + with mock.patch.object(cfg, "read_cache", return_value=None): + update_esm_caches(cfg) + assert cfg.read_cache.call_args_list == status_cache_args_list + assert m_esm_cache.call_args_list == cache_call_list assert ( m_infra.setup_local_esm_repo.call_count == infra_setup_repo_count ) assert m_apps.setup_local_esm_repo.call_count == apps_setup_repo_count + assert ( + m_infra.disable_local_esm_repo.call_count + == infra_disable_repo_count + ) + assert ( + m_apps.disable_local_esm_repo.call_count == apps_disable_repo_count + ) + assert m_status.call_count == status_count + + +class TestGetAptConfigValues: + @mock.patch("uaclient.apt._get_apt_config") + def test_apt_config_values( + self, + m_get_apt_config, + ): + m_dict = mock.MagicMock() + m_get_apt_config.return_value = m_dict + + m_dict.get.side_effect = ["", "foo", "bar", ""] + m_dict.value_list.side_effect = [ + "", + ["test1", "test2"], + ] + + expected_return = { + "val1": None, + "val2": "foo", + "val3": "bar", + "val4": ["test1", "test2"], + } + + assert expected_return == get_apt_config_values( + ["val1", "val2", "val3", "val4"] + ) + + +class TestPreserveAptCfg: + def test_apt_config_is_preserved( + self, + apt_pkg, + ): + class AptDict(dict): + def set(self, key, value): + super().__setitem__(key, value) + + apt_cfg = AptDict() + apt_cfg["test"] = 1 + apt_cfg["test1"] = [1, 2, 3] + apt_cfg["test2"] = {"foo": "bar"} + + type(apt_pkg).config = mock.PropertyMock(return_value=apt_cfg) + + def apt_func(): + apt_cfg["test"] = 3 + apt_cfg["test1"] = [3, 2, 1] + apt_cfg["test2"] = {"foo": "test"} + return apt_cfg + + with PreserveAptCfg(apt_func): + pass + + assert 1 == apt_cfg["test"] + assert [1, 2, 3] == apt_cfg["test1"] + assert {"foo": "bar"} == apt_cfg["test2"] diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_api.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_api.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_api.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_api.py 2023-04-05 15:14:00.000000000 +0000 @@ -24,7 +24,8 @@ class TestActionAPI: - def test_api_help(self, capsys): + @mock.patch("uaclient.cli.entitlements.valid_services", return_value=[]) + def test_api_help(self, valid_services, capsys): with pytest.raises(SystemExit): with mock.patch("sys.argv", ["/usr/bin/ua", "api", "--help"]): main() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_attach.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_attach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_attach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_attach.py 2023-04-05 15:14:00.000000000 +0000 @@ -6,9 +6,8 @@ import mock import pytest -import yaml -from uaclient import event_logger, messages, status +from uaclient import event_logger, messages, status, util from uaclient.cli import ( UA_AUTH_TOKEN_URL, action_attach, @@ -27,6 +26,7 @@ UserFacingError, ) from uaclient.testing.fakes import FakeFile +from uaclient.yaml import safe_dump HELP_OUTPUT = textwrap.dedent( """\ @@ -64,8 +64,8 @@ "contractInfo": { "name": "mycontract", "id": "contract-1", - "createdAt": "2020-05-08T19:02:26Z", - "effectiveTo": "9999-12-31T00:00:00Z", + "createdAt": util.parse_rfc3339_date("2020-05-08T19:02:26Z"), + "effectiveTo": util.parse_rfc3339_date("9999-12-31T00:00:00Z"), "resourceEntitlements": [], "products": ["free"], }, @@ -107,10 +107,11 @@ ] = [ENTITLED_EXAMPLE_ESM_RESOURCE] -@mock.patch(M_PATH + "os.getuid") -def test_non_root_users_are_rejected(getuid, FakeConfig, capsys, event): +@mock.patch(M_PATH + "util.we_are_currently_root", return_value=False) +def test_non_root_users_are_rejected( + m_we_are_currently_root, FakeConfig, capsys, event +): """Check that a UID != 0 will receive a message and exit non-zero""" - getuid.return_value = 1 cfg = FakeConfig() with pytest.raises(NonRootUserError): @@ -141,10 +142,8 @@ assert expected == json.loads(capsys.readouterr()[0]) -# For all of these tests we want to appear as root, so mock on the class -@mock.patch(M_PATH + "os.getuid", return_value=0) class TestActionAttach: - def test_already_attached(self, _m_getuid, capsys, FakeConfig, event): + def test_already_attached(self, capsys, FakeConfig, event): """Check that an already-attached machine emits message and exits 0""" account_name = "test_account" cfg = FakeConfig.for_attached_machine( @@ -181,7 +180,11 @@ @mock.patch("uaclient.system.subp") def test_lock_file_exists( - self, m_subp, _m_getuid, capsys, FakeConfig, event + self, + m_subp, + capsys, + FakeConfig, + event, ): """Check when an operation holds a lock file, attach cannot run.""" cfg = FakeConfig() @@ -233,18 +236,17 @@ ), ) @mock.patch("uaclient.system.should_reboot", return_value=False) - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") @mock.patch("uaclient.status.get_available_resources") - @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") + @mock.patch("uaclient.jobs.update_messaging.update_motd_messages") @mock.patch(M_PATH + "contract.request_updated_contract") def test_status_updated_when_auto_enable_fails( self, request_updated_contract, m_update_apt_and_motd_msgs, _m_get_available_resources, - _m_should_reboot, _m_remove_notice, - _m_get_uid, + _m_should_reboot, error_class, error_str, FakeConfig, @@ -275,8 +277,8 @@ assert [mock.call(cfg)] == m_update_apt_and_motd_msgs.call_args_list @mock.patch("uaclient.system.should_reboot", return_value=False) - @mock.patch("uaclient.files.NoticeFile.remove") - @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") + @mock.patch("uaclient.files.notices.NoticesManager.remove") + @mock.patch("uaclient.jobs.update_messaging.update_motd_messages") @mock.patch( M_PATH + "contract.UAContractClient.request_contract_machine_attach" ) @@ -288,9 +290,8 @@ m_status, contract_machine_attach, m_update_apt_and_motd_msgs, - _m_should_reboot, _m_remove_notice, - _m_getuid, + _m_should_reboot, FakeConfig, event, ): @@ -350,16 +351,15 @@ @pytest.mark.parametrize("auto_enable", (True, False)) @mock.patch("uaclient.system.should_reboot", return_value=False) - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") @mock.patch("uaclient.status.get_available_resources") - @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") + @mock.patch("uaclient.jobs.update_messaging.update_motd_messages") def test_auto_enable_passed_through_to_request_updated_contract( self, m_update_apt_and_motd_msgs, _m_get_available_resources, - _m_should_reboot, _m_remove_notice, - _m_get_uid, + _m_should_reboot, auto_enable, FakeConfig, ): @@ -379,7 +379,8 @@ assert [mock.call(cfg)] == m_update_apt_and_motd_msgs.call_args_list def test_attach_config_and_token_mutually_exclusive( - self, _m_getuid, FakeConfig + self, + FakeConfig, ): args = mock.MagicMock( token="something", attach_config=FakeFile("something") @@ -392,11 +393,14 @@ @mock.patch(M_PATH + "_post_cli_attach") @mock.patch(M_PATH + "actions.attach_with_token") def test_token_from_attach_config( - self, m_attach_with_token, _m_post_cli_attach, _m_getuid, FakeConfig + self, + m_attach_with_token, + _m_post_cli_attach, + FakeConfig, ): args = mock.MagicMock( token=None, - attach_config=FakeFile(yaml.dump({"token": "faketoken"})), + attach_config=FakeFile(safe_dump({"token": "faketoken"})), ) cfg = FakeConfig() action_attach(args, cfg=cfg) @@ -405,12 +409,15 @@ ] == m_attach_with_token.call_args_list def test_attach_config_invalid_config( - self, _m_getuid, FakeConfig, capsys, event + self, + FakeConfig, + capsys, + event, ): args = mock.MagicMock( token=None, attach_config=FakeFile( - yaml.dump({"token": "something", "enable_services": "cis"}), + safe_dump({"token": "something", "enable_services": "cis"}), name="fakename", ), ) @@ -420,7 +427,7 @@ assert "Error while reading fakename: " in e.value.msg args.attach_config = FakeFile( - yaml.dump({"token": "something", "enable_services": "cis"}), + safe_dump({"token": "something", "enable_services": "cis"}), name="fakename", ) with pytest.raises(SystemExit): @@ -476,7 +483,6 @@ m_handle_unicode, m_attach_with_token, m_enable, - _m_getuid, auto_enable, FakeConfig, event, @@ -489,7 +495,7 @@ args = mock.MagicMock( token=None, attach_config=FakeFile( - yaml.dump({"token": "faketoken", "enable_services": ["cis"]}) + safe_dump({"token": "faketoken", "enable_services": ["cis"]}) ), auto_enable=auto_enable, ) @@ -505,7 +511,7 @@ assert [] == m_enable.call_args_list args.attach_config = FakeFile( - yaml.dump({"token": "faketoken", "enable_services": ["cis"]}) + safe_dump({"token": "faketoken", "enable_services": ["cis"]}) ) fake_stdout = io.StringIO() @@ -530,7 +536,7 @@ @mock.patch("uaclient.contract.process_entitlement_delta") @mock.patch("uaclient.contract.apply_contract_overrides") @mock.patch("uaclient.contract.UAContractClient.request_url") - @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages") + @mock.patch("uaclient.jobs.update_messaging.update_motd_messages") def test_attach_when_one_service_fails_to_enable( self, _m_update_messages, @@ -538,7 +544,6 @@ _m_apply_contract_overrides, m_process_entitlement_delta, m_enable_order, - _m_getuid, FakeConfig, event, ): @@ -558,7 +563,9 @@ "accountInfo": { "id": "acct-1", "name": "acc-name", - "createdAt": "2019-06-14T06:45:50Z", + "createdAt": util.parse_rfc3339_date( + "2019-06-14T06:45:50Z" + ), "externalAccountIDs": [ {"IDs": ["id1"], "origin": "AWS"} ], @@ -617,7 +624,6 @@ m_revoke, m_wait, m_initiate, - _m_getuid, FakeConfig, ): m_initiate.return_value = mock.MagicMock( @@ -633,9 +639,7 @@ assert 1 == m_wait.call_count assert 1 == m_revoke.call_count - def test_magic_attach_fails_if_format_json_param_used( - self, _m_getuid, FakeConfig - ): + def test_magic_attach_fails_if_format_json_param_used(self, FakeConfig): m_args = mock.MagicMock(token=None, attach_config=None, format="json") with pytest.raises(MagicAttachInvalidParam) as exc_info: diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_auto_attach.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_auto_attach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_auto_attach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_auto_attach.py 2023-04-05 15:14:00.000000000 +0000 @@ -31,21 +31,18 @@ ) -@mock.patch(M_PATH + "os.getuid") -def test_non_root_users_are_rejected(getuid, FakeConfig): +@mock.patch(M_PATH + "util.we_are_currently_root", return_value=False) +def test_non_root_users_are_rejected(we_are_currently_root, FakeConfig): """Check that a UID != 0 will receive a message and exit non-zero""" - getuid.return_value = 1 cfg = FakeConfig() with pytest.raises(exceptions.NonRootUserError): action_auto_attach(mock.MagicMock(), cfg=cfg) -# For all of these tests we want to appear as root, so mock on the class -@mock.patch(M_PATH + "os.getuid", return_value=0) class TestActionAutoAttach: @mock.patch(M_PATH + "contract.get_available_resources") - def test_auto_attach_help(self, _m_resources, _getuid, capsys, FakeConfig): + def test_auto_attach_help(self, _m_resources, capsys, FakeConfig): with pytest.raises(SystemExit): with mock.patch( "sys.argv", ["/usr/bin/ua", "auto-attach", "--help"] @@ -64,7 +61,6 @@ self, m_full_auto_attach, m_post_cli_attach, - _m_getuid, FakeConfig, ): cfg = FakeConfig() @@ -88,7 +84,6 @@ m_full_auto_attach, m_post_cli_attach, m_event, - _m_getuid, FakeConfig, ): m_full_auto_attach.side_effect = exceptions.UrlError( @@ -127,7 +122,6 @@ m_full_auto_attach, m_post_cli_attach, m_logging, - _m_getuid, api_side_effect, expected_err, capsys, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_collect_logs.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_collect_logs.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_collect_logs.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_collect_logs.py 2023-04-05 15:14:00.000000000 +0000 @@ -28,12 +28,9 @@ ) -@mock.patch(M_PATH + "os.getuid", return_value=0) class TestActionCollectLogs: @mock.patch(M_PATH + "contract.get_available_resources") - def test_collect_logs_help( - self, _m_resources, _getuid, capsys, FakeConfig - ): + def test_collect_logs_help(self, _m_resources, capsys, FakeConfig): with pytest.raises(SystemExit): with mock.patch( "sys.argv", ["/usr/bin/ua", "collect-logs", "--help"] @@ -57,7 +54,7 @@ @mock.patch("builtins.open") @mock.patch(M_PATH + "util.redact_sensitive_logs", return_value="test") # let's pretend all files exist - @mock.patch(M_PATH + "os.path.isfile", return_value=True) + @mock.patch("os.path.isfile", return_value=True) @mock.patch("uaclient.system.write_file") @mock.patch("uaclient.system.load_file") @mock.patch("uaclient.system.subp", return_value=(None, None)) @@ -71,7 +68,6 @@ _fopen, _tarfile, _glob, - _getuid, FakeConfig, ): cfg = FakeConfig() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,57 @@ +import mock +import pytest + +from uaclient.cli import main + +M_PATH = "uaclient.cli." + +HELP_OUTPUT = """\ +usage: pro config [flags] + +Manage Ubuntu Pro configuration + +Flags: + -h, --help show this help message and exit + +Available Commands: + + show show all Ubuntu Pro configuration setting(s) + set set Ubuntu Pro configuration setting + unset unset Ubuntu Pro configuration setting +""" # noqa + + +@mock.patch("uaclient.cli.logging.error") +@mock.patch("uaclient.cli.setup_logging") +@mock.patch(M_PATH + "contract.get_available_resources") +class TestMainConfig: + @pytest.mark.parametrize("additional_params", ([], ["--help"])) + def test_config_help( + self, + _m_resources, + _logging, + logging_error, + additional_params, + capsys, + FakeConfig, + ): + """Show help for --help and absent positional param""" + with pytest.raises(SystemExit): + with mock.patch( + "sys.argv", ["/usr/bin/ua", "config"] + additional_params + ): + with mock.patch( + "uaclient.config.UAConfig", + return_value=FakeConfig(), + ): + main() + out, err = capsys.readouterr() + assert HELP_OUTPUT == out + if additional_params == ["--help"]: + assert "" == err + else: + # When lacking show, set or unset inform about valid values + assert "\n must be one of: show, set, unset\n" == err + assert [ + mock.call("\n must be one of: show, set, unset") + ] == logging_error.call_args_list diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_set.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_set.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_set.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_set.py 2023-04-05 15:14:00.000000000 +0000 @@ -7,7 +7,7 @@ from uaclient.exceptions import NonRootUserError, UserFacingError HELP_OUTPUT = """\ -usage: pro set = [flags] +usage: pro config set = [flags] Set and apply Ubuntu Pro configuration settings @@ -25,7 +25,6 @@ M_LIVEPATCH = "uaclient.entitlements.livepatch." -@mock.patch("uaclient.cli.os.getuid", return_value=0) @mock.patch("uaclient.cli.setup_logging") class TestMainConfigSet: @pytest.mark.parametrize( @@ -63,7 +62,6 @@ self, _m_resources, _logging, - _getuid, kv_pair, err_msg, capsys, @@ -84,15 +82,14 @@ assert err_msg in err -@mock.patch("uaclient.config.UAConfig.write_cfg") -@mock.patch("uaclient.cli.os.getuid", return_value=0) +@mock.patch("uaclient.config.state_files.user_config_file.write") @mock.patch("uaclient.cli.contract.get_available_resources") class TestActionConfigSet: + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) def test_set_error_on_non_root_user( - self, _m_resources, getuid, _write_cfg, FakeConfig + self, _m_resources, _we_are_currently_root, _write, FakeConfig ): """Root is required to run pro config set.""" - getuid.return_value = 1 args = mock.MagicMock(key_value_pair="something=1") cfg = FakeConfig() with pytest.raises(NonRootUserError): @@ -107,7 +104,7 @@ ("https_proxy", "https://proxy", True), ), ) - @mock.patch(M_LIVEPATCH + "configure_livepatch_proxy") + @mock.patch("uaclient.livepatch.configure_livepatch_proxy") @mock.patch(M_LIVEPATCH + "LivepatchEntitlement.application_status") @mock.patch("uaclient.snap.configure_snap_proxy") @mock.patch("uaclient.util.validate_proxy") @@ -118,8 +115,7 @@ livepatch_status, configure_livepatch_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, livepatch_enabled, @@ -182,8 +178,7 @@ validate_proxy, configure_apt_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, scope, @@ -289,8 +284,7 @@ validate_proxy, configure_apt_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, scope, @@ -400,8 +394,7 @@ validate_proxy, configure_apt_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, scope, @@ -464,8 +457,7 @@ self, setup_apt_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, scope, @@ -504,8 +496,7 @@ self, setup_apt_proxy, _m_resources, - _getuid, - _write_cfg, + _write, key, value, scope, @@ -523,9 +514,7 @@ assert 1 == setup_apt_proxy.call_count assert [mock.call(**kwargs)] == setup_apt_proxy.call_args_list - def test_set_timer_interval( - self, _m_resources, _getuid, _write_cfg, FakeConfig - ): + def test_set_timer_interval(self, _m_resources, _write, FakeConfig): args = mock.MagicMock(key_value_pair="update_messaging_timer=28800") cfg = FakeConfig() action_config_set(args, cfg=cfg) @@ -533,7 +522,11 @@ @pytest.mark.parametrize("invalid_value", ("notanumber", -1)) def test_error_when_interval_is_not_valid( - self, _m_resources, _getuid, _write_cfg, FakeConfig, invalid_value + self, + _m_resources, + _write, + FakeConfig, + invalid_value, ): args = mock.MagicMock( key_value_pair="update_messaging_timer={}".format(invalid_value) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_show.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_show.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_show.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_show.py 2023-04-05 15:14:00.000000000 +0000 @@ -6,39 +6,32 @@ M_PATH = "uaclient.cli." HELP_OUTPUT = """\ -usage: pro config [flags] +usage: pro config show [key] [flags] -Manage Ubuntu Pro configuration +Show customisable configuration settings -Flags: - -h, --help show this help message and exit +positional arguments: + key Optional key or key(s) to show configuration settings. -Available Commands: - - show show all Ubuntu Pro configuration setting(s) - set set Ubuntu Pro configuration setting - unset unset Ubuntu Pro configuration setting -""" # noqa +""" @mock.patch("uaclient.cli.logging.error") @mock.patch("uaclient.cli.setup_logging") @mock.patch(M_PATH + "contract.get_available_resources") class TestMainConfigShow: - @pytest.mark.parametrize("additional_params", ([], ["--help"])) def test_config_show_help( self, _m_resources, _logging, logging_error, - additional_params, capsys, FakeConfig, ): """Show help for --help and absent positional param""" with pytest.raises(SystemExit): with mock.patch( - "sys.argv", ["/usr/bin/ua", "config"] + additional_params + "sys.argv", ["/usr/bin/ua", "config", "show", "--help"] ): with mock.patch( "uaclient.config.UAConfig", @@ -46,15 +39,8 @@ ): main() out, err = capsys.readouterr() - assert HELP_OUTPUT == out - if additional_params == ["--help"]: - assert "" == err - else: - # When lacking show, set or unset inform about valid values - assert "\n must be one of: show, set, unset\n" == err - assert [ - mock.call("\n must be one of: show, set, unset") - ] == logging_error.call_args_list + assert out.startswith(HELP_OUTPUT) + assert "" == err def test_config_show_error_on_invalid_subcommand( self, _m_resources, _logging, _logging_error, capsys, FakeConfig @@ -89,15 +75,16 @@ "global_apt_https_proxy", ), ) - @mock.patch("uaclient.config.UAConfig.write_cfg") def test_show_values_and_limit_when_optional_key_provided( - self, _write_cfg, optional_key, FakeConfig, capsys + self, optional_key, FakeConfig, capsys ): cfg = FakeConfig() - cfg.http_proxy = "http://http_proxy" - cfg.https_proxy = "http://https_proxy" - cfg.global_apt_http_proxy = "http://global_apt_http_proxy" - cfg.global_apt_https_proxy = "http://global_apt_https_proxy" + cfg.user_config.http_proxy = "http://http_proxy" + cfg.user_config.https_proxy = "http://https_proxy" + cfg.user_config.global_apt_http_proxy = "http://global_apt_http_proxy" + cfg.user_config.global_apt_https_proxy = ( + "http://global_apt_https_proxy" + ) args = mock.MagicMock(key=optional_key) action_config_show(args, cfg=cfg) out, err = capsys.readouterr() @@ -114,8 +101,8 @@ ua_apt_https_proxy None global_apt_http_proxy http://global_apt_http_proxy global_apt_https_proxy http://global_apt_https_proxy -update_messaging_timer None -metering_timer None +update_messaging_timer 21600 +metering_timer 14400 apt_news True apt_news_url https://motd.ubuntu.com/aptnews.json """ diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_unset.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_unset.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_config_unset.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_config_unset.py 2023-04-05 15:14:00.000000000 +0000 @@ -6,7 +6,7 @@ from uaclient.exceptions import NonRootUserError HELP_OUTPUT = """\ -usage: pro unset [flags] +usage: pro config unset [flags] Unset Ubuntu Pro configuration setting @@ -24,7 +24,6 @@ M_LIVEPATCH = "uaclient.entitlements.livepatch." -@mock.patch("uaclient.cli.os.getuid", return_value=0) @mock.patch("uaclient.cli.setup_logging") @mock.patch("uaclient.cli.contract.get_available_resources") class TestMainConfigUnSet: @@ -53,7 +52,6 @@ self, _m_resources, _logging, - _getuid, kv_pair, err_msg, capsys, @@ -74,12 +72,13 @@ assert err_msg in err -@mock.patch("uaclient.config.UAConfig.write_cfg") -@mock.patch("uaclient.cli.os.getuid", return_value=0) +@mock.patch("uaclient.config.state_files.user_config_file.write") class TestActionConfigUnSet: - def test_set_error_on_non_root_user(self, getuid, _write_cfg, FakeConfig): + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) + def test_set_error_on_non_root_user( + self, we_are_currently_root, _write, FakeConfig + ): """Root is required to run pro config unset.""" - getuid.return_value = 1 args = mock.MagicMock(key="https_proxy") cfg = FakeConfig() with pytest.raises(NonRootUserError): @@ -94,7 +93,7 @@ ("https_proxy", True), ), ) - @mock.patch(M_LIVEPATCH + "unconfigure_livepatch_proxy") + @mock.patch("uaclient.livepatch.unconfigure_livepatch_proxy") @mock.patch(M_LIVEPATCH + "LivepatchEntitlement.application_status") @mock.patch("uaclient.snap.unconfigure_snap_proxy") def test_set_http_proxy_and_https_proxy_affects_snap_and_maybe_livepatch( @@ -102,8 +101,7 @@ unconfigure_snap_proxy, livepatch_status, unconfigure_livepatch_proxy, - _getuid, - _write_cfg, + _write, key, livepatch_enabled, FakeConfig, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_detach.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_detach.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_detach.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_detach.py 2023-04-05 15:14:00.000000000 +0000 @@ -28,13 +28,13 @@ @mock.patch("uaclient.cli.util.prompt_for_confirmation", return_value=True) -@mock.patch("uaclient.cli.os.getuid") class TestActionDetach: + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) def test_non_root_users_are_rejected( - self, m_getuid, _m_prompt, FakeConfig, event, capsys + self, m_we_are_currently_root, _m_prompt, FakeConfig, event, capsys ): """Check that a UID != 0 will receive a message and exit non-zero""" - m_getuid.return_value = 1 + m_we_are_currently_root.return_value = False args = mock.MagicMock() cfg = FakeConfig.for_attached_machine() @@ -67,11 +67,10 @@ assert expected == json.loads(capsys.readouterr()[0]) def test_unattached_error_message( - self, m_getuid, _m_prompt, FakeConfig, capsys, event + self, _m_prompt, FakeConfig, capsys, event ): """Check that root user gets unattached message.""" - m_getuid.return_value = 0 cfg = FakeConfig() args = mock.MagicMock() with pytest.raises(exceptions.UnattachedError) as err: @@ -105,10 +104,14 @@ @mock.patch("uaclient.system.subp") def test_lock_file_exists( - self, m_subp, m_getuid, m_prompt, FakeConfig, capsys, event + self, + m_subp, + m_prompt, + FakeConfig, + capsys, + event, ): """Check when an operation holds a lock file, detach cannot run.""" - m_getuid.return_value = 0 cfg = FakeConfig.for_attached_machine() args = mock.MagicMock() cfg.write_cache("lock", "123:pro enable") @@ -149,7 +152,7 @@ [(True, False, True), (False, False, False), (True, True, True)], ) @mock.patch("uaclient.contract.UAContractClient") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") @mock.patch("uaclient.cli.entitlements_disable_order") @mock.patch("uaclient.cli.entitlements.entitlement_factory") def test_entitlements_disabled_appropriately( @@ -158,7 +161,6 @@ m_disable_order, m_update_apt_and_motd_msgs, m_client, - m_getuid, m_prompt, prompt_response, assume_yes, @@ -173,7 +175,6 @@ # to the action # expect_disable: whether or not the enabled entitlement is expected # to be disabled by the action - m_getuid.return_value = 0 cfg = FakeConfig.for_attached_machine() fake_client = FakeContractClient(cfg) @@ -229,18 +230,16 @@ @mock.patch("uaclient.cli.entitlements_disable_order") @mock.patch("uaclient.contract.UAContractClient") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") def test_config_cache_deleted( self, m_update_apt_and_motd_msgs, m_client, m_disable_order, - m_getuid, _m_prompt, FakeConfig, tmpdir, ): - m_getuid.return_value = 0 m_disable_order.return_value = [] fake_client = FakeContractClient(FakeConfig.for_attached_machine()) @@ -256,19 +255,17 @@ @mock.patch("uaclient.cli.entitlements_disable_order") @mock.patch("uaclient.contract.UAContractClient") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") def test_correct_message_emitted( self, m_update_apt_and_motd_msgs, m_client, m_disable_order, - m_getuid, _m_prompt, capsys, FakeConfig, tmpdir, ): - m_getuid.return_value = 0 m_disable_order.return_value = [] fake_client = FakeContractClient(FakeConfig.for_attached_machine()) @@ -286,18 +283,16 @@ @mock.patch("uaclient.cli.entitlements_disable_order") @mock.patch("uaclient.contract.UAContractClient") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") def test_returns_zero( self, m_update_apt_and_motd_msgs, m_client, m_disable_order, - m_getuid, _m_prompt, FakeConfig, tmpdir, ): - m_getuid.return_value = 0 m_disable_order.return_value = [] fake_client = FakeContractClient(FakeConfig.for_attached_machine()) @@ -345,7 +340,7 @@ ], ) @mock.patch("uaclient.contract.UAContractClient") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") @mock.patch("uaclient.entitlements.entitlement_factory") @mock.patch("uaclient.cli.entitlements_disable_order") def test_informational_message_emitted( @@ -354,7 +349,6 @@ m_ent_factory, m_update_apt_and_motd_msgs, m_client, - m_getuid, _m_prompt, capsys, classes, @@ -365,7 +359,6 @@ tmpdir, event, ): - m_getuid.return_value = 0 m_ent_factory.side_effect = classes m_disable_order.return_value = disable_order diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_disable.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_disable.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_disable.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_disable.py 2023-04-05 15:14:00.000000000 +0000 @@ -51,10 +51,9 @@ ) -@mock.patch("uaclient.cli.os.getuid", return_value=0) class TestDisable: @mock.patch("uaclient.cli.contract.get_available_resources") - def test_disable_help(self, _m_resources, _getuid, capsys, FakeConfig): + def test_disable_help(self, _m_resources, capsys, FakeConfig): with pytest.raises(SystemExit): with mock.patch("sys.argv", ["/usr/bin/ua", "disable", "--help"]): with mock.patch( @@ -79,7 +78,6 @@ m_status, m_valid_services, m_entitlement_factory, - _m_getuid, disable_return, return_code, assume_yes, @@ -190,7 +188,6 @@ m_status, m_valid_services, m_entitlement_factory, - _m_getuid, assume_yes, tmpdir, event, @@ -314,28 +311,29 @@ assert expected == json.loads(fake_stdout.getvalue()) @pytest.mark.parametrize( - "uid,expected_error_template", + "root,expected_error_template", [ - (0, messages.INVALID_SERVICE_OP_FAILURE), - (1000, messages.NONROOT_USER), + (True, messages.INVALID_SERVICE_OP_FAILURE), + (False, messages.NONROOT_USER), ], ) + @mock.patch("uaclient.util.we_are_currently_root") def test_invalid_service_error_message( self, - m_getuid, - uid, + m_we_are_currently_root, + root, expected_error_template, FakeConfig, event, all_service_msg, ): """Check invalid service name results in custom error message.""" - m_getuid.return_value = uid + m_we_are_currently_root.return_value = root cfg = FakeConfig.for_attached_machine() args = mock.MagicMock() - if not uid: + if root: expected_error = expected_error_template.format( operation="disable", invalid_service="bogus", @@ -380,9 +378,12 @@ @pytest.mark.parametrize("service", [["bogus"], ["bogus1", "bogus2"]]) def test_invalid_service_names( - self, m_getuid, service, FakeConfig, event, all_service_msg + self, + service, + FakeConfig, + event, + all_service_msg, ): - m_getuid.return_value = 0 expected_error_tmpl = messages.INVALID_SERVICE_OP_FAILURE cfg = FakeConfig.for_attached_machine() @@ -428,22 +429,28 @@ assert expected == json.loads(fake_stdout.getvalue()) @pytest.mark.parametrize( - "uid,expected_error_template", + "root,expected_error_template", [ - (0, messages.VALID_SERVICE_FAILURE_UNATTACHED), - (1000, messages.NONROOT_USER), + (True, messages.VALID_SERVICE_FAILURE_UNATTACHED), + (False, messages.NONROOT_USER), ], ) + @mock.patch("uaclient.util.we_are_currently_root") def test_unattached_error_message( - self, m_getuid, uid, expected_error_template, FakeConfig, event + self, + m_we_are_currently_root, + root, + expected_error_template, + FakeConfig, + event, ): """Check that root user gets unattached message.""" - m_getuid.return_value = uid + m_we_are_currently_root.return_value = root cfg = FakeConfig() args = mock.MagicMock() args.command = "disable" - if not uid: + if root: expected_error = expected_error_template.format( valid_service="esm-infra" ) @@ -486,7 +493,12 @@ assert expected == json.loads(fake_stdout.getvalue()) @mock.patch("uaclient.system.subp") - def test_lock_file_exists(self, m_subp, m_getuid, FakeConfig, event): + def test_lock_file_exists( + self, + m_subp, + FakeConfig, + event, + ): """Check inability to disable if operation in progress holds lock.""" cfg = FakeConfig().for_attached_machine() args = mock.MagicMock() @@ -529,9 +541,7 @@ } assert expected == json.loads(fake_stdout.getvalue()) - def test_format_json_fails_when_assume_yes_flag_not_used( - self, _m_getuid, event - ): + def test_format_json_fails_when_assume_yes_flag_not_used(self, event): cfg = mock.MagicMock() args_mock = mock.MagicMock() args_mock.format = "json" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_enable.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_enable.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_enable.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_enable.py 2023-04-05 15:14:00.000000000 +0000 @@ -34,14 +34,12 @@ """ -@mock.patch("uaclient.cli.os.getuid") @mock.patch("uaclient.contract.request_updated_contract") class TestActionEnable: @mock.patch("uaclient.cli.contract.get_available_resources") def test_enable_help( self, _m_resources, - _getuid, _request_updated_contract, capsys, FakeConfig, @@ -56,18 +54,18 @@ out, _err = capsys.readouterr() assert HELP_OUTPUT == out + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) @mock.patch("uaclient.cli.contract.get_available_resources") def test_non_root_users_are_rejected( self, _m_resources, _request_updated_contract, - getuid, + we_are_currently_root, capsys, event, FakeConfig, ): """Check that a UID != 0 will receive a message and exit non-zero""" - getuid.return_value = 1 args = mock.MagicMock() cfg = FakeConfig.for_attached_machine() @@ -116,13 +114,11 @@ self, m_subp, _request_updated_contract, - getuid, capsys, event, FakeConfig, ): """Check inability to enable if operation holds lock file.""" - getuid.return_value = 0 cfg = FakeConfig.for_attached_machine() cfg.write_cache("lock", "123:pro disable") args = mock.MagicMock() @@ -168,17 +164,18 @@ assert expected == json.loads(capsys.readouterr()[0]) @pytest.mark.parametrize( - "uid,expected_error_template", + "root,expected_error_template", [ - (0, messages.VALID_SERVICE_FAILURE_UNATTACHED), - (1000, messages.NONROOT_USER), + (True, messages.VALID_SERVICE_FAILURE_UNATTACHED), + (False, messages.NONROOT_USER), ], ) + @mock.patch("uaclient.util.we_are_currently_root") def test_unattached_error_message( self, + m_we_are_currently_root, _request_updated_contract, - m_getuid, - uid, + root, expected_error_template, capsys, event, @@ -186,14 +183,14 @@ ): """Check that root user gets unattached message.""" - m_getuid.return_value = uid + m_we_are_currently_root.return_value = root cfg = FakeConfig() args = mock.MagicMock() args.command = "enable" args.service = ["esm-infra"] - if not uid: + if root: expected_error = expected_error_template.format( valid_service="esm-infra" ) @@ -230,17 +227,18 @@ @pytest.mark.parametrize("is_attached", (True, False)) @pytest.mark.parametrize( - "uid,expected_error_template", + "root,expected_error_template", [ - (0, messages.INVALID_SERVICE_OP_FAILURE), - (1000, messages.NONROOT_USER), + (True, messages.INVALID_SERVICE_OP_FAILURE), + (False, messages.NONROOT_USER), ], ) + @mock.patch("uaclient.util.we_are_currently_root") def test_invalid_service_error_message( self, + m_we_are_currently_root, _request_updated_contract, - m_getuid, - uid, + root, expected_error_template, is_attached, event, @@ -248,7 +246,7 @@ ): """Check invalid service name results in custom error message.""" - m_getuid.return_value = uid + m_we_are_currently_root.return_value = root if is_attached: cfg = FakeConfig.for_attached_machine() service_msg = "\n".join( @@ -277,7 +275,7 @@ with pytest.raises(exceptions.UserFacingError) as err: action_enable(args, cfg) - if not uid: + if root: expected_error = expected_error_template.format( operation="enable", invalid_service="bogus", @@ -307,7 +305,7 @@ "type": "system", } ], - "failed_services": ["bogus"] if not uid and is_attached else [], + "failed_services": ["bogus"] if root and is_attached else [], "needs_reboot": False, "processed_services": [], "warnings": [], @@ -315,24 +313,25 @@ assert expected == json.loads(fake_stdout.getvalue()) @pytest.mark.parametrize( - "uid,expected_error_template", + "root,expected_error_template", [ - (0, messages.MIXED_SERVICES_FAILURE_UNATTACHED), - (1000, messages.NONROOT_USER), + (True, messages.MIXED_SERVICES_FAILURE_UNATTACHED), + (False, messages.NONROOT_USER), ], ) + @mock.patch("uaclient.util.we_are_currently_root") def test_unattached_invalid_and_valid_service_error_message( self, + m_we_are_currently_root, _request_updated_contract, - m_getuid, - uid, + root, expected_error_template, event, FakeConfig, ): """Check invalid service name results in custom error message.""" - m_getuid.return_value = uid + m_we_are_currently_root.return_value = root cfg = FakeConfig() args = mock.MagicMock() @@ -341,7 +340,7 @@ with pytest.raises(exceptions.UserFacingError) as err: action_enable(args, cfg) - if not uid: + if root: expected_error = expected_error_template.format( operation="enable", valid_service="fips", @@ -387,12 +386,10 @@ m_valid_services, _m_get_available_resources, m_request_updated_contract, - m_getuid, assume_yes, FakeConfig, ): """assume-yes parameter is passed to entitlement instantiation.""" - m_getuid.return_value = 0 m_entitlement_cls = mock.MagicMock() m_valid_services.return_value = ["testitlement"] @@ -431,11 +428,9 @@ m_entitlement_factory, _m_get_available_resources, _m_request_updated_contract, - m_getuid, event, FakeConfig, ): - m_getuid.return_value = 0 expected_error_tmpl = messages.INVALID_SERVICE_OP_FAILURE m_ent1_cls = mock.Mock() @@ -550,12 +545,10 @@ m_entitlement_factory, _m_get_available_resources, _m_request_updated_contract, - m_getuid, beta_flag, event, FakeConfig, ): - m_getuid.return_value = 0 expected_error_tmpl = messages.INVALID_SERVICE_OP_FAILURE m_ent1_cls = mock.Mock() @@ -691,11 +684,9 @@ self, _m_get_available_resources, _m_request_updated_contract, - m_getuid, event, FakeConfig, ): - 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 @@ -767,13 +758,11 @@ def test_invalid_service_names( self, _m_request_updated_contract, - m_getuid, service, beta, event, FakeConfig, ): - m_getuid.return_value = 0 expected_error_tmpl = messages.INVALID_SERVICE_OP_FAILURE expected_msg = "One moment, checking your subscription first\n" @@ -840,12 +829,10 @@ m_status, _m_get_available_resources, _m_request_updated_contract, - m_getuid, allow_beta, event, FakeConfig, ): - m_getuid.return_value = 0 m_entitlement_cls = mock.Mock() m_entitlement_obj = m_entitlement_cls.return_value m_entitlement_obj.enable.return_value = (True, None) @@ -910,7 +897,7 @@ assert expected_ret == ret def test_format_json_fails_when_assume_yes_flag_not_used( - self, _m_get_available_resources, _m_getuid, event + self, _m_get_available_resources, event ): cfg = mock.MagicMock() args_mock = mock.MagicMock() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli.py 2023-04-05 15:14:00.000000000 +0000 @@ -31,6 +31,7 @@ UnattachedError, UserFacingError, ) +from uaclient.files.notices import Notice BIG_DESC = "123456789 " * 7 + "next line" BIG_URL = "http://" + "adsf" * 10 @@ -145,10 +146,11 @@ class TestCLIParser: maxDiff = None + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) @mock.patch("uaclient.cli.entitlements") @mock.patch("uaclient.cli.contract") def test_help_descr_and_url_is_wrapped_at_eighty_chars( - self, m_contract, m_entitlements, get_help + self, m_contract, m_entitlements, m_we_are_currently_root, get_help ): """Help lines are wrapped at 80 chars""" @@ -169,9 +171,10 @@ out, _ = get_help() assert "\n".join(lines) in out + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) @mock.patch("uaclient.cli.contract") def test_help_sourced_dynamically_from_each_entitlement( - self, m_contract, get_help + self, m_contract, m_we_are_currently_root, get_help ): """Help output is sourced from entitlement name and description.""" m_contract.get_available_resources.return_value = AVAILABLE_RESOURCES @@ -357,13 +360,13 @@ class TestAssertLockFile: @mock.patch("os.getpid", return_value=123) @mock.patch(M_PATH_UACONFIG + "delete_cache_key") - @mock.patch("uaclient.files.NoticeFile.add") + @mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch(M_PATH_UACONFIG + "write_cache") def test_assert_root_creates_lock_and_notice( self, m_write_cache, m_add_notice, - m_remove_notice, + m_delete_cache, _m_getpid, FakeConfig, ): @@ -379,8 +382,10 @@ ret = test_function(arg, cfg=FakeConfig()) assert mock.sentinel.success == ret lock_msg = "Operation in progress: some operation" - assert [mock.call("", lock_msg)] == m_add_notice.call_args_list - assert [mock.call("lock")] == m_remove_notice.call_args_list + assert [ + mock.call(Notice.OPERATION_IN_PROGRESS, lock_msg) + ] == m_add_notice.call_args_list + assert [mock.call("lock")] == m_delete_cache.call_args_list assert [ mock.call("lock", "123:some operation") ] == m_write_cache.call_args_list @@ -388,6 +393,7 @@ class TestAssertRoot: def test_assert_root_when_root(self): + # autouse mock for we_are_currently_root defaults it to True arg, kwarg = mock.sentinel.arg, mock.sentinel.kwarg @assert_root @@ -397,8 +403,7 @@ return mock.sentinel.success - with mock.patch("uaclient.cli.os.getuid", return_value=0): - ret = test_function(arg, kwarg=kwarg) + ret = test_function(arg, kwarg=kwarg) assert mock.sentinel.success == ret @@ -407,22 +412,26 @@ def test_function(): pass - with mock.patch("uaclient.cli.os.getuid", return_value=1000): + with mock.patch( + "uaclient.cli.util.we_are_currently_root", return_value=False + ): with pytest.raises(NonRootUserError): test_function() # Test multiple uids, to be sure that the root checking is absent -@pytest.mark.parametrize("uid", [0, 1000]) +@pytest.mark.parametrize("root", [True, False]) class TestAssertAttached: - def test_assert_attached_when_attached(self, capsys, uid, FakeConfig): + def test_assert_attached_when_attached(self, capsys, root, FakeConfig): @assert_attached() def test_function(args, cfg): return mock.sentinel.success cfg = FakeConfig.for_attached_machine() - with mock.patch("uaclient.cli.os.getuid", return_value=uid): + with mock.patch( + "uaclient.cli.util.we_are_currently_root", return_value=root + ): ret = test_function(mock.Mock(), cfg) assert mock.sentinel.success == ret @@ -430,39 +439,45 @@ out, _err = capsys.readouterr() assert "" == out.strip() - def test_assert_attached_when_unattached(self, uid, FakeConfig): + def test_assert_attached_when_unattached(self, root, FakeConfig): @assert_attached() def test_function(args, cfg): pass cfg = FakeConfig() - with mock.patch("uaclient.cli.os.getuid", return_value=uid): + with mock.patch( + "uaclient.cli.util.we_are_currently_root", return_value=root + ): with pytest.raises(UnattachedError): test_function(mock.Mock(), cfg) -@pytest.mark.parametrize("uid", [0, 1000]) +@pytest.mark.parametrize("root", [True, False]) class TestAssertNotAttached: - def test_when_attached(self, uid, FakeConfig): + def test_when_attached(self, root, FakeConfig): @assert_not_attached def test_function(args, cfg): pass cfg = FakeConfig.for_attached_machine() - with mock.patch("uaclient.cli.os.getuid", return_value=uid): + with mock.patch( + "uaclient.cli.util.we_are_currently_root", return_value=root + ): with pytest.raises(AlreadyAttachedError): test_function(mock.Mock(), cfg) - def test_when_not_attached(self, capsys, uid, FakeConfig): + def test_when_not_attached(self, capsys, root, FakeConfig): @assert_not_attached def test_function(args, cfg): return mock.sentinel.success cfg = FakeConfig() - with mock.patch("uaclient.cli.os.getuid", return_value=uid): + with mock.patch( + "uaclient.cli.util.we_are_currently_root", return_value=root + ): ret = test_function(mock.Mock(), cfg) assert mock.sentinel.success == ret @@ -649,7 +664,6 @@ logging_sandbox, caplog_text, ): - m_args = m_get_parser.return_value.parse_args.return_value m_args.action.side_effect = exceptions.UrlError( socket.gaierror(-2, "Name or service not known"), url=error_url @@ -727,8 +741,9 @@ class TestSetupLogging: @pytest.mark.parametrize("level", (logging.INFO, logging.ERROR)) + @mock.patch("uaclient.cli.util.we_are_currently_root", return_value=False) def test_console_log_configured_if_not_present( - self, level, capsys, logging_sandbox + self, m_we_are_currently_root, level, capsys, logging_sandbox ): setup_logging(level, logging.INFO) logging.log(level, "after setup") @@ -738,8 +753,9 @@ assert "after setup" in err assert "not present" not in err + @mock.patch("uaclient.cli.util.we_are_currently_root", return_value=False) def test_console_log_configured_if_already_present( - self, capsys, logging_sandbox + self, m_we_are_currently_root, capsys, logging_sandbox ): logging.getLogger().addHandler(logging.StreamHandler(sys.stderr)) @@ -753,9 +769,9 @@ assert "ERROR: before setup" not in err assert "ERROR: after setup" in err - @mock.patch("uaclient.cli.os.getuid", return_value=100) + @mock.patch("uaclient.cli.util.we_are_currently_root", return_value=False) def test_file_log_not_configured_if_not_root( - self, m_getuid, tmpdir, logging_sandbox + self, m_we_are_currently_root, tmpdir, logging_sandbox ): log_file = tmpdir.join("log_file") @@ -765,10 +781,13 @@ assert not log_file.exists() @pytest.mark.parametrize("log_filename", (None, "file.log")) - @mock.patch("uaclient.cli.os.getuid", return_value=0) @mock.patch("uaclient.cli.config") def test_file_log_configured_if_root( - self, m_config, _m_getuid, log_filename, logging_sandbox, tmpdir + self, + m_config, + log_filename, + logging_sandbox, + tmpdir, ): if log_filename is None: log_filename = "default.log" @@ -782,30 +801,31 @@ assert "after setup" in log_file.read() - @mock.patch("uaclient.cli.os.getuid", return_value=0) - @mock.patch("uaclient.cli.config.UAConfig") def test_file_log_configured_if_already_present( - self, m_config, _m_getuid, logging_sandbox, tmpdir, FakeConfig + self, + logging_sandbox, + tmpdir, ): - some_file = log_file = tmpdir.join("default.log") + some_file = tmpdir.join("default.log") logging.getLogger().addHandler(logging.FileHandler(some_file.strpath)) log_file = tmpdir.join("file.log") - cfg = FakeConfig({"log_file": log_file.strpath}) - m_config.return_value = cfg logging.error("before setup") - setup_logging(logging.INFO, logging.INFO) + setup_logging(logging.INFO, logging.INFO, log_file=log_file.strpath) logging.error("after setup") content = log_file.read() - assert "[ERROR]: before setup" not in content - assert "[ERROR]: after setup" in content + assert re.match(r'\[.*"ERROR", "before setup"', content) is None + assert re.match(r'\[.*"ERROR",.*"after setup"', content) @mock.patch("uaclient.cli.config.UAConfig") - @mock.patch("uaclient.cli.os.getuid", return_value=0) def test_custom_logger_configuration( - self, m_getuid, m_config, logging_sandbox, tmpdir, FakeConfig + self, + m_config, + logging_sandbox, + tmpdir, + FakeConfig, ): log_file = tmpdir.join("file.log") cfg = FakeConfig({"log_file": log_file.strpath}) @@ -821,9 +841,12 @@ assert len(root_logger.handlers) == n_root_handlers @mock.patch("uaclient.cli.config.UAConfig") - @mock.patch("uaclient.cli.os.getuid", return_value=0) def test_no_duplicate_ua_handlers( - self, m_getuid, m_config, logging_sandbox, tmpdir, FakeConfig + self, + m_config, + logging_sandbox, + tmpdir, + FakeConfig, ): log_file = tmpdir.join("file.log") cfg = FakeConfig({"log_file": log_file.strpath}) @@ -865,10 +888,13 @@ assert len(file_handlers) == 1 @pytest.mark.parametrize("pre_existing", (True, False)) - @mock.patch("uaclient.cli.os.getuid", return_value=0) @mock.patch("uaclient.cli.config") def test_file_log_is_world_readable( - self, m_config, _m_getuid, logging_sandbox, tmpdir, pre_existing + self, + m_config, + logging_sandbox, + tmpdir, + pre_existing, ): log_file = tmpdir.join("root-only.log") log_path = log_file.strpath diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_refresh.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_refresh.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_refresh.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_refresh.py 2023-04-05 15:14:00.000000000 +0000 @@ -3,6 +3,7 @@ from uaclient import exceptions, messages from uaclient.cli import action_refresh, main +from uaclient.files.notices import Notice HELP_OUTPUT = """\ usage: pro refresh [contract|config|messages] [flags] @@ -26,10 +27,9 @@ """ -@mock.patch("os.getuid", return_value=0) class TestActionRefresh: @mock.patch("uaclient.cli.contract.get_available_resources") - def test_refresh_help(self, _m_resources, _getuid, capsys, FakeConfig): + def test_refresh_help(self, _m_resources, capsys, FakeConfig): with pytest.raises(SystemExit): with mock.patch("sys.argv", ["/usr/bin/ua", "refresh", "--help"]): with mock.patch( @@ -40,9 +40,11 @@ out, _err = capsys.readouterr() assert HELP_OUTPUT in out - def test_non_root_users_are_rejected(self, getuid, FakeConfig): + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) + def test_non_root_users_are_rejected( + self, we_are_currently_root, FakeConfig + ): """Check that a UID != 0 will receive a message and exit non-zero""" - getuid.return_value = 1 cfg = FakeConfig.for_attached_machine() with pytest.raises(exceptions.NonRootUserError): @@ -52,15 +54,17 @@ "target, expect_unattached_error", [(None, True), ("contract", True), ("config", False)], ) - @mock.patch("uaclient.config.UAConfig.write_cfg") def test_not_attached_errors( - self, _m_write_cfg, getuid, target, expect_unattached_error, FakeConfig + self, + target, + expect_unattached_error, + FakeConfig, ): """Check that an unattached machine emits message and exits 1""" cfg = FakeConfig() - cfg.update_messaging_timer = 0 - cfg.metering_timer = 0 + cfg.user_config.update_messaging_timer = 0 + cfg.user_config.metering_timer = 0 if expect_unattached_error: with pytest.raises(exceptions.UnattachedError): @@ -69,7 +73,7 @@ action_refresh(mock.MagicMock(target=target), cfg=cfg) @mock.patch("uaclient.system.subp") - def test_lock_file_exists(self, m_subp, _getuid, FakeConfig): + def test_lock_file_exists(self, m_subp, FakeConfig): """Check inability to refresh if operation holds lock file.""" cfg = FakeConfig().for_attached_machine() cfg.write_cache("lock", "123:pro disable") @@ -83,13 +87,12 @@ @mock.patch("logging.exception") @mock.patch("uaclient.contract.request_updated_contract") - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_refresh_contract_error_on_failure_to_update_contract( self, m_remove_notice, request_updated_contract, logging_error, - getuid, FakeConfig, ): """On failure in request_updates_contract emit an error.""" @@ -108,12 +111,11 @@ ] != m_remove_notice.call_args_list @mock.patch("uaclient.contract.request_updated_contract") - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_refresh_contract_happy_path( self, m_remove_notice, request_updated_contract, - getuid, capsys, FakeConfig, ): @@ -127,13 +129,13 @@ assert messages.REFRESH_CONTRACT_SUCCESS in capsys.readouterr()[0] assert [mock.call(cfg)] == request_updated_contract.call_args_list assert [ - mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING), - mock.call("", "Operation in progress.*"), + mock.call(Notice.CONTRACT_REFRESH_WARNING), + mock.call(Notice.OPERATION_IN_PROGRESS), ] == m_remove_notice.call_args_list - @mock.patch("uaclient.cli.update_apt_and_motd_messages") - def test_refresh_messages_error(self, m_update_motd, getuid, FakeConfig): - """On failure in update_apt_and_motd_messages emit an error.""" + @mock.patch("uaclient.cli.update_motd_messages") + def test_refresh_messages_error(self, m_update_motd, FakeConfig): + """On failure in update_motd_messages emit an error.""" m_update_motd.side_effect = Exception("test") with pytest.raises(exceptions.UserFacingError) as excinfo: @@ -141,17 +143,18 @@ assert messages.REFRESH_MESSAGES_FAILURE == excinfo.value.msg + @mock.patch("uaclient.apt_news.update_apt_news") @mock.patch("uaclient.jobs.update_messaging.exists", return_value=True) @mock.patch("logging.exception") @mock.patch("uaclient.system.subp") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") def test_refresh_messages_doesnt_fail_if_update_notifier_does( self, m_update_motd, m_subp, logging_error, _m_path, - getuid, + _m_update_apt_news, capsys, FakeConfig, ): @@ -167,27 +170,16 @@ assert [mock.call(subp_exc)] == logging_error.call_args_list assert messages.REFRESH_MESSAGES_SUCCESS in capsys.readouterr()[0] - @mock.patch("uaclient.jobs.update_messaging.exists", return_value=True) - @mock.patch("logging.exception") - @mock.patch("uaclient.system.subp") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") - def test_refresh_messages_systemctl_error( - self, m_update_motd, m_subp, logging_error, _m_path, getuid, FakeConfig - ): - subp_exc = Exception("test") - m_subp.side_effect = ["", subp_exc] - - with pytest.raises(exceptions.UserFacingError) as excinfo: - action_refresh(mock.MagicMock(target="messages"), cfg=FakeConfig()) - - assert 1 == logging_error.call_count - assert [mock.call(subp_exc)] == logging_error.call_args_list - assert messages.REFRESH_MESSAGES_FAILURE == excinfo.value.msg - + @mock.patch("uaclient.apt_news.update_apt_news") @mock.patch("uaclient.cli.refresh_motd") - @mock.patch("uaclient.cli.update_apt_and_motd_messages") + @mock.patch("uaclient.cli.update_motd_messages") def test_refresh_messages_happy_path( - self, m_update_motd, m_refresh_motd, getuid, capsys, FakeConfig + self, + m_update_motd, + m_refresh_motd, + m_update_apt_news, + capsys, + FakeConfig, ): """On success from request_updates_contract root user can refresh.""" cfg = FakeConfig() @@ -197,13 +189,19 @@ assert messages.REFRESH_MESSAGES_SUCCESS in capsys.readouterr()[0] assert [mock.call(cfg)] == m_update_motd.call_args_list assert [mock.call()] == m_refresh_motd.call_args_list + assert 1 == m_update_motd.call_count + assert 1 == m_refresh_motd.call_count + assert 1 == m_update_apt_news.call_count @mock.patch("logging.exception") @mock.patch( "uaclient.config.UAConfig.process_config", side_effect=RuntimeError() ) def test_refresh_config_error_on_failure_to_process_config( - self, _m_process_config, _m_logging_error, getuid, FakeConfig + self, + _m_process_config, + _m_logging_error, + FakeConfig, ): """On failure in process_config emit an error.""" @@ -216,7 +214,10 @@ @mock.patch("uaclient.config.UAConfig.process_config") def test_refresh_config_happy_path( - self, m_process_config, getuid, capsys, FakeConfig + self, + m_process_config, + capsys, + FakeConfig, ): """On success from process_config root user gets success message.""" @@ -227,15 +228,20 @@ assert messages.REFRESH_CONFIG_SUCCESS in capsys.readouterr()[0] assert [mock.call()] == m_process_config.call_args_list + @mock.patch("uaclient.apt_news.update_apt_news") + @mock.patch("uaclient.cli.refresh_motd") + @mock.patch("uaclient.cli.update_motd_messages") @mock.patch("uaclient.contract.request_updated_contract") @mock.patch("uaclient.config.UAConfig.process_config") - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_refresh_all_happy_path( self, m_remove_notice, m_process_config, m_request_updated_contract, - getuid, + m_update_motd, + m_refresh_motd, + m_update_apt_news, capsys, FakeConfig, ): @@ -248,9 +254,13 @@ assert 0 == ret assert messages.REFRESH_CONFIG_SUCCESS in out assert messages.REFRESH_CONTRACT_SUCCESS in out + assert messages.REFRESH_MESSAGES_SUCCESS in out assert [mock.call()] == m_process_config.call_args_list assert [mock.call(cfg)] == m_request_updated_contract.call_args_list assert [ - mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING), - mock.call("", "Operation in progress.*"), + mock.call(Notice.CONTRACT_REFRESH_WARNING), + mock.call(Notice.OPERATION_IN_PROGRESS), ] == m_remove_notice.call_args_list + assert 1 == m_update_motd.call_count + assert 1 == m_refresh_motd.call_count + assert 1 == m_update_apt_news.call_count diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_security_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_security_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_security_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_security_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -75,7 +75,7 @@ assert re.match(HELP_OUTPUT, out) @pytest.mark.parametrize("output_format", ("json", "yaml", "text")) - @mock.patch(M_PATH + "yaml.safe_dump") + @mock.patch(M_PATH + "safe_dump") @mock.patch(M_PATH + "json.dumps") def test_action_security_status( self, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_cli_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_cli_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -9,11 +9,13 @@ import mock import pytest -import yaml -from uaclient import exceptions, messages, status +from uaclient import exceptions, messages, status, util from uaclient.cli import action_status, get_parser, main, status_parser +from uaclient.conftest import FakeNotice from uaclient.event_logger import EventLoggerMode +from uaclient.files.notices import Notice, NoticesManager +from uaclient.yaml import safe_load M_PATH = "uaclient.cli." @@ -31,15 +33,15 @@ RESPONSE_CONTRACT_INFO = { "accountInfo": { - "createdAt": "2019-06-14T06:45:50Z", + "createdAt": util.parse_rfc3339_date("2019-06-14T06:45:50Z"), "id": "some_id", "name": "Name", "type": "paid", }, "contractInfo": { - "createdAt": "2021-05-21T20:00:53Z", + "createdAt": util.parse_rfc3339_date("2021-05-21T20:00:53Z"), "createdBy": "someone", - "effectiveTo": "9999-12-31T00:00:00Z", + "effectiveTo": util.parse_rfc3339_date("9999-12-31T00:00:00Z"), "id": "some_id", "name": "Name", "products": ["uai-essential-virtual"], @@ -157,6 +159,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, { "description": "Expanded Security Maintenance for Infrastructure", @@ -167,6 +170,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, { "description": "NIST-certified core packages", @@ -177,6 +181,7 @@ "status_details": "", "available": "no", "blocked_by": [], + "warning": None, }, { "description": ( @@ -189,6 +194,7 @@ "status_details": "", "available": "no", "blocked_by": [], + "warning": None, }, { "description": "Canonical Livepatch service", @@ -199,6 +205,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, { "description": "Ubuntu kernel with PREEMPT_RT patches integrated", @@ -209,6 +216,7 @@ "status_details": "", "available": "no", "blocked_by": [], + "warning": None, }, { "description": "Security Updates for the Robot Operating System", @@ -219,6 +227,7 @@ "status_details": "", "available": "no", "blocked_by": [], + "warning": None, }, { "description": "All Updates for the Robot Operating System", @@ -229,6 +238,7 @@ "status_details": "", "available": "no", "blocked_by": [], + "warning": None, }, ] @@ -242,6 +252,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, { "description": "Expanded Security Maintenance for Infrastructure", @@ -252,6 +263,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, { "description": "Canonical Livepatch service", @@ -262,6 +274,7 @@ "status_details": "", "available": "yes", "blocked_by": [], + "warning": None, }, ] @@ -315,8 +328,8 @@ ) +@mock.patch("uaclient.livepatch.on_supported_kernel", return_value=None) @mock.patch("uaclient.cli.contract.is_contract_changed", return_value=False) -@mock.patch("uaclient.files.NoticeFile.remove") @mock.patch("uaclient.system.should_reboot", return_value=False) @mock.patch( "uaclient.status.get_available_resources", @@ -326,16 +339,14 @@ "uaclient.status.get_contract_information", return_value=RESPONSE_CONTRACT_INFO, ) -@mock.patch(M_PATH + "os.getuid", return_value=0) class TestActionStatus: def test_status_help( self, - _m_getuid, _m_get_contract_information, _m_get_available_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, capsys, FakeConfig, ): @@ -355,8 +366,11 @@ ( ([], ""), ( - [["a", "adesc"], ["b2", "bdesc"]], - "\nNOTICES\n a: adesc\nb2: bdesc\n", + [ + [FakeNotice.a, "adesc"], + [FakeNotice.b, "bdesc"], + ], + "\nNOTICES\nadesc\nbdesc\n", ), ), ) @@ -373,24 +387,25 @@ @mock.patch("uaclient.status.format_expires", return_value="formatteddate") def test_attached( self, - _m_getuid, + _m_format_expires, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, - _m_format_expires, - notices, - notice_status, + _m_on_supported_kernel, features, feature_status, + notices, + notice_status, use_all, capsys, FakeConfig, ): """Check that root and non-root will emit attached status""" cfg = FakeConfig.for_attached_machine() - cfg.write_cache("notices", notices) + mock_notice = NoticesManager() + for notice in notices: + mock_notice.add(notice[0], notice[1]) with mock.patch( "uaclient.config.UAConfig.features", new_callable=mock.PropertyMock, @@ -431,12 +446,11 @@ ) def test_unattached( self, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, use_all, capsys, FakeConfig, @@ -459,12 +473,11 @@ ) def test_simulated( self, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, use_all, capsys, FakeConfig, @@ -487,24 +500,24 @@ m_sleep, _m_subp, _m_get_version, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, capsys, FakeConfig, ): """Check that --wait will will block and poll until lock released.""" cfg = FakeConfig() + mock_notice = NoticesManager() lock_file = cfg.data_path("lock") cfg.write_cache("lock", "123:pro auto-attach") def fake_sleep(seconds): if m_sleep.call_count == 3: os.unlink(lock_file) - os.unlink(cfg.notice_file.file.path) + mock_notice.remove(Notice.OPERATION_IN_PROGRESS) m_sleep.side_effect = fake_sleep @@ -539,12 +552,11 @@ ) def test_unattached_formats( self, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, environ, format_type, event_logger_mode, @@ -578,6 +590,7 @@ { "name": service["name"], "description": service["description"], + "description_override": service["description_override"], "available": service["available"], } for service in services @@ -614,7 +627,10 @@ "external_account_ids": [], }, "config_path": None, - "config": {"data_dir": mock.ANY}, + "config": { + "data_dir": mock.ANY, + "ua_config": mock.ANY, + }, "simulated": False, "errors": [], "warnings": [], @@ -624,7 +640,7 @@ if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: - assert expected == yaml.safe_load(capsys.readouterr()[0]) + assert expected == safe_load(capsys.readouterr()[0]) @pytest.mark.parametrize( "format_type,event_logger_mode", @@ -645,12 +661,11 @@ @pytest.mark.parametrize("use_all", (True, False)) def test_attached_formats( self, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, use_all, environ, format_type, @@ -736,7 +751,10 @@ "external_account_ids": [{"IDs": ["id1"], "origin": "AWS"}], }, "config_path": None, - "config": {"data_dir": mock.ANY}, + "config": { + "data_dir": mock.ANY, + "ua_config": mock.ANY, + }, "simulated": False, "errors": [], "warnings": [], @@ -746,7 +764,7 @@ if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: - yaml_output = yaml.safe_load(capsys.readouterr()[0]) + yaml_output = safe_load(capsys.readouterr()[0]) # On earlier versions of pyyaml, we don't add the timezone # info when converting a date string into a datetime object. @@ -775,12 +793,11 @@ @pytest.mark.parametrize("use_all", (True, False)) def test_simulated_formats( self, - _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, use_all, format_type, event_logger_mode, @@ -880,13 +897,13 @@ "features": {}, "notices": [], "account": { - "created_at": "2019-06-14T06:45:50Z", + "created_at": util.parse_rfc3339_date("2019-06-14T06:45:50Z"), "external_account_ids": [], "id": "some_id", "name": "Name", }, "contract": { - "created_at": "2021-05-21T20:00:53Z", + "created_at": util.parse_rfc3339_date("2021-05-21T20:00:53Z"), "id": "some_id", "name": "Name", "products": ["uai-essential-virtual"], @@ -895,31 +912,42 @@ "environment_vars": [], "execution_status": "inactive", "execution_details": "No Ubuntu Pro operations are running", - "expires": "9999-12-31T00:00:00Z", + "expires": util.parse_rfc3339_date("9999-12-31T00:00:00Z"), "effective": None, "services": expected_services, "simulated": True, "version": mock.ANY, "config_path": None, - "config": {"data_dir": mock.ANY}, + "config": { + "data_dir": mock.ANY, + "ua_config": mock.ANY, + }, "errors": [], "warnings": [], "result": "success", } if format_type == "json": - assert expected == json.loads(capsys.readouterr()[0]) + assert expected == json.loads( + capsys.readouterr()[0], cls=util.DatetimeAwareJSONDecoder + ) else: - assert expected == yaml.safe_load(capsys.readouterr()[0]) + expected["account"]["created_at"] = safe_load( + "2019-06-14 06:45:50+00:00" + ) + expected["contract"]["created_at"] = safe_load( + "2021-05-21 20:00:53+00:00" + ) + expected["expires"] = safe_load("9999-12-31 00:00:00+00:00") + assert expected == safe_load(capsys.readouterr()[0]) def test_error_on_connectivity_errors( self, - _m_getuid, _m_get_contract_information, m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, FakeConfig, ): """Raise UrlError on connectivity issues""" @@ -942,13 +970,12 @@ @mock.patch("uaclient.status.format_expires", return_value="formatteddate") def test_unicode_dash_replacement_when_unprintable( self, - _m_getuid, + _m_format_expires, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, - _m_format_expires, + _m_on_supported_kernel, encoding, expected_dash, use_all, @@ -958,11 +985,12 @@ # encoding accurately in older versions of pytest underlying_stdout = io.BytesIO() fake_stdout = io.TextIOWrapper(underlying_stdout, encoding=encoding) + cfg = FakeConfig.for_attached_machine() with mock.patch("sys.stdout", fake_stdout): action_status( mock.MagicMock(all=use_all, simulate_with_token=None), - cfg=FakeConfig.for_attached_machine(), + cfg=cfg, ) fake_stdout.flush() # Make sure all output is in underlying_stdout @@ -1003,12 +1031,11 @@ ) def test_errors_are_raised_appropriately( self, - _m_getuid, m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, exception_to_throw, exception_type, exception_message, @@ -1038,13 +1065,13 @@ "expired_token", 'Contract "some_id" expired on December 31, 2019', "effectiveTo", - "2019-12-31T00:00:00Z", + util.parse_rfc3339_date("2019-12-31T00:00:00Z"), ), ( "token_not_valid_yet", 'Contract "some_id" is not effective until December 31, 9999', "effectiveFrom", - "9999-12-31T00:00:00Z", + util.parse_rfc3339_date("9999-12-31T00:00:00Z"), ), ), ) @@ -1054,12 +1081,11 @@ ) def test_errors_for_token_dates( self, - _m_getuid, m_get_contract_information, _m_get_avail_resources, _m_should_reboot, - _m_remove_notice, _m_contract_changed, + _m_on_supported_kernel, format_type, event_logger_mode, token_to_use, @@ -1080,7 +1106,6 @@ m_get_contract_information.side_effect = contract_info_side_effect cfg = FakeConfig() - args = mock.MagicMock( format=format_type, all=False, simulate_with_token=token_to_use ) @@ -1093,61 +1118,10 @@ if format_type == "json": output = json.loads(capsys.readouterr()[0]) else: - output = yaml.safe_load(capsys.readouterr()[0]) + output = safe_load(capsys.readouterr()[0]) assert output["errors"][0]["message"] == warning_message - @pytest.mark.parametrize( - "contract_changed,is_attached", - ( - (False, True), - (True, False), - (True, True), - (False, False), - ), - ) - @mock.patch("uaclient.files.NoticeFile.try_remove") - @mock.patch("uaclient.files.NoticeFile.add") - def test_is_contract_changed( - self, - m_add_notice, - m_try_remove_notice, - _m_getuid, - _m_get_contract_information, - _m_get_available_resources, - _m_should_reboot, - _m_remove_notice, - _m_contract_changed, - contract_changed, - is_attached, - capsys, - FakeConfig, - ): - _m_contract_changed.return_value = contract_changed - if is_attached: - cfg = FakeConfig().for_attached_machine() - else: - cfg = FakeConfig() - - action_status( - mock.MagicMock(all=False, simulate_with_token=None), cfg=cfg - ) - - if is_attached: - if contract_changed: - assert [ - mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) - ] == m_add_notice.call_args_list - else: - assert [ - mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) - ] not in m_add_notice.call_args_list - assert [ - mock.call("", messages.NOTICE_REFRESH_CONTRACT_WARNING) - ] in m_try_remove_notice.call_args_list - else: - assert _m_contract_changed.call_count == 0 - class TestStatusParser: @mock.patch(M_PATH + "contract.get_available_resources") diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_config.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_config.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_config.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_config.py 2023-04-05 15:14:00.000000000 +0000 @@ -8,7 +8,6 @@ import mock import pytest -import yaml from uaclient import apt, exceptions, messages, util from uaclient.config import ( @@ -19,10 +18,14 @@ get_config_path, parse_config, ) +from uaclient.conftest import FakeNotice from uaclient.defaults import DEFAULT_CONFIG_FILE from uaclient.entitlements import valid_services from uaclient.entitlements.entitlement_status import ApplicationStatus +from uaclient.files import notices +from uaclient.files.notices import NoticesManager from uaclient.util import depth_first_merge_overlay_dict +from uaclient.yaml import safe_dump KNOWN_DATA_PATHS = ( ("machine-access-cis", "machine-access-cis.json"), @@ -71,63 +74,113 @@ @pytest.mark.parametrize( "notices,expected", ( - ([], []), - ([["a", "a1"]], [["a", "a1"]]), - ([["a", "a1"], ["a", "a1"]], [["a", "a1"]]), + ([], ()), + ( + [[FakeNotice.a2, "a1"]], + ["a1"], + ), + ( + [ + [FakeNotice.a, "a1"], + [FakeNotice.a2, "a2"], + ], + [ + "a1", + "a2", + ], + ), + ( + [ + [FakeNotice.a, "a1"], + [FakeNotice.a, "a1"], + ], + [ + "a1", + ], + ), ), ) def test_add_notice_avoids_duplicates( - self, notices, expected, tmpdir, FakeConfig + self, + notices, + expected, ): - cfg = FakeConfig() - assert None is cfg.notice_file.read() - for notice in notices: - cfg.notice_file.add(*notice) + notice = NoticesManager() + assert [] == notice.list() + for notice_ in notices: + notice.add(*notice_) if notices: - assert expected == cfg.notice_file.read() + assert expected == notice.list() else: - assert None is cfg.notice_file.read() + assert [] == notice.list() @pytest.mark.parametrize( - "notices,expected", + "_notices", ( - ([], []), - ([["a", "a1"]], [["a", "a1"]]), - ([["a", "a1"], ["a", "a1"]], [["a", "a1"]]), + ([]), + ([[FakeNotice.a]]), + ( + [ + [FakeNotice.a], + [FakeNotice.a2], + ] + ), ), ) + @mock.patch("uaclient.util.we_are_currently_root", return_value=False) def test_add_notice_fails_as_nonroot( - self, notices, expected, tmpdir, FakeConfig + self, + m_we_are_currently_root, + _notices, ): - cfg = FakeConfig(root_mode=False) - assert None is cfg.notice_file.read() - for notice in notices: - with pytest.raises(exceptions.NonRootUserError): - cfg.notice_file.add(*notice) - assert None is cfg.notice_file.read() + assert [] == notices.list() + for notice_ in _notices: + notices.add(*notice_) + assert [] == notices.list() @pytest.mark.parametrize( - "notices,removes,expected", + "notices_,removes,expected", ( - ([], [["a", "a1"]], None), - ([["a", "a1"]], [["a", "a1"]], None), - ([["a", "a1"], ["a", "a2"]], [["a", "a1"]], [["a", "a2"]]), - ( - [["a", "a1"], ["a", "a2"], ["b", "b2"]], - [["a", ".*"]], - [["b", "b2"]], + ([], [FakeNotice.a], []), + ( + [[FakeNotice.a2]], + [FakeNotice.a2], + [], + ), + ( + [ + [FakeNotice.a], + [FakeNotice.a2], + ], + [FakeNotice.a], + ["notice_a2"], + ), + ( + [ + [FakeNotice.a], + [FakeNotice.a2], + [FakeNotice.b], + ], + [ + FakeNotice.a, + FakeNotice.a2, + ], + ["notice_b"], ), ), ) def test_remove_notice_removes_matching( - self, notices, removes, expected, tmpdir, FakeConfig + self, + notices_, + removes, + expected, ): - cfg = FakeConfig() - for notice in notices: - cfg.notice_file.add(*notice) - for label, descr in removes: - cfg.notice_file.remove(label, descr) - assert expected == cfg.notice_file.read() + + for notice_ in notices_: + notices.add(*notice_) + for label in removes: + notices.remove(label) + assert expected == notices.list() class TestEntitlements: @@ -158,9 +211,8 @@ } assert expected == cfg.machine_token_file.entitlements - @mock.patch("os.getuid", return_value=0) def test_entitlements_uses_resource_token_from_machine_token( - self, tmpdir, FakeConfig, all_resources_available + self, FakeConfig, all_resources_available ): """Include entitlement-specific resourceTokens from machine_token""" cfg = FakeConfig() @@ -291,91 +343,36 @@ timer_log_file: /var/log/ubuntu-advantage-timer.log """ -UA_CFG_DICT = { - "ua_config": { - "apt_http_proxy": None, - "apt_https_proxy": None, - "apt_news": True, - "apt_news_url": "https://motd.ubuntu.com/aptnews.json", - "global_apt_http_proxy": None, - "global_apt_https_proxy": None, - "ua_apt_http_proxy": None, - "ua_apt_https_proxy": None, - "http_proxy": None, - "https_proxy": None, - "update_messaging_timer": None, - "metering_timer": None, - } +USER_CFG_DICT = { + "apt_http_proxy": None, + "apt_https_proxy": None, + "apt_news": True, + "apt_news_url": "https://motd.ubuntu.com/aptnews.json", + "global_apt_http_proxy": None, + "global_apt_https_proxy": None, + "ua_apt_http_proxy": None, + "ua_apt_https_proxy": None, + "http_proxy": None, + "https_proxy": None, + "update_messaging_timer": 21600, + "metering_timer": 14400, } -class TestUAConfigKeys: +class TestUserConfigKeys: @pytest.mark.parametrize("attr_name", UA_CONFIGURABLE_KEYS) - @mock.patch("uaclient.config.UAConfig.write_cfg") - def test_ua_configurable_keys_set_ua_config_dict( - self, write_cfg, attr_name, tmpdir, FakeConfig + @mock.patch("uaclient.config.state_files.user_config_file.write") + def test_user_configurable_keys_set_user_config( + self, write, attr_name, tmpdir, FakeConfig ): """Getters and settings are available fo UA_CONFIGURABLE_KEYS.""" cfg = FakeConfig() - assert UA_CFG_DICT["ua_config"][attr_name] == getattr( - cfg, attr_name, None - ) + assert USER_CFG_DICT[attr_name] == getattr(cfg, attr_name, None) cfg_non_members = ("apt_http_proxy", "apt_https_proxy") if attr_name not in cfg_non_members: setattr(cfg, attr_name, attr_name + "value") assert attr_name + "value" == getattr(cfg, attr_name) - assert attr_name + "value" == cfg.cfg["ua_config"][attr_name] - - -class TestWriteCfg: - @pytest.mark.parametrize( - "orig_content, expected", - ( - ( - CFG_BASE_CONTENT, - CFG_BASE_CONTENT - + yaml.dump(UA_CFG_DICT, default_flow_style=False), - ), - ( # Yaml output is sorted alphabetically by key - "\n".join(sorted(CFG_BASE_CONTENT.splitlines(), reverse=True)), - CFG_BASE_CONTENT - + yaml.dump(UA_CFG_DICT, default_flow_style=False), - ), - # Any custom comments or unrecognized config keys are dropped - ( - "unknown-keys-not-preserved: true\n# user comments are lost" - + CFG_BASE_CONTENT, - CFG_BASE_CONTENT - + yaml.dump(UA_CFG_DICT, default_flow_style=False), - ), - # All features/settings_overrides ordered after ua_config - ( - CFG_BASE_CONTENT - + "features:\n new: 2\n extra_security_params:\n hide: true\n" - " show_beta: true\nsettings_overrides:\n d: 2\n c: 1\n", - CFG_FEATURES_CONTENT - + yaml.dump(UA_CFG_DICT, default_flow_style=False), - ), - ( - "settings_overrides:\n c: 1\n d: 2\nfeatures:\n" - " show_beta: true\n new: 2\n extra_security_params:\n" - " hide: true\nsettings_overrides:\n d: 2\n c: 1\n" - + CFG_BASE_CONTENT, - CFG_FEATURES_CONTENT - + yaml.dump(UA_CFG_DICT, default_flow_style=False), - ), - ), - ) - def test_write_cfg_reads_cfg_andpersists_structured_content_to_config_path( - self, orig_content, expected, tmpdir, FakeConfig - ): - """write_cfg writes structured, ordered config YAML to config_path.""" - orig_conf = tmpdir.join("orig_uaclient.conf") - orig_conf.write(orig_content) - cfg = FakeConfig(cfg_overrides=parse_config(orig_conf.strpath)[0]) - out_conf = tmpdir.join("uaclient.conf") - cfg.write_cfg(out_conf.strpath) - assert expected == out_conf.read() + assert attr_name + "value" == getattr(cfg.user_config, attr_name) class TestWriteCache: @@ -864,8 +861,8 @@ ], ) @mock.patch("uaclient.util.validate_proxy") - @mock.patch("uaclient.entitlements.livepatch.get_config_option_value") - @mock.patch("uaclient.entitlements.livepatch.configure_livepatch_proxy") + @mock.patch("uaclient.livepatch.get_config_option_value") + @mock.patch("uaclient.livepatch.configure_livepatch_proxy") @mock.patch( "uaclient.entitlements.livepatch.LivepatchEntitlement.application_status" # noqa: E501 ) @@ -873,10 +870,10 @@ @mock.patch("uaclient.snap.configure_snap_proxy") @mock.patch("uaclient.snap.is_installed") @mock.patch("uaclient.apt.setup_apt_proxy") - @mock.patch("uaclient.config.UAConfig.write_cfg") + @mock.patch("uaclient.config.state_files.user_config_file.write") def test_process_config( self, - m_write_cfg, + m_write, m_apt_configure_proxy, m_snap_is_installed, m_snap_configure_proxy, @@ -915,23 +912,17 @@ livepatch_http_val, livepatch_https_val, ] - cfg = FakeConfig( - { - "ua_config": { - "apt_http_proxy": apt_http, - "apt_https_proxy": apt_https, - "global_apt_https_proxy": global_https, - "global_apt_http_proxy": global_http, - "ua_apt_https_proxy": ua_https, - "ua_apt_http_proxy": ua_http, - "http_proxy": http_proxy, - "https_proxy": https_proxy, - "update_messaging_timer": 21600, - "metering_timer": 0, - }, - "data_dir": tmpdir.strpath, - } - ) + cfg = FakeConfig({"data_dir": tmpdir.strpath}) + cfg.user_config.apt_http_proxy = apt_http + cfg.user_config.apt_https_proxy = apt_https + cfg.user_config.global_apt_https_proxy = global_https + cfg.user_config.global_apt_http_proxy = global_http + cfg.user_config.ua_apt_https_proxy = ua_https + cfg.user_config.ua_apt_http_proxy = ua_http + cfg.user_config.http_proxy = http_proxy + cfg.user_config.https_proxy = https_proxy + cfg.user_config.update_messaging_timer = 21600 + cfg.user_config.metering_timer = 0 if global_https is None and apt_https is not None: global_https = apt_https @@ -1020,13 +1011,8 @@ assert "" == err def test_process_config_errors_for_wrong_timers(self, FakeConfig): - cfg = FakeConfig( - { - "ua_config": { - "update_messaging_timer": "wrong", - } - } - ) + cfg = FakeConfig() + cfg.user_config.update_messaging_timer = "wrong" with pytest.raises( exceptions.UserFacingError, @@ -1057,7 +1043,7 @@ "log_file": "/var/log/ubuntu-advantage.log", "timer_log_file": "/var/log/ubuntu-advantage-timer.log", "daemon_log_file": "/var/log/ubuntu-advantage-daemon.log", # noqa: E501 - "log_level": "INFO", + "log_level": "debug", } assert expected_default_config == config @@ -1075,7 +1061,7 @@ self, config_dict, expected_invalid_keys, tmpdir ): config_file = tmpdir.join("uaclient.conf") - config_file.write(yaml.dump(config_dict)) + config_file.write(safe_dump(config_dict)) env_vars = {"UA_CONFIG_FILE": config_file.strpath} with mock.patch.dict("uaclient.config.os.environ", values=env_vars): cfg, invalid_keys = parse_config(config_file.strpath) @@ -1316,10 +1302,7 @@ assert expected == cfg.machine_token @mock.patch("uaclient.files.MachineTokenFile.read") - @mock.patch("os.getuid", return_value=0) - def test_machine_token_without_overlay( - self, _m_getuid, m_token_read, FakeConfig - ): + def test_machine_token_without_overlay(self, m_token_read, FakeConfig): user_cfg = {} m_token_read.return_value = self.machine_token_dict cfg = FakeConfig(cfg_overrides=user_cfg) @@ -1418,3 +1401,29 @@ with mock.patch.dict("uaclient.config.os.environ", values={}): assert DEFAULT_CONFIG_FILE == get_config_path() assert _m_exists.call_count == 1 + + +class TestCheckLockInfo: + @pytest.mark.parametrize("lock_content", ((""), ("corrupted"))) + @mock.patch("os.path.exists", return_value=True) + @mock.patch("uaclient.system.load_file") + def test_raise_exception_for_corrupted_lock( + self, + m_load_file, + _m_path_exists, + lock_content, + FakeConfig, + ): + cfg = FakeConfig() + m_load_file.return_value = lock_content + + expected_msg = messages.INVALID_LOCK_FILE.format( + lock_file_path=cfg.data_dir + "/lock" + ) + + with pytest.raises(exceptions.InvalidLockFile) as exc_info: + cfg.check_lock_info() + + assert expected_msg.msg == exc_info.value.msg + assert m_load_file.call_count == 1 + assert _m_path_exists.call_count == 1 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_contract.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_contract.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_contract.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_contract.py 2023-04-05 15:14:00.000000000 +0000 @@ -158,6 +158,7 @@ "arch": "arch", "kernel": "kernel", "series": "series", + "virt": "mtype", } get_machine_id.return_value = "machineId" @@ -225,10 +226,8 @@ @pytest.mark.parametrize( "enabled_services", (([]), (["esm-apps", "livepatch"])) ) - @mock.patch("os.getuid", return_value=0) def test_report_machine_activity( self, - _m_getuid, get_machine_id, request_url, activity_id, @@ -999,10 +998,10 @@ self, get_updated_contract_info, has_contract_expired, FakeConfig ): if has_contract_expired: - expiry_date = "2041-05-08T19:02:26Z" + expiry_date = util.parse_rfc3339_date("2041-05-08T19:02:26Z") ret_val = True else: - expiry_date = "2040-05-08T19:02:26Z" + expiry_date = util.parse_rfc3339_date("2040-05-08T19:02:26Z") ret_val = False get_updated_contract_info.return_value = { "machineTokenInfo": { @@ -1029,7 +1028,9 @@ "machineId": "test_machine_id", "resourceTokens": resourceTokens, "contractInfo": { - "effectiveTo": "2040-05-08T19:02:26Z", + "effectiveTo": util.parse_rfc3339_date( + "2040-05-08T19:02:26Z" + ), "resourceEntitlements": resourceEntitlements, }, }, diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_data_types.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_data_types.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_data_types.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_data_types.py 2023-04-05 15:14:00.000000000 +0000 @@ -356,6 +356,13 @@ self.dtlist_opt = dtlist_opt +class ExampleObjectWithDictKey(DataObject): + fields = [Field("string", StringDataValue, dict_key="S-t/R*i_n#g")] + + def __init__(self, *, string: str): + self.string = string + + example_data_object_dict_no_optionals = { "string": "string", "integer": 1, @@ -683,6 +690,40 @@ ExampleDataObject.from_value(val) assert e.value.msg == error.msg + def test_dict_key(self): + d = {"S-t/R*i_n#g": "something"} + do = ExampleObjectWithDictKey.from_dict(d) + assert do.string == "something" + assert d == do.to_dict() + + def test_optional_type_errors_become_null(self): + result = ExampleDataObject.from_dict( + { + **example_data_object_dict_with_optionals, + "string_opt": 4, + "integer_opt": {}, + "obj_opt": [], + "stringlist_opt": [4], + "integerlist_opt": ["hello"], + "objlist_opt": [[]], + "enum_opt": "nope", + "enum_opt_list": "nope", + "dt_opt": 1234, + "dtlist_opt": 1234, + }, + optional_type_errors_become_null=True, + ) + assert result.string_opt is None + assert result.integer_opt is None + assert result.obj_opt is None + assert result.stringlist_opt is None + assert result.integerlist_opt is None + assert result.objlist_opt is None + assert result.enum_opt is None + assert result.enum_opt_list is None + assert result.dt_opt is None + assert result.dtlist_opt is None + @pytest.mark.parametrize( "d", ( diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_event_logger.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_event_logger.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_event_logger.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_event_logger.py 2023-04-05 15:14:00.000000000 +0000 @@ -4,9 +4,9 @@ import mock import pytest -import yaml from uaclient.event_logger import JSON_SCHEMA_VERSION, EventLoggerMode +from uaclient.yaml import safe_load class TestEventLogger: @@ -87,7 +87,7 @@ fake_stdout.getvalue().strip() ) else: - assert expected_machine_out == yaml.safe_load( + assert expected_machine_out == safe_load( fake_stdout.getvalue().strip() ) @@ -162,6 +162,6 @@ fake_stdout.getvalue().strip() ) else: - assert expected_machine_out == yaml.safe_load( + assert expected_machine_out == safe_load( fake_stdout.getvalue().strip() ) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_files.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_files.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_files.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_files.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,175 +0,0 @@ -import os - -import mock -import pytest - -from uaclient import exceptions, system -from uaclient.data_types import ( - DataObject, - Field, - IncorrectFieldTypeError, - IntDataValue, - StringDataValue, -) -from uaclient.files import ( - DataObjectFile, - DataObjectFileFormat, - MachineTokenFile, - UAFile, -) - - -class MockUAFile: - def __init__(self): - self.write = mock.MagicMock() - self.read = mock.MagicMock() - self.delete = mock.MagicMock() - self.path = mock.MagicMock() - - -class TestUAFile: - def test_read_write(self, tmpdir): - file_name = "temp_file" - file = UAFile(file_name, tmpdir.strpath, False) - content = "dummy file words" - file.write(content) - path = os.path.join(tmpdir.strpath, file_name) - res = system.load_file(path) - assert res == file.read() - assert res == content - - -class NestedTestData(DataObject): - fields = [ - Field("integer", IntDataValue), - ] - - def __init__(self, integer: int): - self.integer = integer - - -class TestData(DataObject): - fields = [ - Field("string", StringDataValue), - Field("nested", NestedTestData), - ] - - def __init__(self, string: str, nested: NestedTestData): - self.string = string - self.nested = nested - - -class TestDataObjectFile: - def test_write_valid_json(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - ) - dof.write(TestData(string="test", nested=NestedTestData(integer=1))) - assert mock_file.write.call_args_list == [ - mock.call("""{"nested": {"integer": 1}, "string": "test"}""") - ] - - def test_write_valid_yaml(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - DataObjectFileFormat.YAML, - ) - dof.write(TestData(string="test", nested=NestedTestData(integer=1))) - assert mock_file.write.call_args_list == [ - mock.call("""nested:\n integer: 1\nstring: test\n""") - ] - - def test_read_valid_json(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - ) - mock_file.read.return_value = ( - """{"string": "test", "nested": {"integer": 1}}""" - ) - do = dof.read() - assert do.string == "test" - assert do.nested.integer == 1 - - def test_read_valid_yaml(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - DataObjectFileFormat.YAML, - ) - mock_file.read.return_value = ( - """nested:\n integer: 1\nstring: test\n""" - ) - do = dof.read() - assert do.string == "test" - assert do.nested.integer == 1 - - def test_read_invalid_data(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - ) - mock_file.read.return_value = """{"nested": {"integer": 1}}""" - with pytest.raises(IncorrectFieldTypeError): - dof.read() - - def test_read_invalid_json(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - ) - mock_file.read.return_value = """{"nested": {""" - with pytest.raises(exceptions.InvalidFileFormatError): - dof.read() - - def test_read_invalid_yaml(self): - mock_file = MockUAFile() - dof = DataObjectFile( - TestData, - mock_file, - DataObjectFileFormat.YAML, - ) - mock_file.read.return_value = """nested": {""" - with pytest.raises(exceptions.InvalidFileFormatError): - dof.read() - - -class TestMachineTokenFile: - def test_deleting(self, tmpdir): - token_file = MachineTokenFile( - directory=tmpdir.strpath, - ) - token = {"machineTokenInfo": {"machineId": "random-id"}} - token_file.write(token) - assert token_file.machine_token == token - token_file.delete() - assert token_file.machine_token is None - - def test_public_file_filtering(self, tmpdir): - # root access of machine token file - token_file = MachineTokenFile( - directory=tmpdir.strpath, - ) - token = { - "machineTokenInfo": {"machineId": "random-id"}, - "machineToken": "token", - } - token_file.write(token) - root_token = token_file.machine_token - assert token == root_token - # non root access of machine token file - token_file = MachineTokenFile( - directory=tmpdir.strpath, root_mode=False - ) - nonroot_token = token_file.machine_token - assert root_token != nonroot_token - machine_token = nonroot_token.get("machineToken", None) - assert machine_token is None diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_lib_migrate_user_config.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_lib_migrate_user_config.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_lib_migrate_user_config.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_lib_migrate_user_config.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,242 @@ +import mock +import pytest + +from lib.migrate_user_config import ( + create_new_uaclient_conffile, + create_new_user_config_file, + load_pre_upgrade_conf, +) +from uaclient import messages +from uaclient.testing.fakes import FakeFile +from uaclient.yaml import safe_load + + +class TestLoadPreUpgradeConf: + @pytest.mark.parametrize( + ["contents"], + [ + ( + """\ +contracts_url: urlhere +log_level: debug +features: {allow_beta: true} +""", + ) + ], + ) + @mock.patch("builtins.open") + def test_success(self, m_open, contents, capsys): + m_open.return_value = FakeFile(contents) + assert safe_load(contents) == load_pre_upgrade_conf() + assert ("", "") == capsys.readouterr() + + @pytest.mark.parametrize( + ["contents"], + [ + ( + """\ +contracts_url: {invalid +""", + ) + ], + ) + @mock.patch("builtins.open") + def test_invalid_yaml(self, m_open, contents, capsys): + m_open.return_value = FakeFile(contents) + assert load_pre_upgrade_conf() is None + assert ( + "", + messages.USER_CONFIG_MIGRATION_WARNING_UACLIENT_CONF_LOAD + "\n", + ) == capsys.readouterr() + + @mock.patch("builtins.open") + def test_file_access_error(self, m_open, capsys): + m_open.side_effect = Exception() + assert load_pre_upgrade_conf() is None + assert ( + "", + messages.USER_CONFIG_MIGRATION_WARNING_UACLIENT_CONF_LOAD + "\n", + ) == capsys.readouterr() + + +class TestCreateNewUserConfigFile: + @pytest.mark.parametrize( + ["old_conf", "expected_msg"], + [ + ({}, ""), + ({"log_level": "warning"}, ""), + ({"ua_config": "invalid"}, ""), + ({"ua_config": {"apt_news": True}}, ""), + ( + {"ua_config": {"apt_news": False}}, + messages.USER_CONFIG_MIGRATION_WARNING_NEW_USER_CONFIG_WRITE + + "\n pro config set apt_news=False\n", + ), + ( + {"ua_config": {"apt_news": False, "http_proxy": "custom"}}, + messages.USER_CONFIG_MIGRATION_WARNING_NEW_USER_CONFIG_WRITE + + "\n" + + " pro config set apt_news=False\n" + + " pro config set http_proxy=custom\n", + ), + ], + ) + @mock.patch("builtins.open") + def test_file_access_error(self, m_open, old_conf, expected_msg, capsys): + m_open.side_effect = Exception() + create_new_user_config_file(old_conf) + assert ("", expected_msg) == capsys.readouterr() + + @pytest.mark.parametrize( + ["old_conf", "new_user_config"], + [ + ({}, {}), + ({"log_level": "warning"}, {}), + ({"ua_config": "invalid"}, {}), + ({"ua_config": {"apt_news": True}}, {}), + ({"ua_config": {"apt_news": False}}, {"apt_news": False}), + ( + {"ua_config": {"apt_news": False, "http_proxy": "custom"}}, + {"http_proxy": "custom", "apt_news": False}, + ), + ], + ) + @mock.patch("json.dump") + @mock.patch("builtins.open") + def test_success( + self, m_open, m_json_dump, old_conf, new_user_config, capsys + ): + create_new_user_config_file(old_conf) + assert [ + mock.call(new_user_config, mock.ANY) + ] == m_json_dump.call_args_list + assert ("", "") == capsys.readouterr() + + +class TestCreateNewUaclientConffile: + @pytest.mark.parametrize( + ["old_conf", "expected_msg"], + [ + ( + {}, + messages.USER_CONFIG_MIGRATION_MIGRATING + + "\n" + + messages.USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE # noqa: E501 + + "\n" + + " contract_url: 'https://contracts.canonical.com'\n" # noqa: E501 + + " log_level: 'debug'\n", + ), + ( + {"log_level": "debug"}, + messages.USER_CONFIG_MIGRATION_MIGRATING + + "\n" + + messages.USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE # noqa: E501 + + "\n" + + " contract_url: 'https://contracts.canonical.com'\n" # noqa: E501 + + " log_level: 'debug'\n", + ), + ( + {"log_level": "warning"}, + messages.USER_CONFIG_MIGRATION_MIGRATING + + "\n" + + messages.USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE # noqa: E501 + + "\n" + + " contract_url: 'https://contracts.canonical.com'\n" # noqa: E501 + + " log_level: 'warning'\n", + ), + ( + {"features": {}}, + messages.USER_CONFIG_MIGRATION_MIGRATING + + "\n" + + messages.USER_CONFIG_MIGRATION_WARNING_NEW_UACLIENT_CONF_WRITE # noqa: E501 + + "\n" + + " contract_url: 'https://contracts.canonical.com'\n" # noqa: E501 + + " features: {}\n" + + " log_level: 'debug'\n", + ), + ], + ) + @mock.patch("os.rename") + @mock.patch("builtins.open") + def test_file_access_error( + self, m_open, _m_rename, old_conf, expected_msg, capsys + ): + m_open.side_effect = Exception() + create_new_uaclient_conffile(old_conf) + assert ("", expected_msg) == capsys.readouterr() + + @pytest.mark.parametrize( + ["old_conf", "new_uaclient_conf"], + [ + ( + {}, + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + }, + ), + ( + {"log_level": "warning"}, + { + "contract_url": "https://contracts.canonical.com", + "log_level": "warning", + }, + ), + ( + {"features": {"allow_beta": True}}, + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + "features": {"allow_beta": True}, + }, + ), + ( + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + "log_file": "/var/log/ubuntu-advantage.log", + "daemon_log_file": "/var/log/ubuntu-advantage-daemon.log", + "timer_log_file": "/var/log/ubuntu-advantage-timer.log", + "data_dir": "/var/lib/ubuntu-advantage", + "ua_config": {"apt_news": False}, + }, + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + }, + ), + ( + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + "log_file": "/var/log/ubuntu-advantage.log", + "data_dir": "/var/lib/custom", + }, + { + "contract_url": "https://contracts.canonical.com", + "log_level": "debug", + "data_dir": "/var/lib/custom", + }, + ), + ], + ) + @mock.patch("os.rename") + @mock.patch("lib.migrate_user_config.safe_dump") + @mock.patch("builtins.open") + def test_success( + self, + m_open, + m_yaml_dump, + _m_rename, + old_conf, + new_uaclient_conf, + capsys, + ): + create_new_uaclient_conffile(old_conf) + assert [ + mock.call(new_uaclient_conf, default_flow_style=False) + ] == m_yaml_dump.call_args_list + assert ( + "", + messages.USER_CONFIG_MIGRATION_MIGRATING + "\n", + ) == capsys.readouterr() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_livepatch.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_livepatch.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_livepatch.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_livepatch.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,875 @@ +import datetime +import json + +import mock +import pytest + +from uaclient import exceptions, messages, system +from uaclient.entitlements.livepatch import LivepatchEntitlement +from uaclient.files.state_files import LivepatchSupportCacheData +from uaclient.livepatch import ( + LIVEPATCH_CMD, + LivepatchPatchFixStatus, + LivepatchPatchStatus, + LivepatchStatusStatus, + UALivepatchClient, + _on_supported_kernel_api, + _on_supported_kernel_cache, + _on_supported_kernel_cli, + configure_livepatch_proxy, + get_config_option_value, + on_supported_kernel, + status, + unconfigure_livepatch_proxy, +) + +M_PATH = "uaclient.livepatch." + + +class TestStatus: + @pytest.mark.parametrize( + [ + "is_installed", + "subp_sideeffect", + "expected", + ], + [ + (False, None, None), + (True, exceptions.ProcessExecutionError(""), None), + (True, [("", None)], None), + (True, [("{", None)], None), + (True, [("{}", None)], None), + (True, [('{"Status": false}', None)], None), + (True, [('{"Status": []}', None)], None), + ( + True, + [('{"Status": [{}]}', None)], + LivepatchStatusStatus( + kernel=None, livepatch=None, supported=None + ), + ), + ( + True, + [ + ( + json.dumps( + { + "Status": [ + { + "Kernel": "installed-kernel-generic", + "Livepatch": { + "State": "nothing-to-apply", + }, + } + ], + } + ), + None, + ) + ], + LivepatchStatusStatus( + kernel="installed-kernel-generic", + livepatch=LivepatchPatchStatus( + state="nothing-to-apply", + fixes=None, + ), + supported=None, + ), + ), + ( + True, + [ + ( + json.dumps( + { + "Status": [ + { + "Kernel": "installed-kernel-generic", + "Livepatch": { + "State": "applied", + "Fixes": [ + { + "Name": "cve-example", + "Description": "", + "Bug": "", + "Patched": True, + }, + ], + }, + } + ], + } + ), + None, + ) + ], + LivepatchStatusStatus( + kernel="installed-kernel-generic", + livepatch=LivepatchPatchStatus( + state="applied", + fixes=[ + LivepatchPatchFixStatus( + name="cve-example", + patched=True, + ) + ], + ), + supported=None, + ), + ), + ( + True, + [ + ( + json.dumps( + { + "Client-Version": "version", + "Machine-Id": "machine-id", + "Architecture": "x86_64", + "CPU-Model": "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz", # noqa: E501 + "Last-Check": "2022-07-05T18:29:00Z", + "Boot-Time": "2022-07-05T18:27:12Z", + "Uptime": "203", + "Status": [ + { + "Kernel": "4.15.0-187.198-generic", + "Running": True, + "Livepatch": { + "CheckState": "checked", + "State": "nothing-to-apply", + "Version": "", + }, + } + ], + "tier": "stable", + } + ), + None, + ) + ], + LivepatchStatusStatus( + kernel="4.15.0-187.198-generic", + livepatch=LivepatchPatchStatus( + state="nothing-to-apply", + fixes=None, + ), + supported=None, + ), + ), + ( + True, + [ + ( + json.dumps( + { + "Status": [ + { + "Supported": "supported", + } + ], + } + ), + None, + ) + ], + LivepatchStatusStatus( + kernel=None, + livepatch=None, + supported="supported", + ), + ), + ], + ) + @mock.patch(M_PATH + "system.subp") + @mock.patch(M_PATH + "is_livepatch_installed") + def test_status( + self, + m_is_livepatch_installed, + m_subp, + is_installed, + subp_sideeffect, + expected, + ): + m_is_livepatch_installed.return_value = is_installed + m_subp.side_effect = subp_sideeffect + assert expected == status() + + +@mock.patch(M_PATH + "serviceclient.UAServiceClient.request_url") +@mock.patch(M_PATH + "serviceclient.UAServiceClient.headers") +class TestUALivepatchClient: + @pytest.mark.parametrize( + [ + "version", + "flavor", + "arch", + "codename", + "expected_request_calls", + ], + [ + ( + "1.23-4", + "generic", + "amd64", + "xenial", + [ + mock.call( + "/v1/api/kernels/supported", + headers=mock.ANY, + query_params={ + "kernel-version": "1.23-4", + "flavour": "generic", + "architecture": "amd64", + "codename": "xenial", + }, + ) + ], + ), + ( + "5.67-8", + "kvm", + "arm64", + "kinetic", + [ + mock.call( + "/v1/api/kernels/supported", + headers=mock.ANY, + query_params={ + "kernel-version": "5.67-8", + "flavour": "kvm", + "architecture": "arm64", + "codename": "kinetic", + }, + ) + ], + ), + ], + ) + def test_is_kernel_supported_calls_api_with_correct_params( + self, + m_headers, + m_request_url, + version, + flavor, + arch, + codename, + expected_request_calls, + ): + m_request_url.return_value = ("mock", "mock") + lp_client = UALivepatchClient() + lp_client.is_kernel_supported(version, flavor, arch, codename) + assert m_request_url.call_args_list == expected_request_calls + + @pytest.mark.parametrize( + [ + "request_side_effect", + "expected", + ], + [ + ([({"Supported": True}, None)], True), + ([({"Supported": False}, None)], False), + ([({}, None)], False), + ([([], None)], None), + ([("string", None)], None), + (exceptions.UrlError(mock.MagicMock()), None), + (Exception(), None), + ], + ) + def test_is_kernel_supported_interprets_api_response( + self, + m_headers, + m_request_url, + request_side_effect, + expected, + ): + m_request_url.side_effect = request_side_effect + lp_client = UALivepatchClient() + assert lp_client.is_kernel_supported("", "", "", "") == expected + + +class TestOnSupportedKernel: + @pytest.mark.parametrize( + [ + "livepatch_status", + "expected", + ], + [ + (None, None), + ( + LivepatchStatusStatus( + kernel=None, livepatch=None, supported=None + ), + None, + ), + ( + LivepatchStatusStatus( + kernel=None, livepatch=None, supported="supported" + ), + True, + ), + ( + LivepatchStatusStatus( + kernel=None, livepatch=None, supported="unsupported" + ), + False, + ), + ( + LivepatchStatusStatus( + kernel=None, livepatch=None, supported="unknown" + ), + None, + ), + ], + ) + @mock.patch(M_PATH + "status") + def test_on_supported_kernel_cli( + self, + m_livepatch_status, + livepatch_status, + expected, + ): + m_livepatch_status.return_value = livepatch_status + assert _on_supported_kernel_cli() == expected + + @pytest.mark.parametrize( + [ + "args", + "cache_contents", + "expected", + ], + [ + # valid true + ( + ("5.14-14", "generic", "amd64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=True, + ), + (True, True), + ), + # valid false + ( + ("5.14-14", "generic", "amd64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=False, + ), + (True, False), + ), + # valid none + ( + ("5.14-14", "generic", "amd64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=None, + ), + (True, None), + ), + # invalid version doesn't match + ( + ("5.14-13", "generic", "amd64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=True, + ), + (False, None), + ), + # invalid flavor doesn't match + ( + ("5.14-14", "kvm", "amd64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=True, + ), + (False, None), + ), + # invalid arch doesn't match + ( + ("5.14-14", "generic", "arm64", "focal"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=True, + ), + (False, None), + ), + # invalid codename doesn't match + ( + ("5.14-14", "generic", "amd64", "xenial"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=6), + supported=True, + ), + (False, None), + ), + # invalid too old + ( + ("5.14-14", "generic", "amd64", "xenial"), + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + cached_at=datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=8), + supported=True, + ), + (False, None), + ), + ], + ) + @mock.patch(M_PATH + "state_files.livepatch_support_cache.read") + def test_on_supported_kernel_cache( + self, + m_cache_read, + args, + cache_contents, + expected, + ): + m_cache_read.return_value = cache_contents + assert _on_supported_kernel_cache(*args) == expected + + @pytest.mark.parametrize( + [ + "args", + "is_kernel_supported", + "expected_cache_write_call_args", + "expected", + ], + [ + ( + ("5.14-14", "generic", "amd64", "focal"), + True, + [ + mock.call( + LivepatchSupportCacheData( + version="5.14-14", + flavor="generic", + arch="amd64", + codename="focal", + supported=True, + cached_at=mock.ANY, + ) + ) + ], + True, + ), + ( + ("5.14-14", "kvm", "arm64", "focal"), + False, + [ + mock.call( + LivepatchSupportCacheData( + version="5.14-14", + flavor="kvm", + arch="arm64", + codename="focal", + supported=False, + cached_at=mock.ANY, + ) + ) + ], + False, + ), + ( + ("4.14-14", "kvm", "arm64", "xenial"), + None, + [ + mock.call( + LivepatchSupportCacheData( + version="4.14-14", + flavor="kvm", + arch="arm64", + codename="xenial", + supported=None, + cached_at=mock.ANY, + ) + ) + ], + None, + ), + ], + ) + @mock.patch(M_PATH + "state_files.livepatch_support_cache.write") + @mock.patch(M_PATH + "UALivepatchClient.is_kernel_supported") + def test_on_supported_kernel_api( + self, + m_is_kernel_supported, + m_cache_write, + args, + is_kernel_supported, + expected_cache_write_call_args, + expected, + ): + m_is_kernel_supported.return_value = is_kernel_supported + assert _on_supported_kernel_api(*args) == expected + assert m_cache_write.call_args_list == expected_cache_write_call_args + + @pytest.mark.parametrize( + [ + "cli_result", + "get_kernel_info_result", + "standardize_arch_name_result", + "get_platform_info_result", + "cache_result", + "api_result", + "cache_call_args", + "api_call_args", + "expected", + ], + [ + # cli result true + ( + True, + None, + None, + None, + None, + None, + [], + [], + True, + ), + # cli result false + ( + False, + None, + None, + None, + None, + None, + [], + [], + False, + ), + # insufficient kernel info + ( + None, + system.KernelInfo( + uname_release="", + proc_version_signature_version="", + flavor=None, + major=5, + minor=6, + abi=7, + patch=None, + ), + None, + None, + None, + None, + [], + [], + None, + ), + # cache result true + ( + None, + system.KernelInfo( + uname_release="", + proc_version_signature_version="", + flavor="generic", + major=5, + minor=6, + abi=7, + patch=None, + ), + "amd64", + {"series": "xenial"}, + (True, True), + None, + [mock.call("5.6", "generic", "amd64", "xenial")], + [], + True, + ), + # cache result false + ( + None, + system.KernelInfo( + uname_release="", + proc_version_signature_version="", + flavor="generic", + major=5, + minor=6, + abi=7, + patch=None, + ), + "amd64", + {"series": "xenial"}, + (True, False), + None, + [mock.call("5.6", "generic", "amd64", "xenial")], + [], + False, + ), + # cache result none + ( + None, + system.KernelInfo( + uname_release="", + proc_version_signature_version="", + flavor="generic", + major=5, + minor=6, + abi=7, + patch=None, + ), + "amd64", + {"series": "xenial"}, + (True, None), + None, + [mock.call("5.6", "generic", "amd64", "xenial")], + [], + None, + ), + # api result true + ( + None, + system.KernelInfo( + uname_release="", + proc_version_signature_version="", + flavor="generic", + major=5, + minor=6, + abi=7, + patch=None, + ), + "amd64", + {"series": "xenial"}, + (False, None), + True, + [mock.call("5.6", "generic", "amd64", "xenial")], + [mock.call("5.6", "generic", "amd64", "xenial")], + True, + ), + ], + ) + @mock.patch(M_PATH + "_on_supported_kernel_api") + @mock.patch(M_PATH + "_on_supported_kernel_cache") + @mock.patch(M_PATH + "system.get_platform_info") + @mock.patch(M_PATH + "util.standardize_arch_name") + @mock.patch(M_PATH + "system.get_dpkg_arch") + @mock.patch(M_PATH + "system.get_kernel_info") + @mock.patch(M_PATH + "_on_supported_kernel_cli") + def test_on_supported_kernel( + self, + m_supported_cli, + m_get_kernel_info, + m_get_dpkg_arch, + m_standardize_arch_name, + m_get_platform_info, + m_supported_cache, + m_supported_api, + cli_result, + get_kernel_info_result, + standardize_arch_name_result, + get_platform_info_result, + cache_result, + api_result, + cache_call_args, + api_call_args, + expected, + ): + m_supported_cli.return_value = cli_result + m_get_kernel_info.return_value = get_kernel_info_result + m_standardize_arch_name.return_value = standardize_arch_name_result + m_get_platform_info.return_value = get_platform_info_result + m_supported_cache.return_value = cache_result + m_supported_api.return_value = api_result + assert on_supported_kernel.__wrapped__() == expected + assert m_supported_cache.call_args_list == cache_call_args + assert m_supported_api.call_args_list == api_call_args + + +class TestConfigureLivepatchProxy: + @pytest.mark.parametrize( + "http_proxy,https_proxy,retry_sleeps", + ( + ("http_proxy", "https_proxy", [1, 2]), + ("http_proxy", "", None), + ("", "https_proxy", [1, 2]), + ("http_proxy", None, [1, 2]), + (None, "https_proxy", None), + (None, None, [1, 2]), + ), + ) + @mock.patch("uaclient.system.subp") + def test_configure_livepatch_proxy( + self, m_subp, http_proxy, https_proxy, retry_sleeps, capsys, event + ): + configure_livepatch_proxy(http_proxy, https_proxy, retry_sleeps) + expected_calls = [] + if http_proxy: + expected_calls.append( + mock.call( + [ + LIVEPATCH_CMD, + "config", + "http-proxy={}".format(http_proxy), + ], + retry_sleeps=retry_sleeps, + ) + ) + + if https_proxy: + expected_calls.append( + mock.call( + [ + LIVEPATCH_CMD, + "config", + "https-proxy={}".format(https_proxy), + ], + retry_sleeps=retry_sleeps, + ) + ) + + assert m_subp.call_args_list == expected_calls + + out, _ = capsys.readouterr() + if http_proxy or https_proxy: + assert out.strip() == messages.SETTING_SERVICE_PROXY.format( + service=LivepatchEntitlement.title + ) + + @pytest.mark.parametrize( + "key, subp_return_value, expected_ret", + [ + ("http-proxy", ("nonsense", ""), None), + ("http-proxy", ("", "nonsense"), None), + ( + "http-proxy", + ( + """\ +http-proxy: "" +https-proxy: "" +no-proxy: "" +remote-server: https://livepatch.canonical.com +ca-certs: "" +check-interval: 60 # minutes""", + "", + ), + None, + ), + ( + "http-proxy", + ( + """\ +http-proxy: one +https-proxy: two +no-proxy: "" +remote-server: https://livepatch.canonical.com +ca-certs: "" +check-interval: 60 # minutes""", + "", + ), + "one", + ), + ( + "https-proxy", + ( + """\ +http-proxy: one +https-proxy: two +no-proxy: "" +remote-server: https://livepatch.canonical.com +ca-certs: "" +check-interval: 60 # minutes""", + "", + ), + "two", + ), + ( + "nonexistentkey", + ( + """\ +http-proxy: one +https-proxy: two +no-proxy: "" +remote-server: https://livepatch.canonical.com +ca-certs: "" +check-interval: 60 # minutes""", + "", + ), + None, + ), + ], + ) + @mock.patch("uaclient.system.subp") + def test_get_config_option_value( + self, m_util_subp, key, subp_return_value, expected_ret + ): + m_util_subp.return_value = subp_return_value + ret = get_config_option_value(key) + assert ret == expected_ret + assert [ + mock.call([LIVEPATCH_CMD, "config"]) + ] == m_util_subp.call_args_list + + +class TestUnconfigureLivepatchProxy: + @pytest.mark.parametrize( + "livepatch_installed, protocol_type, retry_sleeps", + ( + (True, "http", None), + (True, "https", [1]), + (True, "http", []), + (False, "http", None), + ), + ) + @mock.patch("uaclient.system.which") + @mock.patch("uaclient.system.subp") + def test_unconfigure_livepatch_proxy( + self, subp, which, livepatch_installed, protocol_type, retry_sleeps + ): + if livepatch_installed: + which.return_value = LIVEPATCH_CMD + else: + which.return_value = None + kwargs = {"protocol_type": protocol_type} + if retry_sleeps is not None: + kwargs["retry_sleeps"] = retry_sleeps + assert None is unconfigure_livepatch_proxy(**kwargs) + if livepatch_installed: + expected_calls = [ + mock.call( + [LIVEPATCH_CMD, "config", protocol_type + "-proxy="], + retry_sleeps=retry_sleeps, + ) + ] + else: + expected_calls = [] + assert expected_calls == subp.call_args_list diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_lock.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_lock.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_lock.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_lock.py 2023-04-05 15:14:00.000000000 +0000 @@ -2,6 +2,7 @@ import pytest from uaclient.exceptions import LockHeldError +from uaclient.files.notices import Notice from uaclient.lock import SingleAttemptLock, SpinLock from uaclient.messages import LOCK_HELD @@ -12,7 +13,7 @@ @pytest.mark.parametrize("lock_cls", (SingleAttemptLock, SpinLock)) @mock.patch("os.getpid", return_value=123) @mock.patch(M_PATH_UACONFIG + "delete_cache_key") -@mock.patch("uaclient.files.NoticeFile.add") +@mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch(M_PATH_UACONFIG + "write_cache") class TestLockCommon: def test_creates_and_releases_lock( @@ -39,7 +40,9 @@ mock.call("lock", "123:some operation") ] == m_write_cache.call_args_list lock_msg = "Operation in progress: some operation" - assert [mock.call("", lock_msg)] == m_add_notice.call_args_list + assert [ + mock.call(Notice.OPERATION_IN_PROGRESS, lock_msg) + ] == m_add_notice.call_args_list assert [mock.call("lock")] == m_delete_cache_key.call_args_list def test_creates_and_releases_lock_when_error_occurs( @@ -65,13 +68,15 @@ mock.call("lock", "123:some operation") ] == m_write_cache.call_args_list lock_msg = "Operation in progress: some operation" - assert [mock.call("", lock_msg)] == m_add_notice.call_args_list + assert [ + mock.call(Notice.OPERATION_IN_PROGRESS, lock_msg) + ] == m_add_notice.call_args_list assert [mock.call("lock")] == m_delete_cache_key.call_args_list @mock.patch("os.getpid", return_value=123) @mock.patch(M_PATH_UACONFIG + "delete_cache_key") -@mock.patch("uaclient.files.NoticeFile.add") +@mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch(M_PATH_UACONFIG + "write_cache") class TestSingleAttemptLock: @mock.patch(M_PATH_UACONFIG + "check_lock_info", return_value=(10, "held")) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_log.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_log.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_log.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_log.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,159 @@ +import json +import logging +from io import StringIO + +import pytest + +from uaclient import log as pro_log + +LOG = logging.getLogger(__name__) +LOG_FMT = "%(asctime)s%(name)s%(funcName)s%(lineno)s\ +%(levelname)s%(message)s%(extra)s" +DATE_FMT = "%Y-%m-%dT%H:%M:%S%z" + + +class TestLogger: + @pytest.mark.parametrize("caplog_text", [logging.INFO], indirect=True) + def test_unredacted_text(self, caplog_text): + text = "Bearer SEKRET" + LOG.info(text) + log = caplog_text() + assert text in log + + @pytest.mark.parametrize( + "raw_log,expected", + ( + ("Super valuable", "Super valuable"), + ( + "Hi 'Bearer not the droids you are looking for', data", + "Hi 'Bearer ', data", + ), + ( + "Hi 'Bearer not the droids you are looking for', data", + "Hi 'Bearer ', data", + ), + ( + "Executed with sys.argv: ['/usr/bin/ua', 'attach', 'SEKRET']", + "Executed with sys.argv:" + " ['/usr/bin/ua', 'attach', '']", + ), + ( + "'resourceTokens': [{'token': 'SEKRET', 'type': 'cc-eal'}]'", + "'resourceTokens':" + " [{'token': '', 'type': 'cc-eal'}]'", + ), + ( + "'machineToken': 'SEKRET', 'machineTokenInfo': 'blah'", + "'machineToken': '', 'machineTokenInfo': 'blah'", + ), + ( + "Failed running command '/usr/lib/apt/apt-helper download-file" + "https://bearer:S3-Kr3T@esm.ubuntu.com/infra/ubuntu/pool/ " + "[exit(100)]. Message: Download of file failed" + " pkgAcquire::Run (13: Permission denied)", + "Failed running command '/usr/lib/apt/apt-helper download-file" + "https://bearer:@esm.ubuntu.com/infra/ubuntu/pool/ " + "[exit(100)]. Message: Download of file failed" + " pkgAcquire::Run (13: Permission denied)", + ), + ( + "/snap/bin/canonical-livepatch enable S3-Kr3T, foobar", + "/snap/bin/canonical-livepatch enable foobar", + ), + ( + "Contract value for 'resourceToken' changed to S3kR3T", + "Contract value for 'resourceToken' changed to ", + ), + ( + "data: {'contractToken': 'SEKRET', " + "'contractTokenInfo':{'expiry'}}", + "data: {'contractToken': '', " + "'contractTokenInfo':{'expiry'}}", + ), + ( + "data: {'resourceToken': 'SEKRET', " + "'entitlement': {'affordances':'blah blah' }}", + "data: {'resourceToken': '', " + "'entitlement': {'affordances':'blah blah' }}", + ), + ( + "https://contracts.canonical.com/v1/resources/livepatch" + "?token=SEKRET: invalid token", + "https://contracts.canonical.com/v1/resources/livepatch" + "?token= invalid token", + ), + ( + 'data: {"identityToken": "SEket.124-_ys"}', + 'data: {"identityToken": ""}', + ), + ( + "http://metadata/computeMetadata/v1/instance/service-accounts/" + "default/identity?audience=contracts.canon, data: none", + "http://metadata/computeMetadata/v1/instance/service-accounts/" + "default/identity?audience=contracts.canon, data: none", + ), + ( + "response: " + "http://metadata/computeMetadata/v1/instance/service-accounts/" + "default/identity?audience=contracts.canon, data: none", + "response: " + "http://metadata/computeMetadata/v1/instance/service-accounts/" + "default/identity?audience=contracts.canon, data: ", + ), + ( + "'token': 'SEKRET'", + "'token': ''", + ), + ( + "'userCode': 'SEKRET'", + "'userCode': ''", + ), + ( + "'magic_token=SEKRET'", + "'magic_token='", + ), + ), + ) + @pytest.mark.parametrize("caplog_text", [logging.INFO], indirect=True) + def test_redacted_text(self, caplog_text, raw_log, expected): + LOG.addFilter(pro_log.RedactionFilter()) + LOG.info(raw_log) + log = caplog_text() + assert expected in log + + +class TestLoggerFormatter: + @pytest.mark.parametrize( + "message,level,log_fn,levelname,extra", + ( + ("mIDValue", logging.DEBUG, LOG.debug, "DEBUG", None), + ("2B||~2B", logging.INFO, LOG.info, "INFO", None), + ( + "2B||~2B", + logging.WARNING, + LOG.warning, + "WARNING", + {"key": "value"}, + ), + ), + ) + @pytest.mark.parametrize("caplog_text", [logging.NOTSET], indirect=True) + def test_valid_json_output( + self, caplog_text, message, level, log_fn, levelname, extra + ): + formatter = pro_log.JsonArrayFormatter(LOG_FMT, DATE_FMT) + buffer = StringIO() + sh = logging.StreamHandler(buffer) + sh.setLevel(level) + sh.setFormatter(formatter) + LOG.addHandler(sh) + log_fn(message, extra={"extra": extra}) + logged_value = buffer.getvalue() + val = json.loads(logged_value) + assert val[1] == levelname + assert val[2] == __name__ + assert val[5] == message + if extra: + assert val[6].get("key") == extra.get("key") + else: + assert 7 == len(val) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_reboot_cmds.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_reboot_cmds.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_reboot_cmds.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_reboot_cmds.py 2023-04-05 15:14:00.000000000 +0000 @@ -9,12 +9,9 @@ process_reboot_operations, run_command, ) +from uaclient import messages from uaclient.exceptions import ProcessExecutionError -from uaclient.messages import ( - FIPS_REBOOT_REQUIRED_MSG, - FIPS_SYSTEM_REBOOT_REQUIRED, - REBOOT_SCRIPT_FAILED, -) +from uaclient.files.notices import Notice M_FIPS_PATH = "uaclient.entitlements.fips.FIPSEntitlement." @@ -46,7 +43,10 @@ @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) @mock.patch("lib.reboot_cmds.subp") def test_main_unattached_removes_marker( - self, m_subp, FakeConfig, caplog_text + self, + m_subp, + FakeConfig, + caplog_text, ): cfg = FakeConfig() cfg.write_cache("marker-reboot-cmds", "samplecontent") @@ -64,7 +64,9 @@ @mock.patch("lib.reboot_cmds.subp") def test_main_unattached_removes_marker_file( - self, m_subp, FakeConfig, tmpdir + self, + m_subp, + FakeConfig, ): cfg = FakeConfig.for_attached_machine() assert None is cfg.read_cache("marker-reboot-cmds") @@ -81,7 +83,7 @@ @mock.patch("sys.exit") @mock.patch(M_FIPS_PATH + "install_packages") @mock.patch(M_FIPS_PATH + "setup_apt_config") - @mock.patch("uaclient.files.NoticeFile.remove") + @mock.patch("uaclient.files.notices.NoticesManager.remove") def test_calls_setup_apt_config_and_install_packages_when_enabled( self, m_remove_notice, @@ -103,10 +105,6 @@ assert [ mock.call(cleanup_on_failure=False) ] == install_packages.call_args_list - assert [ - mock.call("", FIPS_SYSTEM_REBOOT_REQUIRED.msg), - mock.call("", FIPS_REBOOT_REQUIRED_MSG), - ] == m_remove_notice.call_args_list else: assert 0 == setup_apt_config.call_count assert 0 == install_packages.call_count @@ -148,7 +146,7 @@ @pytest.mark.parametrize("caplog_text", [logging.ERROR], indirect=True) @mock.patch("uaclient.config.UAConfig.delete_cache_key") @mock.patch("uaclient.config.UAConfig.check_lock_info") - @mock.patch("uaclient.files.NoticeFile.add") + @mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch("lib.reboot_cmds.fix_pro_pkg_holds") def test_process_reboot_operations_create_notice_when_it_fails( self, @@ -167,7 +165,12 @@ with mock.patch("uaclient.config.UAConfig.write_cache"): process_reboot_operations(cfg=cfg) - expected_calls = [mock.call("", REBOOT_SCRIPT_FAILED)] + expected_calls = [ + mock.call( + Notice.REBOOT_SCRIPT_FAILED, + messages.REBOOT_SCRIPT_FAILED, + ), + ] assert expected_calls == m_add_notice.call_args_list diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_security.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_security.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_security.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_security.py 2023-04-05 15:14:00.000000000 +0000 @@ -13,6 +13,7 @@ ApplicabilityStatus, UserFacingStatus, ) +from uaclient.files.notices import Notice from uaclient.messages import ( ENABLE_REBOOT_REQUIRED_TMPL, FAIL_X, @@ -44,6 +45,7 @@ _check_attached, _check_subscription_for_required_service, _check_subscription_is_expired, + _prompt_for_attach, fix_security_issue_id, get_cve_affected_source_packages_status, get_related_usns, @@ -307,7 +309,7 @@ textwrap.dedent( """\ CVE-2020-1472: Samba vulnerability - https://ubuntu.com/security/CVE-2020-1472""" + - https://ubuntu.com/security/CVE-2020-1472""" ) == cve.get_url_header() ) @@ -505,8 +507,8 @@ """\ USN-4510-2: Samba vulnerability Found CVEs: - https://ubuntu.com/security/CVE-2020-1473 - https://ubuntu.com/security/CVE-2020-1472""" + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472""" ), ), ( @@ -517,22 +519,22 @@ """\ USN-4510-2: Samba vulnerability Found CVEs: -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472 -https://ubuntu.com/security/CVE-2020-1473 -https://ubuntu.com/security/CVE-2020-1472""", + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472 + - https://ubuntu.com/security/CVE-2020-1473 + - https://ubuntu.com/security/CVE-2020-1472""", ), ( SAMPLE_USN_RESPONSE_NO_CVES, @@ -540,7 +542,7 @@ """\ USN-4038-3: USN vulnerability Found Launchpad bugs: - https://launchpad.net/bugs/1834494""" + - https://launchpad.net/bugs/1834494""" ), ), ), @@ -1002,6 +1004,7 @@ textwrap.dedent( """\ No affected source packages are installed. + {check} USN-### does not affect your system. """.format( check=OKGREEN_CHECK # noqa: E126 @@ -1020,6 +1023,7 @@ (1/1) slsrc: A fix is available in Ubuntu standard updates. The update is already installed. + {check} USN-### is resolved. """.format( check=OKGREEN_CHECK # noqa: E126 @@ -1042,7 +1046,7 @@ + colorize_commands( [["apt update && apt install --only-upgrade" " -y sl"]] ) - + "\n" + + "\n\n" + "{check} USN-### is resolved.\n".format(check=OKGREEN_CHECK), FixStatus.SYSTEM_NON_VULNERABLE, ), @@ -1068,6 +1072,7 @@ ] ] ), + "", "{check} USN-### is resolved.\n".format( check=OKGREEN_CHECK ), @@ -1153,7 +1158,7 @@ SECURITY_UPDATE_NOT_INSTALLED_SUBSCRIPTION, ] ) - + "\n" + + "\n\n" + "1 package is still affected: slsrc", FixStatus.SYSTEM_STILL_VULNERABLE, ), @@ -1242,7 +1247,7 @@ SECURITY_UPDATE_NOT_INSTALLED_SUBSCRIPTION, ] ) - + "\n" + + "\n\n" + "13 packages are still affected: {}".format( ( "pkg1, pkg12, pkg13, pkg14, pkg15, pkg2, pkg3,\n" @@ -1282,6 +1287,7 @@ "pkg1, pkg2, pkg3, pkg4, pkg5, pkg6,\n" " pkg7, pkg8, pkg9" ) + + "\n" + "9 packages are still affected: {}".format( "pkg1, pkg2, pkg3, pkg4, pkg5, pkg6, pkg7, pkg8,\n" " pkg9" @@ -1349,7 +1355,7 @@ ] ] ) - + "\n" + + "\n\n" + "{check} USN-### is resolved.\n".format(check=OKGREEN_CHECK), FixStatus.SYSTEM_NON_VULNERABLE, ), @@ -1358,6 +1364,7 @@ @mock.patch("uaclient.entitlements.base.UAEntitlement.user_facing_status") @mock.patch("uaclient.system.should_reboot", return_value=False) @mock.patch("os.getuid", return_value=0) + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("uaclient.apt.run_apt_command", return_value="") @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="c") @@ -1366,6 +1373,7 @@ prompt_choices, get_cloud_type, m_run_apt_cmd, + _m_get_pkg_cand_ver, _m_os_getuid, _m_should_reboot, m_user_facing_status, @@ -1459,7 +1467,7 @@ + colorize_commands( [["apt update && apt install --only-upgrade" " -y pkg1"]] ) - + "\n" + + "\n\n" + "{check} USN-### is resolved.\n".format(check=OKGREEN_CHECK), ), ), @@ -1470,16 +1478,16 @@ @mock.patch("uaclient.security._check_subscription_for_required_service") @mock.patch("uaclient.cli.action_attach") @mock.patch("builtins.input", return_value="token") - @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.apt.run_apt_command", return_value="") @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="a") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") def test_messages_for_affected_packages_covering_all_release_pockets( self, + _m_apt_pkg_candidate_version, m_prompt_choices, m_get_cloud_type, m_run_apt_cmd, - _m_os_getuid, _m_input, m_action_attach, m_check_subscription_for_service, @@ -1551,6 +1559,7 @@ A fix is available in Ubuntu standard updates. """ ) + + "\n" + "3 packages are still affected: pkg1, pkg2, pkg3" + "\n" + "{check} USN-### is not resolved.\n".format(check=FAIL_X), @@ -1559,8 +1568,10 @@ ) @mock.patch("uaclient.system.should_reboot", return_value=False) @mock.patch("uaclient.security.upgrade_packages_and_attach") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") def test_messages_for_affected_packages_when_fix_fail( self, + _m_apt_pkg_candidate_version, m_upgrade_packages, _m_should_reboot, affected_pkg_status, @@ -1592,6 +1603,70 @@ out, err = capsys.readouterr() assert expected in out + @pytest.mark.parametrize( + "affected_pkg_status,installed_packages,usn_released_pkgs,expected", + ( + ( + { + "pkg1": CVEPackageStatus(CVE_PKG_STATUS_RELEASED), + "pkg2": CVEPackageStatus(CVE_PKG_STATUS_RELEASED), + }, + { + "pkg1": {"pkg1": "1.8"}, + "pkg2": {"pkg2": "1.8"}, + }, + { + "pkg1": {"pkg1": {"version": "2.0"}}, + "pkg2": {"pkg2": {"version": "1.8"}}, + }, + textwrap.dedent( + """\ + 2 affected source packages are installed: pkg1, pkg2 + (1/2, 2/2) pkg1, pkg2: + A fix is available in Ubuntu standard updates. + - Cannot install package pkg1 version 2.0 + """ + ) + + "\n" + + "1 package is still affected: pkg1" + + "\n" + + "{check} CVE-### is not resolved.\n".format(check=FAIL_X), + ), + ), + ) + @mock.patch("uaclient.apt.compare_versions") + @mock.patch("uaclient.apt.get_pkg_candidate_version") + def test_messages_for_affected_packages_when_pkg_cannot_be_upgraded( + self, + m_apt_pkg_candidate_version, + m_apt_compare_versions, + affected_pkg_status, + installed_packages, + usn_released_pkgs, + expected, + FakeConfig, + capsys, + _subp, + ): + m_apt_pkg_candidate_version.return_value = 1.8 + m_apt_compare_versions.side_effect = [False, True, False] + + cfg = FakeConfig() + with mock.patch("uaclient.util.sys") as m_sys: + m_stdout = mock.MagicMock() + type(m_sys).stdout = m_stdout + type(m_stdout).encoding = mock.PropertyMock(return_value="utf-8") + prompt_for_affected_packages( + cfg=cfg, + issue_id="CVE-###", + affected_pkg_status=affected_pkg_status, + installed_packages=installed_packages, + usn_released_pkgs=usn_released_pkgs, + dry_run=False, + ) + out, err = capsys.readouterr() + assert expected in out + @pytest.mark.parametrize("should_reboot", (False, True)) @pytest.mark.parametrize( "service_status", @@ -1622,7 +1697,7 @@ + colorize_commands([["pro attach token"]]) + "\n" + SECURITY_UA_SERVICE_NOT_ENTITLED.format(service="esm-infra") - + "\n" + + "\n\n" + "1 package is still affected: pkg1" + "\n" + "{check} USN-### is not resolved.\n".format(check=FAIL_X), @@ -1632,16 +1707,16 @@ @mock.patch("uaclient.util.is_config_value_true", return_value=True) @mock.patch("uaclient.system.should_reboot") @mock.patch("uaclient.cli.action_attach") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("builtins.input", return_value="token") - @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="a") def test_messages_for_affected_packages_when_required_service_not_enabled( self, m_prompt_choices, m_get_cloud_type, - _m_os_getuid, _m_input, + _m_apt_pkg_candidate_version, m_action_attach, m_should_reboot, _m_is_config_value_true, @@ -1719,7 +1794,7 @@ + colorize_commands( [["apt update && apt install --only-upgrade" " -y pkg1"]] ) - + "\n" + + "\n\n" + "{check} USN-### is resolved.\n".format(check=OKGREEN_CHECK), ), ), @@ -1730,6 +1805,7 @@ @mock.patch("uaclient.security._check_subscription_is_expired") @mock.patch("uaclient.cli.action_enable", return_value=0) @mock.patch("uaclient.apt.run_apt_command", return_value="") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="e") @@ -1738,6 +1814,7 @@ m_prompt_choices, m_get_cloud_type, _m_os_getuid, + _m_apt_pkg_candidate_version, _m_run_apt, m_action_enable, m_check_subscription_expired, @@ -1810,7 +1887,7 @@ + SECURITY_SERVICE_DISABLED.format(service="esm-infra") + "\n" + SECURITY_UA_SERVICE_NOT_ENABLED.format(service="esm-infra") - + "\n" + + "\n\n" + "1 package is still affected: pkg1" + "\n" + "{check} USN-### is not resolved.\n".format(check=FAIL_X), @@ -1821,6 +1898,7 @@ @mock.patch("uaclient.util.is_config_value_true", return_value=False) @mock.patch("uaclient.system.should_reboot", return_value=False) @mock.patch("uaclient.security._check_subscription_is_expired") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="c") @@ -1829,6 +1907,7 @@ m_prompt_choices, m_get_cloud_type, _m_os_getuid, + _m_apt_pkg_candidate_version, m_check_subscription_expired, _m_should_reboot, _m_is_config_value_true, @@ -1889,7 +1968,8 @@ {"pkg1": CVEPackageStatus(CVE_PKG_STATUS_RELEASED_ESM_INFRA)}, {"pkg1": {"pkg1": "1.8"}}, {"pkg1": {"pkg1": {"version": "2.0"}}}, - textwrap.dedent( + "\n" + + textwrap.dedent( """\ 1 affected source package is installed: pkg1 (1/1) pkg1: @@ -1907,7 +1987,7 @@ + colorize_commands( [["apt update && apt install --only-upgrade" " -y pkg1"]] ) - + "\n" + + "\n\n" + "{check} USN-### is resolved.\n".format(check=OKGREEN_CHECK), ), ), @@ -1915,22 +1995,22 @@ @mock.patch("uaclient.security._is_pocket_used_by_beta_service") @mock.patch("uaclient.system.should_reboot", return_value=False) @mock.patch("uaclient.apt.run_apt_command", return_value="") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("uaclient.cli.action_attach") @mock.patch("builtins.input", return_value="token") @mock.patch("uaclient.cli.action_detach") @mock.patch("uaclient.security._check_subscription_for_required_service") - @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="r") def test_messages_for_affected_packages_when_subscription_expired( self, m_prompt_choices, m_get_cloud_type, - _m_os_getuid, m_check_subscription_for_service, _m_cli_detach, _m_input, m_cli_attach, + _m_apt_pkg_candidate_version, _m_run_apt_command, _m_should_reboot, m_is_pocket_beta_service, @@ -1987,7 +2067,7 @@ """ ) + SECURITY_UPDATE_NOT_INSTALLED_EXPIRED - + "\n" + + "\n\n" + "1 package is still affected: pkg1" + "\n" + "{check} USN-### is not resolved.\n".format(check=FAIL_X), @@ -1996,6 +2076,7 @@ ) @mock.patch("uaclient.security._is_pocket_used_by_beta_service") @mock.patch("uaclient.system.should_reboot", return_value=False) + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") @mock.patch("uaclient.security.util.prompt_choices", return_value="c") @@ -2004,6 +2085,7 @@ m_prompt_choices, m_get_cloud_type, _m_os_getuid, + _m_apt_pkg_candidate_version, _m_should_reboot, m_is_pocket_beta_service, affected_pkg_status, @@ -2060,7 +2142,7 @@ + colorize_commands( [["apt update && apt install --only-upgrade" " -y pkg1"]] ) - + "\n" + + "\n\n" + "A reboot is required to complete fix operation." + "\n" + "{check} USN-### is not resolved.\n".format(check=FAIL_X), @@ -2068,15 +2150,17 @@ ), ), ) - @mock.patch("uaclient.files.NoticeFile.add") + @mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch("uaclient.system.should_reboot", return_value=True) @mock.patch("uaclient.apt.run_apt_command", return_value="") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") def test_messages_for_affected_packages_when_reboot_required( self, m_get_cloud_type, _m_os_getuid, + _m_apt_pkg_candidate_version, _m_run_apt_command, _m_should_reboot, m_add_notice, @@ -2113,7 +2197,7 @@ assert [ mock.call( - "", + Notice.ENABLE_REBOOT_REQUIRED, ENABLE_REBOOT_REQUIRED_TMPL.format(operation="fix operation"), ) ] == m_add_notice.call_args_list @@ -2131,6 +2215,7 @@ (1/1) slsrc: A fix is available in Ubuntu standard updates. The update is already installed. + {check} USN-### is resolved. """.format( check=OKGREEN_CHECK # noqa: E126 @@ -2139,15 +2224,17 @@ ), ), ) - @mock.patch("uaclient.files.NoticeFile.add") + @mock.patch("uaclient.files.notices.NoticesManager.add") @mock.patch("uaclient.system.should_reboot", return_value=True) @mock.patch("uaclient.apt.run_apt_command", return_value="") + @mock.patch("uaclient.apt.get_pkg_candidate_version", return_value="99.9") @mock.patch("os.getuid", return_value=0) @mock.patch("uaclient.security.get_cloud_type") def test_messages_for_affected_packages_when_reboot_required_but_update_already_installed( # noqa: E501 self, m_get_cloud_type, _m_os_getuid, + _m_apt_pkg_candidate_version, _m_run_apt_command, _m_should_reboot, m_add_notice, @@ -2178,14 +2265,14 @@ class TestUpgradePackagesAndAttach: - @pytest.mark.parametrize("getuid_value", ((0), (1))) - @mock.patch("os.getuid") + @pytest.mark.parametrize("root", ((True), (False))) + @mock.patch("uaclient.util.we_are_currently_root") @mock.patch("uaclient.security.system.subp") def test_upgrade_packages_are_installed_without_need_for_ua( - self, m_subp, m_os_getuid, getuid_value, capsys + self, m_subp, m_we_are_currently_root, root, capsys ): m_subp.return_value = ("", "") - m_os_getuid.return_value = getuid_value + m_we_are_currently_root.return_value = root upgrade_packages_and_attach( cfg=None, @@ -2195,7 +2282,7 @@ ) out, err = capsys.readouterr() - if getuid_value == 0: + if root: assert m_subp.call_count == 2 assert "apt update" in out assert "apt install --only-upgrade -y t1 t2" in out @@ -2203,6 +2290,32 @@ assert SECURITY_APT_NON_ROOT in out assert m_subp.call_count == 0 + @pytest.mark.parametrize( + "exception_cls, expected_error_msg", + ( + (Exception, "base-exception"), + (exceptions.UserFacingError, "pro-exception"), + ), + ) + @mock.patch("os.getuid", return_value=0) + @mock.patch("uaclient.security.system.subp") + def test_upgrade_packages_fail_if_apt_command_fails( + self, m_subp, m_os_getuid, exception_cls, expected_error_msg, capsys + ): + m_subp.side_effect = exception_cls(expected_error_msg) + assert ( + upgrade_packages_and_attach( + cfg=None, + upgrade_pkgs=["t1=123"], + pocket="Ubuntu standard updates", + dry_run=False, + ) + is False + ) + + out, _ = capsys.readouterr() + assert expected_error_msg in out + class TestGetRelatedUSNs: def test_original_usn_returned_when_no_cves_are_found(self, FakeConfig): @@ -2630,3 +2743,32 @@ status_cache=status_cache, cfg=None, dry_run=False ) assert 1 == m_prompt.call_count + + +class TestPromptForAttach: + @mock.patch("uaclient.security._initiate") + @mock.patch("uaclient.security._wait") + @mock.patch("uaclient.security._revoke") + @mock.patch("uaclient.security._inform_ubuntu_pro_existence_if_applicable") + @mock.patch("uaclient.util.prompt_choices") + def test_magic_attach_revoke_token_if_wait_fails( + self, + m_prompt_choices, + _m_inform_pro, + m_revoke, + m_wait, + m_initiate, + FakeConfig, + ): + m_prompt_choices.return_value = "s" + m_initiate.return_value = mock.MagicMock( + token="token", user_code="user_code" + ) + m_wait.side_effect = exceptions.MagicAttachTokenError() + + with pytest.raises(exceptions.MagicAttachTokenError): + _prompt_for_attach(cfg=FakeConfig()) + + assert 1 == m_initiate.call_count + assert 1 == m_wait.call_count + assert 1 == m_revoke.call_count diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_security_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_security_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_security_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_security_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -1,12 +1,10 @@ -import logging from collections import defaultdict -from json import JSONDecodeError from typing import List, Optional import mock import pytest -from uaclient.exceptions import ProcessExecutionError +from uaclient import livepatch from uaclient.security_status import ( RebootStatus, UpdateStatus, @@ -524,7 +522,7 @@ @mock.patch(M_PATH + "status", return_value={"attached": False}) @mock.patch(M_PATH + "get_origin_for_package", return_value="main") @mock.patch(M_PATH + "filter_security_updates") - @mock.patch(M_PATH + "apt.Cache") + @mock.patch(M_PATH + "get_apt_cache") def test_security_status_dict( self, m_cache, @@ -594,34 +592,21 @@ assert expected_output == security_status_dict(cfg) -@mock.patch(M_PATH + "json.loads") +@mock.patch(M_PATH + "livepatch.status") @mock.patch(M_PATH + "get_kernel_info") class TestGetLivepatchFixedCVEs: - @mock.patch(M_PATH + "subp") - def test_livepatch_subp_error(self, m_subp, _m_kernel_info, _m_loads): - m_subp.side_effect = ProcessExecutionError("error") - - assert [] == get_livepatch_fixed_cves() - - @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) - def test_livepatch_wrong_json(self, _m_kernel_info, m_loads, caplog_text): - m_loads.side_effect = JSONDecodeError("", "", 0) - + def test_livepatch_status_none(self, _m_kernel_info, m_livepatch_status): + m_livepatch_status.return_value = None assert [] == get_livepatch_fixed_cves() - assert "Could not parse Livepatch Status JSON" in caplog_text() - @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) - def test_cant_get_kernel_info(self, m_kernel_info, m_loads, caplog_text): - m_loads.return_value = { - "Status": [ - { - "Kernel": "installed-kernel-generic", - "Livepatch": { - "State": "nothing-to-apply", - }, - } - ], - } + def test_cant_get_kernel_info(self, m_kernel_info, m_livepatch_status): + m_livepatch_status.return_value = livepatch.LivepatchStatusStatus( + kernel="installed-kernel-generic", + livepatch=livepatch.LivepatchPatchStatus( + state="nothing-to-apply", fixes=None + ), + supported=None, + ) m_kernel_info.return_value = KernelInfo( uname_release="", @@ -635,45 +620,36 @@ assert [] == get_livepatch_fixed_cves() - def test_livepatch_no_fixes(self, m_kernel_info, m_loads): + def test_livepatch_no_fixes(self, m_kernel_info, m_livepatch_status): m_kernel_info.return_value.proc_version_signature_version = ( "installed-kernel-generic" ) - m_loads.return_value = { - "Status": [ - { - "Kernel": "installed-kernel-generic", - "Livepatch": { - "State": "nothing-to-apply", - }, - } - ], - } + m_livepatch_status.return_value = livepatch.LivepatchStatusStatus( + kernel="installed-kernel-generic", + livepatch=livepatch.LivepatchPatchStatus( + state="nothing-to-apply", fixes=None + ), + supported=None, + ) assert [] == get_livepatch_fixed_cves() - def test_livepatch_has_fixes(self, m_kernel_info, m_loads): + def test_livepatch_has_fixes(self, m_kernel_info, m_livepatch_status): m_kernel_info.return_value.proc_version_signature_version = ( "installed-kernel-generic" ) - m_loads.return_value = { - "Status": [ - { - "Kernel": "installed-kernel-generic", - "Livepatch": { - "State": "applied", - "Fixes": [ - { - "Name": "cve-example", - "Description": "", - "Bug": "", - "Patched": True, - }, - ], - }, - } - ], - } + m_livepatch_status.return_value = livepatch.LivepatchStatusStatus( + kernel="installed-kernel-generic", + livepatch=livepatch.LivepatchPatchStatus( + state="applied", + fixes=[ + livepatch.LivepatchPatchFixStatus( + name="cve-example", patched=True + ) + ], + ), + supported=None, + ) assert [ {"name": "cve-example", "patched": True} @@ -696,24 +672,23 @@ assert 1 == m_should_reboot.call_count assert 1 == m_load_file.call_count - @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) - @mock.patch("uaclient.security_status.subp") - @mock.patch("uaclient.security_status.which", return_value=True) + @mock.patch("uaclient.security_status.livepatch.status") + @mock.patch( + "uaclient.security_status.livepatch.is_livepatch_installed", + return_value=True, + ) @mock.patch("uaclient.security_status.load_file") @mock.patch("uaclient.security_status.should_reboot", return_value=True) - def test_get_reboot_status_fail_to_parse_livepatch_output( + def test_get_reboot_status_livepatch_status_none( self, m_should_reboot, m_load_file, - _m_which, - m_subp, - caplog_text, + _m_is_livepatch_installed, + m_livepatch_status, ): m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" - m_subp.return_value = ('{"test": 123', "") - + m_livepatch_status.return_value = None assert get_reboot_status() == RebootStatus.REBOOT_REQUIRED - assert "Could not parse Livepatch Status JSON" in caplog_text() @pytest.mark.parametrize( "pkgs,expected_state", @@ -762,16 +737,16 @@ ), ) @mock.patch("uaclient.security_status.get_kernel_info") - @mock.patch("uaclient.security_status.which") - @mock.patch("uaclient.security_status.subp") + @mock.patch("uaclient.security_status.livepatch.is_livepatch_installed") + @mock.patch("uaclient.security_status.livepatch.status") @mock.patch("uaclient.security_status.load_file") @mock.patch("uaclient.security_status.should_reboot", return_value=True) def test_get_reboot_status_reboot_pkgs_file_only_kernel_pkgs( self, m_should_reboot, m_load_file, - m_subp, - m_which, + m_livepatch_status, + m_is_livepatch_installed, m_kernel_info, livepatch_state, expected_state, @@ -780,92 +755,52 @@ m_kernel_info.return_value = mock.MagicMock( proc_version_signature_version=kernel_name ) - m_which.return_value = True + m_is_livepatch_installed.return_value = True m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" - m_subp.return_value = ( - """ - {{ - "Client-Version": "version", - "Machine-Id": "machine-id", - "Architecture": "x86_64", - "CPU-Model": "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz", - "Last-Check": "2022-07-05T18:29:00Z", - "Boot-Time": "2022-07-05T18:27:12Z", - "Uptime": "203", - "Status": [ - {{ - "Kernel": "4.15.0-187.198-generic", - "Running": true, - "Livepatch": {{ - "CheckState": "checked", - "State": "{}", - "Version": "" - }} - }} - ], - "tier": "stable" - }} - """.format( - livepatch_state + m_livepatch_status.return_value = livepatch.LivepatchStatusStatus( + kernel="4.15.0-187.198-generic", + livepatch=livepatch.LivepatchPatchStatus( + state=livepatch_state, fixes=None ), - "", + supported=None, ) assert get_reboot_status() == expected_state assert 1 == m_should_reboot.call_count assert 1 == m_load_file.call_count - assert 1 == m_which.call_count - assert 1 == m_subp.call_count + assert 1 == m_is_livepatch_installed.call_count + assert 1 == m_livepatch_status.call_count assert 1 == m_kernel_info.call_count @mock.patch("uaclient.security_status.get_kernel_info") - @mock.patch("uaclient.security_status.which") - @mock.patch("uaclient.security_status.subp") + @mock.patch("uaclient.security_status.livepatch.is_livepatch_installed") + @mock.patch("uaclient.security_status.livepatch.status") @mock.patch("uaclient.security_status.load_file") @mock.patch("uaclient.security_status.should_reboot", return_value=True) def test_get_reboot_status_fail_parsing_kernel_info( self, m_should_reboot, m_load_file, - m_subp, - m_which, + m_livepatch_status, + m_is_livepatch_installed, m_kernel_info, ): m_kernel_info.return_value = mock.MagicMock( proc_version_signature_version=None ) - m_which.return_value = True + m_is_livepatch_installed.return_value = True m_load_file.return_value = "linux-image-5.4.0-1074\nlinux-base" - m_subp.return_value = ( - """ - { - "Client-Version": "version", - "Machine-Id": "machine-id", - "Architecture": "x86_64", - "CPU-Model": "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz", - "Last-Check": "2022-07-05T18:29:00Z", - "Boot-Time": "2022-07-05T18:27:12Z", - "Uptime": "203", - "Status": [ - { - "Kernel": "4.15.0-187.198-generic", - "Running": true, - "Livepatch": { - "CheckState": "checked", - "State": "applied", - "Version": "" - } - } - ], - "tier": "stable" - } - """, - "", + m_livepatch_status.return_value = livepatch.LivepatchStatusStatus( + kernel="4.15.0-187.198-generic", + livepatch=livepatch.LivepatchPatchStatus( + state="applied", fixes=None + ), + supported=None, ) assert get_reboot_status() == RebootStatus.REBOOT_REQUIRED assert 1 == m_should_reboot.call_count assert 1 == m_load_file.call_count - assert 1 == m_which.call_count - assert 1 == m_subp.call_count + assert 1 == m_is_livepatch_installed.call_count + assert 1 == m_livepatch_status.call_count assert 1 == m_kernel_info.call_count diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_serviceclient.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_serviceclient.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_serviceclient.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_serviceclient.py 2023-04-05 15:14:00.000000000 +0000 @@ -90,7 +90,7 @@ headers=client.headers(), method=None, timeout=30, - potentially_sensitive=True, + log_response_body=True, ) ] == m_readurl.call_args_list diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_status.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_status.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_status.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_status.py 2023-04-05 15:14:00.000000000 +0000 @@ -23,6 +23,7 @@ from uaclient.entitlements.fips import FIPSEntitlement from uaclient.entitlements.ros import ROSEntitlement from uaclient.entitlements.tests.test_base import ConcreteTestEntitlement +from uaclient.files.notices import Notice, NoticesManager from uaclient.status import ( DEFAULT_STATUS, TxtColor, @@ -129,6 +130,7 @@ assert colorize_commands(commands) == expected +@mock.patch("uaclient.livepatch.on_supported_kernel", return_value=None) class TestFormatTabular: @pytest.mark.parametrize( "support_level,expected_colour,istty", @@ -150,6 +152,7 @@ def test_support_colouring( self, m_isatty, + m_on_supported_kernel, support_level, expected_colour, istty, @@ -168,7 +171,9 @@ assert expected_string in tabular_output @pytest.mark.parametrize("origin", ["free", "not-free"]) - def test_header_alignment(self, origin, status_dict_attached): + def test_header_alignment( + self, m_on_supported_kernel, origin, status_dict_attached + ): status_dict_attached["origin"] = origin tabular_output = format_tabular(status_dict_attached) colon_idx = None @@ -191,7 +196,11 @@ ], ) def test_correct_header_keys_included( - self, origin, expected_headers, status_dict_attached + self, + m_on_supported_kernel, + origin, + expected_headers, + status_dict_attached, ): status_dict_attached["origin"] = origin @@ -209,7 +218,9 @@ ] assert list(expected_headers) == headers - def test_correct_unattached_column_alignment(self, status_dict_unattached): + def test_correct_unattached_column_alignment( + self, m_on_supported_kernel, status_dict_unattached + ): tabular_output = format_tabular(status_dict_unattached) [header, eal_service_line] = [ line @@ -224,7 +235,11 @@ @pytest.mark.parametrize("attached", [True, False]) def test_no_leading_newline( - self, attached, status_dict_attached, status_dict_unattached + self, + m_on_supported_kernel, + attached, + status_dict_attached, + status_dict_unattached, ): if attached: status_dict = status_dict_attached @@ -242,7 +257,12 @@ ), ) def test_custom_descr( - self, description_override, uf_status, uf_descr, status_dict_attached + self, + m_on_supported_kernel, + description_override, + uf_status, + uf_descr, + status_dict_attached, ): """Services can provide a custom call to action if present.""" default_descr = "Common Criteria EAL2 default descr" @@ -272,7 +292,8 @@ return entitlement_factory(cfg=FakeConfig(), name="ros").description -@mock.patch("uaclient.files.NoticeFile.remove") +@mock.patch("uaclient.livepatch.on_supported_kernel", return_value=None) +@mock.patch("uaclient.files.notices.NoticesManager.remove") @mock.patch("uaclient.system.should_reboot", return_value=False) class TestStatus: def check_beta(self, cls, show_all, uacfg=None, status=""): @@ -298,7 +319,8 @@ self, m_get_available_resources, _m_should_reboot, - m_remove_notice, + _m_remove_notice, + m_on_supported_kernel, ros_desc, esm_desc, show_all, @@ -311,11 +333,13 @@ "available": "yes", "name": "esm-infra", "description": esm_desc, + "description_override": None, }, { "available": "no", "name": "ros", "description": ros_desc, + "description_override": None, }, ] else: @@ -324,6 +348,7 @@ "available": "yes", "name": "esm-infra", "description": esm_desc, + "description_override": None, } ] cfg = FakeConfig() @@ -340,17 +365,6 @@ m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_all=show_all) - expected_calls = [ - mock.call( - "", - messages.ENABLE_REBOOT_REQUIRED_TMPL.format( - operation="fix operation" - ), - ) - ] - - assert expected_calls == m_remove_notice.call_args_list - @pytest.mark.parametrize( "features_override", ((None), ({"allow_beta": True})) ) @@ -394,6 +408,7 @@ _m_livepatch_status, _m_should_reboot, _m_remove_notice, + m_on_supported_kernel, avail_res, entitled_res, uf_entitled, @@ -493,6 +508,7 @@ "description_override": None, "available": mock.ANY, "blocked_by": [], + "warning": None, } for cls in ENTITLEMENT_CLASSES ] @@ -546,36 +562,46 @@ m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_all=True) + @mock.patch("uaclient.util.we_are_currently_root") @mock.patch("uaclient.status.get_available_resources") def test_nonroot_unattached_is_same_as_unattached_root( self, m_get_available_resources, + m_we_are_currently_root, _m_should_reboot, _m_remove_notice, + m_on_supported_kernel, FakeConfig, ): m_get_available_resources.return_value = [ {"name": "esm-infra", "available": True} ] - cfg = FakeConfig(root_mode=False) + m_we_are_currently_root.return_value = False + cfg = FakeConfig() nonroot_status = status.status(cfg=cfg) - cfg = FakeConfig(root_mode=True) + m_we_are_currently_root.return_value = True + cfg = FakeConfig() root_unattached_status = status.status(cfg=cfg) assert root_unattached_status == nonroot_status + @mock.patch("uaclient.util.we_are_currently_root") @mock.patch("uaclient.status.get_available_resources") def test_root_and_non_root_are_same_attached( self, m_get_available_resources, + m_we_are_currently_root, _m_should_reboot, _m_remove_notice, + m_on_supported_kernel, FakeConfig, ): + m_we_are_currently_root.return_value = True root_cfg = FakeConfig.for_attached_machine() root_status = status.status(cfg=root_cfg) - normal_cfg = FakeConfig.for_attached_machine(root_mode=False) + m_we_are_currently_root.return_value = False + normal_cfg = FakeConfig.for_attached_machine() normal_status = status.status(cfg=normal_cfg) assert normal_status == root_status @@ -585,6 +611,7 @@ _m_get_available_resources, _m_should_reboot, m_remove_notice, + m_on_supported_kernel, FakeConfig, ): cfg = FakeConfig() @@ -594,17 +621,6 @@ os.lstat(cfg.data_path("status-cache")).st_mode ) - expected_calls = [ - mock.call( - "", - messages.ENABLE_REBOOT_REQUIRED_TMPL.format( - operation="fix operation" - ), - ) - ] - - assert expected_calls == m_remove_notice.call_args_list - @pytest.mark.parametrize("show_all", (True, False)) @pytest.mark.parametrize( "features_override", ((None), ({"allow_beta": False})) @@ -648,7 +664,8 @@ _m_livepatch_status, _m_fips_status, _m_should_reboot, - m_remove_notice, + _m_remove_notice, + m_on_supported_kernel, all_resources_available, entitlements, features_override, @@ -698,6 +715,7 @@ account_name="accountname", machine_token=token, ) + mock_notice = NoticesManager() if features_override: cfg.override_features(features_override) if not entitlements: @@ -762,6 +780,7 @@ "description_override": None, "available": mock.ANY, "blocked_by": [], + "warning": None, } ) with mock.patch( @@ -770,34 +789,30 @@ m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_all=show_all) - assert len(ENTITLEMENT_CLASSES) - 2 == m_repo_uf_status.call_count - assert 1 == m_livepatch_uf_status.call_count + assert len(ENTITLEMENT_CLASSES) - 2 == m_repo_uf_status.call_count + assert 1 == m_livepatch_uf_status.call_count expected_calls = [ - mock.call( - "", - messages.AUTO_ATTACH_RETRY_NOTICE_PREFIX, - ), - mock.call( - "", - messages.ENABLE_REBOOT_REQUIRED_TMPL.format( - operation="fix operation" - ), - ), + mock.call(Notice.AUTO_ATTACH_RETRY_FULL_NOTICE), + mock.call(Notice.AUTO_ATTACH_RETRY_TOTAL_FAILURE), ] - assert expected_calls == m_remove_notice.call_args_list + assert expected_calls == mock_notice.remove.call_args_list @pytest.mark.usefixtures("all_resources_available") + @mock.patch("uaclient.util.we_are_currently_root") @mock.patch("uaclient.status.get_available_resources") def test_expires_handled_appropriately( self, _m_get_available_resources, + m_we_are_currently_root, _m_should_reboot, _m_remove_notice, + m_on_supported_kernel, all_resources_available, FakeConfig, ): + m_we_are_currently_root.return_value = True token = { "availableResources": all_resources_available, "machineTokenInfo": { @@ -830,10 +845,10 @@ # Test that the read from the status cache work properly for non-root # users + m_we_are_currently_root.return_value = False cfg = FakeConfig.for_attached_machine( account_name="accountname", machine_token=token, - root_mode=False, ) assert expected_dt == status.status(cfg=cfg)["expires"] @@ -843,6 +858,7 @@ _m_get_available_resources, _m_should_reboot, m_remove_notice, + m_on_supported_kernel, FakeConfig, ): diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_system.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_system.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_system.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_system.py 2023-04-05 15:14:00.000000000 +0000 @@ -133,92 +133,6 @@ assert system.get_kernel_info.__wrapped__() == expected -class TestGetLscpuArch: - @pytest.mark.parametrize( - "stdout, expected", - ( - ( - """\ -Architecture: x86_64 - CPU op-mode(s): 32-bit, 64-bit - Address sizes: 39 bits physical, 48 bits virtual - Byte Order: Little Endian -CPU(s): 8 - On-line CPU(s) list: 0-7 -""", - "x86_64", - ), - ( - """\ -Architecture: aarch64 -""", - "aarch64", - ), - ( - """\ -CPU(s): 8 - On-line CPU(s) list: 0-7 -Architecture: x86_64 - CPU op-mode(s): 32-bit, 64-bit - Address sizes: 39 bits physical, 48 bits virtual - Byte Order: Little Endian -""", - "x86_64", - ), - ( - """\ - CPU(s): 8 - On-line CPU(s) list: 0-7 - - Architecture: x86_64 - CPU op-mode(s): 32-bit, 64-bit - Address sizes: 39 bits physical, 48 bits virtual - Byte Order: Little Endian -""", - "x86_64", - ), - ( - """Architecture: x86_64""", - "x86_64", - ), - ( - """Architecture:x86_64""", - "x86_64", - ), - ( - """ Architecture: x86_64 """, - "x86_64", - ), - ), - ) - @mock.patch("uaclient.system.subp") - def test_get_lscpu_arch_success(self, m_subp, stdout, expected): - m_subp.return_value = (stdout, "") - assert system.get_lscpu_arch.__wrapped__() == expected - assert m_subp.call_args_list == [mock.call(["lscpu"])] - - @pytest.mark.parametrize( - "stdout", - ( - "Architecture x86_64", - "Architectur: x86_64", - "rchitecture: x86_64", - "architecture: x86_64", - ": x86_64", - "Architecture:", - "Architecture: ", - "Architecture: ", - "", - ), - ) - @mock.patch("uaclient.system.subp") - def test_get_lscpu_arch_error(self, m_subp, stdout): - m_subp.return_value = (stdout, "") - with pytest.raises(exceptions.UserFacingError) as e: - system.get_lscpu_arch.__wrapped__() - assert e.msg_code == messages.LSCPU_ARCH_PARSE_ERROR.name - - class TestGetDpkgArch: @pytest.mark.parametrize( "stdout, expected", @@ -620,7 +534,7 @@ system.get_platform_info.__wrapped__() @pytest.mark.parametrize( - "os_release, arch, kernel, expected", + "os_release,arch,kernel,virt,expected", [ ( { @@ -629,6 +543,7 @@ }, "arm64", "kernel-ver1", + "lxd", { "arch": "arm64", "distribution": "Ubuntu", @@ -637,6 +552,7 @@ "series": "xenial", "type": "Linux", "version": "16.04 LTS (Xenial Xerus)", + "virt": "lxd", }, ), ( @@ -646,6 +562,7 @@ }, "amd64", "kernel-ver2", + "none", { "arch": "amd64", "distribution": "Ubuntu", @@ -654,6 +571,7 @@ "series": "bionic", "type": "Linux", "version": "18.04 LTS (Bionic Beaver)", + "virt": "none", }, ), ( @@ -663,6 +581,7 @@ }, "arm64", "kernel-ver3", + "qemu", { "arch": "arm64", "distribution": "Ubuntu", @@ -671,6 +590,7 @@ "series": "jammy", "type": "Linux", "version": "22.04 LTS (Jammy Jellyfish)", + "virt": "qemu", }, ), ( @@ -680,6 +600,7 @@ }, "amd64", "kernel-ver4", + "wsl", { "arch": "amd64", "distribution": "Ubuntu", @@ -688,6 +609,7 @@ "series": "kinetic", "type": "Linux", "version": "22.10 LTS (Kinetic Kudu)", + "virt": "wsl", }, ), ( @@ -698,6 +620,7 @@ }, "amd64", "kernel-ver4", + "lxd", { "arch": "amd64", "distribution": "Ubuntu", @@ -706,6 +629,7 @@ "series": "jammy", "type": "Linux", "version": "22.04 LTS", + "virt": "lxd", }, ), ( @@ -717,6 +641,7 @@ }, "amd64", "kernel-ver4", + "lxd", { "arch": "amd64", "distribution": "Ubuntu", @@ -725,6 +650,7 @@ "series": "jammy", "type": "Linux", "version": "CORRUPTED", + "virt": "lxd", }, ), ], @@ -732,19 +658,23 @@ @mock.patch("uaclient.system.get_kernel_info") @mock.patch("uaclient.system.get_dpkg_arch") @mock.patch("uaclient.system.parse_os_release") + @mock.patch("uaclient.system.get_virt_type") def test_get_platform_info_with_version( self, + m_get_virt_type, m_parse_os_release, m_get_dpkg_arch, m_get_kernel_info, os_release, arch, kernel, + virt, expected, ): m_parse_os_release.return_value = os_release m_get_dpkg_arch.return_value = arch m_get_kernel_info.return_value = mock.MagicMock(uname_release=kernel) + m_get_virt_type.return_value = virt assert expected == system.get_platform_info.__wrapped__() @@ -1025,3 +955,35 @@ assert log in logs else: assert log not in logs + + +class TestGetSystemdJobState: + @pytest.mark.parametrize( + "systemd_return,expected_return", + ( + ("active", True), + ("inactive", False), + ("", False), + (None, False), + ("test", False), + ), + ) + @mock.patch("uaclient.system.subp") + def test_get_systemd_job_state( + self, + subp, + systemd_return, + expected_return, + ): + subp.return_value = (systemd_return, "") + assert expected_return == system.get_systemd_job_state(job_name="test") + + @mock.patch("uaclient.system.subp") + def test_systemd_job_state_non_zero( + self, + subp, + ): + subp.side_effect = exceptions.ProcessExecutionError( + cmd="test", exit_code=3, stdout="inactive", stderr="" + ) + assert False is system.get_systemd_job_state(job_name="test") diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_ua_timer.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_ua_timer.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_ua_timer.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_ua_timer.py 2023-04-06 13:50:05.000000000 +0000 @@ -107,12 +107,14 @@ m_job_status.next_run = next_run fake_file.read.return_value = mock.MagicMock( - metering=m_job_status, update_messaging=None + metering=m_job_status, + update_messaging=None, + update_contract_info=None, ) expected_next_run = now + datetime.timedelta(seconds=43200) m_job_func = mock.Mock() - m_jobs = TimedJob("metering", m_job_func, 43200) + m_jobs = TimedJob("fake_job", m_job_func, 43200) with mock.patch("lib.timer.metering_job", m_jobs): with mock.patch.object(timer, "timer_jobs_state_file", fake_file): @@ -134,7 +136,9 @@ next_run=now + datetime.timedelta(seconds=14400), last_run=None ) fake_file.read.return_value = mock.MagicMock( - metering=m_job_status, update_messaging=None + metering=m_job_status, + update_messaging=None, + update_contract_info=None, ) m_job_func = mock.Mock() diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_util.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_util.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/tests/test_util.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/tests/test_util.py 2023-04-05 15:14:00.000000000 +0000 @@ -186,9 +186,12 @@ assert data == req.data +@mock.patch("uaclient.util.we_are_currently_root", return_value=False) class TestDisableLogToConsole: @pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True) - def test_no_error_if_console_handler_not_found(self, caplog_text): + def test_no_error_if_console_handler_not_found( + self, m_we_are_currently_root, caplog_text + ): with mock.patch("uaclient.util.logging.getLogger") as m_getlogger: m_getlogger.return_value.handlers = [] with util.disable_log_to_console(): @@ -198,7 +201,7 @@ @pytest.mark.parametrize("disable_log", (True, False)) def test_disable_log_to_console( - self, logging_sandbox, capsys, disable_log + self, m_we_are_currently_root, logging_sandbox, capsys, disable_log ): # This test is parameterised so that we are sure that the context # manager is suppressing the output, not some other config change @@ -223,7 +226,7 @@ assert "test info" in combined_output def test_disable_log_to_console_does_nothing_at_debug_level( - self, logging_sandbox, capsys + self, m_we_are_currently_root, logging_sandbox, capsys ): cli.setup_logging(logging.DEBUG, logging.DEBUG) diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/util.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/util.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/util.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/util.py 2023-04-05 15:14:00.000000000 +0000 @@ -182,7 +182,7 @@ + str(new_value) + "'" ) - logging.debug(redact_sensitive_logs(log)) + logging.debug(log) deltas[key] = new_value for key, value in new_dict.items(): if key not in orig_dict: @@ -301,7 +301,7 @@ headers: Dict[str, str] = {}, method: Optional[str] = None, timeout: Optional[int] = None, - potentially_sensitive: bool = True, + log_response_body: bool = True, ) -> Tuple[Any, Union[HTTPMessage, Mapping[str, str]]]: if data and not method: method = "POST" @@ -310,13 +310,11 @@ ["'{}': '{}'".format(k, headers[k]) for k in sorted(headers)] ) logging.debug( - redact_sensitive_logs( - "URL [{}]: {}, headers: {{{}}}, data: {}".format( - method or "GET", - url, - sorted_header_str, - data.decode("utf-8") if data else None, - ) + "URL [{}]: {}, headers: {{{}}}, data: {}".format( + method or "GET", + url, + sorted_header_str, + data.decode("utf-8") if data else None, ) ) http_error_found = False @@ -329,16 +327,17 @@ setattr(resp, "body", resp.read().decode("utf-8")) content = resp.body if "application/json" in str(resp.headers.get("Content-type", "")): - content = json.loads(content) + content = json.loads(content, cls=DatetimeAwareJSONDecoder) sorted_header_str = ", ".join( ["'{}': '{}'".format(k, resp.headers[k]) for k in sorted(resp.headers)] ) - debug_msg = "URL [{}] response: {}, headers: {{{}}}, data: {}".format( - method or "GET", url, sorted_header_str, content + debug_msg = "URL [{}] response: {}, headers: {{{}}}".format( + method or "GET", url, sorted_header_str ) - if potentially_sensitive: - # For large responses, this is very slow (several minutes) - debug_msg = redact_sensitive_logs(debug_msg) + if log_response_body: + # Due to implicit logging redaction, large responses might take longer + debug_msg += ", data: {}".format(content) + logging.debug(debug_msg) if http_error_found: raise resp @@ -609,15 +608,26 @@ base_dict[key] = value +ARCH_ALIASES = { + "x86_64": "amd64", + "i686": "i386", + "ppc64le": "ppc64el", + "aarch64": "arm64", + "armv7l": "armhf", +} + + +def standardize_arch_name(arch: str) -> str: + arch_lower = arch.lower() + return ARCH_ALIASES.get(arch_lower, arch_lower) + + def deduplicate_arches(arches: List[str]) -> List[str]: deduplicated_arches = set() - arch_aliases = { - "x86_64": "amd64", - "i686": "i386", - "ppc64le": "ppc64el", - "aarch64": "arm64", - "armv7l": "armhf", - } for arch in arches: - deduplicated_arches.add(arch_aliases.get(arch.lower(), arch)) + deduplicated_arches.add(standardize_arch_name(arch)) return sorted(list(deduplicated_arches)) + + +def we_are_currently_root() -> bool: + return os.getuid() == 0 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/version.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/version.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/version.py 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/version.py 2023-04-06 13:50:05.000000000 +0000 @@ -15,7 +15,7 @@ from uaclient.exceptions import ProcessExecutionError from uaclient.system import subp -__VERSION__ = "27.13.6" +__VERSION__ = "27.14.4" PACKAGED_VERSION = "@@PACKAGED_VERSION@@" CANDIDATE_REGEX = r"Candidate: (?P.*?)\n" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/yaml.py ubuntu-advantage-tools-27.14.4~16.04/uaclient/yaml.py --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient/yaml.py 1970-01-01 00:00:00.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient/yaml.py 2023-04-05 15:14:00.000000000 +0000 @@ -0,0 +1,29 @@ +import logging +import sys + +from uaclient.messages import BROKEN_YAML_MODULE, MISSING_YAML_MODULE + +try: + import yaml +except ImportError: + logging.error(MISSING_YAML_MODULE.msg) + sys.exit(1) + + +def safe_load(stream): + try: + return yaml.safe_load(stream) + except AttributeError: + logging.error(BROKEN_YAML_MODULE.format(path=yaml.__path__).msg) + sys.exit(1) + + +def safe_dump(data, stream=None, **kwargs): + try: + return yaml.safe_dump(data, stream, **kwargs) + except AttributeError: + logging.error(BROKEN_YAML_MODULE.format(path=yaml.__path__).msg) + sys.exit(1) + + +parser = yaml.parser diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient.conf ubuntu-advantage-tools-27.14.4~16.04/uaclient.conf --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient.conf 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient.conf 2023-04-05 15:14:00.000000000 +0000 @@ -1,18 +1,2 @@ -# Ubuntu Pro Client config file. -# If you modify this file, run "pro refresh config" to ensure changes are -# picked up by Ubuntu Pro Client. - contract_url: https://contracts.canonical.com -data_dir: /var/lib/ubuntu-advantage -log_file: /var/log/ubuntu-advantage.log log_level: debug -security_url: https://ubuntu.com/security -timer_log_file: /var/log/ubuntu-advantage-timer.log -daemon_log_file: /var/log/ubuntu-advantage-daemon.log -ua_config: - apt_http_proxy: null - apt_https_proxy: null - http_proxy: null - https_proxy: null - update_messaging_timer: 21600 - metering_timer: 14400 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/uaclient-devel.conf ubuntu-advantage-tools-27.14.4~16.04/uaclient-devel.conf --- ubuntu-advantage-tools-27.13.6~16.04.1/uaclient-devel.conf 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/uaclient-devel.conf 2023-04-05 15:14:00.000000000 +0000 @@ -6,10 +6,3 @@ security_url: https://ubuntu.com/security timer_log_file: ubuntu-advantage-timer-devel.log daemon_log_file: ubuntu-advantage-daemon-devel.log -ua_config: - apt_http_proxy: null - apt_https_proxy: null - http_proxy: null - https_proxy: null - update_messaging_timer: 21600 - metering_timer: 0 diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/ubuntu-advantage.1 ubuntu-advantage-tools-27.14.4~16.04/ubuntu-advantage.1 --- ubuntu-advantage-tools-27.13.6~16.04.1/ubuntu-advantage.1 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/ubuntu-advantage.1 2023-04-05 15:14:00.000000000 +0000 @@ -62,11 +62,15 @@ also disables all enabled services that can be. .TP -.BR "disable" " [cc-eal|cis|esm|fips|fips-updates|livepatch|ros|ros-updates]" +.BR "disable" " [cc-eal|cis|esm-apps|esm-infra|fips|fips-updates|" + livepatch|realtime-kernel|ros|ros-updates] + Disable this machine's access to an Ubuntu Pro service. .TP -.BR "enable" " [cc-eal|cis|esm|fips|fips-updates|livepatch|ros|ros-updates]" +.BR "enable" " [cc-eal|cis|esm-apps|esm-infra|fips|fips-updates|" +livepatch|realtime-kernel|ros|ros-updates] + Activate and configure this machine's access to an Ubuntu Pro service. @@ -203,7 +207,7 @@ The log file for the Ubuntu Pro daemon .P -\fBThe following options must be nested under the "ua_config" key:\fP +\fBThe following options are set using the `pro config set` subcommand:\fP .TP .B diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/update-motd.d/88-esm-announce ubuntu-advantage-tools-27.14.4~16.04/update-motd.d/88-esm-announce --- ubuntu-advantage-tools-27.13.6~16.04.1/update-motd.d/88-esm-announce 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/update-motd.d/88-esm-announce 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ -#!/bin/sh -stamp="/var/lib/ubuntu-advantage/messages/motd-esm-announce" - -[ ! -r "$stamp" ] || cat "$stamp" diff -Nru ubuntu-advantage-tools-27.13.6~16.04.1/update-motd.d/91-contract-ua-esm-status ubuntu-advantage-tools-27.14.4~16.04/update-motd.d/91-contract-ua-esm-status --- ubuntu-advantage-tools-27.13.6~16.04.1/update-motd.d/91-contract-ua-esm-status 2023-02-28 19:17:34.000000000 +0000 +++ ubuntu-advantage-tools-27.14.4~16.04/update-motd.d/91-contract-ua-esm-status 2023-04-05 15:14:00.000000000 +0000 @@ -1,7 +1,7 @@ #!/bin/sh -esm_stamp="/var/lib/ubuntu-advantage/messages/motd-esm-service-status" +contract_status_stamp="/var/lib/ubuntu-advantage/messages/motd-contract-status" -[ ! -r "$esm_stamp" ] || cat "$esm_stamp" +[ ! -r "$contract_status_stamp" ] || cat "$contract_status_stamp" auto_attach_stamp="/var/lib/ubuntu-advantage/messages/motd-auto-attach-status"