xref: /qemu/tests/docker/docker.py (revision 920776ea5ea3d9f243d266581da5345e5d7b2306)
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
24a9f8d038SAlex Bennéefrom shutil import copy, rmtree
254485b04bSFam Zheng
264485b04bSFam Zhengdef _text_checksum(text):
274485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
284485b04bSFam Zheng    return hashlib.sha1(text).hexdigest()
294485b04bSFam Zheng
304485b04bSFam Zhengdef _guess_docker_command():
314485b04bSFam Zheng    """ Guess a working docker command or raise exception if not found"""
324485b04bSFam Zheng    commands = [["docker"], ["sudo", "-n", "docker"]]
334485b04bSFam Zheng    for cmd in commands:
344485b04bSFam Zheng        if subprocess.call(cmd + ["images"],
354485b04bSFam Zheng                           stdout=subprocess.PIPE,
364485b04bSFam Zheng                           stderr=subprocess.PIPE) == 0:
374485b04bSFam Zheng            return cmd
384485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
394485b04bSFam Zheng    raise Exception("Cannot find working docker command. Tried:\n%s" % \
404485b04bSFam Zheng                    commands_txt)
414485b04bSFam Zheng
42504ca3c2SAlex Bennéedef _copy_with_mkdir(src, root_dir, sub_path):
43504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
44504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
45504ca3c2SAlex Bennée    try:
46504ca3c2SAlex Bennée        os.makedirs(dest_dir)
47504ca3c2SAlex Bennée    except OSError:
48504ca3c2SAlex Bennée        # we can safely ignore already created directories
49504ca3c2SAlex Bennée        pass
50504ca3c2SAlex Bennée
51504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
52504ca3c2SAlex Bennée    copy(src, dest_file)
53504ca3c2SAlex Bennée
54504ca3c2SAlex Bennée
55504ca3c2SAlex Bennéedef _get_so_libs(executable):
56504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
57504ca3c2SAlex Bennée
58504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
59504ca3c2SAlex Bennée    ensure theright data is copied."""
60504ca3c2SAlex Bennée
61504ca3c2SAlex Bennée    libs = []
62504ca3c2SAlex Bennée    ldd_re = re.compile(r"(/.*/)(\S*)")
63504ca3c2SAlex Bennée    try:
64504ca3c2SAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable])
65504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
66504ca3c2SAlex Bennée            search = ldd_re.search(line)
67504ca3c2SAlex Bennée            if search and len(search.groups()) == 2:
68504ca3c2SAlex Bennée                so_path = search.groups()[0]
69504ca3c2SAlex Bennée                so_lib = search.groups()[1]
70504ca3c2SAlex Bennée                libs.append("%s/%s" % (so_path, so_lib))
71504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
72504ca3c2SAlex Bennée        print "%s had no associated libraries (static build?)" % (executable)
73504ca3c2SAlex Bennée
74504ca3c2SAlex Bennée    return libs
75504ca3c2SAlex Bennée
76504ca3c2SAlex Bennéedef _copy_binary_with_libs(src, dest_dir):
77504ca3c2SAlex Bennée    """Copy a binary executable and all its dependant libraries.
78504ca3c2SAlex Bennée
79504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
80504ca3c2SAlex Bennée    aware so the file don't clash with the guests layout."""
81504ca3c2SAlex Bennée
82504ca3c2SAlex Bennée    _copy_with_mkdir(src, dest_dir, "/usr/bin")
83504ca3c2SAlex Bennée
84504ca3c2SAlex Bennée    libs = _get_so_libs(src)
85504ca3c2SAlex Bennée    if libs:
86504ca3c2SAlex Bennée        for l in libs:
87504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
88504ca3c2SAlex Bennée            _copy_with_mkdir(l , dest_dir, so_path)
89504ca3c2SAlex Bennée
904485b04bSFam Zhengclass Docker(object):
914485b04bSFam Zheng    """ Running Docker commands """
924485b04bSFam Zheng    def __init__(self):
934485b04bSFam Zheng        self._command = _guess_docker_command()
944485b04bSFam Zheng        self._instances = []
954485b04bSFam Zheng        atexit.register(self._kill_instances)
964485b04bSFam Zheng
974485b04bSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
984485b04bSFam Zheng        if quiet:
994485b04bSFam Zheng            kwargs["stdout"] = subprocess.PIPE
1004485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
1014485b04bSFam Zheng
1024485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
1034485b04bSFam Zheng        cmd = ["ps", "-q"]
1044485b04bSFam Zheng        if not only_active:
1054485b04bSFam Zheng            cmd.append("-a")
1064485b04bSFam Zheng        for i in self._output(cmd).split():
1074485b04bSFam Zheng            resp = self._output(["inspect", i])
1084485b04bSFam Zheng            labels = json.loads(resp)[0]["Config"]["Labels"]
1094485b04bSFam Zheng            active = json.loads(resp)[0]["State"]["Running"]
1104485b04bSFam Zheng            if not labels:
1114485b04bSFam Zheng                continue
1124485b04bSFam Zheng            instance_uuid = labels.get("com.qemu.instance.uuid", None)
1134485b04bSFam Zheng            if not instance_uuid:
1144485b04bSFam Zheng                continue
1154485b04bSFam Zheng            if only_known and instance_uuid not in self._instances:
1164485b04bSFam Zheng                continue
1174485b04bSFam Zheng            print "Terminating", i
1184485b04bSFam Zheng            if active:
1194485b04bSFam Zheng                self._do(["kill", i])
1204485b04bSFam Zheng            self._do(["rm", i])
1214485b04bSFam Zheng
1224485b04bSFam Zheng    def clean(self):
1234485b04bSFam Zheng        self._do_kill_instances(False, False)
1244485b04bSFam Zheng        return 0
1254485b04bSFam Zheng
1264485b04bSFam Zheng    def _kill_instances(self):
1274485b04bSFam Zheng        return self._do_kill_instances(True)
1284485b04bSFam Zheng
1294485b04bSFam Zheng    def _output(self, cmd, **kwargs):
1304485b04bSFam Zheng        return subprocess.check_output(self._command + cmd,
1314485b04bSFam Zheng                                       stderr=subprocess.STDOUT,
1324485b04bSFam Zheng                                       **kwargs)
1334485b04bSFam Zheng
1344485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
1354485b04bSFam Zheng        resp = self._output(["inspect", tag])
1364485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
1374485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
1384485b04bSFam Zheng
139a9f8d038SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile, quiet=True, argv=None):
1404485b04bSFam Zheng        if argv == None:
1414485b04bSFam Zheng            argv = []
1424485b04bSFam Zheng
143a9f8d038SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
1444485b04bSFam Zheng        tmp_df.write(dockerfile)
1454485b04bSFam Zheng
1464485b04bSFam Zheng        tmp_df.write("\n")
1474485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
1484485b04bSFam Zheng                     _text_checksum(dockerfile))
1494485b04bSFam Zheng        tmp_df.flush()
150a9f8d038SAlex Bennée
1514485b04bSFam Zheng        self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
152a9f8d038SAlex Bennée                 [docker_dir],
1534485b04bSFam Zheng                 quiet=quiet)
1544485b04bSFam Zheng
1554485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
1564485b04bSFam Zheng        try:
1574485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
1584485b04bSFam Zheng        except Exception:
1594485b04bSFam Zheng            return False
1604485b04bSFam Zheng        return checksum == _text_checksum(dockerfile)
1614485b04bSFam Zheng
1624485b04bSFam Zheng    def run(self, cmd, keep, quiet):
1634485b04bSFam Zheng        label = uuid.uuid1().hex
1644485b04bSFam Zheng        if not keep:
1654485b04bSFam Zheng            self._instances.append(label)
1664485b04bSFam Zheng        ret = self._do(["run", "--label",
1674485b04bSFam Zheng                        "com.qemu.instance.uuid=" + label] + cmd,
1684485b04bSFam Zheng                       quiet=quiet)
1694485b04bSFam Zheng        if not keep:
1704485b04bSFam Zheng            self._instances.remove(label)
1714485b04bSFam Zheng        return ret
1724485b04bSFam Zheng
1734485b04bSFam Zhengclass SubCommand(object):
1744485b04bSFam Zheng    """A SubCommand template base class"""
1754485b04bSFam Zheng    name = None # Subcommand name
1764485b04bSFam Zheng    def shared_args(self, parser):
1774485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
1784485b04bSFam Zheng                            help="Run quietly unless an error occured")
1794485b04bSFam Zheng
1804485b04bSFam Zheng    def args(self, parser):
1814485b04bSFam Zheng        """Setup argument parser"""
1824485b04bSFam Zheng        pass
1834485b04bSFam Zheng    def run(self, args, argv):
1844485b04bSFam Zheng        """Run command.
1854485b04bSFam Zheng        args: parsed argument by argument parser.
1864485b04bSFam Zheng        argv: remaining arguments from sys.argv.
1874485b04bSFam Zheng        """
1884485b04bSFam Zheng        pass
1894485b04bSFam Zheng
1904485b04bSFam Zhengclass RunCommand(SubCommand):
1914485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
1924485b04bSFam Zheng    name = "run"
1934485b04bSFam Zheng    def args(self, parser):
1944485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
1954485b04bSFam Zheng                            help="Don't remove image when command completes")
1964485b04bSFam Zheng    def run(self, args, argv):
1974485b04bSFam Zheng        return Docker().run(argv, args.keep, quiet=args.quiet)
1984485b04bSFam Zheng
1994485b04bSFam Zhengclass BuildCommand(SubCommand):
2004485b04bSFam Zheng    """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
2014485b04bSFam Zheng    name = "build"
2024485b04bSFam Zheng    def args(self, parser):
203504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
204504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
205504ca3c2SAlex Bennée                            container together with all its dependent
206504ca3c2SAlex Bennée                            libraries""")
2074485b04bSFam Zheng        parser.add_argument("tag",
2084485b04bSFam Zheng                            help="Image Tag")
2094485b04bSFam Zheng        parser.add_argument("dockerfile",
2104485b04bSFam Zheng                            help="Dockerfile name")
2114485b04bSFam Zheng
2124485b04bSFam Zheng    def run(self, args, argv):
2134485b04bSFam Zheng        dockerfile = open(args.dockerfile, "rb").read()
2144485b04bSFam Zheng        tag = args.tag
2154485b04bSFam Zheng
2164485b04bSFam Zheng        dkr = Docker()
2174485b04bSFam Zheng        if dkr.image_matches_dockerfile(tag, dockerfile):
2184485b04bSFam Zheng            if not args.quiet:
2194485b04bSFam Zheng                print "Image is up to date."
220a9f8d038SAlex Bennée        else:
221a9f8d038SAlex Bennée            # Create a docker context directory for the build
222a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
2234485b04bSFam Zheng
224*920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
225*920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
226*920776eaSAlex Bennée            if os.path.exists(docker_pre):
227*920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
228*920776eaSAlex Bennée                                     cwd=docker_dir)
229*920776eaSAlex Bennée                if rc == 3:
230*920776eaSAlex Bennée                    print "Skip"
231*920776eaSAlex Bennée                    return 0
232*920776eaSAlex Bennée                elif rc != 0:
233*920776eaSAlex Bennée                    print "%s exited with code %d" % (docker_pre, rc)
234*920776eaSAlex Bennée                    return 1
235*920776eaSAlex Bennée
236504ca3c2SAlex Bennée            # Do we include a extra binary?
237504ca3c2SAlex Bennée            if args.include_executable:
238504ca3c2SAlex Bennée                _copy_binary_with_libs(args.include_executable,
239504ca3c2SAlex Bennée                                       docker_dir)
240504ca3c2SAlex Bennée
241a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
2424485b04bSFam Zheng                            quiet=args.quiet, argv=argv)
243a9f8d038SAlex Bennée
244a9f8d038SAlex Bennée            rmtree(docker_dir)
245a9f8d038SAlex Bennée
2464485b04bSFam Zheng        return 0
2474485b04bSFam Zheng
2484485b04bSFam Zhengclass CleanCommand(SubCommand):
2494485b04bSFam Zheng    """Clean up docker instances"""
2504485b04bSFam Zheng    name = "clean"
2514485b04bSFam Zheng    def run(self, args, argv):
2524485b04bSFam Zheng        Docker().clean()
2534485b04bSFam Zheng        return 0
2544485b04bSFam Zheng
2554485b04bSFam Zhengdef main():
2564485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
2574485b04bSFam Zheng            usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
2584485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
2594485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
2604485b04bSFam Zheng        cmd = cls()
2614485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
2624485b04bSFam Zheng        cmd.shared_args(subp)
2634485b04bSFam Zheng        cmd.args(subp)
2644485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
2654485b04bSFam Zheng    args, argv = parser.parse_known_args()
2664485b04bSFam Zheng    return args.cmdobj.run(args, argv)
2674485b04bSFam Zheng
2684485b04bSFam Zhengif __name__ == "__main__":
2694485b04bSFam Zheng    sys.exit(main())
270