diff --git a/subiquity/common/types.py b/subiquity/common/types.py index bffa9f03..55eb0cb1 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -79,11 +79,26 @@ class ApplicationStatus: event_syslog_id: str +class PasswordKind(enum.Enum): + NONE = enum.auto() + KNOWN = enum.auto() + UNKNOWN = enum.auto() + + +@attr.s(auto_attribs=True) +class KeyFingerprint: + keytype: str + fingerprint: str + + @attr.s(auto_attribs=True) class LiveSessionSSHInfo: username: str - password: str + password_kind: PasswordKind + password: Optional[str] + authorized_key_fingerprints: List[KeyFingerprint] ips: List[str] + host_key_fingerprints: List[KeyFingerprint] class RefreshCheckState(enum.Enum): diff --git a/subiquity/server/server.py b/subiquity/server/server.py index fcc393cc..f14789f6 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -16,7 +16,6 @@ import asyncio import logging import os -import pwd import shlex import sys import time @@ -26,6 +25,7 @@ from aiohttp import web from cloudinit import atomic_helper, safeyaml, stages from cloudinit.config.cc_set_passwords import rand_user_password +from cloudinit.distros import ug_util import jsonschema @@ -37,6 +37,10 @@ from subiquitycore.async_helpers import run_in_thread, schedule_task from subiquitycore.context import with_context from subiquitycore.core import Application from subiquitycore.prober import Prober +from subiquitycore.ssh import ( + host_key_fingerprints, + user_key_fingerprints, + ) from subiquitycore.utils import arun_command, run_command from subiquity.common.api.server import ( @@ -53,7 +57,9 @@ from subiquity.common.types import ( ApplicationState, ApplicationStatus, ErrorReportRef, + KeyFingerprint, LiveSessionSSHInfo, + PasswordKind, ) from subiquity.server.controller import SubiquityController from subiquity.models.subiquity import SubiquityModel @@ -102,18 +108,32 @@ class MetaController: controller.configured() async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]: - password = self.app.installer_user_passwd - if password is None: - return None ips = [] for dev in self.app.base_model.network.get_all_netdevs(): ips.extend(map(str, dev.actual_global_ip_addresses)) if not ips: return None + username = self.app.installer_user_name + if username is None: + return None + user_fingerprints = [ + KeyFingerprint(keytype, fingerprint) + for keytype, fingerprint in user_key_fingerprints(username) + ] + if self.app.installer_user_passwd_kind == PasswordKind.NONE: + if not user_key_fingerprints: + return None + host_fingerprints = [ + KeyFingerprint(keytype, fingerprint) + for keytype, fingerprint in host_key_fingerprints() + ] return LiveSessionSSHInfo( - username='installer', - password=password, - ips=ips) + username=username, + password_kind=self.app.installer_user_passwd_kind, + password=self.app.installer_user_passwd, + authorized_key_fingerprints=user_fingerprints, + ips=ips, + host_key_fingerprints=host_fingerprints) def get_installer_password_from_cloudinit_log(): @@ -188,6 +208,8 @@ class SubiquityServer(Application): self.confirming_tty = '' self.fatal_error = None self.running_error_commands = False + self.installer_user_name = None + self.installer_user_passwd_kind = PasswordKind.NONE self.installer_user_passwd = None self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid()) @@ -398,14 +420,14 @@ class SubiquityServer(Application): init = stages.Init() init.read_cfg() init.fetch(existing="trust") - cloud = init.cloudify() + self.cloud = init.cloudify() autoinstall_path = '/autoinstall.yaml' - if 'autoinstall' in cloud.cfg: + if 'autoinstall' in self.cloud.cfg: if not os.path.exists(autoinstall_path): atomic_helper.write_file( autoinstall_path, safeyaml.dumps( - cloud.cfg['autoinstall']).encode('utf-8'), + self.cloud.cfg['autoinstall']).encode('utf-8'), mode=0o600) if os.path.exists(autoinstall_path): self.opts.autoinstall = autoinstall_path @@ -414,35 +436,60 @@ class SubiquityServer(Application): "cloud-init status: %r, assumed disabled", status_txt) + def _user_has_password(self, username): + with open('/etc/shadow') as fp: + for line in fp: + if line.startswith(username + ":$"): + return True + return False + def set_installer_password(self): - passfile = self.state_path("installer-passwd") + passfile = self.state_path("installer-user-passwd") + if os.path.exists(passfile): with open(passfile) as fp: - self.installer_user_passwd = fp.read() + contents = fp.read() + self.installer_user_passwd_kind = PasswordKind.KNOWN + self.installer_user_name, self.installer_user_passwd = \ + contents.split(':', 1) return - if self.opts.dry_run: - self.installer_user_passwd = rand_user_password() - return - # refreshing from a version of subiquity that relied on - # cloud-init to set the password to one that does not should - # not reset the password. - passwd = get_installer_password_from_cloudinit_log() - if passwd: - self.installer_user_passwd = passwd - return - try: - pwd.getpwnam('installer') - except KeyError: - log.info("no installer user") - return - passwd = rand_user_password() - cp = run_command('chpasswd', input='installer:'+passwd+'\n') - if cp.returncode == 0: + + def use_passwd(passwd): self.installer_user_passwd = passwd + self.installer_user_passwd_kind = PasswordKind.KNOWN with open(passfile, 'w') as fp: - fp.write(passwd) + fp.write(self.installer_user_name + ':' + passwd) + + if self.opts.dry_run: + self.installer_user_name = os.environ['USER'] + use_passwd(rand_user_password()) + return + + (users, _groups) = ug_util.normalize_users_groups( + self.cloud.cfg, self.cloud.distro) + (username, _user_config) = ug_util.extract_default(users) + + self.installer_user_name = username + + if self._user_has_password(username): + # Was the password set to a random password by a version of + # cloud-init that records the username in the log? (This is the + # case we hit on upgrading the subiquity snap) + passwd = get_installer_password_from_cloudinit_log() + if passwd: + use_passwd(passwd) + else: + self.installer_user_passwd_kind = PasswordKind.UNKNOWN + elif not user_key_fingerprints(username): + passwd = rand_user_password() + cp = run_command('chpasswd', input=username + ':'+passwd+'\n') + if cp.returncode == 0: + use_passwd(passwd) + else: + log.info("setting installer password failed %s", cp) + self.installer_user_passwd_kind = PasswordKind.NONE else: - log.info("setting installer password failed %s", cp) + self.installer_user_passwd_kind = PasswordKind.NONE async def start(self): self.controllers.load_all() diff --git a/subiquity/ui/views/help.py b/subiquity/ui/views/help.py index 12d7a743..233002f8 100644 --- a/subiquity/ui/views/help.py +++ b/subiquity/ui/views/help.py @@ -25,7 +25,7 @@ from urwid import ( ) from subiquitycore.lsb_release import lsb_release -from subiquitycore.ssh import host_key_info +from subiquitycore.ssh import summarize_host_keys from subiquitycore.ui.buttons import ( header_btn, other_btn, @@ -55,6 +55,7 @@ from subiquitycore.ui.width import ( widget_width, ) +from subiquity.common.types import PasswordKind from subiquity.ui.views.error import ErrorReportListStretchy log = logging.getLogger('subiquity.ui.help') @@ -108,13 +109,30 @@ To connect, SSH to any of these addresses: """) SSH_HELP_ONE_ADDRESSES = _(""" -To connect, SSH to {username}@{ip}. +To connect, SSH to {username}@{ip}.""") + +SSH_HELP_EPILOGUE_KNOWN_PASS_NO_KEYS = _("""\ +The password you should use is "{password}".""") + +SSH_HELP_EPILOGUE_UNKNOWN_PASS_NO_KEYS = _("""\ +You should use the preconfigured password passed to cloud-init.""") + +SSH_HELP_EPILOGUE_ONE_KEY = _("""\ +You can log in with the {keytype} key with fingerprint: + + {fingerprint} """) -SSH_HELP_EPILOGUE = _(""" -The password you should use is "{password}". +SSH_HELP_EPILOGUE_MULTIPLE_KEYS = _("""\ +You can log in with one of the following keys: """) +SSH_HELP_EPILOGUE_KNOWN_PASS_KEYS = _(""" +Or you can use the password "{password}".""") + +SSH_HELP_EPILOGUE_UNKNOWN_PASS_KEYS = _(""" +Or you can use the preconfigured password passed to cloud-init.""") + SSH_HELP_NO_ADDRESSES = _(""" Unfortunately this system seems to have no global IP addresses at this time. @@ -132,7 +150,7 @@ def ssh_help_texts(ssh_info): if len(ssh_info.ips) > 0: if len(ssh_info.ips) > 1: - texts.append(rewrap(_(SSH_HELP_MULTIPLE_ADDRESSES))) + texts.append(_(SSH_HELP_MULTIPLE_ADDRESSES)) texts.append("") for ip in ssh_info.ips: texts.append(Text( @@ -141,11 +159,37 @@ def ssh_help_texts(ssh_info): texts.append(_(SSH_HELP_ONE_ADDRESSES).format( username=ssh_info.username, ip=str(ssh_info.ips[0]))) texts.append("") - texts.append( - rewrap(_(SSH_HELP_EPILOGUE).format( - password=ssh_info.password))) + if ssh_info.authorized_key_fingerprints: + if len(ssh_info.authorized_key_fingerprints) == 1: + key = ssh_info.authorized_key_fingerprints[0] + texts.append(Text(_(SSH_HELP_EPILOGUE_ONE_KEY).format( + keytype=key.keytype, fingerprint=key.fingerprint))) + else: + texts.append(_(SSH_HELP_EPILOGUE_MULTIPLE_KEYS)) + texts.append("") + rows = [] + for key in ssh_info.authorized_key_fingerprints: + rows.append( + TableRow([Text(key.keytype), Text(key.fingerprint)])) + texts.append(TablePile(rows)) + if ssh_info.password_kind == PasswordKind.KNOWN: + texts.append("") + texts.append(SSH_HELP_EPILOGUE_KNOWN_PASS_KEYS.format( + password=ssh_info.password)) + elif ssh_info.password_kind == PasswordKind.UNKNOWN: + texts.append("") + texts.append(SSH_HELP_EPILOGUE_UNKNOWN_PASS_KEYS) + else: + if ssh_info.password_kind == PasswordKind.KNOWN: + texts.append(SSH_HELP_EPILOGUE_KNOWN_PASS_NO_KEYS.format( + password=ssh_info.password)) + elif ssh_info.password_kind == PasswordKind.UNKNOWN: + texts.append(SSH_HELP_EPILOGUE_UNKNOWN_PASS_NO_KEYS) texts.append("") - texts.append(Text(host_key_info())) + texts.append(Text(summarize_host_keys([ + (key.keytype, key.fingerprint) + for key in ssh_info.host_key_fingerprints + ]))) else: texts.append("") texts.append(_(SSH_HELP_NO_ADDRESSES)) diff --git a/subiquitycore/ssh.py b/subiquitycore/ssh.py index fa3a5eab..eedd2453 100644 --- a/subiquitycore/ssh.py +++ b/subiquitycore/ssh.py @@ -14,6 +14,8 @@ # along with this program. If not, see . import logging +import os +import pwd from subiquitycore.utils import run_command @@ -36,13 +38,20 @@ def host_key_fingerprints(): keyfiles.append(line.split(None, 1)[1]) info = [] for keyfile in keyfiles: - cp = run_command(['ssh-keygen', '-lf', keyfile]) - if cp.returncode != 0: - log.debug("ssh-keygen -lf %s failed %r", keyfile, cp.stderr) - continue - parts = cp.stdout.strip().split() - length, fingerprint, host, keytype = parts - keytype = keytype.strip('()') + info.extend(fingerprints(keyfile)) + return info + + +def fingerprints(keyfile): + info = [] + cp = run_command(['ssh-keygen', '-lf', keyfile]) + if cp.returncode != 0: + log.debug("ssh-keygen -lf %s failed %r", keyfile, cp.stderr) + return info + for line in cp.stdout.splitlines(): + parts = line.strip().replace('\r', '').split() + fingerprint = parts[1] + keytype = parts[-1].strip('()') info.append((keytype, fingerprint)) return info @@ -55,13 +64,16 @@ host_key_tmpl = """ {keytype:{width}} {fingerprint}""" single_host_key_tmpl = _("""\ -The {keytype} host key fingerprints is: +The {keytype} host key fingerprint is: {fingerprint} """) def host_key_info(): - fingerprints = host_key_fingerprints() + return summarize_host_keys(host_key_fingerprints()) + + +def summarize_host_keys(fingerprints): if len(fingerprints) == 0: return '' if len(fingerprints) == 1: @@ -77,6 +89,19 @@ def host_key_info(): return "".join(lines) +def user_key_fingerprints(username): + try: + user_info = pwd.getpwnam(username) + except KeyError: + log.exception("getpwnam(%s) failed", username) + return [] + user_key_file = '{}/.ssh/authorized_keys'.format(user_info.pw_dir) + if os.path.exists(user_key_file): + return fingerprints(user_key_file) + else: + return [] + + def get_ips_standalone(): from probert.prober import Prober from subiquitycore.models.network import NETDEV_IGNORED_IFACE_TYPES