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 23from shutil import copy 24 25def _text_checksum(text): 26 """Calculate a digest string unique to the text content""" 27 return hashlib.sha1(text).hexdigest() 28 29def _guess_docker_command(): 30 """ Guess a working docker command or raise exception if not found""" 31 commands = [["docker"], ["sudo", "-n", "docker"]] 32 for cmd in commands: 33 if subprocess.call(cmd + ["images"], 34 stdout=subprocess.PIPE, 35 stderr=subprocess.PIPE) == 0: 36 return cmd 37 commands_txt = "\n".join([" " + " ".join(x) for x in commands]) 38 raise Exception("Cannot find working docker command. Tried:\n%s" % \ 39 commands_txt) 40 41class Docker(object): 42 """ Running Docker commands """ 43 def __init__(self): 44 self._command = _guess_docker_command() 45 self._instances = [] 46 atexit.register(self._kill_instances) 47 48 def _do(self, cmd, quiet=True, **kwargs): 49 if quiet: 50 kwargs["stdout"] = subprocess.PIPE 51 return subprocess.call(self._command + cmd, **kwargs) 52 53 def _do_kill_instances(self, only_known, only_active=True): 54 cmd = ["ps", "-q"] 55 if not only_active: 56 cmd.append("-a") 57 for i in self._output(cmd).split(): 58 resp = self._output(["inspect", i]) 59 labels = json.loads(resp)[0]["Config"]["Labels"] 60 active = json.loads(resp)[0]["State"]["Running"] 61 if not labels: 62 continue 63 instance_uuid = labels.get("com.qemu.instance.uuid", None) 64 if not instance_uuid: 65 continue 66 if only_known and instance_uuid not in self._instances: 67 continue 68 print "Terminating", i 69 if active: 70 self._do(["kill", i]) 71 self._do(["rm", i]) 72 73 def clean(self): 74 self._do_kill_instances(False, False) 75 return 0 76 77 def _kill_instances(self): 78 return self._do_kill_instances(True) 79 80 def _output(self, cmd, **kwargs): 81 return subprocess.check_output(self._command + cmd, 82 stderr=subprocess.STDOUT, 83 **kwargs) 84 85 def get_image_dockerfile_checksum(self, tag): 86 resp = self._output(["inspect", tag]) 87 labels = json.loads(resp)[0]["Config"].get("Labels", {}) 88 return labels.get("com.qemu.dockerfile-checksum", "") 89 90 def build_image(self, tag, dockerfile, df_path, quiet=True, argv=None): 91 if argv == None: 92 argv = [] 93 tmp_dir = tempfile.mkdtemp(prefix="docker_build") 94 95 tmp_df = tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".docker") 96 tmp_df.write(dockerfile) 97 98 tmp_df.write("\n") 99 tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % 100 _text_checksum(dockerfile)) 101 tmp_df.flush() 102 self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \ 103 [tmp_dir], 104 quiet=quiet) 105 106 def image_matches_dockerfile(self, tag, dockerfile): 107 try: 108 checksum = self.get_image_dockerfile_checksum(tag) 109 except Exception: 110 return False 111 return checksum == _text_checksum(dockerfile) 112 113 def run(self, cmd, keep, quiet): 114 label = uuid.uuid1().hex 115 if not keep: 116 self._instances.append(label) 117 ret = self._do(["run", "--label", 118 "com.qemu.instance.uuid=" + label] + cmd, 119 quiet=quiet) 120 if not keep: 121 self._instances.remove(label) 122 return ret 123 124class SubCommand(object): 125 """A SubCommand template base class""" 126 name = None # Subcommand name 127 def shared_args(self, parser): 128 parser.add_argument("--quiet", action="store_true", 129 help="Run quietly unless an error occured") 130 131 def args(self, parser): 132 """Setup argument parser""" 133 pass 134 def run(self, args, argv): 135 """Run command. 136 args: parsed argument by argument parser. 137 argv: remaining arguments from sys.argv. 138 """ 139 pass 140 141class RunCommand(SubCommand): 142 """Invoke docker run and take care of cleaning up""" 143 name = "run" 144 def args(self, parser): 145 parser.add_argument("--keep", action="store_true", 146 help="Don't remove image when command completes") 147 def run(self, args, argv): 148 return Docker().run(argv, args.keep, quiet=args.quiet) 149 150class BuildCommand(SubCommand): 151 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>""" 152 name = "build" 153 def args(self, parser): 154 parser.add_argument("tag", 155 help="Image Tag") 156 parser.add_argument("dockerfile", 157 help="Dockerfile name") 158 159 def run(self, args, argv): 160 dockerfile = open(args.dockerfile, "rb").read() 161 tag = args.tag 162 163 dkr = Docker() 164 if dkr.image_matches_dockerfile(tag, dockerfile): 165 if not args.quiet: 166 print "Image is up to date." 167 return 0 168 169 dkr.build_image(tag, dockerfile, args.dockerfile, 170 quiet=args.quiet, argv=argv) 171 return 0 172 173class CleanCommand(SubCommand): 174 """Clean up docker instances""" 175 name = "clean" 176 def run(self, args, argv): 177 Docker().clean() 178 return 0 179 180def main(): 181 parser = argparse.ArgumentParser(description="A Docker helper", 182 usage="%s <subcommand> ..." % os.path.basename(sys.argv[0])) 183 subparsers = parser.add_subparsers(title="subcommands", help=None) 184 for cls in SubCommand.__subclasses__(): 185 cmd = cls() 186 subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) 187 cmd.shared_args(subp) 188 cmd.args(subp) 189 subp.set_defaults(cmdobj=cmd) 190 args, argv = parser.parse_known_args() 191 return args.cmdobj.run(args, argv) 192 193if __name__ == "__main__": 194 sys.exit(main()) 195