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/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)"
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/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/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,
+ )
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 = [
_(
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())