Merge pull request #1140 from ogayot/FR-1652

Add initial integration of Ubuntu Advantage in Subiquity
This commit is contained in:
Dan Bungert 2021-12-16 16:55:46 -07:00 committed by GitHub
commit 73bc16b8fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 397 additions and 0 deletions

View File

@ -45,6 +45,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: false install_server: false
SnapList: SnapList:

View File

@ -20,6 +20,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: false install_server: false
SnapList: SnapList:

View File

@ -20,6 +20,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: true install_server: true
pwauth: false pwauth: false

View File

@ -64,6 +64,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: true install_server: true
pwauth: false pwauth: false

View File

@ -45,6 +45,8 @@ Filesystem:
fstype: ext4 fstype: ext4
mount: / mount: /
- action: done - action: done
UbuntuAdvantage:
token: "a1b2c3d4"
Identity: Identity:
realname: Ubuntu realname: Ubuntu
username: ubuntu username: ubuntu

View File

@ -25,6 +25,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: true install_server: true
pwauth: false pwauth: false

View File

@ -96,6 +96,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: false install_server: false
SnapList: SnapList:

View File

@ -55,6 +55,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: false install_server: false
SnapList: SnapList:

View File

@ -24,6 +24,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: true install_server: true
pwauth: false pwauth: false

View File

@ -28,6 +28,8 @@ Identity:
hostname: ubuntu-server hostname: ubuntu-server
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
UbuntuAdvantage:
token: "a1b2c3d4"
SSH: SSH:
install_server: false install_server: false
SnapList: SnapList:

View File

@ -22,6 +22,8 @@ Identity:
# ubuntu # ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
ssh-import-id: gh:mwhudson ssh-import-id: gh:mwhudson
UbuntuAdvantage:
token: "a1b2c3d4"
SnapList: SnapList:
snaps: snaps:
hello: hello:

View File

@ -0,0 +1,4 @@
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04 LTS"

View File

@ -0,0 +1,4 @@
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=21.10
DISTRIB_CODENAME=impish
DISTRIB_DESCRIPTION="Ubuntu 21.10"

View File

@ -110,6 +110,7 @@ class SubiquityClient(TuiApplication):
"Refresh", "Refresh",
"Filesystem", "Filesystem",
"Identity", "Identity",
"UbuntuAdvantage",
"SSH", "SSH",
"SnapList", "SnapList",
"Progress", "Progress",

View File

@ -26,6 +26,7 @@ from .serial import SerialController
from .snaplist import SnapListController from .snaplist import SnapListController
from .source import SourceController from .source import SourceController
from .ssh import SSHController from .ssh import SSHController
from .ubuntu_advantage import UbuntuAdvantageController
from .welcome import WelcomeController from .welcome import WelcomeController
from .zdev import ZdevController from .zdev import ZdevController
@ -44,6 +45,7 @@ __all__ = [
'SnapListController', 'SnapListController',
'SourceController', 'SourceController',
'SSHController', 'SSHController',
'UbuntuAdvantageController',
'WelcomeController', 'WelcomeController',
'ZdevController', 'ZdevController',
] ]

View File

@ -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 <http://www.gnu.org/licenses/>.
""" 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))
)

View File

@ -48,6 +48,7 @@ from subiquity.common.types import (
StorageResponse, StorageResponse,
StorageResponseV2, StorageResponseV2,
TimeZoneInfo, TimeZoneInfo,
UbuntuAdvantageInfo,
WLANSupportInstallState, WLANSupportInstallState,
ZdevInfo, ZdevInfo,
WSLConfigurationBase, WSLConfigurationBase,
@ -312,6 +313,13 @@ class API:
def GET() -> List[str]: ... def GET() -> List[str]: ...
def POST(data: Payload[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): class LinkAction(enum.Enum):
NEW = enum.auto() NEW = enum.auto()

View File

@ -389,6 +389,11 @@ class TimeZoneInfo:
from_geoip: bool from_geoip: bool
@attr.s(auto_attribs=True)
class UbuntuAdvantageInfo:
token: str
class ShutdownMode(enum.Enum): class ShutdownMode(enum.Enum):
REBOOT = enum.auto() REBOOT = enum.auto()
POWEROFF = enum.auto() POWEROFF = enum.auto()

View File

@ -41,6 +41,7 @@ from .snaplist import SnapListModel
from .source import SourceModel from .source import SourceModel
from .ssh import SSHModel from .ssh import SSHModel
from .timezone import TimeZoneModel from .timezone import TimeZoneModel
from .ubuntu_advantage import UbuntuAdvantageModel
from .updates import UpdatesModel from .updates import UpdatesModel
@ -143,6 +144,7 @@ class SubiquityModel:
self.ssh = SSHModel() self.ssh = SSHModel()
self.source = SourceModel() self.source = SourceModel()
self.timezone = TimeZoneModel() self.timezone = TimeZoneModel()
self.ubuntu_advantage = UbuntuAdvantageModel()
self.updates = UpdatesModel() self.updates = UpdatesModel()
self.userdata = {} self.userdata = {}

View File

@ -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 <http://www.gnu.org/licenses/>.
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(), {})

View File

@ -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 <http://www.gnu.org/licenses/>.
""" 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,
},
}

View File

@ -32,6 +32,7 @@ from .snaplist import SnapListController
from .source import SourceController from .source import SourceController
from .ssh import SSHController from .ssh import SSHController
from .timezone import TimeZoneController from .timezone import TimeZoneController
from .ubuntu_advantage import UbuntuAdvantageController
from .updates import UpdatesController from .updates import UpdatesController
from .userdata import UserdataController from .userdata import UserdataController
from .zdev import ZdevController from .zdev import ZdevController
@ -58,6 +59,7 @@ __all__ = [
'SourceController', 'SourceController',
'SSHController', 'SSHController',
'TimeZoneController', 'TimeZoneController',
'UbuntuAdvantageController',
'UpdatesController', 'UpdatesController',
'UserdataController', 'UserdataController',
'ZdevController', 'ZdevController',

View File

@ -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 <http://www.gnu.org/licenses/>.
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")

View File

@ -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 <http://www.gnu.org/licenses/>.
""" 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()

View File

@ -204,6 +204,7 @@ POSTINSTALL_MODEL_NAMES = ModelNames({
"packages", "packages",
"snaplist", "snaplist",
"ssh", "ssh",
"ubuntu_advantage",
"userdata", "userdata",
}, },
desktop={"timezone"}) desktop={"timezone"})
@ -242,6 +243,7 @@ class SubiquityServer(Application):
"Zdev", "Zdev",
"Source", "Source",
"Network", "Network",
"UbuntuAdvantage",
"Proxy", "Proxy",
"Mirror", "Mirror",
"Filesystem", "Filesystem",

View File

@ -215,6 +215,10 @@ class TestFlow(TestAPI):
} }
await inst.post('/ssh', ssh) await inst.post('/ssh', ssh)
await inst.post('/snaplist', []) 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': for state in 'RUNNING', 'POST_WAIT', 'POST_RUNNING', 'UU_RUNNING':
await inst.get('/meta/status', cur=state) await inst.get('/meta/status', cur=state)

View File

@ -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 <http://www.gnu.org/licenses/>.
""" 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()