Merge remote-tracking branch 'kraxel/tags/pull-ui-20170519-1' into staging
[qemu.git] / tests / docker / docker.py
blob8747f6a4404370239273b99bcdbbf7bdde4638aa
1 #!/usr/bin/env python2
3 # Docker controlling module
5 # Copyright (c) 2016 Red Hat Inc.
7 # Authors:
8 # Fam Zheng <famz@redhat.com>
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.
14 import os
15 import sys
16 import subprocess
17 import json
18 import hashlib
19 import atexit
20 import uuid
21 import argparse
22 import tempfile
23 import re
24 import signal
25 from tarfile import TarFile, TarInfo
26 from StringIO import StringIO
27 from shutil import copy, rmtree
28 from pwd import getpwuid
31 FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
34 DEVNULL = open(os.devnull, 'wb')
37 def _text_checksum(text):
38 """Calculate a digest string unique to the text content"""
39 return hashlib.sha1(text).hexdigest()
41 def _guess_docker_command():
42 """ Guess a working docker command or raise exception if not found"""
43 commands = [["docker"], ["sudo", "-n", "docker"]]
44 for cmd in commands:
45 try:
46 if subprocess.call(cmd + ["images"],
47 stdout=DEVNULL, stderr=DEVNULL) == 0:
48 return cmd
49 except OSError:
50 pass
51 commands_txt = "\n".join([" " + " ".join(x) for x in commands])
52 raise Exception("Cannot find working docker command. Tried:\n%s" % \
53 commands_txt)
55 def _copy_with_mkdir(src, root_dir, sub_path):
56 """Copy src into root_dir, creating sub_path as needed."""
57 dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
58 try:
59 os.makedirs(dest_dir)
60 except OSError:
61 # we can safely ignore already created directories
62 pass
64 dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
65 copy(src, dest_file)
68 def _get_so_libs(executable):
69 """Return a list of libraries associated with an executable.
71 The paths may be symbolic links which would need to be resolved to
72 ensure theright data is copied."""
74 libs = []
75 ldd_re = re.compile(r"(/.*/)(\S*)")
76 try:
77 ldd_output = subprocess.check_output(["ldd", executable])
78 for line in ldd_output.split("\n"):
79 search = ldd_re.search(line)
80 if search and len(search.groups()) == 2:
81 so_path = search.groups()[0]
82 so_lib = search.groups()[1]
83 libs.append("%s/%s" % (so_path, so_lib))
84 except subprocess.CalledProcessError:
85 print "%s had no associated libraries (static build?)" % (executable)
87 return libs
89 def _copy_binary_with_libs(src, dest_dir):
90 """Copy a binary executable and all its dependant libraries.
92 This does rely on the host file-system being fairly multi-arch
93 aware so the file don't clash with the guests layout."""
95 _copy_with_mkdir(src, dest_dir, "/usr/bin")
97 libs = _get_so_libs(src)
98 if libs:
99 for l in libs:
100 so_path = os.path.dirname(l)
101 _copy_with_mkdir(l , dest_dir, so_path)
103 class Docker(object):
104 """ Running Docker commands """
105 def __init__(self):
106 self._command = _guess_docker_command()
107 self._instances = []
108 atexit.register(self._kill_instances)
109 signal.signal(signal.SIGTERM, self._kill_instances)
110 signal.signal(signal.SIGHUP, self._kill_instances)
112 def _do(self, cmd, quiet=True, infile=None, **kwargs):
113 if quiet:
114 kwargs["stdout"] = DEVNULL
115 if infile:
116 kwargs["stdin"] = infile
117 return subprocess.call(self._command + cmd, **kwargs)
119 def _do_kill_instances(self, only_known, only_active=True):
120 cmd = ["ps", "-q"]
121 if not only_active:
122 cmd.append("-a")
123 for i in self._output(cmd).split():
124 resp = self._output(["inspect", i])
125 labels = json.loads(resp)[0]["Config"]["Labels"]
126 active = json.loads(resp)[0]["State"]["Running"]
127 if not labels:
128 continue
129 instance_uuid = labels.get("com.qemu.instance.uuid", None)
130 if not instance_uuid:
131 continue
132 if only_known and instance_uuid not in self._instances:
133 continue
134 print "Terminating", i
135 if active:
136 self._do(["kill", i])
137 self._do(["rm", i])
139 def clean(self):
140 self._do_kill_instances(False, False)
141 return 0
143 def _kill_instances(self, *args, **kwargs):
144 return self._do_kill_instances(True)
146 def _output(self, cmd, **kwargs):
147 return subprocess.check_output(self._command + cmd,
148 stderr=subprocess.STDOUT,
149 **kwargs)
151 def get_image_dockerfile_checksum(self, tag):
152 resp = self._output(["inspect", tag])
153 labels = json.loads(resp)[0]["Config"].get("Labels", {})
154 return labels.get("com.qemu.dockerfile-checksum", "")
156 def build_image(self, tag, docker_dir, dockerfile,
157 quiet=True, user=False, argv=None):
158 if argv == None:
159 argv = []
161 tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
162 tmp_df.write(dockerfile)
164 if user:
165 uid = os.getuid()
166 uname = getpwuid(uid).pw_name
167 tmp_df.write("\n")
168 tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
169 (uname, uid, uname))
171 tmp_df.write("\n")
172 tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
173 _text_checksum(dockerfile))
174 tmp_df.flush()
176 self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
177 [docker_dir],
178 quiet=quiet)
180 def update_image(self, tag, tarball, quiet=True):
181 "Update a tagged image using "
183 self._do(["build", "-t", tag, "-"], quiet=quiet, infile=tarball)
185 def image_matches_dockerfile(self, tag, dockerfile):
186 try:
187 checksum = self.get_image_dockerfile_checksum(tag)
188 except Exception:
189 return False
190 return checksum == _text_checksum(dockerfile)
192 def run(self, cmd, keep, quiet):
193 label = uuid.uuid1().hex
194 if not keep:
195 self._instances.append(label)
196 ret = self._do(["run", "--label",
197 "com.qemu.instance.uuid=" + label] + cmd,
198 quiet=quiet)
199 if not keep:
200 self._instances.remove(label)
201 return ret
203 def command(self, cmd, argv, quiet):
204 return self._do([cmd] + argv, quiet=quiet)
206 class SubCommand(object):
207 """A SubCommand template base class"""
208 name = None # Subcommand name
209 def shared_args(self, parser):
210 parser.add_argument("--quiet", action="store_true",
211 help="Run quietly unless an error occured")
213 def args(self, parser):
214 """Setup argument parser"""
215 pass
216 def run(self, args, argv):
217 """Run command.
218 args: parsed argument by argument parser.
219 argv: remaining arguments from sys.argv.
221 pass
223 class RunCommand(SubCommand):
224 """Invoke docker run and take care of cleaning up"""
225 name = "run"
226 def args(self, parser):
227 parser.add_argument("--keep", action="store_true",
228 help="Don't remove image when command completes")
229 def run(self, args, argv):
230 return Docker().run(argv, args.keep, quiet=args.quiet)
232 class BuildCommand(SubCommand):
233 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
234 name = "build"
235 def args(self, parser):
236 parser.add_argument("--include-executable", "-e",
237 help="""Specify a binary that will be copied to the
238 container together with all its dependent
239 libraries""")
240 parser.add_argument("--add-current-user", "-u", dest="user",
241 action="store_true",
242 help="Add the current user to image's passwd")
243 parser.add_argument("tag",
244 help="Image Tag")
245 parser.add_argument("dockerfile",
246 help="Dockerfile name")
248 def run(self, args, argv):
249 dockerfile = open(args.dockerfile, "rb").read()
250 tag = args.tag
252 dkr = Docker()
253 if dkr.image_matches_dockerfile(tag, dockerfile):
254 if not args.quiet:
255 print "Image is up to date."
256 else:
257 # Create a docker context directory for the build
258 docker_dir = tempfile.mkdtemp(prefix="docker_build")
260 # Is there a .pre file to run in the build context?
261 docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
262 if os.path.exists(docker_pre):
263 stdout = DEVNULL if args.quiet else None
264 rc = subprocess.call(os.path.realpath(docker_pre),
265 cwd=docker_dir, stdout=stdout)
266 if rc == 3:
267 print "Skip"
268 return 0
269 elif rc != 0:
270 print "%s exited with code %d" % (docker_pre, rc)
271 return 1
273 # Do we include a extra binary?
274 if args.include_executable:
275 _copy_binary_with_libs(args.include_executable,
276 docker_dir)
278 argv += ["--build-arg=" + k.lower() + "=" + v
279 for k, v in os.environ.iteritems()
280 if k.lower() in FILTERED_ENV_NAMES]
281 dkr.build_image(tag, docker_dir, dockerfile,
282 quiet=args.quiet, user=args.user, argv=argv)
284 rmtree(docker_dir)
286 return 0
288 class UpdateCommand(SubCommand):
289 """ Update a docker image with new executables. Arguments: <tag> <executable>"""
290 name = "update"
291 def args(self, parser):
292 parser.add_argument("tag",
293 help="Image Tag")
294 parser.add_argument("executable",
295 help="Executable to copy")
297 def run(self, args, argv):
298 # Create a temporary tarball with our whole build context and
299 # dockerfile for the update
300 tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
301 tmp_tar = TarFile(fileobj=tmp, mode='w')
303 # Add the executable to the tarball
304 bn = os.path.basename(args.executable)
305 ff = "/usr/bin/%s" % bn
306 tmp_tar.add(args.executable, arcname=ff)
308 # Add any associated libraries
309 libs = _get_so_libs(args.executable)
310 if libs:
311 for l in libs:
312 tmp_tar.add(os.path.realpath(l), arcname=l)
314 # Create a Docker buildfile
315 df = StringIO()
316 df.write("FROM %s\n" % args.tag)
317 df.write("ADD . /\n")
318 df.seek(0)
320 df_tar = TarInfo(name="Dockerfile")
321 df_tar.size = len(df.buf)
322 tmp_tar.addfile(df_tar, fileobj=df)
324 tmp_tar.close()
326 # reset the file pointers
327 tmp.flush()
328 tmp.seek(0)
330 # Run the build with our tarball context
331 dkr = Docker()
332 dkr.update_image(args.tag, tmp, quiet=args.quiet)
334 return 0
336 class CleanCommand(SubCommand):
337 """Clean up docker instances"""
338 name = "clean"
339 def run(self, args, argv):
340 Docker().clean()
341 return 0
343 class ImagesCommand(SubCommand):
344 """Run "docker images" command"""
345 name = "images"
346 def run(self, args, argv):
347 return Docker().command("images", argv, args.quiet)
349 def main():
350 parser = argparse.ArgumentParser(description="A Docker helper",
351 usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
352 subparsers = parser.add_subparsers(title="subcommands", help=None)
353 for cls in SubCommand.__subclasses__():
354 cmd = cls()
355 subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
356 cmd.shared_args(subp)
357 cmd.args(subp)
358 subp.set_defaults(cmdobj=cmd)
359 args, argv = parser.parse_known_args()
360 return args.cmdobj.run(args, argv)
362 if __name__ == "__main__":
363 sys.exit(main())