| #!/usr/bin/env python3 | 
 | # | 
 | # Docker controlling module | 
 | # | 
 | # Copyright (c) 2016 Red Hat Inc. | 
 | # | 
 | # Authors: | 
 | #  Fam Zheng <famz@redhat.com> | 
 | # | 
 | # This work is licensed under the terms of the GNU GPL, version 2 | 
 | # or (at your option) any later version. See the COPYING file in | 
 | # the top-level directory. | 
 |  | 
 | import os | 
 | import sys | 
 | import subprocess | 
 | import json | 
 | import hashlib | 
 | import atexit | 
 | import uuid | 
 | import argparse | 
 | import enum | 
 | import tempfile | 
 | import re | 
 | import signal | 
 | import getpass | 
 | from tarfile import TarFile, TarInfo | 
 | from io import StringIO, BytesIO | 
 | from shutil import copy, rmtree | 
 | from datetime import datetime, timedelta | 
 |  | 
 |  | 
 | FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy'] | 
 |  | 
 |  | 
 | DEVNULL = open(os.devnull, 'wb') | 
 |  | 
 | class EngineEnum(enum.IntEnum): | 
 |     AUTO = 1 | 
 |     DOCKER = 2 | 
 |     PODMAN = 3 | 
 |  | 
 |     def __str__(self): | 
 |         return self.name.lower() | 
 |  | 
 |     def __repr__(self): | 
 |         return str(self) | 
 |  | 
 |     @staticmethod | 
 |     def argparse(s): | 
 |         try: | 
 |             return EngineEnum[s.upper()] | 
 |         except KeyError: | 
 |             return s | 
 |  | 
 |  | 
 | USE_ENGINE = EngineEnum.AUTO | 
 |  | 
 | def _bytes_checksum(bytes): | 
 |     """Calculate a digest string unique to the text content""" | 
 |     return hashlib.sha1(bytes).hexdigest() | 
 |  | 
 | def _text_checksum(text): | 
 |     """Calculate a digest string unique to the text content""" | 
 |     return _bytes_checksum(text.encode('utf-8')) | 
 |  | 
 | def _read_dockerfile(path): | 
 |     return open(path, 'rt', encoding='utf-8').read() | 
 |  | 
 | def _file_checksum(filename): | 
 |     return _bytes_checksum(open(filename, 'rb').read()) | 
 |  | 
 |  | 
 | def _guess_engine_command(): | 
 |     """ Guess a working engine command or raise exception if not found""" | 
 |     commands = [] | 
 |  | 
 |     if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]: | 
 |         commands += [["podman"]] | 
 |     if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]: | 
 |         commands += [["docker"], ["sudo", "-n", "docker"]] | 
 |     for cmd in commands: | 
 |         try: | 
 |             # docker version will return the client details in stdout | 
 |             # but still report a status of 1 if it can't contact the daemon | 
 |             if subprocess.call(cmd + ["version"], | 
 |                                stdout=DEVNULL, stderr=DEVNULL) == 0: | 
 |                 return cmd | 
 |         except OSError: | 
 |             pass | 
 |     commands_txt = "\n".join(["  " + " ".join(x) for x in commands]) | 
 |     raise Exception("Cannot find working engine command. Tried:\n%s" % | 
 |                     commands_txt) | 
 |  | 
 |  | 
 | def _copy_with_mkdir(src, root_dir, sub_path='.', name=None): | 
 |     """Copy src into root_dir, creating sub_path as needed.""" | 
 |     dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path)) | 
 |     try: | 
 |         os.makedirs(dest_dir) | 
 |     except OSError: | 
 |         # we can safely ignore already created directories | 
 |         pass | 
 |  | 
 |     dest_file = "%s/%s" % (dest_dir, name if name else os.path.basename(src)) | 
 |  | 
 |     try: | 
 |         copy(src, dest_file) | 
 |     except FileNotFoundError: | 
 |         print("Couldn't copy %s to %s" % (src, dest_file)) | 
 |         pass | 
 |  | 
 |  | 
 | def _get_so_libs(executable): | 
 |     """Return a list of libraries associated with an executable. | 
 |  | 
 |     The paths may be symbolic links which would need to be resolved to | 
 |     ensure the right data is copied.""" | 
 |  | 
 |     libs = [] | 
 |     ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)") | 
 |     try: | 
 |         ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8') | 
 |         for line in ldd_output.split("\n"): | 
 |             search = ldd_re.search(line) | 
 |             if search: | 
 |                 try: | 
 |                     libs.append(search.group(1)) | 
 |                 except IndexError: | 
 |                     pass | 
 |     except subprocess.CalledProcessError: | 
 |         print("%s had no associated libraries (static build?)" % (executable)) | 
 |  | 
 |     return libs | 
 |  | 
 |  | 
 | def _copy_binary_with_libs(src, bin_dest, dest_dir): | 
 |     """Maybe copy a binary and all its dependent libraries. | 
 |  | 
 |     If bin_dest isn't set we only copy the support libraries because | 
 |     we don't need qemu in the docker path to run (due to persistent | 
 |     mapping). Indeed users may get confused if we aren't running what | 
 |     is in the image. | 
 |  | 
 |     This does rely on the host file-system being fairly multi-arch | 
 |     aware so the file don't clash with the guests layout. | 
 |     """ | 
 |  | 
 |     if bin_dest: | 
 |         _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest)) | 
 |     else: | 
 |         print("only copying support libraries for %s" % (src)) | 
 |  | 
 |     libs = _get_so_libs(src) | 
 |     if libs: | 
 |         for l in libs: | 
 |             so_path = os.path.dirname(l) | 
 |             name = os.path.basename(l) | 
 |             real_l = os.path.realpath(l) | 
 |             _copy_with_mkdir(real_l, dest_dir, so_path, name) | 
 |  | 
 |  | 
 | def _check_binfmt_misc(executable): | 
 |     """Check binfmt_misc has entry for executable in the right place. | 
 |  | 
 |     The details of setting up binfmt_misc are outside the scope of | 
 |     this script but we should at least fail early with a useful | 
 |     message if it won't work. | 
 |  | 
 |     Returns the configured binfmt path and a valid flag. For | 
 |     persistent configurations we will still want to copy and dependent | 
 |     libraries. | 
 |     """ | 
 |  | 
 |     binary = os.path.basename(executable) | 
 |     binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary) | 
 |  | 
 |     if not os.path.exists(binfmt_entry): | 
 |         print ("No binfmt_misc entry for %s" % (binary)) | 
 |         return None, False | 
 |  | 
 |     with open(binfmt_entry) as x: entry = x.read() | 
 |  | 
 |     if re.search("flags:.*F.*\n", entry): | 
 |         print("binfmt_misc for %s uses persistent(F) mapping to host binary" % | 
 |               (binary)) | 
 |         return None, True | 
 |  | 
 |     m = re.search(r"interpreter (\S+)\n", entry) | 
 |     interp = m.group(1) | 
 |     if interp and interp != executable: | 
 |         print("binfmt_misc for %s does not point to %s, using %s" % | 
 |               (binary, executable, interp)) | 
 |  | 
 |     return interp, True | 
 |  | 
 |  | 
 | def _read_qemu_dockerfile(img_name): | 
 |     # special case for Debian linux-user images | 
 |     if img_name.startswith("debian") and img_name.endswith("user"): | 
 |         img_name = "debian-bootstrap" | 
 |  | 
 |     df = os.path.join(os.path.dirname(__file__), "dockerfiles", | 
 |                       img_name + ".docker") | 
 |     return _read_dockerfile(df) | 
 |  | 
 |  | 
 | def _dockerfile_verify_flat(df): | 
 |     "Verify we do not include other qemu/ layers" | 
 |     for l in df.splitlines(): | 
 |         if len(l.strip()) == 0 or l.startswith("#"): | 
 |             continue | 
 |         from_pref = "FROM qemu/" | 
 |         if l.startswith(from_pref): | 
 |             print("We no longer support multiple QEMU layers.") | 
 |             print("Dockerfiles should be flat, ideally created by lcitool") | 
 |             return False | 
 |     return True | 
 |  | 
 |  | 
 | class Docker(object): | 
 |     """ Running Docker commands """ | 
 |     def __init__(self): | 
 |         self._command = _guess_engine_command() | 
 |  | 
 |         if ("docker" in self._command and | 
 |             "TRAVIS" not in os.environ and | 
 |             "GITLAB_CI" not in os.environ): | 
 |             os.environ["DOCKER_BUILDKIT"] = "1" | 
 |             self._buildkit = True | 
 |         else: | 
 |             self._buildkit = False | 
 |  | 
 |         self._instance = None | 
 |         atexit.register(self._kill_instances) | 
 |         signal.signal(signal.SIGTERM, self._kill_instances) | 
 |         signal.signal(signal.SIGHUP, self._kill_instances) | 
 |  | 
 |     def _do(self, cmd, quiet=True, **kwargs): | 
 |         if quiet: | 
 |             kwargs["stdout"] = DEVNULL | 
 |         return subprocess.call(self._command + cmd, **kwargs) | 
 |  | 
 |     def _do_check(self, cmd, quiet=True, **kwargs): | 
 |         if quiet: | 
 |             kwargs["stdout"] = DEVNULL | 
 |         return subprocess.check_call(self._command + cmd, **kwargs) | 
 |  | 
 |     def _do_kill_instances(self, only_known, only_active=True): | 
 |         cmd = ["ps", "-q"] | 
 |         if not only_active: | 
 |             cmd.append("-a") | 
 |  | 
 |         filter = "--filter=label=com.qemu.instance.uuid" | 
 |         if only_known: | 
 |             if self._instance: | 
 |                 filter += "=%s" % (self._instance) | 
 |             else: | 
 |                 # no point trying to kill, we finished | 
 |                 return | 
 |  | 
 |         print("filter=%s" % (filter)) | 
 |         cmd.append(filter) | 
 |         for i in self._output(cmd).split(): | 
 |             self._do(["rm", "-f", i]) | 
 |  | 
 |     def clean(self): | 
 |         self._do_kill_instances(False, False) | 
 |         return 0 | 
 |  | 
 |     def _kill_instances(self, *args, **kwargs): | 
 |         return self._do_kill_instances(True) | 
 |  | 
 |     def _output(self, cmd, **kwargs): | 
 |         try: | 
 |             return subprocess.check_output(self._command + cmd, | 
 |                                            stderr=subprocess.STDOUT, | 
 |                                            encoding='utf-8', | 
 |                                            **kwargs) | 
 |         except TypeError: | 
 |             # 'encoding' argument was added in 3.6+ | 
 |             return subprocess.check_output(self._command + cmd, | 
 |                                            stderr=subprocess.STDOUT, | 
 |                                            **kwargs).decode('utf-8') | 
 |  | 
 |  | 
 |     def inspect_tag(self, tag): | 
 |         try: | 
 |             return self._output(["inspect", tag]) | 
 |         except subprocess.CalledProcessError: | 
 |             return None | 
 |  | 
 |     def get_image_creation_time(self, info): | 
 |         return json.loads(info)[0]["Created"] | 
 |  | 
 |     def get_image_dockerfile_checksum(self, tag): | 
 |         resp = self.inspect_tag(tag) | 
 |         labels = json.loads(resp)[0]["Config"].get("Labels", {}) | 
 |         return labels.get("com.qemu.dockerfile-checksum", "") | 
 |  | 
 |     def build_image(self, tag, docker_dir, dockerfile, | 
 |                     quiet=True, user=False, argv=None, registry=None, | 
 |                     extra_files_cksum=[]): | 
 |         if argv is None: | 
 |             argv = [] | 
 |  | 
 |         if not _dockerfile_verify_flat(dockerfile): | 
 |             return -1 | 
 |  | 
 |         checksum = _text_checksum(dockerfile) | 
 |  | 
 |         tmp_df = tempfile.NamedTemporaryFile(mode="w+t", | 
 |                                              encoding='utf-8', | 
 |                                              dir=docker_dir, suffix=".docker") | 
 |         tmp_df.write(dockerfile) | 
 |  | 
 |         if user: | 
 |             uid = os.getuid() | 
 |             uname = getpass.getuser() | 
 |             tmp_df.write("\n") | 
 |             tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % | 
 |                          (uname, uid, uname)) | 
 |  | 
 |         tmp_df.write("\n") | 
 |         tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum)) | 
 |         for f, c in extra_files_cksum: | 
 |             tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c)) | 
 |  | 
 |         tmp_df.flush() | 
 |  | 
 |         build_args = ["build", "-t", tag, "-f", tmp_df.name] | 
 |         if self._buildkit: | 
 |             build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"] | 
 |  | 
 |         if registry is not None: | 
 |             pull_args = ["pull", "%s/%s" % (registry, tag)] | 
 |             self._do(pull_args, quiet=quiet) | 
 |             cache = "%s/%s" % (registry, tag) | 
 |             build_args += ["--cache-from", cache] | 
 |         build_args += argv | 
 |         build_args += [docker_dir] | 
 |  | 
 |         self._do_check(build_args, | 
 |                        quiet=quiet) | 
 |  | 
 |     def update_image(self, tag, tarball, quiet=True): | 
 |         "Update a tagged image using " | 
 |  | 
 |         self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball) | 
 |  | 
 |     def image_matches_dockerfile(self, tag, dockerfile): | 
 |         try: | 
 |             checksum = self.get_image_dockerfile_checksum(tag) | 
 |         except Exception: | 
 |             return False | 
 |         return checksum == _text_checksum(dockerfile) | 
 |  | 
 |     def run(self, cmd, keep, quiet, as_user=False): | 
 |         label = uuid.uuid4().hex | 
 |         if not keep: | 
 |             self._instance = label | 
 |  | 
 |         if as_user: | 
 |             uid = os.getuid() | 
 |             cmd = [ "-u", str(uid) ] + cmd | 
 |             # podman requires a bit more fiddling | 
 |             if self._command[0] == "podman": | 
 |                 cmd.insert(0, '--userns=keep-id') | 
 |  | 
 |         ret = self._do_check(["run", "--rm", "--label", | 
 |                              "com.qemu.instance.uuid=" + label] + cmd, | 
 |                              quiet=quiet) | 
 |         if not keep: | 
 |             self._instance = None | 
 |         return ret | 
 |  | 
 |     def command(self, cmd, argv, quiet): | 
 |         return self._do([cmd] + argv, quiet=quiet) | 
 |  | 
 |  | 
 | class SubCommand(object): | 
 |     """A SubCommand template base class""" | 
 |     name = None  # Subcommand name | 
 |  | 
 |     def shared_args(self, parser): | 
 |         parser.add_argument("--quiet", action="store_true", | 
 |                             help="Run quietly unless an error occurred") | 
 |  | 
 |     def args(self, parser): | 
 |         """Setup argument parser""" | 
 |         pass | 
 |  | 
 |     def run(self, args, argv): | 
 |         """Run command. | 
 |         args: parsed argument by argument parser. | 
 |         argv: remaining arguments from sys.argv. | 
 |         """ | 
 |         pass | 
 |  | 
 |  | 
 | class RunCommand(SubCommand): | 
 |     """Invoke docker run and take care of cleaning up""" | 
 |     name = "run" | 
 |  | 
 |     def args(self, parser): | 
 |         parser.add_argument("--keep", action="store_true", | 
 |                             help="Don't remove image when command completes") | 
 |         parser.add_argument("--run-as-current-user", action="store_true", | 
 |                             help="Run container using the current user's uid") | 
 |  | 
 |     def run(self, args, argv): | 
 |         return Docker().run(argv, args.keep, quiet=args.quiet, | 
 |                             as_user=args.run_as_current_user) | 
 |  | 
 |  | 
 | class BuildCommand(SubCommand): | 
 |     """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>""" | 
 |     name = "build" | 
 |  | 
 |     def args(self, parser): | 
 |         parser.add_argument("--include-executable", "-e", | 
 |                             help="""Specify a binary that will be copied to the | 
 |                             container together with all its dependent | 
 |                             libraries""") | 
 |         parser.add_argument("--skip-binfmt", | 
 |                             action="store_true", | 
 |                             help="""Skip binfmt entry check (used for testing)""") | 
 |         parser.add_argument("--extra-files", nargs='*', | 
 |                             help="""Specify files that will be copied in the | 
 |                             Docker image, fulfilling the ADD directive from the | 
 |                             Dockerfile""") | 
 |         parser.add_argument("--add-current-user", "-u", dest="user", | 
 |                             action="store_true", | 
 |                             help="Add the current user to image's passwd") | 
 |         parser.add_argument("--registry", "-r", | 
 |                             help="cache from docker registry") | 
 |         parser.add_argument("-t", dest="tag", | 
 |                             help="Image Tag") | 
 |         parser.add_argument("-f", dest="dockerfile", | 
 |                             help="Dockerfile name") | 
 |  | 
 |     def run(self, args, argv): | 
 |         dockerfile = _read_dockerfile(args.dockerfile) | 
 |         tag = args.tag | 
 |  | 
 |         dkr = Docker() | 
 |         if "--no-cache" not in argv and \ | 
 |            dkr.image_matches_dockerfile(tag, dockerfile): | 
 |             if not args.quiet: | 
 |                 print("Image is up to date.") | 
 |         else: | 
 |             # Create a docker context directory for the build | 
 |             docker_dir = tempfile.mkdtemp(prefix="docker_build") | 
 |  | 
 |             # Validate binfmt_misc will work | 
 |             if args.skip_binfmt: | 
 |                 qpath = args.include_executable | 
 |             elif args.include_executable: | 
 |                 qpath, enabled = _check_binfmt_misc(args.include_executable) | 
 |                 if not enabled: | 
 |                     return 1 | 
 |  | 
 |             # Is there a .pre file to run in the build context? | 
 |             docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" | 
 |             if os.path.exists(docker_pre): | 
 |                 stdout = DEVNULL if args.quiet else None | 
 |                 rc = subprocess.call(os.path.realpath(docker_pre), | 
 |                                      cwd=docker_dir, stdout=stdout) | 
 |                 if rc == 3: | 
 |                     print("Skip") | 
 |                     return 0 | 
 |                 elif rc != 0: | 
 |                     print("%s exited with code %d" % (docker_pre, rc)) | 
 |                     return 1 | 
 |  | 
 |             # Copy any extra files into the Docker context. These can be | 
 |             # included by the use of the ADD directive in the Dockerfile. | 
 |             cksum = [] | 
 |             if args.include_executable: | 
 |                 # FIXME: there is no checksum of this executable and the linked | 
 |                 # libraries, once the image built any change of this executable | 
 |                 # or any library won't trigger another build. | 
 |                 _copy_binary_with_libs(args.include_executable, | 
 |                                        qpath, docker_dir) | 
 |  | 
 |             for filename in args.extra_files or []: | 
 |                 _copy_with_mkdir(filename, docker_dir) | 
 |                 cksum += [(filename, _file_checksum(filename))] | 
 |  | 
 |             argv += ["--build-arg=" + k.lower() + "=" + v | 
 |                      for k, v in os.environ.items() | 
 |                      if k.lower() in FILTERED_ENV_NAMES] | 
 |             dkr.build_image(tag, docker_dir, dockerfile, | 
 |                             quiet=args.quiet, user=args.user, | 
 |                             argv=argv, registry=args.registry, | 
 |                             extra_files_cksum=cksum) | 
 |  | 
 |             rmtree(docker_dir) | 
 |  | 
 |         return 0 | 
 |  | 
 | class FetchCommand(SubCommand): | 
 |     """ Fetch a docker image from the registry. Args: <tag> <registry>""" | 
 |     name = "fetch" | 
 |  | 
 |     def args(self, parser): | 
 |         parser.add_argument("tag", | 
 |                             help="Local tag for image") | 
 |         parser.add_argument("registry", | 
 |                             help="Docker registry") | 
 |  | 
 |     def run(self, args, argv): | 
 |         dkr = Docker() | 
 |         dkr.command(cmd="pull", quiet=args.quiet, | 
 |                     argv=["%s/%s" % (args.registry, args.tag)]) | 
 |         dkr.command(cmd="tag", quiet=args.quiet, | 
 |                     argv=["%s/%s" % (args.registry, args.tag), args.tag]) | 
 |  | 
 |  | 
 | class UpdateCommand(SubCommand): | 
 |     """ Update a docker image. Args: <tag> <actions>""" | 
 |     name = "update" | 
 |  | 
 |     def args(self, parser): | 
 |         parser.add_argument("tag", | 
 |                             help="Image Tag") | 
 |         parser.add_argument("--executable", | 
 |                             help="Executable to copy") | 
 |         parser.add_argument("--add-current-user", "-u", dest="user", | 
 |                             action="store_true", | 
 |                             help="Add the current user to image's passwd") | 
 |  | 
 |     def run(self, args, argv): | 
 |         # Create a temporary tarball with our whole build context and | 
 |         # dockerfile for the update | 
 |         tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") | 
 |         tmp_tar = TarFile(fileobj=tmp, mode='w') | 
 |  | 
 |         # Create a Docker buildfile | 
 |         df = StringIO() | 
 |         df.write(u"FROM %s\n" % args.tag) | 
 |  | 
 |         if args.executable: | 
 |             # Add the executable to the tarball, using the current | 
 |             # configured binfmt_misc path. If we don't get a path then we | 
 |             # only need the support libraries copied | 
 |             ff, enabled = _check_binfmt_misc(args.executable) | 
 |  | 
 |             if not enabled: | 
 |                 print("binfmt_misc not enabled, update disabled") | 
 |                 return 1 | 
 |  | 
 |             if ff: | 
 |                 tmp_tar.add(args.executable, arcname=ff) | 
 |  | 
 |             # Add any associated libraries | 
 |             libs = _get_so_libs(args.executable) | 
 |             if libs: | 
 |                 for l in libs: | 
 |                     so_path = os.path.dirname(l) | 
 |                     name = os.path.basename(l) | 
 |                     real_l = os.path.realpath(l) | 
 |                     try: | 
 |                         tmp_tar.add(real_l, arcname="%s/%s" % (so_path, name)) | 
 |                     except FileNotFoundError: | 
 |                         print("Couldn't add %s/%s to archive" % (so_path, name)) | 
 |                         pass | 
 |  | 
 |             df.write(u"ADD . /\n") | 
 |  | 
 |         if args.user: | 
 |             uid = os.getuid() | 
 |             uname = getpass.getuser() | 
 |             df.write("\n") | 
 |             df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % | 
 |                      (uname, uid, uname)) | 
 |  | 
 |         df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8")) | 
 |  | 
 |         df_tar = TarInfo(name="Dockerfile") | 
 |         df_tar.size = df_bytes.getbuffer().nbytes | 
 |         tmp_tar.addfile(df_tar, fileobj=df_bytes) | 
 |  | 
 |         tmp_tar.close() | 
 |  | 
 |         # reset the file pointers | 
 |         tmp.flush() | 
 |         tmp.seek(0) | 
 |  | 
 |         # Run the build with our tarball context | 
 |         dkr = Docker() | 
 |         dkr.update_image(args.tag, tmp, quiet=args.quiet) | 
 |  | 
 |         return 0 | 
 |  | 
 |  | 
 | class CleanCommand(SubCommand): | 
 |     """Clean up docker instances""" | 
 |     name = "clean" | 
 |  | 
 |     def run(self, args, argv): | 
 |         Docker().clean() | 
 |         return 0 | 
 |  | 
 |  | 
 | class ImagesCommand(SubCommand): | 
 |     """Run "docker images" command""" | 
 |     name = "images" | 
 |  | 
 |     def run(self, args, argv): | 
 |         return Docker().command("images", argv, args.quiet) | 
 |  | 
 |  | 
 | class ProbeCommand(SubCommand): | 
 |     """Probe if we can run docker automatically""" | 
 |     name = "probe" | 
 |  | 
 |     def run(self, args, argv): | 
 |         try: | 
 |             docker = Docker() | 
 |             if docker._command[0] == "docker": | 
 |                 print("docker") | 
 |             elif docker._command[0] == "sudo": | 
 |                 print("sudo docker") | 
 |             elif docker._command[0] == "podman": | 
 |                 print("podman") | 
 |         except Exception: | 
 |             print("no") | 
 |  | 
 |         return | 
 |  | 
 |  | 
 | class CcCommand(SubCommand): | 
 |     """Compile sources with cc in images""" | 
 |     name = "cc" | 
 |  | 
 |     def args(self, parser): | 
 |         parser.add_argument("--image", "-i", required=True, | 
 |                             help="The docker image in which to run cc") | 
 |         parser.add_argument("--cc", default="cc", | 
 |                             help="The compiler executable to call") | 
 |         parser.add_argument("--source-path", "-s", nargs="*", dest="paths", | 
 |                             help="""Extra paths to (ro) mount into container for | 
 |                             reading sources""") | 
 |  | 
 |     def run(self, args, argv): | 
 |         if argv and argv[0] == "--": | 
 |             argv = argv[1:] | 
 |         cwd = os.getcwd() | 
 |         cmd = ["-w", cwd, | 
 |                "-v", "%s:%s:rw" % (cwd, cwd)] | 
 |         if args.paths: | 
 |             for p in args.paths: | 
 |                 cmd += ["-v", "%s:%s:ro,z" % (p, p)] | 
 |         cmd += [args.image, args.cc] | 
 |         cmd += argv | 
 |         return Docker().run(cmd, False, quiet=args.quiet, | 
 |                             as_user=True) | 
 |  | 
 |  | 
 | def main(): | 
 |     global USE_ENGINE | 
 |  | 
 |     parser = argparse.ArgumentParser(description="A Docker helper", | 
 |                                      usage="%s <subcommand> ..." % | 
 |                                      os.path.basename(sys.argv[0])) | 
 |     parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum), | 
 |                         help="specify which container engine to use") | 
 |     subparsers = parser.add_subparsers(title="subcommands", help=None) | 
 |     for cls in SubCommand.__subclasses__(): | 
 |         cmd = cls() | 
 |         subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) | 
 |         cmd.shared_args(subp) | 
 |         cmd.args(subp) | 
 |         subp.set_defaults(cmdobj=cmd) | 
 |     args, argv = parser.parse_known_args() | 
 |     if args.engine: | 
 |         USE_ENGINE = args.engine | 
 |     return args.cmdobj.run(args, argv) | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     sys.exit(main()) |