3 # Docker controlling module
5 # Copyright (c) 2016 Red Hat Inc.
8 # Fam Zheng <famz@redhat.com>
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.
25 from tarfile
import TarFile
, TarInfo
26 from StringIO
import StringIO
27 from shutil
import copy
, rmtree
30 DEVNULL
= open(os
.devnull
, 'wb')
33 def _text_checksum(text
):
34 """Calculate a digest string unique to the text content"""
35 return hashlib
.sha1(text
).hexdigest()
37 def _guess_docker_command():
38 """ Guess a working docker command or raise exception if not found"""
39 commands
= [["docker"], ["sudo", "-n", "docker"]]
42 if subprocess
.call(cmd
+ ["images"],
43 stdout
=DEVNULL
, stderr
=DEVNULL
) == 0:
47 commands_txt
= "\n".join([" " + " ".join(x
) for x
in commands
])
48 raise Exception("Cannot find working docker command. Tried:\n%s" % \
51 def _copy_with_mkdir(src
, root_dir
, sub_path
):
52 """Copy src into root_dir, creating sub_path as needed."""
53 dest_dir
= os
.path
.normpath("%s/%s" % (root_dir
, sub_path
))
57 # we can safely ignore already created directories
60 dest_file
= "%s/%s" % (dest_dir
, os
.path
.basename(src
))
64 def _get_so_libs(executable
):
65 """Return a list of libraries associated with an executable.
67 The paths may be symbolic links which would need to be resolved to
68 ensure theright data is copied."""
71 ldd_re
= re
.compile(r
"(/.*/)(\S*)")
73 ldd_output
= subprocess
.check_output(["ldd", executable
])
74 for line
in ldd_output
.split("\n"):
75 search
= ldd_re
.search(line
)
76 if search
and len(search
.groups()) == 2:
77 so_path
= search
.groups()[0]
78 so_lib
= search
.groups()[1]
79 libs
.append("%s/%s" % (so_path
, so_lib
))
80 except subprocess
.CalledProcessError
:
81 print "%s had no associated libraries (static build?)" % (executable
)
85 def _copy_binary_with_libs(src
, dest_dir
):
86 """Copy a binary executable and all its dependant libraries.
88 This does rely on the host file-system being fairly multi-arch
89 aware so the file don't clash with the guests layout."""
91 _copy_with_mkdir(src
, dest_dir
, "/usr/bin")
93 libs
= _get_so_libs(src
)
96 so_path
= os
.path
.dirname(l
)
97 _copy_with_mkdir(l
, dest_dir
, so_path
)
100 """ Running Docker commands """
102 self
._command
= _guess_docker_command()
104 atexit
.register(self
._kill
_instances
)
105 signal
.signal(signal
.SIGTERM
, self
._kill
_instances
)
106 signal
.signal(signal
.SIGHUP
, self
._kill
_instances
)
108 def _do(self
, cmd
, quiet
=True, infile
=None, **kwargs
):
110 kwargs
["stdout"] = DEVNULL
112 kwargs
["stdin"] = infile
113 return subprocess
.call(self
._command
+ cmd
, **kwargs
)
115 def _do_kill_instances(self
, only_known
, only_active
=True):
119 for i
in self
._output
(cmd
).split():
120 resp
= self
._output
(["inspect", i
])
121 labels
= json
.loads(resp
)[0]["Config"]["Labels"]
122 active
= json
.loads(resp
)[0]["State"]["Running"]
125 instance_uuid
= labels
.get("com.qemu.instance.uuid", None)
126 if not instance_uuid
:
128 if only_known
and instance_uuid
not in self
._instances
:
130 print "Terminating", i
132 self
._do
(["kill", i
])
136 self
._do
_kill
_instances
(False, False)
139 def _kill_instances(self
, *args
, **kwargs
):
140 return self
._do
_kill
_instances
(True)
142 def _output(self
, cmd
, **kwargs
):
143 return subprocess
.check_output(self
._command
+ cmd
,
144 stderr
=subprocess
.STDOUT
,
147 def get_image_dockerfile_checksum(self
, tag
):
148 resp
= self
._output
(["inspect", tag
])
149 labels
= json
.loads(resp
)[0]["Config"].get("Labels", {})
150 return labels
.get("com.qemu.dockerfile-checksum", "")
152 def build_image(self
, tag
, docker_dir
, dockerfile
, quiet
=True, argv
=None):
156 tmp_df
= tempfile
.NamedTemporaryFile(dir=docker_dir
, suffix
=".docker")
157 tmp_df
.write(dockerfile
)
160 tmp_df
.write("LABEL com.qemu.dockerfile-checksum=%s" %
161 _text_checksum(dockerfile
))
164 self
._do
(["build", "-t", tag
, "-f", tmp_df
.name
] + argv
+ \
168 def update_image(self
, tag
, tarball
, quiet
=True):
169 "Update a tagged image using "
171 self
._do
(["build", "-t", tag
, "-"], quiet
=quiet
, infile
=tarball
)
173 def image_matches_dockerfile(self
, tag
, dockerfile
):
175 checksum
= self
.get_image_dockerfile_checksum(tag
)
178 return checksum
== _text_checksum(dockerfile
)
180 def run(self
, cmd
, keep
, quiet
):
181 label
= uuid
.uuid1().hex
183 self
._instances
.append(label
)
184 ret
= self
._do
(["run", "--label",
185 "com.qemu.instance.uuid=" + label
] + cmd
,
188 self
._instances
.remove(label
)
191 def command(self
, cmd
, argv
, quiet
):
192 return self
._do
([cmd
] + argv
, quiet
=quiet
)
194 class SubCommand(object):
195 """A SubCommand template base class"""
196 name
= None # Subcommand name
197 def shared_args(self
, parser
):
198 parser
.add_argument("--quiet", action
="store_true",
199 help="Run quietly unless an error occured")
201 def args(self
, parser
):
202 """Setup argument parser"""
204 def run(self
, args
, argv
):
206 args: parsed argument by argument parser.
207 argv: remaining arguments from sys.argv.
211 class RunCommand(SubCommand
):
212 """Invoke docker run and take care of cleaning up"""
214 def args(self
, parser
):
215 parser
.add_argument("--keep", action
="store_true",
216 help="Don't remove image when command completes")
217 def run(self
, args
, argv
):
218 return Docker().run(argv
, args
.keep
, quiet
=args
.quiet
)
220 class BuildCommand(SubCommand
):
221 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
223 def args(self
, parser
):
224 parser
.add_argument("--include-executable", "-e",
225 help="""Specify a binary that will be copied to the
226 container together with all its dependent
228 parser
.add_argument("tag",
230 parser
.add_argument("dockerfile",
231 help="Dockerfile name")
233 def run(self
, args
, argv
):
234 dockerfile
= open(args
.dockerfile
, "rb").read()
238 if dkr
.image_matches_dockerfile(tag
, dockerfile
):
240 print "Image is up to date."
242 # Create a docker context directory for the build
243 docker_dir
= tempfile
.mkdtemp(prefix
="docker_build")
245 # Is there a .pre file to run in the build context?
246 docker_pre
= os
.path
.splitext(args
.dockerfile
)[0]+".pre"
247 if os
.path
.exists(docker_pre
):
248 stdout
= DEVNULL
if args
.quiet
else None
249 rc
= subprocess
.call(os
.path
.realpath(docker_pre
),
250 cwd
=docker_dir
, stdout
=stdout
)
255 print "%s exited with code %d" % (docker_pre
, rc
)
258 # Do we include a extra binary?
259 if args
.include_executable
:
260 _copy_binary_with_libs(args
.include_executable
,
263 dkr
.build_image(tag
, docker_dir
, dockerfile
,
264 quiet
=args
.quiet
, argv
=argv
)
270 class UpdateCommand(SubCommand
):
271 """ Update a docker image with new executables. Arguments: <tag> <executable>"""
273 def args(self
, parser
):
274 parser
.add_argument("tag",
276 parser
.add_argument("executable",
277 help="Executable to copy")
279 def run(self
, args
, argv
):
280 # Create a temporary tarball with our whole build context and
281 # dockerfile for the update
282 tmp
= tempfile
.NamedTemporaryFile(suffix
="dckr.tar.gz")
283 tmp_tar
= TarFile(fileobj
=tmp
, mode
='w')
285 # Add the executable to the tarball
286 bn
= os
.path
.basename(args
.executable
)
287 ff
= "/usr/bin/%s" % bn
288 tmp_tar
.add(args
.executable
, arcname
=ff
)
290 # Add any associated libraries
291 libs
= _get_so_libs(args
.executable
)
294 tmp_tar
.add(os
.path
.realpath(l
), arcname
=l
)
296 # Create a Docker buildfile
298 df
.write("FROM %s\n" % args
.tag
)
299 df
.write("ADD . /\n")
302 df_tar
= TarInfo(name
="Dockerfile")
303 df_tar
.size
= len(df
.buf
)
304 tmp_tar
.addfile(df_tar
, fileobj
=df
)
308 # reset the file pointers
312 # Run the build with our tarball context
314 dkr
.update_image(args
.tag
, tmp
, quiet
=args
.quiet
)
318 class CleanCommand(SubCommand
):
319 """Clean up docker instances"""
321 def run(self
, args
, argv
):
325 class ImagesCommand(SubCommand
):
326 """Run "docker images" command"""
328 def run(self
, args
, argv
):
329 return Docker().command("images", argv
, args
.quiet
)
332 parser
= argparse
.ArgumentParser(description
="A Docker helper",
333 usage
="%s <subcommand> ..." % os
.path
.basename(sys
.argv
[0]))
334 subparsers
= parser
.add_subparsers(title
="subcommands", help=None)
335 for cls
in SubCommand
.__subclasses
__():
337 subp
= subparsers
.add_parser(cmd
.name
, help=cmd
.__doc
__)
338 cmd
.shared_args(subp
)
340 subp
.set_defaults(cmdobj
=cmd
)
341 args
, argv
= parser
.parse_known_args()
342 return args
.cmdobj
.run(args
, argv
)
344 if __name__
== "__main__":