From c4841e5b3af2ebaa448a1a0b43a65941cf5c6c3e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 17 Mar 2018 19:35:30 +0200 Subject: Add: ActionEnvironment class for executing in various contexts --- ick2/actionenvs.py | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 ick2/actionenvs.py (limited to 'ick2/actionenvs.py') diff --git a/ick2/actionenvs.py b/ick2/actionenvs.py new file mode 100644 index 0000000..7a2b57a --- /dev/null +++ b/ick2/actionenvs.py @@ -0,0 +1,167 @@ +# Copyright (C) 2018 Lars Wirzenius +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import logging +import os +import subprocess + + +import cliapp + + +class Runner: + + def __init__(self, reporter, maxbuf=128*1024): + self._reporter = reporter + self._buffers = { + 'stdout': b'', + 'stderr': b'', + } + self._maxbuf = maxbuf + self._timeout = 1.0 + + def runcmd(self, *argvs, **kwargs): + for argv in argvs: + logging.debug('Runner.runcmd: argv: %r', argv) + for key in kwargs: + logging.debug('Runner.runcmd: kwargs: %r=%r', key, kwargs[key]) + assert all(argv is not None for argv in argvs) + exit_code, _, _ = cliapp.runcmd_unchecked( + *argvs, + stdout_callback=self.capture_stdout, + stderr=subprocess.STDOUT, + output_timeout=self._timeout, + timeout_callback=self.flush, + **kwargs + ) + self.flush() + logging.debug('Runner.runcmd: finished, exit code: %d', exit_code) + return exit_code + + def capture_stdout(self, data): + return self.capture('stdout', data) + + def capture(self, stream_name, data): + self._buffers[stream_name] += data + if len(self._buffers[stream_name]) >= self._maxbuf: + self.flush() + + return b'' + + def flush(self): + stdout = self._buffers['stdout'] + stderr = self._buffers['stderr'] + self._reporter.report(None, stdout, stderr) + self._buffers['stdout'] = b'' + self._buffers['stderr'] = b'' + + +class Mounter: # pragma: no cover + + def __init__(self, mounts, runner): + self._mounts = mounts + self._runner = runner + + def __enter__(self): + self.mount() + return self + + def __exit__(self, *args): + self.unmount() + + def mount(self): + for dirname, mp in self._mounts: + if not os.path.exists(mp): + os.mkdir(mp) + self._runner.runcmd(['sudo', 'mount', '--bind', dirname, mp]) + + def unmount(self): + for dirname, mp in reversed(self._mounts): + try: + self._runner.runcmd(['sudo', 'umount', mp]) + except BaseException as e: + logging.error( + 'Ignoring error while unmounting %s: %s', mp, str(e)) + + +class ActionEnvironment: # pragma: no cover + + def __init__(self, systree, workspace, reporter): + super().__init__() + self._systree = systree + self._workspace = workspace + self._reporter = reporter + + def get_systree_directory(self): + return self._systree + + def get_workspace_directory(self): + return self._workspace + + def get_mounts(self): + return [] + + def report(self, exit_code, msg): + self._reporter.report(exit_code, msg, None) + + def runcmd(self, argv): + raise NotImplementedError() + + def host_runcmd(self, *argvs, cwd=None): + env = self.get_env_vars() + runner = Runner(self._reporter) + mounts = self.get_mounts() + with Mounter(mounts, runner): + return runner.runcmd(*argvs, cwd=cwd, env=env) + + def get_env_vars(self): + env = dict(os.environ) + env.update({ + 'LC_ALL': 'C', + 'DEBIAN_FRONTEND': 'noninteractive', + }) + return env + + +class HostEnvironment(ActionEnvironment): + + def runcmd(self, argv): + return self.host_runcmd(argv, cwd=self._workspace) + + +class ChrootEnvironment(ActionEnvironment): + + def get_mounts(self): # pragma: no cover + return [ + ('/proc', os.path.join(self._workspace, 'proc')), + ('/sys', os.path.join(self._workspace, 'sys')), + ] + + def runcmd(self, argv): + prefix = ['sudo', 'chroot', self._workspace] + return self.host_runcmd(prefix + argv) + + +class ContainerEnvironment(ActionEnvironment): + + def runcmd(self, argv): + bind = '{}:/workspace'.format(self._workspace) + prefix = [ + 'sudo', 'systemd-nspawn', + '-D', self._systree, + '--bind', bind, + '--chdir', '/workspace', + ] + return self.host_runcmd(prefix + argv) -- cgit v1.2.1