]> git.proxmox.com Git - mirror_qemu.git/blame - tests/docker/docker.py
tests/docker/docker.py: support --include-executable
[mirror_qemu.git] / tests / docker / docker.py
CommitLineData
4485b04b
FZ
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
504ca3c2 23import re
a9f8d038 24from shutil import copy, rmtree
4485b04b
FZ
25
26def _text_checksum(text):
27 """Calculate a digest string unique to the text content"""
28 return hashlib.sha1(text).hexdigest()
29
30def _guess_docker_command():
31 """ Guess a working docker command or raise exception if not found"""
32 commands = [["docker"], ["sudo", "-n", "docker"]]
33 for cmd in commands:
34 if subprocess.call(cmd + ["images"],
35 stdout=subprocess.PIPE,
36 stderr=subprocess.PIPE) == 0:
37 return cmd
38 commands_txt = "\n".join([" " + " ".join(x) for x in commands])
39 raise Exception("Cannot find working docker command. Tried:\n%s" % \
40 commands_txt)
41
504ca3c2
AB
42def _copy_with_mkdir(src, root_dir, sub_path):
43 """Copy src into root_dir, creating sub_path as needed."""
44 dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
45 try:
46 os.makedirs(dest_dir)
47 except OSError:
48 # we can safely ignore already created directories
49 pass
50
51 dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
52 copy(src, dest_file)
53
54
55def _get_so_libs(executable):
56 """Return a list of libraries associated with an executable.
57
58 The paths may be symbolic links which would need to be resolved to
59 ensure theright data is copied."""
60
61 libs = []
62 ldd_re = re.compile(r"(/.*/)(\S*)")
63 try:
64 ldd_output = subprocess.check_output(["ldd", executable])
65 for line in ldd_output.split("\n"):
66 search = ldd_re.search(line)
67 if search and len(search.groups()) == 2:
68 so_path = search.groups()[0]
69 so_lib = search.groups()[1]
70 libs.append("%s/%s" % (so_path, so_lib))
71 except subprocess.CalledProcessError:
72 print "%s had no associated libraries (static build?)" % (executable)
73
74 return libs
75
76def _copy_binary_with_libs(src, dest_dir):
77 """Copy a binary executable and all its dependant libraries.
78
79 This does rely on the host file-system being fairly multi-arch
80 aware so the file don't clash with the guests layout."""
81
82 _copy_with_mkdir(src, dest_dir, "/usr/bin")
83
84 libs = _get_so_libs(src)
85 if libs:
86 for l in libs:
87 so_path = os.path.dirname(l)
88 _copy_with_mkdir(l , dest_dir, so_path)
89
4485b04b
FZ
90class Docker(object):
91 """ Running Docker commands """
92 def __init__(self):
93 self._command = _guess_docker_command()
94 self._instances = []
95 atexit.register(self._kill_instances)
96
97 def _do(self, cmd, quiet=True, **kwargs):
98 if quiet:
99 kwargs["stdout"] = subprocess.PIPE
100 return subprocess.call(self._command + cmd, **kwargs)
101
102 def _do_kill_instances(self, only_known, only_active=True):
103 cmd = ["ps", "-q"]
104 if not only_active:
105 cmd.append("-a")
106 for i in self._output(cmd).split():
107 resp = self._output(["inspect", i])
108 labels = json.loads(resp)[0]["Config"]["Labels"]
109 active = json.loads(resp)[0]["State"]["Running"]
110 if not labels:
111 continue
112 instance_uuid = labels.get("com.qemu.instance.uuid", None)
113 if not instance_uuid:
114 continue
115 if only_known and instance_uuid not in self._instances:
116 continue
117 print "Terminating", i
118 if active:
119 self._do(["kill", i])
120 self._do(["rm", i])
121
122 def clean(self):
123 self._do_kill_instances(False, False)
124 return 0
125
126 def _kill_instances(self):
127 return self._do_kill_instances(True)
128
129 def _output(self, cmd, **kwargs):
130 return subprocess.check_output(self._command + cmd,
131 stderr=subprocess.STDOUT,
132 **kwargs)
133
134 def get_image_dockerfile_checksum(self, tag):
135 resp = self._output(["inspect", tag])
136 labels = json.loads(resp)[0]["Config"].get("Labels", {})
137 return labels.get("com.qemu.dockerfile-checksum", "")
138
a9f8d038 139 def build_image(self, tag, docker_dir, dockerfile, quiet=True, argv=None):
4485b04b
FZ
140 if argv == None:
141 argv = []
4485b04b 142
a9f8d038 143 tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
4485b04b
FZ
144 tmp_df.write(dockerfile)
145
146 tmp_df.write("\n")
147 tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
148 _text_checksum(dockerfile))
149 tmp_df.flush()
a9f8d038 150
4485b04b 151 self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
a9f8d038 152 [docker_dir],
4485b04b
FZ
153 quiet=quiet)
154
155 def image_matches_dockerfile(self, tag, dockerfile):
156 try:
157 checksum = self.get_image_dockerfile_checksum(tag)
158 except Exception:
159 return False
160 return checksum == _text_checksum(dockerfile)
161
162 def run(self, cmd, keep, quiet):
163 label = uuid.uuid1().hex
164 if not keep:
165 self._instances.append(label)
166 ret = self._do(["run", "--label",
167 "com.qemu.instance.uuid=" + label] + cmd,
168 quiet=quiet)
169 if not keep:
170 self._instances.remove(label)
171 return ret
172
173class SubCommand(object):
174 """A SubCommand template base class"""
175 name = None # Subcommand name
176 def shared_args(self, parser):
177 parser.add_argument("--quiet", action="store_true",
178 help="Run quietly unless an error occured")
179
180 def args(self, parser):
181 """Setup argument parser"""
182 pass
183 def run(self, args, argv):
184 """Run command.
185 args: parsed argument by argument parser.
186 argv: remaining arguments from sys.argv.
187 """
188 pass
189
190class RunCommand(SubCommand):
191 """Invoke docker run and take care of cleaning up"""
192 name = "run"
193 def args(self, parser):
194 parser.add_argument("--keep", action="store_true",
195 help="Don't remove image when command completes")
196 def run(self, args, argv):
197 return Docker().run(argv, args.keep, quiet=args.quiet)
198
199class BuildCommand(SubCommand):
200 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
201 name = "build"
202 def args(self, parser):
504ca3c2
AB
203 parser.add_argument("--include-executable", "-e",
204 help="""Specify a binary that will be copied to the
205 container together with all its dependent
206 libraries""")
4485b04b
FZ
207 parser.add_argument("tag",
208 help="Image Tag")
209 parser.add_argument("dockerfile",
210 help="Dockerfile name")
211
212 def run(self, args, argv):
213 dockerfile = open(args.dockerfile, "rb").read()
214 tag = args.tag
215
216 dkr = Docker()
217 if dkr.image_matches_dockerfile(tag, dockerfile):
218 if not args.quiet:
219 print "Image is up to date."
a9f8d038
AB
220 else:
221 # Create a docker context directory for the build
222 docker_dir = tempfile.mkdtemp(prefix="docker_build")
223
504ca3c2
AB
224 # Do we include a extra binary?
225 if args.include_executable:
226 _copy_binary_with_libs(args.include_executable,
227 docker_dir)
228
a9f8d038
AB
229 dkr.build_image(tag, docker_dir, dockerfile,
230 quiet=args.quiet, argv=argv)
231
232 rmtree(docker_dir)
4485b04b 233
4485b04b
FZ
234 return 0
235
236class CleanCommand(SubCommand):
237 """Clean up docker instances"""
238 name = "clean"
239 def run(self, args, argv):
240 Docker().clean()
241 return 0
242
243def main():
244 parser = argparse.ArgumentParser(description="A Docker helper",
245 usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
246 subparsers = parser.add_subparsers(title="subcommands", help=None)
247 for cls in SubCommand.__subclasses__():
248 cmd = cls()
249 subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
250 cmd.shared_args(subp)
251 cmd.args(subp)
252 subp.set_defaults(cmdobj=cmd)
253 args, argv = parser.parse_known_args()
254 return args.cmdobj.run(args, argv)
255
256if __name__ == "__main__":
257 sys.exit(main())