cachepc-qemu

Fork of AMDESE/qemu with changes for cachepc side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-qemu
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

docker.py (25856B)


      1#!/usr/bin/env python3
      2#
      3# Docker controlling module
      4#
      5# Copyright (c) 2016 Red Hat Inc.
      6#
      7# Authors:
      8#  Fam Zheng <famz@redhat.com>
      9#
     10# This work is licensed under the terms of the GNU GPL, version 2
     11# or (at your option) any later version. See the COPYING file in
     12# the top-level directory.
     13
     14import os
     15import sys
     16import subprocess
     17import json
     18import hashlib
     19import atexit
     20import uuid
     21import argparse
     22import enum
     23import tempfile
     24import re
     25import signal
     26from tarfile import TarFile, TarInfo
     27from io import StringIO, BytesIO
     28from shutil import copy, rmtree
     29from pwd import getpwuid
     30from datetime import datetime, timedelta
     31
     32
     33FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
     34
     35
     36DEVNULL = open(os.devnull, 'wb')
     37
     38class EngineEnum(enum.IntEnum):
     39    AUTO = 1
     40    DOCKER = 2
     41    PODMAN = 3
     42
     43    def __str__(self):
     44        return self.name.lower()
     45
     46    def __repr__(self):
     47        return str(self)
     48
     49    @staticmethod
     50    def argparse(s):
     51        try:
     52            return EngineEnum[s.upper()]
     53        except KeyError:
     54            return s
     55
     56
     57USE_ENGINE = EngineEnum.AUTO
     58
     59def _bytes_checksum(bytes):
     60    """Calculate a digest string unique to the text content"""
     61    return hashlib.sha1(bytes).hexdigest()
     62
     63def _text_checksum(text):
     64    """Calculate a digest string unique to the text content"""
     65    return _bytes_checksum(text.encode('utf-8'))
     66
     67def _read_dockerfile(path):
     68    return open(path, 'rt', encoding='utf-8').read()
     69
     70def _file_checksum(filename):
     71    return _bytes_checksum(open(filename, 'rb').read())
     72
     73
     74def _guess_engine_command():
     75    """ Guess a working engine command or raise exception if not found"""
     76    commands = []
     77
     78    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]:
     79        commands += [["podman"]]
     80    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]:
     81        commands += [["docker"], ["sudo", "-n", "docker"]]
     82    for cmd in commands:
     83        try:
     84            # docker version will return the client details in stdout
     85            # but still report a status of 1 if it can't contact the daemon
     86            if subprocess.call(cmd + ["version"],
     87                               stdout=DEVNULL, stderr=DEVNULL) == 0:
     88                return cmd
     89        except OSError:
     90            pass
     91    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
     92    raise Exception("Cannot find working engine command. Tried:\n%s" %
     93                    commands_txt)
     94
     95
     96def _copy_with_mkdir(src, root_dir, sub_path='.', name=None):
     97    """Copy src into root_dir, creating sub_path as needed."""
     98    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
     99    try:
    100        os.makedirs(dest_dir)
    101    except OSError:
    102        # we can safely ignore already created directories
    103        pass
    104
    105    dest_file = "%s/%s" % (dest_dir, name if name else os.path.basename(src))
    106
    107    try:
    108        copy(src, dest_file)
    109    except FileNotFoundError:
    110        print("Couldn't copy %s to %s" % (src, dest_file))
    111        pass
    112
    113
    114def _get_so_libs(executable):
    115    """Return a list of libraries associated with an executable.
    116
    117    The paths may be symbolic links which would need to be resolved to
    118    ensure the right data is copied."""
    119
    120    libs = []
    121    ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)")
    122    try:
    123        ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8')
    124        for line in ldd_output.split("\n"):
    125            search = ldd_re.search(line)
    126            if search:
    127                try:
    128                    libs.append(search.group(1))
    129                except IndexError:
    130                    pass
    131    except subprocess.CalledProcessError:
    132        print("%s had no associated libraries (static build?)" % (executable))
    133
    134    return libs
    135
    136
    137def _copy_binary_with_libs(src, bin_dest, dest_dir):
    138    """Maybe copy a binary and all its dependent libraries.
    139
    140    If bin_dest isn't set we only copy the support libraries because
    141    we don't need qemu in the docker path to run (due to persistent
    142    mapping). Indeed users may get confused if we aren't running what
    143    is in the image.
    144
    145    This does rely on the host file-system being fairly multi-arch
    146    aware so the file don't clash with the guests layout.
    147    """
    148
    149    if bin_dest:
    150        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
    151    else:
    152        print("only copying support libraries for %s" % (src))
    153
    154    libs = _get_so_libs(src)
    155    if libs:
    156        for l in libs:
    157            so_path = os.path.dirname(l)
    158            name = os.path.basename(l)
    159            real_l = os.path.realpath(l)
    160            _copy_with_mkdir(real_l, dest_dir, so_path, name)
    161
    162
    163def _check_binfmt_misc(executable):
    164    """Check binfmt_misc has entry for executable in the right place.
    165
    166    The details of setting up binfmt_misc are outside the scope of
    167    this script but we should at least fail early with a useful
    168    message if it won't work.
    169
    170    Returns the configured binfmt path and a valid flag. For
    171    persistent configurations we will still want to copy and dependent
    172    libraries.
    173    """
    174
    175    binary = os.path.basename(executable)
    176    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
    177
    178    if not os.path.exists(binfmt_entry):
    179        print ("No binfmt_misc entry for %s" % (binary))
    180        return None, False
    181
    182    with open(binfmt_entry) as x: entry = x.read()
    183
    184    if re.search("flags:.*F.*\n", entry):
    185        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
    186              (binary))
    187        return None, True
    188
    189    m = re.search("interpreter (\S+)\n", entry)
    190    interp = m.group(1)
    191    if interp and interp != executable:
    192        print("binfmt_misc for %s does not point to %s, using %s" %
    193              (binary, executable, interp))
    194
    195    return interp, True
    196
    197
    198def _read_qemu_dockerfile(img_name):
    199    # special case for Debian linux-user images
    200    if img_name.startswith("debian") and img_name.endswith("user"):
    201        img_name = "debian-bootstrap"
    202
    203    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
    204                      img_name + ".docker")
    205    return _read_dockerfile(df)
    206
    207
    208def _dockerfile_preprocess(df):
    209    out = ""
    210    for l in df.splitlines():
    211        if len(l.strip()) == 0 or l.startswith("#"):
    212            continue
    213        from_pref = "FROM qemu/"
    214        if l.startswith(from_pref):
    215            # TODO: Alternatively we could replace this line with "FROM $ID"
    216            # where $ID is the image's hex id obtained with
    217            #    $ docker images $IMAGE --format="{{.Id}}"
    218            # but unfortunately that's not supported by RHEL 7.
    219            inlining = _read_qemu_dockerfile(l[len(from_pref):])
    220            out += _dockerfile_preprocess(inlining)
    221            continue
    222        out += l + "\n"
    223    return out
    224
    225
    226class Docker(object):
    227    """ Running Docker commands """
    228    def __init__(self):
    229        self._command = _guess_engine_command()
    230
    231        if ("docker" in self._command and
    232            "TRAVIS" not in os.environ and
    233            "GITLAB_CI" not in os.environ):
    234            os.environ["DOCKER_BUILDKIT"] = "1"
    235            self._buildkit = True
    236        else:
    237            self._buildkit = False
    238
    239        self._instance = None
    240        atexit.register(self._kill_instances)
    241        signal.signal(signal.SIGTERM, self._kill_instances)
    242        signal.signal(signal.SIGHUP, self._kill_instances)
    243
    244    def _do(self, cmd, quiet=True, **kwargs):
    245        if quiet:
    246            kwargs["stdout"] = DEVNULL
    247        return subprocess.call(self._command + cmd, **kwargs)
    248
    249    def _do_check(self, cmd, quiet=True, **kwargs):
    250        if quiet:
    251            kwargs["stdout"] = DEVNULL
    252        return subprocess.check_call(self._command + cmd, **kwargs)
    253
    254    def _do_kill_instances(self, only_known, only_active=True):
    255        cmd = ["ps", "-q"]
    256        if not only_active:
    257            cmd.append("-a")
    258
    259        filter = "--filter=label=com.qemu.instance.uuid"
    260        if only_known:
    261            if self._instance:
    262                filter += "=%s" % (self._instance)
    263            else:
    264                # no point trying to kill, we finished
    265                return
    266
    267        print("filter=%s" % (filter))
    268        cmd.append(filter)
    269        for i in self._output(cmd).split():
    270            self._do(["rm", "-f", i])
    271
    272    def clean(self):
    273        self._do_kill_instances(False, False)
    274        return 0
    275
    276    def _kill_instances(self, *args, **kwargs):
    277        return self._do_kill_instances(True)
    278
    279    def _output(self, cmd, **kwargs):
    280        try:
    281            return subprocess.check_output(self._command + cmd,
    282                                           stderr=subprocess.STDOUT,
    283                                           encoding='utf-8',
    284                                           **kwargs)
    285        except TypeError:
    286            # 'encoding' argument was added in 3.6+
    287            return subprocess.check_output(self._command + cmd,
    288                                           stderr=subprocess.STDOUT,
    289                                           **kwargs).decode('utf-8')
    290
    291
    292    def inspect_tag(self, tag):
    293        try:
    294            return self._output(["inspect", tag])
    295        except subprocess.CalledProcessError:
    296            return None
    297
    298    def get_image_creation_time(self, info):
    299        return json.loads(info)[0]["Created"]
    300
    301    def get_image_dockerfile_checksum(self, tag):
    302        resp = self.inspect_tag(tag)
    303        labels = json.loads(resp)[0]["Config"].get("Labels", {})
    304        return labels.get("com.qemu.dockerfile-checksum", "")
    305
    306    def build_image(self, tag, docker_dir, dockerfile,
    307                    quiet=True, user=False, argv=None, registry=None,
    308                    extra_files_cksum=[]):
    309        if argv is None:
    310            argv = []
    311
    312        # pre-calculate the docker checksum before any
    313        # substitutions we make for caching
    314        checksum = _text_checksum(_dockerfile_preprocess(dockerfile))
    315
    316        if registry is not None:
    317            sources = re.findall("FROM qemu\/(.*)", dockerfile)
    318            # Fetch any cache layers we can, may fail
    319            for s in sources:
    320                pull_args = ["pull", "%s/qemu/%s" % (registry, s)]
    321                if self._do(pull_args, quiet=quiet) != 0:
    322                    registry = None
    323                    break
    324            # Make substitutions
    325            if registry is not None:
    326                dockerfile = dockerfile.replace("FROM qemu/",
    327                                                "FROM %s/qemu/" %
    328                                                (registry))
    329
    330        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
    331                                             encoding='utf-8',
    332                                             dir=docker_dir, suffix=".docker")
    333        tmp_df.write(dockerfile)
    334
    335        if user:
    336            uid = os.getuid()
    337            uname = getpwuid(uid).pw_name
    338            tmp_df.write("\n")
    339            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
    340                         (uname, uid, uname))
    341
    342        tmp_df.write("\n")
    343        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum))
    344        for f, c in extra_files_cksum:
    345            tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c))
    346
    347        tmp_df.flush()
    348
    349        build_args = ["build", "-t", tag, "-f", tmp_df.name]
    350        if self._buildkit:
    351            build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"]
    352
    353        if registry is not None:
    354            pull_args = ["pull", "%s/%s" % (registry, tag)]
    355            self._do(pull_args, quiet=quiet)
    356            cache = "%s/%s" % (registry, tag)
    357            build_args += ["--cache-from", cache]
    358        build_args += argv
    359        build_args += [docker_dir]
    360
    361        self._do_check(build_args,
    362                       quiet=quiet)
    363
    364    def update_image(self, tag, tarball, quiet=True):
    365        "Update a tagged image using "
    366
    367        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
    368
    369    def image_matches_dockerfile(self, tag, dockerfile):
    370        try:
    371            checksum = self.get_image_dockerfile_checksum(tag)
    372        except Exception:
    373            return False
    374        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
    375
    376    def run(self, cmd, keep, quiet, as_user=False):
    377        label = uuid.uuid4().hex
    378        if not keep:
    379            self._instance = label
    380
    381        if as_user:
    382            uid = os.getuid()
    383            cmd = [ "-u", str(uid) ] + cmd
    384            # podman requires a bit more fiddling
    385            if self._command[0] == "podman":
    386                cmd.insert(0, '--userns=keep-id')
    387
    388        ret = self._do_check(["run", "--rm", "--label",
    389                             "com.qemu.instance.uuid=" + label] + cmd,
    390                             quiet=quiet)
    391        if not keep:
    392            self._instance = None
    393        return ret
    394
    395    def command(self, cmd, argv, quiet):
    396        return self._do([cmd] + argv, quiet=quiet)
    397
    398
    399class SubCommand(object):
    400    """A SubCommand template base class"""
    401    name = None  # Subcommand name
    402
    403    def shared_args(self, parser):
    404        parser.add_argument("--quiet", action="store_true",
    405                            help="Run quietly unless an error occurred")
    406
    407    def args(self, parser):
    408        """Setup argument parser"""
    409        pass
    410
    411    def run(self, args, argv):
    412        """Run command.
    413        args: parsed argument by argument parser.
    414        argv: remaining arguments from sys.argv.
    415        """
    416        pass
    417
    418
    419class RunCommand(SubCommand):
    420    """Invoke docker run and take care of cleaning up"""
    421    name = "run"
    422
    423    def args(self, parser):
    424        parser.add_argument("--keep", action="store_true",
    425                            help="Don't remove image when command completes")
    426        parser.add_argument("--run-as-current-user", action="store_true",
    427                            help="Run container using the current user's uid")
    428
    429    def run(self, args, argv):
    430        return Docker().run(argv, args.keep, quiet=args.quiet,
    431                            as_user=args.run_as_current_user)
    432
    433
    434class BuildCommand(SubCommand):
    435    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
    436    name = "build"
    437
    438    def args(self, parser):
    439        parser.add_argument("--include-executable", "-e",
    440                            help="""Specify a binary that will be copied to the
    441                            container together with all its dependent
    442                            libraries""")
    443        parser.add_argument("--skip-binfmt",
    444                            action="store_true",
    445                            help="""Skip binfmt entry check (used for testing)""")
    446        parser.add_argument("--extra-files", nargs='*',
    447                            help="""Specify files that will be copied in the
    448                            Docker image, fulfilling the ADD directive from the
    449                            Dockerfile""")
    450        parser.add_argument("--add-current-user", "-u", dest="user",
    451                            action="store_true",
    452                            help="Add the current user to image's passwd")
    453        parser.add_argument("--registry", "-r",
    454                            help="cache from docker registry")
    455        parser.add_argument("-t", dest="tag",
    456                            help="Image Tag")
    457        parser.add_argument("-f", dest="dockerfile",
    458                            help="Dockerfile name")
    459
    460    def run(self, args, argv):
    461        dockerfile = _read_dockerfile(args.dockerfile)
    462        tag = args.tag
    463
    464        dkr = Docker()
    465        if "--no-cache" not in argv and \
    466           dkr.image_matches_dockerfile(tag, dockerfile):
    467            if not args.quiet:
    468                print("Image is up to date.")
    469        else:
    470            # Create a docker context directory for the build
    471            docker_dir = tempfile.mkdtemp(prefix="docker_build")
    472
    473            # Validate binfmt_misc will work
    474            if args.skip_binfmt:
    475                qpath = args.include_executable
    476            elif args.include_executable:
    477                qpath, enabled = _check_binfmt_misc(args.include_executable)
    478                if not enabled:
    479                    return 1
    480
    481            # Is there a .pre file to run in the build context?
    482            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
    483            if os.path.exists(docker_pre):
    484                stdout = DEVNULL if args.quiet else None
    485                rc = subprocess.call(os.path.realpath(docker_pre),
    486                                     cwd=docker_dir, stdout=stdout)
    487                if rc == 3:
    488                    print("Skip")
    489                    return 0
    490                elif rc != 0:
    491                    print("%s exited with code %d" % (docker_pre, rc))
    492                    return 1
    493
    494            # Copy any extra files into the Docker context. These can be
    495            # included by the use of the ADD directive in the Dockerfile.
    496            cksum = []
    497            if args.include_executable:
    498                # FIXME: there is no checksum of this executable and the linked
    499                # libraries, once the image built any change of this executable
    500                # or any library won't trigger another build.
    501                _copy_binary_with_libs(args.include_executable,
    502                                       qpath, docker_dir)
    503
    504            for filename in args.extra_files or []:
    505                _copy_with_mkdir(filename, docker_dir)
    506                cksum += [(filename, _file_checksum(filename))]
    507
    508            argv += ["--build-arg=" + k.lower() + "=" + v
    509                     for k, v in os.environ.items()
    510                     if k.lower() in FILTERED_ENV_NAMES]
    511            dkr.build_image(tag, docker_dir, dockerfile,
    512                            quiet=args.quiet, user=args.user,
    513                            argv=argv, registry=args.registry,
    514                            extra_files_cksum=cksum)
    515
    516            rmtree(docker_dir)
    517
    518        return 0
    519
    520class FetchCommand(SubCommand):
    521    """ Fetch a docker image from the registry. Args: <tag> <registry>"""
    522    name = "fetch"
    523
    524    def args(self, parser):
    525        parser.add_argument("tag",
    526                            help="Local tag for image")
    527        parser.add_argument("registry",
    528                            help="Docker registry")
    529
    530    def run(self, args, argv):
    531        dkr = Docker()
    532        dkr.command(cmd="pull", quiet=args.quiet,
    533                    argv=["%s/%s" % (args.registry, args.tag)])
    534        dkr.command(cmd="tag", quiet=args.quiet,
    535                    argv=["%s/%s" % (args.registry, args.tag), args.tag])
    536
    537
    538class UpdateCommand(SubCommand):
    539    """ Update a docker image. Args: <tag> <actions>"""
    540    name = "update"
    541
    542    def args(self, parser):
    543        parser.add_argument("tag",
    544                            help="Image Tag")
    545        parser.add_argument("--executable",
    546                            help="Executable to copy")
    547        parser.add_argument("--add-current-user", "-u", dest="user",
    548                            action="store_true",
    549                            help="Add the current user to image's passwd")
    550
    551    def run(self, args, argv):
    552        # Create a temporary tarball with our whole build context and
    553        # dockerfile for the update
    554        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
    555        tmp_tar = TarFile(fileobj=tmp, mode='w')
    556
    557        # Create a Docker buildfile
    558        df = StringIO()
    559        df.write(u"FROM %s\n" % args.tag)
    560
    561        if args.executable:
    562            # Add the executable to the tarball, using the current
    563            # configured binfmt_misc path. If we don't get a path then we
    564            # only need the support libraries copied
    565            ff, enabled = _check_binfmt_misc(args.executable)
    566
    567            if not enabled:
    568                print("binfmt_misc not enabled, update disabled")
    569                return 1
    570
    571            if ff:
    572                tmp_tar.add(args.executable, arcname=ff)
    573
    574            # Add any associated libraries
    575            libs = _get_so_libs(args.executable)
    576            if libs:
    577                for l in libs:
    578                    so_path = os.path.dirname(l)
    579                    name = os.path.basename(l)
    580                    real_l = os.path.realpath(l)
    581                    try:
    582                        tmp_tar.add(real_l, arcname="%s/%s" % (so_path, name))
    583                    except FileNotFoundError:
    584                        print("Couldn't add %s/%s to archive" % (so_path, name))
    585                        pass
    586
    587            df.write(u"ADD . /\n")
    588
    589        if args.user:
    590            uid = os.getuid()
    591            uname = getpwuid(uid).pw_name
    592            df.write("\n")
    593            df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
    594                     (uname, uid, uname))
    595
    596        df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8"))
    597
    598        df_tar = TarInfo(name="Dockerfile")
    599        df_tar.size = df_bytes.getbuffer().nbytes
    600        tmp_tar.addfile(df_tar, fileobj=df_bytes)
    601
    602        tmp_tar.close()
    603
    604        # reset the file pointers
    605        tmp.flush()
    606        tmp.seek(0)
    607
    608        # Run the build with our tarball context
    609        dkr = Docker()
    610        dkr.update_image(args.tag, tmp, quiet=args.quiet)
    611
    612        return 0
    613
    614
    615class CleanCommand(SubCommand):
    616    """Clean up docker instances"""
    617    name = "clean"
    618
    619    def run(self, args, argv):
    620        Docker().clean()
    621        return 0
    622
    623
    624class ImagesCommand(SubCommand):
    625    """Run "docker images" command"""
    626    name = "images"
    627
    628    def run(self, args, argv):
    629        return Docker().command("images", argv, args.quiet)
    630
    631
    632class ProbeCommand(SubCommand):
    633    """Probe if we can run docker automatically"""
    634    name = "probe"
    635
    636    def run(self, args, argv):
    637        try:
    638            docker = Docker()
    639            if docker._command[0] == "docker":
    640                print("docker")
    641            elif docker._command[0] == "sudo":
    642                print("sudo docker")
    643            elif docker._command[0] == "podman":
    644                print("podman")
    645        except Exception:
    646            print("no")
    647
    648        return
    649
    650
    651class CcCommand(SubCommand):
    652    """Compile sources with cc in images"""
    653    name = "cc"
    654
    655    def args(self, parser):
    656        parser.add_argument("--image", "-i", required=True,
    657                            help="The docker image in which to run cc")
    658        parser.add_argument("--cc", default="cc",
    659                            help="The compiler executable to call")
    660        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
    661                            help="""Extra paths to (ro) mount into container for
    662                            reading sources""")
    663
    664    def run(self, args, argv):
    665        if argv and argv[0] == "--":
    666            argv = argv[1:]
    667        cwd = os.getcwd()
    668        cmd = ["-w", cwd,
    669               "-v", "%s:%s:rw" % (cwd, cwd)]
    670        if args.paths:
    671            for p in args.paths:
    672                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
    673        cmd += [args.image, args.cc]
    674        cmd += argv
    675        return Docker().run(cmd, False, quiet=args.quiet,
    676                            as_user=True)
    677
    678
    679class CheckCommand(SubCommand):
    680    """Check if we need to re-build a docker image out of a dockerfile.
    681    Arguments: <tag> <dockerfile>"""
    682    name = "check"
    683
    684    def args(self, parser):
    685        parser.add_argument("tag",
    686                            help="Image Tag")
    687        parser.add_argument("dockerfile", default=None,
    688                            help="Dockerfile name", nargs='?')
    689        parser.add_argument("--checktype", choices=["checksum", "age"],
    690                            default="checksum", help="check type")
    691        parser.add_argument("--olderthan", default=60, type=int,
    692                            help="number of minutes")
    693
    694    def run(self, args, argv):
    695        tag = args.tag
    696
    697        try:
    698            dkr = Docker()
    699        except subprocess.CalledProcessError:
    700            print("Docker not set up")
    701            return 1
    702
    703        info = dkr.inspect_tag(tag)
    704        if info is None:
    705            print("Image does not exist")
    706            return 1
    707
    708        if args.checktype == "checksum":
    709            if not args.dockerfile:
    710                print("Need a dockerfile for tag:%s" % (tag))
    711                return 1
    712
    713            dockerfile = _read_dockerfile(args.dockerfile)
    714
    715            if dkr.image_matches_dockerfile(tag, dockerfile):
    716                if not args.quiet:
    717                    print("Image is up to date")
    718                return 0
    719            else:
    720                print("Image needs updating")
    721                return 1
    722        elif args.checktype == "age":
    723            timestr = dkr.get_image_creation_time(info).split(".")[0]
    724            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
    725            past = datetime.now() - timedelta(minutes=args.olderthan)
    726            if created < past:
    727                print ("Image created @ %s more than %d minutes old" %
    728                       (timestr, args.olderthan))
    729                return 1
    730            else:
    731                if not args.quiet:
    732                    print ("Image less than %d minutes old" % (args.olderthan))
    733                return 0
    734
    735
    736def main():
    737    global USE_ENGINE
    738
    739    parser = argparse.ArgumentParser(description="A Docker helper",
    740                                     usage="%s <subcommand> ..." %
    741                                     os.path.basename(sys.argv[0]))
    742    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
    743                        help="specify which container engine to use")
    744    subparsers = parser.add_subparsers(title="subcommands", help=None)
    745    for cls in SubCommand.__subclasses__():
    746        cmd = cls()
    747        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
    748        cmd.shared_args(subp)
    749        cmd.args(subp)
    750        subp.set_defaults(cmdobj=cmd)
    751    args, argv = parser.parse_known_args()
    752    if args.engine:
    753        USE_ENGINE = args.engine
    754    return args.cmdobj.run(args, argv)
    755
    756
    757if __name__ == "__main__":
    758    sys.exit(main())