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