xref: /qemu/tests/docker/docker.py (revision 52ea63de)
1#!/usr/bin/env python2
2#
3# Docker controlling module
4#
5# Copyright (c) 2016 Red Hat Inc.
6#
7# Authors:
8#  Fam Zheng <famz@redhat.com>
9#
10# This work is licensed under the terms of the GNU GPL, version 2
11# or (at your option) any later version. See the COPYING file in
12# the top-level directory.
13
14import os
15import sys
16import subprocess
17import json
18import hashlib
19import atexit
20import uuid
21import argparse
22import tempfile
23from shutil import copy
24
25def _text_checksum(text):
26    """Calculate a digest string unique to the text content"""
27    return hashlib.sha1(text).hexdigest()
28
29def _guess_docker_command():
30    """ Guess a working docker command or raise exception if not found"""
31    commands = [["docker"], ["sudo", "-n", "docker"]]
32    for cmd in commands:
33        if subprocess.call(cmd + ["images"],
34                           stdout=subprocess.PIPE,
35                           stderr=subprocess.PIPE) == 0:
36            return cmd
37    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
38    raise Exception("Cannot find working docker command. Tried:\n%s" % \
39                    commands_txt)
40
41class Docker(object):
42    """ Running Docker commands """
43    def __init__(self):
44        self._command = _guess_docker_command()
45        self._instances = []
46        atexit.register(self._kill_instances)
47
48    def _do(self, cmd, quiet=True, **kwargs):
49        if quiet:
50            kwargs["stdout"] = subprocess.PIPE
51        return subprocess.call(self._command + cmd, **kwargs)
52
53    def _do_kill_instances(self, only_known, only_active=True):
54        cmd = ["ps", "-q"]
55        if not only_active:
56            cmd.append("-a")
57        for i in self._output(cmd).split():
58            resp = self._output(["inspect", i])
59            labels = json.loads(resp)[0]["Config"]["Labels"]
60            active = json.loads(resp)[0]["State"]["Running"]
61            if not labels:
62                continue
63            instance_uuid = labels.get("com.qemu.instance.uuid", None)
64            if not instance_uuid:
65                continue
66            if only_known and instance_uuid not in self._instances:
67                continue
68            print "Terminating", i
69            if active:
70                self._do(["kill", i])
71            self._do(["rm", i])
72
73    def clean(self):
74        self._do_kill_instances(False, False)
75        return 0
76
77    def _kill_instances(self):
78        return self._do_kill_instances(True)
79
80    def _output(self, cmd, **kwargs):
81        return subprocess.check_output(self._command + cmd,
82                                       stderr=subprocess.STDOUT,
83                                       **kwargs)
84
85    def get_image_dockerfile_checksum(self, tag):
86        resp = self._output(["inspect", tag])
87        labels = json.loads(resp)[0]["Config"].get("Labels", {})
88        return labels.get("com.qemu.dockerfile-checksum", "")
89
90    def build_image(self, tag, dockerfile, df_path, quiet=True, argv=None):
91        if argv == None:
92            argv = []
93        tmp_dir = tempfile.mkdtemp(prefix="docker_build")
94
95        tmp_df = tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".docker")
96        tmp_df.write(dockerfile)
97
98        tmp_df.write("\n")
99        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
100                     _text_checksum(dockerfile))
101        tmp_df.flush()
102        self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
103                 [tmp_dir],
104                 quiet=quiet)
105
106    def image_matches_dockerfile(self, tag, dockerfile):
107        try:
108            checksum = self.get_image_dockerfile_checksum(tag)
109        except Exception:
110            return False
111        return checksum == _text_checksum(dockerfile)
112
113    def run(self, cmd, keep, quiet):
114        label = uuid.uuid1().hex
115        if not keep:
116            self._instances.append(label)
117        ret = self._do(["run", "--label",
118                        "com.qemu.instance.uuid=" + label] + cmd,
119                       quiet=quiet)
120        if not keep:
121            self._instances.remove(label)
122        return ret
123
124class SubCommand(object):
125    """A SubCommand template base class"""
126    name = None # Subcommand name
127    def shared_args(self, parser):
128        parser.add_argument("--quiet", action="store_true",
129                            help="Run quietly unless an error occured")
130
131    def args(self, parser):
132        """Setup argument parser"""
133        pass
134    def run(self, args, argv):
135        """Run command.
136        args: parsed argument by argument parser.
137        argv: remaining arguments from sys.argv.
138        """
139        pass
140
141class RunCommand(SubCommand):
142    """Invoke docker run and take care of cleaning up"""
143    name = "run"
144    def args(self, parser):
145        parser.add_argument("--keep", action="store_true",
146                            help="Don't remove image when command completes")
147    def run(self, args, argv):
148        return Docker().run(argv, args.keep, quiet=args.quiet)
149
150class BuildCommand(SubCommand):
151    """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
152    name = "build"
153    def args(self, parser):
154        parser.add_argument("tag",
155                            help="Image Tag")
156        parser.add_argument("dockerfile",
157                            help="Dockerfile name")
158
159    def run(self, args, argv):
160        dockerfile = open(args.dockerfile, "rb").read()
161        tag = args.tag
162
163        dkr = Docker()
164        if dkr.image_matches_dockerfile(tag, dockerfile):
165            if not args.quiet:
166                print "Image is up to date."
167            return 0
168
169        dkr.build_image(tag, dockerfile, args.dockerfile,
170                        quiet=args.quiet, argv=argv)
171        return 0
172
173class CleanCommand(SubCommand):
174    """Clean up docker instances"""
175    name = "clean"
176    def run(self, args, argv):
177        Docker().clean()
178        return 0
179
180def main():
181    parser = argparse.ArgumentParser(description="A Docker helper",
182            usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
183    subparsers = parser.add_subparsers(title="subcommands", help=None)
184    for cls in SubCommand.__subclasses__():
185        cmd = cls()
186        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
187        cmd.shared_args(subp)
188        cmd.args(subp)
189        subp.set_defaults(cmdobj=cmd)
190    args, argv = parser.parse_known_args()
191    return args.cmdobj.run(args, argv)
192
193if __name__ == "__main__":
194    sys.exit(main())
195