qemu/tests/docker/docker.py
<<
>>
Prefs
   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())
 759