Example #1
0
    def __init__(self, opts):
        if is_linux_tty():
            self.input_filter = KeyCodesFilter()
        else:
            self.input_filter = DummyKeycodesFilter()

        self.help_menu = HelpMenu(self)
        super().__init__(opts)
        self.interactive = None
        self.server_updated = None
        self.restarting_server = False
        self.global_overlays = []

        try:
            self.our_tty = os.ttyname(0)
        except OSError:
            self.our_tty = "not a tty"

        self.conn = aiohttp.UnixConnector(self.opts.socket)
        self.client = make_client_for_conn(API, self.conn, self.resp_hook)

        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
            self.client)

        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
Example #2
0
    def __init__(self, opts, block_log_dir):
        super().__init__(opts)
        self.block_log_dir = block_log_dir
        self.cloud_init_ok = None
        self._state = ApplicationState.STARTING_UP
        self.state_event = asyncio.Event()
        self.interactive = None
        self.confirming_tty = ''
        self.fatal_error = None
        self.running_error_commands = False

        self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid())
        self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
        self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())

        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
        self.prober = Prober(opts.machine_config, self.debug_flags)
        self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
        if opts.snaps_from_examples:
            connection = FakeSnapdConnection(
                os.path.join(
                    os.path.dirname(os.path.dirname(
                        os.path.dirname(__file__))), "examples", "snaps"),
                self.scale_factor)
        else:
            connection = SnapdConnection(self.root, self.snapd_socket_path)
        self.snapd = AsyncSnapd(connection)
        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.event_listeners = []
        self.autoinstall_config = None
        self.signal.connect_signals([
            ('network-proxy-set', lambda: schedule_task(self._proxy_set())),
            ('network-change', self._network_change),
        ])
Example #3
0
    def __init__(self, opts, block_log_dir):
        if is_linux_tty():
            self.input_filter = KeyCodesFilter()
        else:
            self.input_filter = DummyKeycodesFilter()

        self.help_menu = HelpMenu(self)
        super().__init__(opts)

        self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
        self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())

        self.server_updated = None
        self.restarting_server = False
        self.prober = Prober(opts.machine_config, self.debug_flags)
        journald_listen(self.aio_loop, ["subiquity"],
                        self.subiquity_event,
                        seek=True)
        self.event_listeners = []
        self.install_lock_file = Lockfile(self.state_path("installing"))
        self.global_overlays = []
        self.block_log_dir = block_log_dir
        self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
        if opts.snaps_from_examples:
            connection = FakeSnapdConnection(
                os.path.join(os.path.dirname(os.path.dirname(__file__)),
                             "examples", "snaps"), self.scale_factor)
        else:
            connection = SnapdConnection(self.root, self.snapd_socket_path)
        self.snapd = AsyncSnapd(connection)
        self.signal.connect_signals([
            ('network-proxy-set', lambda: schedule_task(self._proxy_set())),
            ('network-change', self._network_change),
        ])

        self.conn = aiohttp.UnixConnector(self.opts.socket)
        self.client = make_client_for_conn(API, self.conn, self.resp_hook)

        self.autoinstall_config = {}
        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
            self.client)

        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
Example #4
0
 def __init__(self, opts):
     super().__init__(opts)
     self.status = ApplicationState.STARTING
     self.server_proc = None
     self.error_reporter = ErrorReporter(
         self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
Example #5
0
class SubiquityServer(Application):

    project = "subiquity"
    from subiquity.server import controllers as controllers_mod
    controllers = []

    def make_model(self):
        return None

    def __init__(self, opts):
        super().__init__(opts)
        self.status = ApplicationState.STARTING
        self.server_proc = None
        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root)

    def note_file_for_apport(self, key, path):
        self.error_reporter.note_file_for_apport(key, path)

    def note_data_for_apport(self, key, value):
        self.error_reporter.note_data_for_apport(key, value)

    def make_apport_report(self, kind, thing, *, wait=False, **kw):
        return self.error_reporter.make_apport_report(
            kind, thing, wait=wait, **kw)

    @web.middleware
    async def middleware(self, request, handler):
        if self.updated:
            updated = 'yes'
        else:
            updated = 'no'
        controller = await controller_for_request(request)
        if isinstance(controller, SubiquityController):
            if not controller.interactive():
                return web.Response(
                    headers={'x-status': 'skip', 'x-updated': updated})
            elif self.base_model.needs_confirmation(controller.model_name):
                return web.Response(
                    headers={'x-status': 'confirm', 'x-updated': updated})
        resp = await handler(request)
        resp.headers['x-updated'] = updated
        if resp.get('exception'):
            exc = resp['exception']
            log.debug(
                'request to {} crashed'.format(request.raw_path), exc_info=exc)
            report = self.make_apport_report(
                ErrorReportKind.SERVER_REQUEST_FAIL,
                "request to {}".format(request.raw_path),
                exc=exc)
            resp.headers['x-error-report'] = to_json(
                ErrorReportRef, report.ref())
        return resp

    async def start_api_server(self):
        app = web.Application(middlewares=[self.middleware])
        bind(app.router, API.meta, MetaController(self))
        bind(app.router, API.errors, ErrorController(self))
        if self.opts.dry_run:
            from .dryrun import DryRunController
            bind(app.router, API.dry_run, DryRunController(self))
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.UnixSite(runner, self.opts.socket)
        await site.start()

    async def start(self):
        await super().start()
        await self.start_api_server()

    def restart(self):
        cmdline = ['snap', 'run', 'subiquity']
        if self.opts.dry_run:
            cmdline = [
                sys.executable, '-m', 'subiquity.cmd.server',
                ] + sys.argv[1:]
        os.execvp(cmdline[0], cmdline)
Example #6
0
class SubiquityClient(TuiApplication):

    snapd_socket_path = '/run/snapd.socket'

    from subiquity.client import controllers as controllers_mod
    project = "subiquity"

    def make_model(self):
        return None

    def make_ui(self):
        return SubiquityUI(self, self.help_menu)

    controllers = [
        "Welcome",
        "Refresh",
        "Keyboard",
        "Zdev",
        "Network",
        "Proxy",
        "Mirror",
        "Refresh",
        "Filesystem",
        "Identity",
        "SSH",
        "SnapList",
        "Progress",
    ]

    def __init__(self, opts):
        if is_linux_tty():
            self.input_filter = KeyCodesFilter()
        else:
            self.input_filter = DummyKeycodesFilter()

        self.help_menu = HelpMenu(self)
        super().__init__(opts)
        self.interactive = None
        self.server_updated = None
        self.restarting_server = False
        self.global_overlays = []

        try:
            self.our_tty = os.ttyname(0)
        except OSError:
            self.our_tty = "not a tty"

        self.conn = aiohttp.UnixConnector(self.opts.socket)
        self.client = make_client_for_conn(API, self.conn, self.resp_hook)

        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
            self.client)

        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))

    async def _restart_server(self):
        log.debug("_restart_server")
        try:
            await self.client.meta.restart.POST()
        except aiohttp.ServerDisconnectedError:
            pass
        self.restart(remove_last_screen=False)

    def restart(self, remove_last_screen=True, restart_server=False):
        log.debug(f"restart {remove_last_screen} {restart_server}")
        if remove_last_screen:
            self._remove_last_screen()
        if restart_server:
            self.restarting_server = True
            self.ui.block_input = True
            self.aio_loop.create_task(self._restart_server())
            return
        if self.urwid_loop is not None:
            self.urwid_loop.stop()
        cmdline = ['snap', 'run', 'subiquity']
        if self.opts.dry_run:
            cmdline = [
                sys.executable,
                '-m',
                'subiquity.cmd.tui',
            ] + sys.argv[1:] + ['--socket', self.opts.socket]
            if self.opts.server_pid is not None:
                cmdline.extend(['--server-pid', self.opts.server_pid])
            log.debug("restarting %r", cmdline)

        os.execvp(cmdline[0], cmdline)

    def resp_hook(self, response):
        headers = response.headers
        if 'x-updated' in headers:
            if self.server_updated is None:
                self.server_updated = headers['x-updated']
            elif self.server_updated != headers['x-updated']:
                self.restart(remove_last_screen=False)
        status = headers.get('x-status')
        if status == 'skip':
            raise Skip
        elif status == 'confirm':
            raise Confirm
        if headers.get('x-error-report') is not None:
            ref = from_json(ErrorReportRef, headers['x-error-report'])
            raise Abort(ref)
        try:
            response.raise_for_status()
        except aiohttp.ClientError:
            report = self.error_reporter.make_apport_report(
                ErrorReportKind.SERVER_REQUEST_FAIL,
                "request to {}".format(response.url.path))
            raise Abort(report.ref())
        return response

    async def noninteractive_confirmation(self):
        await asyncio.sleep(1)
        yes = _('yes')
        no = _('no')
        answer = no
        print(_("Confirmation is required to continue."))
        print(_("Add 'autoinstall' to your kernel command line to avoid this"))
        print()
        prompt = "\n\n{} ({}|{})".format(_("Continue with autoinstall?"), yes,
                                         no)
        while answer != yes:
            print(prompt)
            answer = await run_in_thread(input)
        await self.confirm_install()

    async def noninteractive_watch_app_state(self, initial_status):
        app_status = initial_status
        confirm_task = None
        while True:
            app_state = app_status.state
            if app_state == ApplicationState.NEEDS_CONFIRMATION:
                if confirm_task is None:
                    confirm_task = self.aio_loop.create_task(
                        self.noninteractive_confirmation())
            elif confirm_task is not None:
                confirm_task.cancel()
                confirm_task = None
            try:
                app_status = await self.client.meta.status.GET(cur=app_state)
            except aiohttp.ClientError:
                await asyncio.sleep(1)
                continue

    def subiquity_event_noninteractive(self, event):
        if event['SUBIQUITY_EVENT_TYPE'] == 'start':
            print('start: ' + event["MESSAGE"])
        elif event['SUBIQUITY_EVENT_TYPE'] == 'finish':
            print('finish: ' + event["MESSAGE"])
            context_name = event.get('SUBIQUITY_CONTEXT_NAME', '')
            if context_name == 'subiquity/Reboot/reboot':
                self.exit()

    async def connect(self):
        print("connecting...", end='', flush=True)
        while True:
            try:
                status = await self.client.meta.status.GET()
            except aiohttp.ClientError:
                await asyncio.sleep(1)
                print(".", end='', flush=True)
            else:
                break
        print("\nconnected")
        journald_listen(self.aio_loop, [status.echo_syslog_id],
                        lambda e: print(e['MESSAGE']))
        if status.state == ApplicationState.STARTING_UP:
            status = await self.client.meta.status.GET(cur=status.state)
            await asyncio.sleep(0.5)
        return status

    async def start(self):
        status = await self.connect()
        self.interactive = status.interactive
        if self.interactive:
            if self.opts.ssh:
                from subiquity.ui.views.help import (ssh_help_texts,
                                                     get_installer_password)
                from subiquitycore.ssh import get_ips_standalone
                texts = ssh_help_texts(
                    get_ips_standalone(),
                    get_installer_password(self.opts.dry_run))
                for line in texts:
                    if hasattr(line, 'text'):
                        if line.text.startswith('installer@'):
                            print(' ' * 4 + line.text)
                        else:
                            print(line.text)
                    else:
                        print(line)
                return
            await super().start()
            journald_listen(self.aio_loop, [status.event_syslog_id],
                            self.controllers.Progress.event)
            journald_listen(self.aio_loop, [status.log_syslog_id],
                            self.controllers.Progress.log_line)
            if not status.cloud_init_ok:
                self.add_global_overlay(CloudInitFail(self))
            self.error_reporter.load_reports()
            for report in self.error_reporter.reports:
                if report.kind == ErrorReportKind.UI and not report.seen:
                    self.show_error_report(report.ref())
                    break
        else:
            if self.opts.run_on_serial:
                # Thanks to the fact that we are launched with agetty's
                # --skip-login option, on serial lines we can end up starting
                # with some strange terminal settings (see the docs for
                # --skip-login in agetty(8)). For an interactive install this
                # does not matter as the settings will soon be clobbered but
                # for a non-interactive one we need to clear things up or the
                # prompting for confirmation will be confusing.
                os.system('stty sane')
            journald_listen(self.aio_loop, [status.event_syslog_id],
                            self.subiquity_event_noninteractive,
                            seek=True)
            self.aio_loop.create_task(
                self.noninteractive_watch_app_state(status))

    def _exception_handler(self, loop, context):
        exc = context.get('exception')
        if self.restarting_server:
            log.debug('ignoring %s %s during restart', exc, type(exc))
            return
        if isinstance(exc, Abort):
            if self.interactive:
                self.show_error_report(exc.error_report_ref)
                return
        super()._exception_handler(loop, context)

    def extra_urwid_loop_args(self):
        return dict(input_filter=self.input_filter.filter)

    def run(self):
        try:
            super().run()
        except Exception:
            print("generating crash report")
            try:
                report = self.make_apport_report(ErrorReportKind.UI,
                                                 "Installer UI",
                                                 interrupt=False,
                                                 wait=True)
                if report is not None:
                    print("report saved to {path}".format(path=report.path))
            except Exception:
                print("report generation failed")
                traceback.print_exc()
            if self.interactive:
                self._remove_last_screen()
                raise
            else:
                traceback.print_exc()
                signal.pause()
        finally:
            if self.opts.server_pid:
                print('killing server {}'.format(self.opts.server_pid))
                pid = int(self.opts.server_pid)
                os.kill(pid, 2)
                os.waitpid(pid, 0)

    async def confirm_install(self):
        await self.client.meta.confirm.POST(self.our_tty)

    def add_global_overlay(self, overlay):
        self.global_overlays.append(overlay)
        if isinstance(self.ui.body, BaseView):
            self.ui.body.show_stretchy_overlay(overlay)

    def remove_global_overlay(self, overlay):
        self.global_overlays.remove(overlay)
        if isinstance(self.ui.body, BaseView):
            self.ui.body.remove_overlay(overlay)

    def _remove_last_screen(self):
        last_screen = self.state_path('last-screen')
        if os.path.exists(last_screen):
            os.unlink(last_screen)

    def exit(self):
        self._remove_last_screen()
        super().exit()

    def select_initial_screen(self):
        last_screen = None
        if self.updated:
            state_path = self.state_path('last-screen')
            if os.path.exists(state_path):
                with open(state_path) as fp:
                    last_screen = fp.read().strip()
        index = 0
        if last_screen:
            for i, controller in enumerate(self.controllers.instances):
                if controller.name == last_screen:
                    index = i
        self.aio_loop.create_task(self._select_initial_screen(index))

    async def _select_initial_screen(self, index):
        endpoint_names = []
        for c in self.controllers.instances[:index]:
            if c.endpoint_name:
                endpoint_names.append(c.endpoint_name)
        if endpoint_names:
            await self.client.meta.mark_configured.POST(endpoint_names)
        self.controllers.index = index - 1
        self.next_screen()

    async def move_screen(self, increment, coro):
        try:
            await super().move_screen(increment, coro)
        except Confirm:
            self.show_confirm_install()

    def show_confirm_install(self):
        log.debug("showing InstallConfirmation over %s", self.ui.body)
        self.add_global_overlay(InstallConfirmation(self))

    async def _start_answers_for_view(self, controller, view):
        # The view returned by make_view_for_controller is not always shown
        # immediately (if progress is being shown, but has not yet been shown
        # for a full second) so wait until it is before starting the answers.
        while self.ui.body is not view:
            await asyncio.sleep(0.1)
        coro = controller.run_answers()
        if inspect.iscoroutine(coro):
            await coro

    async def make_view_for_controller(self, new):
        view = await super().make_view_for_controller(new)
        if new.answers:
            self.aio_loop.create_task(self._start_answers_for_view(new, view))
        with open(self.state_path('last-screen'), 'w') as fp:
            fp.write(new.name)
        return view

    def show_progress(self):
        self.ui.set_body(self.controllers.Progress.progress_view)

    def unhandled_input(self, key):
        if key == 'f1':
            if not self.ui.right_icon.current_help:
                self.ui.right_icon.open_pop_up()
        elif key in ['ctrl z', 'f2']:
            self.debug_shell()
        elif self.opts.dry_run:
            self.unhandled_input_dry_run(key)
        else:
            super().unhandled_input(key)

    def unhandled_input_dry_run(self, key):
        if key in ['ctrl e', 'ctrl r']:
            interrupt = key == 'ctrl e'
            try:
                1 / 0
            except ZeroDivisionError:
                self.make_apport_report(ErrorReportKind.UNKNOWN,
                                        "example",
                                        interrupt=interrupt)
        elif key == 'ctrl u':
            1 / 0
        elif key == 'ctrl b':
            self.aio_loop.create_task(self.client.dry_run.crash.GET())
        else:
            super().unhandled_input(key)

    def debug_shell(self, after_hook=None):
        def _before():
            os.system("clear")
            print(DEBUG_SHELL_INTRO)

        self.run_command_in_foreground(["bash"],
                                       before_hook=_before,
                                       after_hook=after_hook,
                                       cwd='/')

    def note_file_for_apport(self, key, path):
        self.error_reporter.note_file_for_apport(key, path)

    def note_data_for_apport(self, key, value):
        self.error_reporter.note_data_for_apport(key, value)

    def make_apport_report(self, kind, thing, *, interrupt, wait=False, **kw):
        report = self.error_reporter.make_apport_report(kind,
                                                        thing,
                                                        wait=wait,
                                                        **kw)

        if report is not None and interrupt:
            self.show_error_report(report.ref())

        return report

    def show_error_report(self, error_ref):
        log.debug("show_error_report %r", error_ref.base)
        if isinstance(self.ui.body, BaseView):
            w = getattr(self.ui.body._w, 'stretchy', None)
            if isinstance(w, ErrorReportStretchy):
                # Don't show an error if already looking at one.
                return
        self.add_global_overlay(ErrorReportStretchy(self, error_ref))
Example #7
0
class Subiquity(TuiApplication):

    snapd_socket_path = '/run/snapd.socket'

    base_schema = {
        'type': 'object',
        'properties': {
            'version': {
                'type': 'integer',
                'minimum': 1,
                'maximum': 1,
            },
        },
        'required': ['version'],
        'additionalProperties': True,
    }

    from subiquity import controllers as controllers_mod
    project = "subiquity"

    def make_model(self):
        root = '/'
        if self.opts.dry_run:
            root = os.path.abspath('.subiquity')
        return SubiquityModel(root, self.opts.sources)

    def make_ui(self):
        return SubiquityUI(self, self.help_menu)

    controllers = [
        "Early",
        "Reporting",
        "Error",
        "Userdata",
        "Package",
        "Debconf",
        "Welcome",
        "Refresh",
        "Keyboard",
        "Zdev",
        "Network",
        "Proxy",
        "Mirror",
        "Refresh",
        "Filesystem",
        "Identity",
        "SSH",
        "SnapList",
        "InstallProgress",
        "Late",
        "Reboot",
    ]

    def __init__(self, opts, block_log_dir):
        if is_linux_tty():
            self.input_filter = KeyCodesFilter()
        else:
            self.input_filter = DummyKeycodesFilter()

        self.help_menu = HelpMenu(self)
        super().__init__(opts)

        self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
        self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())

        self.server_updated = None
        self.restarting_server = False
        self.prober = Prober(opts.machine_config, self.debug_flags)
        journald_listen(self.aio_loop, ["subiquity"],
                        self.subiquity_event,
                        seek=True)
        self.event_listeners = []
        self.install_lock_file = Lockfile(self.state_path("installing"))
        self.global_overlays = []
        self.block_log_dir = block_log_dir
        self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
        if opts.snaps_from_examples:
            connection = FakeSnapdConnection(
                os.path.join(os.path.dirname(os.path.dirname(__file__)),
                             "examples", "snaps"), self.scale_factor)
        else:
            connection = SnapdConnection(self.root, self.snapd_socket_path)
        self.snapd = AsyncSnapd(connection)
        self.signal.connect_signals([
            ('network-proxy-set', lambda: schedule_task(self._proxy_set())),
            ('network-change', self._network_change),
        ])

        self.conn = aiohttp.UnixConnector(self.opts.socket)
        self.client = make_client_for_conn(API, self.conn, self.resp_hook)

        self.autoinstall_config = {}
        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
            self.client)

        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))

    def subiquity_event(self, event):
        if event["MESSAGE"] == "starting install":
            if event["_PID"] == os.getpid():
                return
            if not self.install_lock_file.is_exclusively_locked():
                return
            from subiquity.ui.views.installprogress import (
                InstallRunning, )
            tty = self.install_lock_file.read_content()
            install_running = InstallRunning(self.ui.body, self, tty)
            self.add_global_overlay(install_running)
            schedule_task(self._hide_install_running(install_running))

    async def _hide_install_running(self, install_running):
        # Wait until the install has completed...
        async with self.install_lock_file.shared():
            # And remove the overlay.
            self.remove_global_overlay(install_running)

    async def _restart_server(self):
        log.debug("_restart_server")
        try:
            await self.client.meta.restart.POST()
        except aiohttp.ServerDisconnectedError:
            pass
        self.restart(remove_last_screen=False)

    def restart(self, remove_last_screen=True, restart_server=False):
        log.debug(f"restart {remove_last_screen} {restart_server}")
        if remove_last_screen:
            self._remove_last_screen()
        if restart_server:
            self.restarting_server = True
            self.ui.block_input = True
            self.aio_loop.create_task(self._restart_server())
            return
        if self.urwid_loop is not None:
            self.urwid_loop.stop()
        cmdline = ['snap', 'run', 'subiquity']
        if self.opts.dry_run:
            cmdline = [
                sys.executable,
                '-m',
                'subiquity.cmd.tui',
            ] + sys.argv[1:] + ['--socket', self.opts.socket]
            if self.opts.server_pid is not None:
                cmdline.extend(['--server-pid', self.opts.server_pid])
            log.debug("restarting %r", cmdline)

        os.execvp(cmdline[0], cmdline)

    def get_primary_tty(self):
        tty = '/dev/tty1'
        for work in self.kernel_cmdline:
            if work.startswith('console='):
                tty = '/dev/' + work[len('console='):].split(',')[0]
        return tty

    async def load_autoinstall_config(self):
        with open(self.opts.autoinstall) as fp:
            self.autoinstall_config = yaml.safe_load(fp)
        primary_tty = self.get_primary_tty()
        try:
            our_tty = os.ttyname(0)
        except OSError:
            # This is a gross hack for testing in travis.
            our_tty = "/dev/not a tty"
        if not self.interactive() and our_tty != primary_tty:
            while True:
                print(
                    _("the installer running on {tty} will perform the "
                      "autoinstall").format(tty=primary_tty))
                print()
                print(_("press enter to start a shell"))
                input()
                os.system("cd / && bash")
        self.controllers.Reporting.start()
        with self.context.child("core_validation", level="INFO"):
            jsonschema.validate(self.autoinstall_config, self.base_schema)
        self.controllers.Reporting.setup_autoinstall()
        self.controllers.Early.setup_autoinstall()
        self.controllers.Error.setup_autoinstall()
        if self.controllers.Early.cmds:
            stamp_file = self.state_path("early-commands")
            if our_tty != primary_tty:
                print(
                    _("waiting for installer running on {tty} to run early "
                      "commands").format(tty=primary_tty))
                while not os.path.exists(stamp_file):
                    await asyncio.sleep(1)
            elif not os.path.exists(stamp_file):
                await self.controllers.Early.run()
                open(stamp_file, 'w').close()
            with open(self.opts.autoinstall) as fp:
                self.autoinstall_config = yaml.safe_load(fp)
            with self.context.child("core_validation", level="INFO"):
                jsonschema.validate(self.autoinstall_config, self.base_schema)
        for controller in self.controllers.instances:
            controller.setup_autoinstall()
        if not self.interactive() and self.opts.run_on_serial:
            # Thanks to the fact that we are launched with agetty's
            # --skip-login option, on serial lines we can end up starting with
            # some strange terminal settings (see the docs for --skip-login in
            # agetty(8)). For an interactive install this does not matter as
            # the settings will soon be clobbered but for a non-interactive
            # one we need to clear things up or the prompting for confirmation
            # in next_screen below will be confusing.
            os.system('stty sane')

    def resp_hook(self, response):
        headers = response.headers
        if 'x-updated' in headers:
            if self.server_updated is None:
                self.server_updated = headers['x-updated']
            elif self.server_updated != headers['x-updated']:
                self.restart(remove_last_screen=False)
        status = response.headers.get('x-status')
        if status == 'skip':
            raise Skip
        elif status == 'confirm':
            raise Confirm
        if headers.get('x-error-report') is not None:
            ref = from_json(ErrorReportRef, headers['x-error-report'])
            raise Abort(ref)
        try:
            response.raise_for_status()
        except aiohttp.ClientError:
            report = self.error_reporter.make_apport_report(
                ErrorReportKind.SERVER_REQUEST_FAIL,
                "request to {}".format(response.url.path))
            raise Abort(report.ref())
        return response

    def subiquity_event_noninteractive(self, event):
        if event['SUBIQUITY_EVENT_TYPE'] == 'start':
            print('start: ' + event["MESSAGE"])
        elif event['SUBIQUITY_EVENT_TYPE'] == 'finish':
            print('finish: ' + event["MESSAGE"])
            context_name = event.get('SUBIQUITY_CONTEXT_NAME', '')
            if context_name == 'subiquity/Reboot/reboot':
                self.exit()

    async def connect(self):
        print("connecting...", end='', flush=True)
        while True:
            try:
                await self.client.meta.status.GET()
            except aiohttp.ClientError:
                await asyncio.sleep(1)
                print(".", end='', flush=True)
            else:
                print()
                break

    def load_serialized_state(self):
        for controller in self.controllers.instances:
            controller.load_state()

    async def start(self):
        self.controllers.load_all()
        self.load_serialized_state()
        await self.connect()
        if self.opts.autoinstall is not None:
            await self.load_autoinstall_config()
            if not self.interactive() and not self.opts.dry_run:
                open('/run/casper-no-prompt', 'w').close()
        interactive = self.interactive()
        if interactive:
            journald_listen(self.aio_loop, [self.event_syslog_id],
                            self.controllers.InstallProgress.event)
            journald_listen(self.aio_loop, [self.log_syslog_id],
                            self.controllers.InstallProgress.log_line)
        else:
            journald_listen(self.aio_loop, [self.event_syslog_id],
                            self.subiquity_event_noninteractive,
                            seek=True)
            await asyncio.sleep(1)
        await super().start(start_urwid=interactive)
        if not interactive:
            self.select_initial_screen()

    def _exception_handler(self, loop, context):
        exc = context.get('exception')
        if self.restarting_server:
            log.debug('ignoring %s %s during restart', exc, type(exc))
            return
        if isinstance(exc, Abort):
            self.show_error_report(exc.error_report_ref)
            return
        super()._exception_handler(loop, context)

    def _remove_last_screen(self):
        last_screen = self.state_path('last-screen')
        if os.path.exists(last_screen):
            os.unlink(last_screen)

    def exit(self):
        self._remove_last_screen()
        super().exit()

    def extra_urwid_loop_args(self):
        return dict(input_filter=self.input_filter.filter)

    def run(self):
        try:
            super().run()
        except Exception:
            print("generating crash report")
            try:
                report = self.make_apport_report(ErrorReportKind.UI,
                                                 "Installer UI",
                                                 interrupt=False,
                                                 wait=True)
                if report is not None:
                    print("report saved to {path}".format(path=report.path))
            except Exception:
                print("report generation failed")
                traceback.print_exc()
            Error = getattr(self.controllers, "Error", None)
            if Error is not None and Error.cmds:
                new_loop = asyncio.new_event_loop()
                asyncio.set_event_loop(new_loop)
                new_loop.run_until_complete(Error.run())
            if self.interactive():
                self._remove_last_screen()
                raise
            else:
                traceback.print_exc()
                signal.pause()
        finally:
            if self.opts.server_pid:
                print('killing server {}'.format(self.opts.server_pid))
                pid = int(self.opts.server_pid)
                os.kill(pid, 2)
                os.waitpid(pid, 0)

    def add_event_listener(self, listener):
        self.event_listeners.append(listener)

    def report_start_event(self, context, description):
        for listener in self.event_listeners:
            listener.report_start_event(context, description)
        self._maybe_push_to_journal('start', context, description)

    def report_finish_event(self, context, description, status):
        for listener in self.event_listeners:
            listener.report_finish_event(context, description, status)
        self._maybe_push_to_journal('finish', context, description)

    def _maybe_push_to_journal(self, event_type, context, description):
        if not context.get('is-install-context') and self.interactive():
            controller = context.get('controller')
            if controller is None or controller.interactive():
                return
        if context.get('request'):
            return
        indent = context.full_name().count('/') - 2
        if context.get('is-install-context') and self.interactive():
            indent -= 1
            msg = context.description
        else:
            msg = context.full_name()
            if description:
                msg += ': ' + description
        msg = '  ' * indent + msg
        if context.parent:
            parent_id = str(context.parent.id)
        else:
            parent_id = ''
        journal.send(msg,
                     PRIORITY=context.level,
                     SYSLOG_IDENTIFIER=self.event_syslog_id,
                     SUBIQUITY_CONTEXT_NAME=context.full_name(),
                     SUBIQUITY_EVENT_TYPE=event_type,
                     SUBIQUITY_CONTEXT_ID=str(context.id),
                     SUBIQUITY_CONTEXT_PARENT_ID=parent_id)

    async def confirm_install(self):
        self.base_model.confirm()

    def interactive(self):
        if not self.autoinstall_config:
            return True
        return bool(self.autoinstall_config.get('interactive-sections'))

    def add_global_overlay(self, overlay):
        self.global_overlays.append(overlay)
        if isinstance(self.ui.body, BaseView):
            self.ui.body.show_stretchy_overlay(overlay)

    def remove_global_overlay(self, overlay):
        self.global_overlays.remove(overlay)
        if isinstance(self.ui.body, BaseView):
            self.ui.body.remove_overlay(overlay)

    def initial_controller_index(self):
        if not self.updated:
            return 0
        state_path = self.state_path('last-screen')
        if not os.path.exists(state_path):
            return 0
        with open(state_path) as fp:
            last_screen = fp.read().strip()
        controller_index = 0
        for i, controller in enumerate(self.controllers.instances):
            if controller.name == last_screen:
                controller_index = i
        return controller_index

    def select_initial_screen(self):
        self.error_reporter.load_reports()
        for report in self.error_reporter.reports:
            if report.kind == ErrorReportKind.UI and not report.seen:
                self.show_error_report(report.ref())
                break
        index = self.initial_controller_index()
        for controller in self.controllers.instances[:index]:
            controller.configured()
        self.controllers.index = index - 1
        self.next_screen()

    async def move_screen(self, increment, coro):
        try:
            await super().move_screen(increment, coro)
        except Confirm:
            if self.interactive():
                log.debug("showing InstallConfirmation over %s", self.ui.body)
                from subiquity.ui.views.installprogress import (
                    InstallConfirmation, )
                self.add_global_overlay(InstallConfirmation(self))
            else:
                yes = _('yes')
                no = _('no')
                answer = no
                if 'autoinstall' in self.kernel_cmdline:
                    answer = yes
                else:
                    print(_("Confirmation is required to continue."))
                    print(
                        _("Add 'autoinstall' to your kernel command line to"
                          " avoid this"))
                    print()
                prompt = "\n\n{} ({}|{})".format(
                    _("Continue with autoinstall?"), yes, no)
                while answer != yes:
                    print(prompt)
                    answer = input()
                self.next_screen(self.confirm_install())

    async def make_view_for_controller(self, new):
        if self.base_model.needs_confirmation(new.model_name):
            raise Confirm
        if new.interactive():
            view = await super().make_view_for_controller(new)
            if new.answers:
                self.aio_loop.call_soon(new.run_answers)
            with open(self.state_path('last-screen'), 'w') as fp:
                fp.write(new.name)
            return view
        else:
            if self.autoinstall_config and not new.autoinstall_applied:
                await new.apply_autoinstall_config()
                new.autoinstall_applied = True
            new.configured()
            raise Skip

    def show_progress(self):
        self.ui.set_body(self.controllers.InstallProgress.progress_view)

    def _network_change(self):
        self.signal.emit_signal('snapd-network-change')

    async def _proxy_set(self):
        await run_in_thread(self.snapd.connection.configure_proxy,
                            self.base_model.proxy)
        self.signal.emit_signal('snapd-network-change')

    def unhandled_input(self, key):
        if key == 'f1':
            if not self.ui.right_icon.current_help:
                self.ui.right_icon.open_pop_up()
        elif key in ['ctrl z', 'f2']:
            self.debug_shell()
        elif self.opts.dry_run:
            self.unhandled_input_dry_run(key)
        else:
            super().unhandled_input(key)

    def unhandled_input_dry_run(self, key):
        if key == 'ctrl g':
            from systemd import journal

            async def mock_install():
                async with self.install_lock_file.exclusive():
                    self.install_lock_file.write_content("nowhere")
                    journal.send("starting install",
                                 SYSLOG_IDENTIFIER="subiquity")
                    await asyncio.sleep(5)

            schedule_task(mock_install())
        elif key in ['ctrl e', 'ctrl r']:
            interrupt = key == 'ctrl e'
            try:
                1 / 0
            except ZeroDivisionError:
                self.make_apport_report(ErrorReportKind.UNKNOWN,
                                        "example",
                                        interrupt=interrupt)
        elif key == 'ctrl u':
            1 / 0
        elif key == 'ctrl b':
            self.aio_loop.create_task(self.client.dry_run.crash.GET())
        else:
            super().unhandled_input(key)

    def debug_shell(self, after_hook=None):
        def _before():
            os.system("clear")
            print(DEBUG_SHELL_INTRO)

        self.run_command_in_foreground(["bash"],
                                       before_hook=_before,
                                       after_hook=after_hook,
                                       cwd='/')

    def note_file_for_apport(self, key, path):
        self.error_reporter.note_file_for_apport(key, path)

    def note_data_for_apport(self, key, value):
        self.error_reporter.note_data_for_apport(key, value)

    def make_apport_report(self, kind, thing, *, interrupt, wait=False, **kw):
        report = self.error_reporter.make_apport_report(kind,
                                                        thing,
                                                        wait=wait,
                                                        **kw)

        if report is not None and interrupt and self.interactive():
            self.show_error_report(report.ref())

        return report

    def show_error_report(self, error_ref):
        log.debug("show_error_report %r", error_ref.base)
        if isinstance(self.ui.body, BaseView):
            w = getattr(self.ui.body._w, 'stretchy', None)
            if isinstance(w, ErrorReportStretchy):
                # Don't show an error if already looking at one.
                return
        self.add_global_overlay(ErrorReportStretchy(self, error_ref))

    def make_autoinstall(self):
        config = {'version': 1}
        for controller in self.controllers.instances:
            controller_conf = controller.make_autoinstall()
            if controller_conf:
                config[controller.autoinstall_key] = controller_conf
        return config
Example #8
0
class SubiquityServer(Application):

    snapd_socket_path = '/run/snapd.socket'

    base_schema = {
        'type': 'object',
        'properties': {
            'version': {
                'type': 'integer',
                'minimum': 1,
                'maximum': 1,
                },
            },
        'required': ['version'],
        'additionalProperties': True,
        }

    project = "subiquity"
    from subiquity.server import controllers as controllers_mod
    controllers = [
        "Early",
        "Reporting",
        "Error",
        "Userdata",
        "Package",
        "Debconf",
        "Locale",
        "Refresh",
        "Kernel",
        "Keyboard",
        "Zdev",
        "Network",
        "Proxy",
        "Mirror",
        "Filesystem",
        "Identity",
        "SSH",
        "SnapList",
        "Install",
        "Updates",
        "Late",
        "Reboot",
        ]

    def make_model(self):
        root = '/'
        if self.opts.dry_run:
            root = os.path.abspath('.subiquity')
        return SubiquityModel(root)

    def __init__(self, opts, block_log_dir):
        super().__init__(opts)
        self.block_log_dir = block_log_dir
        self.cloud = None
        self.cloud_init_ok = None
        self._state = ApplicationState.STARTING_UP
        self.state_event = asyncio.Event()
        self.interactive = None
        self.confirming_tty = ''
        self.fatal_error = None
        self.running_error_commands = False
        self.installer_user_name = None
        self.installer_user_passwd_kind = PasswordKind.NONE
        self.installer_user_passwd = None

        self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid())
        self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
        self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())

        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
        self.prober = Prober(opts.machine_config, self.debug_flags)
        self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
        if opts.snaps_from_examples:
            connection = FakeSnapdConnection(
                os.path.join(
                    os.path.dirname(
                        os.path.dirname(
                            os.path.dirname(__file__))),
                    "examples", "snaps"),
                self.scale_factor)
        else:
            connection = SnapdConnection(self.root, self.snapd_socket_path)
        self.snapd = AsyncSnapd(connection)
        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.event_listeners = []
        self.autoinstall_config = None
        self.hub.subscribe('network-up', self._network_change)
        self.hub.subscribe('network-proxy-set', self._proxy_set)
        self.geoip = GeoIP(self)

    def load_serialized_state(self):
        for controller in self.controllers.instances:
            controller.load_state()

    def add_event_listener(self, listener):
        self.event_listeners.append(listener)

    def _maybe_push_to_journal(self, event_type, context, description):
        if not context.get('is-install-context') and \
          self.interactive in [True, None]:
            controller = context.get('controller')
            if controller is None or controller.interactive():
                return
        if context.get('request'):
            return
        indent = context.full_name().count('/') - 2
        if context.get('is-install-context') and self.interactive:
            indent -= 1
            msg = context.description
        else:
            msg = context.full_name()
            if description:
                msg += ': ' + description
        msg = '  ' * indent + msg
        if context.parent:
            parent_id = str(context.parent.id)
        else:
            parent_id = ''
        journal.send(
            msg,
            PRIORITY=context.level,
            SYSLOG_IDENTIFIER=self.event_syslog_id,
            SUBIQUITY_CONTEXT_NAME=context.full_name(),
            SUBIQUITY_EVENT_TYPE=event_type,
            SUBIQUITY_CONTEXT_ID=str(context.id),
            SUBIQUITY_CONTEXT_PARENT_ID=parent_id)

    def report_start_event(self, context, description):
        for listener in self.event_listeners:
            listener.report_start_event(context, description)
        self._maybe_push_to_journal('start', context, description)

    def report_finish_event(self, context, description, status):
        for listener in self.event_listeners:
            listener.report_finish_event(context, description, status)
        self._maybe_push_to_journal('finish', context, description)

    @property
    def state(self):
        return self._state

    def update_state(self, state):
        self._state = state
        self.state_event.set()
        self.state_event.clear()

    def note_file_for_apport(self, key, path):
        self.error_reporter.note_file_for_apport(key, path)

    def note_data_for_apport(self, key, value):
        self.error_reporter.note_data_for_apport(key, value)

    def make_apport_report(self, kind, thing, *, wait=False, **kw):
        return self.error_reporter.make_apport_report(
            kind, thing, wait=wait, **kw)

    async def _run_error_cmds(self, report):
        await report._info_task
        Error = getattr(self.controllers, "Error", None)
        if Error is not None and Error.cmds:
            try:
                await Error.run()
            except Exception:
                log.exception("running error-commands failed")
        if not self.interactive:
            self.update_state(ApplicationState.ERROR)

    def _exception_handler(self, loop, context):
        exc = context.get('exception')
        if exc is None:
            super()._exception_handler(loop, context)
            return
        report = self.error_reporter.report_for_exc(exc)
        log.error("top level error", exc_info=exc)
        if not report:
            report = self.make_apport_report(
                ErrorReportKind.UNKNOWN, "unknown error",
                exc=exc)
        self.fatal_error = report
        if self.interactive:
            self.update_state(ApplicationState.ERROR)
        if not self.running_error_commands:
            self.running_error_commands = True
            self.aio_loop.create_task(self._run_error_cmds(report))

    @web.middleware
    async def middleware(self, request, handler):
        override_status = None
        controller = await controller_for_request(request)
        if isinstance(controller, SubiquityController):
            if not controller.interactive():
                override_status = 'skip'
            elif self.state == ApplicationState.NEEDS_CONFIRMATION:
                if self.base_model.needs_configuration(controller.model_name):
                    override_status = 'confirm'
        if override_status is not None:
            resp = web.Response(headers={'x-status': override_status})
        else:
            resp = await handler(request)
        if self.updated:
            resp.headers['x-updated'] = 'yes'
        else:
            resp.headers['x-updated'] = 'no'
        if resp.get('exception'):
            exc = resp['exception']
            log.debug(
                'request to {} crashed'.format(request.raw_path), exc_info=exc)
            report = self.make_apport_report(
                ErrorReportKind.SERVER_REQUEST_FAIL,
                "request to {}".format(request.raw_path),
                exc=exc)
            resp.headers['x-error-report'] = to_json(
                ErrorReportRef, report.ref())
        return resp

    @with_context()
    async def apply_autoinstall_config(self, context):
        for controller in self.controllers.instances:
            if controller.interactive():
                log.debug(
                    "apply_autoinstall_config: skipping %s as interactive",
                    controller.name)
                continue
            await controller.apply_autoinstall_config()
            controller.configured()

    def load_autoinstall_config(self, *, only_early):
        log.debug("load_autoinstall_config only_early %s", only_early)
        if self.opts.autoinstall is None:
            return
        with open(self.opts.autoinstall) as fp:
            self.autoinstall_config = yaml.safe_load(fp)
        if only_early:
            self.controllers.Reporting.setup_autoinstall()
            self.controllers.Reporting.start()
            self.controllers.Error.setup_autoinstall()
            with self.context.child("core_validation", level="INFO"):
                jsonschema.validate(self.autoinstall_config, self.base_schema)
            self.controllers.Early.setup_autoinstall()
        else:
            for controller in self.controllers.instances:
                controller.setup_autoinstall()

    async def start_api_server(self):
        app = web.Application(middlewares=[self.middleware])
        bind(app.router, API.meta, MetaController(self))
        bind(app.router, API.errors, ErrorController(self))
        if self.opts.dry_run:
            from .dryrun import DryRunController
            bind(app.router, API.dry_run, DryRunController(self))
        for controller in self.controllers.instances:
            controller.add_routes(app)
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.UnixSite(runner, self.opts.socket)
        await site.start()
        # It is intended that a non-root client can connect.
        os.chmod(self.opts.socket, 0o666)

    async def wait_for_cloudinit(self):
        if self.opts.dry_run:
            self.cloud_init_ok = True
            return
        ci_start = time.time()
        status_coro = arun_command(["cloud-init", "status", "--wait"])
        try:
            status_cp = await asyncio.wait_for(status_coro, 600)
        except asyncio.CancelledError:
            status_txt = '<timeout>'
            self.cloud_init_ok = False
        else:
            status_txt = status_cp.stdout
            self.cloud_init_ok = True
        log.debug("waited %ss for cloud-init", time.time() - ci_start)
        if "status: done" in status_txt:
            log.debug("loading cloud config")
            init = stages.Init()
            init.read_cfg()
            init.fetch(existing="trust")
            self.cloud = init.cloudify()
            autoinstall_path = '/autoinstall.yaml'
            if 'autoinstall' in self.cloud.cfg:
                if not os.path.exists(autoinstall_path):
                    atomic_helper.write_file(
                        autoinstall_path,
                        safeyaml.dumps(
                            self.cloud.cfg['autoinstall']).encode('utf-8'),
                        mode=0o600)
            if os.path.exists(autoinstall_path):
                self.opts.autoinstall = autoinstall_path
        else:
            log.debug(
                "cloud-init status: %r, assumed disabled",
                status_txt)

    def _user_has_password(self, username):
        with open('/etc/shadow') as fp:
            for line in fp:
                if line.startswith(username + ":$"):
                    return True
        return False

    def set_installer_password(self):
        if self.cloud is None:
            return

        passfile = self.state_path("installer-user-passwd")

        if os.path.exists(passfile):
            with open(passfile) as fp:
                contents = fp.read()
            self.installer_user_passwd_kind = PasswordKind.KNOWN
            self.installer_user_name, self.installer_user_passwd = \
                contents.split(':', 1)
            return

        def use_passwd(passwd):
            self.installer_user_passwd = passwd
            self.installer_user_passwd_kind = PasswordKind.KNOWN
            with open(passfile, 'w') as fp:
                fp.write(self.installer_user_name + ':' + passwd)

        if self.opts.dry_run:
            self.installer_user_name = os.environ['USER']
            use_passwd(rand_user_password())
            return

        (users, _groups) = ug_util.normalize_users_groups(
            self.cloud.cfg, self.cloud.distro)
        (username, _user_config) = ug_util.extract_default(users)

        self.installer_user_name = username

        if self._user_has_password(username):
            # Was the password set to a random password by a version of
            # cloud-init that records the username in the log?  (This is the
            # case we hit on upgrading the subiquity snap)
            passwd = get_installer_password_from_cloudinit_log()
            if passwd:
                use_passwd(passwd)
            else:
                self.installer_user_passwd_kind = PasswordKind.UNKNOWN
        elif not user_key_fingerprints(username):
            passwd = rand_user_password()
            cp = run_command('chpasswd', input=username + ':'+passwd+'\n')
            if cp.returncode == 0:
                use_passwd(passwd)
            else:
                log.info("setting installer password failed %s", cp)
                self.installer_user_passwd_kind = PasswordKind.NONE
        else:
            self.installer_user_passwd_kind = PasswordKind.NONE

    async def start(self):
        self.controllers.load_all()
        await self.start_api_server()
        self.update_state(ApplicationState.CLOUD_INIT_WAIT)
        await self.wait_for_cloudinit()
        self.set_installer_password()
        self.load_autoinstall_config(only_early=True)
        if self.autoinstall_config and self.controllers.Early.cmds:
            stamp_file = self.state_path("early-commands")
            if not os.path.exists(stamp_file):
                self.update_state(ApplicationState.EARLY_COMMANDS)
                # Just wait a second for any clients to get ready to print
                # output.
                await asyncio.sleep(1)
                await self.controllers.Early.run()
                open(stamp_file, 'w').close()
                await asyncio.sleep(1)
        self.load_autoinstall_config(only_early=False)
        if self.autoinstall_config:
            self.interactive = bool(
                self.autoinstall_config.get('interactive-sections'))
        else:
            self.interactive = True
        if not self.interactive and not self.opts.dry_run:
            open('/run/casper-no-prompt', 'w').close()
        self.load_serialized_state()
        self.update_state(ApplicationState.WAITING)
        await super().start()
        await self.apply_autoinstall_config()

    def _network_change(self):
        self.hub.broadcast('snapd-network-change')

    async def _proxy_set(self):
        await run_in_thread(
            self.snapd.connection.configure_proxy, self.base_model.proxy)
        self.hub.broadcast('snapd-network-change')

    def restart(self):
        cmdline = ['snap', 'run', 'subiquity.subiquity-server']
        if self.opts.dry_run:
            cmdline = [
                sys.executable, '-m', 'subiquity.cmd.server',
                ] + sys.argv[1:]
        os.execvp(cmdline[0], cmdline)

    def make_autoinstall(self):
        config = {'version': 1}
        for controller in self.controllers.instances:
            controller_conf = controller.make_autoinstall()
            if controller_conf:
                config[controller.autoinstall_key] = controller_conf
        return config
Example #9
0
class SubiquityServer(Application):

    snapd_socket_path = '/run/snapd.socket'

    base_schema = {
        'type': 'object',
        'properties': {
            'version': {
                'type': 'integer',
                'minimum': 1,
                'maximum': 1,
                },
            },
        'required': ['version'],
        'additionalProperties': True,
        }

    project = "subiquity"
    from subiquity.server import controllers as controllers_mod
    controllers = [
        "Early",
        "Reporting",
        "Error",
        "Userdata",
        "Package",
        "Debconf",
        "Locale",
        "Refresh",
        "Keyboard",
        "Zdev",
        "Network",
        "Proxy",
        "Mirror",
        "Filesystem",
        "Identity",
        "SSH",
        "SnapList",
        "Install",
        "Late",
        "Reboot",
        ]

    def make_model(self):
        root = '/'
        if self.opts.dry_run:
            root = os.path.abspath('.subiquity')
        return SubiquityModel(root, self.opts.sources)

    def __init__(self, opts, block_log_dir, cloud_init_ok):
        super().__init__(opts)
        self.block_log_dir = block_log_dir
        self.cloud_init_ok = cloud_init_ok
        self._state = ApplicationState.STARTING_UP
        self.state_event = asyncio.Event()
        self.interactive = None
        self.confirming_tty = ''
        self.fatal_error = None
        self.running_error_commands = False

        self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid())
        self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
        self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())

        self.error_reporter = ErrorReporter(
            self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
        self.prober = Prober(opts.machine_config, self.debug_flags)
        self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
        if opts.snaps_from_examples:
            connection = FakeSnapdConnection(
                os.path.join(
                    os.path.dirname(
                        os.path.dirname(
                            os.path.dirname(__file__))),
                    "examples", "snaps"),
                self.scale_factor)
        else:
            connection = SnapdConnection(self.root, self.snapd_socket_path)
        self.snapd = AsyncSnapd(connection)
        self.note_data_for_apport("SnapUpdated", str(self.updated))
        self.event_listeners = []
        self.autoinstall_config = None
        self.signal.connect_signals([
            ('network-proxy-set', lambda: schedule_task(self._proxy_set())),
            ('network-change', self._network_change),
            ])

    def load_serialized_state(self):
        for controller in self.controllers.instances:
            controller.load_state()

    def add_event_listener(self, listener):
        self.event_listeners.append(listener)

    def _maybe_push_to_journal(self, event_type, context, description):
        if not context.get('is-install-context') and self.interactive:
            controller = context.get('controller')
            if controller is None or controller.interactive():
                return
        if context.get('request'):
            return
        indent = context.full_name().count('/') - 2
        if context.get('is-install-context') and self.interactive:
            indent -= 1
            msg = context.description
        else:
            msg = context.full_name()
            if description:
                msg += ': ' + description
        msg = '  ' * indent + msg
        if context.parent:
            parent_id = str(context.parent.id)
        else:
            parent_id = ''
        journal.send(
            msg,
            PRIORITY=context.level,
            SYSLOG_IDENTIFIER=self.event_syslog_id,
            SUBIQUITY_CONTEXT_NAME=context.full_name(),
            SUBIQUITY_EVENT_TYPE=event_type,
            SUBIQUITY_CONTEXT_ID=str(context.id),
            SUBIQUITY_CONTEXT_PARENT_ID=parent_id)

    def report_start_event(self, context, description):
        for listener in self.event_listeners:
            listener.report_start_event(context, description)
        self._maybe_push_to_journal('start', context, description)

    def report_finish_event(self, context, description, status):
        for listener in self.event_listeners:
            listener.report_finish_event(context, description, status)
        self._maybe_push_to_journal('finish', context, description)

    @property
    def state(self):
        return self._state

    def update_state(self, state):
        self._state = state
        self.state_event.set()
        self.state_event.clear()

    def note_file_for_apport(self, key, path):
        self.error_reporter.note_file_for_apport(key, path)

    def note_data_for_apport(self, key, value):
        self.error_reporter.note_data_for_apport(key, value)

    def make_apport_report(self, kind, thing, *, wait=False, **kw):
        return self.error_reporter.make_apport_report(
            kind, thing, wait=wait, **kw)

    async def _run_error_cmds(self, report):
        await report._info_task
        Error = getattr(self.controllers, "Error", None)
        if Error is not None and Error.cmds:
            await Error.run()

    def _exception_handler(self, loop, context):
        exc = context.get('exception')
        if exc is None:
            super()._exception_handler(loop, context)
            return
        report = self.error_reporter.report_for_exc(exc)
        log.error("top level error", exc_info=exc)
        if not report:
            report = self.make_apport_report(
                ErrorReportKind.UNKNOWN, "unknown error",
                exc=exc)
        self.fatal_error = report
        self.update_state(ApplicationState.ERROR)
        if not self.running_error_commands:
            self.running_error_commands = True
            self.aio_loop.create_task(self._run_error_cmds(report))

    @web.middleware
    async def middleware(self, request, handler):
        override_status = None
        controller = await controller_for_request(request)
        if isinstance(controller, SubiquityController):
            if not controller.interactive():
                override_status = 'skip'
            elif self.state == ApplicationState.NEEDS_CONFIRMATION:
                if self.base_model.needs_configuration(controller.model_name):
                    override_status = 'confirm'
        if override_status is not None:
            resp = web.Response(headers={'x-status': override_status})
        else:
            resp = await handler(request)
        if self.updated:
            resp.headers['x-updated'] = 'yes'
        else:
            resp.headers['x-updated'] = 'no'
        if resp.get('exception'):
            exc = resp['exception']
            log.debug(
                'request to {} crashed'.format(request.raw_path), exc_info=exc)
            report = self.make_apport_report(
                ErrorReportKind.SERVER_REQUEST_FAIL,
                "request to {}".format(request.raw_path),
                exc=exc)
            resp.headers['x-error-report'] = to_json(
                ErrorReportRef, report.ref())
        return resp

    @with_context()
    async def apply_autoinstall_config(self, context):
        for controller in self.controllers.instances:
            if controller.interactive():
                log.debug(
                    "apply_autoinstall_config: skipping %s as interactive",
                    controller.name)
                continue
            await controller.apply_autoinstall_config()
            controller.configured()

    def load_autoinstall_config(self, only_early):
        log.debug("load_autoinstall_config only_early %s", only_early)
        if self.opts.autoinstall is None:
            return
        with open(self.opts.autoinstall) as fp:
            self.autoinstall_config = yaml.safe_load(fp)
        if only_early:
            self.controllers.Reporting.setup_autoinstall()
            self.controllers.Reporting.start()
            self.controllers.Error.setup_autoinstall()
            with self.context.child("core_validation", level="INFO"):
                jsonschema.validate(self.autoinstall_config, self.base_schema)
            self.controllers.Early.setup_autoinstall()
        else:
            for controller in self.controllers.instances:
                controller.setup_autoinstall()

    async def start_api_server(self):
        app = web.Application(middlewares=[self.middleware])
        bind(app.router, API.meta, MetaController(self))
        bind(app.router, API.errors, ErrorController(self))
        if self.opts.dry_run:
            from .dryrun import DryRunController
            bind(app.router, API.dry_run, DryRunController(self))
        for controller in self.controllers.instances:
            controller.add_routes(app)
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.UnixSite(runner, self.opts.socket)
        await site.start()

    async def start(self):
        self.controllers.load_all()
        await self.start_api_server()
        self.load_autoinstall_config(only_early=True)
        if self.autoinstall_config and self.controllers.Early.cmds:
            stamp_file = self.state_path("early-commands")
            if not os.path.exists(stamp_file):
                await self.controllers.Early.run()
                open(stamp_file, 'w').close()
                await asyncio.sleep(1)
        self.load_autoinstall_config(only_early=False)
        if self.autoinstall_config:
            self.interactive = bool(
                self.autoinstall_config.get('interactive-sections'))
        else:
            self.interactive = True
        if not self.interactive and not self.opts.dry_run:
            open('/run/casper-no-prompt', 'w').close()
        self.load_serialized_state()
        self.update_state(ApplicationState.WAITING)
        await super().start()
        await self.apply_autoinstall_config()

    def _network_change(self):
        self.signal.emit_signal('snapd-network-change')

    async def _proxy_set(self):
        await run_in_thread(
            self.snapd.connection.configure_proxy, self.base_model.proxy)
        self.signal.emit_signal('snapd-network-change')

    def restart(self):
        cmdline = ['snap', 'run', 'subiquity.subiquity-server']
        if self.opts.dry_run:
            cmdline = [
                sys.executable, '-m', 'subiquity.cmd.server',
                ] + sys.argv[1:]
        os.execvp(cmdline[0], cmdline)

    def make_autoinstall(self):
        config = {'version': 1}
        for controller in self.controllers.instances:
            controller_conf = controller.make_autoinstall()
            if controller_conf:
                config[controller.autoinstall_key] = controller_conf
        return config