xref: /qemu/tests/docker/docker.py (revision 97cba1a1d1e62ea3e2718ad22188d10f2aa5478d)
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
144485b04bSFam Zhengimport os
154485b04bSFam Zhengimport sys
164485b04bSFam Zhengimport subprocess
174485b04bSFam Zhengimport json
184485b04bSFam Zhengimport hashlib
194485b04bSFam Zhengimport atexit
204485b04bSFam Zhengimport uuid
214485b04bSFam Zhengimport argparse
224485b04bSFam Zhengimport tempfile
23504ca3c2SAlex Bennéeimport re
24*97cba1a1SFam Zhengimport signal
256e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo
266e733da6SAlex Bennéefrom StringIO import StringIO
27a9f8d038SAlex Bennéefrom shutil import copy, rmtree
284485b04bSFam Zheng
29c9772570SSascha Silbe
30c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb')
31c9772570SSascha Silbe
32c9772570SSascha Silbe
334485b04bSFam Zhengdef _text_checksum(text):
344485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
354485b04bSFam Zheng    return hashlib.sha1(text).hexdigest()
364485b04bSFam Zheng
374485b04bSFam Zhengdef _guess_docker_command():
384485b04bSFam Zheng    """ Guess a working docker command or raise exception if not found"""
394485b04bSFam Zheng    commands = [["docker"], ["sudo", "-n", "docker"]]
404485b04bSFam Zheng    for cmd in commands:
410679f98bSEduardo Habkost        try:
424485b04bSFam Zheng            if subprocess.call(cmd + ["images"],
43c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
444485b04bSFam Zheng                return cmd
450679f98bSEduardo Habkost        except OSError:
460679f98bSEduardo Habkost            pass
474485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
484485b04bSFam Zheng    raise Exception("Cannot find working docker command. Tried:\n%s" % \
494485b04bSFam Zheng                    commands_txt)
504485b04bSFam Zheng
51504ca3c2SAlex Bennéedef _copy_with_mkdir(src, root_dir, sub_path):
52504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
53504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
54504ca3c2SAlex Bennée    try:
55504ca3c2SAlex Bennée        os.makedirs(dest_dir)
56504ca3c2SAlex Bennée    except OSError:
57504ca3c2SAlex Bennée        # we can safely ignore already created directories
58504ca3c2SAlex Bennée        pass
59504ca3c2SAlex Bennée
60504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
61504ca3c2SAlex Bennée    copy(src, dest_file)
62504ca3c2SAlex Bennée
63504ca3c2SAlex Bennée
64504ca3c2SAlex Bennéedef _get_so_libs(executable):
65504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
66504ca3c2SAlex Bennée
67504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
68504ca3c2SAlex Bennée    ensure theright data is copied."""
69504ca3c2SAlex Bennée
70504ca3c2SAlex Bennée    libs = []
71504ca3c2SAlex Bennée    ldd_re = re.compile(r"(/.*/)(\S*)")
72504ca3c2SAlex Bennée    try:
73504ca3c2SAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable])
74504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
75504ca3c2SAlex Bennée            search = ldd_re.search(line)
76504ca3c2SAlex Bennée            if search and len(search.groups()) == 2:
77504ca3c2SAlex Bennée                so_path = search.groups()[0]
78504ca3c2SAlex Bennée                so_lib = search.groups()[1]
79504ca3c2SAlex Bennée                libs.append("%s/%s" % (so_path, so_lib))
80504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
81504ca3c2SAlex Bennée        print "%s had no associated libraries (static build?)" % (executable)
82504ca3c2SAlex Bennée
83504ca3c2SAlex Bennée    return libs
84504ca3c2SAlex Bennée
85504ca3c2SAlex Bennéedef _copy_binary_with_libs(src, dest_dir):
86504ca3c2SAlex Bennée    """Copy a binary executable and all its dependant libraries.
87504ca3c2SAlex Bennée
88504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
89504ca3c2SAlex Bennée    aware so the file don't clash with the guests layout."""
90504ca3c2SAlex Bennée
91504ca3c2SAlex Bennée    _copy_with_mkdir(src, dest_dir, "/usr/bin")
92504ca3c2SAlex Bennée
93504ca3c2SAlex Bennée    libs = _get_so_libs(src)
94504ca3c2SAlex Bennée    if libs:
95504ca3c2SAlex Bennée        for l in libs:
96504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
97504ca3c2SAlex Bennée            _copy_with_mkdir(l , dest_dir, so_path)
98504ca3c2SAlex Bennée
994485b04bSFam Zhengclass Docker(object):
1004485b04bSFam Zheng    """ Running Docker commands """
1014485b04bSFam Zheng    def __init__(self):
1024485b04bSFam Zheng        self._command = _guess_docker_command()
1034485b04bSFam Zheng        self._instances = []
1044485b04bSFam Zheng        atexit.register(self._kill_instances)
105*97cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
106*97cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
1074485b04bSFam Zheng
1086e733da6SAlex Bennée    def _do(self, cmd, quiet=True, infile=None, **kwargs):
1094485b04bSFam Zheng        if quiet:
110c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
1116e733da6SAlex Bennée        if infile:
1126e733da6SAlex Bennée            kwargs["stdin"] = infile
1134485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
1144485b04bSFam Zheng
1154485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
1164485b04bSFam Zheng        cmd = ["ps", "-q"]
1174485b04bSFam Zheng        if not only_active:
1184485b04bSFam Zheng            cmd.append("-a")
1194485b04bSFam Zheng        for i in self._output(cmd).split():
1204485b04bSFam Zheng            resp = self._output(["inspect", i])
1214485b04bSFam Zheng            labels = json.loads(resp)[0]["Config"]["Labels"]
1224485b04bSFam Zheng            active = json.loads(resp)[0]["State"]["Running"]
1234485b04bSFam Zheng            if not labels:
1244485b04bSFam Zheng                continue
1254485b04bSFam Zheng            instance_uuid = labels.get("com.qemu.instance.uuid", None)
1264485b04bSFam Zheng            if not instance_uuid:
1274485b04bSFam Zheng                continue
1284485b04bSFam Zheng            if only_known and instance_uuid not in self._instances:
1294485b04bSFam Zheng                continue
1304485b04bSFam Zheng            print "Terminating", i
1314485b04bSFam Zheng            if active:
1324485b04bSFam Zheng                self._do(["kill", i])
1334485b04bSFam Zheng            self._do(["rm", i])
1344485b04bSFam Zheng
1354485b04bSFam Zheng    def clean(self):
1364485b04bSFam Zheng        self._do_kill_instances(False, False)
1374485b04bSFam Zheng        return 0
1384485b04bSFam Zheng
139*97cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
1404485b04bSFam Zheng        return self._do_kill_instances(True)
1414485b04bSFam Zheng
1424485b04bSFam Zheng    def _output(self, cmd, **kwargs):
1434485b04bSFam Zheng        return subprocess.check_output(self._command + cmd,
1444485b04bSFam Zheng                                       stderr=subprocess.STDOUT,
1454485b04bSFam Zheng                                       **kwargs)
1464485b04bSFam Zheng
1474485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
1484485b04bSFam Zheng        resp = self._output(["inspect", tag])
1494485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
1504485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
1514485b04bSFam Zheng
152a9f8d038SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile, quiet=True, argv=None):
1534485b04bSFam Zheng        if argv == None:
1544485b04bSFam Zheng            argv = []
1554485b04bSFam Zheng
156a9f8d038SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
1574485b04bSFam Zheng        tmp_df.write(dockerfile)
1584485b04bSFam Zheng
1594485b04bSFam Zheng        tmp_df.write("\n")
1604485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
1614485b04bSFam Zheng                     _text_checksum(dockerfile))
1624485b04bSFam Zheng        tmp_df.flush()
163a9f8d038SAlex Bennée
1644485b04bSFam Zheng        self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
165a9f8d038SAlex Bennée                 [docker_dir],
1664485b04bSFam Zheng                 quiet=quiet)
1674485b04bSFam Zheng
1686e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
1696e733da6SAlex Bennée        "Update a tagged image using "
1706e733da6SAlex Bennée
1716e733da6SAlex Bennée        self._do(["build", "-t", tag, "-"], quiet=quiet, infile=tarball)
1726e733da6SAlex Bennée
1734485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
1744485b04bSFam Zheng        try:
1754485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
1764485b04bSFam Zheng        except Exception:
1774485b04bSFam Zheng            return False
1784485b04bSFam Zheng        return checksum == _text_checksum(dockerfile)
1794485b04bSFam Zheng
1804485b04bSFam Zheng    def run(self, cmd, keep, quiet):
1814485b04bSFam Zheng        label = uuid.uuid1().hex
1824485b04bSFam Zheng        if not keep:
1834485b04bSFam Zheng            self._instances.append(label)
1844485b04bSFam Zheng        ret = self._do(["run", "--label",
1854485b04bSFam Zheng                        "com.qemu.instance.uuid=" + label] + cmd,
1864485b04bSFam Zheng                       quiet=quiet)
1874485b04bSFam Zheng        if not keep:
1884485b04bSFam Zheng            self._instances.remove(label)
1894485b04bSFam Zheng        return ret
1904485b04bSFam Zheng
1914b08af60SFam Zheng    def command(self, cmd, argv, quiet):
1924b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
1934b08af60SFam Zheng
1944485b04bSFam Zhengclass SubCommand(object):
1954485b04bSFam Zheng    """A SubCommand template base class"""
1964485b04bSFam Zheng    name = None # Subcommand name
1974485b04bSFam Zheng    def shared_args(self, parser):
1984485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
1994485b04bSFam Zheng                            help="Run quietly unless an error occured")
2004485b04bSFam Zheng
2014485b04bSFam Zheng    def args(self, parser):
2024485b04bSFam Zheng        """Setup argument parser"""
2034485b04bSFam Zheng        pass
2044485b04bSFam Zheng    def run(self, args, argv):
2054485b04bSFam Zheng        """Run command.
2064485b04bSFam Zheng        args: parsed argument by argument parser.
2074485b04bSFam Zheng        argv: remaining arguments from sys.argv.
2084485b04bSFam Zheng        """
2094485b04bSFam Zheng        pass
2104485b04bSFam Zheng
2114485b04bSFam Zhengclass RunCommand(SubCommand):
2124485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
2134485b04bSFam Zheng    name = "run"
2144485b04bSFam Zheng    def args(self, parser):
2154485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
2164485b04bSFam Zheng                            help="Don't remove image when command completes")
2174485b04bSFam Zheng    def run(self, args, argv):
2184485b04bSFam Zheng        return Docker().run(argv, args.keep, quiet=args.quiet)
2194485b04bSFam Zheng
2204485b04bSFam Zhengclass BuildCommand(SubCommand):
2214485b04bSFam Zheng    """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
2224485b04bSFam Zheng    name = "build"
2234485b04bSFam Zheng    def args(self, parser):
224504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
225504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
226504ca3c2SAlex Bennée                            container together with all its dependent
227504ca3c2SAlex Bennée                            libraries""")
2284485b04bSFam Zheng        parser.add_argument("tag",
2294485b04bSFam Zheng                            help="Image Tag")
2304485b04bSFam Zheng        parser.add_argument("dockerfile",
2314485b04bSFam Zheng                            help="Dockerfile name")
2324485b04bSFam Zheng
2334485b04bSFam Zheng    def run(self, args, argv):
2344485b04bSFam Zheng        dockerfile = open(args.dockerfile, "rb").read()
2354485b04bSFam Zheng        tag = args.tag
2364485b04bSFam Zheng
2374485b04bSFam Zheng        dkr = Docker()
2384485b04bSFam Zheng        if dkr.image_matches_dockerfile(tag, dockerfile):
2394485b04bSFam Zheng            if not args.quiet:
2404485b04bSFam Zheng                print "Image is up to date."
241a9f8d038SAlex Bennée        else:
242a9f8d038SAlex Bennée            # Create a docker context directory for the build
243a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
2444485b04bSFam Zheng
245920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
246920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
247920776eaSAlex Bennée            if os.path.exists(docker_pre):
248f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
249920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
250f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
251920776eaSAlex Bennée                if rc == 3:
252920776eaSAlex Bennée                    print "Skip"
253920776eaSAlex Bennée                    return 0
254920776eaSAlex Bennée                elif rc != 0:
255920776eaSAlex Bennée                    print "%s exited with code %d" % (docker_pre, rc)
256920776eaSAlex Bennée                    return 1
257920776eaSAlex Bennée
258504ca3c2SAlex Bennée            # Do we include a extra binary?
259504ca3c2SAlex Bennée            if args.include_executable:
260504ca3c2SAlex Bennée                _copy_binary_with_libs(args.include_executable,
261504ca3c2SAlex Bennée                                       docker_dir)
262504ca3c2SAlex Bennée
263a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
2644485b04bSFam Zheng                            quiet=args.quiet, argv=argv)
265a9f8d038SAlex Bennée
266a9f8d038SAlex Bennée            rmtree(docker_dir)
267a9f8d038SAlex Bennée
2684485b04bSFam Zheng        return 0
2694485b04bSFam Zheng
2706e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
2716e733da6SAlex Bennée    """ Update a docker image with new executables. Arguments: <tag> <executable>"""
2726e733da6SAlex Bennée    name = "update"
2736e733da6SAlex Bennée    def args(self, parser):
2746e733da6SAlex Bennée        parser.add_argument("tag",
2756e733da6SAlex Bennée                            help="Image Tag")
2766e733da6SAlex Bennée        parser.add_argument("executable",
2776e733da6SAlex Bennée                            help="Executable to copy")
2786e733da6SAlex Bennée
2796e733da6SAlex Bennée    def run(self, args, argv):
2806e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
2816e733da6SAlex Bennée        # dockerfile for the update
2826e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
2836e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
2846e733da6SAlex Bennée
2856e733da6SAlex Bennée        # Add the executable to the tarball
2866e733da6SAlex Bennée        bn = os.path.basename(args.executable)
2876e733da6SAlex Bennée        ff = "/usr/bin/%s" % bn
2886e733da6SAlex Bennée        tmp_tar.add(args.executable, arcname=ff)
2896e733da6SAlex Bennée
2906e733da6SAlex Bennée        # Add any associated libraries
2916e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
2926e733da6SAlex Bennée        if libs:
2936e733da6SAlex Bennée            for l in libs:
2946e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
2956e733da6SAlex Bennée
2966e733da6SAlex Bennée        # Create a Docker buildfile
2976e733da6SAlex Bennée        df = StringIO()
2986e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
2996e733da6SAlex Bennée        df.write("ADD . /\n")
3006e733da6SAlex Bennée        df.seek(0)
3016e733da6SAlex Bennée
3026e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
3036e733da6SAlex Bennée        df_tar.size = len(df.buf)
3046e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
3056e733da6SAlex Bennée
3066e733da6SAlex Bennée        tmp_tar.close()
3076e733da6SAlex Bennée
3086e733da6SAlex Bennée        # reset the file pointers
3096e733da6SAlex Bennée        tmp.flush()
3106e733da6SAlex Bennée        tmp.seek(0)
3116e733da6SAlex Bennée
3126e733da6SAlex Bennée        # Run the build with our tarball context
3136e733da6SAlex Bennée        dkr = Docker()
3146e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
3156e733da6SAlex Bennée
3166e733da6SAlex Bennée        return 0
3176e733da6SAlex Bennée
3184485b04bSFam Zhengclass CleanCommand(SubCommand):
3194485b04bSFam Zheng    """Clean up docker instances"""
3204485b04bSFam Zheng    name = "clean"
3214485b04bSFam Zheng    def run(self, args, argv):
3224485b04bSFam Zheng        Docker().clean()
3234485b04bSFam Zheng        return 0
3244485b04bSFam Zheng
3254b08af60SFam Zhengclass ImagesCommand(SubCommand):
3264b08af60SFam Zheng    """Run "docker images" command"""
3274b08af60SFam Zheng    name = "images"
3284b08af60SFam Zheng    def run(self, args, argv):
3294b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
3304b08af60SFam Zheng
3314485b04bSFam Zhengdef main():
3324485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
3334485b04bSFam Zheng            usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
3344485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
3354485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
3364485b04bSFam Zheng        cmd = cls()
3374485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
3384485b04bSFam Zheng        cmd.shared_args(subp)
3394485b04bSFam Zheng        cmd.args(subp)
3404485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
3414485b04bSFam Zheng    args, argv = parser.parse_known_args()
3424485b04bSFam Zheng    return args.cmdobj.run(args, argv)
3434485b04bSFam Zheng
3444485b04bSFam Zhengif __name__ == "__main__":
3454485b04bSFam Zheng    sys.exit(main())
346