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