diff --git a/subiquity/common/types.py b/subiquity/common/types.py new file mode 100644 index 00000000..35f6591c --- /dev/null +++ b/subiquity/common/types.py @@ -0,0 +1,37 @@ +# Copyright 2020 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 . + +# This module defines types that will be used in the API when subiquity gets +# split into client and server processes. View code should only use these +# types! + +from typing import List + +import attr + + +@attr.s(auto_attribs=True) +class IdentityData: + realname: str = '' + username: str = '' + crypted_password: str = attr.ib(default='', repr=False) + hostname: str = '' + + +@attr.s(auto_attribs=True) +class SSHData: + install_server: bool + allow_pw: bool + authorized_keys: List[str] = attr.Factory(list) diff --git a/subiquity/controllers/identity.py b/subiquity/controllers/identity.py index 33a85153..c9d239a9 100644 --- a/subiquity/controllers/identity.py +++ b/subiquity/controllers/identity.py @@ -19,6 +19,7 @@ import attr from subiquitycore.context import with_context +from subiquity.common.types import IdentityData from subiquity.controller import SubiquityTuiController from subiquity.ui.views import IdentityView @@ -42,7 +43,13 @@ class IdentityController(SubiquityTuiController): def load_autoinstall_data(self, data): if data is not None: - self.model.add_user(data) + identity_data = IdentityData( + realname=data.get('realname', ''), + username=data['username'], + hostname=data['hostname'], + crypted_password=data['password'], + ) + self.model.add_user(identity_data) @with_context() async def apply_autoinstall_config(self, context=None): @@ -51,29 +58,32 @@ class IdentityController(SubiquityTuiController): raise Exception("no identity data provided") def make_ui(self): - return IdentityView(self.model, self) + data = IdentityData() + if self.model.user is not None: + data.username = self.model.user.username + data.realname = self.model.user.realname + if self.model.hostname: + data.hostname = self.model.hostname + return IdentityView(self, data) def run_answers(self): if all(elem in self.answers for elem in ['realname', 'username', 'password', 'hostname']): - d = { - 'realname': self.answers['realname'], - 'username': self.answers['username'], - 'hostname': self.answers['hostname'], - 'password': self.answers['password'], - } - self.done(d) + identity = IdentityData( + realname=self.answers['realname'], + username=self.answers['username'], + hostname=self.answers['hostname'], + crypted_password=self.answers['password']) + self.done(identity) def cancel(self): self.app.prev_screen() - def done(self, user_spec): - safe_spec = user_spec.copy() - safe_spec['password'] = '' + def done(self, identity_data): log.debug( "IdentityController.done next_screen user_spec=%s", - safe_spec) - self.model.add_user(user_spec) + identity_data) + self.model.add_user(identity_data) self.configured() self.app.next_screen() diff --git a/subiquity/controllers/mirror.py b/subiquity/controllers/mirror.py index 446bbf0d..6abe4199 100644 --- a/subiquity/controllers/mirror.py +++ b/subiquity/controllers/mirror.py @@ -124,7 +124,7 @@ class MirrorController(SubiquityTuiController): def make_ui(self): self.check_state = CheckState.DONE - return MirrorView(self.model, self) + return MirrorView(self, self.model.get_mirror()) def run_answers(self): if 'mirror' in self.answers: diff --git a/subiquity/controllers/proxy.py b/subiquity/controllers/proxy.py index 5433f616..a61524c8 100644 --- a/subiquity/controllers/proxy.py +++ b/subiquity/controllers/proxy.py @@ -49,7 +49,7 @@ class ProxyController(SubiquityTuiController): pass def make_ui(self): - return ProxyView(self.model, self) + return ProxyView(self, self.model.proxy) def run_answers(self): if 'proxy' in self.answers: diff --git a/subiquity/controllers/ssh.py b/subiquity/controllers/ssh.py index a10d97bc..476ffeb7 100644 --- a/subiquity/controllers/ssh.py +++ b/subiquity/controllers/ssh.py @@ -20,6 +20,7 @@ from subiquitycore.async_helpers import schedule_task from subiquitycore.context import with_context from subiquitycore import utils +from subiquity.common.types import SSHData from subiquity.controller import SubiquityTuiController from subiquity.ui.views.ssh import SSHView @@ -66,25 +67,25 @@ class SSHController(SubiquityTuiController): 'allow-pw', not self.model.authorized_keys) def make_ui(self): - return SSHView(self.model, self) + ssh_data = SSHData( + install_server=self.model.install_server, + allow_pw=self.model.pwauth) + return SSHView(self, ssh_data) def run_answers(self): if 'ssh-import-id' in self.answers: import_id = self.answers['ssh-import-id'] - d = { - "ssh_import_id": import_id.split(":", 1)[0], - "import_username": import_id.split(":", 1)[1], - "install_server": True, - "pwauth": True, - } - self.fetch_ssh_keys(d) + ssh = SSHData( + install_server=True, + authorized_keys=[], + allow_pw=True) + self.fetch_ssh_keys(ssh_import_id=import_id, ssh_data=ssh) else: - d = { - "install_server": self.answers.get("install_server", False), - "authorized_keys": self.answers.get("authorized_keys", []), - "pwauth": self.answers.get("pwauth", True), - } - self.done(d) + ssh = SSHData( + install_server=self.answers.get("install_server", False), + authorized_keys=self.answers.get("authorized_keys", []), + allow_pw=self.answers.get("pwauth", True)) + self.done(ssh) def cancel(self): self.app.prev_screen() @@ -103,10 +104,8 @@ class SSHController(SubiquityTuiController): return cp @with_context( - name="ssh_import_id", - description="{user_spec[ssh_import_id]}:{user_spec[import_username]}") - async def _fetch_ssh_keys(self, *, context, user_spec): - ssh_import_id = "{ssh_import_id}:{import_username}".format(**user_spec) + name="ssh_import_id", description="{ssh_import_id}") + async def _fetch_ssh_keys(self, *, context, ssh_import_id, ssh_data): with self.context.child("ssh_import_id", ssh_import_id): try: cp = await self.run_cmd_checked( @@ -131,22 +130,21 @@ class SSHController(SubiquityTuiController): "").strip().splitlines() if 'ssh-import-id' in self.app.answers.get("Identity", {}): - user_spec['authorized_keys'] = key_material.splitlines() - self.done(user_spec) + ssh_data.authorized_keys = key_material.splitlines() + self.done(ssh_data) else: self.ui.body.confirm_ssh_keys( - user_spec, ssh_import_id, key_material, fingerprints) + ssh_data, ssh_import_id, key_material, fingerprints) - def fetch_ssh_keys(self, user_spec): + def fetch_ssh_keys(self, ssh_import_id, ssh_data): self._fetch_task = schedule_task( - self._fetch_ssh_keys(user_spec=user_spec)) + self._fetch_ssh_keys( + ssh_import_id=ssh_import_id, ssh_data=ssh_data)) - def done(self, result): - log.debug("SSHController.done next_screen result=%s", result) - self.model.install_server = result['install_server'] - self.model.authorized_keys = result.get('authorized_keys', []) - self.model.pwauth = result.get('pwauth', True) - self.model.ssh_import_id = result.get('ssh_import_id', None) + def done(self, data: SSHData): + self.model.install_server = data.install_server + self.model.authorized_keys = data.authorized_keys + self.model.pwauth = data.allow_pw self.configured() self.app.next_screen() diff --git a/subiquity/models/identity.py b/subiquity/models/identity.py index 02670d57..9dd4db2b 100644 --- a/subiquity/models/identity.py +++ b/subiquity/models/identity.py @@ -17,8 +17,6 @@ import logging import attr -from subiquitycore.utils import crypt_password - log = logging.getLogger('subiquity.models.identity') @@ -38,12 +36,15 @@ class IdentityModel(object): self._user = None self._hostname = None - def add_user(self, result): - result = result.copy() - self._hostname = result.pop('hostname') - if not result.get('realname'): - result['realname'] = result['username'] - self._user = User(**result) + def add_user(self, identity_data): + self._hostname = identity_data.hostname + d = {} + d['realname'] = identity_data.realname + d['username'] = identity_data.username + d['password'] = identity_data.crypted_password + if not d['realname']: + d['realname'] = identity_data.username + self._user = User(**d) @property def hostname(self): @@ -53,8 +54,5 @@ class IdentityModel(object): def user(self): return self._user - def encrypt_password(self, passinput): - return crypt_password(passinput) - def __repr__(self): return "".format(self.user, self.hostname) diff --git a/subiquity/ui/views/identity.py b/subiquity/ui/views/identity.py index f258915a..fcab46c7 100644 --- a/subiquity/ui/views/identity.py +++ b/subiquity/ui/views/identity.py @@ -31,8 +31,11 @@ from subiquitycore.ui.form import ( WantsToKnowFormField, ) from subiquitycore.ui.utils import screen +from subiquitycore.utils import crypt_password from subiquitycore.view import BaseView +from subiquity.common.types import IdentityData + log = logging.getLogger("subiquity.views.identity") @@ -153,10 +156,8 @@ class IdentityView(BaseView): "the system. You can configure SSH access on the next screen " "but a password is still needed for sudo.") - def __init__(self, model, controller): - self.model = model + def __init__(self, controller, identity_data): self.controller = controller - self.signal = controller.signal reserved_usernames_path = ( os.path.join(os.environ.get("SNAP", "."), "reserved-usernames")) @@ -171,14 +172,11 @@ class IdentityView(BaseView): else: reserved_usernames.add('root') - if model.user: - initial = { - 'realname': model.user.realname, - 'username': model.user.username, - 'hostname': model.hostname, + initial = { + 'realname': identity_data.realname, + 'username': identity_data.username, + 'hostname': identity_data.hostname, } - else: - initial = {} self.form = IdentityForm(reserved_usernames, initial) @@ -193,10 +191,9 @@ class IdentityView(BaseView): focus_buttons=False)) def done(self, result): - result = { - "hostname": self.form.hostname.value, - "realname": self.form.realname.value, - "username": self.form.username.value, - "password": self.model.encrypt_password(self.form.password.value), - } - self.controller.done(result) + self.controller.done(IdentityData( + hostname=self.form.hostname.value, + realname=self.form.realname.value, + username=self.form.username.value, + crypted_password=crypt_password(self.form.password.value), + )) diff --git a/subiquity/ui/views/mirror.py b/subiquity/ui/views/mirror.py index 7b2ba194..ad8c7f99 100644 --- a/subiquity/ui/views/mirror.py +++ b/subiquity/ui/views/mirror.py @@ -46,11 +46,10 @@ class MirrorView(BaseView): excerpt = _("If you use an alternative mirror for Ubuntu, enter its " "details here.") - def __init__(self, model, controller): - self.model = model + def __init__(self, controller, mirror): self.controller = controller - self.form = MirrorForm(initial={'url': self.model.get_mirror()}) + self.form = MirrorForm(initial={'url': mirror}) connect_signal(self.form, 'submit', self.done) connect_signal(self.form, 'cancel', self.cancel) diff --git a/subiquity/ui/views/proxy.py b/subiquity/ui/views/proxy.py index 887a5e93..aa2afa42 100644 --- a/subiquity/ui/views/proxy.py +++ b/subiquity/ui/views/proxy.py @@ -49,11 +49,10 @@ class ProxyView(BaseView): excerpt = _("If this system requires a proxy to connect to the internet, " "enter its details here.") - def __init__(self, model, controller): - self.model = model + def __init__(self, controller, proxy): self.controller = controller - self.form = ProxyForm(initial={'url': self.model.proxy}) + self.form = ProxyForm(initial={'url': proxy}) connect_signal(self.form, 'submit', self.done) connect_signal(self.form, 'cancel', self.cancel) diff --git a/subiquity/ui/views/ssh.py b/subiquity/ui/views/ssh.py index 1f8c96ac..9a12c424 100644 --- a/subiquity/ui/views/ssh.py +++ b/subiquity/ui/views/ssh.py @@ -51,6 +51,7 @@ from subiquitycore.ui.utils import ( SomethingFailed, ) +from subiquity.common.types import SSHData from subiquity.ui.views.identity import ( UsernameField, ) @@ -176,11 +177,9 @@ class FetchingSSHKeys(WidgetWrap): class ConfirmSSHKeys(Stretchy): - def __init__(self, parent, result, ssh_import_id, key_material, - fingerprints): + def __init__(self, parent, ssh_data, key_material, fingerprints): self.parent = parent - self.result = result - self.ssh_import_id = ssh_import_id + self.ssh_data = ssh_data self.key_material = key_material ok = ok_btn(label=_("Yes"), on_press=self.ok) @@ -212,9 +211,8 @@ class ConfirmSSHKeys(Stretchy): self.parent.remove_overlay() def ok(self, sender): - self.result['authorized_keys'] = self.key_material.splitlines() - self.result['ssh_import_id'] = self.ssh_import_id - self.parent.controller.done(self.result) + self.ssh_data.authorized_keys = self.key_material.splitlines() + self.parent.controller.done(self.ssh_data) class SSHView(BaseView): @@ -223,18 +221,13 @@ class SSHView(BaseView): excerpt = _("You can choose to install the OpenSSH server package to " "enable secure remote access to your server.") - def __init__(self, model, controller): - self.model = model + def __init__(self, controller, ssh_data): self.controller = controller initial = { - "install_server": self.model.install_server, - "pwauth": self.model.pwauth, + "install_server": ssh_data.install_server, + "pwauth": ssh_data.allow_pw, } - if self.model.ssh_import_id: - prefix, username = self.model.ssh_import_id.split(':', 1) - initial['ssh_import_id'] = prefix - initial['import_username'] = username self.form = SSHForm(initial=initial) @@ -276,25 +269,29 @@ class SSHView(BaseView): iu.validate() def done(self, sender): - result = self.form.as_data() - log.debug("User input: {}".format(result)) + log.debug("User input: {}".format(self.form.as_data())) + ssh_data = SSHData( + install_server=self.form.install_server.value, + allow_pw=self.form.pwauth.value) # if user specifed a value, allow user to validate fingerprint if self.form.ssh_import_id.value: + ssh_import_id = self.form.ssh_import_id.value + ":" + \ + self.form.import_username.value fsk = FetchingSSHKeys(self) self.show_overlay(fsk, width=fsk.width, min_width=None) - self.controller.fetch_ssh_keys(result) + self.controller.fetch_ssh_keys( + ssh_import_id=ssh_import_id, ssh_data=ssh_data) else: - self.controller.done(result) + self.controller.done(ssh_data) def cancel(self, result=None): self.controller.cancel() - def confirm_ssh_keys(self, result, ssh_import_id, ssh_key, fingerprints): + def confirm_ssh_keys(self, ssh_data, ssh_import_id, ssh_key, fingerprints): self.remove_overlay() self.show_stretchy_overlay( - ConfirmSSHKeys( - self, result, ssh_import_id, ssh_key, fingerprints)) + ConfirmSSHKeys(self, ssh_data, ssh_key, fingerprints)) def fetching_ssh_keys_failed(self, msg, stderr): self.remove_overlay() diff --git a/subiquity/ui/views/tests/test_identity.py b/subiquity/ui/views/tests/test_identity.py index 6256e8c9..ae6c976e 100644 --- a/subiquity/ui/views/tests/test_identity.py +++ b/subiquity/ui/views/tests/test_identity.py @@ -4,8 +4,8 @@ from unittest import mock from subiquitycore.signals import Signal from subiquitycore.testing import view_helpers -from subiquity.models.identity import IdentityModel from subiquity.controllers.identity import IdentityController +from subiquity.common.types import IdentityData from subiquity.ui.views.identity import IdentityView @@ -21,10 +21,9 @@ valid_data = { class IdentityViewTests(unittest.TestCase): def make_view(self): - model = mock.create_autospec(spec=IdentityModel) controller = mock.create_autospec(spec=IdentityController) controller.signal = mock.create_autospec(spec=Signal) - return IdentityView(model, controller) + return IdentityView(controller, IdentityData()) def test_initial_focus(self): view = self.make_view() @@ -46,16 +45,17 @@ class IdentityViewTests(unittest.TestCase): def test_click_done(self): view = self.make_view() - view_helpers.enter_data(view.form, valid_data) CRYPTED = '' - view.model.encrypt_password.side_effect = lambda p: CRYPTED - expected = valid_data.copy() - expected['password'] = CRYPTED - del expected['confirm_password'] - - done_btn = view_helpers.find_button_matching(view, "^Done$") - view_helpers.click(done_btn) - + with mock.patch('subiquity.ui.views.identity.crypt_password') as cp: + cp.side_effect = lambda p: CRYPTED + view_helpers.enter_data(view.form, valid_data) + done_btn = view_helpers.find_button_matching(view, "^Done$") + view_helpers.click(done_btn) + expected = IdentityData( + realname=valid_data['realname'], + username=valid_data['username'], + hostname=valid_data['hostname'], + crypted_password=CRYPTED) view.controller.done.assert_called_once_with(expected) def test_can_tab_to_done_when_valid(self):