예제 #1
0
class InstallProgressController(SubiquityController):
    def __init__(self, app):
        super().__init__(app)
        self.model = app.base_model
        self.progress_view = ProgressView(self)
        app.add_event_listener(self)
        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_identifier = 'curtin_event.%s' % (os.getpid(), )
        self._log_syslog_identifier = 'curtin_log.%s' % (os.getpid(), )
        self.tb_extractor = TracebackExtractor()
        self.curtin_event_contexts = {}
        self.confirmation = asyncio.Event()

    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

    def _push_to_progress(self, context):
        if not self.app.interactive():
            return False
        if context.get('hidden', False):
            return False
        controller = context.get('controller')
        if controller is None or controller.interactive():
            return False
        return True

    def report_start_event(self, context, description):
        if self._push_to_progress(context):
            msg = context.full_name()
            if description:
                msg += ': ' + description
            self.progress_view.event_start(context, msg)
        if context.get('is-install-context'):
            self.progress_view.event_start(context, context.description)

    def report_finish_event(self, context, description, status):
        if self._push_to_progress(context):
            self.progress_view.event_finish(context)
        if context.get('is-install-context'):
            self.progress_view.event_finish(context)

    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.finish_all()
        self.progress_view.set_status(
            ('info_error', _("An error has occurred")))
        self.start_ui()
        if crash_report is not None:
            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)

    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(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 _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

    @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.install_state = InstallState.RUNNING
        self.curtin_event_contexts[''] = context

        journal_fd, watcher = journald_listener(
            [self._event_syslog_identifier, self._log_syslog_identifier],
            self._journal_event)
        self.app.aio_loop.add_reader(journal_fd, watcher)

        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")
            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

    @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})

            await self.confirmation.wait()

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

            await self.curtin_install(context=context)

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

            await self.drain_curtin_events(context=context)

            await self.postinstall(context=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=context)
                self.progress_view.update_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:
            await self.stop_unattended_upgrades()
        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())
예제 #2
0
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())