195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
|
#!/usr/bin/env python2
|
||
|
#
|
||
|
# Docker controlling module
|
||
|
#
|
||
|
# Copyright (c) 2016 Red Hat Inc.
|
||
|
#
|
||
|
# Authors:
|
||
|
# Fam Zheng <famz@redhat.com>
|
||
|
#
|
||
|
# This work is licensed under the terms of the GNU GPL, version 2
|
||
|
# or (at your option) any later version. See the COPYING file in
|
||
|
# the top-level directory.
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import subprocess
|
||
|
import json
|
||
|
import hashlib
|
||
|
import atexit
|
||
|
import uuid
|
||
|
import argparse
|
||
|
import tempfile
|
||
|
from shutil import copy
|
||
|
|
||
|
def _text_checksum(text):
|
||
|
"""Calculate a digest string unique to the text content"""
|
||
|
return hashlib.sha1(text).hexdigest()
|
||
|
|
||
|
def _guess_docker_command():
|
||
|
""" Guess a working docker command or raise exception if not found"""
|
||
|
commands = [["docker"], ["sudo", "-n", "docker"]]
|
||
|
for cmd in commands:
|
||
|
if subprocess.call(cmd + ["images"],
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE) == 0:
|
||
|
return cmd
|
||
|
commands_txt = "\n".join([" " + " ".join(x) for x in commands])
|
||
|
raise Exception("Cannot find working docker command. Tried:\n%s" % \
|
||
|
commands_txt)
|
||
|
|
||
|
class Docker(object):
|
||
|
""" Running Docker commands """
|
||
|
def __init__(self):
|
||
|
self._command = _guess_docker_command()
|
||
|
self._instances = []
|
||
|
atexit.register(self._kill_instances)
|
||
|
|
||
|
def _do(self, cmd, quiet=True, **kwargs):
|
||
|
if quiet:
|
||
|
kwargs["stdout"] = subprocess.PIPE
|
||
|
return subprocess.call(self._command + cmd, **kwargs)
|
||
|
|
||
|
def _do_kill_instances(self, only_known, only_active=True):
|
||
|
cmd = ["ps", "-q"]
|
||
|
if not only_active:
|
||
|
cmd.append("-a")
|
||
|
for i in self._output(cmd).split():
|
||
|
resp = self._output(["inspect", i])
|
||
|
labels = json.loads(resp)[0]["Config"]["Labels"]
|
||
|
active = json.loads(resp)[0]["State"]["Running"]
|
||
|
if not labels:
|
||
|
continue
|
||
|
instance_uuid = labels.get("com.qemu.instance.uuid", None)
|
||
|
if not instance_uuid:
|
||
|
continue
|
||
|
if only_known and instance_uuid not in self._instances:
|
||
|
continue
|
||
|
print "Terminating", i
|
||
|
if active:
|
||
|
self._do(["kill", i])
|
||
|
self._do(["rm", i])
|
||
|
|
||
|
def clean(self):
|
||
|
self._do_kill_instances(False, False)
|
||
|
return 0
|
||
|
|
||
|
def _kill_instances(self):
|
||
|
return self._do_kill_instances(True)
|
||
|
|
||
|
def _output(self, cmd, **kwargs):
|
||
|
return subprocess.check_output(self._command + cmd,
|
||
|
stderr=subprocess.STDOUT,
|
||
|
**kwargs)
|
||
|
|
||
|
def get_image_dockerfile_checksum(self, tag):
|
||
|
resp = self._output(["inspect", tag])
|
||
|
labels = json.loads(resp)[0]["Config"].get("Labels", {})
|
||
|
return labels.get("com.qemu.dockerfile-checksum", "")
|
||
|
|
||
|
def build_image(self, tag, dockerfile, df_path, quiet=True, argv=None):
|
||
|
if argv == None:
|
||
|
argv = []
|
||
|
tmp_dir = tempfile.mkdtemp(prefix="docker_build")
|
||
|
|
||
|
tmp_df = tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".docker")
|
||
|
tmp_df.write(dockerfile)
|
||
|
|
||
|
tmp_df.write("\n")
|
||
|
tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
|
||
|
_text_checksum(dockerfile))
|
||
|
tmp_df.flush()
|
||
|
self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \
|
||
|
[tmp_dir],
|
||
|
quiet=quiet)
|
||
|
|
||
|
def image_matches_dockerfile(self, tag, dockerfile):
|
||
|
try:
|
||
|
checksum = self.get_image_dockerfile_checksum(tag)
|
||
|
except Exception:
|
||
|
return False
|
||
|
return checksum == _text_checksum(dockerfile)
|
||
|
|
||
|
def run(self, cmd, keep, quiet):
|
||
|
label = uuid.uuid1().hex
|
||
|
if not keep:
|
||
|
self._instances.append(label)
|
||
|
ret = self._do(["run", "--label",
|
||
|
"com.qemu.instance.uuid=" + label] + cmd,
|
||
|
quiet=quiet)
|
||
|
if not keep:
|
||
|
self._instances.remove(label)
|
||
|
return ret
|
||
|
|
||
|
class SubCommand(object):
|
||
|
"""A SubCommand template base class"""
|
||
|
name = None # Subcommand name
|
||
|
def shared_args(self, parser):
|
||
|
parser.add_argument("--quiet", action="store_true",
|
||
|
help="Run quietly unless an error occured")
|
||
|
|
||
|
def args(self, parser):
|
||
|
"""Setup argument parser"""
|
||
|
pass
|
||
|
def run(self, args, argv):
|
||
|
"""Run command.
|
||
|
args: parsed argument by argument parser.
|
||
|
argv: remaining arguments from sys.argv.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
class RunCommand(SubCommand):
|
||
|
"""Invoke docker run and take care of cleaning up"""
|
||
|
name = "run"
|
||
|
def args(self, parser):
|
||
|
parser.add_argument("--keep", action="store_true",
|
||
|
help="Don't remove image when command completes")
|
||
|
def run(self, args, argv):
|
||
|
return Docker().run(argv, args.keep, quiet=args.quiet)
|
||
|
|
||
|
class BuildCommand(SubCommand):
|
||
|
""" Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
|
||
|
name = "build"
|
||
|
def args(self, parser):
|
||
|
parser.add_argument("tag",
|
||
|
help="Image Tag")
|
||
|
parser.add_argument("dockerfile",
|
||
|
help="Dockerfile name")
|
||
|
|
||
|
def run(self, args, argv):
|
||
|
dockerfile = open(args.dockerfile, "rb").read()
|
||
|
tag = args.tag
|
||
|
|
||
|
dkr = Docker()
|
||
|
if dkr.image_matches_dockerfile(tag, dockerfile):
|
||
|
if not args.quiet:
|
||
|
print "Image is up to date."
|
||
|
return 0
|
||
|
|
||
|
dkr.build_image(tag, dockerfile, args.dockerfile,
|
||
|
quiet=args.quiet, argv=argv)
|
||
|
return 0
|
||
|
|
||
|
class CleanCommand(SubCommand):
|
||
|
"""Clean up docker instances"""
|
||
|
name = "clean"
|
||
|
def run(self, args, argv):
|
||
|
Docker().clean()
|
||
|
return 0
|
||
|
|
||
|
def main():
|
||
|
parser = argparse.ArgumentParser(description="A Docker helper",
|
||
|
usage="%s <subcommand> ..." % os.path.basename(sys.argv[0]))
|
||
|
subparsers = parser.add_subparsers(title="subcommands", help=None)
|
||
|
for cls in SubCommand.__subclasses__():
|
||
|
cmd = cls()
|
||
|
subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
|
||
|
cmd.shared_args(subp)
|
||
|
cmd.args(subp)
|
||
|
subp.set_defaults(cmdobj=cmd)
|
||
|
args, argv = parser.parse_known_args()
|
||
|
return args.cmdobj.run(args, argv)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
sys.exit(main())
|