From 1c9c04d55d079209a8b118dd3d3d6306bcf2568d Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Fri, 3 Dec 2021 10:42:49 +0100 Subject: [PATCH 1/3] Add initial integration of Ubuntu Advantage in subiquity Introduce a new view, model and controllers pair to enable Ubuntu Advantage from Subiquity. For now, we expect the user to input an alphanumeric token and rely on cloud-init to enable the features associated with the subscription. This implementation comes with a number of limitations: * inability to validate the token before first-boot * inability to check what features are associated with the Ubuntu Advantage subscription, before first-boot * the user must input the token directly: there is no mechanism to deduce the token based on email address and password. The new screen is placed after the identity scree. The installation should already be running when the UA screen will be shown to the user. Signed-off-by: Olivier Gayot --- examples/answers-bond.yaml | 2 + examples/answers-guided-lvm.yaml | 2 + examples/answers-imsm.yaml | 2 + examples/answers-lvm-dmcrypt.yaml | 2 + examples/answers-lvm.yaml | 2 + examples/answers-preserve.yaml | 2 + examples/answers-raid-lvm.yaml | 2 + examples/answers-raid.yaml | 2 + examples/answers-serial.yaml | 2 + examples/answers-swap.yaml | 2 + examples/answers.yaml | 2 + subiquity/client/client.py | 1 + subiquity/client/controllers/__init__.py | 2 + .../client/controllers/ubuntu_advantage.py | 48 +++++++++ subiquity/common/apidef.py | 2 + subiquity/common/types.py | 5 + subiquity/models/subiquity.py | 2 + .../models/tests/test_ubuntu_advantage.py | 38 +++++++ subiquity/models/ubuntu_advantage.py | 47 +++++++++ subiquity/server/controllers/__init__.py | 2 + .../tests/test_ubuntu_advantage.py | 34 +++++++ .../server/controllers/ubuntu_advantage.py | 53 ++++++++++ subiquity/server/server.py | 2 + subiquity/tests/api/test_api.py | 4 + subiquity/ui/views/ubuntu_advantage.py | 99 +++++++++++++++++++ 25 files changed, 361 insertions(+) create mode 100644 subiquity/client/controllers/ubuntu_advantage.py create mode 100644 subiquity/models/tests/test_ubuntu_advantage.py create mode 100644 subiquity/models/ubuntu_advantage.py create mode 100644 subiquity/server/controllers/tests/test_ubuntu_advantage.py create mode 100644 subiquity/server/controllers/ubuntu_advantage.py create mode 100644 subiquity/ui/views/ubuntu_advantage.py diff --git a/examples/answers-bond.yaml b/examples/answers-bond.yaml index 7d613ff5..f38de8cd 100644 --- a/examples/answers-bond.yaml +++ b/examples/answers-bond.yaml @@ -45,6 +45,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: false SnapList: diff --git a/examples/answers-guided-lvm.yaml b/examples/answers-guided-lvm.yaml index 8bbd1c3c..35f500c4 100644 --- a/examples/answers-guided-lvm.yaml +++ b/examples/answers-guided-lvm.yaml @@ -20,6 +20,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: false SnapList: diff --git a/examples/answers-imsm.yaml b/examples/answers-imsm.yaml index 96f64b1d..0ec69af3 100644 --- a/examples/answers-imsm.yaml +++ b/examples/answers-imsm.yaml @@ -20,6 +20,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: true pwauth: false diff --git a/examples/answers-lvm-dmcrypt.yaml b/examples/answers-lvm-dmcrypt.yaml index 05ab12dd..251fd298 100644 --- a/examples/answers-lvm-dmcrypt.yaml +++ b/examples/answers-lvm-dmcrypt.yaml @@ -64,6 +64,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: true pwauth: false diff --git a/examples/answers-lvm.yaml b/examples/answers-lvm.yaml index e3a1155a..d81c8f5d 100644 --- a/examples/answers-lvm.yaml +++ b/examples/answers-lvm.yaml @@ -45,6 +45,8 @@ Filesystem: fstype: ext4 mount: / - action: done +UbuntuAdvantage: + token: "a1b2c3d4" Identity: realname: Ubuntu username: ubuntu diff --git a/examples/answers-preserve.yaml b/examples/answers-preserve.yaml index 1c0eacb0..142adf68 100644 --- a/examples/answers-preserve.yaml +++ b/examples/answers-preserve.yaml @@ -25,6 +25,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: true pwauth: false diff --git a/examples/answers-raid-lvm.yaml b/examples/answers-raid-lvm.yaml index 437e0548..b3d71e07 100644 --- a/examples/answers-raid-lvm.yaml +++ b/examples/answers-raid-lvm.yaml @@ -96,6 +96,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: false SnapList: diff --git a/examples/answers-raid.yaml b/examples/answers-raid.yaml index 87ed33bb..16434788 100644 --- a/examples/answers-raid.yaml +++ b/examples/answers-raid.yaml @@ -55,6 +55,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: false SnapList: diff --git a/examples/answers-serial.yaml b/examples/answers-serial.yaml index 788f3eca..0ccfa3af 100644 --- a/examples/answers-serial.yaml +++ b/examples/answers-serial.yaml @@ -24,6 +24,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: true pwauth: false diff --git a/examples/answers-swap.yaml b/examples/answers-swap.yaml index 6eef12c1..095b8037 100644 --- a/examples/answers-swap.yaml +++ b/examples/answers-swap.yaml @@ -28,6 +28,8 @@ Identity: hostname: ubuntu-server # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +UbuntuAdvantage: + token: "a1b2c3d4" SSH: install_server: false SnapList: diff --git a/examples/answers.yaml b/examples/answers.yaml index fe4d736c..96c17d12 100644 --- a/examples/answers.yaml +++ b/examples/answers.yaml @@ -22,6 +22,8 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' ssh-import-id: gh:mwhudson +UbuntuAdvantage: + token: "a1b2c3d4" SnapList: snaps: hello: diff --git a/subiquity/client/client.py b/subiquity/client/client.py index d402dce5..c6e59766 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -110,6 +110,7 @@ class SubiquityClient(TuiApplication): "Refresh", "Filesystem", "Identity", + "UbuntuAdvantage", "SSH", "SnapList", "Progress", diff --git a/subiquity/client/controllers/__init__.py b/subiquity/client/controllers/__init__.py index 11abc3b2..4b1ed23a 100644 --- a/subiquity/client/controllers/__init__.py +++ b/subiquity/client/controllers/__init__.py @@ -26,6 +26,7 @@ from .serial import SerialController from .snaplist import SnapListController from .source import SourceController from .ssh import SSHController +from .ubuntu_advantage import UbuntuAdvantageController from .welcome import WelcomeController from .zdev import ZdevController @@ -44,6 +45,7 @@ __all__ = [ 'SnapListController', 'SourceController', 'SSHController', + 'UbuntuAdvantageController', 'WelcomeController', 'ZdevController', ] diff --git a/subiquity/client/controllers/ubuntu_advantage.py b/subiquity/client/controllers/ubuntu_advantage.py new file mode 100644 index 00000000..4f0725c9 --- /dev/null +++ b/subiquity/client/controllers/ubuntu_advantage.py @@ -0,0 +1,48 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +""" Module that defines the client-side controller class for Ubuntu Advantage. +""" + +import logging + +from subiquity.client.controller import SubiquityTuiController +from subiquity.common.types import UbuntuAdvantageInfo +from subiquity.ui.views.ubuntu_advantage import UbuntuAdvantageView + +log = logging.getLogger("subiquity.client.controllers.ubuntu_advantage") + + +class UbuntuAdvantageController(SubiquityTuiController): + """ Client-side controller for Ubuntu Advantage configuration. """ + + endpoint_name = "ubuntu_advantage" + + async def make_ui(self) -> UbuntuAdvantageView: + """ Generate the UI, based on the data provided by the model. """ + ubuntu_advantage_info = await self.endpoint.GET() + return UbuntuAdvantageView(self, ubuntu_advantage_info.token) + + def run_answers(self) -> None: + if "token" in self.answers: + self.done(self.answers["token"]) + + def cancel(self) -> None: + self.app.prev_screen() + + def done(self, token: str) -> None: + log.debug("UbuntuAdvantageController.done token=%s", token) + self.app.next_screen( + self.endpoint.POST(UbuntuAdvantageInfo(token=token)) + ) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 845248dd..bb1f322b 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -48,6 +48,7 @@ from subiquity.common.types import ( StorageResponse, StorageResponseV2, TimeZoneInfo, + UbuntuAdvantageInfo, WLANSupportInstallState, ZdevInfo, WSLConfigurationBase, @@ -62,6 +63,7 @@ class API: locale = simple_endpoint(str) proxy = simple_endpoint(str) ssh = simple_endpoint(SSHData) + ubuntu_advantage = simple_endpoint(UbuntuAdvantageInfo) updates = simple_endpoint(str) wslconfbase = simple_endpoint(WSLConfigurationBase) wslconfadvanced = simple_endpoint(WSLConfigurationAdvanced) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 08e4bd56..620be578 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -389,6 +389,11 @@ class TimeZoneInfo: from_geoip: bool +@attr.s(auto_attribs=True) +class UbuntuAdvantageInfo: + token: str + + class ShutdownMode(enum.Enum): REBOOT = enum.auto() POWEROFF = enum.auto() diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index d41d6088..0f056f11 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -41,6 +41,7 @@ from .snaplist import SnapListModel from .source import SourceModel from .ssh import SSHModel from .timezone import TimeZoneModel +from .ubuntu_advantage import UbuntuAdvantageModel from .updates import UpdatesModel @@ -143,6 +144,7 @@ class SubiquityModel: self.ssh = SSHModel() self.source = SourceModel() self.timezone = TimeZoneModel() + self.ubuntu_advantage = UbuntuAdvantageModel() self.updates = UpdatesModel() self.userdata = {} diff --git a/subiquity/models/tests/test_ubuntu_advantage.py b/subiquity/models/tests/test_ubuntu_advantage.py new file mode 100644 index 00000000..c718164c --- /dev/null +++ b/subiquity/models/tests/test_ubuntu_advantage.py @@ -0,0 +1,38 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import unittest + +from subiquity.models.ubuntu_advantage import UbuntuAdvantageModel + + +class TestUbuntuAdvantageModel(unittest.TestCase): + def test_make_cloudconfig_(self): + model = UbuntuAdvantageModel() + + # Test with a token + model.token = "0a1b2c3d4e5f6" + expected = { + "ubuntu_advantage": { + "token": "0a1b2c3d4e5f6", + } + } + self.assertEqual(model.make_cloudconfig(), expected) + + # Test without token + model.token = "" + self.assertEqual(model.make_cloudconfig(), {}) + model.token = None + self.assertEqual(model.make_cloudconfig(), {}) diff --git a/subiquity/models/ubuntu_advantage.py b/subiquity/models/ubuntu_advantage.py new file mode 100644 index 00000000..40ce7a6a --- /dev/null +++ b/subiquity/models/ubuntu_advantage.py @@ -0,0 +1,47 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +""" Module that defines the model for Ubuntu Advantage configuration. """ + +import logging + +log = logging.getLogger("subiquitycore.models.ubuntu_advantage") + + +class UbuntuAdvantageModel: + """ + Model that represents the Ubuntu Advantage configuration. + Currently, we rely only on cloud-init so we have no means to validate that + the provided token is correct ; nor to retrieve information about the + subscription. + """ + def __init__(self): + """ Initialize the model. """ + self.token: str = "" + + def make_cloudconfig(self) -> dict: + """ + Return a dictionary that will be included in cloud-init config. + Having the token set to the empty-string disables the configuration. + """ + if not self.token: + return {} + # Both "ubuntu_advantage" and "ubuntu-advantage" keys are accepted, but + # "ubuntu-advantage" is deprecated despite not being mentioned in the + # documentation. + return { + "ubuntu_advantage": { + "token": self.token, + }, + } diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py index d875238b..3e0e65d3 100644 --- a/subiquity/server/controllers/__init__.py +++ b/subiquity/server/controllers/__init__.py @@ -32,6 +32,7 @@ from .snaplist import SnapListController from .source import SourceController from .ssh import SSHController from .timezone import TimeZoneController +from .ubuntu_advantage import UbuntuAdvantageController from .updates import UpdatesController from .userdata import UserdataController from .zdev import ZdevController @@ -58,6 +59,7 @@ __all__ = [ 'SourceController', 'SSHController', 'TimeZoneController', + 'UbuntuAdvantageController', 'UpdatesController', 'UserdataController', 'ZdevController', diff --git a/subiquity/server/controllers/tests/test_ubuntu_advantage.py b/subiquity/server/controllers/tests/test_ubuntu_advantage.py new file mode 100644 index 00000000..19564144 --- /dev/null +++ b/subiquity/server/controllers/tests/test_ubuntu_advantage.py @@ -0,0 +1,34 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import unittest + +from subiquity.server.controllers.ubuntu_advantage import ( + UbuntuAdvantageController, +) +from subiquitycore.tests.mocks import make_app + + +class TestUbuntuAdvantageController(unittest.TestCase): + def setUp(self): + self.controller = UbuntuAdvantageController(make_app()) + + def test_serialize(self): + self.controller.model.token = "1a2b3C" + self.assertEqual(self.controller.serialize(), "1a2b3C") + + def test_deserialize(self): + self.controller.deserialize("1A2B3C4D") + self.assertEqual(self.controller.model.token, "1A2B3C4D") diff --git a/subiquity/server/controllers/ubuntu_advantage.py b/subiquity/server/controllers/ubuntu_advantage.py new file mode 100644 index 00000000..68d9865c --- /dev/null +++ b/subiquity/server/controllers/ubuntu_advantage.py @@ -0,0 +1,53 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +""" Module defining the server-side controller class for Ubuntu Advantage. """ + +import logging + +from subiquity.common.apidef import API +from subiquity.common.types import UbuntuAdvantageInfo +from subiquity.server.controller import SubiquityController + +log = logging.getLogger("subiquity.server.controllers.ubuntu_advantage") + + +class UbuntuAdvantageController(SubiquityController): + """ Represent the server-side Ubuntu Advantage controller. """ + + endpoint = API.ubuntu_advantage + + model_name = "ubuntu_advantage" + + def serialize(self) -> str: + """ Save the current state of the model so it can be loaded later. + Currently this function is called automatically by .configured(). + """ + return self.model.token + + def deserialize(self, token: str) -> None: + """ Loads the last-known state of the model. """ + self.model.token = token + + async def GET(self) -> UbuntuAdvantageInfo: + """ Handle a GET request coming from the client-side controller. """ + return UbuntuAdvantageInfo(token=self.model.token) + + async def POST(self, data: UbuntuAdvantageInfo): + """ Handle a POST request coming from the client-side controller and + then call .configured(). + """ + log.debug("Received POST: %s", data) + self.model.token = data.token + await self.configured() diff --git a/subiquity/server/server.py b/subiquity/server/server.py index aaf59eaf..1ca50a4d 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -204,6 +204,7 @@ POSTINSTALL_MODEL_NAMES = ModelNames({ "packages", "snaplist", "ssh", + "ubuntu_advantage", "userdata", }, desktop={"timezone"}) @@ -242,6 +243,7 @@ class SubiquityServer(Application): "Zdev", "Source", "Network", + "UbuntuAdvantage", "Proxy", "Mirror", "Filesystem", diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index e4b7144e..d6fc7f98 100755 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -215,6 +215,10 @@ class TestFlow(TestAPI): } await inst.post('/ssh', ssh) await inst.post('/snaplist', []) + ua_params = { + "token": "a1b2c3d4e6f7g8h9I0K1", + } + await inst.post('/ubuntu_advantage', ua_params) for state in 'RUNNING', 'POST_WAIT', 'POST_RUNNING', 'UU_RUNNING': await inst.get('/meta/status', cur=state) diff --git a/subiquity/ui/views/ubuntu_advantage.py b/subiquity/ui/views/ubuntu_advantage.py new file mode 100644 index 00000000..73cf17f7 --- /dev/null +++ b/subiquity/ui/views/ubuntu_advantage.py @@ -0,0 +1,99 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +""" Module that defines the view class for Ubuntu Advantage configuration. """ + +import logging +import re + +from urwid import connect_signal + +from subiquitycore.view import BaseView +from subiquitycore.ui.form import ( + Form, + simple_field, + WantsToKnowFormField, +) + +from subiquitycore.ui.interactive import StringEditor + + +log = logging.getLogger('subiquity.ui.views.ubuntu_advantage') + +ua_help = _("If you want to enroll this system using your Ubuntu Advantage " + "subscription, enter your Ubuntu Advantage token here. " + "Otherwise, leave this blank.") + + +class UATokenEditor(StringEditor, WantsToKnowFormField): + """ Represent a text-box editor for the Ubuntu Advantage Token. """ + def __init__(self): + """ Initialize the text-field editor for UA token. """ + self.valid_char_pat = r"[a-zA-Z0-9]" + self.error_invalid_char = _("The only characters permitted in this " + "field are alphanumeric characters.") + super().__init__() + + def valid_char(self, ch: str) -> bool: + """ Tells whether the character passed is within the range of allowed + characters + """ + if len(ch) == 1 and not re.match(self.valid_char_pat, ch): + self.bff.in_error = True + self.bff.show_extra(("info_error", self.error_invalid_char)) + return False + return super().valid_char(ch) + + +class UbuntuAdvantageForm(Form): + """ + Represents a form requesting Ubuntu Advantage information + """ + cancel_label = _("Back") + + UATokenField = simple_field(UATokenEditor) + + token = UATokenField(_("Ubuntu Advantage token:"), help=ua_help) + + +class UbuntuAdvantageView(BaseView): + """ Represent the view of the Ubuntu Advantage configuration. """ + + title = _("Enable Ubuntu Advantage") + excerpt = _("Enter your Ubuntu Advantage token if you want to enroll " + "this system.") + + def __init__(self, controller, token: str): + """ Initialize the view with the default value for the token. """ + self.controller = controller + + self.form = UbuntuAdvantageForm(initial={"token": token}) + + def on_cancel(_: UbuntuAdvantageForm): + self.cancel() + + connect_signal(self.form, 'submit', self.done) + connect_signal(self.form, 'cancel', on_cancel) + + super().__init__(self.form.as_screen(excerpt=_(self.excerpt))) + + def done(self, form: UbuntuAdvantageForm) -> None: + """ Called when the user presses the Done button. """ + log.debug("User input: %r", form.as_data()) + + self.controller.done(form.token.value) + + def cancel(self) -> None: + """ Called when the user presses the Back button. """ + self.controller.cancel() From fe4b6fde6fe6eb29db172c4fe9cb013d55ab5da5 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 15 Dec 2021 14:07:58 +0100 Subject: [PATCH 2/3] Only show UA screen if running on LTS release The UA screen is now shown only if we are running on an LTS release. In dry-run mode, we pretend to always be on Ubuntu focal. To implement the possibility to skip the screen, we added another POST resource to inform the server that we are finished configuring UbuntuAdvantage. Signed-off-by: Olivier Gayot --- examples/lsb-release-focal | 4 ++++ examples/lsb-release-impish | 4 ++++ subiquity/client/controllers/ubuntu_advantage.py | 12 ++++++++++++ subiquity/common/apidef.py | 8 +++++++- subiquity/server/controllers/ubuntu_advantage.py | 7 ++++++- 5 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 examples/lsb-release-focal create mode 100644 examples/lsb-release-impish diff --git a/examples/lsb-release-focal b/examples/lsb-release-focal new file mode 100644 index 00000000..5de4df74 --- /dev/null +++ b/examples/lsb-release-focal @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=20.04 +DISTRIB_CODENAME=focal +DISTRIB_DESCRIPTION="Ubuntu 20.04 LTS" diff --git a/examples/lsb-release-impish b/examples/lsb-release-impish new file mode 100644 index 00000000..d488d79f --- /dev/null +++ b/examples/lsb-release-impish @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=21.10 +DISTRIB_CODENAME=impish +DISTRIB_DESCRIPTION="Ubuntu 21.10" diff --git a/subiquity/client/controllers/ubuntu_advantage.py b/subiquity/client/controllers/ubuntu_advantage.py index 4f0725c9..52b7b164 100644 --- a/subiquity/client/controllers/ubuntu_advantage.py +++ b/subiquity/client/controllers/ubuntu_advantage.py @@ -16,11 +16,15 @@ """ import logging +from typing import Optional from subiquity.client.controller import SubiquityTuiController from subiquity.common.types import UbuntuAdvantageInfo from subiquity.ui.views.ubuntu_advantage import UbuntuAdvantageView +from subiquitycore.lsb_release import lsb_release +from subiquitycore.tuicontroller import Skip + log = logging.getLogger("subiquity.client.controllers.ubuntu_advantage") @@ -31,6 +35,14 @@ class UbuntuAdvantageController(SubiquityTuiController): async def make_ui(self) -> UbuntuAdvantageView: """ Generate the UI, based on the data provided by the model. """ + path_lsb_release: Optional[str] = None + if self.app.opts.dry_run: + # In dry-run mode, always pretend to be on LTS + path_lsb_release = "examples/lsb-release-focal" + if "LTS" not in lsb_release(path_lsb_release)["description"]: + await self.endpoint.skip.POST() + raise Skip("Not running LTS version") + ubuntu_advantage_info = await self.endpoint.GET() return UbuntuAdvantageView(self, ubuntu_advantage_info.token) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index bb1f322b..ef13ab41 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -63,7 +63,6 @@ class API: locale = simple_endpoint(str) proxy = simple_endpoint(str) ssh = simple_endpoint(SSHData) - ubuntu_advantage = simple_endpoint(UbuntuAdvantageInfo) updates = simple_endpoint(str) wslconfbase = simple_endpoint(WSLConfigurationBase) wslconfadvanced = simple_endpoint(WSLConfigurationAdvanced) @@ -314,6 +313,13 @@ class API: def GET() -> List[str]: ... def POST(data: Payload[List[str]]): ... + class ubuntu_advantage: + def GET() -> UbuntuAdvantageInfo: ... + def POST(data: Payload[UbuntuAdvantageInfo]) -> None: ... + + class skip: + def POST() -> None: ... + class LinkAction(enum.Enum): NEW = enum.auto() diff --git a/subiquity/server/controllers/ubuntu_advantage.py b/subiquity/server/controllers/ubuntu_advantage.py index 68d9865c..025c10aa 100644 --- a/subiquity/server/controllers/ubuntu_advantage.py +++ b/subiquity/server/controllers/ubuntu_advantage.py @@ -44,10 +44,15 @@ class UbuntuAdvantageController(SubiquityController): """ Handle a GET request coming from the client-side controller. """ return UbuntuAdvantageInfo(token=self.model.token) - async def POST(self, data: UbuntuAdvantageInfo): + async def POST(self, data: UbuntuAdvantageInfo) -> None: """ Handle a POST request coming from the client-side controller and then call .configured(). """ log.debug("Received POST: %s", data) self.model.token = data.token await self.configured() + + async def skip_POST(self) -> None: + """ When running on a non-LTS release, we want to call this so we can + skip the screen on the client side. """ + await self.configured() From 5403222956386b3a75b3c4a493f67b7669489364 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Thu, 16 Dec 2021 10:41:51 +0100 Subject: [PATCH 3/3] Skip the UbuntuAdvantage screen until we can validate the token Signed-off-by: Olivier Gayot --- subiquity/client/controllers/ubuntu_advantage.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/subiquity/client/controllers/ubuntu_advantage.py b/subiquity/client/controllers/ubuntu_advantage.py index 52b7b164..2e992ffd 100644 --- a/subiquity/client/controllers/ubuntu_advantage.py +++ b/subiquity/client/controllers/ubuntu_advantage.py @@ -35,6 +35,11 @@ class UbuntuAdvantageController(SubiquityTuiController): async def make_ui(self) -> UbuntuAdvantageView: """ Generate the UI, based on the data provided by the model. """ + + # TODO remove these two lines to enable this screen + await self.endpoint.skip.POST() + raise Skip("Hiding the screen until we can validate the token.") + path_lsb_release: Optional[str] = None if self.app.opts.dry_run: # In dry-run mode, always pretend to be on LTS