Merge pull request #1910 from ogayot/pro-dynamic-eol

Do not use hard-coded EOL year for ESM updates on the ubuntu-pro screen
This commit is contained in:
Olivier Gayot 2024-02-16 09:46:08 +01:00 committed by GitHub
commit c8501d81db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 121 additions and 20 deletions

View File

@ -27,6 +27,7 @@ python3-bson
python3-coverage python3-coverage
python3-debian python3-debian
python3-dev python3-dev
python3-distro-info
python3-distutils-extra python3-distutils-extra
python3-flake8 python3-flake8
python3-gi python3-gi

View File

@ -0,0 +1,4 @@
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04 LTS"

View File

@ -0,0 +1,4 @@
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu Noble Numbat (development branch)"

View File

@ -132,6 +132,7 @@ parts:
- python3-attr - python3-attr
- python3-bson - python3-bson
- python3-debian - python3-debian
- python3-distro-info
- python3-jsonschema - python3-jsonschema
- python3-minimal - python3-minimal
- python3-oauthlib - python3-oauthlib

View File

@ -14,7 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest 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.client.controllers.ubuntu_pro import UbuntuProController
from subiquity.common.types import UbuntuProResponse from subiquity.common.types import UbuntuProResponse
@ -43,7 +43,7 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase):
await ctrler.make_ui() await ctrler.make_ui()
view.assert_called_once_with( 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") @patch("subiquity.client.controllers.ubuntu_pro.UbuntuProView")
@ -76,7 +76,7 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase):
view.assert_called_once() view.assert_called_once()
view.assert_called_once_with( 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") @patch("subiquity.client.controllers.ubuntu_pro.UbuntuProView")
@ -98,5 +98,5 @@ class TestUbuntuProController(unittest.IsolatedAsyncioTestCase):
view.assert_called_once() view.assert_called_once()
view.assert_called_once_with( view.assert_called_once_with(
ctrler, token="", has_network=False, pre_release=True ctrler, token="", has_network=False, pre_release=True, info=ANY
) )

View File

@ -77,11 +77,13 @@ class UbuntuProController(SubiquityTuiController):
raise Skip("Not running LTS version") raise Skip("Not running LTS version")
ubuntu_pro_info: UbuntuProResponse = await self.endpoint.GET() ubuntu_pro_info: UbuntuProResponse = await self.endpoint.GET()
general_info = await self.endpoint.info.GET()
return UbuntuProView( return UbuntuProView(
self, self,
token=ubuntu_pro_info.token, token=ubuntu_pro_info.token,
has_network=ubuntu_pro_info.has_network, has_network=ubuntu_pro_info.has_network,
pre_release=pre_release, pre_release=pre_release,
info=general_info,
) )
async def run_answers(self) -> None: async def run_answers(self) -> None:

View File

@ -67,6 +67,7 @@ from subiquity.common.types import (
StorageResponseV2, StorageResponseV2,
TimeZoneInfo, TimeZoneInfo,
UbuntuProCheckTokenAnswer, UbuntuProCheckTokenAnswer,
UbuntuProGeneralInfo,
UbuntuProInfo, UbuntuProInfo,
UbuntuProResponse, UbuntuProResponse,
UPCSInitiateResponse, UPCSInitiateResponse,
@ -513,6 +514,10 @@ class API:
def POST() -> None: def POST() -> None:
... ...
class info:
def GET() -> UbuntuProGeneralInfo:
...
class identity: class identity:
def GET() -> IdentityData: def GET() -> IdentityData:
... ...

View File

@ -718,6 +718,13 @@ class UbuntuProCheckTokenStatus(enum.Enum):
UNKNOWN_ERROR = enum.auto() 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) @attr.s(auto_attribs=True)
class UPCSInitiateResponse: class UPCSInitiateResponse:
"""Response to Ubuntu Pro contract selection initiate request.""" """Response to Ubuntu Pro contract selection initiate request."""

View File

@ -13,17 +13,20 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest from unittest import mock
import jsonschema import jsonschema
from jsonschema.validators import validator_for from jsonschema.validators import validator_for
from subiquity.server.controllers.ubuntu_pro import UbuntuProController from subiquity.server.controllers.ubuntu_pro import UbuntuProController
from subiquity.server.dryrun import DRConfig 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.mocks import make_app
from subiquitycore.tests.parameterized import parameterized
class TestUbuntuProController(unittest.TestCase): class TestUbuntuProController(SubiTestCase):
def setUp(self): def setUp(self):
app = make_app() app = make_app()
app.dr_cfg = DRConfig() app.dr_cfg = DRConfig()
@ -45,3 +48,32 @@ class TestUbuntuProController(unittest.TestCase):
) )
JsonValidator.check_schema(UbuntuProController.autoinstall_schema) 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)

View File

@ -19,10 +19,13 @@ import logging
import os import os
from typing import Optional from typing import Optional
import distro_info
from subiquity.common.apidef import API from subiquity.common.apidef import API
from subiquity.common.types import ( from subiquity.common.types import (
UbuntuProCheckTokenAnswer, UbuntuProCheckTokenAnswer,
UbuntuProCheckTokenStatus, UbuntuProCheckTokenStatus,
UbuntuProGeneralInfo,
UbuntuProInfo, UbuntuProInfo,
UbuntuProResponse, UbuntuProResponse,
UPCSInitiateResponse, UPCSInitiateResponse,
@ -40,6 +43,7 @@ from subiquity.server.ubuntu_advantage import (
UAInterface, UAInterface,
UAInterfaceStrategy, UAInterfaceStrategy,
) )
from subiquitycore.lsb_release import lsb_release
log = logging.getLogger("subiquity.server.controllers.ubuntu_pro") log = logging.getLogger("subiquity.server.controllers.ubuntu_pro")
@ -199,3 +203,27 @@ class UbuntuProController(SubiquityController):
self.cs.cancel() self.cs.cancel()
self.cs = None 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,
)

View File

@ -21,7 +21,11 @@ from typing import Callable, List
from urwid import Columns, LineBox, Text, Widget, connect_signal 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.buttons import back_btn, cancel_btn, done_btn, menu_btn, ok_btn
from subiquitycore.ui.container import ListBox, Pile, WidgetWrap from subiquitycore.ui.container import ListBox, Pile, WidgetWrap
from subiquitycore.ui.form import ( from subiquitycore.ui.form import (
@ -270,12 +274,21 @@ class UbuntuProView(BaseView):
title = _("Upgrade to Ubuntu Pro") title = _("Upgrade to Ubuntu Pro")
subscription_done_label = _("Continue") 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.""" """Initialize the view with the default value for the token."""
self.controller = controller self.controller = controller
self.has_network = has_network self.has_network = has_network
self.pre_release = pre_release self.pre_release = pre_release
self.info = info
if self.has_network: if self.has_network:
self.upgrade_yes_no_form = UpgradeYesNoForm( self.upgrade_yes_no_form = UpgradeYesNoForm(
@ -380,7 +393,7 @@ class UbuntuProView(BaseView):
| [ Back ] | | [ Back ] |
+---------------------------------------------------------+ +---------------------------------------------------------+
""" """
security_updates_until = 2032 security_updates_until = self.info.eol_esm_year or "20xx"
excerpt = _( excerpt = _(
"Upgrade this machine to Ubuntu Pro for security updates" "Upgrade this machine to Ubuntu Pro for security updates"
@ -815,8 +828,8 @@ class AboutProWidget(Stretchy):
" patches covering a wider range of packages." " patches covering a wider range of packages."
) )
universe_packages = 23000 universe_packages = self.parent.info.universe_packages
main_packages = 2300 main_packages = self.parent.info.main_packages
services = [ services = [
_( _(

View File

@ -6,15 +6,7 @@ LSB_RELEASE_FILE = "/etc/lsb-release"
LSB_RELEASE_EXAMPLE = "examples/lsb-release-focal" LSB_RELEASE_EXAMPLE = "examples/lsb-release-focal"
def lsb_release(path=None, dry_run: bool = False) -> Dict[str, str]: def lsb_release_from_path(path: str) -> 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
ret: Dict[str, str] = {} ret: Dict[str, str] = {}
try: try:
with open(path, "r") as fp: with open(path, "r") as fp:
@ -30,5 +22,17 @@ def lsb_release(path=None, dry_run: bool = False) -> Dict[str, str]:
return ret 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__": if __name__ == "__main__":
print(lsb_release()) print(lsb_release())