]>
Commit | Line | Data |
---|---|---|
4485b04b FZ |
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 | ||
14 | import os | |
15 | import sys | |
16 | import subprocess | |
17 | import json | |
18 | import hashlib | |
19 | import atexit | |
20 | import uuid | |
21 | import argparse | |
22 | import tempfile | |
504ca3c2 | 23 | import re |
a9f8d038 | 24 | from shutil import copy, rmtree |
4485b04b FZ |
25 | |
26 | def _text_checksum(text): | |
27 | """Calculate a digest string unique to the text content""" | |
28 | return hashlib.sha1(text).hexdigest() | |
29 | ||
30 | def _guess_docker_command(): | |
31 | """ Guess a working docker command or raise exception if not found""" | |
32 | commands = [["docker"], ["sudo", "-n", "docker"]] | |
33 | for cmd in commands: | |
34 | if subprocess.call(cmd + ["images"], | |
35 | stdout=subprocess.PIPE, | |
36 | stderr=subprocess.PIPE) == 0: | |
37 | return cmd | |
38 | commands_txt = "\n".join([" " + " ".join(x) for x in commands]) | |
39 | raise Exception("Cannot find working docker command. Tried:\n%s" % \ | |
40 | commands_txt) | |
41 | ||
504ca3c2 AB |
42 | def _copy_with_mkdir(src, root_dir, sub_path): |
43 | """Copy src into root_dir, creating sub_path as needed.""" | |
44 | dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path)) | |
45 | try: | |
46 | os.makedirs(dest_dir) | |
47 | except OSError: | |
48 | # we can safely ignore already created directories | |
49 | pass | |
50 | ||
51 | dest_file = "%s/%s" % (dest_dir, os.path.basename(src)) | |
52 | copy(src, dest_file) | |
53 | ||
54 | ||
55 | def _get_so_libs(executable): | |
56 | """Return a list of libraries associated with an executable. | |
57 | ||
58 | The paths may be symbolic links which would need to be resolved to | |
59 | ensure theright data is copied.""" | |
60 | ||
61 | libs = [] | |
62 | ldd_re = re.compile(r"(/.*/)(\S*)") | |
63 | try: | |
64 | ldd_output = subprocess.check_output(["ldd", executable]) | |
65 | for line in ldd_output.split("\n"): | |
66 | search = ldd_re.search(line) | |
67 | if search and len(search.groups()) == 2: | |
68 | so_path = search.groups()[0] | |
69 | so_lib = search.groups()[1] | |
70 | libs.append("%s/%s" % (so_path, so_lib)) | |
71 | except subprocess.CalledProcessError: | |
72 | print "%s had no associated libraries (static build?)" % (executable) | |
73 | ||
74 | return libs | |
75 | ||
76 | def _copy_binary_with_libs(src, dest_dir): | |
77 | """Copy a binary executable and all its dependant libraries. | |
78 | ||
79 | This does rely on the host file-system being fairly multi-arch | |
80 | aware so the file don't clash with the guests layout.""" | |
81 | ||
82 | _copy_with_mkdir(src, dest_dir, "/usr/bin") | |
83 | ||
84 | libs = _get_so_libs(src) | |
85 | if libs: | |
86 | for l in libs: | |
87 | so_path = os.path.dirname(l) | |
88 | _copy_with_mkdir(l , dest_dir, so_path) | |
89 | ||
4485b04b FZ |
90 | class Docker(object): |
91 | """ Running Docker commands """ | |
92 | def __init__(self): | |
93 | self._command = _guess_docker_command() | |
94 | self._instances = [] | |
95 | atexit.register(self._kill_instances) | |
96 | ||
97 | def _do(self, cmd, quiet=True, **kwargs): | |
98 | if quiet: | |
99 | kwargs["stdout"] = subprocess.PIPE | |
100 | return subprocess.call(self._command + cmd, **kwargs) | |
101 | ||
102 | def _do_kill_instances(self, only_known, only_active=True): | |
103 | cmd = ["ps", "-q"] | |
104 | if not only_active: | |
105 | cmd.append("-a") | |
106 | for i in self._output(cmd).split(): | |
107 | resp = self._output(["inspect", i]) | |
108 | labels = json.loads(resp)[0]["Config"]["Labels"] | |
109 | active = json.loads(resp)[0]["State"]["Running"] | |
110 | if not labels: | |
111 | continue | |
112 | instance_uuid = labels.get("com.qemu.instance.uuid", None) | |
113 | if not instance_uuid: | |
114 | continue | |
115 | if only_known and instance_uuid not in self._instances: | |
116 | continue | |
117 | print "Terminating", i | |
118 | if active: | |
119 | self._do(["kill", i]) | |
120 | self._do(["rm", i]) | |
121 | ||
122 | def clean(self): | |
123 | self._do_kill_instances(False, False) | |
124 | return 0 | |
125 | ||
126 | def _kill_instances(self): | |
127 | return self._do_kill_instances(True) | |
128 | ||
129 | def _output(self, cmd, **kwargs): | |
130 | return subprocess.check_output(self._command + cmd, | |
131 | stderr=subprocess.STDOUT, | |
132 | **kwargs) | |
133 | ||
134 | def get_image_dockerfile_checksum(self, tag): | |
135 | resp = self._output(["inspect", tag]) | |
136 | labels = json.loads(resp)[0]["Config"].get("Labels", {}) | |
137 | return labels.get("com.qemu.dockerfile-checksum", "") | |
138 | ||
a9f8d038 | 139 | def build_image(self, tag, docker_dir, dockerfile, quiet=True, argv=None): |
4485b04b FZ |
140 | if argv == None: |
141 | argv = [] | |
4485b04b | 142 | |
a9f8d038 | 143 | tmp_df = tempfile.NamedTemporaryFile(dir=docker_dir, suffix=".docker") |
4485b04b FZ |
144 | tmp_df.write(dockerfile) |
145 | ||
146 | tmp_df.write("\n") | |
147 | tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % | |
148 | _text_checksum(dockerfile)) | |
149 | tmp_df.flush() | |
a9f8d038 | 150 | |
4485b04b | 151 | self._do(["build", "-t", tag, "-f", tmp_df.name] + argv + \ |
a9f8d038 | 152 | [docker_dir], |
4485b04b FZ |
153 | quiet=quiet) |
154 | ||
155 | def image_matches_dockerfile(self, tag, dockerfile): | |
156 | try: | |
157 | checksum = self.get_image_dockerfile_checksum(tag) | |
158 | except Exception: | |
159 | return False | |
160 | return checksum == _text_checksum(dockerfile) | |
161 | ||
162 | def run(self, cmd, keep, quiet): | |
163 | label = uuid.uuid1().hex | |
164 | if not keep: | |
165 | self._instances.append(label) | |
166 | ret = self._do(["run", "--label", | |
167 | "com.qemu.instance.uuid=" + label] + cmd, | |
168 | quiet=quiet) | |
169 | if not keep: | |
170 | self._instances.remove(label) | |
171 | return ret | |
172 | ||
173 | class SubCommand(object): | |
174 | """A SubCommand template base class""" | |
175 | name = None # Subcommand name | |
176 | def shared_args(self, parser): | |
177 | parser.add_argument("--quiet", action="store_true", | |
178 | help="Run quietly unless an error occured") | |
179 | ||
180 | def args(self, parser): | |
181 | """Setup argument parser""" | |
182 | pass | |
183 | def run(self, args, argv): | |
184 | """Run command. | |
185 | args: parsed argument by argument parser. | |
186 | argv: remaining arguments from sys.argv. | |
187 | """ | |
188 | pass | |
189 | ||
190 | class RunCommand(SubCommand): | |
191 | """Invoke docker run and take care of cleaning up""" | |
192 | name = "run" | |
193 | def args(self, parser): | |
194 | parser.add_argument("--keep", action="store_true", | |
195 | help="Don't remove image when command completes") | |
196 | def run(self, args, argv): | |
197 | return Docker().run(argv, args.keep, quiet=args.quiet) | |
198 | ||
199 | class BuildCommand(SubCommand): | |
200 | """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>""" | |
201 | name = "build" | |
202 | def args(self, parser): | |
504ca3c2 AB |
203 | parser.add_argument("--include-executable", "-e", |
204 | help="""Specify a binary that will be copied to the | |
205 | container together with all its dependent | |
206 | libraries""") | |
4485b04b FZ |
207 | parser.add_argument("tag", |
208 | help="Image Tag") | |
209 | parser.add_argument("dockerfile", | |
210 | help="Dockerfile name") | |
211 | ||
212 | def run(self, args, argv): | |
213 | dockerfile = open(args.dockerfile, "rb").read() | |
214 | tag = args.tag | |
215 | ||
216 | dkr = Docker() | |
217 | if dkr.image_matches_dockerfile(tag, dockerfile): | |
218 | if not args.quiet: | |
219 | print "Image is up to date." | |
a9f8d038 AB |
220 | else: |
221 | # Create a docker context directory for the build | |
222 | docker_dir = tempfile.mkdtemp(prefix="docker_build") | |
223 | ||
504ca3c2 AB |
224 | # Do we include a extra binary? |
225 | if args.include_executable: | |
226 | _copy_binary_with_libs(args.include_executable, | |
227 | docker_dir) | |
228 | ||
a9f8d038 AB |
229 | dkr.build_image(tag, docker_dir, dockerfile, |
230 | quiet=args.quiet, argv=argv) | |
231 | ||
232 | rmtree(docker_dir) | |
4485b04b | 233 | |
4485b04b FZ |
234 | return 0 |
235 | ||
236 | class CleanCommand(SubCommand): | |
237 | """Clean up docker instances""" | |
238 | name = "clean" | |
239 | def run(self, args, argv): | |
240 | Docker().clean() | |
241 | return 0 | |
242 | ||
243 | def main(): | |
244 | parser = argparse.ArgumentParser(description="A Docker helper", | |
245 | usage="%s <subcommand> ..." % os.path.basename(sys.argv[0])) | |
246 | subparsers = parser.add_subparsers(title="subcommands", help=None) | |
247 | for cls in SubCommand.__subclasses__(): | |
248 | cmd = cls() | |
249 | subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) | |
250 | cmd.shared_args(subp) | |
251 | cmd.args(subp) | |
252 | subp.set_defaults(cmdobj=cmd) | |
253 | args, argv = parser.parse_known_args() | |
254 | return args.cmdobj.run(args, argv) | |
255 | ||
256 | if __name__ == "__main__": | |
257 | sys.exit(main()) |