Ejemplo n.º 1
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
Ejemplo n.º 2
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