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/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/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..2e992ffd --- /dev/null +++ b/subiquity/client/controllers/ubuntu_advantage.py @@ -0,0 +1,65 @@ +# 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 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") + + +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. """ + + # 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 + 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) + + 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..ef13ab41 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, @@ -312,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/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..025c10aa --- /dev/null +++ b/subiquity/server/controllers/ubuntu_advantage.py @@ -0,0 +1,58 @@ +# 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) -> 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() 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()