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:
commit
c8501d81db
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
DISTRIB_ID=Ubuntu
|
||||||
|
DISTRIB_RELEASE=22.04
|
||||||
|
DISTRIB_CODENAME=jammy
|
||||||
|
DISTRIB_DESCRIPTION="Ubuntu 22.04 LTS"
|
|
@ -0,0 +1,4 @@
|
||||||
|
DISTRIB_ID=Ubuntu
|
||||||
|
DISTRIB_RELEASE=24.04
|
||||||
|
DISTRIB_CODENAME=noble
|
||||||
|
DISTRIB_DESCRIPTION="Ubuntu Noble Numbat (development branch)"
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
...
|
...
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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 = [
|
||||||
_(
|
_(
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in New Issue