xref: /qemu/tests/docker/docker.py (revision d10404b19393808394a129dd77974810ea87235d)
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
46438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
47438d1168SPhilippe Mathieu-Daudé    return _text_checksum(open(filename, 'rb').read())
48438d1168SPhilippe Mathieu-Daudé
494485b04bSFam Zhengdef _guess_docker_command():
504485b04bSFam Zheng    """ Guess a working docker command or raise exception if not found"""
514485b04bSFam Zheng    commands = [["docker"], ["sudo", "-n", "docker"]]
524485b04bSFam Zheng    for cmd in commands:
530679f98bSEduardo Habkost        try:
5483405c45SAlex Bennée            # docker version will return the client details in stdout
5583405c45SAlex Bennée            # but still report a status of 1 if it can't contact the daemon
5683405c45SAlex Bennée            if subprocess.call(cmd + ["version"],
57c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
584485b04bSFam Zheng                return cmd
590679f98bSEduardo Habkost        except OSError:
600679f98bSEduardo Habkost            pass
614485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
624485b04bSFam Zheng    raise Exception("Cannot find working docker command. Tried:\n%s" % \
634485b04bSFam Zheng                    commands_txt)
644485b04bSFam Zheng
652499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'):
66504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
67504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
68504ca3c2SAlex Bennée    try:
69504ca3c2SAlex Bennée        os.makedirs(dest_dir)
70504ca3c2SAlex Bennée    except OSError:
71504ca3c2SAlex Bennée        # we can safely ignore already created directories
72504ca3c2SAlex Bennée        pass
73504ca3c2SAlex Bennée
74504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
75504ca3c2SAlex Bennée    copy(src, dest_file)
76504ca3c2SAlex Bennée
77504ca3c2SAlex Bennée
78504ca3c2SAlex Bennéedef _get_so_libs(executable):
79504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
80504ca3c2SAlex Bennée
81504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
82504ca3c2SAlex Bennée    ensure theright data is copied."""
83504ca3c2SAlex Bennée
84504ca3c2SAlex Bennée    libs = []
85504ca3c2SAlex Bennée    ldd_re = re.compile(r"(/.*/)(\S*)")
86504ca3c2SAlex Bennée    try:
87504ca3c2SAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable])
88504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
89504ca3c2SAlex Bennée            search = ldd_re.search(line)
90504ca3c2SAlex Bennée            if search and len(search.groups()) == 2:
91504ca3c2SAlex Bennée                so_path = search.groups()[0]
92504ca3c2SAlex Bennée                so_lib = search.groups()[1]
93504ca3c2SAlex Bennée                libs.append("%s/%s" % (so_path, so_lib))
94504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
95f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
96504ca3c2SAlex Bennée
97504ca3c2SAlex Bennée    return libs
98504ca3c2SAlex Bennée
99*d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
100*d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
101*d10404b1SAlex Bennée
102*d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
103*d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
104*d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
105*d10404b1SAlex Bennée    is in the image.
106504ca3c2SAlex Bennée
107504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
108*d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
109*d10404b1SAlex Bennée    """
110504ca3c2SAlex Bennée
111*d10404b1SAlex Bennée    if bin_dest:
112*d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
113*d10404b1SAlex Bennée    else:
114*d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
115504ca3c2SAlex Bennée
116504ca3c2SAlex Bennée    libs = _get_so_libs(src)
117504ca3c2SAlex Bennée    if libs:
118504ca3c2SAlex Bennée        for l in libs:
119504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
120504ca3c2SAlex Bennée            _copy_with_mkdir(l , dest_dir, so_path)
121504ca3c2SAlex Bennée
12215352decSAlex Bennée
12315352decSAlex Bennéedef _check_binfmt_misc(executable):
12415352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
12515352decSAlex Bennée
12615352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
12715352decSAlex Bennée    this script but we should at least fail early with a useful
128*d10404b1SAlex Bennée    message if it won't work.
129*d10404b1SAlex Bennée
130*d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
131*d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
132*d10404b1SAlex Bennée    libraries.
133*d10404b1SAlex Bennée    """
13415352decSAlex Bennée
13515352decSAlex Bennée    binary = os.path.basename(executable)
13615352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
13715352decSAlex Bennée
13815352decSAlex Bennée    if not os.path.exists(binfmt_entry):
13915352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
140*d10404b1SAlex Bennée        return None, False
14115352decSAlex Bennée
14215352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
14315352decSAlex Bennée
14443c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
14543c898b7SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary\n" %
14643c898b7SAlex Bennée              (binary))
147*d10404b1SAlex Bennée        return None, True
14843c898b7SAlex Bennée
1497e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1507e81d198SAlex Bennée    interp = m.group(1)
1517e81d198SAlex Bennée    if interp and interp != executable:
1527e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1537e81d198SAlex Bennée              (binary, executable, interp))
15415352decSAlex Bennée
155*d10404b1SAlex Bennée    return interp, True
156*d10404b1SAlex Bennée
15715352decSAlex Bennée
158c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
159547cb45eSAlex Bennée    # special case for Debian linux-user images
160547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
161547cb45eSAlex Bennée        img_name = "debian-bootstrap"
162547cb45eSAlex Bennée
163c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
164c1958e9dSFam Zheng                      img_name + ".docker")
165c1958e9dSFam Zheng    return open(df, "r").read()
166c1958e9dSFam Zheng
167c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
168c1958e9dSFam Zheng    out = ""
169c1958e9dSFam Zheng    for l in df.splitlines():
170c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
171c1958e9dSFam Zheng            continue
172c1958e9dSFam Zheng        from_pref = "FROM qemu:"
173c1958e9dSFam Zheng        if l.startswith(from_pref):
174c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
175c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
176c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
177c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
178c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
179c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
180c1958e9dSFam Zheng            continue
181c1958e9dSFam Zheng        out += l + "\n"
182c1958e9dSFam Zheng    return out
183c1958e9dSFam Zheng
1844485b04bSFam Zhengclass Docker(object):
1854485b04bSFam Zheng    """ Running Docker commands """
1864485b04bSFam Zheng    def __init__(self):
1874485b04bSFam Zheng        self._command = _guess_docker_command()
1884485b04bSFam Zheng        self._instances = []
1894485b04bSFam Zheng        atexit.register(self._kill_instances)
19097cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
19197cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
1924485b04bSFam Zheng
19358bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
1944485b04bSFam Zheng        if quiet:
195c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
1964485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
1974485b04bSFam Zheng
1980b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
1990b95ff72SFam Zheng        if quiet:
2000b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2010b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2020b95ff72SFam Zheng
2034485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2044485b04bSFam Zheng        cmd = ["ps", "-q"]
2054485b04bSFam Zheng        if not only_active:
2064485b04bSFam Zheng            cmd.append("-a")
2074485b04bSFam Zheng        for i in self._output(cmd).split():
2084485b04bSFam Zheng            resp = self._output(["inspect", i])
2094485b04bSFam Zheng            labels = json.loads(resp)[0]["Config"]["Labels"]
2104485b04bSFam Zheng            active = json.loads(resp)[0]["State"]["Running"]
2114485b04bSFam Zheng            if not labels:
2124485b04bSFam Zheng                continue
2134485b04bSFam Zheng            instance_uuid = labels.get("com.qemu.instance.uuid", None)
2144485b04bSFam Zheng            if not instance_uuid:
2154485b04bSFam Zheng                continue
2164485b04bSFam Zheng            if only_known and instance_uuid not in self._instances:
2174485b04bSFam Zheng                continue
218f03868bdSEduardo Habkost            print("Terminating", i)
2194485b04bSFam Zheng            if active:
2204485b04bSFam Zheng                self._do(["kill", i])
2214485b04bSFam Zheng            self._do(["rm", i])
2224485b04bSFam Zheng
2234485b04bSFam Zheng    def clean(self):
2244485b04bSFam Zheng        self._do_kill_instances(False, False)
2254485b04bSFam Zheng        return 0
2264485b04bSFam Zheng
22797cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2284485b04bSFam Zheng        return self._do_kill_instances(True)
2294485b04bSFam Zheng
2304485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2314485b04bSFam Zheng        return subprocess.check_output(self._command + cmd,
2324485b04bSFam Zheng                                       stderr=subprocess.STDOUT,
2334485b04bSFam Zheng                                       **kwargs)
2344485b04bSFam Zheng
235f97da1f7SAlex Bennée    def inspect_tag(self, tag):
236f97da1f7SAlex Bennée        try:
237f97da1f7SAlex Bennée            return self._output(["inspect", tag])
238f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
239f97da1f7SAlex Bennée            return None
240f97da1f7SAlex Bennée
2417b882245SAlex Bennée    def get_image_creation_time(self, info):
2427b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2437b882245SAlex Bennée
2444485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
245f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2464485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2474485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
2484485b04bSFam Zheng
249414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
250438d1168SPhilippe Mathieu-Daudé                    quiet=True, user=False, argv=None, extra_files_cksum=[]):
2514485b04bSFam Zheng        if argv == None:
2524485b04bSFam Zheng            argv = []
2534485b04bSFam Zheng
254a9f8d038SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
2554485b04bSFam Zheng        tmp_df.write(dockerfile)
2564485b04bSFam Zheng
257414a8ce5SAlex Bennée        if user:
258414a8ce5SAlex Bennée            uid = os.getuid()
259414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
260414a8ce5SAlex Bennée            tmp_df.write("\n")
261414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
262414a8ce5SAlex Bennée                         (uname, uid, uname))
263414a8ce5SAlex Bennée
2644485b04bSFam Zheng        tmp_df.write("\n")
2654485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
266f9172822SAlex Bennée                     _text_checksum(_dockerfile_preprocess(dockerfile)))
267f9172822SAlex Bennée        for f, c in extra_files_cksum:
268f9172822SAlex Bennée            tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
269f9172822SAlex Bennée
2704485b04bSFam Zheng        tmp_df.flush()
271a9f8d038SAlex Bennée
2720b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv + \
273a9f8d038SAlex Bennée                       [docker_dir],
2744485b04bSFam Zheng                       quiet=quiet)
2754485b04bSFam Zheng
2766e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
2776e733da6SAlex Bennée        "Update a tagged image using "
2786e733da6SAlex Bennée
2790b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
2806e733da6SAlex Bennée
2814485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
2824485b04bSFam Zheng        try:
2834485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
2844485b04bSFam Zheng        except Exception:
2854485b04bSFam Zheng            return False
286c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
2874485b04bSFam Zheng
2884485b04bSFam Zheng    def run(self, cmd, keep, quiet):
2894485b04bSFam Zheng        label = uuid.uuid1().hex
2904485b04bSFam Zheng        if not keep:
2914485b04bSFam Zheng            self._instances.append(label)
2920b95ff72SFam Zheng        ret = self._do_check(["run", "--label",
2934485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
2944485b04bSFam Zheng                             quiet=quiet)
2954485b04bSFam Zheng        if not keep:
2964485b04bSFam Zheng            self._instances.remove(label)
2974485b04bSFam Zheng        return ret
2984485b04bSFam Zheng
2994b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3004b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3014b08af60SFam Zheng
3024485b04bSFam Zhengclass SubCommand(object):
3034485b04bSFam Zheng    """A SubCommand template base class"""
3044485b04bSFam Zheng    name = None # Subcommand name
3054485b04bSFam Zheng    def shared_args(self, parser):
3064485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
307e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3084485b04bSFam Zheng
3094485b04bSFam Zheng    def args(self, parser):
3104485b04bSFam Zheng        """Setup argument parser"""
3114485b04bSFam Zheng        pass
3124485b04bSFam Zheng    def run(self, args, argv):
3134485b04bSFam Zheng        """Run command.
3144485b04bSFam Zheng        args: parsed argument by argument parser.
3154485b04bSFam Zheng        argv: remaining arguments from sys.argv.
3164485b04bSFam Zheng        """
3174485b04bSFam Zheng        pass
3184485b04bSFam Zheng
3194485b04bSFam Zhengclass RunCommand(SubCommand):
3204485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
3214485b04bSFam Zheng    name = "run"
3224485b04bSFam Zheng    def args(self, parser):
3234485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
3244485b04bSFam Zheng                            help="Don't remove image when command completes")
3254485b04bSFam Zheng    def run(self, args, argv):
3264485b04bSFam Zheng        return Docker().run(argv, args.keep, quiet=args.quiet)
3274485b04bSFam Zheng
3284485b04bSFam Zhengclass BuildCommand(SubCommand):
3294485b04bSFam Zheng    """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
3304485b04bSFam Zheng    name = "build"
3314485b04bSFam Zheng    def args(self, parser):
332504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
333504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
334504ca3c2SAlex Bennée                            container together with all its dependent
335504ca3c2SAlex Bennée                            libraries""")
3364c84f662SPhilippe Mathieu-Daudé        parser.add_argument("--extra-files", "-f", nargs='*',
3374c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
3384c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
3394c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
340414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
341414a8ce5SAlex Bennée                            action="store_true",
342414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
3434485b04bSFam Zheng        parser.add_argument("tag",
3444485b04bSFam Zheng                            help="Image Tag")
3454485b04bSFam Zheng        parser.add_argument("dockerfile",
3464485b04bSFam Zheng                            help="Dockerfile name")
3474485b04bSFam Zheng
3484485b04bSFam Zheng    def run(self, args, argv):
3494485b04bSFam Zheng        dockerfile = open(args.dockerfile, "rb").read()
3504485b04bSFam Zheng        tag = args.tag
3514485b04bSFam Zheng
3524485b04bSFam Zheng        dkr = Docker()
3536fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
3546fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
3554485b04bSFam Zheng            if not args.quiet:
356f03868bdSEduardo Habkost                print("Image is up to date.")
357a9f8d038SAlex Bennée        else:
358a9f8d038SAlex Bennée            # Create a docker context directory for the build
359a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
3604485b04bSFam Zheng
36115352decSAlex Bennée            # Validate binfmt_misc will work
36215352decSAlex Bennée            if args.include_executable:
363*d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
364*d10404b1SAlex Bennée                if not enabled:
36515352decSAlex Bennée                    return 1
36615352decSAlex Bennée
367920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
368920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
369920776eaSAlex Bennée            if os.path.exists(docker_pre):
370f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
371920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
372f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
373920776eaSAlex Bennée                if rc == 3:
374f03868bdSEduardo Habkost                    print("Skip")
375920776eaSAlex Bennée                    return 0
376920776eaSAlex Bennée                elif rc != 0:
377f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
378920776eaSAlex Bennée                    return 1
379920776eaSAlex Bennée
3804c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
3814c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
382438d1168SPhilippe Mathieu-Daudé            cksum = []
383504ca3c2SAlex Bennée            if args.include_executable:
384438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
385438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
386438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
387*d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
388*d10404b1SAlex Bennée                                       qpath, docker_dir)
389*d10404b1SAlex Bennée
3904c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
3914c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
392f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
393504ca3c2SAlex Bennée
39406cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
39506cc3551SPhilippe Mathieu-Daudé                        for k, v in os.environ.iteritems()
39606cc3551SPhilippe Mathieu-Daudé                        if k.lower() in FILTERED_ENV_NAMES]
397a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
398438d1168SPhilippe Mathieu-Daudé                            quiet=args.quiet, user=args.user, argv=argv,
399438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
400a9f8d038SAlex Bennée
401a9f8d038SAlex Bennée            rmtree(docker_dir)
402a9f8d038SAlex Bennée
4034485b04bSFam Zheng        return 0
4044485b04bSFam Zheng
4056e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
4066e733da6SAlex Bennée    """ Update a docker image with new executables. Arguments: <tag> <executable>"""
4076e733da6SAlex Bennée    name = "update"
4086e733da6SAlex Bennée    def args(self, parser):
4096e733da6SAlex Bennée        parser.add_argument("tag",
4106e733da6SAlex Bennée                            help="Image Tag")
4116e733da6SAlex Bennée        parser.add_argument("executable",
4126e733da6SAlex Bennée                            help="Executable to copy")
4136e733da6SAlex Bennée
4146e733da6SAlex Bennée    def run(self, args, argv):
4156e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
4166e733da6SAlex Bennée        # dockerfile for the update
4176e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
4186e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
4196e733da6SAlex Bennée
4207e81d198SAlex Bennée        # Add the executable to the tarball, using the current
421*d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
422*d10404b1SAlex Bennée        # only need the support libraries copied
423*d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
4247e81d198SAlex Bennée
425*d10404b1SAlex Bennée        if not enabled:
426*d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
427*d10404b1SAlex Bennée            return 1
428*d10404b1SAlex Bennée
429*d10404b1SAlex Bennée        if ff:
4306e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
4316e733da6SAlex Bennée
4326e733da6SAlex Bennée        # Add any associated libraries
4336e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
4346e733da6SAlex Bennée        if libs:
4356e733da6SAlex Bennée            for l in libs:
4366e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
4376e733da6SAlex Bennée
4386e733da6SAlex Bennée        # Create a Docker buildfile
4396e733da6SAlex Bennée        df = StringIO()
4406e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
4416e733da6SAlex Bennée        df.write("ADD . /\n")
4426e733da6SAlex Bennée        df.seek(0)
4436e733da6SAlex Bennée
4446e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
4456e733da6SAlex Bennée        df_tar.size = len(df.buf)
4466e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
4476e733da6SAlex Bennée
4486e733da6SAlex Bennée        tmp_tar.close()
4496e733da6SAlex Bennée
4506e733da6SAlex Bennée        # reset the file pointers
4516e733da6SAlex Bennée        tmp.flush()
4526e733da6SAlex Bennée        tmp.seek(0)
4536e733da6SAlex Bennée
4546e733da6SAlex Bennée        # Run the build with our tarball context
4556e733da6SAlex Bennée        dkr = Docker()
4566e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
4576e733da6SAlex Bennée
4586e733da6SAlex Bennée        return 0
4596e733da6SAlex Bennée
4604485b04bSFam Zhengclass CleanCommand(SubCommand):
4614485b04bSFam Zheng    """Clean up docker instances"""
4624485b04bSFam Zheng    name = "clean"
4634485b04bSFam Zheng    def run(self, args, argv):
4644485b04bSFam Zheng        Docker().clean()
4654485b04bSFam Zheng        return 0
4664485b04bSFam Zheng
4674b08af60SFam Zhengclass ImagesCommand(SubCommand):
4684b08af60SFam Zheng    """Run "docker images" command"""
4694b08af60SFam Zheng    name = "images"
4704b08af60SFam Zheng    def run(self, args, argv):
4714b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
4724b08af60SFam Zheng
47315df9d37SAlex Bennée
47415df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
47515df9d37SAlex Bennée    """Probe if we can run docker automatically"""
47615df9d37SAlex Bennée    name = "probe"
47715df9d37SAlex Bennée
47815df9d37SAlex Bennée    def run(self, args, argv):
47915df9d37SAlex Bennée        try:
48015df9d37SAlex Bennée            docker = Docker()
48115df9d37SAlex Bennée            if docker._command[0] == "docker":
482f03868bdSEduardo Habkost                print("yes")
48315df9d37SAlex Bennée            elif docker._command[0] == "sudo":
484f03868bdSEduardo Habkost                print("sudo")
48515df9d37SAlex Bennée        except Exception:
486f03868bdSEduardo Habkost            print("no")
48715df9d37SAlex Bennée
48815df9d37SAlex Bennée        return
48915df9d37SAlex Bennée
49015df9d37SAlex Bennée
4915e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
4925e03c2d8SAlex Bennée    """Compile sources with cc in images"""
4935e03c2d8SAlex Bennée    name = "cc"
4945e03c2d8SAlex Bennée
4955e03c2d8SAlex Bennée    def args(self, parser):
4965e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
4975e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
49899cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
49999cfdb86SAlex Bennée                            help="The compiler executable to call")
50050b72738SAlex Bennée        parser.add_argument("--user",
50150b72738SAlex Bennée                            help="The user-id to run under")
5025e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
5035e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
5045e03c2d8SAlex Bennée                            reading sources""")
5055e03c2d8SAlex Bennée
5065e03c2d8SAlex Bennée    def run(self, args, argv):
5075e03c2d8SAlex Bennée        if argv and argv[0] == "--":
5085e03c2d8SAlex Bennée            argv = argv[1:]
5095e03c2d8SAlex Bennée        cwd = os.getcwd()
5105e03c2d8SAlex Bennée        cmd = ["--rm", "-w", cwd,
5115e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
5125e03c2d8SAlex Bennée        if args.paths:
5135e03c2d8SAlex Bennée            for p in args.paths:
5145e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
51550b72738SAlex Bennée        if args.user:
51650b72738SAlex Bennée            cmd += ["-u", args.user]
51799cfdb86SAlex Bennée        cmd += [args.image, args.cc]
5185e03c2d8SAlex Bennée        cmd += argv
5195e03c2d8SAlex Bennée        return Docker().command("run", cmd, args.quiet)
5205e03c2d8SAlex Bennée
5215e03c2d8SAlex Bennée
522f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
523f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
524f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
525f97da1f7SAlex Bennée    name = "check"
526f97da1f7SAlex Bennée
527f97da1f7SAlex Bennée    def args(self, parser):
528f97da1f7SAlex Bennée        parser.add_argument("tag",
529f97da1f7SAlex Bennée                            help="Image Tag")
5307b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
5317b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
5327b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
5337b882245SAlex Bennée                            default="checksum", help="check type")
5347b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
5357b882245SAlex Bennée                            help="number of minutes")
536f97da1f7SAlex Bennée
537f97da1f7SAlex Bennée    def run(self, args, argv):
538f97da1f7SAlex Bennée        tag = args.tag
539f97da1f7SAlex Bennée
54043e1b2ffSAlex Bennée        try:
541f97da1f7SAlex Bennée            dkr = Docker()
54243e1b2ffSAlex Bennée        except:
54343e1b2ffSAlex Bennée            print("Docker not set up")
54443e1b2ffSAlex Bennée            return 1
54543e1b2ffSAlex Bennée
546f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
547f97da1f7SAlex Bennée        if info is None:
548f97da1f7SAlex Bennée            print("Image does not exist")
549f97da1f7SAlex Bennée            return 1
550f97da1f7SAlex Bennée
5517b882245SAlex Bennée        if args.checktype == "checksum":
5527b882245SAlex Bennée            if not args.dockerfile:
5537b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
5547b882245SAlex Bennée                return 1
5557b882245SAlex Bennée
5567b882245SAlex Bennée            dockerfile = open(args.dockerfile, "rb").read()
5577b882245SAlex Bennée
558f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
559f97da1f7SAlex Bennée                if not args.quiet:
560f97da1f7SAlex Bennée                    print("Image is up to date")
561f97da1f7SAlex Bennée                return 0
562f97da1f7SAlex Bennée            else:
563f97da1f7SAlex Bennée                print("Image needs updating")
564f97da1f7SAlex Bennée                return 1
5657b882245SAlex Bennée        elif args.checktype == "age":
5667b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
5677b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
5687b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
5697b882245SAlex Bennée            if created < past:
5707b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
5717b882245SAlex Bennée                       (timestr, args.olderthan))
5727b882245SAlex Bennée                return 1
5737b882245SAlex Bennée            else:
5747b882245SAlex Bennée                if not args.quiet:
5757b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
5767b882245SAlex Bennée                return 0
577f97da1f7SAlex Bennée
578f97da1f7SAlex Bennée
5794485b04bSFam Zhengdef main():
5804485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
5814485b04bSFam Zheng            usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
5824485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
5834485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
5844485b04bSFam Zheng        cmd = cls()
5854485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
5864485b04bSFam Zheng        cmd.shared_args(subp)
5874485b04bSFam Zheng        cmd.args(subp)
5884485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
5894485b04bSFam Zheng    args, argv = parser.parse_known_args()
5904485b04bSFam Zheng    return args.cmdobj.run(args, argv)
5914485b04bSFam Zheng
5924485b04bSFam Zhengif __name__ == "__main__":
5934485b04bSFam Zheng    sys.exit(main())
594