xref: /qemu/tests/docker/docker.py (revision ddd5ed8331652cd77546c331caee49d76fffe4a4)
14112aff7SAlex Bennée#!/usr/bin/env python3
24485b04bSFam Zheng#
34485b04bSFam Zheng# Docker controlling module
44485b04bSFam Zheng#
54485b04bSFam Zheng# Copyright (c) 2016 Red Hat Inc.
64485b04bSFam Zheng#
74485b04bSFam Zheng# Authors:
84485b04bSFam Zheng#  Fam Zheng <famz@redhat.com>
94485b04bSFam Zheng#
104485b04bSFam Zheng# This work is licensed under the terms of the GNU GPL, version 2
114485b04bSFam Zheng# or (at your option) any later version. See the COPYING file in
124485b04bSFam Zheng# the top-level directory.
134485b04bSFam Zheng
144485b04bSFam Zhengimport os
154485b04bSFam Zhengimport sys
164485b04bSFam Zhengimport subprocess
174485b04bSFam Zhengimport json
184485b04bSFam Zhengimport hashlib
194485b04bSFam Zhengimport atexit
204485b04bSFam Zhengimport uuid
21ae68fdabSEduardo Habkostimport argparse
229459f754SMarc-André Lureauimport enum
234485b04bSFam Zhengimport tempfile
24504ca3c2SAlex Bennéeimport re
2597cba1a1SFam Zhengimport signal
266e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo
27e336cec3SAlex Bennéefrom io import StringIO, BytesIO
28a9f8d038SAlex Bennéefrom shutil import copy, rmtree
29414a8ce5SAlex Bennéefrom pwd import getpwuid
307b882245SAlex Bennéefrom datetime import datetime, timedelta
314485b04bSFam Zheng
32c9772570SSascha Silbe
3306cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
3406cc3551SPhilippe Mathieu-Daudé
3506cc3551SPhilippe Mathieu-Daudé
36c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb')
37c9772570SSascha Silbe
389459f754SMarc-André Lureauclass EngineEnum(enum.IntEnum):
399459f754SMarc-André Lureau    AUTO = 1
409459f754SMarc-André Lureau    DOCKER = 2
419459f754SMarc-André Lureau    PODMAN = 3
429459f754SMarc-André Lureau
439459f754SMarc-André Lureau    def __str__(self):
449459f754SMarc-André Lureau        return self.name.lower()
459459f754SMarc-André Lureau
469459f754SMarc-André Lureau    def __repr__(self):
479459f754SMarc-André Lureau        return str(self)
489459f754SMarc-André Lureau
499459f754SMarc-André Lureau    @staticmethod
509459f754SMarc-André Lureau    def argparse(s):
519459f754SMarc-André Lureau        try:
529459f754SMarc-André Lureau            return EngineEnum[s.upper()]
539459f754SMarc-André Lureau        except KeyError:
549459f754SMarc-André Lureau            return s
559459f754SMarc-André Lureau
569459f754SMarc-André Lureau
579459f754SMarc-André LureauUSE_ENGINE = EngineEnum.AUTO
58c9772570SSascha Silbe
59af509738SPaolo Bonzinidef _bytes_checksum(bytes):
60af509738SPaolo Bonzini    """Calculate a digest string unique to the text content"""
61af509738SPaolo Bonzini    return hashlib.sha1(bytes).hexdigest()
62af509738SPaolo Bonzini
634485b04bSFam Zhengdef _text_checksum(text):
644485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
65af509738SPaolo Bonzini    return _bytes_checksum(text.encode('utf-8'))
664485b04bSFam Zheng
674112aff7SAlex Bennéedef _read_dockerfile(path):
684112aff7SAlex Bennée    return open(path, 'rt', encoding='utf-8').read()
69432d8ad5SAlex Bennée
70438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
71af509738SPaolo Bonzini    return _bytes_checksum(open(filename, 'rb').read())
72438d1168SPhilippe Mathieu-Daudé
73432d8ad5SAlex Bennée
749459f754SMarc-André Lureaudef _guess_engine_command():
759459f754SMarc-André Lureau    """ Guess a working engine command or raise exception if not found"""
769459f754SMarc-André Lureau    commands = []
779459f754SMarc-André Lureau
789459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]:
799459f754SMarc-André Lureau        commands += [["podman"]]
809459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]:
819459f754SMarc-André Lureau        commands += [["docker"], ["sudo", "-n", "docker"]]
824485b04bSFam Zheng    for cmd in commands:
830679f98bSEduardo Habkost        try:
8483405c45SAlex Bennée            # docker version will return the client details in stdout
8583405c45SAlex Bennée            # but still report a status of 1 if it can't contact the daemon
8683405c45SAlex Bennée            if subprocess.call(cmd + ["version"],
87c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
884485b04bSFam Zheng                return cmd
890679f98bSEduardo Habkost        except OSError:
900679f98bSEduardo Habkost            pass
914485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
929459f754SMarc-André Lureau    raise Exception("Cannot find working engine command. Tried:\n%s" %
934485b04bSFam Zheng                    commands_txt)
944485b04bSFam Zheng
95432d8ad5SAlex Bennée
963971c70fSAlex Bennéedef _copy_with_mkdir(src, root_dir, sub_path='.', name=None):
97504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
98504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
99504ca3c2SAlex Bennée    try:
100504ca3c2SAlex Bennée        os.makedirs(dest_dir)
101504ca3c2SAlex Bennée    except OSError:
102504ca3c2SAlex Bennée        # we can safely ignore already created directories
103504ca3c2SAlex Bennée        pass
104504ca3c2SAlex Bennée
1053971c70fSAlex Bennée    dest_file = "%s/%s" % (dest_dir, name if name else os.path.basename(src))
106dffccf3dSAlex Bennée
107dffccf3dSAlex Bennée    try:
108504ca3c2SAlex Bennée        copy(src, dest_file)
109dffccf3dSAlex Bennée    except FileNotFoundError:
110dffccf3dSAlex Bennée        print("Couldn't copy %s to %s" % (src, dest_file))
111dffccf3dSAlex Bennée        pass
112504ca3c2SAlex Bennée
113504ca3c2SAlex Bennée
114504ca3c2SAlex Bennéedef _get_so_libs(executable):
115504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
116504ca3c2SAlex Bennée
117504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
118504ca3c2SAlex Bennée    ensure the right data is copied."""
119504ca3c2SAlex Bennée
120504ca3c2SAlex Bennée    libs = []
1215e33f7feSAlex Bennée    ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)")
122504ca3c2SAlex Bennée    try:
123eea2153eSAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8')
124504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
125504ca3c2SAlex Bennée            search = ldd_re.search(line)
1265e33f7feSAlex Bennée            if search:
1275e33f7feSAlex Bennée                try:
1284d8f6309SPhilippe Mathieu-Daudé                    libs.append(search.group(1))
1295e33f7feSAlex Bennée                except IndexError:
1305e33f7feSAlex Bennée                    pass
131504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
132f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
133504ca3c2SAlex Bennée
134504ca3c2SAlex Bennée    return libs
135504ca3c2SAlex Bennée
136432d8ad5SAlex Bennée
137d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
138d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
139d10404b1SAlex Bennée
140d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
141d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
142d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
143d10404b1SAlex Bennée    is in the image.
144504ca3c2SAlex Bennée
145504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
146d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
147d10404b1SAlex Bennée    """
148504ca3c2SAlex Bennée
149d10404b1SAlex Bennée    if bin_dest:
150d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
151d10404b1SAlex Bennée    else:
152d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
153504ca3c2SAlex Bennée
154504ca3c2SAlex Bennée    libs = _get_so_libs(src)
155504ca3c2SAlex Bennée    if libs:
156504ca3c2SAlex Bennée        for l in libs:
157504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
1583971c70fSAlex Bennée            name = os.path.basename(l)
1595e33f7feSAlex Bennée            real_l = os.path.realpath(l)
1603971c70fSAlex Bennée            _copy_with_mkdir(real_l, dest_dir, so_path, name)
161504ca3c2SAlex Bennée
16215352decSAlex Bennée
16315352decSAlex Bennéedef _check_binfmt_misc(executable):
16415352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
16515352decSAlex Bennée
16615352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
16715352decSAlex Bennée    this script but we should at least fail early with a useful
168d10404b1SAlex Bennée    message if it won't work.
169d10404b1SAlex Bennée
170d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
171d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
172d10404b1SAlex Bennée    libraries.
173d10404b1SAlex Bennée    """
17415352decSAlex Bennée
17515352decSAlex Bennée    binary = os.path.basename(executable)
17615352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
17715352decSAlex Bennée
17815352decSAlex Bennée    if not os.path.exists(binfmt_entry):
17915352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
180d10404b1SAlex Bennée        return None, False
18115352decSAlex Bennée
18215352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
18315352decSAlex Bennée
18443c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
185432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
18643c898b7SAlex Bennée              (binary))
187d10404b1SAlex Bennée        return None, True
18843c898b7SAlex Bennée
1897e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1907e81d198SAlex Bennée    interp = m.group(1)
1917e81d198SAlex Bennée    if interp and interp != executable:
1927e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1937e81d198SAlex Bennée              (binary, executable, interp))
19415352decSAlex Bennée
195d10404b1SAlex Bennée    return interp, True
196d10404b1SAlex Bennée
19715352decSAlex Bennée
198c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
199547cb45eSAlex Bennée    # special case for Debian linux-user images
200547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
201547cb45eSAlex Bennée        img_name = "debian-bootstrap"
202547cb45eSAlex Bennée
203c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
204c1958e9dSFam Zheng                      img_name + ".docker")
2054112aff7SAlex Bennée    return _read_dockerfile(df)
206c1958e9dSFam Zheng
207432d8ad5SAlex Bennée
208c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
209c1958e9dSFam Zheng    out = ""
210c1958e9dSFam Zheng    for l in df.splitlines():
211c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
212c1958e9dSFam Zheng            continue
213767b6bd2SAlex Bennée        from_pref = "FROM qemu/"
214c1958e9dSFam Zheng        if l.startswith(from_pref):
215c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
216c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
217c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
218c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
219c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
220c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
221c1958e9dSFam Zheng            continue
222c1958e9dSFam Zheng        out += l + "\n"
223c1958e9dSFam Zheng    return out
224c1958e9dSFam Zheng
225432d8ad5SAlex Bennée
2264485b04bSFam Zhengclass Docker(object):
2274485b04bSFam Zheng    """ Running Docker commands """
2284485b04bSFam Zheng    def __init__(self):
2299459f754SMarc-André Lureau        self._command = _guess_engine_command()
230e6f1306bSAlex Bennée
231e6f1306bSAlex Bennée        if "docker" in self._command and "TRAVIS" not in os.environ:
232e6f1306bSAlex Bennée            os.environ["DOCKER_BUILDKIT"] = "1"
233e6f1306bSAlex Bennée            self._buildkit = True
234e6f1306bSAlex Bennée        else:
235e6f1306bSAlex Bennée            self._buildkit = False
236e6f1306bSAlex Bennée
237529994e2SAlex Bennée        self._instance = None
2384485b04bSFam Zheng        atexit.register(self._kill_instances)
23997cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
24097cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
2414485b04bSFam Zheng
24258bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2434485b04bSFam Zheng        if quiet:
244c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2454485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2464485b04bSFam Zheng
2470b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2480b95ff72SFam Zheng        if quiet:
2490b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2500b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2510b95ff72SFam Zheng
2524485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2534485b04bSFam Zheng        cmd = ["ps", "-q"]
2544485b04bSFam Zheng        if not only_active:
2554485b04bSFam Zheng            cmd.append("-a")
256529994e2SAlex Bennée
257529994e2SAlex Bennée        filter = "--filter=label=com.qemu.instance.uuid"
258529994e2SAlex Bennée        if only_known:
259529994e2SAlex Bennée            if self._instance:
260529994e2SAlex Bennée                filter += "=%s" % (self._instance)
261529994e2SAlex Bennée            else:
262529994e2SAlex Bennée                # no point trying to kill, we finished
263529994e2SAlex Bennée                return
264529994e2SAlex Bennée
265529994e2SAlex Bennée        print("filter=%s" % (filter))
266529994e2SAlex Bennée        cmd.append(filter)
2674485b04bSFam Zheng        for i in self._output(cmd).split():
268529994e2SAlex Bennée            self._do(["rm", "-f", i])
2694485b04bSFam Zheng
2704485b04bSFam Zheng    def clean(self):
2714485b04bSFam Zheng        self._do_kill_instances(False, False)
2724485b04bSFam Zheng        return 0
2734485b04bSFam Zheng
27497cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2754485b04bSFam Zheng        return self._do_kill_instances(True)
2764485b04bSFam Zheng
2774485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2782d110c11SJohn Snow        try:
2794485b04bSFam Zheng            return subprocess.check_output(self._command + cmd,
2804485b04bSFam Zheng                                           stderr=subprocess.STDOUT,
2814112aff7SAlex Bennée                                           encoding='utf-8',
2824485b04bSFam Zheng                                           **kwargs)
2832d110c11SJohn Snow        except TypeError:
2842d110c11SJohn Snow            # 'encoding' argument was added in 3.6+
285884fcafcSAlex Bennée            return subprocess.check_output(self._command + cmd,
286884fcafcSAlex Bennée                                           stderr=subprocess.STDOUT,
287884fcafcSAlex Bennée                                           **kwargs).decode('utf-8')
288884fcafcSAlex Bennée
2894485b04bSFam Zheng
290f97da1f7SAlex Bennée    def inspect_tag(self, tag):
291f97da1f7SAlex Bennée        try:
292f97da1f7SAlex Bennée            return self._output(["inspect", tag])
293f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
294f97da1f7SAlex Bennée            return None
295f97da1f7SAlex Bennée
2967b882245SAlex Bennée    def get_image_creation_time(self, info):
2977b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2987b882245SAlex Bennée
2994485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
300f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
3014485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
3024485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
3034485b04bSFam Zheng
304414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
305e6f1306bSAlex Bennée                    quiet=True, user=False, argv=None, registry=None,
306e6f1306bSAlex Bennée                    extra_files_cksum=[]):
307432d8ad5SAlex Bennée        if argv is None:
3084485b04bSFam Zheng            argv = []
3094485b04bSFam Zheng
310e6f1306bSAlex Bennée        # pre-calculate the docker checksum before any
311e6f1306bSAlex Bennée        # substitutions we make for caching
312e6f1306bSAlex Bennée        checksum = _text_checksum(_dockerfile_preprocess(dockerfile))
313e6f1306bSAlex Bennée
314e6f1306bSAlex Bennée        if registry is not None:
315f73e4852SAlex Bennée            sources = re.findall("FROM qemu\/(.*)", dockerfile)
316f73e4852SAlex Bennée            # Fetch any cache layers we can, may fail
317f73e4852SAlex Bennée            for s in sources:
318f73e4852SAlex Bennée                pull_args = ["pull", "%s/qemu/%s" % (registry, s)]
319f73e4852SAlex Bennée                if self._do(pull_args, quiet=quiet) != 0:
320f73e4852SAlex Bennée                    registry = None
321f73e4852SAlex Bennée                    break
322f73e4852SAlex Bennée            # Make substitutions
323f73e4852SAlex Bennée            if registry is not None:
324e6f1306bSAlex Bennée                dockerfile = dockerfile.replace("FROM qemu/",
325e6f1306bSAlex Bennée                                                "FROM %s/qemu/" %
326e6f1306bSAlex Bennée                                                (registry))
327e6f1306bSAlex Bennée
3284112aff7SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
3294112aff7SAlex Bennée                                             encoding='utf-8',
3304112aff7SAlex Bennée                                             dir=docker_dir, suffix=".docker")
3314485b04bSFam Zheng        tmp_df.write(dockerfile)
3324485b04bSFam Zheng
333414a8ce5SAlex Bennée        if user:
334414a8ce5SAlex Bennée            uid = os.getuid()
335414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
336414a8ce5SAlex Bennée            tmp_df.write("\n")
337414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
338414a8ce5SAlex Bennée                         (uname, uid, uname))
339414a8ce5SAlex Bennée
3404485b04bSFam Zheng        tmp_df.write("\n")
341e405a3ebSAlessandro Di Federico        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum))
342f9172822SAlex Bennée        for f, c in extra_files_cksum:
343e405a3ebSAlessandro Di Federico            tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c))
344f9172822SAlex Bennée
3454485b04bSFam Zheng        tmp_df.flush()
346a9f8d038SAlex Bennée
347e6f1306bSAlex Bennée        build_args = ["build", "-t", tag, "-f", tmp_df.name]
348e6f1306bSAlex Bennée        if self._buildkit:
349e6f1306bSAlex Bennée            build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"]
350e6f1306bSAlex Bennée
351e6f1306bSAlex Bennée        if registry is not None:
352f73e4852SAlex Bennée            pull_args = ["pull", "%s/%s" % (registry, tag)]
353f73e4852SAlex Bennée            self._do(pull_args, quiet=quiet)
354e6f1306bSAlex Bennée            cache = "%s/%s" % (registry, tag)
355e6f1306bSAlex Bennée            build_args += ["--cache-from", cache]
356e6f1306bSAlex Bennée        build_args += argv
357e6f1306bSAlex Bennée        build_args += [docker_dir]
358e6f1306bSAlex Bennée
359e6f1306bSAlex Bennée        self._do_check(build_args,
3604485b04bSFam Zheng                       quiet=quiet)
3614485b04bSFam Zheng
3626e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
3636e733da6SAlex Bennée        "Update a tagged image using "
3646e733da6SAlex Bennée
3650b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
3666e733da6SAlex Bennée
3674485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
3684485b04bSFam Zheng        try:
3694485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
3704485b04bSFam Zheng        except Exception:
3714485b04bSFam Zheng            return False
372c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
3734485b04bSFam Zheng
37471ebbe09SAlex Bennée    def run(self, cmd, keep, quiet, as_user=False):
375529994e2SAlex Bennée        label = uuid.uuid4().hex
3764485b04bSFam Zheng        if not keep:
377529994e2SAlex Bennée            self._instance = label
37871ebbe09SAlex Bennée
37971ebbe09SAlex Bennée        if as_user:
38071ebbe09SAlex Bennée            uid = os.getuid()
38171ebbe09SAlex Bennée            cmd = [ "-u", str(uid) ] + cmd
38271ebbe09SAlex Bennée            # podman requires a bit more fiddling
38371ebbe09SAlex Bennée            if self._command[0] == "podman":
384b3a790beSJohn Snow                cmd.insert(0, '--userns=keep-id')
38571ebbe09SAlex Bennée
38617cd6e2bSPaolo Bonzini        ret = self._do_check(["run", "--rm", "--label",
3874485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3884485b04bSFam Zheng                             quiet=quiet)
3894485b04bSFam Zheng        if not keep:
390529994e2SAlex Bennée            self._instance = None
3914485b04bSFam Zheng        return ret
3924485b04bSFam Zheng
3934b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3944b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3954b08af60SFam Zheng
396432d8ad5SAlex Bennée
3974485b04bSFam Zhengclass SubCommand(object):
3984485b04bSFam Zheng    """A SubCommand template base class"""
3994485b04bSFam Zheng    name = None  # Subcommand name
400432d8ad5SAlex Bennée
4014485b04bSFam Zheng    def shared_args(self, parser):
4024485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
403e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
4044485b04bSFam Zheng
4054485b04bSFam Zheng    def args(self, parser):
4064485b04bSFam Zheng        """Setup argument parser"""
4074485b04bSFam Zheng        pass
408432d8ad5SAlex Bennée
4094485b04bSFam Zheng    def run(self, args, argv):
4104485b04bSFam Zheng        """Run command.
4114485b04bSFam Zheng        args: parsed argument by argument parser.
4124485b04bSFam Zheng        argv: remaining arguments from sys.argv.
4134485b04bSFam Zheng        """
4144485b04bSFam Zheng        pass
4154485b04bSFam Zheng
416432d8ad5SAlex Bennée
4174485b04bSFam Zhengclass RunCommand(SubCommand):
4184485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
4194485b04bSFam Zheng    name = "run"
420432d8ad5SAlex Bennée
4214485b04bSFam Zheng    def args(self, parser):
4224485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
4234485b04bSFam Zheng                            help="Don't remove image when command completes")
4242461d80eSMarc-André Lureau        parser.add_argument("--run-as-current-user", action="store_true",
4252461d80eSMarc-André Lureau                            help="Run container using the current user's uid")
426432d8ad5SAlex Bennée
4274485b04bSFam Zheng    def run(self, args, argv):
42871ebbe09SAlex Bennée        return Docker().run(argv, args.keep, quiet=args.quiet,
42971ebbe09SAlex Bennée                            as_user=args.run_as_current_user)
4304485b04bSFam Zheng
431432d8ad5SAlex Bennée
4324485b04bSFam Zhengclass BuildCommand(SubCommand):
433432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
4344485b04bSFam Zheng    name = "build"
435432d8ad5SAlex Bennée
4364485b04bSFam Zheng    def args(self, parser):
437504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
438504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
439504ca3c2SAlex Bennée                            container together with all its dependent
440504ca3c2SAlex Bennée                            libraries""")
441*ddd5ed83SAlex Bennée        parser.add_argument("--skip-binfmt",
442*ddd5ed83SAlex Bennée                            action="store_true",
443*ddd5ed83SAlex Bennée                            help="""Skip binfmt entry check (used for testing)""")
444dfae6284SPaolo Bonzini        parser.add_argument("--extra-files", nargs='*',
4454c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
4464c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
4474c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
448414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
449414a8ce5SAlex Bennée                            action="store_true",
450414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
451e6f1306bSAlex Bennée        parser.add_argument("--registry", "-r",
452e6f1306bSAlex Bennée                            help="cache from docker registry")
453dfae6284SPaolo Bonzini        parser.add_argument("-t", dest="tag",
4544485b04bSFam Zheng                            help="Image Tag")
455dfae6284SPaolo Bonzini        parser.add_argument("-f", dest="dockerfile",
4564485b04bSFam Zheng                            help="Dockerfile name")
4574485b04bSFam Zheng
4584485b04bSFam Zheng    def run(self, args, argv):
4594112aff7SAlex Bennée        dockerfile = _read_dockerfile(args.dockerfile)
4604485b04bSFam Zheng        tag = args.tag
4614485b04bSFam Zheng
4624485b04bSFam Zheng        dkr = Docker()
4636fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
4646fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
4654485b04bSFam Zheng            if not args.quiet:
466f03868bdSEduardo Habkost                print("Image is up to date.")
467a9f8d038SAlex Bennée        else:
468a9f8d038SAlex Bennée            # Create a docker context directory for the build
469a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
4704485b04bSFam Zheng
47115352decSAlex Bennée            # Validate binfmt_misc will work
472*ddd5ed83SAlex Bennée            if args.skip_binfmt:
473*ddd5ed83SAlex Bennée                qpath = args.include_executable
474*ddd5ed83SAlex Bennée            elif args.include_executable:
475d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
476d10404b1SAlex Bennée                if not enabled:
47715352decSAlex Bennée                    return 1
47815352decSAlex Bennée
479920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
480920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
481920776eaSAlex Bennée            if os.path.exists(docker_pre):
482f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
483920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
484f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
485920776eaSAlex Bennée                if rc == 3:
486f03868bdSEduardo Habkost                    print("Skip")
487920776eaSAlex Bennée                    return 0
488920776eaSAlex Bennée                elif rc != 0:
489f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
490920776eaSAlex Bennée                    return 1
491920776eaSAlex Bennée
4924c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
4934c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
494438d1168SPhilippe Mathieu-Daudé            cksum = []
495504ca3c2SAlex Bennée            if args.include_executable:
496438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
497438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
498438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
499d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
500d10404b1SAlex Bennée                                       qpath, docker_dir)
501d10404b1SAlex Bennée
5024c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
5034c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
504f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
505504ca3c2SAlex Bennée
50606cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
5074112aff7SAlex Bennée                     for k, v in os.environ.items()
50806cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
509a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
510e6f1306bSAlex Bennée                            quiet=args.quiet, user=args.user,
511e6f1306bSAlex Bennée                            argv=argv, registry=args.registry,
512438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
513a9f8d038SAlex Bennée
514a9f8d038SAlex Bennée            rmtree(docker_dir)
515a9f8d038SAlex Bennée
5164485b04bSFam Zheng        return 0
5174485b04bSFam Zheng
518432d8ad5SAlex Bennée
5196e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
520432d8ad5SAlex Bennée    """ Update a docker image with new executables. Args: <tag> <executable>"""
5216e733da6SAlex Bennée    name = "update"
522432d8ad5SAlex Bennée
5236e733da6SAlex Bennée    def args(self, parser):
5246e733da6SAlex Bennée        parser.add_argument("tag",
5256e733da6SAlex Bennée                            help="Image Tag")
5266e733da6SAlex Bennée        parser.add_argument("executable",
5276e733da6SAlex Bennée                            help="Executable to copy")
5286e733da6SAlex Bennée
5296e733da6SAlex Bennée    def run(self, args, argv):
5306e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
5316e733da6SAlex Bennée        # dockerfile for the update
5326e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
5336e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
5346e733da6SAlex Bennée
5357e81d198SAlex Bennée        # Add the executable to the tarball, using the current
536d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
537d10404b1SAlex Bennée        # only need the support libraries copied
538d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
5397e81d198SAlex Bennée
540d10404b1SAlex Bennée        if not enabled:
541d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
542d10404b1SAlex Bennée            return 1
543d10404b1SAlex Bennée
544d10404b1SAlex Bennée        if ff:
5456e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
5466e733da6SAlex Bennée
5476e733da6SAlex Bennée        # Add any associated libraries
5486e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
5496e733da6SAlex Bennée        if libs:
5506e733da6SAlex Bennée            for l in libs:
5516e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
5526e733da6SAlex Bennée
5536e733da6SAlex Bennée        # Create a Docker buildfile
5546e733da6SAlex Bennée        df = StringIO()
555e336cec3SAlex Bennée        df.write(u"FROM %s\n" % args.tag)
556e336cec3SAlex Bennée        df.write(u"ADD . /\n")
557e336cec3SAlex Bennée
558e336cec3SAlex Bennée        df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8"))
5596e733da6SAlex Bennée
5606e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
561e336cec3SAlex Bennée        df_tar.size = df_bytes.getbuffer().nbytes
562e336cec3SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df_bytes)
5636e733da6SAlex Bennée
5646e733da6SAlex Bennée        tmp_tar.close()
5656e733da6SAlex Bennée
5666e733da6SAlex Bennée        # reset the file pointers
5676e733da6SAlex Bennée        tmp.flush()
5686e733da6SAlex Bennée        tmp.seek(0)
5696e733da6SAlex Bennée
5706e733da6SAlex Bennée        # Run the build with our tarball context
5716e733da6SAlex Bennée        dkr = Docker()
5726e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
5736e733da6SAlex Bennée
5746e733da6SAlex Bennée        return 0
5756e733da6SAlex Bennée
576432d8ad5SAlex Bennée
5774485b04bSFam Zhengclass CleanCommand(SubCommand):
5784485b04bSFam Zheng    """Clean up docker instances"""
5794485b04bSFam Zheng    name = "clean"
580432d8ad5SAlex Bennée
5814485b04bSFam Zheng    def run(self, args, argv):
5824485b04bSFam Zheng        Docker().clean()
5834485b04bSFam Zheng        return 0
5844485b04bSFam Zheng
585432d8ad5SAlex Bennée
5864b08af60SFam Zhengclass ImagesCommand(SubCommand):
5874b08af60SFam Zheng    """Run "docker images" command"""
5884b08af60SFam Zheng    name = "images"
589432d8ad5SAlex Bennée
5904b08af60SFam Zheng    def run(self, args, argv):
5914b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
5924b08af60SFam Zheng
59315df9d37SAlex Bennée
59415df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
59515df9d37SAlex Bennée    """Probe if we can run docker automatically"""
59615df9d37SAlex Bennée    name = "probe"
59715df9d37SAlex Bennée
59815df9d37SAlex Bennée    def run(self, args, argv):
59915df9d37SAlex Bennée        try:
60015df9d37SAlex Bennée            docker = Docker()
60115df9d37SAlex Bennée            if docker._command[0] == "docker":
6028480517dSAlex Bennée                print("docker")
60315df9d37SAlex Bennée            elif docker._command[0] == "sudo":
6048480517dSAlex Bennée                print("sudo docker")
6059459f754SMarc-André Lureau            elif docker._command[0] == "podman":
6069459f754SMarc-André Lureau                print("podman")
60715df9d37SAlex Bennée        except Exception:
608f03868bdSEduardo Habkost            print("no")
60915df9d37SAlex Bennée
61015df9d37SAlex Bennée        return
61115df9d37SAlex Bennée
61215df9d37SAlex Bennée
6135e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
6145e03c2d8SAlex Bennée    """Compile sources with cc in images"""
6155e03c2d8SAlex Bennée    name = "cc"
6165e03c2d8SAlex Bennée
6175e03c2d8SAlex Bennée    def args(self, parser):
6185e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
6195e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
62099cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
62199cfdb86SAlex Bennée                            help="The compiler executable to call")
6225e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
6235e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
6245e03c2d8SAlex Bennée                            reading sources""")
6255e03c2d8SAlex Bennée
6265e03c2d8SAlex Bennée    def run(self, args, argv):
6275e03c2d8SAlex Bennée        if argv and argv[0] == "--":
6285e03c2d8SAlex Bennée            argv = argv[1:]
6295e03c2d8SAlex Bennée        cwd = os.getcwd()
63017cd6e2bSPaolo Bonzini        cmd = ["-w", cwd,
6315e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
6325e03c2d8SAlex Bennée        if args.paths:
6335e03c2d8SAlex Bennée            for p in args.paths:
6345e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
63599cfdb86SAlex Bennée        cmd += [args.image, args.cc]
6365e03c2d8SAlex Bennée        cmd += argv
63771ebbe09SAlex Bennée        return Docker().run(cmd, False, quiet=args.quiet,
63871ebbe09SAlex Bennée                            as_user=True)
6395e03c2d8SAlex Bennée
6405e03c2d8SAlex Bennée
641f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
642f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
643f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
644f97da1f7SAlex Bennée    name = "check"
645f97da1f7SAlex Bennée
646f97da1f7SAlex Bennée    def args(self, parser):
647f97da1f7SAlex Bennée        parser.add_argument("tag",
648f97da1f7SAlex Bennée                            help="Image Tag")
6497b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
6507b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
6517b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
6527b882245SAlex Bennée                            default="checksum", help="check type")
6537b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
6547b882245SAlex Bennée                            help="number of minutes")
655f97da1f7SAlex Bennée
656f97da1f7SAlex Bennée    def run(self, args, argv):
657f97da1f7SAlex Bennée        tag = args.tag
658f97da1f7SAlex Bennée
65943e1b2ffSAlex Bennée        try:
660f97da1f7SAlex Bennée            dkr = Docker()
661432d8ad5SAlex Bennée        except subprocess.CalledProcessError:
66243e1b2ffSAlex Bennée            print("Docker not set up")
66343e1b2ffSAlex Bennée            return 1
66443e1b2ffSAlex Bennée
665f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
666f97da1f7SAlex Bennée        if info is None:
667f97da1f7SAlex Bennée            print("Image does not exist")
668f97da1f7SAlex Bennée            return 1
669f97da1f7SAlex Bennée
6707b882245SAlex Bennée        if args.checktype == "checksum":
6717b882245SAlex Bennée            if not args.dockerfile:
6727b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
6737b882245SAlex Bennée                return 1
6747b882245SAlex Bennée
6754112aff7SAlex Bennée            dockerfile = _read_dockerfile(args.dockerfile)
6767b882245SAlex Bennée
677f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
678f97da1f7SAlex Bennée                if not args.quiet:
679f97da1f7SAlex Bennée                    print("Image is up to date")
680f97da1f7SAlex Bennée                return 0
681f97da1f7SAlex Bennée            else:
682f97da1f7SAlex Bennée                print("Image needs updating")
683f97da1f7SAlex Bennée                return 1
6847b882245SAlex Bennée        elif args.checktype == "age":
6857b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
6867b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
6877b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
6887b882245SAlex Bennée            if created < past:
6897b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
6907b882245SAlex Bennée                       (timestr, args.olderthan))
6917b882245SAlex Bennée                return 1
6927b882245SAlex Bennée            else:
6937b882245SAlex Bennée                if not args.quiet:
6947b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
6957b882245SAlex Bennée                return 0
696f97da1f7SAlex Bennée
697f97da1f7SAlex Bennée
6984485b04bSFam Zhengdef main():
6999459f754SMarc-André Lureau    global USE_ENGINE
7009459f754SMarc-André Lureau
7014485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
702432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
703432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
7049459f754SMarc-André Lureau    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
7059459f754SMarc-André Lureau                        help="specify which container engine to use")
7064485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
7074485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
7084485b04bSFam Zheng        cmd = cls()
7094485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
7104485b04bSFam Zheng        cmd.shared_args(subp)
7114485b04bSFam Zheng        cmd.args(subp)
7124485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
7134485b04bSFam Zheng    args, argv = parser.parse_known_args()
7148480517dSAlex Bennée    if args.engine:
7159459f754SMarc-André Lureau        USE_ENGINE = args.engine
7164485b04bSFam Zheng    return args.cmdobj.run(args, argv)
7174485b04bSFam Zheng
718432d8ad5SAlex Bennée
7194485b04bSFam Zhengif __name__ == "__main__":
7204485b04bSFam Zheng    sys.exit(main())
721