usb-mtp: fix error conditions for write operation
[qemu/ericb.git] / tests / docker / docker.py
blobd3006d4dae1b31e343eeb303bbbddd80352ff430
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 sys.path.append(os.path.join(os.path.dirname(__file__),
18 '..', '..', 'scripts'))
19 import argparse
20 import subprocess
21 import json
22 import hashlib
23 import atexit
24 import uuid
25 import tempfile
26 import re
27 import signal
28 from tarfile import TarFile, TarInfo
29 try:
30 from StringIO import StringIO
31 except ImportError:
32 from io import StringIO
33 from shutil import copy, rmtree
34 from pwd import getpwuid
35 from datetime import datetime,timedelta
38 FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
41 DEVNULL = open(os.devnull, 'wb')
44 def _text_checksum(text):
45 """Calculate a digest string unique to the text content"""
46 return hashlib.sha1(text).hexdigest()
48 def _file_checksum(filename):
49 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)
67 def _copy_with_mkdir(src, root_dir, sub_path='.'):
68 """Copy src into root_dir, creating sub_path as needed."""
69 dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
70 try:
71 os.makedirs(dest_dir)
72 except OSError:
73 # we can safely ignore already created directories
74 pass
76 dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
77 copy(src, dest_file)
80 def _get_so_libs(executable):
81 """Return a list of libraries associated with an executable.
83 The paths may be symbolic links which would need to be resolved to
84 ensure theright data is copied."""
86 libs = []
87 ldd_re = re.compile(r"(/.*/)(\S*)")
88 try:
89 ldd_output = subprocess.check_output(["ldd", executable])
90 for line in ldd_output.split("\n"):
91 search = ldd_re.search(line)
92 if search and len(search.groups()) == 2:
93 so_path = search.groups()[0]
94 so_lib = search.groups()[1]
95 libs.append("%s/%s" % (so_path, so_lib))
96 except subprocess.CalledProcessError:
97 print("%s had no associated libraries (static build?)" % (executable))
99 return libs
101 def _copy_binary_with_libs(src, dest_dir):
102 """Copy a binary executable and all its dependant libraries.
104 This does rely on the host file-system being fairly multi-arch
105 aware so the file don't clash with the guests layout."""
107 _copy_with_mkdir(src, dest_dir, "/usr/bin")
109 libs = _get_so_libs(src)
110 if libs:
111 for l in libs:
112 so_path = os.path.dirname(l)
113 _copy_with_mkdir(l , dest_dir, so_path)
116 def _check_binfmt_misc(executable):
117 """Check binfmt_misc has entry for executable in the right place.
119 The details of setting up binfmt_misc are outside the scope of
120 this script but we should at least fail early with a useful
121 message if it won't work."""
123 binary = os.path.basename(executable)
124 binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
126 if not os.path.exists(binfmt_entry):
127 print ("No binfmt_misc entry for %s" % (binary))
128 return False
130 with open(binfmt_entry) as x: entry = x.read()
132 qpath = "/usr/bin/%s" % (binary)
133 if not re.search("interpreter %s\n" % (qpath), entry):
134 print ("binfmt_misc for %s does not point to %s" % (binary, qpath))
135 return False
137 return True
140 def _read_qemu_dockerfile(img_name):
141 # special case for Debian linux-user images
142 if img_name.startswith("debian") and img_name.endswith("user"):
143 img_name = "debian-bootstrap"
145 df = os.path.join(os.path.dirname(__file__), "dockerfiles",
146 img_name + ".docker")
147 return open(df, "r").read()
149 def _dockerfile_preprocess(df):
150 out = ""
151 for l in df.splitlines():
152 if len(l.strip()) == 0 or l.startswith("#"):
153 continue
154 from_pref = "FROM qemu:"
155 if l.startswith(from_pref):
156 # TODO: Alternatively we could replace this line with "FROM $ID"
157 # where $ID is the image's hex id obtained with
158 # $ docker images $IMAGE --format="{{.Id}}"
159 # but unfortunately that's not supported by RHEL 7.
160 inlining = _read_qemu_dockerfile(l[len(from_pref):])
161 out += _dockerfile_preprocess(inlining)
162 continue
163 out += l + "\n"
164 return out
166 class Docker(object):
167 """ Running Docker commands """
168 def __init__(self):
169 self._command = _guess_docker_command()
170 self._instances = []
171 atexit.register(self._kill_instances)
172 signal.signal(signal.SIGTERM, self._kill_instances)
173 signal.signal(signal.SIGHUP, self._kill_instances)
175 def _do(self, cmd, quiet=True, **kwargs):
176 if quiet:
177 kwargs["stdout"] = DEVNULL
178 return subprocess.call(self._command + cmd, **kwargs)
180 def _do_check(self, cmd, quiet=True, **kwargs):
181 if quiet:
182 kwargs["stdout"] = DEVNULL
183 return subprocess.check_call(self._command + cmd, **kwargs)
185 def _do_kill_instances(self, only_known, only_active=True):
186 cmd = ["ps", "-q"]
187 if not only_active:
188 cmd.append("-a")
189 for i in self._output(cmd).split():
190 resp = self._output(["inspect", i])
191 labels = json.loads(resp)[0]["Config"]["Labels"]
192 active = json.loads(resp)[0]["State"]["Running"]
193 if not labels:
194 continue
195 instance_uuid = labels.get("com.qemu.instance.uuid", None)
196 if not instance_uuid:
197 continue
198 if only_known and instance_uuid not in self._instances:
199 continue
200 print("Terminating", i)
201 if active:
202 self._do(["kill", i])
203 self._do(["rm", i])
205 def clean(self):
206 self._do_kill_instances(False, False)
207 return 0
209 def _kill_instances(self, *args, **kwargs):
210 return self._do_kill_instances(True)
212 def _output(self, cmd, **kwargs):
213 return subprocess.check_output(self._command + cmd,
214 stderr=subprocess.STDOUT,
215 **kwargs)
217 def inspect_tag(self, tag):
218 try:
219 return self._output(["inspect", tag])
220 except subprocess.CalledProcessError:
221 return None
223 def get_image_creation_time(self, info):
224 return json.loads(info)[0]["Created"]
226 def get_image_dockerfile_checksum(self, tag):
227 resp = self.inspect_tag(tag)
228 labels = json.loads(resp)[0]["Config"].get("Labels", {})
229 return labels.get("com.qemu.dockerfile-checksum", "")
231 def build_image(self, tag, docker_dir, dockerfile,
232 quiet=True, user=False, argv=None, extra_files_cksum=[]):
233 if argv == None:
234 argv = []
236 tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker")
237 tmp_df.write(dockerfile)
239 if user:
240 uid = os.getuid()
241 uname = getpwuid(uid).pw_name
242 tmp_df.write("\n")
243 tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
244 (uname, uid, uname))
246 tmp_df.write("\n")
247 tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
248 _text_checksum(_dockerfile_preprocess(dockerfile)))
249 for f, c in extra_files_cksum:
250 tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
252 tmp_df.flush()
254 self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv + \
255 [docker_dir],
256 quiet=quiet)
258 def update_image(self, tag, tarball, quiet=True):
259 "Update a tagged image using "
261 self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
263 def image_matches_dockerfile(self, tag, dockerfile):
264 try:
265 checksum = self.get_image_dockerfile_checksum(tag)
266 except Exception:
267 return False
268 return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
270 def run(self, cmd, keep, quiet):
271 label = uuid.uuid1().hex
272 if not keep:
273 self._instances.append(label)
274 ret = self._do_check(["run", "--label",
275 "com.qemu.instance.uuid=" + label] + cmd,
276 quiet=quiet)
277 if not keep:
278 self._instances.remove(label)
279 return ret
281 def command(self, cmd, argv, quiet):
282 return self._do([cmd] + argv, quiet=quiet)
284 class SubCommand(object):
285 """A SubCommand template base class"""
286 name = None # Subcommand name
287 def shared_args(self, parser):
288 parser.add_argument("--quiet", action="store_true",
289 help="Run quietly unless an error occured")
291 def args(self, parser):
292 """Setup argument parser"""
293 pass
294 def run(self, args, argv):
295 """Run command.
296 args: parsed argument by argument parser.
297 argv: remaining arguments from sys.argv.
299 pass
301 class RunCommand(SubCommand):
302 """Invoke docker run and take care of cleaning up"""
303 name = "run"
304 def args(self, parser):
305 parser.add_argument("--keep", action="store_true",
306 help="Don't remove image when command completes")
307 def run(self, args, argv):
308 return Docker().run(argv, args.keep, quiet=args.quiet)
310 class BuildCommand(SubCommand):
311 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
312 name = "build"
313 def args(self, parser):
314 parser.add_argument("--include-executable", "-e",
315 help="""Specify a binary that will be copied to the
316 container together with all its dependent
317 libraries""")
318 parser.add_argument("--extra-files", "-f", nargs='*',
319 help="""Specify files that will be copied in the
320 Docker image, fulfilling the ADD directive from the
321 Dockerfile""")
322 parser.add_argument("--add-current-user", "-u", dest="user",
323 action="store_true",
324 help="Add the current user to image's passwd")
325 parser.add_argument("tag",
326 help="Image Tag")
327 parser.add_argument("dockerfile",
328 help="Dockerfile name")
330 def run(self, args, argv):
331 dockerfile = open(args.dockerfile, "rb").read()
332 tag = args.tag
334 dkr = Docker()
335 if "--no-cache" not in argv and \
336 dkr.image_matches_dockerfile(tag, dockerfile):
337 if not args.quiet:
338 print("Image is up to date.")
339 else:
340 # Create a docker context directory for the build
341 docker_dir = tempfile.mkdtemp(prefix="docker_build")
343 # Validate binfmt_misc will work
344 if args.include_executable:
345 if not _check_binfmt_misc(args.include_executable):
346 return 1
348 # Is there a .pre file to run in the build context?
349 docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
350 if os.path.exists(docker_pre):
351 stdout = DEVNULL if args.quiet else None
352 rc = subprocess.call(os.path.realpath(docker_pre),
353 cwd=docker_dir, stdout=stdout)
354 if rc == 3:
355 print("Skip")
356 return 0
357 elif rc != 0:
358 print("%s exited with code %d" % (docker_pre, rc))
359 return 1
361 # Copy any extra files into the Docker context. These can be
362 # included by the use of the ADD directive in the Dockerfile.
363 cksum = []
364 if args.include_executable:
365 # FIXME: there is no checksum of this executable and the linked
366 # libraries, once the image built any change of this executable
367 # or any library won't trigger another build.
368 _copy_binary_with_libs(args.include_executable, docker_dir)
369 for filename in args.extra_files or []:
370 _copy_with_mkdir(filename, docker_dir)
371 cksum += [(filename, _file_checksum(filename))]
373 argv += ["--build-arg=" + k.lower() + "=" + v
374 for k, v in os.environ.iteritems()
375 if k.lower() in FILTERED_ENV_NAMES]
376 dkr.build_image(tag, docker_dir, dockerfile,
377 quiet=args.quiet, user=args.user, argv=argv,
378 extra_files_cksum=cksum)
380 rmtree(docker_dir)
382 return 0
384 class UpdateCommand(SubCommand):
385 """ Update a docker image with new executables. Arguments: <tag> <executable>"""
386 name = "update"
387 def args(self, parser):
388 parser.add_argument("tag",
389 help="Image Tag")
390 parser.add_argument("executable",
391 help="Executable to copy")
393 def run(self, args, argv):
394 # Create a temporary tarball with our whole build context and
395 # dockerfile for the update
396 tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
397 tmp_tar = TarFile(fileobj=tmp, mode='w')
399 # Add the executable to the tarball
400 bn = os.path.basename(args.executable)
401 ff = "/usr/bin/%s" % bn
402 tmp_tar.add(args.executable, arcname=ff)
404 # Add any associated libraries
405 libs = _get_so_libs(args.executable)
406 if libs:
407 for l in libs:
408 tmp_tar.add(os.path.realpath(l), arcname=l)
410 # Create a Docker buildfile
411 df = StringIO()
412 df.write("FROM %s\n" % args.tag)
413 df.write("ADD . /\n")
414 df.seek(0)
416 df_tar = TarInfo(name="Dockerfile")
417 df_tar.size = len(df.buf)
418 tmp_tar.addfile(df_tar, fileobj=df)
420 tmp_tar.close()
422 # reset the file pointers
423 tmp.flush()
424 tmp.seek(0)
426 # Run the build with our tarball context
427 dkr = Docker()
428 dkr.update_image(args.tag, tmp, quiet=args.quiet)
430 return 0
432 class CleanCommand(SubCommand):
433 """Clean up docker instances"""
434 name = "clean"
435 def run(self, args, argv):
436 Docker().clean()
437 return 0
439 class ImagesCommand(SubCommand):
440 """Run "docker images" command"""
441 name = "images"
442 def run(self, args, argv):
443 return Docker().command("images", argv, args.quiet)
446 class ProbeCommand(SubCommand):
447 """Probe if we can run docker automatically"""
448 name = "probe"
450 def run(self, args, argv):
451 try:
452 docker = Docker()
453 if docker._command[0] == "docker":
454 print("yes")
455 elif docker._command[0] == "sudo":
456 print("sudo")
457 except Exception:
458 print("no")
460 return
463 class CcCommand(SubCommand):
464 """Compile sources with cc in images"""
465 name = "cc"
467 def args(self, parser):
468 parser.add_argument("--image", "-i", required=True,
469 help="The docker image in which to run cc")
470 parser.add_argument("--cc", default="cc",
471 help="The compiler executable to call")
472 parser.add_argument("--user",
473 help="The user-id to run under")
474 parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
475 help="""Extra paths to (ro) mount into container for
476 reading sources""")
478 def run(self, args, argv):
479 if argv and argv[0] == "--":
480 argv = argv[1:]
481 cwd = os.getcwd()
482 cmd = ["--rm", "-w", cwd,
483 "-v", "%s:%s:rw" % (cwd, cwd)]
484 if args.paths:
485 for p in args.paths:
486 cmd += ["-v", "%s:%s:ro,z" % (p, p)]
487 if args.user:
488 cmd += ["-u", args.user]
489 cmd += [args.image, args.cc]
490 cmd += argv
491 return Docker().command("run", cmd, args.quiet)
494 class CheckCommand(SubCommand):
495 """Check if we need to re-build a docker image out of a dockerfile.
496 Arguments: <tag> <dockerfile>"""
497 name = "check"
499 def args(self, parser):
500 parser.add_argument("tag",
501 help="Image Tag")
502 parser.add_argument("dockerfile", default=None,
503 help="Dockerfile name", nargs='?')
504 parser.add_argument("--checktype", choices=["checksum", "age"],
505 default="checksum", help="check type")
506 parser.add_argument("--olderthan", default=60, type=int,
507 help="number of minutes")
509 def run(self, args, argv):
510 tag = args.tag
512 try:
513 dkr = Docker()
514 except:
515 print("Docker not set up")
516 return 1
518 info = dkr.inspect_tag(tag)
519 if info is None:
520 print("Image does not exist")
521 return 1
523 if args.checktype == "checksum":
524 if not args.dockerfile:
525 print("Need a dockerfile for tag:%s" % (tag))
526 return 1
528 dockerfile = open(args.dockerfile, "rb").read()
530 if dkr.image_matches_dockerfile(tag, dockerfile):
531 if not args.quiet:
532 print("Image is up to date")
533 return 0
534 else:
535 print("Image needs updating")
536 return 1
537 elif args.checktype == "age":
538 timestr = dkr.get_image_creation_time(info).split(".")[0]
539 created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
540 past = datetime.now() - timedelta(minutes=args.olderthan)
541 if created < past:
542 print ("Image created @ %s more than %d minutes old" %
543 (timestr, args.olderthan))
544 return 1
545 else:
546 if not args.quiet:
547 print ("Image less than %d minutes old" % (args.olderthan))
548 return 0
551 def main():
552 parser = argparse.ArgumentParser(description="A Docker helper",
553 usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
554 subparsers = parser.add_subparsers(title="subcommands", help=None)
555 for cls in SubCommand.__subclasses__():
556 cmd = cls()
557 subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
558 cmd.shared_args(subp)
559 cmd.args(subp)
560 subp.set_defaults(cmdobj=cmd)
561 args, argv = parser.parse_known_args()
562 return args.cmdobj.run(args, argv)
564 if __name__ == "__main__":
565 sys.exit(main())