Beispiel #1
0
    def curtin_start_install(self):
        log.debug('Curtin Install: starting curtin')
        self.install_state = InstallState.RUNNING
        self.footer_description = urwid.Text(_("starting..."))
        self.progress_view = ProgressView(self)
        self.footer_spinner = self.progress_view.spinner

        self.ui.set_footer(
            urwid.Columns([('pack', urwid.Text(_("Install in progress:"))),
                           (self.footer_description),
                           ('pack', self.footer_spinner)],
                          dividechars=1))

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('Curtin install cmd: {}'.format(curtin_cmd))
        env = os.environ.copy()
        if 'SNAP' in env:
            del env['SNAP']
        self.run_in_bg(lambda: self._bg_run_command_logged(curtin_cmd, env),
                       self.curtin_install_completed)
Beispiel #2
0
    def curtin_start_install(self):
        log.debug('curtin_start_install')
        self.install_state = InstallState.RUNNING
        self.progress_view = ProgressView(self)

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('curtin install cmd: {}'.format(curtin_cmd))
        self.run_in_bg(lambda: self._bg_run_command_logged(curtin_cmd),
                       self.curtin_install_completed)
Beispiel #3
0
    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.progress_view = ProgressView(self)
        self.crash_report_ref = None
        self._install_state = InstallState.NOT_STARTED

        self.reboot_clicked = asyncio.Event()
        if self.answers.get('reboot', False):
            self.reboot_clicked.set()

        self.unattended_upgrades_proc = None
        self.unattended_upgrades_ctx = None
        self._event_syslog_id = 'curtin_event.%s' % (os.getpid(), )
        self.tb_extractor = TracebackExtractor()
        self.curtin_event_contexts = {}
    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.progress_view = ProgressView(self)
        self.install_state = InstallState.NOT_STARTED
        self.journal_listener_handle = None

        self.reboot_clicked = asyncio.Event()
        if self.answers.get('reboot', False):
            self.reboot_clicked.set()

        self.uu_running = False
        self.uu = None
        self._event_indent = ""
        self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(), )
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(), )
        self.tb_extractor = TracebackExtractor()
        self.curtin_context = None
Beispiel #5
0
class InstallProgressController(BaseController):
    signals = [
        ('installprogress:filesystem-config-done', 'filesystem_config_done'),
        ('installprogress:identity-config-done', 'identity_config_done'),
    ]

    def __init__(self, common):
        super().__init__(common)
        self.answers = self.all_answers.get('InstallProgress', {})
        self.answers.setdefault('reboot', False)
        self.progress_view = None
        self.progress_view_showing = False
        self.install_state = InstallState.NOT_STARTED
        self.journal_listener_handle = None
        self._identity_config_done = False
        self._event_indent = ""
        self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(), )
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(), )

    def filesystem_config_done(self):
        self.curtin_start_install()

    def identity_config_done(self):
        if self.install_state == InstallState.DONE:
            self.postinstall_configuration()
        else:
            self._identity_config_done = True

    def curtin_error(self):
        log.debug('curtin_error')
        self.install_state = InstallState.ERROR
        self.progress_view.spinner.stop()
        self.progress_view.set_status(
            ('info_error', _("An error has occurred")))
        self.progress_view.show_complete(True)
        self.default()

    def _bg_run_command_logged(self, cmd, env):
        cmd = [
            'systemd-cat', '--level-prefix=false',
            '--identifier=' + self._log_syslog_identifier
        ] + cmd
        return utils.run_command(cmd, env=env)

    def _journal_event(self, event):
        if event['SYSLOG_IDENTIFIER'] == self._event_syslog_identifier:
            self.curtin_event(event)
        elif event['SYSLOG_IDENTIFIER'] == self._log_syslog_identifier:
            self.curtin_log(event)

    def curtin_event(self, event):
        e = {}
        for k, v in event.items():
            if k.startswith("CURTIN_"):
                e[k] = v
        log.debug("curtin_event received %r", e)
        event_type = event.get("CURTIN_EVENT_TYPE")
        if event_type not in ['start', 'finish']:
            return
        if event_type == 'start':
            message = event.get("CURTIN_MESSAGE", "??")
            if self.progress_view_showing is not None:
                self.footer_description.set_text(message)
            self.progress_view.add_event(self._event_indent + message)
            self._event_indent += "  "
            self.footer_spinner.start()
        if event_type == 'finish':
            self._event_indent = self._event_indent[:-2]
            self.footer_spinner.stop()

    def curtin_log(self, event):
        self.progress_view.add_log_line(event['MESSAGE'])

    def start_journald_listener(self, identifiers, callback):
        reader = journal.Reader()
        args = []
        for identifier in identifiers:
            args.append("SYSLOG_IDENTIFIER={}".format(identifier))
        reader.add_match(*args)

        def watch():
            if reader.process() != journal.APPEND:
                return
            for event in reader:
                callback(event)

        return self.loop.watch_file(reader.fileno(), watch)

    def _write_config(self, path, config):
        with open(path, 'w') as conf:
            datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
                str(datetime.datetime.utcnow()))
            conf.write(datestr)
            conf.write(yaml.dump(config))

    def _get_curtin_command(self):
        config_file_name = 'subiquity-curtin-install.conf'

        if self.opts.dry_run:
            log.debug("Installprogress: this is a dry-run")
            config_location = os.path.join('.subiquity/', config_file_name)
            curtin_cmd = [
                "python3", "scripts/replay-curtin-log.py",
                "examples/curtin-events.json", self._event_syslog_identifier
            ]
        else:
            log.debug("Installprogress: this is the *REAL* thing")
            config_location = os.path.join('/var/log/installer',
                                           config_file_name)
            curtin_cmd = [
                'curtin', '--showtrace', '-c', config_location, 'install'
            ]

        ident = self._event_syslog_identifier
        self._write_config(
            config_location,
            self.base_model.render(target=TARGET, syslog_identifier=ident))

        return curtin_cmd

    def curtin_start_install(self):
        log.debug('Curtin Install: starting curtin')
        self.install_state = InstallState.RUNNING
        self.footer_description = urwid.Text(_("starting..."))
        self.progress_view = ProgressView(self)
        self.footer_spinner = self.progress_view.spinner

        self.ui.set_footer(
            urwid.Columns([('pack', urwid.Text(_("Install in progress:"))),
                           (self.footer_description),
                           ('pack', self.footer_spinner)],
                          dividechars=1))

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('Curtin install cmd: {}'.format(curtin_cmd))
        env = os.environ.copy()
        if 'SNAP' in env:
            del env['SNAP']
        self.run_in_bg(lambda: self._bg_run_command_logged(curtin_cmd, env),
                       self.curtin_install_completed)

    def curtin_install_completed(self, fut):
        cp = fut.result()
        log.debug('curtin_install completed: %s', cp.returncode)
        if cp.returncode != 0:
            self.curtin_error()
            return
        self.install_state = InstallState.DONE
        log.debug('After curtin install OK')
        self.ui.progress_current += 1
        if not self.progress_view_showing:
            self.ui.set_footer(_("Install complete"))
        else:
            # Re-set footer so progress bar updates.
            self.ui.set_footer(_("Thank you for using Ubuntu!"))
        if self._identity_config_done:
            self.postinstall_configuration()

    def cancel(self):
        pass

    def postinstall_configuration(self):
        # If we need to do anything that takes time here (like running
        # dpkg-reconfigure maas-rack-controller, for example...) we
        # should switch to doing that work in a background thread.
        self.configure_cloud_init()
        self.copy_logs_to_target()

        self.ui.set_header(_("Installation complete!"))
        self.progress_view.set_status(_("Finished install!"))
        self.progress_view.show_complete()

        if self.answers['reboot']:
            self.reboot()

    def configure_cloud_init(self):
        if self.opts.dry_run:
            target = '.subiquity'
        else:
            target = TARGET
        self.base_model.configure_cloud_init(target)

    def copy_logs_to_target(self):
        if self.opts.dry_run:
            return
        utils.run_command(
            ['cp', '-aT', '/var/log/installer', '/target/var/log/installer'])
        try:
            with open('/target/var/log/installer/installer-journal.txt',
                      'w') as output:
                utils.run_command(['journalctl'],
                                  stdout=output,
                                  stderr=subprocess.STDOUT)
        except Exception:
            log.exception("saving journal failed")

    def reboot(self):
        if self.opts.dry_run:
            log.debug('dry-run enabled, skipping reboot, quiting instead')
            self.signal.emit_signal('quit')
        else:
            # Should probably run curtin -c $CONFIG unmount -t TARGET first.
            utils.run_command(["/sbin/reboot"])

    def quit(self):
        if not self.opts.dry_run:
            utils.disable_subiquity()
        self.signal.emit_signal('quit')

    def default(self):
        self.progress_view_showing = True
        if self.install_state == InstallState.RUNNING:
            self.progress_view.title = _("Installing system")
            self.progress_view.footer = _("Thank you for using Ubuntu!")
        elif self.install_state == InstallState.DONE:
            self.progress_view.title = _("Install complete!")
            self.progress_view.footer = _("Thank you for using Ubuntu!")
        elif self.install_state == InstallState.ERROR:
            self.progress_view.title = (
                _('An error occurred during installation'))
            self.progress_view.footer = (
                _('Please report this error in Launchpad'))
        self.ui.set_body(self.progress_view)
Beispiel #6
0
class InstallProgressController(BaseController):
    signals = [
        ('installprogress:filesystem-config-done', 'filesystem_config_done'),
        ('installprogress:identity-config-done',   'identity_config_done'),
        ('installprogress:ssh-config-done',        'ssh_config_done'),
        ('installprogress:snap-config-done',       'snap_config_done'),
    ]

    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.answers.setdefault('reboot', False)
        self.progress_view = None
        self.install_state = InstallState.NOT_STARTED
        self.journal_listener_handle = None
        self._postinstall_prerequisites = {
            'install': False,
            'ssh': False,
            'identity': False,
            'snap': False,
            }
        self._event_indent = ""
        self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(),)
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(),)
        self.sm = None

    def tpath(self, *path):
        return os.path.join(self.model.target, *path)

    def filesystem_config_done(self):
        self.curtin_start_install()

    def _step_done(self, step):
        self._postinstall_prerequisites[step] = True
        log.debug("_step_done %s %s", step, self._postinstall_prerequisites)
        if all(self._postinstall_prerequisites.values()):
            self.start_postinstall_configuration()

    def identity_config_done(self):
        self._step_done('identity')

    def ssh_config_done(self):
        self._step_done('ssh')

    def snap_config_done(self):
        self._step_done('snap')

    def curtin_error(self):
        self.install_state = InstallState.ERROR
        self.app.make_apport_report(
            ErrorReportKind.INSTALL_FAIL, "install failed")
        self.progress_view.spinner.stop()
        if sys.exc_info()[0] is not None:
            self.progress_view.add_log_line(traceback.format_exc())
        self.progress_view.set_status(('info_error',
                                       _("An error has occurred")))
        self.progress_view.show_complete(True)
        self.start_ui()

    def _bg_run_command_logged(self, cmd, **kwargs):
        cmd = ['systemd-cat', '--level-prefix=false',
               '--identifier=' + self._log_syslog_identifier] + cmd
        return utils.run_command(cmd, **kwargs)

    def _journal_event(self, event):
        if event['SYSLOG_IDENTIFIER'] == self._event_syslog_identifier:
            self.curtin_event(event)
        elif event['SYSLOG_IDENTIFIER'] == self._log_syslog_identifier:
            self.curtin_log(event)

    def _install_event_start(self, message):
        log.debug("_install_event_start %s", message)
        self.progress_view.add_event(self._event_indent + message)
        self._event_indent += "  "
        self.progress_view.spinner.start()

    def _install_event_finish(self):
        self._event_indent = self._event_indent[:-2]
        log.debug("_install_event_finish %r", self._event_indent)
        self.progress_view.spinner.stop()

    def curtin_event(self, event):
        e = {}
        for k, v in event.items():
            if k.startswith("CURTIN_"):
                e[k] = v
        log.debug("curtin_event received %r", e)
        event_type = event.get("CURTIN_EVENT_TYPE")
        if event_type not in ['start', 'finish']:
            return
        if event_type == 'start':
            self._install_event_start(event.get("CURTIN_MESSAGE", "??"))
        if event_type == 'finish':
            self._install_event_finish()

    def curtin_log(self, event):
        self.progress_view.add_log_line(event['MESSAGE'])

    def start_journald_listener(self, identifiers, callback):
        reader = journal.Reader()
        args = []
        for identifier in identifiers:
            args.append("SYSLOG_IDENTIFIER={}".format(identifier))
        reader.add_match(*args)

        def watch():
            if reader.process() != journal.APPEND:
                return
            for event in reader:
                callback(event)
        return self.loop.watch_file(reader.fileno(), watch)

    def _write_config(self, path, config):
        with open(path, 'w') as conf:
            datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
                str(datetime.datetime.utcnow()))
            conf.write(datestr)
            conf.write(yaml.dump(config))

    def _get_curtin_command(self):
        config_file_name = 'subiquity-curtin-install.conf'

        if self.opts.dry_run:
            config_location = os.path.join('.subiquity/', config_file_name)
            curtin_cmd = ["python3", "scripts/replay-curtin-log.py",
                          "examples/curtin-events.json",
                          self._event_syslog_identifier]
        else:
            config_location = os.path.join('/var/log/installer',
                                           config_file_name)
            curtin_cmd = [sys.executable, '-m', 'curtin', '--showtrace', '-c',
                          config_location, 'install']

        ident = self._event_syslog_identifier
        self._write_config(config_location,
                           self.model.render(syslog_identifier=ident))

        self.app.note_file_for_apport("CurtinConfig", config_location)
        self.app.note_file_for_apport("CurtinLog", INSTALL_LOG)
        self.app.note_file_for_apport("CurtinErrors", ERROR_TARFILE)

        return curtin_cmd

    def curtin_start_install(self):
        log.debug('curtin_start_install')
        self.install_state = InstallState.RUNNING
        self.progress_view = ProgressView(self)

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('curtin install cmd: {}'.format(curtin_cmd))
        self.run_in_bg(
            lambda: self._bg_run_command_logged(curtin_cmd),
            self.curtin_install_completed)

    def curtin_install_completed(self, fut):
        cp = fut.result()
        log.debug('curtin_install completed: %s', cp.returncode)
        if cp.returncode != 0:
            self.curtin_error()
            return
        self.install_state = InstallState.DONE
        log.debug('After curtin install OK')
        self._step_done('install')

    def cancel(self):
        pass

    def start_postinstall_configuration(self):
        has_network = self.model.network.has_network

        def filter_task(func):
            if func._extra.get('net_only') and not has_network:
                return False
            if func._name == 'install_openssh' \
               and not self.model.ssh.install_server:
                return False
            return True

        log.debug("starting state machine")
        self.sm = StateMachine(self, collect_tasks(self, filter_task))
        self.sm.run()

    @task
    def _bg_drain_curtin_events(self):
        waited = 0.0
        while self._event_indent and waited < 5.0:
            time.sleep(0.1)
            waited += 0.1
        log.debug("waited %s seconds for events to drain", waited)

    @task
    def start_final_configuration(self):
        self._install_event_start("final system configuration")

    @task(label="configuring cloud-init")
    def _bg_configure_cloud_init(self):
        self.model.configure_cloud_init()

    @task(label="installing openssh")
    def _bg_install_openssh(self):
        if self.opts.dry_run:
            cmd = ["sleep", str(2/self.app.scale_factor)]
        else:
            cmd = [
                sys.executable, "-m", "curtin", "system-install", "-t",
                "/target",
                "--", "openssh-server",
                ]
        self._bg_run_command_logged(cmd, check=True)

    @task(label="restoring apt configuration")
    def _bg_restore_apt_config(self):
        if self.opts.dry_run:
            cmds = [["sleep", str(1/self.app.scale_factor)]]
        else:
            cmds = [
                ["umount", self.tpath('etc/apt')],
                ]
            if self.model.network.has_network:
                cmds.append([
                    sys.executable, "-m", "curtin", "in-target", "-t",
                    "/target", "--", "apt-get", "update",
                    ])
            else:
                cmds.append(["umount", self.tpath('var/lib/apt/lists')])
        for cmd in cmds:
            self._bg_run_command_logged(cmd, check=True)

    @task
    def postinstall_complete(self):
        self._install_event_finish()
        self.ui.set_header(_("Installation complete!"))
        self.progress_view.set_status(_("Finished install!"))
        self.progress_view.show_complete()
        self.copy_logs_transition = 'wait'

    @task(net_only=True)
    def uu_start(self):
        self.progress_view.update_running()

    @task(label="downloading and installing security updates",
          transitions={'reboot': 'abort_uu'},
          net_only=True)
    def _bg_run_uu(self):
        target_tmp = os.path.join(self.model.target, "tmp")
        os.makedirs(target_tmp, exist_ok=True)
        apt_conf = tempfile.NamedTemporaryFile(
            dir=target_tmp, delete=False, mode='w')
        apt_conf.write(uu_apt_conf)
        apt_conf.close()
        env = os.environ.copy()
        env["APT_CONFIG"] = apt_conf.name[len(self.model.target):]
        if self.opts.dry_run:
            self.uu = utils.start_command([
                "sleep", str(10/self.app.scale_factor)])
            self.uu.wait()
        else:
            self._bg_run_command_logged([
                sys.executable, "-m", "curtin", "in-target", "-t", "/target",
                "--", "unattended-upgrades", "-v",
            ], env=env, check=True)
        os.remove(apt_conf.name)

    @task(transitions={'success': 'copy_logs_to_target'}, net_only=True)
    def uu_done(self):
        self.progress_view.update_done()

    @task(net_only=True)
    def abort_uu(self):
        self._install_event_finish()

    @task(label="cancelling update", net_only=True)
    def _bg_stop_uu(self):
        if self.opts.dry_run:
            time.sleep(1)
            self.uu.terminate()
        else:
            self._bg_run_command_logged([
                'chroot', '/target',
                '/usr/share/unattended-upgrades/unattended-upgrade-shutdown',
                '--stop-only',
                ], check=True)

    @task(net_only=True, transitions={'success': 'copy_logs_to_target'})
    def _bg_wait_for_uu(self):
        r, w = os.pipe()

        def callback(fut):
            os.write(w, b'x')

        self.sm.subscribe('run_uu', callback)
        os.read(r, 1)
        os.close(w)
        os.close(r)
        self.copy_logs_transition = 'reboot'

    @task(label="copying logs to installed system",
          transitions={'reboot': 'reboot'})
    def _bg_copy_logs_to_target(self):
        if self.opts.dry_run:
            if 'copy-logs-fail' in self.debug_flags:
                raise PermissionError()
            return
        target_logs = self.tpath('var/log/installer')
        utils.run_command(['cp', '-aT', '/var/log/installer', target_logs])
        try:
            with open(os.path.join(target_logs,
                                   'installer-journal.txt'), 'w') as output:
                utils.run_command(
                    ['journalctl'],
                    stdout=output, stderr=subprocess.STDOUT)
        except Exception:
            log.exception("saving journal failed")

    @task(transitions={'wait': 'wait_for_click', 'reboot': 'reboot'})
    def copy_logs_done(self):
        self.sm.transition(self.copy_logs_transition)

    @task(transitions={'reboot': 'reboot'})
    def _bg_wait_for_click(self):
        if not self.answers['reboot']:
            signal.pause()

    @task
    def reboot(self):
        if self.opts.dry_run:
            log.debug('dry-run enabled, skipping reboot, quitting instead')
            self.signal.emit_signal('quit')
        else:
            # TODO Possibly run this earlier, to show a warning; or
            # switch to shutdown if chreipl fails
            if platform.machine() == 's390x':
                utils.run_command(["chreipl", "/target/boot"])
            # Should probably run curtin -c $CONFIG unmount -t TARGET first.
            utils.run_command(["/sbin/reboot"])

    def click_reboot(self):
        if self.sm is None:
            # If the curtin install itself crashes, the state machine
            # that manages post install steps won't be running. Just
            # reboot anyway.
            self.reboot()
        else:
            self.sm.transition('reboot')

    def quit(self):
        if not self.opts.dry_run:
            utils.disable_subiquity()
        self.signal.emit_signal('quit')

    def start_ui(self):
        if self.install_state == InstallState.RUNNING:
            self.progress_view.title = _("Installing system")
        elif self.install_state == InstallState.DONE:
            self.progress_view.title = _("Install complete!")
        elif self.install_state == InstallState.ERROR:
            self.progress_view.title = (
                _('An error occurred during installation'))
        self.ui.set_body(self.progress_view)
 def make_view(self):
     controller = mock.create_autospec(spec=InstallProgressController)
     return ProgressView(controller)
Beispiel #8
0
class InstallProgressController(BaseController):
    signals = [
        ('installprogress:filesystem-config-done', 'filesystem_config_done'),
        ('installprogress:identity-config-done',   'identity_config_done'),
        ('installprogress:ssh-config-done',        'ssh_config_done'),
        ('installprogress:snap-config-done',       'snap_config_done'),
    ]

    def __init__(self, common):
        super().__init__(common)
        self.answers = self.all_answers.get('InstallProgress', {})
        self.answers.setdefault('reboot', False)
        self.progress_view = None
        self.progress_view_showing = False
        self.install_state = InstallState.NOT_STARTED
        self.journal_listener_handle = None
        self._postinstall_prerequisites = {
            'install': False,
            'ssh': False,
            'identity': False,
            'snap': False,
            }
        self._event_indent = ""
        self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(),)
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(),)

    def filesystem_config_done(self):
        self.curtin_start_install()

    def _step_done(self, step):
        self._postinstall_prerequisites[step] = True
        if all(self._postinstall_prerequisites.values()):
            self.start_postinstall_configuration()

    def identity_config_done(self):
        self._step_done('identity')

    def ssh_config_done(self):
        self._step_done('ssh')

    def snap_config_done(self):
        self._step_done('snap')

    def curtin_error(self):
        log.debug('curtin_error')
        self.install_state = InstallState.ERROR
        self.progress_view.spinner.stop()
        self.progress_view.set_status(('info_error',
                                       _("An error has occurred")))
        self.progress_view.show_complete(True)
        self.default()

    def _bg_run_command_logged(self, cmd):
        cmd = ['systemd-cat', '--level-prefix=false',
               '--identifier=' + self._log_syslog_identifier] + cmd
        return utils.run_command(cmd)

    def _journal_event(self, event):
        if event['SYSLOG_IDENTIFIER'] == self._event_syslog_identifier:
            self.curtin_event(event)
        elif event['SYSLOG_IDENTIFIER'] == self._log_syslog_identifier:
            self.curtin_log(event)

    def _install_event_start(self, message):
        log.debug("_install_event_start %s", message)
        self.footer_description.set_text(message)
        self.progress_view.add_event(self._event_indent + message)
        self._event_indent += "  "
        self.footer_spinner.start()

    def _install_event_finish(self):
        self._event_indent = self._event_indent[:-2]
        log.debug("_install_event_finish %r", self._event_indent)
        self.footer_spinner.stop()

    def curtin_event(self, event):
        e = {}
        for k, v in event.items():
            if k.startswith("CURTIN_"):
                e[k] = v
        log.debug("curtin_event received %r", e)
        event_type = event.get("CURTIN_EVENT_TYPE")
        if event_type not in ['start', 'finish']:
            return
        if event_type == 'start':
            self._install_event_start(event.get("CURTIN_MESSAGE", "??"))
        if event_type == 'finish':
            self._install_event_finish()

    def curtin_log(self, event):
        self.progress_view.add_log_line(event['MESSAGE'])

    def start_journald_listener(self, identifiers, callback):
        reader = journal.Reader()
        args = []
        for identifier in identifiers:
            args.append("SYSLOG_IDENTIFIER={}".format(identifier))
        reader.add_match(*args)

        def watch():
            if reader.process() != journal.APPEND:
                return
            for event in reader:
                callback(event)
        return self.loop.watch_file(reader.fileno(), watch)

    def _write_config(self, path, config):
        with open(path, 'w') as conf:
            datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
                str(datetime.datetime.utcnow()))
            conf.write(datestr)
            conf.write(yaml.dump(config))

    def _get_curtin_command(self):
        config_file_name = 'subiquity-curtin-install.conf'

        if self.opts.dry_run:
            log.debug("Installprogress: this is a dry-run")
            config_location = os.path.join('.subiquity/', config_file_name)
            curtin_cmd = ["python3", "scripts/replay-curtin-log.py",
                          "examples/curtin-events.json",
                          self._event_syslog_identifier]
        else:
            log.debug("Installprogress: this is the *REAL* thing")
            config_location = os.path.join('/var/log/installer',
                                           config_file_name)
            curtin_cmd = [sys.executable, '-m', 'curtin', '--showtrace', '-c',
                          config_location, 'install']

        ident = self._event_syslog_identifier
        self._write_config(config_location,
                           self.base_model.render(syslog_identifier=ident))

        return curtin_cmd

    def curtin_start_install(self):
        log.debug('Curtin Install: starting curtin')
        self.install_state = InstallState.RUNNING
        self.footer_description = urwid.Text(_("starting..."))
        self.progress_view = ProgressView(self)
        self.footer_spinner = self.progress_view.spinner

        self.ui.auto_footer = False
        self.ui.set_footer(urwid.Columns(
            [('pack', urwid.Text(_("Install in progress:"))),
             (self.footer_description),
             ('pack', self.footer_spinner)], dividechars=1))

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('Curtin install cmd: {}'.format(curtin_cmd))
        self.run_in_bg(
            lambda: self._bg_run_command_logged(curtin_cmd),
            self.curtin_install_completed)

    def curtin_install_completed(self, fut):
        cp = fut.result()
        log.debug('curtin_install completed: %s', cp.returncode)
        if cp.returncode != 0:
            self.curtin_error()
            return
        self.install_state = InstallState.DONE
        log.debug('After curtin install OK')
        self.ui.progress_current += 1
        if not self.progress_view_showing:
            self.ui.set_footer(_("Install complete"))
        else:
            # Re-set footer so progress bar updates.
            self.ui.set_footer(_("Thank you for using Ubuntu!"))
        self._step_done('install')

    def cancel(self):
        pass

    def _bg_install_openssh_server(self):
        if self.opts.dry_run:
            cmd = [
                "sleep", str(2/self.scale_factor),
                ]
        else:
            cmd = [
                sys.executable, "-m", "curtin", "system-install", "-t",
                "/target",
                "--", "openssh-server",
                ]
        self._bg_run_command_logged(cmd)

    def _bg_cleanup_apt(self):
        if self.opts.dry_run:
            cmd = [
                "sleep", str(2/self.scale_factor),
                ]
        else:
            os.unlink(
                os.path.join(
                    self.base_model.target, "etc/apt/sources.list.d/iso.list"))
            cmd = [
                sys.executable, "-m", "curtin", "in-target", "-t", "/target",
                "--", "apt-get", "update",
                ]
        self._bg_run_command_logged(cmd)

    def start_postinstall_configuration(self):
        self.copy_logs_to_target()

        class w(TaskWatcher):

            def __init__(self, controller):
                self.controller = controller

            def task_complete(self, stage):
                pass

            def task_error(self, stage, info):
                if isinstance(info, tuple):
                    tb = traceback.format_exception(*info)
                    self.controller.curtin_error("".join(tb))
                else:
                    self.controller.curtin_error()

            def tasks_finished(self):
                self.controller.loop.set_alarm_in(
                    0.0,
                    lambda loop, ud: self.controller.postinstall_complete())
        tasks = [
            ('drain', WaitForCurtinEventsTask(self)),
            ('cloud-init', InstallTask(
                self, "configuring cloud-init",
                self.base_model.configure_cloud_init)),
        ]
        if self.base_model.ssh.install_server:
            tasks.extend([
                ('install-ssh', InstallTask(
                    self, "installing OpenSSH server",
                    self._bg_install_openssh_server)),
                ])
        tasks.extend([
            ('cleanup', InstallTask(
                self, "cleaning up apt configuration",
                self._bg_cleanup_apt)),
            ])
        ts = TaskSequence(self.run_in_bg, tasks, w(self))
        ts.run()

    def postinstall_complete(self):
        self._install_event_finish()
        self.ui.set_header(_("Installation complete!"))
        self.progress_view.set_status(_("Finished install!"))
        self.progress_view.show_complete()

        if self.answers['reboot']:
            self.reboot()

    def copy_logs_to_target(self):
        if self.opts.dry_run:
            return
        target_logs = os.path.join(self.base_model.target, 'var/log/installer')
        utils.run_command(['cp', '-aT', '/var/log/installer', target_logs])
        try:
            with open(os.path.join(target_logs,
                                   'installer-journal.txt'), 'w') as output:
                utils.run_command(
                    ['journalctl'],
                    stdout=output, stderr=subprocess.STDOUT)
        except Exception:
            log.exception("saving journal failed")

    def reboot(self):
        if self.opts.dry_run:
            log.debug('dry-run enabled, skipping reboot, quiting instead')
            self.signal.emit_signal('quit')
        else:
            # TODO Possibly run this earlier, to show a warning; or
            # switch to shutdown if chreipl fails
            if platform.machine() == 's390x':
                utils.run_command(["chreipl", "/target/boot"])
            # Should probably run curtin -c $CONFIG unmount -t TARGET first.
            utils.run_command(["/sbin/reboot"])

    def quit(self):
        if not self.opts.dry_run:
            utils.disable_subiquity()
        self.signal.emit_signal('quit')

    def default(self):
        self.progress_view_showing = True
        if self.install_state == InstallState.RUNNING:
            self.progress_view.title = _("Installing system")
            footer = _("Thank you for using Ubuntu!")
        elif self.install_state == InstallState.DONE:
            self.progress_view.title = _("Install complete!")
            footer = _("Thank you for using Ubuntu!")
        elif self.install_state == InstallState.ERROR:
            self.progress_view.title = (
                _('An error occurred during installation'))
            footer = _('Please report this error in Launchpad')
        self.ui.set_body(self.progress_view)
        self.ui.set_footer(footer)
Beispiel #9
0
class InstallProgressController(SubiquityTuiController):
    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.progress_view = ProgressView(self)
        self.crash_report_ref = None
        self._install_state = InstallState.NOT_STARTED

        self.reboot_clicked = asyncio.Event()
        if self.answers.get('reboot', False):
            self.reboot_clicked.set()

        self.unattended_upgrades_proc = None
        self.unattended_upgrades_ctx = None
        self._event_syslog_id = 'curtin_event.%s' % (os.getpid(), )
        self.tb_extractor = TracebackExtractor()
        self.curtin_event_contexts = {}

    def event(self, event):
        if event["SUBIQUITY_EVENT_TYPE"] == "start":
            self.progress_view.event_start(
                event["SUBIQUITY_CONTEXT_ID"],
                event.get("SUBIQUITY_CONTEXT_PARENT_ID"), event["MESSAGE"])
        elif event["SUBIQUITY_EVENT_TYPE"] == "finish":
            self.progress_view.event_finish(event["SUBIQUITY_CONTEXT_ID"])

    def log_line(self, event):
        log_line = event['MESSAGE']
        self.progress_view.add_log_line(log_line)

    def interactive(self):
        return self.app.interactive()

    def start(self):
        self.install_task = schedule_task(self.install())

    @with_context()
    async def apply_autoinstall_config(self, context):
        await self.install_task
        self.app.reboot_on_exit = True

    @property
    def install_state(self):
        return self._install_state

    def update_state(self, state):
        self._install_state = state
        self.progress_view.update_for_state(state)

    def tpath(self, *path):
        return os.path.join(self.model.target, *path)

    def curtin_error(self):
        kw = {}
        if sys.exc_info()[0] is not None:
            log.exception("curtin_error")
            self.progress_view.add_log_line(traceback.format_exc())
        if self.tb_extractor.traceback:
            kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
        crash_report = self.app.make_apport_report(
            ErrorReportKind.INSTALL_FAIL,
            "install failed",
            interrupt=False,
            **kw)
        if crash_report is not None:
            self.crash_report_ref = crash_report.ref()
        self.progress_view.finish_all()
        self.progress_view.set_status(
            ('info_error', _("An error has occurred")))
        if not self.showing:
            self.app.controllers.index = self.controller_index - 1
            self.app.next_screen()
        self.update_state(InstallState.ERROR)
        if self.crash_report_ref is not None:
            self.app.show_error_report(self.crash_report_ref)

    def logged_command(self, cmd):
        return [
            'systemd-cat', '--level-prefix=false',
            '--identifier=' + self.app.log_syslog_id
        ] + cmd

    def log_event(self, event):
        self.curtin_log(event)

    def curtin_event(self, event):
        e = {
            "EVENT_TYPE": "???",
            "MESSAGE": "???",
            "NAME": "???",
            "RESULT": "???",
        }
        prefix = "CURTIN_"
        for k, v in event.items():
            if k.startswith(prefix):
                e[k[len(prefix):]] = v
        event_type = e["EVENT_TYPE"]
        if event_type == 'start':

            def p(name):
                parts = name.split('/')
                for i in range(len(parts), -1, -1):
                    yield '/'.join(parts[:i]), '/'.join(parts[i:])

            curtin_ctx = None
            for pre, post in p(e["NAME"]):
                if pre in self.curtin_event_contexts:
                    parent = self.curtin_event_contexts[pre]
                    curtin_ctx = parent.child(post, e["MESSAGE"])
                    self.curtin_event_contexts[e["NAME"]] = curtin_ctx
                    break
            if curtin_ctx:
                curtin_ctx.enter()
        if event_type == 'finish':
            status = getattr(Status, e["RESULT"], Status.WARN)
            curtin_ctx = self.curtin_event_contexts.pop(e["NAME"], None)
            if curtin_ctx is not None:
                curtin_ctx.exit(result=status)

    def curtin_log(self, event):
        self.tb_extractor.feed(event['MESSAGE'])

    def _write_config(self, path, config):
        with open(path, 'w') as conf:
            datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
                str(datetime.datetime.utcnow()))
            conf.write(datestr)
            conf.write(yaml.dump(config))

    def _get_curtin_command(self):
        config_file_name = 'subiquity-curtin-install.conf'

        if self.opts.dry_run:
            config_location = os.path.join('.subiquity/', config_file_name)
            log_location = '.subiquity/install.log'
            event_file = "examples/curtin-events.json"
            if 'install-fail' in self.app.debug_flags:
                event_file = "examples/curtin-events-fail.json"
            curtin_cmd = [
                "python3",
                "scripts/replay-curtin-log.py",
                event_file,
                self._event_syslog_id,
                log_location,
            ]
        else:
            config_location = os.path.join('/var/log/installer',
                                           config_file_name)
            curtin_cmd = [
                sys.executable, '-m', 'curtin', '--showtrace', '-c',
                config_location, 'install'
            ]
            log_location = INSTALL_LOG

        self._write_config(
            config_location,
            self.model.render(syslog_identifier=self._event_syslog_id))

        self.app.note_file_for_apport("CurtinConfig", config_location)
        self.app.note_file_for_apport("CurtinLog", log_location)
        self.app.note_file_for_apport("CurtinErrors", ERROR_TARFILE)

        return curtin_cmd

    @with_context(description="umounting /target dir")
    async def unmount_target(self, *, context, target):
        cmd = [
            sys.executable,
            '-m',
            'curtin',
            'unmount',
            '-t',
            target,
        ]
        if self.opts.dry_run:
            cmd = ['sleep', str(0.2 / self.app.scale_factor)]
        await arun_command(cmd)
        if not self.opts.dry_run:
            shutil.rmtree(target)

    @with_context(description="installing system",
                  level="INFO",
                  childlevel="DEBUG")
    async def curtin_install(self, *, context):
        log.debug('curtin_install')
        self.curtin_event_contexts[''] = context

        loop = self.app.aio_loop

        fds = [
            journald_listen(loop, [self.app.log_syslog_id], self.curtin_log),
            journald_listen(loop, [self._event_syslog_id], self.curtin_event),
        ]

        curtin_cmd = self._get_curtin_command()

        log.debug('curtin install cmd: {}'.format(curtin_cmd))

        async with self.app.install_lock_file.exclusive():
            try:
                our_tty = os.ttyname(0)
            except OSError:
                # This is a gross hack for testing in travis.
                our_tty = "/dev/not a tty"
            self.app.install_lock_file.write_content(our_tty)
            journal.send("starting install", SYSLOG_IDENTIFIER="subiquity")
            try:
                cp = await arun_command(self.logged_command(curtin_cmd),
                                        check=True)
            finally:
                for fd in fds:
                    loop.remove_reader(fd)

        log.debug('curtin_install completed: %s', cp.returncode)

    def cancel(self):
        pass

    @with_context()
    async def install(self, *, context):
        context.set('is-install-context', True)
        try:
            await asyncio.wait({e.wait() for e in self.model.install_events})

            self.update_state(InstallState.NEEDS_CONFIRMATION)

            await self.model.confirmation.wait()

            self.update_state(InstallState.RUNNING)

            if os.path.exists(self.model.target):
                await self.unmount_target(context=context,
                                          target=self.model.target)

            await self.curtin_install(context=context)

            self.update_state(InstallState.POST_WAIT)

            await asyncio.wait(
                {e.wait()
                 for e in self.model.postinstall_events})

            await self.drain_curtin_events(context=context)

            self.update_state(InstallState.POST_RUNNING)

            await self.postinstall(context=context)

            if self.model.network.has_network:
                self.update_state(InstallState.UU_RUNNING)
                await self.run_unattended_upgrades(context=context)

            self.update_state(InstallState.DONE)
        except Exception:
            self.curtin_error()
            if not self.interactive():
                raise

    async def move_on(self):
        await self.install_task
        self.app.next_screen()

    async def drain_curtin_events(self, *, context):
        waited = 0.0
        while self.progress_view.ongoing and waited < 5.0:
            await asyncio.sleep(0.1)
            waited += 0.1
        log.debug("waited %s seconds for events to drain", waited)
        self.curtin_event_contexts.pop('', None)

    @with_context(description="final system configuration",
                  level="INFO",
                  childlevel="DEBUG")
    async def postinstall(self, *, context):
        autoinstall_path = os.path.join(
            self.app.root, 'var/log/installer/autoinstall-user-data')
        autoinstall_config = "#cloud-config\n" + yaml.dump(
            {"autoinstall": self.app.make_autoinstall()})
        write_file(autoinstall_path, autoinstall_config, mode=0o600)
        await self.configure_cloud_init(context=context)
        packages = []
        if self.model.ssh.install_server:
            packages = ['openssh-server']
        packages.extend(self.app.base_model.packages)
        for package in packages:
            await self.install_package(context=context, package=package)
        await self.restore_apt_config(context=context)

    @with_context(description="configuring cloud-init")
    async def configure_cloud_init(self, context):
        await run_in_thread(self.model.configure_cloud_init)

    @with_context(name="install_{package}", description="installing {package}")
    async def install_package(self, *, context, package):
        if self.opts.dry_run:
            cmd = ["sleep", str(2 / self.app.scale_factor)]
        else:
            cmd = [
                sys.executable,
                "-m",
                "curtin",
                "system-install",
                "-t",
                "/target",
                "--",
                package,
            ]
        await arun_command(self.logged_command(cmd), check=True)

    @with_context(description="restoring apt configuration")
    async def restore_apt_config(self, context):
        if self.opts.dry_run:
            cmds = [["sleep", str(1 / self.app.scale_factor)]]
        else:
            cmds = [
                ["umount", self.tpath('etc/apt')],
            ]
            if self.model.network.has_network:
                cmds.append([
                    sys.executable,
                    "-m",
                    "curtin",
                    "in-target",
                    "-t",
                    "/target",
                    "--",
                    "apt-get",
                    "update",
                ])
            else:
                cmds.append(["umount", self.tpath('var/lib/apt/lists')])
        for cmd in cmds:
            await arun_command(self.logged_command(cmd), check=True)

    @with_context(description="downloading and installing security updates")
    async def run_unattended_upgrades(self, context):
        target_tmp = os.path.join(self.model.target, "tmp")
        os.makedirs(target_tmp, exist_ok=True)
        apt_conf = tempfile.NamedTemporaryFile(dir=target_tmp,
                                               delete=False,
                                               mode='w')
        apt_conf.write(uu_apt_conf)
        apt_conf.close()
        env = os.environ.copy()
        env["APT_CONFIG"] = apt_conf.name[len(self.model.target):]
        self.unattended_upgrades_ctx = context
        if self.opts.dry_run:
            self.unattended_upgrades_proc = await astart_command(
                self.logged_command(["sleep",
                                     str(5 / self.app.scale_factor)]),
                env=env)
        else:
            self.unattended_upgrades_proc = await astart_command(
                self.logged_command([
                    sys.executable,
                    "-m",
                    "curtin",
                    "in-target",
                    "-t",
                    "/target",
                    "--",
                    "unattended-upgrades",
                    "-v",
                ]),
                env=env)
        await self.unattended_upgrades_proc.communicate()
        self.unattended_upgrades_proc = None
        self.unattended_upgrades_ctx = None
        os.remove(apt_conf.name)

    async def stop_unattended_upgrades(self):
        self.progress_view.event_finish(self.unattended_upgrades_ctx)
        with self.unattended_upgrades_ctx.parent.child(
                "stop_unattended_upgrades", "cancelling update"):
            if self.opts.dry_run:
                await asyncio.sleep(1)
                self.unattended_upgrades_proc.terminate()
            else:
                await arun_command(self.logged_command([
                    'chroot',
                    '/target',
                    '/usr/share/unattended-upgrades/'
                    'unattended-upgrade-shutdown',
                    '--stop-only',
                ]),
                                   check=True)

    async def _click_reboot(self):
        if self.unattended_upgrades_ctx is not None:
            self.update_state(InstallState.UU_CANCELLING)
            await self.stop_unattended_upgrades()
        self.reboot_clicked.set()

    def click_reboot(self):
        schedule_task(self._click_reboot())

    def make_ui(self):
        schedule_task(self.move_on())
        return self.progress_view

    def run_answers(self):
        pass
Beispiel #10
0
 def make_view(self):
     controller = mock.create_autospec(spec=ProgressController)
     controller.app = mock.Mock()
     controller.app.aio_loop = None
     return ProgressView(controller)
Beispiel #11
0
 def __init__(self, app):
     super().__init__(app)
     self.progress_view = ProgressView(self)
     self.app_state = None
     self.crash_report_ref = None
     self.answers = app.answers.get("InstallProgress", {})
Beispiel #12
0
class ProgressController(SubiquityTuiController):

    def __init__(self, app):
        super().__init__(app)
        self.progress_view = ProgressView(self)
        self.app_state = None
        self.crash_report_ref = None
        self.answers = app.answers.get("InstallProgress", {})

    def event(self, event):
        if event["SUBIQUITY_EVENT_TYPE"] == "start":
            self.progress_view.event_start(
                event["SUBIQUITY_CONTEXT_ID"],
                event.get("SUBIQUITY_CONTEXT_PARENT_ID"),
                event["MESSAGE"])
        elif event["SUBIQUITY_EVENT_TYPE"] == "finish":
            self.progress_view.event_finish(
                event["SUBIQUITY_CONTEXT_ID"])

    def log_line(self, event):
        log_line = event['MESSAGE']
        self.progress_view.add_log_line(log_line)

    def cancel(self):
        pass

    def start(self):
        self.app.aio_loop.create_task(self._wait_status())

    def click_reboot(self):
        self.app.aio_loop.create_task(self.send_reboot_and_wait())

    async def send_reboot_and_wait(self):
        try:
            await self.app.client.reboot.POST()
        except aiohttp.ClientError:
            pass
        self.app.exit()

    @with_context()
    async def _wait_status(self, context):
        install_running = None
        while True:
            try:
                app_status = await self.app.client.meta.status.GET(
                    cur=self.app_state)
            except aiohttp.ClientError:
                await asyncio.sleep(1)
                continue
            self.app_state = app_status.state

            self.progress_view.update_for_state(self.app_state)
            if self.ui.body is self.progress_view:
                self.ui.set_header(self.progress_view.title)

            if app_status.error is not None:
                if self.crash_report_ref is None:
                    self.crash_report_ref = app_status.error
                    self.ui.set_body(self.progress_view)
                    self.app.show_error_report(self.crash_report_ref)

            if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
                if self.showing:
                    self.app.show_confirm_install()

            if self.app_state == ApplicationState.RUNNING:
                if app_status.confirming_tty != self.app.our_tty:
                    install_running = InstallRunning(
                        self.app, app_status.confirming_tty)
                    self.app.add_global_overlay(install_running)
            else:
                if install_running is not None:
                    self.app.remove_global_overlay(install_running)
                    install_running = None

            if self.app_state == ApplicationState.DONE:
                if self.answers.get('reboot', False):
                    self.click_reboot()

    def make_ui(self):
        if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
            self.app.show_confirm_install()
        return self.progress_view

    def run_answers(self):
        pass
class InstallProgressController(SubiquityController):
    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.progress_view = ProgressView(self)
        self.install_state = InstallState.NOT_STARTED
        self.journal_listener_handle = None

        self.reboot_clicked = asyncio.Event()
        if self.answers.get('reboot', False):
            self.reboot_clicked.set()

        self.uu_running = False
        self.uu = None
        self._event_indent = ""
        self._event_syslog_identifier = 'curtin_event.%s' % (os.getpid(), )
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(), )
        self.tb_extractor = TracebackExtractor()
        self.curtin_context = None

    def interactive(self):
        return self.app.interactive()

    def start(self):
        self.install_task = schedule_task(self.install(self.context))

    async def apply_autoinstall_config(self):
        await self.install_task
        self.app.reboot_on_exit = True

    def tpath(self, *path):
        return os.path.join(self.model.target, *path)

    def curtin_error(self):
        self.install_state = InstallState.ERROR
        kw = {}
        if sys.exc_info()[0] is not None:
            log.exception("curtin_error")
            self.progress_view.add_log_line(traceback.format_exc())
        if self.tb_extractor.traceback:
            kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
        crash_report = self.app.make_apport_report(
            ErrorReportKind.INSTALL_FAIL,
            "install failed",
            interrupt=False,
            **kw)
        self.progress_view.spinner.stop()
        self.progress_view.set_status(
            ('info_error', _("An error has occurred")))
        self.start_ui()
        self.progress_view.show_error(crash_report)

    def logged_command(self, cmd):
        return [
            'systemd-cat', '--level-prefix=false',
            '--identifier=' + self._log_syslog_identifier
        ] + cmd

    def _journal_event(self, event):
        if event['SYSLOG_IDENTIFIER'] == self._event_syslog_identifier:
            self.curtin_event(event)
        elif event['SYSLOG_IDENTIFIER'] == self._log_syslog_identifier:
            self.curtin_log(event)

    @contextlib.contextmanager
    def install_context(self, context, name, description, level, childlevel):
        self._install_event_start(description)
        try:
            subcontext = context.child(name, description, level, childlevel)
            with subcontext:
                yield subcontext
        finally:
            self._install_event_finish()

    def _install_event_start(self, message):
        self.progress_view.add_event(self._event_indent + message)
        self._event_indent += "  "
        self.progress_view.spinner.start()

    def _install_event_finish(self):
        self._event_indent = self._event_indent[:-2]
        self.progress_view.spinner.stop()

    def curtin_event(self, event):
        e = {
            "EVENT_TYPE": "???",
            "MESSAGE": "???",
            "NAME": "???",
            "RESULT": "???",
        }
        prefix = "CURTIN_"
        for k, v in event.items():
            if k.startswith(prefix):
                e[k[len(prefix):]] = v
        event_type = e["EVENT_TYPE"]
        if event_type == 'start':
            self._install_event_start(e["MESSAGE"])
            if self.curtin_context is not None:
                self.curtin_context.child(e["NAME"], e["MESSAGE"]).enter()
        if event_type == 'finish':
            self._install_event_finish()
            status = getattr(Status, e["RESULT"], Status.WARN)
            if self.curtin_context is not None:
                self.curtin_context.child(e["NAME"], e["MESSAGE"]).exit(status)

    def curtin_log(self, event):
        log_line = event['MESSAGE']
        self.progress_view.add_log_line(log_line)
        self.tb_extractor.feed(log_line)

    def start_journald_listener(self, identifiers, callback):
        reader = journal.Reader()
        args = []
        for identifier in identifiers:
            args.append("SYSLOG_IDENTIFIER={}".format(identifier))
        reader.add_match(*args)

        def watch():
            if reader.process() != journal.APPEND:
                return
            for event in reader:
                callback(event)

        loop = asyncio.get_event_loop()
        return loop.add_reader(reader.fileno(), watch)

    def _write_config(self, path, config):
        with open(path, 'w') as conf:
            datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
                str(datetime.datetime.utcnow()))
            conf.write(datestr)
            conf.write(yaml.dump(config))

    def _get_curtin_command(self):
        config_file_name = 'subiquity-curtin-install.conf'

        if self.opts.dry_run:
            config_location = os.path.join('.subiquity/', config_file_name)
            log_location = '.subiquity/install.log'
            event_file = "examples/curtin-events.json"
            if 'install-fail' in self.app.debug_flags:
                event_file = "examples/curtin-events-fail.json"
            curtin_cmd = [
                "python3",
                "scripts/replay-curtin-log.py",
                event_file,
                self._event_syslog_identifier,
                log_location,
            ]
        else:
            config_location = os.path.join('/var/log/installer',
                                           config_file_name)
            curtin_cmd = [
                sys.executable, '-m', 'curtin', '--showtrace', '-c',
                config_location, 'install'
            ]
            log_location = INSTALL_LOG

        ident = self._event_syslog_identifier
        self._write_config(config_location,
                           self.model.render(syslog_identifier=ident))

        self.app.note_file_for_apport("CurtinConfig", config_location)
        self.app.note_file_for_apport("CurtinLog", log_location)
        self.app.note_file_for_apport("CurtinErrors", ERROR_TARFILE)

        return curtin_cmd

    @install_step("installing system", level="INFO", childlevel="DEBUG")
    async def curtin_install(self, context):
        log.debug('curtin_install')
        self.install_state = InstallState.RUNNING
        self.curtin_context = context

        self.journal_listener_handle = self.start_journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)

        curtin_cmd = self._get_curtin_command()

        log.debug('curtin install cmd: {}'.format(curtin_cmd))

        cp = await arun_command(self.logged_command(curtin_cmd), check=True)

        log.debug('curtin_install completed: %s', cp.returncode)

        self.install_state = InstallState.DONE
        log.debug('After curtin install OK')

    def cancel(self):
        pass

    async def install(self, context):
        try:
            await asyncio.wait({e.wait() for e in self.model.install_events})

            await self.curtin_install(context)

            await asyncio.wait(
                {e.wait()
                 for e in self.model.postinstall_events})

            await self.drain_curtin_events(context)

            await self.postinstall(context)

            self.ui.set_header(_("Installation complete!"))
            self.progress_view.set_status(_("Finished install!"))
            self.progress_view.show_complete()

            if self.model.network.has_network:
                self.progress_view.update_running()
                await self.run_unattended_upgrades(context)
                self.progress_view.update_done()

            await self.copy_logs_to_target(context)
        except Exception:
            self.curtin_error()
            if not self.interactive():
                raise

    async def move_on(self):
        await asyncio.wait({self.reboot_clicked.wait(), self.install_task})
        self.app.reboot_on_exit = True
        if not self.opts.dry_run and platform.machine() == 's390x':
            run_command(["chreipl", "/target/boot"])
        self.app.next_screen()

    async def drain_curtin_events(self, context):
        waited = 0.0
        while self._event_indent and waited < 5.0:
            await asyncio.sleep(0.1)
            waited += 0.1
        log.debug("waited %s seconds for events to drain", waited)
        self.curtin_context = None

    @install_step("final system configuration",
                  level="INFO",
                  childlevel="DEBUG")
    async def postinstall(self, context):
        await self.configure_cloud_init(context)
        if self.model.ssh.install_server:
            await self.install_openssh(context)
        await self.restore_apt_config(context)

    @install_step("configuring cloud-init")
    async def configure_cloud_init(self, context):
        await run_in_thread(self.model.configure_cloud_init)

    @install_step("installing openssh")
    async def install_openssh(self, context):
        if self.opts.dry_run:
            cmd = ["sleep", str(2 / self.app.scale_factor)]
        else:
            cmd = [
                sys.executable,
                "-m",
                "curtin",
                "system-install",
                "-t",
                "/target",
                "--",
                "openssh-server",
            ]
        await arun_command(self.logged_command(cmd), check=True)

    @install_step("restoring apt configuration")
    async def restore_apt_config(self, context):
        if self.opts.dry_run:
            cmds = [["sleep", str(1 / self.app.scale_factor)]]
        else:
            cmds = [
                ["umount", self.tpath('etc/apt')],
            ]
            if self.model.network.has_network:
                cmds.append([
                    sys.executable,
                    "-m",
                    "curtin",
                    "in-target",
                    "-t",
                    "/target",
                    "--",
                    "apt-get",
                    "update",
                ])
            else:
                cmds.append(["umount", self.tpath('var/lib/apt/lists')])
        for cmd in cmds:
            await arun_command(self.logged_command(cmd), check=True)

    @install_step("downloading and installing security updates")
    async def run_unattended_upgrades(self, context):
        target_tmp = os.path.join(self.model.target, "tmp")
        os.makedirs(target_tmp, exist_ok=True)
        apt_conf = tempfile.NamedTemporaryFile(dir=target_tmp,
                                               delete=False,
                                               mode='w')
        apt_conf.write(uu_apt_conf)
        apt_conf.close()
        env = os.environ.copy()
        env["APT_CONFIG"] = apt_conf.name[len(self.model.target):]
        self.uu_running = True
        if self.opts.dry_run:
            self.uu = await astart_command(self.logged_command(
                ["sleep", str(5 / self.app.scale_factor)]),
                                           env=env)
        else:
            self.uu = await astart_command(self.logged_command([
                sys.executable,
                "-m",
                "curtin",
                "in-target",
                "-t",
                "/target",
                "--",
                "unattended-upgrades",
                "-v",
            ]),
                                           env=env)
        await self.uu.communicate()
        self.uu_running = False
        self.uu = None
        os.remove(apt_conf.name)

    async def stop_uu(self):
        self._install_event_finish()
        self._install_event_start("cancelling update")
        if self.opts.dry_run:
            await asyncio.sleep(1)
            self.uu.terminate()
        else:
            await arun_command(
                self.logged_command([
                    'chroot',
                    '/target',
                    '/usr/share/unattended-upgrades/unattended-upgrade-shutdown',
                    '--stop-only',
                ],
                                    check=True))

    @install_step("copying logs to installed system")
    async def copy_logs_to_target(self, context):
        if self.opts.dry_run:
            if 'copy-logs-fail' in self.app.debug_flags:
                raise PermissionError()
            return
        target_logs = self.tpath('var/log/installer')
        await arun_command(['cp', '-aT', '/var/log/installer', target_logs])
        try:
            with open(os.path.join(target_logs, 'installer-journal.txt'),
                      'w') as output:
                await arun_command(['journalctl'],
                                   stdout=output,
                                   stderr=subprocess.STDOUT)
        except Exception:
            log.exception("saving journal failed")

    async def _click_reboot(self):
        if self.uu_running:
            await self.stop_uu()
        self.reboot_clicked.set()

    def click_reboot(self):
        schedule_task(self._click_reboot())

    def start_ui(self):
        if self.install_state in [
                InstallState.NOT_STARTED,
                InstallState.RUNNING,
        ]:
            self.progress_view.title = _("Installing system")
        elif self.install_state == InstallState.DONE:
            self.progress_view.title = _("Install complete!")
        elif self.install_state == InstallState.ERROR:
            self.progress_view.title = (
                _('An error occurred during installation'))
        self.ui.set_body(self.progress_view)
        schedule_task(self.move_on())