xref: /qemu/tests/docker/docker.py (revision f73e485285bc2da944a08e1dd20a30df93e23f25)
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
277a5d936bSAlex Bennéefrom io import StringIO
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
962499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'):
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
105504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
106504ca3c2SAlex Bennée    copy(src, dest_file)
107504ca3c2SAlex Bennée
108504ca3c2SAlex Bennée
109504ca3c2SAlex Bennéedef _get_so_libs(executable):
110504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
111504ca3c2SAlex Bennée
112504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
113504ca3c2SAlex Bennée    ensure the right data is copied."""
114504ca3c2SAlex Bennée
115504ca3c2SAlex Bennée    libs = []
1165e33f7feSAlex Bennée    ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)")
117504ca3c2SAlex Bennée    try:
118eea2153eSAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8')
119504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
120504ca3c2SAlex Bennée            search = ldd_re.search(line)
1215e33f7feSAlex Bennée            if search:
1225e33f7feSAlex Bennée                try:
1235e33f7feSAlex Bennée                    libs.append(s.group(1))
1245e33f7feSAlex Bennée                except IndexError:
1255e33f7feSAlex Bennée                    pass
126504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
127f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
128504ca3c2SAlex Bennée
129504ca3c2SAlex Bennée    return libs
130504ca3c2SAlex Bennée
131432d8ad5SAlex Bennée
132d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
133d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
134d10404b1SAlex Bennée
135d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
136d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
137d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
138d10404b1SAlex Bennée    is in the image.
139504ca3c2SAlex Bennée
140504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
141d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
142d10404b1SAlex Bennée    """
143504ca3c2SAlex Bennée
144d10404b1SAlex Bennée    if bin_dest:
145d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
146d10404b1SAlex Bennée    else:
147d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
148504ca3c2SAlex Bennée
149504ca3c2SAlex Bennée    libs = _get_so_libs(src)
150504ca3c2SAlex Bennée    if libs:
151504ca3c2SAlex Bennée        for l in libs:
152504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
1535e33f7feSAlex Bennée            real_l = os.path.realpath(l)
1545e33f7feSAlex Bennée            _copy_with_mkdir(real_l, dest_dir, so_path)
155504ca3c2SAlex Bennée
15615352decSAlex Bennée
15715352decSAlex Bennéedef _check_binfmt_misc(executable):
15815352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
15915352decSAlex Bennée
16015352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
16115352decSAlex Bennée    this script but we should at least fail early with a useful
162d10404b1SAlex Bennée    message if it won't work.
163d10404b1SAlex Bennée
164d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
165d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
166d10404b1SAlex Bennée    libraries.
167d10404b1SAlex Bennée    """
16815352decSAlex Bennée
16915352decSAlex Bennée    binary = os.path.basename(executable)
17015352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
17115352decSAlex Bennée
17215352decSAlex Bennée    if not os.path.exists(binfmt_entry):
17315352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
174d10404b1SAlex Bennée        return None, False
17515352decSAlex Bennée
17615352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
17715352decSAlex Bennée
17843c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
179432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
18043c898b7SAlex Bennée              (binary))
181d10404b1SAlex Bennée        return None, True
18243c898b7SAlex Bennée
1837e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1847e81d198SAlex Bennée    interp = m.group(1)
1857e81d198SAlex Bennée    if interp and interp != executable:
1867e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1877e81d198SAlex Bennée              (binary, executable, interp))
18815352decSAlex Bennée
189d10404b1SAlex Bennée    return interp, True
190d10404b1SAlex Bennée
19115352decSAlex Bennée
192c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
193547cb45eSAlex Bennée    # special case for Debian linux-user images
194547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
195547cb45eSAlex Bennée        img_name = "debian-bootstrap"
196547cb45eSAlex Bennée
197c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
198c1958e9dSFam Zheng                      img_name + ".docker")
1994112aff7SAlex Bennée    return _read_dockerfile(df)
200c1958e9dSFam Zheng
201432d8ad5SAlex Bennée
202c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
203c1958e9dSFam Zheng    out = ""
204c1958e9dSFam Zheng    for l in df.splitlines():
205c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
206c1958e9dSFam Zheng            continue
207767b6bd2SAlex Bennée        from_pref = "FROM qemu/"
208c1958e9dSFam Zheng        if l.startswith(from_pref):
209c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
210c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
211c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
212c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
213c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
214c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
215c1958e9dSFam Zheng            continue
216c1958e9dSFam Zheng        out += l + "\n"
217c1958e9dSFam Zheng    return out
218c1958e9dSFam Zheng
219432d8ad5SAlex Bennée
2204485b04bSFam Zhengclass Docker(object):
2214485b04bSFam Zheng    """ Running Docker commands """
2224485b04bSFam Zheng    def __init__(self):
2239459f754SMarc-André Lureau        self._command = _guess_engine_command()
224e6f1306bSAlex Bennée
225e6f1306bSAlex Bennée        if "docker" in self._command and "TRAVIS" not in os.environ:
226e6f1306bSAlex Bennée            os.environ["DOCKER_BUILDKIT"] = "1"
227e6f1306bSAlex Bennée            self._buildkit = True
228e6f1306bSAlex Bennée        else:
229e6f1306bSAlex Bennée            self._buildkit = False
230e6f1306bSAlex Bennée
231529994e2SAlex Bennée        self._instance = None
2324485b04bSFam Zheng        atexit.register(self._kill_instances)
23397cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
23497cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
2354485b04bSFam Zheng
23658bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2374485b04bSFam Zheng        if quiet:
238c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2394485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2404485b04bSFam Zheng
2410b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2420b95ff72SFam Zheng        if quiet:
2430b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2440b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2450b95ff72SFam Zheng
2464485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2474485b04bSFam Zheng        cmd = ["ps", "-q"]
2484485b04bSFam Zheng        if not only_active:
2494485b04bSFam Zheng            cmd.append("-a")
250529994e2SAlex Bennée
251529994e2SAlex Bennée        filter = "--filter=label=com.qemu.instance.uuid"
252529994e2SAlex Bennée        if only_known:
253529994e2SAlex Bennée            if self._instance:
254529994e2SAlex Bennée                filter += "=%s" % (self._instance)
255529994e2SAlex Bennée            else:
256529994e2SAlex Bennée                # no point trying to kill, we finished
257529994e2SAlex Bennée                return
258529994e2SAlex Bennée
259529994e2SAlex Bennée        print("filter=%s" % (filter))
260529994e2SAlex Bennée        cmd.append(filter)
2614485b04bSFam Zheng        for i in self._output(cmd).split():
262529994e2SAlex Bennée            self._do(["rm", "-f", i])
2634485b04bSFam Zheng
2644485b04bSFam Zheng    def clean(self):
2654485b04bSFam Zheng        self._do_kill_instances(False, False)
2664485b04bSFam Zheng        return 0
2674485b04bSFam Zheng
26897cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2694485b04bSFam Zheng        return self._do_kill_instances(True)
2704485b04bSFam Zheng
2714485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2722d110c11SJohn Snow        try:
2734485b04bSFam Zheng            return subprocess.check_output(self._command + cmd,
2744485b04bSFam Zheng                                           stderr=subprocess.STDOUT,
2754112aff7SAlex Bennée                                           encoding='utf-8',
2764485b04bSFam Zheng                                           **kwargs)
2772d110c11SJohn Snow        except TypeError:
2782d110c11SJohn Snow            # 'encoding' argument was added in 3.6+
279884fcafcSAlex Bennée            return subprocess.check_output(self._command + cmd,
280884fcafcSAlex Bennée                                           stderr=subprocess.STDOUT,
281884fcafcSAlex Bennée                                           **kwargs).decode('utf-8')
282884fcafcSAlex Bennée
2834485b04bSFam Zheng
284f97da1f7SAlex Bennée    def inspect_tag(self, tag):
285f97da1f7SAlex Bennée        try:
286f97da1f7SAlex Bennée            return self._output(["inspect", tag])
287f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
288f97da1f7SAlex Bennée            return None
289f97da1f7SAlex Bennée
2907b882245SAlex Bennée    def get_image_creation_time(self, info):
2917b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2927b882245SAlex Bennée
2934485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
294f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2954485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2964485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
2974485b04bSFam Zheng
298414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
299e6f1306bSAlex Bennée                    quiet=True, user=False, argv=None, registry=None,
300e6f1306bSAlex Bennée                    extra_files_cksum=[]):
301432d8ad5SAlex Bennée        if argv is None:
3024485b04bSFam Zheng            argv = []
3034485b04bSFam Zheng
304e6f1306bSAlex Bennée        # pre-calculate the docker checksum before any
305e6f1306bSAlex Bennée        # substitutions we make for caching
306e6f1306bSAlex Bennée        checksum = _text_checksum(_dockerfile_preprocess(dockerfile))
307e6f1306bSAlex Bennée
308e6f1306bSAlex Bennée        if registry is not None:
309*f73e4852SAlex Bennée            sources = re.findall("FROM qemu\/(.*)", dockerfile)
310*f73e4852SAlex Bennée            # Fetch any cache layers we can, may fail
311*f73e4852SAlex Bennée            for s in sources:
312*f73e4852SAlex Bennée                pull_args = ["pull", "%s/qemu/%s" % (registry, s)]
313*f73e4852SAlex Bennée                if self._do(pull_args, quiet=quiet) != 0:
314*f73e4852SAlex Bennée                    registry = None
315*f73e4852SAlex Bennée                    break
316*f73e4852SAlex Bennée            # Make substitutions
317*f73e4852SAlex Bennée            if registry is not None:
318e6f1306bSAlex Bennée                dockerfile = dockerfile.replace("FROM qemu/",
319e6f1306bSAlex Bennée                                                "FROM %s/qemu/" %
320e6f1306bSAlex Bennée                                                (registry))
321e6f1306bSAlex Bennée
3224112aff7SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
3234112aff7SAlex Bennée                                             encoding='utf-8',
3244112aff7SAlex Bennée                                             dir=docker_dir, suffix=".docker")
3254485b04bSFam Zheng        tmp_df.write(dockerfile)
3264485b04bSFam Zheng
327414a8ce5SAlex Bennée        if user:
328414a8ce5SAlex Bennée            uid = os.getuid()
329414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
330414a8ce5SAlex Bennée            tmp_df.write("\n")
331414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
332414a8ce5SAlex Bennée                         (uname, uid, uname))
333414a8ce5SAlex Bennée
3344485b04bSFam Zheng        tmp_df.write("\n")
335e6f1306bSAlex Bennée        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % (checksum))
336f9172822SAlex Bennée        for f, c in extra_files_cksum:
337f9172822SAlex Bennée            tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
338f9172822SAlex Bennée
3394485b04bSFam Zheng        tmp_df.flush()
340a9f8d038SAlex Bennée
341e6f1306bSAlex Bennée        build_args = ["build", "-t", tag, "-f", tmp_df.name]
342e6f1306bSAlex Bennée        if self._buildkit:
343e6f1306bSAlex Bennée            build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"]
344e6f1306bSAlex Bennée
345e6f1306bSAlex Bennée        if registry is not None:
346*f73e4852SAlex Bennée            pull_args = ["pull", "%s/%s" % (registry, tag)]
347*f73e4852SAlex Bennée            self._do(pull_args, quiet=quiet)
348e6f1306bSAlex Bennée            cache = "%s/%s" % (registry, tag)
349e6f1306bSAlex Bennée            build_args += ["--cache-from", cache]
350e6f1306bSAlex Bennée        build_args += argv
351e6f1306bSAlex Bennée        build_args += [docker_dir]
352e6f1306bSAlex Bennée
353e6f1306bSAlex Bennée        self._do_check(build_args,
3544485b04bSFam Zheng                       quiet=quiet)
3554485b04bSFam Zheng
3566e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
3576e733da6SAlex Bennée        "Update a tagged image using "
3586e733da6SAlex Bennée
3590b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
3606e733da6SAlex Bennée
3614485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
3624485b04bSFam Zheng        try:
3634485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
3644485b04bSFam Zheng        except Exception:
3654485b04bSFam Zheng            return False
366c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
3674485b04bSFam Zheng
36871ebbe09SAlex Bennée    def run(self, cmd, keep, quiet, as_user=False):
369529994e2SAlex Bennée        label = uuid.uuid4().hex
3704485b04bSFam Zheng        if not keep:
371529994e2SAlex Bennée            self._instance = label
37271ebbe09SAlex Bennée
37371ebbe09SAlex Bennée        if as_user:
37471ebbe09SAlex Bennée            uid = os.getuid()
37571ebbe09SAlex Bennée            cmd = [ "-u", str(uid) ] + cmd
37671ebbe09SAlex Bennée            # podman requires a bit more fiddling
37771ebbe09SAlex Bennée            if self._command[0] == "podman":
378b3a790beSJohn Snow                cmd.insert(0, '--userns=keep-id')
37971ebbe09SAlex Bennée
3800b95ff72SFam Zheng        ret = self._do_check(["run", "--label",
3814485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3824485b04bSFam Zheng                             quiet=quiet)
3834485b04bSFam Zheng        if not keep:
384529994e2SAlex Bennée            self._instance = None
3854485b04bSFam Zheng        return ret
3864485b04bSFam Zheng
3874b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3884b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3894b08af60SFam Zheng
390432d8ad5SAlex Bennée
3914485b04bSFam Zhengclass SubCommand(object):
3924485b04bSFam Zheng    """A SubCommand template base class"""
3934485b04bSFam Zheng    name = None  # Subcommand name
394432d8ad5SAlex Bennée
3954485b04bSFam Zheng    def shared_args(self, parser):
3964485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
397e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3984485b04bSFam Zheng
3994485b04bSFam Zheng    def args(self, parser):
4004485b04bSFam Zheng        """Setup argument parser"""
4014485b04bSFam Zheng        pass
402432d8ad5SAlex Bennée
4034485b04bSFam Zheng    def run(self, args, argv):
4044485b04bSFam Zheng        """Run command.
4054485b04bSFam Zheng        args: parsed argument by argument parser.
4064485b04bSFam Zheng        argv: remaining arguments from sys.argv.
4074485b04bSFam Zheng        """
4084485b04bSFam Zheng        pass
4094485b04bSFam Zheng
410432d8ad5SAlex Bennée
4114485b04bSFam Zhengclass RunCommand(SubCommand):
4124485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
4134485b04bSFam Zheng    name = "run"
414432d8ad5SAlex Bennée
4154485b04bSFam Zheng    def args(self, parser):
4164485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
4174485b04bSFam Zheng                            help="Don't remove image when command completes")
4182461d80eSMarc-André Lureau        parser.add_argument("--run-as-current-user", action="store_true",
4192461d80eSMarc-André Lureau                            help="Run container using the current user's uid")
420432d8ad5SAlex Bennée
4214485b04bSFam Zheng    def run(self, args, argv):
42271ebbe09SAlex Bennée        return Docker().run(argv, args.keep, quiet=args.quiet,
42371ebbe09SAlex Bennée                            as_user=args.run_as_current_user)
4244485b04bSFam Zheng
425432d8ad5SAlex Bennée
4264485b04bSFam Zhengclass BuildCommand(SubCommand):
427432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
4284485b04bSFam Zheng    name = "build"
429432d8ad5SAlex Bennée
4304485b04bSFam Zheng    def args(self, parser):
431504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
432504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
433504ca3c2SAlex Bennée                            container together with all its dependent
434504ca3c2SAlex Bennée                            libraries""")
435dfae6284SPaolo Bonzini        parser.add_argument("--extra-files", nargs='*',
4364c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
4374c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
4384c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
439414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
440414a8ce5SAlex Bennée                            action="store_true",
441414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
442e6f1306bSAlex Bennée        parser.add_argument("--registry", "-r",
443e6f1306bSAlex Bennée                            help="cache from docker registry")
444dfae6284SPaolo Bonzini        parser.add_argument("-t", dest="tag",
4454485b04bSFam Zheng                            help="Image Tag")
446dfae6284SPaolo Bonzini        parser.add_argument("-f", dest="dockerfile",
4474485b04bSFam Zheng                            help="Dockerfile name")
4484485b04bSFam Zheng
4494485b04bSFam Zheng    def run(self, args, argv):
4504112aff7SAlex Bennée        dockerfile = _read_dockerfile(args.dockerfile)
4514485b04bSFam Zheng        tag = args.tag
4524485b04bSFam Zheng
4534485b04bSFam Zheng        dkr = Docker()
4546fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
4556fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
4564485b04bSFam Zheng            if not args.quiet:
457f03868bdSEduardo Habkost                print("Image is up to date.")
458a9f8d038SAlex Bennée        else:
459a9f8d038SAlex Bennée            # Create a docker context directory for the build
460a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
4614485b04bSFam Zheng
46215352decSAlex Bennée            # Validate binfmt_misc will work
46315352decSAlex Bennée            if args.include_executable:
464d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
465d10404b1SAlex Bennée                if not enabled:
46615352decSAlex Bennée                    return 1
46715352decSAlex Bennée
468920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
469920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
470920776eaSAlex Bennée            if os.path.exists(docker_pre):
471f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
472920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
473f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
474920776eaSAlex Bennée                if rc == 3:
475f03868bdSEduardo Habkost                    print("Skip")
476920776eaSAlex Bennée                    return 0
477920776eaSAlex Bennée                elif rc != 0:
478f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
479920776eaSAlex Bennée                    return 1
480920776eaSAlex Bennée
4814c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
4824c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
483438d1168SPhilippe Mathieu-Daudé            cksum = []
484504ca3c2SAlex Bennée            if args.include_executable:
485438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
486438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
487438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
488d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
489d10404b1SAlex Bennée                                       qpath, docker_dir)
490d10404b1SAlex Bennée
4914c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
4924c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
493f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
494504ca3c2SAlex Bennée
49506cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
4964112aff7SAlex Bennée                     for k, v in os.environ.items()
49706cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
498a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
499e6f1306bSAlex Bennée                            quiet=args.quiet, user=args.user,
500e6f1306bSAlex Bennée                            argv=argv, registry=args.registry,
501438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
502a9f8d038SAlex Bennée
503a9f8d038SAlex Bennée            rmtree(docker_dir)
504a9f8d038SAlex Bennée
5054485b04bSFam Zheng        return 0
5064485b04bSFam Zheng
507432d8ad5SAlex Bennée
5086e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
509432d8ad5SAlex Bennée    """ Update a docker image with new executables. Args: <tag> <executable>"""
5106e733da6SAlex Bennée    name = "update"
511432d8ad5SAlex Bennée
5126e733da6SAlex Bennée    def args(self, parser):
5136e733da6SAlex Bennée        parser.add_argument("tag",
5146e733da6SAlex Bennée                            help="Image Tag")
5156e733da6SAlex Bennée        parser.add_argument("executable",
5166e733da6SAlex Bennée                            help="Executable to copy")
5176e733da6SAlex Bennée
5186e733da6SAlex Bennée    def run(self, args, argv):
5196e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
5206e733da6SAlex Bennée        # dockerfile for the update
5216e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
5226e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
5236e733da6SAlex Bennée
5247e81d198SAlex Bennée        # Add the executable to the tarball, using the current
525d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
526d10404b1SAlex Bennée        # only need the support libraries copied
527d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
5287e81d198SAlex Bennée
529d10404b1SAlex Bennée        if not enabled:
530d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
531d10404b1SAlex Bennée            return 1
532d10404b1SAlex Bennée
533d10404b1SAlex Bennée        if ff:
5346e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
5356e733da6SAlex Bennée
5366e733da6SAlex Bennée        # Add any associated libraries
5376e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
5386e733da6SAlex Bennée        if libs:
5396e733da6SAlex Bennée            for l in libs:
5406e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
5416e733da6SAlex Bennée
5426e733da6SAlex Bennée        # Create a Docker buildfile
5436e733da6SAlex Bennée        df = StringIO()
5446e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
5456e733da6SAlex Bennée        df.write("ADD . /\n")
5466e733da6SAlex Bennée        df.seek(0)
5476e733da6SAlex Bennée
5486e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
5496e733da6SAlex Bennée        df_tar.size = len(df.buf)
5506e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
5516e733da6SAlex Bennée
5526e733da6SAlex Bennée        tmp_tar.close()
5536e733da6SAlex Bennée
5546e733da6SAlex Bennée        # reset the file pointers
5556e733da6SAlex Bennée        tmp.flush()
5566e733da6SAlex Bennée        tmp.seek(0)
5576e733da6SAlex Bennée
5586e733da6SAlex Bennée        # Run the build with our tarball context
5596e733da6SAlex Bennée        dkr = Docker()
5606e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
5616e733da6SAlex Bennée
5626e733da6SAlex Bennée        return 0
5636e733da6SAlex Bennée
564432d8ad5SAlex Bennée
5654485b04bSFam Zhengclass CleanCommand(SubCommand):
5664485b04bSFam Zheng    """Clean up docker instances"""
5674485b04bSFam Zheng    name = "clean"
568432d8ad5SAlex Bennée
5694485b04bSFam Zheng    def run(self, args, argv):
5704485b04bSFam Zheng        Docker().clean()
5714485b04bSFam Zheng        return 0
5724485b04bSFam Zheng
573432d8ad5SAlex Bennée
5744b08af60SFam Zhengclass ImagesCommand(SubCommand):
5754b08af60SFam Zheng    """Run "docker images" command"""
5764b08af60SFam Zheng    name = "images"
577432d8ad5SAlex Bennée
5784b08af60SFam Zheng    def run(self, args, argv):
5794b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
5804b08af60SFam Zheng
58115df9d37SAlex Bennée
58215df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
58315df9d37SAlex Bennée    """Probe if we can run docker automatically"""
58415df9d37SAlex Bennée    name = "probe"
58515df9d37SAlex Bennée
58615df9d37SAlex Bennée    def run(self, args, argv):
58715df9d37SAlex Bennée        try:
58815df9d37SAlex Bennée            docker = Docker()
58915df9d37SAlex Bennée            if docker._command[0] == "docker":
5908480517dSAlex Bennée                print("docker")
59115df9d37SAlex Bennée            elif docker._command[0] == "sudo":
5928480517dSAlex Bennée                print("sudo docker")
5939459f754SMarc-André Lureau            elif docker._command[0] == "podman":
5949459f754SMarc-André Lureau                print("podman")
59515df9d37SAlex Bennée        except Exception:
596f03868bdSEduardo Habkost            print("no")
59715df9d37SAlex Bennée
59815df9d37SAlex Bennée        return
59915df9d37SAlex Bennée
60015df9d37SAlex Bennée
6015e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
6025e03c2d8SAlex Bennée    """Compile sources with cc in images"""
6035e03c2d8SAlex Bennée    name = "cc"
6045e03c2d8SAlex Bennée
6055e03c2d8SAlex Bennée    def args(self, parser):
6065e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
6075e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
60899cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
60999cfdb86SAlex Bennée                            help="The compiler executable to call")
6105e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
6115e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
6125e03c2d8SAlex Bennée                            reading sources""")
6135e03c2d8SAlex Bennée
6145e03c2d8SAlex Bennée    def run(self, args, argv):
6155e03c2d8SAlex Bennée        if argv and argv[0] == "--":
6165e03c2d8SAlex Bennée            argv = argv[1:]
6175e03c2d8SAlex Bennée        cwd = os.getcwd()
6185e03c2d8SAlex Bennée        cmd = ["--rm", "-w", cwd,
6195e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
6205e03c2d8SAlex Bennée        if args.paths:
6215e03c2d8SAlex Bennée            for p in args.paths:
6225e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
62399cfdb86SAlex Bennée        cmd += [args.image, args.cc]
6245e03c2d8SAlex Bennée        cmd += argv
62571ebbe09SAlex Bennée        return Docker().run(cmd, False, quiet=args.quiet,
62671ebbe09SAlex Bennée                            as_user=True)
6275e03c2d8SAlex Bennée
6285e03c2d8SAlex Bennée
629f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
630f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
631f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
632f97da1f7SAlex Bennée    name = "check"
633f97da1f7SAlex Bennée
634f97da1f7SAlex Bennée    def args(self, parser):
635f97da1f7SAlex Bennée        parser.add_argument("tag",
636f97da1f7SAlex Bennée                            help="Image Tag")
6377b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
6387b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
6397b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
6407b882245SAlex Bennée                            default="checksum", help="check type")
6417b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
6427b882245SAlex Bennée                            help="number of minutes")
643f97da1f7SAlex Bennée
644f97da1f7SAlex Bennée    def run(self, args, argv):
645f97da1f7SAlex Bennée        tag = args.tag
646f97da1f7SAlex Bennée
64743e1b2ffSAlex Bennée        try:
648f97da1f7SAlex Bennée            dkr = Docker()
649432d8ad5SAlex Bennée        except subprocess.CalledProcessError:
65043e1b2ffSAlex Bennée            print("Docker not set up")
65143e1b2ffSAlex Bennée            return 1
65243e1b2ffSAlex Bennée
653f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
654f97da1f7SAlex Bennée        if info is None:
655f97da1f7SAlex Bennée            print("Image does not exist")
656f97da1f7SAlex Bennée            return 1
657f97da1f7SAlex Bennée
6587b882245SAlex Bennée        if args.checktype == "checksum":
6597b882245SAlex Bennée            if not args.dockerfile:
6607b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
6617b882245SAlex Bennée                return 1
6627b882245SAlex Bennée
6634112aff7SAlex Bennée            dockerfile = _read_dockerfile(args.dockerfile)
6647b882245SAlex Bennée
665f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
666f97da1f7SAlex Bennée                if not args.quiet:
667f97da1f7SAlex Bennée                    print("Image is up to date")
668f97da1f7SAlex Bennée                return 0
669f97da1f7SAlex Bennée            else:
670f97da1f7SAlex Bennée                print("Image needs updating")
671f97da1f7SAlex Bennée                return 1
6727b882245SAlex Bennée        elif args.checktype == "age":
6737b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
6747b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
6757b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
6767b882245SAlex Bennée            if created < past:
6777b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
6787b882245SAlex Bennée                       (timestr, args.olderthan))
6797b882245SAlex Bennée                return 1
6807b882245SAlex Bennée            else:
6817b882245SAlex Bennée                if not args.quiet:
6827b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
6837b882245SAlex Bennée                return 0
684f97da1f7SAlex Bennée
685f97da1f7SAlex Bennée
6864485b04bSFam Zhengdef main():
6879459f754SMarc-André Lureau    global USE_ENGINE
6889459f754SMarc-André Lureau
6894485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
690432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
691432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
6929459f754SMarc-André Lureau    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
6939459f754SMarc-André Lureau                        help="specify which container engine to use")
6944485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
6954485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
6964485b04bSFam Zheng        cmd = cls()
6974485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
6984485b04bSFam Zheng        cmd.shared_args(subp)
6994485b04bSFam Zheng        cmd.args(subp)
7004485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
7014485b04bSFam Zheng    args, argv = parser.parse_known_args()
7028480517dSAlex Bennée    if args.engine:
7039459f754SMarc-André Lureau        USE_ENGINE = args.engine
7044485b04bSFam Zheng    return args.cmdobj.run(args, argv)
7054485b04bSFam Zheng
706432d8ad5SAlex Bennée
7074485b04bSFam Zhengif __name__ == "__main__":
7084485b04bSFam Zheng    sys.exit(main())
709