From 8e3751f6472a1a669bbcb86c2189af76fb9a0a0e Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 13 Feb 2024 11:51:48 +0100 Subject: [PATCH 1/4] lsb-release: split the function so it can be tested with more ease Signed-off-by: Olivier Gayot --- subiquitycore/lsb_release.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/subiquitycore/lsb_release.py b/subiquitycore/lsb_release.py index 9e3ac136..7bf91728 100644 --- a/subiquitycore/lsb_release.py +++ b/subiquitycore/lsb_release.py @@ -6,15 +6,7 @@ LSB_RELEASE_FILE = "/etc/lsb-release" LSB_RELEASE_EXAMPLE = "examples/lsb-release-focal" -def lsb_release(path=None, dry_run: bool = False) -> Dict[str, str]: - """return a dictionary of values from /etc/lsb-release. - keys are lower case with DISTRIB_ prefix removed.""" - if dry_run and path is not None: - raise ValueError("Both dry_run and path are specified.") - - if path is None: - path = LSB_RELEASE_EXAMPLE if dry_run else LSB_RELEASE_FILE - +def lsb_release_from_path(path: str) -> Dict[str, str]: ret: Dict[str, str] = {} try: with open(path, "r") as fp: @@ -30,5 +22,17 @@ def lsb_release(path=None, dry_run: bool = False) -> Dict[str, str]: return ret +def lsb_release(path=None, dry_run: bool = False) -> Dict[str, str]: + """return a dictionary of values from /etc/lsb-release. + keys are lower case with DISTRIB_ prefix removed.""" + if dry_run and path is not None: + raise ValueError("Both dry_run and path are specified.") + + if path is None: + path = LSB_RELEASE_EXAMPLE if dry_run else LSB_RELEASE_FILE + + return lsb_release_from_path(path) + + if __name__ == "__main__": print(lsb_release()) From 7a5ff9e76a6b31fb8202c2070f8a4eace10583d7 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 13 Feb 2024 11:55:55 +0100 Subject: [PATCH 2/4] examples: add lsb-release for jammy and noble Signed-off-by: Olivier Gayot --- examples/lsb-release-jammy | 4 ++++ examples/lsb-release-noble | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 examples/lsb-release-jammy create mode 100644 examples/lsb-release-noble diff --git a/examples/lsb-release-jammy b/examples/lsb-release-jammy new file mode 100644 index 00000000..87045df4 --- /dev/null +++ b/examples/lsb-release-jammy @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=22.04 +DISTRIB_CODENAME=jammy +DISTRIB_DESCRIPTION="Ubuntu 22.04 LTS" diff --git a/examples/lsb-release-noble b/examples/lsb-release-noble new file mode 100644 index 00000000..bfd9cff2 --- /dev/null +++ b/examples/lsb-release-noble @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=24.04 +DISTRIB_CODENAME=noble +DISTRIB_DESCRIPTION="Ubuntu Noble Numbat (development branch)" From a42ea0a6850a96873e727cdfd5ac554d92d72965 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 13 Feb 2024 11:03:52 +0100 Subject: [PATCH 3/4] ubuntu-pro: return number of packages and EOL ESM in /ubuntu_pro/info We now rely on distro-info to find out the EOL ESM date on LTS releases. This information is meant to be shown on the Ubuntu Pro screens ; instead of hardcoded values. Signed-off-by: Olivier Gayot --- apt-deps.txt | 1 + snapcraft.yaml | 1 + subiquity/common/apidef.py | 5 +++ subiquity/common/types.py | 7 ++++ .../controllers/tests/test_ubuntu_pro.py | 36 +++++++++++++++++-- subiquity/server/controllers/ubuntu_pro.py | 28 +++++++++++++++ 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/apt-deps.txt b/apt-deps.txt index c752b41a..8a3d0686 100644 --- a/apt-deps.txt +++ b/apt-deps.txt @@ -27,6 +27,7 @@ python3-bson python3-coverage python3-debian python3-dev +python3-distro-info python3-distutils-extra python3-flake8 python3-gi diff --git a/snapcraft.yaml b/snapcraft.yaml index ba6dc260..8433475a 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -132,6 +132,7 @@ parts: - python3-attr - python3-bson - python3-debian + - python3-distro-info - python3-jsonschema - python3-minimal - python3-oauthlib diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 93efdeef..7a7b10e1 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -67,6 +67,7 @@ from subiquity.common.types import ( StorageResponseV2, TimeZoneInfo, UbuntuProCheckTokenAnswer, + UbuntuProGeneralInfo, UbuntuProInfo, UbuntuProResponse, UPCSInitiateResponse, @@ -513,6 +514,10 @@ class API: def POST() -> None: ... + class info: + def GET() -> UbuntuProGeneralInfo: + ... + class identity: def GET() -> IdentityData: ... diff --git a/subiquity/common/types.py b/subiquity/common/types.py index c50f96cf..2c656753 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -718,6 +718,13 @@ class UbuntuProCheckTokenStatus(enum.Enum): UNKNOWN_ERROR = enum.auto() +@attr.s(auto_attribs=True) +class UbuntuProGeneralInfo: + eol_esm_year: Optional[int] + universe_packages: int + main_packages: int + + @attr.s(auto_attribs=True) class UPCSInitiateResponse: """Response to Ubuntu Pro contract selection initiate request.""" diff --git a/subiquity/server/controllers/tests/test_ubuntu_pro.py b/subiquity/server/controllers/tests/test_ubuntu_pro.py index c2787a53..90f19c6d 100644 --- a/subiquity/server/controllers/tests/test_ubuntu_pro.py +++ b/subiquity/server/controllers/tests/test_ubuntu_pro.py @@ -13,17 +13,20 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import unittest +from unittest import mock import jsonschema from jsonschema.validators import validator_for from subiquity.server.controllers.ubuntu_pro import UbuntuProController from subiquity.server.dryrun import DRConfig +from subiquitycore.lsb_release import lsb_release_from_path +from subiquitycore.tests import SubiTestCase from subiquitycore.tests.mocks import make_app +from subiquitycore.tests.parameterized import parameterized -class TestUbuntuProController(unittest.TestCase): +class TestUbuntuProController(SubiTestCase): def setUp(self): app = make_app() app.dr_cfg = DRConfig() @@ -45,3 +48,32 @@ class TestUbuntuProController(unittest.TestCase): ) JsonValidator.check_schema(UbuntuProController.autoinstall_schema) + + @parameterized.expand( + [ + ("focal", 23000, 2300, 2030), + ("impish", 23000, 2300, None), + ("jammy", 23000, 2300, 2032), + ("noble", 23000, 2300, 2034), + ] + ) + async def test_info_GET__series( + self, series: str, universe_pkgs: int, main_pkgs: int, esm_eol_year: int | None + ): + def fake_lsb_release(*args, **kwargs): + return lsb_release_from_path(f"examples/lsb-release-{series}") + + with mock.patch( + "subiquity.server.controllers.ubuntu_pro.lsb_release", + wraps=fake_lsb_release, + ) as m_lsb_release: + info = await self.controller.info_GET() + + m_lsb_release.assert_called_once() + + self.assertEqual(universe_pkgs, info.universe_packages) + self.assertEqual(main_pkgs, info.main_packages) + if esm_eol_year is not None: + self.assertEqual(esm_eol_year, info.eol_esm_year) + else: + self.assertIsNone(info.eol_esm_year) diff --git a/subiquity/server/controllers/ubuntu_pro.py b/subiquity/server/controllers/ubuntu_pro.py index 238b9cd6..f7d52096 100644 --- a/subiquity/server/controllers/ubuntu_pro.py +++ b/subiquity/server/controllers/ubuntu_pro.py @@ -19,10 +19,13 @@ import logging import os from typing import Optional +import distro_info + from subiquity.common.apidef import API from subiquity.common.types import ( UbuntuProCheckTokenAnswer, UbuntuProCheckTokenStatus, + UbuntuProGeneralInfo, UbuntuProInfo, UbuntuProResponse, UPCSInitiateResponse, @@ -40,6 +43,7 @@ from subiquity.server.ubuntu_advantage import ( UAInterface, UAInterfaceStrategy, ) +from subiquitycore.lsb_release import lsb_release log = logging.getLogger("subiquity.server.controllers.ubuntu_pro") @@ -199,3 +203,27 @@ class UbuntuProController(SubiquityController): self.cs.cancel() self.cs = None + + async def info_GET(self) -> UbuntuProGeneralInfo: + # Currently the number of packages is hard-coded. + main_packages = 2300 + universe_packages = 23000 + eol_esm_year: Optional[int] = None + + series = lsb_release(dry_run=self.app.opts.dry_run)["codename"] + + for release in distro_info.UbuntuDistroInfo()._releases: + if release.series == series: + try: + eol_esm_year = release.eol_esm.year + except AttributeError: + log.warning("series %s does not have an ESM EOL date", series) + break + else: + log.warning("could not find distro info for %s", series) + + return UbuntuProGeneralInfo( + eol_esm_year=eol_esm_year, + main_packages=main_packages, + universe_packages=universe_packages, + ) From b5bbb76900f35139e39ce3e648a0bdbb2cbae525 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 13 Feb 2024 12:08:09 +0100 Subject: [PATCH 4/4] ubuntu-pro: consume general info and show on the UI Signed-off-by: Olivier Gayot --- .../controllers/tests/test_ubuntu_pro.py | 8 +++---- subiquity/client/controllers/ubuntu_pro.py | 2 ++ subiquity/ui/views/ubuntu_pro.py | 23 +++++++++++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/subiquity/client/controllers/tests/test_ubuntu_pro.py b/subiquity/client/controllers/tests/test_ubuntu_pro.py index 2fbd8b5d..d120b2a4 100644 --- a/subiquity/client/controllers/tests/test_ubuntu_pro.py +++ b/subiquity/client/controllers/tests/test_ubuntu_pro.py @@ -14,7 +14,7 @@ # along with this program. If not, see . import unittest -from unittest.mock import AsyncMock, patch +from unittest.mock import ANY, AsyncMock, patch from subiquity.client.controllers.ubuntu_pro import UbuntuProController from subiquity.common.types import UbuntuProResponse @@ -43,7 +43,7 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase): await ctrler.make_ui() view.assert_called_once_with( - ctrler, token="", has_network=False, pre_release=False + ctrler, token="", has_network=False, pre_release=False, info=ANY ) @patch("subiquity.client.controllers.ubuntu_pro.UbuntuProView") @@ -76,7 +76,7 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase): view.assert_called_once() view.assert_called_once_with( - ctrler, token="", has_network=False, pre_release=True + ctrler, token="", has_network=False, pre_release=True, info=ANY ) @patch("subiquity.client.controllers.ubuntu_pro.UbuntuProView") @@ -98,5 +98,5 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase): view.assert_called_once() view.assert_called_once_with( - ctrler, token="", has_network=False, pre_release=True + ctrler, token="", has_network=False, pre_release=True, info=ANY ) diff --git a/subiquity/client/controllers/ubuntu_pro.py b/subiquity/client/controllers/ubuntu_pro.py index 36ae05fe..378bdaeb 100644 --- a/subiquity/client/controllers/ubuntu_pro.py +++ b/subiquity/client/controllers/ubuntu_pro.py @@ -77,11 +77,13 @@ class UbuntuProController(SubiquityTuiController): raise Skip("Not running LTS version") ubuntu_pro_info: UbuntuProResponse = await self.endpoint.GET() + general_info = await self.endpoint.info.GET() return UbuntuProView( self, token=ubuntu_pro_info.token, has_network=ubuntu_pro_info.has_network, pre_release=pre_release, + info=general_info, ) async def run_answers(self) -> None: diff --git a/subiquity/ui/views/ubuntu_pro.py b/subiquity/ui/views/ubuntu_pro.py index 6ae2f84d..3fb602b5 100644 --- a/subiquity/ui/views/ubuntu_pro.py +++ b/subiquity/ui/views/ubuntu_pro.py @@ -21,7 +21,11 @@ from typing import Callable, List from urwid import Columns, LineBox, Text, Widget, connect_signal -from subiquity.common.types import UbuntuProCheckTokenStatus, UbuntuProSubscription +from subiquity.common.types import ( + UbuntuProCheckTokenStatus, + UbuntuProGeneralInfo, + UbuntuProSubscription, +) from subiquitycore.ui.buttons import back_btn, cancel_btn, done_btn, menu_btn, ok_btn from subiquitycore.ui.container import ListBox, Pile, WidgetWrap from subiquitycore.ui.form import ( @@ -270,12 +274,21 @@ class UbuntuProView(BaseView): title = _("Upgrade to Ubuntu Pro") subscription_done_label = _("Continue") - def __init__(self, controller, token: str, has_network: bool, *, pre_release=False): + def __init__( + self, + controller, + token: str, + has_network: bool, + *, + pre_release=False, + info: UbuntuProGeneralInfo, + ): """Initialize the view with the default value for the token.""" self.controller = controller self.has_network = has_network self.pre_release = pre_release + self.info = info if self.has_network: self.upgrade_yes_no_form = UpgradeYesNoForm( @@ -380,7 +393,7 @@ class UbuntuProView(BaseView): | [ Back ] | +---------------------------------------------------------+ """ - security_updates_until = 2032 + security_updates_until = self.info.eol_esm_year or "20xx" excerpt = _( "Upgrade this machine to Ubuntu Pro for security updates" @@ -815,8 +828,8 @@ class AboutProWidget(Stretchy): " patches covering a wider range of packages." ) - universe_packages = 23000 - main_packages = 2300 + universe_packages = self.parent.info.universe_packages + main_packages = self.parent.info.main_packages services = [ _(