spapr/xive: Add proper rollback to kvmppc_xive_connect()
[qemu/ar7.git] / tests / docker / docker.py
blob53a8c9c801e96d37118a727319d3afb603769839
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 from __future__ import print_function
15 import os
16 import sys
17 import subprocess
18 import json
19 import hashlib
20 import atexit
21 import uuid
22 import argparse
23 import tempfile
24 import re
25 import signal
26 from tarfile import TarFile, TarInfo
27 try:
28 from StringIO import StringIO
29 except ImportError:
30 from io import StringIO
31 from shutil import copy, rmtree
32 from pwd import getpwuid
33 from datetime import datetime, timedelta
36 FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
39 DEVNULL = open(os.devnull, 'wb')
42 def _text_checksum(text):
43 """Calculate a digest string unique to the text content"""
44 return hashlib.sha1(text).hexdigest()
47 def _file_checksum(filename):
48 return _text_checksum(open(filename, 'rb').read())
51 def _guess_docker_command():
52 """ Guess a working docker command or raise exception if not found"""
53 commands = [["docker"], ["sudo", "-n", "docker"]]
54 for cmd in commands:
55 try:
56 # docker version will return the client details in stdout
57 # but still report a status of 1 if it can't contact the daemon
58 if subprocess.call(cmd + ["version"],
59 stdout=DEVNULL, stderr=DEVNULL) == 0:
60 return cmd
61 except OSError:
62 pass
63 commands_txt = "\n".join([" " + " ".join(x) for x in commands])
64 raise Exception("Cannot find working docker command. Tried:\n%s" %
65 commands_txt)
68 def _copy_with_mkdir(src, root_dir, sub_path='.'):
69 """Copy src into root_dir, creating sub_path as needed."""
70 dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
71 try:
72 os.makedirs(dest_dir)
73 except OSError:
74 # we can safely ignore already created directories
75 pass
77 dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
78 copy(src, dest_file)
81 def _get_so_libs(executable):
82 """Return a list of libraries associated with an executable.
84 The paths may be symbolic links which would need to be resolved to
85 ensure theright data is copied."""
87 libs = []
88 ldd_re = re.compile(r"(/.*/)(\S*)")
89 try:
90 ldd_output = subprocess.check_output(["ldd", executable])
91 for line in ldd_output.split("\n"):
92 search = ldd_re.search(line)
93 if search and len(search.groups()) == 2:
94 so_path = search.groups()[0]
95 so_lib = search.groups()[1]
96 libs.append("%s/%s" % (so_path, so_lib))
97 except subprocess.CalledProcessError:
98 print("%s had no associated libraries (static build?)" % (executable))
100 return libs
103 def _copy_binary_with_libs(src, bin_dest, dest_dir):
104 """Maybe copy a binary and all its dependent libraries.
106 If bin_dest isn't set we only copy the support libraries because
107 we don't need qemu in the docker path to run (due to persistent
108 mapping). Indeed users may get confused if we aren't running what
109 is in the image.
111 This does rely on the host file-system being fairly multi-arch
112 aware so the file don't clash with the guests layout.
115 if bin_dest:
116 _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
117 else:
118 print("only copying support libraries for %s" % (src))
120 libs = _get_so_libs(src)
121 if libs:
122 for l in libs:
123 so_path = os.path.dirname(l)
124 _copy_with_mkdir(l, dest_dir, so_path)
127 def _check_binfmt_misc(executable):
128 """Check binfmt_misc has entry for executable in the right place.
130 The details of setting up binfmt_misc are outside the scope of
131 this script but we should at least fail early with a useful
132 message if it won't work.
134 Returns the configured binfmt path and a valid flag. For
135 persistent configurations we will still want to copy and dependent
136 libraries.
139 binary = os.path.basename(executable)
140 binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
142 if not os.path.exists(binfmt_entry):
143 print ("No binfmt_misc entry for %s" % (binary))
144 return None, False
146 with open(binfmt_entry) as x: entry = x.read()
148 if re.search("flags:.*F.*\n", entry):
149 print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
150 (binary))
151 return None, True
153 m = re.search("interpreter (\S+)\n", entry)
154 interp = m.group(1)
155 if interp and interp != executable:
156 print("binfmt_misc for %s does not point to %s, using %s" %
157 (binary, executable, interp))
159 return interp, True
162 def _read_qemu_dockerfile(img_name):
163 # special case for Debian linux-user images
164 if img_name.startswith("debian") and img_name.endswith("user"):
165 img_name = "debian-bootstrap"
167 df = os.path.join(os.path.dirname(__file__), "dockerfiles",
168 img_name + ".docker")
169 return open(df, "r").read()
172 def _dockerfile_preprocess(df):
173 out = ""
174 for l in df.splitlines():
175 if len(l.strip()) == 0 or l.startswith("#"):
176 continue
177 from_pref = "FROM qemu:"
178 if l.startswith(from_pref):
179 # TODO: Alternatively we could replace this line with "FROM $ID"
180 # where $ID is the image's hex id obtained with
181 # $ docker images $IMAGE --format="{{.Id}}"
182 # but unfortunately that's not supported by RHEL 7.
183 inlining = _read_qemu_dockerfile(l[len(from_pref):])
184 out += _dockerfile_preprocess(inlining)
185 continue
186 out += l + "\n"
187 return out
190 class Docker(object):
191 """ Running Docker commands """
192 def __init__(self):
193 self._command = _guess_docker_command()
194 self._instances = []
195 atexit.register(self._kill_instances)
196 signal.signal(signal.SIGTERM, self._kill_instances)
197 signal.signal(signal.SIGHUP, self._kill_instances)
199 def _do(self, cmd, quiet=True, **kwargs):
200 if quiet:
201 kwargs["stdout"] = DEVNULL
202 return subprocess.call(self._command + cmd, **kwargs)
204 def _do_check(self, cmd, quiet=True, **kwargs):
205 if quiet:
206 kwargs["stdout"] = DEVNULL
207 return subprocess.check_call(self._command + cmd, **kwargs)
209 def _do_kill_instances(self, only_known, only_active=True):
210 cmd = ["ps", "-q"]
211 if not only_active:
212 cmd.append("-a")
213 for i in self._output(cmd).split():
214 resp = self._output(["inspect", i])
215 labels = json.loads(resp)[0]["Config"]["Labels"]
216 active = json.loads(resp)[0]["State"]["Running"]
217 if not labels:
218 continue
219 instance_uuid = labels.get("com.qemu.instance.uuid", None)
220 if not instance_uuid:
221 continue
222 if only_known and instance_uuid not in self._instances:
223 continue
224 print("Terminating", i)
225 if active:
226 self._do(["kill", i])
227 self._do(["rm", i])
229 def clean(self):
230 self._do_kill_instances(False, False)
231 return 0
233 def _kill_instances(self, *args, **kwargs):
234 return self._do_kill_instances(True)
236 def _output(self, cmd, **kwargs):
237 return subprocess.check_output(self._command + cmd,
238 stderr=subprocess.STDOUT,
239 **kwargs)
241 def inspect_tag(self, tag):
242 try:
243 return self._output(["inspect", tag])
244 except subprocess.CalledProcessError:
245 return None
247 def get_image_creation_time(self, info):
248 return json.loads(info)[0]["Created"]
250 def get_image_dockerfile_checksum(self, tag):
251 resp = self.inspect_tag(tag)
252 labels = json.loads(resp)[0]["Config"].get("Labels", {})
253 return labels.get("com.qemu.dockerfile-checksum", "")
255 def build_image(self, tag, docker_dir, dockerfile,
256 quiet=True, user=False, argv=None, extra_files_cksum=[]):
257 if argv is None:
258 argv = []
260 tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
261 tmp_df.write(dockerfile)
263 if user:
264 uid = os.getuid()
265 uname = getpwuid(uid).pw_name
266 tmp_df.write("\n")
267 tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
268 (uname, uid, uname))
270 tmp_df.write("\n")
271 tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
272 _text_checksum(_dockerfile_preprocess(dockerfile)))
273 for f, c in extra_files_cksum:
274 tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
276 tmp_df.flush()
278 self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv +
279 [docker_dir],
280 quiet=quiet)
282 def update_image(self, tag, tarball, quiet=True):
283 "Update a tagged image using "
285 self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
287 def image_matches_dockerfile(self, tag, dockerfile):
288 try:
289 checksum = self.get_image_dockerfile_checksum(tag)
290 except Exception:
291 return False
292 return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
294 def run(self, cmd, keep, quiet):
295 label = uuid.uuid1().hex
296 if not keep:
297 self._instances.append(label)
298 ret = self._do_check(["run", "--label",
299 "com.qemu.instance.uuid=" + label] + cmd,
300 quiet=quiet)
301 if not keep:
302 self._instances.remove(label)
303 return ret
305 def command(self, cmd, argv, quiet):
306 return self._do([cmd] + argv, quiet=quiet)
309 class SubCommand(object):
310 """A SubCommand template base class"""
311 name = None # Subcommand name
313 def shared_args(self, parser):
314 parser.add_argument("--quiet", action="store_true",
315 help="Run quietly unless an error occurred")
317 def args(self, parser):
318 """Setup argument parser"""
319 pass
321 def run(self, args, argv):
322 """Run command.
323 args: parsed argument by argument parser.
324 argv: remaining arguments from sys.argv.
326 pass
329 class RunCommand(SubCommand):
330 """Invoke docker run and take care of cleaning up"""
331 name = "run"
333 def args(self, parser):
334 parser.add_argument("--keep", action="store_true",
335 help="Don't remove image when command completes")
337 def run(self, args, argv):
338 return Docker().run(argv, args.keep, quiet=args.quiet)
341 class BuildCommand(SubCommand):
342 """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
343 name = "build"
345 def args(self, parser):
346 parser.add_argument("--include-executable", "-e",
347 help="""Specify a binary that will be copied to the
348 container together with all its dependent
349 libraries""")
350 parser.add_argument("--extra-files", "-f", nargs='*',
351 help="""Specify files that will be copied in the
352 Docker image, fulfilling the ADD directive from the
353 Dockerfile""")
354 parser.add_argument("--add-current-user", "-u", dest="user",
355 action="store_true",
356 help="Add the current user to image's passwd")
357 parser.add_argument("tag",
358 help="Image Tag")
359 parser.add_argument("dockerfile",
360 help="Dockerfile name")
362 def run(self, args, argv):
363 dockerfile = open(args.dockerfile, "rb").read()
364 tag = args.tag
366 dkr = Docker()
367 if "--no-cache" not in argv and \
368 dkr.image_matches_dockerfile(tag, dockerfile):
369 if not args.quiet:
370 print("Image is up to date.")
371 else:
372 # Create a docker context directory for the build
373 docker_dir = tempfile.mkdtemp(prefix="docker_build")
375 # Validate binfmt_misc will work
376 if args.include_executable:
377 qpath, enabled = _check_binfmt_misc(args.include_executable)
378 if not enabled:
379 return 1
381 # Is there a .pre file to run in the build context?
382 docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
383 if os.path.exists(docker_pre):
384 stdout = DEVNULL if args.quiet else None
385 rc = subprocess.call(os.path.realpath(docker_pre),
386 cwd=docker_dir, stdout=stdout)
387 if rc == 3:
388 print("Skip")
389 return 0
390 elif rc != 0:
391 print("%s exited with code %d" % (docker_pre, rc))
392 return 1
394 # Copy any extra files into the Docker context. These can be
395 # included by the use of the ADD directive in the Dockerfile.
396 cksum = []
397 if args.include_executable:
398 # FIXME: there is no checksum of this executable and the linked
399 # libraries, once the image built any change of this executable
400 # or any library won't trigger another build.
401 _copy_binary_with_libs(args.include_executable,
402 qpath, docker_dir)
404 for filename in args.extra_files or []:
405 _copy_with_mkdir(filename, docker_dir)
406 cksum += [(filename, _file_checksum(filename))]
408 argv += ["--build-arg=" + k.lower() + "=" + v
409 for k, v in os.environ.iteritems()
410 if k.lower() in FILTERED_ENV_NAMES]
411 dkr.build_image(tag, docker_dir, dockerfile,
412 quiet=args.quiet, user=args.user, argv=argv,
413 extra_files_cksum=cksum)
415 rmtree(docker_dir)
417 return 0
420 class UpdateCommand(SubCommand):
421 """ Update a docker image with new executables. Args: <tag> <executable>"""
422 name = "update"
424 def args(self, parser):
425 parser.add_argument("tag",
426 help="Image Tag")
427 parser.add_argument("executable",
428 help="Executable to copy")
430 def run(self, args, argv):
431 # Create a temporary tarball with our whole build context and
432 # dockerfile for the update
433 tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
434 tmp_tar = TarFile(fileobj=tmp, mode='w')
436 # Add the executable to the tarball, using the current
437 # configured binfmt_misc path. If we don't get a path then we
438 # only need the support libraries copied
439 ff, enabled = _check_binfmt_misc(args.executable)
441 if not enabled:
442 print("binfmt_misc not enabled, update disabled")
443 return 1
445 if ff:
446 tmp_tar.add(args.executable, arcname=ff)
448 # Add any associated libraries
449 libs = _get_so_libs(args.executable)
450 if libs:
451 for l in libs:
452 tmp_tar.add(os.path.realpath(l), arcname=l)
454 # Create a Docker buildfile
455 df = StringIO()
456 df.write("FROM %s\n" % args.tag)
457 df.write("ADD . /\n")
458 df.seek(0)
460 df_tar = TarInfo(name="Dockerfile")
461 df_tar.size = len(df.buf)
462 tmp_tar.addfile(df_tar, fileobj=df)
464 tmp_tar.close()
466 # reset the file pointers
467 tmp.flush()
468 tmp.seek(0)
470 # Run the build with our tarball context
471 dkr = Docker()
472 dkr.update_image(args.tag, tmp, quiet=args.quiet)
474 return 0
477 class CleanCommand(SubCommand):
478 """Clean up docker instances"""
479 name = "clean"
481 def run(self, args, argv):
482 Docker().clean()
483 return 0
486 class ImagesCommand(SubCommand):
487 """Run "docker images" command"""
488 name = "images"
490 def run(self, args, argv):
491 return Docker().command("images", argv, args.quiet)
494 class ProbeCommand(SubCommand):
495 """Probe if we can run docker automatically"""
496 name = "probe"
498 def run(self, args, argv):
499 try:
500 docker = Docker()
501 if docker._command[0] == "docker":
502 print("yes")
503 elif docker._command[0] == "sudo":
504 print("sudo")
505 except Exception:
506 print("no")
508 return
511 class CcCommand(SubCommand):
512 """Compile sources with cc in images"""
513 name = "cc"
515 def args(self, parser):
516 parser.add_argument("--image", "-i", required=True,
517 help="The docker image in which to run cc")
518 parser.add_argument("--cc", default="cc",
519 help="The compiler executable to call")
520 parser.add_argument("--user",
521 help="The user-id to run under")
522 parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
523 help="""Extra paths to (ro) mount into container for
524 reading sources""")
526 def run(self, args, argv):
527 if argv and argv[0] == "--":
528 argv = argv[1:]
529 cwd = os.getcwd()
530 cmd = ["--rm", "-w", cwd,
531 "-v", "%s:%s:rw" % (cwd, cwd)]
532 if args.paths:
533 for p in args.paths:
534 cmd += ["-v", "%s:%s:ro,z" % (p, p)]
535 if args.user:
536 cmd += ["-u", args.user]
537 cmd += [args.image, args.cc]
538 cmd += argv
539 return Docker().command("run", cmd, args.quiet)
542 class CheckCommand(SubCommand):
543 """Check if we need to re-build a docker image out of a dockerfile.
544 Arguments: <tag> <dockerfile>"""
545 name = "check"
547 def args(self, parser):
548 parser.add_argument("tag",
549 help="Image Tag")
550 parser.add_argument("dockerfile", default=None,
551 help="Dockerfile name", nargs='?')
552 parser.add_argument("--checktype", choices=["checksum", "age"],
553 default="checksum", help="check type")
554 parser.add_argument("--olderthan", default=60, type=int,
555 help="number of minutes")
557 def run(self, args, argv):
558 tag = args.tag
560 try:
561 dkr = Docker()
562 except subprocess.CalledProcessError:
563 print("Docker not set up")
564 return 1
566 info = dkr.inspect_tag(tag)
567 if info is None:
568 print("Image does not exist")
569 return 1
571 if args.checktype == "checksum":
572 if not args.dockerfile:
573 print("Need a dockerfile for tag:%s" % (tag))
574 return 1
576 dockerfile = open(args.dockerfile, "rb").read()
578 if dkr.image_matches_dockerfile(tag, dockerfile):
579 if not args.quiet:
580 print("Image is up to date")
581 return 0
582 else:
583 print("Image needs updating")
584 return 1
585 elif args.checktype == "age":
586 timestr = dkr.get_image_creation_time(info).split(".")[0]
587 created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
588 past = datetime.now() - timedelta(minutes=args.olderthan)
589 if created < past:
590 print ("Image created @ %s more than %d minutes old" %
591 (timestr, args.olderthan))
592 return 1
593 else:
594 if not args.quiet:
595 print ("Image less than %d minutes old" % (args.olderthan))
596 return 0
599 def main():
600 parser = argparse.ArgumentParser(description="A Docker helper",
601 usage="%s <subcommand> ..." %
602 os.path.basename(sys.argv[0]))
603 subparsers = parser.add_subparsers(title="subcommands", help=None)
604 for cls in SubCommand.__subclasses__():
605 cmd = cls()
606 subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
607 cmd.shared_args(subp)
608 cmd.args(subp)
609 subp.set_defaults(cmdobj=cmd)
610 args, argv = parser.parse_known_args()
611 return args.cmdobj.run(args, argv)
614 if __name__ == "__main__":
615 sys.exit(main())