xref: /qemu/tests/docker/docker.py (revision 432d8ad5f6a47c09648320412c9552637261bdc1)
14485b04bSFam Zheng#!/usr/bin/env python2
24485b04bSFam Zheng#
34485b04bSFam Zheng# Docker controlling module
44485b04bSFam Zheng#
54485b04bSFam Zheng# Copyright (c) 2016 Red Hat Inc.
64485b04bSFam Zheng#
74485b04bSFam Zheng# Authors:
84485b04bSFam Zheng#  Fam Zheng <famz@redhat.com>
94485b04bSFam Zheng#
104485b04bSFam Zheng# This work is licensed under the terms of the GNU GPL, version 2
114485b04bSFam Zheng# or (at your option) any later version. See the COPYING file in
124485b04bSFam Zheng# the top-level directory.
134485b04bSFam Zheng
14f03868bdSEduardo Habkostfrom __future__ import print_function
154485b04bSFam Zhengimport os
164485b04bSFam Zhengimport sys
174485b04bSFam Zhengimport subprocess
184485b04bSFam Zhengimport json
194485b04bSFam Zhengimport hashlib
204485b04bSFam Zhengimport atexit
214485b04bSFam Zhengimport uuid
22ae68fdabSEduardo Habkostimport argparse
234485b04bSFam Zhengimport tempfile
24504ca3c2SAlex Bennéeimport re
2597cba1a1SFam Zhengimport signal
266e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo
277a5d936bSAlex Bennéetry:
286e733da6SAlex Bennée    from StringIO import StringIO
297a5d936bSAlex Bennéeexcept ImportError:
307a5d936bSAlex Bennée    from io import StringIO
31a9f8d038SAlex Bennéefrom shutil import copy, rmtree
32414a8ce5SAlex Bennéefrom pwd import getpwuid
337b882245SAlex Bennéefrom datetime import datetime, timedelta
344485b04bSFam Zheng
35c9772570SSascha Silbe
3606cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
3706cc3551SPhilippe Mathieu-Daudé
3806cc3551SPhilippe Mathieu-Daudé
39c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb')
40c9772570SSascha Silbe
41c9772570SSascha Silbe
424485b04bSFam Zhengdef _text_checksum(text):
434485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
444485b04bSFam Zheng    return hashlib.sha1(text).hexdigest()
454485b04bSFam Zheng
46*432d8ad5SAlex Bennée
47438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
48438d1168SPhilippe Mathieu-Daudé    return _text_checksum(open(filename, 'rb').read())
49438d1168SPhilippe Mathieu-Daudé
50*432d8ad5SAlex Bennée
514485b04bSFam Zhengdef _guess_docker_command():
524485b04bSFam Zheng    """ Guess a working docker command or raise exception if not found"""
534485b04bSFam Zheng    commands = [["docker"], ["sudo", "-n", "docker"]]
544485b04bSFam Zheng    for cmd in commands:
550679f98bSEduardo Habkost        try:
5683405c45SAlex Bennée            # docker version will return the client details in stdout
5783405c45SAlex Bennée            # but still report a status of 1 if it can't contact the daemon
5883405c45SAlex Bennée            if subprocess.call(cmd + ["version"],
59c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
604485b04bSFam Zheng                return cmd
610679f98bSEduardo Habkost        except OSError:
620679f98bSEduardo Habkost            pass
634485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
64*432d8ad5SAlex Bennée    raise Exception("Cannot find working docker command. Tried:\n%s" %
654485b04bSFam Zheng                    commands_txt)
664485b04bSFam Zheng
67*432d8ad5SAlex Bennée
682499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'):
69504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
70504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
71504ca3c2SAlex Bennée    try:
72504ca3c2SAlex Bennée        os.makedirs(dest_dir)
73504ca3c2SAlex Bennée    except OSError:
74504ca3c2SAlex Bennée        # we can safely ignore already created directories
75504ca3c2SAlex Bennée        pass
76504ca3c2SAlex Bennée
77504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
78504ca3c2SAlex Bennée    copy(src, dest_file)
79504ca3c2SAlex Bennée
80504ca3c2SAlex Bennée
81504ca3c2SAlex Bennéedef _get_so_libs(executable):
82504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
83504ca3c2SAlex Bennée
84504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
85504ca3c2SAlex Bennée    ensure theright data is copied."""
86504ca3c2SAlex Bennée
87504ca3c2SAlex Bennée    libs = []
88504ca3c2SAlex Bennée    ldd_re = re.compile(r"(/.*/)(\S*)")
89504ca3c2SAlex Bennée    try:
90504ca3c2SAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable])
91504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
92504ca3c2SAlex Bennée            search = ldd_re.search(line)
93504ca3c2SAlex Bennée            if search and len(search.groups()) == 2:
94504ca3c2SAlex Bennée                so_path = search.groups()[0]
95504ca3c2SAlex Bennée                so_lib = search.groups()[1]
96504ca3c2SAlex Bennée                libs.append("%s/%s" % (so_path, so_lib))
97504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
98f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
99504ca3c2SAlex Bennée
100504ca3c2SAlex Bennée    return libs
101504ca3c2SAlex Bennée
102*432d8ad5SAlex Bennée
103d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
104d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
105d10404b1SAlex Bennée
106d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
107d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
108d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
109d10404b1SAlex Bennée    is in the image.
110504ca3c2SAlex Bennée
111504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
112d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
113d10404b1SAlex Bennée    """
114504ca3c2SAlex Bennée
115d10404b1SAlex Bennée    if bin_dest:
116d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
117d10404b1SAlex Bennée    else:
118d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
119504ca3c2SAlex Bennée
120504ca3c2SAlex Bennée    libs = _get_so_libs(src)
121504ca3c2SAlex Bennée    if libs:
122504ca3c2SAlex Bennée        for l in libs:
123504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
124504ca3c2SAlex Bennée            _copy_with_mkdir(l, dest_dir, so_path)
125504ca3c2SAlex Bennée
12615352decSAlex Bennée
12715352decSAlex Bennéedef _check_binfmt_misc(executable):
12815352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
12915352decSAlex Bennée
13015352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
13115352decSAlex Bennée    this script but we should at least fail early with a useful
132d10404b1SAlex Bennée    message if it won't work.
133d10404b1SAlex Bennée
134d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
135d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
136d10404b1SAlex Bennée    libraries.
137d10404b1SAlex Bennée    """
13815352decSAlex Bennée
13915352decSAlex Bennée    binary = os.path.basename(executable)
14015352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
14115352decSAlex Bennée
14215352decSAlex Bennée    if not os.path.exists(binfmt_entry):
14315352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
144d10404b1SAlex Bennée        return None, False
14515352decSAlex Bennée
14615352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
14715352decSAlex Bennée
14843c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
149*432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
15043c898b7SAlex Bennée              (binary))
151d10404b1SAlex Bennée        return None, True
15243c898b7SAlex Bennée
1537e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1547e81d198SAlex Bennée    interp = m.group(1)
1557e81d198SAlex Bennée    if interp and interp != executable:
1567e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1577e81d198SAlex Bennée              (binary, executable, interp))
15815352decSAlex Bennée
159d10404b1SAlex Bennée    return interp, True
160d10404b1SAlex Bennée
16115352decSAlex Bennée
162c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
163547cb45eSAlex Bennée    # special case for Debian linux-user images
164547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
165547cb45eSAlex Bennée        img_name = "debian-bootstrap"
166547cb45eSAlex Bennée
167c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
168c1958e9dSFam Zheng                      img_name + ".docker")
169c1958e9dSFam Zheng    return open(df, "r").read()
170c1958e9dSFam Zheng
171*432d8ad5SAlex Bennée
172c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
173c1958e9dSFam Zheng    out = ""
174c1958e9dSFam Zheng    for l in df.splitlines():
175c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
176c1958e9dSFam Zheng            continue
177c1958e9dSFam Zheng        from_pref = "FROM qemu:"
178c1958e9dSFam Zheng        if l.startswith(from_pref):
179c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
180c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
181c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
182c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
183c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
184c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
185c1958e9dSFam Zheng            continue
186c1958e9dSFam Zheng        out += l + "\n"
187c1958e9dSFam Zheng    return out
188c1958e9dSFam Zheng
189*432d8ad5SAlex Bennée
1904485b04bSFam Zhengclass Docker(object):
1914485b04bSFam Zheng    """ Running Docker commands """
1924485b04bSFam Zheng    def __init__(self):
1934485b04bSFam Zheng        self._command = _guess_docker_command()
1944485b04bSFam Zheng        self._instances = []
1954485b04bSFam Zheng        atexit.register(self._kill_instances)
19697cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
19797cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
1984485b04bSFam Zheng
19958bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2004485b04bSFam Zheng        if quiet:
201c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2024485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2034485b04bSFam Zheng
2040b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2050b95ff72SFam Zheng        if quiet:
2060b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2070b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2080b95ff72SFam Zheng
2094485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2104485b04bSFam Zheng        cmd = ["ps", "-q"]
2114485b04bSFam Zheng        if not only_active:
2124485b04bSFam Zheng            cmd.append("-a")
2134485b04bSFam Zheng        for i in self._output(cmd).split():
2144485b04bSFam Zheng            resp = self._output(["inspect", i])
2154485b04bSFam Zheng            labels = json.loads(resp)[0]["Config"]["Labels"]
2164485b04bSFam Zheng            active = json.loads(resp)[0]["State"]["Running"]
2174485b04bSFam Zheng            if not labels:
2184485b04bSFam Zheng                continue
2194485b04bSFam Zheng            instance_uuid = labels.get("com.qemu.instance.uuid", None)
2204485b04bSFam Zheng            if not instance_uuid:
2214485b04bSFam Zheng                continue
2224485b04bSFam Zheng            if only_known and instance_uuid not in self._instances:
2234485b04bSFam Zheng                continue
224f03868bdSEduardo Habkost            print("Terminating", i)
2254485b04bSFam Zheng            if active:
2264485b04bSFam Zheng                self._do(["kill", i])
2274485b04bSFam Zheng            self._do(["rm", i])
2284485b04bSFam Zheng
2294485b04bSFam Zheng    def clean(self):
2304485b04bSFam Zheng        self._do_kill_instances(False, False)
2314485b04bSFam Zheng        return 0
2324485b04bSFam Zheng
23397cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2344485b04bSFam Zheng        return self._do_kill_instances(True)
2354485b04bSFam Zheng
2364485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2374485b04bSFam Zheng        return subprocess.check_output(self._command + cmd,
2384485b04bSFam Zheng                                       stderr=subprocess.STDOUT,
2394485b04bSFam Zheng                                       **kwargs)
2404485b04bSFam Zheng
241f97da1f7SAlex Bennée    def inspect_tag(self, tag):
242f97da1f7SAlex Bennée        try:
243f97da1f7SAlex Bennée            return self._output(["inspect", tag])
244f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
245f97da1f7SAlex Bennée            return None
246f97da1f7SAlex Bennée
2477b882245SAlex Bennée    def get_image_creation_time(self, info):
2487b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2497b882245SAlex Bennée
2504485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
251f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2524485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2534485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
2544485b04bSFam Zheng
255414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
256438d1168SPhilippe Mathieu-Daudé                    quiet=True, user=False, argv=None, extra_files_cksum=[]):
257*432d8ad5SAlex Bennée        if argv is None:
2584485b04bSFam Zheng            argv = []
2594485b04bSFam Zheng
260a9f8d038SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
2614485b04bSFam Zheng        tmp_df.write(dockerfile)
2624485b04bSFam Zheng
263414a8ce5SAlex Bennée        if user:
264414a8ce5SAlex Bennée            uid = os.getuid()
265414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
266414a8ce5SAlex Bennée            tmp_df.write("\n")
267414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
268414a8ce5SAlex Bennée                         (uname, uid, uname))
269414a8ce5SAlex Bennée
2704485b04bSFam Zheng        tmp_df.write("\n")
2714485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
272f9172822SAlex Bennée                     _text_checksum(_dockerfile_preprocess(dockerfile)))
273f9172822SAlex Bennée        for f, c in extra_files_cksum:
274f9172822SAlex Bennée            tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
275f9172822SAlex Bennée
2764485b04bSFam Zheng        tmp_df.flush()
277a9f8d038SAlex Bennée
278*432d8ad5SAlex Bennée        self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv +
279a9f8d038SAlex Bennée                       [docker_dir],
2804485b04bSFam Zheng                       quiet=quiet)
2814485b04bSFam Zheng
2826e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
2836e733da6SAlex Bennée        "Update a tagged image using "
2846e733da6SAlex Bennée
2850b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
2866e733da6SAlex Bennée
2874485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
2884485b04bSFam Zheng        try:
2894485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
2904485b04bSFam Zheng        except Exception:
2914485b04bSFam Zheng            return False
292c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
2934485b04bSFam Zheng
2944485b04bSFam Zheng    def run(self, cmd, keep, quiet):
2954485b04bSFam Zheng        label = uuid.uuid1().hex
2964485b04bSFam Zheng        if not keep:
2974485b04bSFam Zheng            self._instances.append(label)
2980b95ff72SFam Zheng        ret = self._do_check(["run", "--label",
2994485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3004485b04bSFam Zheng                             quiet=quiet)
3014485b04bSFam Zheng        if not keep:
3024485b04bSFam Zheng            self._instances.remove(label)
3034485b04bSFam Zheng        return ret
3044485b04bSFam Zheng
3054b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3064b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3074b08af60SFam Zheng
308*432d8ad5SAlex Bennée
3094485b04bSFam Zhengclass SubCommand(object):
3104485b04bSFam Zheng    """A SubCommand template base class"""
3114485b04bSFam Zheng    name = None  # Subcommand name
312*432d8ad5SAlex Bennée
3134485b04bSFam Zheng    def shared_args(self, parser):
3144485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
315e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3164485b04bSFam Zheng
3174485b04bSFam Zheng    def args(self, parser):
3184485b04bSFam Zheng        """Setup argument parser"""
3194485b04bSFam Zheng        pass
320*432d8ad5SAlex Bennée
3214485b04bSFam Zheng    def run(self, args, argv):
3224485b04bSFam Zheng        """Run command.
3234485b04bSFam Zheng        args: parsed argument by argument parser.
3244485b04bSFam Zheng        argv: remaining arguments from sys.argv.
3254485b04bSFam Zheng        """
3264485b04bSFam Zheng        pass
3274485b04bSFam Zheng
328*432d8ad5SAlex Bennée
3294485b04bSFam Zhengclass RunCommand(SubCommand):
3304485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
3314485b04bSFam Zheng    name = "run"
332*432d8ad5SAlex Bennée
3334485b04bSFam Zheng    def args(self, parser):
3344485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
3354485b04bSFam Zheng                            help="Don't remove image when command completes")
336*432d8ad5SAlex Bennée
3374485b04bSFam Zheng    def run(self, args, argv):
3384485b04bSFam Zheng        return Docker().run(argv, args.keep, quiet=args.quiet)
3394485b04bSFam Zheng
340*432d8ad5SAlex Bennée
3414485b04bSFam Zhengclass BuildCommand(SubCommand):
342*432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
3434485b04bSFam Zheng    name = "build"
344*432d8ad5SAlex Bennée
3454485b04bSFam Zheng    def args(self, parser):
346504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
347504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
348504ca3c2SAlex Bennée                            container together with all its dependent
349504ca3c2SAlex Bennée                            libraries""")
3504c84f662SPhilippe Mathieu-Daudé        parser.add_argument("--extra-files", "-f", nargs='*',
3514c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
3524c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
3534c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
354414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
355414a8ce5SAlex Bennée                            action="store_true",
356414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
3574485b04bSFam Zheng        parser.add_argument("tag",
3584485b04bSFam Zheng                            help="Image Tag")
3594485b04bSFam Zheng        parser.add_argument("dockerfile",
3604485b04bSFam Zheng                            help="Dockerfile name")
3614485b04bSFam Zheng
3624485b04bSFam Zheng    def run(self, args, argv):
3634485b04bSFam Zheng        dockerfile = open(args.dockerfile, "rb").read()
3644485b04bSFam Zheng        tag = args.tag
3654485b04bSFam Zheng
3664485b04bSFam Zheng        dkr = Docker()
3676fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
3686fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
3694485b04bSFam Zheng            if not args.quiet:
370f03868bdSEduardo Habkost                print("Image is up to date.")
371a9f8d038SAlex Bennée        else:
372a9f8d038SAlex Bennée            # Create a docker context directory for the build
373a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
3744485b04bSFam Zheng
37515352decSAlex Bennée            # Validate binfmt_misc will work
37615352decSAlex Bennée            if args.include_executable:
377d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
378d10404b1SAlex Bennée                if not enabled:
37915352decSAlex Bennée                    return 1
38015352decSAlex Bennée
381920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
382920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
383920776eaSAlex Bennée            if os.path.exists(docker_pre):
384f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
385920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
386f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
387920776eaSAlex Bennée                if rc == 3:
388f03868bdSEduardo Habkost                    print("Skip")
389920776eaSAlex Bennée                    return 0
390920776eaSAlex Bennée                elif rc != 0:
391f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
392920776eaSAlex Bennée                    return 1
393920776eaSAlex Bennée
3944c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
3954c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
396438d1168SPhilippe Mathieu-Daudé            cksum = []
397504ca3c2SAlex Bennée            if args.include_executable:
398438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
399438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
400438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
401d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
402d10404b1SAlex Bennée                                       qpath, docker_dir)
403d10404b1SAlex Bennée
4044c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
4054c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
406f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
407504ca3c2SAlex Bennée
40806cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
40906cc3551SPhilippe Mathieu-Daudé                     for k, v in os.environ.iteritems()
41006cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
411a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
412438d1168SPhilippe Mathieu-Daudé                            quiet=args.quiet, user=args.user, argv=argv,
413438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
414a9f8d038SAlex Bennée
415a9f8d038SAlex Bennée            rmtree(docker_dir)
416a9f8d038SAlex Bennée
4174485b04bSFam Zheng        return 0
4184485b04bSFam Zheng
419*432d8ad5SAlex Bennée
4206e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
421*432d8ad5SAlex Bennée    """ Update a docker image with new executables. Args: <tag> <executable>"""
4226e733da6SAlex Bennée    name = "update"
423*432d8ad5SAlex Bennée
4246e733da6SAlex Bennée    def args(self, parser):
4256e733da6SAlex Bennée        parser.add_argument("tag",
4266e733da6SAlex Bennée                            help="Image Tag")
4276e733da6SAlex Bennée        parser.add_argument("executable",
4286e733da6SAlex Bennée                            help="Executable to copy")
4296e733da6SAlex Bennée
4306e733da6SAlex Bennée    def run(self, args, argv):
4316e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
4326e733da6SAlex Bennée        # dockerfile for the update
4336e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
4346e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
4356e733da6SAlex Bennée
4367e81d198SAlex Bennée        # Add the executable to the tarball, using the current
437d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
438d10404b1SAlex Bennée        # only need the support libraries copied
439d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
4407e81d198SAlex Bennée
441d10404b1SAlex Bennée        if not enabled:
442d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
443d10404b1SAlex Bennée            return 1
444d10404b1SAlex Bennée
445d10404b1SAlex Bennée        if ff:
4466e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
4476e733da6SAlex Bennée
4486e733da6SAlex Bennée        # Add any associated libraries
4496e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
4506e733da6SAlex Bennée        if libs:
4516e733da6SAlex Bennée            for l in libs:
4526e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
4536e733da6SAlex Bennée
4546e733da6SAlex Bennée        # Create a Docker buildfile
4556e733da6SAlex Bennée        df = StringIO()
4566e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
4576e733da6SAlex Bennée        df.write("ADD . /\n")
4586e733da6SAlex Bennée        df.seek(0)
4596e733da6SAlex Bennée
4606e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
4616e733da6SAlex Bennée        df_tar.size = len(df.buf)
4626e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
4636e733da6SAlex Bennée
4646e733da6SAlex Bennée        tmp_tar.close()
4656e733da6SAlex Bennée
4666e733da6SAlex Bennée        # reset the file pointers
4676e733da6SAlex Bennée        tmp.flush()
4686e733da6SAlex Bennée        tmp.seek(0)
4696e733da6SAlex Bennée
4706e733da6SAlex Bennée        # Run the build with our tarball context
4716e733da6SAlex Bennée        dkr = Docker()
4726e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
4736e733da6SAlex Bennée
4746e733da6SAlex Bennée        return 0
4756e733da6SAlex Bennée
476*432d8ad5SAlex Bennée
4774485b04bSFam Zhengclass CleanCommand(SubCommand):
4784485b04bSFam Zheng    """Clean up docker instances"""
4794485b04bSFam Zheng    name = "clean"
480*432d8ad5SAlex Bennée
4814485b04bSFam Zheng    def run(self, args, argv):
4824485b04bSFam Zheng        Docker().clean()
4834485b04bSFam Zheng        return 0
4844485b04bSFam Zheng
485*432d8ad5SAlex Bennée
4864b08af60SFam Zhengclass ImagesCommand(SubCommand):
4874b08af60SFam Zheng    """Run "docker images" command"""
4884b08af60SFam Zheng    name = "images"
489*432d8ad5SAlex Bennée
4904b08af60SFam Zheng    def run(self, args, argv):
4914b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
4924b08af60SFam Zheng
49315df9d37SAlex Bennée
49415df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
49515df9d37SAlex Bennée    """Probe if we can run docker automatically"""
49615df9d37SAlex Bennée    name = "probe"
49715df9d37SAlex Bennée
49815df9d37SAlex Bennée    def run(self, args, argv):
49915df9d37SAlex Bennée        try:
50015df9d37SAlex Bennée            docker = Docker()
50115df9d37SAlex Bennée            if docker._command[0] == "docker":
502f03868bdSEduardo Habkost                print("yes")
50315df9d37SAlex Bennée            elif docker._command[0] == "sudo":
504f03868bdSEduardo Habkost                print("sudo")
50515df9d37SAlex Bennée        except Exception:
506f03868bdSEduardo Habkost            print("no")
50715df9d37SAlex Bennée
50815df9d37SAlex Bennée        return
50915df9d37SAlex Bennée
51015df9d37SAlex Bennée
5115e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
5125e03c2d8SAlex Bennée    """Compile sources with cc in images"""
5135e03c2d8SAlex Bennée    name = "cc"
5145e03c2d8SAlex Bennée
5155e03c2d8SAlex Bennée    def args(self, parser):
5165e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
5175e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
51899cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
51999cfdb86SAlex Bennée                            help="The compiler executable to call")
52050b72738SAlex Bennée        parser.add_argument("--user",
52150b72738SAlex Bennée                            help="The user-id to run under")
5225e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
5235e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
5245e03c2d8SAlex Bennée                            reading sources""")
5255e03c2d8SAlex Bennée
5265e03c2d8SAlex Bennée    def run(self, args, argv):
5275e03c2d8SAlex Bennée        if argv and argv[0] == "--":
5285e03c2d8SAlex Bennée            argv = argv[1:]
5295e03c2d8SAlex Bennée        cwd = os.getcwd()
5305e03c2d8SAlex Bennée        cmd = ["--rm", "-w", cwd,
5315e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
5325e03c2d8SAlex Bennée        if args.paths:
5335e03c2d8SAlex Bennée            for p in args.paths:
5345e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
53550b72738SAlex Bennée        if args.user:
53650b72738SAlex Bennée            cmd += ["-u", args.user]
53799cfdb86SAlex Bennée        cmd += [args.image, args.cc]
5385e03c2d8SAlex Bennée        cmd += argv
5395e03c2d8SAlex Bennée        return Docker().command("run", cmd, args.quiet)
5405e03c2d8SAlex Bennée
5415e03c2d8SAlex Bennée
542f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
543f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
544f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
545f97da1f7SAlex Bennée    name = "check"
546f97da1f7SAlex Bennée
547f97da1f7SAlex Bennée    def args(self, parser):
548f97da1f7SAlex Bennée        parser.add_argument("tag",
549f97da1f7SAlex Bennée                            help="Image Tag")
5507b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
5517b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
5527b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
5537b882245SAlex Bennée                            default="checksum", help="check type")
5547b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
5557b882245SAlex Bennée                            help="number of minutes")
556f97da1f7SAlex Bennée
557f97da1f7SAlex Bennée    def run(self, args, argv):
558f97da1f7SAlex Bennée        tag = args.tag
559f97da1f7SAlex Bennée
56043e1b2ffSAlex Bennée        try:
561f97da1f7SAlex Bennée            dkr = Docker()
562*432d8ad5SAlex Bennée        except subprocess.CalledProcessError:
56343e1b2ffSAlex Bennée            print("Docker not set up")
56443e1b2ffSAlex Bennée            return 1
56543e1b2ffSAlex Bennée
566f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
567f97da1f7SAlex Bennée        if info is None:
568f97da1f7SAlex Bennée            print("Image does not exist")
569f97da1f7SAlex Bennée            return 1
570f97da1f7SAlex Bennée
5717b882245SAlex Bennée        if args.checktype == "checksum":
5727b882245SAlex Bennée            if not args.dockerfile:
5737b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
5747b882245SAlex Bennée                return 1
5757b882245SAlex Bennée
5767b882245SAlex Bennée            dockerfile = open(args.dockerfile, "rb").read()
5777b882245SAlex Bennée
578f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
579f97da1f7SAlex Bennée                if not args.quiet:
580f97da1f7SAlex Bennée                    print("Image is up to date")
581f97da1f7SAlex Bennée                return 0
582f97da1f7SAlex Bennée            else:
583f97da1f7SAlex Bennée                print("Image needs updating")
584f97da1f7SAlex Bennée                return 1
5857b882245SAlex Bennée        elif args.checktype == "age":
5867b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
5877b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
5887b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
5897b882245SAlex Bennée            if created < past:
5907b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
5917b882245SAlex Bennée                       (timestr, args.olderthan))
5927b882245SAlex Bennée                return 1
5937b882245SAlex Bennée            else:
5947b882245SAlex Bennée                if not args.quiet:
5957b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
5967b882245SAlex Bennée                return 0
597f97da1f7SAlex Bennée
598f97da1f7SAlex Bennée
5994485b04bSFam Zhengdef main():
6004485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
601*432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
602*432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
6034485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
6044485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
6054485b04bSFam Zheng        cmd = cls()
6064485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
6074485b04bSFam Zheng        cmd.shared_args(subp)
6084485b04bSFam Zheng        cmd.args(subp)
6094485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
6104485b04bSFam Zheng    args, argv = parser.parse_known_args()
6114485b04bSFam Zheng    return args.cmdobj.run(args, argv)
6124485b04bSFam Zheng
613*432d8ad5SAlex Bennée
6144485b04bSFam Zhengif __name__ == "__main__":
6154485b04bSFam Zheng    sys.exit(main())
616