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.
14 from __future__
import print_function
17 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
),
18 '..', '..', 'scripts'))
28 from tarfile
import TarFile
, TarInfo
29 from StringIO
import StringIO
30 from shutil
import copy
, rmtree
31 from pwd
import getpwuid
34 FILTERED_ENV_NAMES
= ['ftp_proxy', 'http_proxy', 'https_proxy']
37 DEVNULL
= open(os
.devnull
, 'wb')
40 def _text_checksum(text
):
41 """Calculate a digest string unique to the text content"""
42 return hashlib
.sha1(text
).hexdigest()
44 def _file_checksum(filename
):
45 return _text_checksum(open(filename
, 'rb').read())
47 def _guess_docker_command():
48 """ Guess a working docker command or raise exception if not found"""
49 commands
= [["docker"], ["sudo", "-n", "docker"]]
52 if subprocess
.call(cmd
+ ["images"],
53 stdout
=DEVNULL
, stderr
=DEVNULL
) == 0:
57 commands_txt
= "\n".join([" " + " ".join(x
) for x
in commands
])
58 raise Exception("Cannot find working docker command. Tried:\n%s" % \
61 def _copy_with_mkdir(src
, root_dir
, sub_path
='.'):
62 """Copy src into root_dir, creating sub_path as needed."""
63 dest_dir
= os
.path
.normpath("%s/%s" % (root_dir
, sub_path
))
67 # we can safely ignore already created directories
70 dest_file
= "%s/%s" % (dest_dir
, os
.path
.basename(src
))
74 def _get_so_libs(executable
):
75 """Return a list of libraries associated with an executable.
77 The paths may be symbolic links which would need to be resolved to
78 ensure theright data is copied."""
81 ldd_re
= re
.compile(r
"(/.*/)(\S*)")
83 ldd_output
= subprocess
.check_output(["ldd", executable
])
84 for line
in ldd_output
.split("\n"):
85 search
= ldd_re
.search(line
)
86 if search
and len(search
.groups()) == 2:
87 so_path
= search
.groups()[0]
88 so_lib
= search
.groups()[1]
89 libs
.append("%s/%s" % (so_path
, so_lib
))
90 except subprocess
.CalledProcessError
:
91 print("%s had no associated libraries (static build?)" % (executable
))
95 def _copy_binary_with_libs(src
, dest_dir
):
96 """Copy a binary executable and all its dependant libraries.
98 This does rely on the host file-system being fairly multi-arch
99 aware so the file don't clash with the guests layout."""
101 _copy_with_mkdir(src
, dest_dir
, "/usr/bin")
103 libs
= _get_so_libs(src
)
106 so_path
= os
.path
.dirname(l
)
107 _copy_with_mkdir(l
, dest_dir
, so_path
)
109 def _read_qemu_dockerfile(img_name
):
110 df
= os
.path
.join(os
.path
.dirname(__file__
), "dockerfiles",
111 img_name
+ ".docker")
112 return open(df
, "r").read()
114 def _dockerfile_preprocess(df
):
116 for l
in df
.splitlines():
117 if len(l
.strip()) == 0 or l
.startswith("#"):
119 from_pref
= "FROM qemu:"
120 if l
.startswith(from_pref
):
121 # TODO: Alternatively we could replace this line with "FROM $ID"
122 # where $ID is the image's hex id obtained with
123 # $ docker images $IMAGE --format="{{.Id}}"
124 # but unfortunately that's not supported by RHEL 7.
125 inlining
= _read_qemu_dockerfile(l
[len(from_pref
):])
126 out
+= _dockerfile_preprocess(inlining
)
131 class Docker(object):
132 """ Running Docker commands """
134 self
._command
= _guess_docker_command()
136 atexit
.register(self
._kill
_instances
)
137 signal
.signal(signal
.SIGTERM
, self
._kill
_instances
)
138 signal
.signal(signal
.SIGHUP
, self
._kill
_instances
)
140 def _do(self
, cmd
, quiet
=True, **kwargs
):
142 kwargs
["stdout"] = DEVNULL
143 return subprocess
.call(self
._command
+ cmd
, **kwargs
)
145 def _do_check(self
, cmd
, quiet
=True, **kwargs
):
147 kwargs
["stdout"] = DEVNULL
148 return subprocess
.check_call(self
._command
+ cmd
, **kwargs
)
150 def _do_kill_instances(self
, only_known
, only_active
=True):
154 for i
in self
._output
(cmd
).split():
155 resp
= self
._output
(["inspect", i
])
156 labels
= json
.loads(resp
)[0]["Config"]["Labels"]
157 active
= json
.loads(resp
)[0]["State"]["Running"]
160 instance_uuid
= labels
.get("com.qemu.instance.uuid", None)
161 if not instance_uuid
:
163 if only_known
and instance_uuid
not in self
._instances
:
165 print("Terminating", i
)
167 self
._do
(["kill", i
])
171 self
._do
_kill
_instances
(False, False)
174 def _kill_instances(self
, *args
, **kwargs
):
175 return self
._do
_kill
_instances
(True)
177 def _output(self
, cmd
, **kwargs
):
178 return subprocess
.check_output(self
._command
+ cmd
,
179 stderr
=subprocess
.STDOUT
,
182 def get_image_dockerfile_checksum(self
, tag
):
183 resp
= self
._output
(["inspect", tag
])
184 labels
= json
.loads(resp
)[0]["Config"].get("Labels", {})
185 return labels
.get("com.qemu.dockerfile-checksum", "")
187 def build_image(self
, tag
, docker_dir
, dockerfile
,
188 quiet
=True, user
=False, argv
=None, extra_files_cksum
=[]):
192 tmp_df
= tempfile
.NamedTemporaryFile(dir=docker_dir
, suffix
=".docker")
193 tmp_df
.write(dockerfile
)
197 uname
= getpwuid(uid
).pw_name
199 tmp_df
.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
203 tmp_df
.write("LABEL com.qemu.dockerfile-checksum=%s" %
204 _text_checksum("\n".join([dockerfile
] +
208 self
._do
_check
(["build", "-t", tag
, "-f", tmp_df
.name
] + argv
+ \
212 def update_image(self
, tag
, tarball
, quiet
=True):
213 "Update a tagged image using "
215 self
._do
_check
(["build", "-t", tag
, "-"], quiet
=quiet
, stdin
=tarball
)
217 def image_matches_dockerfile(self
, tag
, dockerfile
):
219 checksum
= self
.get_image_dockerfile_checksum(tag
)
222 return checksum
== _text_checksum(_dockerfile_preprocess(dockerfile
))
224 def run(self
, cmd
, keep
, quiet
):
225 label
= uuid
.uuid1().hex
227 self
._instances
.append(label
)
228 ret
= self
._do
_check
(["run", "--label",
229 "com.qemu.instance.uuid=" + label
] + cmd
,
232 self
._instances
.remove(label
)
235 def command(self
, cmd
, argv
, quiet
):
236 return self
._do
([cmd
] + argv
, quiet
=quiet
)
238 class SubCommand(object):
239 """A SubCommand template base class"""
240 name
= None # Subcommand name
241 def shared_args(self
, parser
):
242 parser
.add_argument("--quiet", action
="store_true",
243 help="Run quietly unless an error occured")
245 def args(self
, parser
):
246 """Setup argument parser"""
248 def run(self
, args
, argv
):
250 args: parsed argument by argument parser.
251 argv: remaining arguments from sys.argv.
255 class RunCommand(SubCommand
):
256 """Invoke docker run and take care of cleaning up"""
258 def args(self
, parser
):
259 parser
.add_argument("--keep", action
="store_true",
260 help="Don't remove image when command completes")
261 def run(self
, args
, argv
):
262 return Docker().run(argv
, args
.keep
, quiet
=args
.quiet
)
264 class BuildCommand(SubCommand
):
265 """ Build docker image out of a dockerfile. Arguments: <tag> <dockerfile>"""
267 def args(self
, parser
):
268 parser
.add_argument("--include-executable", "-e",
269 help="""Specify a binary that will be copied to the
270 container together with all its dependent
272 parser
.add_argument("--extra-files", "-f", nargs
='*',
273 help="""Specify files that will be copied in the
274 Docker image, fulfilling the ADD directive from the
276 parser
.add_argument("--add-current-user", "-u", dest
="user",
278 help="Add the current user to image's passwd")
279 parser
.add_argument("tag",
281 parser
.add_argument("dockerfile",
282 help="Dockerfile name")
284 def run(self
, args
, argv
):
285 dockerfile
= open(args
.dockerfile
, "rb").read()
289 if "--no-cache" not in argv
and \
290 dkr
.image_matches_dockerfile(tag
, dockerfile
):
292 print("Image is up to date.")
294 # Create a docker context directory for the build
295 docker_dir
= tempfile
.mkdtemp(prefix
="docker_build")
297 # Is there a .pre file to run in the build context?
298 docker_pre
= os
.path
.splitext(args
.dockerfile
)[0]+".pre"
299 if os
.path
.exists(docker_pre
):
300 stdout
= DEVNULL
if args
.quiet
else None
301 rc
= subprocess
.call(os
.path
.realpath(docker_pre
),
302 cwd
=docker_dir
, stdout
=stdout
)
307 print("%s exited with code %d" % (docker_pre
, rc
))
310 # Copy any extra files into the Docker context. These can be
311 # included by the use of the ADD directive in the Dockerfile.
313 if args
.include_executable
:
314 # FIXME: there is no checksum of this executable and the linked
315 # libraries, once the image built any change of this executable
316 # or any library won't trigger another build.
317 _copy_binary_with_libs(args
.include_executable
, docker_dir
)
318 for filename
in args
.extra_files
or []:
319 _copy_with_mkdir(filename
, docker_dir
)
320 cksum
+= [_file_checksum(filename
)]
322 argv
+= ["--build-arg=" + k
.lower() + "=" + v
323 for k
, v
in os
.environ
.iteritems()
324 if k
.lower() in FILTERED_ENV_NAMES
]
325 dkr
.build_image(tag
, docker_dir
, dockerfile
,
326 quiet
=args
.quiet
, user
=args
.user
, argv
=argv
,
327 extra_files_cksum
=cksum
)
333 class UpdateCommand(SubCommand
):
334 """ Update a docker image with new executables. Arguments: <tag> <executable>"""
336 def args(self
, parser
):
337 parser
.add_argument("tag",
339 parser
.add_argument("executable",
340 help="Executable to copy")
342 def run(self
, args
, argv
):
343 # Create a temporary tarball with our whole build context and
344 # dockerfile for the update
345 tmp
= tempfile
.NamedTemporaryFile(suffix
="dckr.tar.gz")
346 tmp_tar
= TarFile(fileobj
=tmp
, mode
='w')
348 # Add the executable to the tarball
349 bn
= os
.path
.basename(args
.executable
)
350 ff
= "/usr/bin/%s" % bn
351 tmp_tar
.add(args
.executable
, arcname
=ff
)
353 # Add any associated libraries
354 libs
= _get_so_libs(args
.executable
)
357 tmp_tar
.add(os
.path
.realpath(l
), arcname
=l
)
359 # Create a Docker buildfile
361 df
.write("FROM %s\n" % args
.tag
)
362 df
.write("ADD . /\n")
365 df_tar
= TarInfo(name
="Dockerfile")
366 df_tar
.size
= len(df
.buf
)
367 tmp_tar
.addfile(df_tar
, fileobj
=df
)
371 # reset the file pointers
375 # Run the build with our tarball context
377 dkr
.update_image(args
.tag
, tmp
, quiet
=args
.quiet
)
381 class CleanCommand(SubCommand
):
382 """Clean up docker instances"""
384 def run(self
, args
, argv
):
388 class ImagesCommand(SubCommand
):
389 """Run "docker images" command"""
391 def run(self
, args
, argv
):
392 return Docker().command("images", argv
, args
.quiet
)
395 class ProbeCommand(SubCommand
):
396 """Probe if we can run docker automatically"""
399 def run(self
, args
, argv
):
402 if docker
._command
[0] == "docker":
404 elif docker
._command
[0] == "sudo":
413 parser
= argparse
.ArgumentParser(description
="A Docker helper",
414 usage
="%s <subcommand> ..." % os
.path
.basename(sys
.argv
[0]))
415 subparsers
= parser
.add_subparsers(title
="subcommands", help=None)
416 for cls
in SubCommand
.__subclasses
__():
418 subp
= subparsers
.add_parser(cmd
.name
, help=cmd
.__doc
__)
419 cmd
.shared_args(subp
)
421 subp
.set_defaults(cmdobj
=cmd
)
422 args
, argv
= parser
.parse_known_args()
423 return args
.cmdobj
.run(args
, argv
)
425 if __name__
== "__main__":