diff --git a/subiquity/server/controllers/install.py b/subiquity/server/controllers/install.py index 707950dd..3ad63122 100644 --- a/subiquity/server/controllers/install.py +++ b/subiquity/server/controllers/install.py @@ -40,7 +40,8 @@ from subiquity.journald import ( journald_listen, ) from subiquity.server.curtin import ( - get_curtin_command_runner, + run_curtin_command, + start_curtin_command, ) from subiquity.server.controller import ( SubiquityController, @@ -75,11 +76,9 @@ class InstallController(SubiquityController): super().__init__(app) self.model = app.base_model - self.unattended_upgrades_proc = None + self.unattended_upgrades_cmd = None self.unattended_upgrades_ctx = None - self._event_syslog_id = 'curtin_event.%s' % (os.getpid(),) self.tb_extractor = TracebackExtractor() - self.curtin_runner = None def stop_uu(self): if self.app.state == ApplicationState.UU_RUNNING: @@ -97,7 +96,7 @@ class InstallController(SubiquityController): def log_event(self, event): self.tb_extractor.feed(event['MESSAGE']) - def make_curtin_command_runner(self): + def write_config(self): config = self.model.render() config_location = '/var/log/installer/subiquity-curtin-install.conf' log_location = INSTALL_LOG @@ -114,19 +113,19 @@ class InstallController(SubiquityController): self.app.note_file_for_apport("CurtinConfig", config_location) self.app.note_file_for_apport("CurtinErrors", ERROR_TARFILE) self.app.note_file_for_apport("CurtinLog", log_location) - self.curtin_runner = get_curtin_command_runner( - self.app, config_location) + return config_location @with_context(description="umounting /target dir") async def unmount_target(self, *, context, target): - await self.curtin_runner.run(context, 'unmount', '-t', target) + await run_curtin_command(self.app, context, 'unmount', '-t', target) if not self.app.opts.dry_run: shutil.rmtree(target) @with_context( description="installing system", level="INFO", childlevel="DEBUG") - async def curtin_install(self, *, context): - await self.curtin_runner.run(context, 'install') + async def curtin_install(self, *, context, config_location): + await run_curtin_command( + self.app, context, 'install', config=config_location) @with_context() async def install(self, *, context): @@ -148,13 +147,14 @@ class InstallController(SubiquityController): self.app.update_state(ApplicationState.RUNNING) - self.make_curtin_command_runner() + config_location = self.write_config() if os.path.exists(self.model.target): await self.unmount_target( context=context, target=self.model.target) - await self.curtin_install(context=context) + await self.curtin_install( + context=context, config_location=config_location) self.app.update_state(ApplicationState.POST_WAIT) @@ -205,14 +205,15 @@ class InstallController(SubiquityController): name="install_{package}", description="installing {package}") async def install_package(self, *, context, package): - await self.curtin_runner.run(context, 'system-install', '--', package) + await run_curtin_command( + self.app, context, 'system-install', '--', package) @with_context(description="restoring apt configuration") async def restore_apt_config(self, context): await self.app.command_runner.run(["umount", self.tpath('etc/apt')]) if self.model.network.has_network: - await self.curtin_runner.run( - context, "in-target", "-t", self.tpath(), + await run_curtin_command( + self.app, context, "in-target", "-t", self.tpath(), "--", "apt-get", "update") else: await self.app.command_runner.run( @@ -235,13 +236,11 @@ class InstallController(SubiquityController): apt_conf.write(apt_conf_contents) apt_conf.close() self.unattended_upgrades_ctx = context - self.unattended_upgrades_proc = \ - await self.app.command_runner.start( - self.curtin_runner.make_command( - "in-target", "-t", self.tpath(), - "--", "unattended-upgrades", "-v")) - await self.unattended_upgrades_proc.communicate() - self.unattended_upgrades_proc = None + self.unattended_upgrades_cmd = await start_curtin_command( + self.app, context, "in-target", "-t", self.tpath(), + "--", "unattended-upgrades", "-v") + await self.unattended_upgrades_cmd.wait() + self.unattended_upgrades_cmd = None self.unattended_upgrades_ctx = None async def stop_unattended_upgrades(self): @@ -255,8 +254,8 @@ class InstallController(SubiquityController): '--stop-only', ]) if self.app.opts.dry_run and \ - self.unattended_upgrades_proc is not None: - self.unattended_upgrades_proc.terminate() + self.unattended_upgrades_cmd is not None: + self.unattended_upgrades_cmd.proc.terminate() uu_apt_conf = b"""\ diff --git a/subiquity/server/curtin.py b/subiquity/server/curtin.py index 9c1ebd25..a00bfd08 100644 --- a/subiquity/server/curtin.py +++ b/subiquity/server/curtin.py @@ -33,19 +33,19 @@ from subiquity.journald import ( log = logging.getLogger('subiquity.server.curtin') -class CurtinCommandRunner: +class _CurtinCommand: _count = 0 - def __init__(self, runner, config_location=None): + def __init__(self, runner, command, *args, config=None): self.runner = runner - self._event_syslog_id = 'curtin_event.%s.%s' % ( - os.getpid(), CurtinCommandRunner._count) - CurtinCommandRunner._count += 1 - self.config_location = config_location self._event_contexts = {} - journald_listen( - asyncio.get_event_loop(), [self._event_syslog_id], self._event) + _CurtinCommand._count += 1 + self._event_syslog_id = 'curtin_event.%s.%s' % ( + os.getpid(), _CurtinCommand._count) + self._fd = None + self.proc = None + self._cmd = self.make_command(command, *args, config=config) def _event(self, event): e = { @@ -80,62 +80,85 @@ class CurtinCommandRunner: if curtin_ctx is not None: curtin_ctx.exit(result=status) - def make_command(self, command, *args, **conf): - cmd = [ - sys.executable, '-m', 'curtin', '--showtrace', - ] - if self.config_location is not None: - cmd.extend([ - '-c', self.config_location, - ]) - conf.setdefault('reporting', {})['subiquity'] = { - 'type': 'journald', - 'identifier': self._event_syslog_id, + def make_command(self, command, *args, config=None): + reporting_conf = { + 'subiquity': { + 'type': 'journald', + 'identifier': self._event_syslog_id, + }, } - conf['verbosity'] = 3 - for k, v in conf.items(): - cmd.extend(['--set', 'json:' + k + '=' + json.dumps(v)]) + cmd = [ + sys.executable, '-m', 'curtin', '--showtrace', '-vvv', + '--set', 'json:reporting=' + json.dumps(reporting_conf), + ] + if config is not None: + cmd.extend([ + '-c', config, + ]) cmd.append(command) cmd.extend(args) return cmd - async def run(self, context, command, *args, **conf): + async def start(self, context): + self._fd = journald_listen( + asyncio.get_event_loop(), [self._event_syslog_id], self._event) + # Yield to the event loop before starting curtin to avoid missing the + # first couple of events. + await asyncio.sleep(0) self._event_contexts[''] = context - await self.runner.run(self.make_command(command, *args, **conf)) + self.proc = await self.runner.start(self._cmd) + + async def wait(self): + await self.runner.wait(self.proc) waited = 0.0 while len(self._event_contexts) > 1 and waited < 5.0: await asyncio.sleep(0.1) waited += 0.1 log.debug("waited %s seconds for events to drain", waited) self._event_contexts.pop('', None) + asyncio.get_event_loop().remove_reader(self._fd) + + async def run(self, context): + await self.start(context) + await self.wait() -class DryRunCurtinCommandRunner(CurtinCommandRunner): +class _DryRunCurtinCommand(_CurtinCommand): event_file = 'examples/curtin-events.json' - def make_command(self, command, *args, **conf): + def make_command(self, command, *args, config=None): if command == 'install': return [ - sys.executable, "scripts/replay-curtin-log.py", - self.event_file, self._event_syslog_id, + sys.executable, + "scripts/replay-curtin-log.py", + self.event_file, + self._event_syslog_id, '.subiquity' + INSTALL_LOG, ] else: - return super().make_command(command, *args, **conf) + return super().make_command(command, *args, config=config) -class FailingDryRunCurtinCommandRunner(DryRunCurtinCommandRunner): +class _FailingDryRunCurtinCommand(_DryRunCurtinCommand): event_file = 'examples/curtin-events-fail.json' -def get_curtin_command_runner(app, config_location=None): +async def start_curtin_command(app, context, command, *args, config=None): if app.opts.dry_run: if 'install-fail' in app.debug_flags: - cls = FailingDryRunCurtinCommandRunner + cls = _FailingDryRunCurtinCommand else: - cls = DryRunCurtinCommandRunner + cls = _DryRunCurtinCommand else: - cls = CurtinCommandRunner - return cls(app.command_runner, config_location) + cls = _CurtinCommand + curtin_cmd = cls(app.command_runner, command, *args, config=config) + await curtin_cmd.start(context) + return curtin_cmd + + +async def run_curtin_command(app, context, command, *args, config=None): + cmd = await start_curtin_command( + app, context, command, *args, config=config) + await cmd.wait() diff --git a/subiquity/server/runner.py b/subiquity/server/runner.py index c98fc066..bc391eff 100644 --- a/subiquity/server/runner.py +++ b/subiquity/server/runner.py @@ -25,17 +25,22 @@ class LoggedCommandRunner: self.ident = ident async def start(self, cmd): - return await astart_command([ + proc = await astart_command([ 'systemd-cat', '--level-prefix=false', '--identifier='+self.ident, ] + cmd) + proc.args = cmd + return proc + + async def wait(self, proc): + await proc.communicate() + if proc.returncode != 0: + raise subprocess.CalledProcessError(proc.returncode, proc.args) + else: + return subprocess.CompletedProcess(proc.args, proc.returncode) async def run(self, cmd): proc = await self.start(cmd) - await proc.communicate() - if proc.returncode != 0: - raise subprocess.CalledProcessError(proc.returncode, cmd) - else: - return subprocess.CompletedProcess(cmd, proc.returncode) + return await self.wait(proc) class DryRunCommandRunner(LoggedCommandRunner):