14485b04bSFam Zheng#!/usr/bin/env python2 24485b04bSFam Zheng# 34485b04bSFam Zheng# Docker controlling module 44485b04bSFam Zheng# 54485b04bSFam Zheng# Copyright (c) 2016 Red Hat Inc. 64485b04bSFam Zheng# 74485b04bSFam Zheng# Authors: 84485b04bSFam Zheng# Fam Zheng <famz@redhat.com> 94485b04bSFam Zheng# 104485b04bSFam Zheng# This work is licensed under the terms of the GNU GPL, version 2 114485b04bSFam Zheng# or (at your option) any later version. See the COPYING file in 124485b04bSFam Zheng# the top-level directory. 134485b04bSFam Zheng 144485b04bSFam Zhengimport os 154485b04bSFam Zhengimport sys 164485b04bSFam Zhengimport subprocess 174485b04bSFam Zhengimport json 184485b04bSFam Zhengimport hashlib 194485b04bSFam Zhengimport atexit 204485b04bSFam Zhengimport uuid 214485b04bSFam Zhengimport argparse 224485b04bSFam Zhengimport tempfile 23504ca3c2SAlex Bennéeimport re 246e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo 256e733da6SAlex Bennéefrom StringIO import StringIO 26a9f8d038SAlex Bennéefrom shutil import copy, rmtree 274485b04bSFam Zheng 28c9772570SSascha Silbe 29c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb') 30c9772570SSascha Silbe 31c9772570SSascha Silbe 324485b04bSFam Zhengdef _text_checksum(text): 334485b04bSFam Zheng """Calculate a digest string unique to the text content""" 344485b04bSFam Zheng return hashlib.sha1(text).hexdigest() 354485b04bSFam Zheng 364485b04bSFam Zhengdef _guess_docker_command(): 374485b04bSFam Zheng """ Guess a working docker command or raise exception if not found""" 384485b04bSFam Zheng commands = [["docker"], ["sudo", "-n", "docker"]] 394485b04bSFam Zheng for cmd in commands: 40*0679f98bSEduardo Habkost try: 414485b04bSFam Zheng if subprocess.call(cmd + ["images"], 42c9772570SSascha Silbe stdout=DEVNULL, stderr=DEVNULL) == 0: 434485b04bSFam Zheng return cmd 44*0679f98bSEduardo Habkost except OSError: 45*0679f98bSEduardo Habkost pass 464485b04bSFam Zheng commands_txt = "\n".join([" " + " ".join(x) for x in commands]) 474485b04bSFam Zheng raise Exception("Cannot find working docker command. Tried:\n%s" % \ 484485b04bSFam Zheng commands_txt) 494485b04bSFam Zheng 50504ca3c2SAlex Bennéedef _copy_with_mkdir(src, root_dir, sub_path): 51504ca3c2SAlex Bennée """Copy src into root_dir, creating sub_path as needed.""" 52504ca3c2SAlex Bennée dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path)) 53504ca3c2SAlex Bennée try: 54504ca3c2SAlex Bennée os.makedirs(dest_dir) 55504ca3c2SAlex Bennée except OSError: 56504ca3c2SAlex Bennée # we can safely ignore already created directories 57504ca3c2SAlex Bennée pass 58504ca3c2SAlex Bennée 59504ca3c2SAlex Bennée dest_file = "%s/%s" % (dest_dir, os.path.basename(src)) 60504ca3c2SAlex Bennée copy(src, dest_file) 61504ca3c2SAlex Bennée 62504ca3c2SAlex Bennée 63504ca3c2SAlex Bennéedef _get_so_libs(executable): 64504ca3c2SAlex Bennée """Return a list of libraries associated with an executable. 65504ca3c2SAlex Bennée 66504ca3c2SAlex Bennée The paths may be symbolic links which would need to be resolved to 67504ca3c2SAlex Bennée ensure theright data is copied.""" 68504ca3c2SAlex Bennée 69504ca3c2SAlex Bennée libs = [] 70504ca3c2SAlex Bennée ldd_re = re.compile(r"(/.*/)(\S*)") 71504ca3c2SAlex Bennée try: 72504ca3c2SAlex Bennée ldd_output = subprocess.check_output(["ldd", executable]) 73504ca3c2SAlex Bennée for line in ldd_output.split("\n"): 74504ca3c2SAlex Bennée search = ldd_re.search(line) 75504ca3c2SAlex Bennée if search and len(search.groups()) == 2: 76504ca3c2SAlex Bennée so_path = search.groups()[0] 77504ca3c2SAlex Bennée so_lib = search.groups()[1] 78504ca3c2SAlex Bennée libs.append("%s/%s" % (so_path, so_lib)) 79504ca3c2SAlex Bennée except subprocess.CalledProcessError: 80504ca3c2SAlex Bennée print "%s had no associated libraries (static build?)" % (executable) 81504ca3c2SAlex Bennée 82504ca3c2SAlex Bennée return libs 83504ca3c2SAlex Bennée 84504ca3c2SAlex Bennéedef _copy_binary_with_libs(src, dest_dir): 85504ca3c2SAlex Bennée """Copy a binary executable and all its dependant libraries. 86504ca3c2SAlex Bennée 87504ca3c2SAlex Bennée This does rely on the host file-system being fairly multi-arch 88504ca3c2SAlex Bennée aware so the file don't clash with the guests layout.""" 89504ca3c2SAlex Bennée 90504ca3c2SAlex Bennée _copy_with_mkdir(src, dest_dir, "/usr/bin") 91504ca3c2SAlex Bennée 92504ca3c2SAlex Bennée libs = _get_so_libs(src) 93504ca3c2SAlex Bennée if libs: 94504ca3c2SAlex Bennée for l in libs: 95504ca3c2SAlex Bennée so_path = os.path.dirname(l) 96504ca3c2SAlex Bennée _copy_with_mkdir(l , dest_dir, so_path) 97504ca3c2SAlex Bennée 984485b04bSFam Zhengclass Docker(object): 994485b04bSFam Zheng """ Running Docker commands """ 1004485b04bSFam Zheng def __init__(self): 1014485b04bSFam Zheng self._command = _guess_docker_command() 1024485b04bSFam Zheng self._instances = [] 1034485b04bSFam Zheng atexit.register(self._kill_instances) 1044485b04bSFam Zheng 1056e733da6SAlex Bennée def _do(self, cmd, quiet=True, infile=None, **kwargs): 1064485b04bSFam Zheng if quiet: 107c9772570SSascha Silbe kwargs["stdout"] = DEVNULL 1086e733da6SAlex Bennée if infile: 1096e733da6SAlex Bennée kwargs["stdin"] = infile 1104485b04bSFam Zheng return subprocess.call(self._command + cmd, **kwargs) 1114485b04bSFam Zheng 1124485b04bSFam Zheng def _do_kill_instances(self, only_known, only_active=True): 1134485b04bSFam Zheng cmd = ["ps", "-q"] 1144485b04bSFam Zheng if not only_active: 1154485b04bSFam Zheng cmd.append("-a") 1164485b04bSFam Zheng for i in self._output(cmd).split(): 1174485b04bSFam Zheng resp = self._output(["inspect", i]) 1184485b04bSFam Zheng labels = json.loads(resp)[0]["Config"]["Labels"] 1194485b04bSFam Zheng active = json.loads(resp)[0]["State"]["Running"] 1204485b04bSFam Zheng if not labels: 1214485b04bSFam Zheng continue 1224485b04bSFam Zheng instance_uuid = labels.get("com.qemu.instance.uuid", None) 1234485b04bSFam Zheng if not instance_uuid: 1244485b04bSFam Zheng continue 1254485b04bSFam Zheng if only_known and instance_uuid not in self._instances: 1264485b04bSFam Zheng continue 1274485b04bSFam Zheng print "Terminating", i 1284485b04bSFam Zheng if active: 1294485b04bSFam Zheng self._do(["kill", i]) 1304485b04bSFam Zheng self._do(["rm", i]) 1314485b04bSFam Zheng 1324485b04bSFam Zheng def clean(self): 1334485b04bSFam Zheng self._do_kill_instances(False, False) 1344485b04bSFam Zheng return 0 1354485b04bSFam Zheng 1364485b04bSFam Zheng def _kill_instances(self): 1374485b04bSFam Zheng return self._do_kill_instances(True) 1384485b04bSFam Zheng 1394485b04bSFam Zheng def _output(self, cmd, **kwargs): 1404485b04bSFam Zheng return subprocess.check_output(self._command + cmd, 1414485b04bSFam Zheng stderr=subprocess.STDOUT, 1424485b04bSFam Zheng **kwargs) 1434485b04bSFam Zheng 1444485b04bSFam Zheng def get_image_dockerfile_checksum(self, tag): 1454485b04bSFam Zheng resp = self._output(["inspect", tag]) 1464485b04bSFam Zheng labels = json.loads(resp)[0]["Config"].get("Labels", {}) 1474485b04bSFam Zheng return labels.get("com.qemu.dockerfile-checksum", "") 1484485b04bSFam Zheng 149a9f8d038SAlex Bennée def build_image(self, tag, docker_dir, dockerfile, quiet=True, argv=None): 1504485b04bSFam Zheng if argv == None: 1514485b04bSFam Zheng argv = [] 1524485b04bSFam Zheng 153a9f8d038SAlex Bennée tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker") 1544485b04bSFam Zheng tmp_df.write(dockerfile) 1554485b04bSFam Zheng 1564485b04bSFam Zheng tmp_df.write("\n") 1574485b04bSFam Zheng tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % 1584485b04bSFam Zheng _text_checksum(dockerfile)) 1594485b04bSFam Zheng tmp_df.flush() 160a9f8d038SAlex Bennée 1614485b04bSFam Zheng self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \ 162a9f8d038SAlex Bennée [docker_dir], 1634485b04bSFam Zheng quiet=quiet) 1644485b04bSFam Zheng 1656e733da6SAlex Bennée def update_image(self, tag, tarball, quiet=True): 1666e733da6SAlex Bennée "Update a tagged image using " 1676e733da6SAlex Bennée 1686e733da6SAlex Bennée self._do(["build", "-t", tag, "-"], quiet=quiet, infile=tarball) 1696e733da6SAlex Bennée 1704485b04bSFam Zheng def image_matches_dockerfile(self, tag, dockerfile): 1714485b04bSFam Zheng try: 1724485b04bSFam Zheng checksum = self.get_image_dockerfile_checksum(tag) 1734485b04bSFam Zheng except Exception: 1744485b04bSFam Zheng return False 1754485b04bSFam Zheng return checksum == _text_checksum(dockerfile) 1764485b04bSFam Zheng 1774485b04bSFam Zheng def run(self, cmd, keep, quiet): 1784485b04bSFam Zheng label = uuid.uuid1().hex 1794485b04bSFam Zheng if not keep: 1804485b04bSFam Zheng self._instances.append(label) 1814485b04bSFam Zheng ret = self._do(["run", "--label", 1824485b04bSFam Zheng "com.qemu.instance.uuid=" + label] + cmd, 1834485b04bSFam Zheng quiet=quiet) 1844485b04bSFam Zheng if not keep: 1854485b04bSFam Zheng self._instances.remove(label) 1864485b04bSFam Zheng return ret 1874485b04bSFam Zheng 1884b08af60SFam Zheng def command(self, cmd, argv, quiet): 1894b08af60SFam Zheng return self._do([cmd] + argv, quiet=quiet) 1904b08af60SFam Zheng 1914485b04bSFam Zhengclass SubCommand(object): 1924485b04bSFam Zheng """A SubCommand template base class""" 1934485b04bSFam Zheng name = None # Subcommand name 1944485b04bSFam Zheng def shared_args(self, parser): 1954485b04bSFam Zheng parser.add_argument("--quiet", action="store_true", 1964485b04bSFam Zheng help="Run quietly unless an error occured") 1974485b04bSFam Zheng 1984485b04bSFam Zheng def args(self, parser): 1994485b04bSFam Zheng """Setup argument parser""" 2004485b04bSFam Zheng pass 2014485b04bSFam Zheng def run(self, args, argv): 2024485b04bSFam Zheng """Run command. 2034485b04bSFam Zheng args: parsed argument by argument parser. 2044485b04bSFam Zheng argv: remaining arguments from sys.argv. 2054485b04bSFam Zheng """ 2064485b04bSFam Zheng pass 2074485b04bSFam Zheng 2084485b04bSFam Zhengclass RunCommand(SubCommand): 2094485b04bSFam Zheng """Invoke docker run and take care of cleaning up""" 2104485b04bSFam Zheng name = "run" 2114485b04bSFam Zheng def args(self, parser): 2124485b04bSFam Zheng parser.add_argument("--keep", action="store_true", 2134485b04bSFam Zheng help="Don't remove image when command completes") 2144485b04bSFam Zheng def run(self, args, argv): 2154485b04bSFam Zheng return Docker().run(argv, args.keep, quiet=args.quiet) 2164485b04bSFam Zheng 2174485b04bSFam Zhengclass BuildCommand(SubCommand): 2184485b04bSFam Zheng """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>""" 2194485b04bSFam Zheng name = "build" 2204485b04bSFam Zheng def args(self, parser): 221504ca3c2SAlex Bennée parser.add_argument("--include-executable", "-e", 222504ca3c2SAlex Bennée help="""Specify a binary that will be copied to the 223504ca3c2SAlex Bennée container together with all its dependent 224504ca3c2SAlex Bennée libraries""") 2254485b04bSFam Zheng parser.add_argument("tag", 2264485b04bSFam Zheng help="Image Tag") 2274485b04bSFam Zheng parser.add_argument("dockerfile", 2284485b04bSFam Zheng help="Dockerfile name") 2294485b04bSFam Zheng 2304485b04bSFam Zheng def run(self, args, argv): 2314485b04bSFam Zheng dockerfile = open(args.dockerfile, "rb").read() 2324485b04bSFam Zheng tag = args.tag 2334485b04bSFam Zheng 2344485b04bSFam Zheng dkr = Docker() 2354485b04bSFam Zheng if dkr.image_matches_dockerfile(tag, dockerfile): 2364485b04bSFam Zheng if not args.quiet: 2374485b04bSFam Zheng print "Image is up to date." 238a9f8d038SAlex Bennée else: 239a9f8d038SAlex Bennée # Create a docker context directory for the build 240a9f8d038SAlex Bennée docker_dir = tempfile.mkdtemp(prefix="docker_build") 2414485b04bSFam Zheng 242920776eaSAlex Bennée # Is there a .pre file to run in the build context? 243920776eaSAlex Bennée docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" 244920776eaSAlex Bennée if os.path.exists(docker_pre): 245f8042deaSSascha Silbe stdout = DEVNULL if args.quiet else None 246920776eaSAlex Bennée rc = subprocess.call(os.path.realpath(docker_pre), 247f8042deaSSascha Silbe cwd=docker_dir, stdout=stdout) 248920776eaSAlex Bennée if rc == 3: 249920776eaSAlex Bennée print "Skip" 250920776eaSAlex Bennée return 0 251920776eaSAlex Bennée elif rc != 0: 252920776eaSAlex Bennée print "%s exited with code %d" % (docker_pre, rc) 253920776eaSAlex Bennée return 1 254920776eaSAlex Bennée 255504ca3c2SAlex Bennée # Do we include a extra binary? 256504ca3c2SAlex Bennée if args.include_executable: 257504ca3c2SAlex Bennée _copy_binary_with_libs(args.include_executable, 258504ca3c2SAlex Bennée docker_dir) 259504ca3c2SAlex Bennée 260a9f8d038SAlex Bennée dkr.build_image(tag, docker_dir, dockerfile, 2614485b04bSFam Zheng quiet=args.quiet, argv=argv) 262a9f8d038SAlex Bennée 263a9f8d038SAlex Bennée rmtree(docker_dir) 264a9f8d038SAlex Bennée 2654485b04bSFam Zheng return 0 2664485b04bSFam Zheng 2676e733da6SAlex Bennéeclass UpdateCommand(SubCommand): 2686e733da6SAlex Bennée """ Update a docker image with new executables. Arguments: <tag> <executable>""" 2696e733da6SAlex Bennée name = "update" 2706e733da6SAlex Bennée def args(self, parser): 2716e733da6SAlex Bennée parser.add_argument("tag", 2726e733da6SAlex Bennée help="Image Tag") 2736e733da6SAlex Bennée parser.add_argument("executable", 2746e733da6SAlex Bennée help="Executable to copy") 2756e733da6SAlex Bennée 2766e733da6SAlex Bennée def run(self, args, argv): 2776e733da6SAlex Bennée # Create a temporary tarball with our whole build context and 2786e733da6SAlex Bennée # dockerfile for the update 2796e733da6SAlex Bennée tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") 2806e733da6SAlex Bennée tmp_tar = TarFile(fileobj=tmp, mode='w') 2816e733da6SAlex Bennée 2826e733da6SAlex Bennée # Add the executable to the tarball 2836e733da6SAlex Bennée bn = os.path.basename(args.executable) 2846e733da6SAlex Bennée ff = "/usr/bin/%s" % bn 2856e733da6SAlex Bennée tmp_tar.add(args.executable, arcname=ff) 2866e733da6SAlex Bennée 2876e733da6SAlex Bennée # Add any associated libraries 2886e733da6SAlex Bennée libs = _get_so_libs(args.executable) 2896e733da6SAlex Bennée if libs: 2906e733da6SAlex Bennée for l in libs: 2916e733da6SAlex Bennée tmp_tar.add(os.path.realpath(l), arcname=l) 2926e733da6SAlex Bennée 2936e733da6SAlex Bennée # Create a Docker buildfile 2946e733da6SAlex Bennée df = StringIO() 2956e733da6SAlex Bennée df.write("FROM %s\n" % args.tag) 2966e733da6SAlex Bennée df.write("ADD . /\n") 2976e733da6SAlex Bennée df.seek(0) 2986e733da6SAlex Bennée 2996e733da6SAlex Bennée df_tar = TarInfo(name="Dockerfile") 3006e733da6SAlex Bennée df_tar.size = len(df.buf) 3016e733da6SAlex Bennée tmp_tar.addfile(df_tar, fileobj=df) 3026e733da6SAlex Bennée 3036e733da6SAlex Bennée tmp_tar.close() 3046e733da6SAlex Bennée 3056e733da6SAlex Bennée # reset the file pointers 3066e733da6SAlex Bennée tmp.flush() 3076e733da6SAlex Bennée tmp.seek(0) 3086e733da6SAlex Bennée 3096e733da6SAlex Bennée # Run the build with our tarball context 3106e733da6SAlex Bennée dkr = Docker() 3116e733da6SAlex Bennée dkr.update_image(args.tag, tmp, quiet=args.quiet) 3126e733da6SAlex Bennée 3136e733da6SAlex Bennée return 0 3146e733da6SAlex Bennée 3154485b04bSFam Zhengclass CleanCommand(SubCommand): 3164485b04bSFam Zheng """Clean up docker instances""" 3174485b04bSFam Zheng name = "clean" 3184485b04bSFam Zheng def run(self, args, argv): 3194485b04bSFam Zheng Docker().clean() 3204485b04bSFam Zheng return 0 3214485b04bSFam Zheng 3224b08af60SFam Zhengclass ImagesCommand(SubCommand): 3234b08af60SFam Zheng """Run "docker images" command""" 3244b08af60SFam Zheng name = "images" 3254b08af60SFam Zheng def run(self, args, argv): 3264b08af60SFam Zheng return Docker().command("images", argv, args.quiet) 3274b08af60SFam Zheng 3284485b04bSFam Zhengdef main(): 3294485b04bSFam Zheng parser = argparse.ArgumentParser(description="A Docker helper", 3304485b04bSFam Zheng usage="%s <subcommand> ..." % os.path.basename(sys.argv[0])) 3314485b04bSFam Zheng subparsers = parser.add_subparsers(title="subcommands", help=None) 3324485b04bSFam Zheng for cls in SubCommand.__subclasses__(): 3334485b04bSFam Zheng cmd = cls() 3344485b04bSFam Zheng subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) 3354485b04bSFam Zheng cmd.shared_args(subp) 3364485b04bSFam Zheng cmd.args(subp) 3374485b04bSFam Zheng subp.set_defaults(cmdobj=cmd) 3384485b04bSFam Zheng args, argv = parser.parse_known_args() 3394485b04bSFam Zheng return args.cmdobj.run(args, argv) 3404485b04bSFam Zheng 3414485b04bSFam Zhengif __name__ == "__main__": 3424485b04bSFam Zheng sys.exit(main()) 343