xref: /qemu/tests/docker/docker.py (revision 4112aff7cdd932f273e920911a45a5d5a2d5d299)
1*4112aff7SAlex 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
594485b04bSFam Zhengdef _text_checksum(text):
604485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
61*4112aff7SAlex Bennée    return hashlib.sha1(text.encode('utf-8')).hexdigest()
624485b04bSFam Zheng
63*4112aff7SAlex Bennéedef _read_dockerfile(path):
64*4112aff7SAlex Bennée    return open(path, 'rt', encoding='utf-8').read()
65432d8ad5SAlex Bennée
66438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
67*4112aff7SAlex Bennée    return _text_checksum(_read_dockerfile(filename))
68438d1168SPhilippe Mathieu-Daudé
69432d8ad5SAlex Bennée
709459f754SMarc-André Lureaudef _guess_engine_command():
719459f754SMarc-André Lureau    """ Guess a working engine command or raise exception if not found"""
729459f754SMarc-André Lureau    commands = []
739459f754SMarc-André Lureau
749459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]:
759459f754SMarc-André Lureau        commands += [["podman"]]
769459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]:
779459f754SMarc-André Lureau        commands += [["docker"], ["sudo", "-n", "docker"]]
784485b04bSFam Zheng    for cmd in commands:
790679f98bSEduardo Habkost        try:
8083405c45SAlex Bennée            # docker version will return the client details in stdout
8183405c45SAlex Bennée            # but still report a status of 1 if it can't contact the daemon
8283405c45SAlex Bennée            if subprocess.call(cmd + ["version"],
83c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
844485b04bSFam Zheng                return cmd
850679f98bSEduardo Habkost        except OSError:
860679f98bSEduardo Habkost            pass
874485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
889459f754SMarc-André Lureau    raise Exception("Cannot find working engine command. Tried:\n%s" %
894485b04bSFam Zheng                    commands_txt)
904485b04bSFam Zheng
91432d8ad5SAlex Bennée
922499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'):
93504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
94504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
95504ca3c2SAlex Bennée    try:
96504ca3c2SAlex Bennée        os.makedirs(dest_dir)
97504ca3c2SAlex Bennée    except OSError:
98504ca3c2SAlex Bennée        # we can safely ignore already created directories
99504ca3c2SAlex Bennée        pass
100504ca3c2SAlex Bennée
101504ca3c2SAlex Bennée    dest_file = "%s/%s" % (dest_dir, os.path.basename(src))
102504ca3c2SAlex Bennée    copy(src, dest_file)
103504ca3c2SAlex Bennée
104504ca3c2SAlex Bennée
105504ca3c2SAlex Bennéedef _get_so_libs(executable):
106504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
107504ca3c2SAlex Bennée
108504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
109504ca3c2SAlex Bennée    ensure theright data is copied."""
110504ca3c2SAlex Bennée
111504ca3c2SAlex Bennée    libs = []
112504ca3c2SAlex Bennée    ldd_re = re.compile(r"(/.*/)(\S*)")
113504ca3c2SAlex Bennée    try:
114504ca3c2SAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable])
115504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
116504ca3c2SAlex Bennée            search = ldd_re.search(line)
117504ca3c2SAlex Bennée            if search and len(search.groups()) == 2:
118504ca3c2SAlex Bennée                so_path = search.groups()[0]
119504ca3c2SAlex Bennée                so_lib = search.groups()[1]
120504ca3c2SAlex Bennée                libs.append("%s/%s" % (so_path, so_lib))
121504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
122f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
123504ca3c2SAlex Bennée
124504ca3c2SAlex Bennée    return libs
125504ca3c2SAlex Bennée
126432d8ad5SAlex Bennée
127d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
128d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
129d10404b1SAlex Bennée
130d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
131d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
132d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
133d10404b1SAlex Bennée    is in the image.
134504ca3c2SAlex Bennée
135504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
136d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
137d10404b1SAlex Bennée    """
138504ca3c2SAlex Bennée
139d10404b1SAlex Bennée    if bin_dest:
140d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
141d10404b1SAlex Bennée    else:
142d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
143504ca3c2SAlex Bennée
144504ca3c2SAlex Bennée    libs = _get_so_libs(src)
145504ca3c2SAlex Bennée    if libs:
146504ca3c2SAlex Bennée        for l in libs:
147504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
148504ca3c2SAlex Bennée            _copy_with_mkdir(l, dest_dir, so_path)
149504ca3c2SAlex Bennée
15015352decSAlex Bennée
15115352decSAlex Bennéedef _check_binfmt_misc(executable):
15215352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
15315352decSAlex Bennée
15415352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
15515352decSAlex Bennée    this script but we should at least fail early with a useful
156d10404b1SAlex Bennée    message if it won't work.
157d10404b1SAlex Bennée
158d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
159d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
160d10404b1SAlex Bennée    libraries.
161d10404b1SAlex Bennée    """
16215352decSAlex Bennée
16315352decSAlex Bennée    binary = os.path.basename(executable)
16415352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
16515352decSAlex Bennée
16615352decSAlex Bennée    if not os.path.exists(binfmt_entry):
16715352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
168d10404b1SAlex Bennée        return None, False
16915352decSAlex Bennée
17015352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
17115352decSAlex Bennée
17243c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
173432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
17443c898b7SAlex Bennée              (binary))
175d10404b1SAlex Bennée        return None, True
17643c898b7SAlex Bennée
1777e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1787e81d198SAlex Bennée    interp = m.group(1)
1797e81d198SAlex Bennée    if interp and interp != executable:
1807e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1817e81d198SAlex Bennée              (binary, executable, interp))
18215352decSAlex Bennée
183d10404b1SAlex Bennée    return interp, True
184d10404b1SAlex Bennée
18515352decSAlex Bennée
186c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
187547cb45eSAlex Bennée    # special case for Debian linux-user images
188547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
189547cb45eSAlex Bennée        img_name = "debian-bootstrap"
190547cb45eSAlex Bennée
191c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
192c1958e9dSFam Zheng                      img_name + ".docker")
193*4112aff7SAlex Bennée    return _read_dockerfile(df)
194c1958e9dSFam Zheng
195432d8ad5SAlex Bennée
196c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
197c1958e9dSFam Zheng    out = ""
198c1958e9dSFam Zheng    for l in df.splitlines():
199c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
200c1958e9dSFam Zheng            continue
201c1958e9dSFam Zheng        from_pref = "FROM qemu:"
202c1958e9dSFam Zheng        if l.startswith(from_pref):
203c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
204c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
205c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
206c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
207c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
208c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
209c1958e9dSFam Zheng            continue
210c1958e9dSFam Zheng        out += l + "\n"
211c1958e9dSFam Zheng    return out
212c1958e9dSFam Zheng
213432d8ad5SAlex Bennée
2144485b04bSFam Zhengclass Docker(object):
2154485b04bSFam Zheng    """ Running Docker commands """
2164485b04bSFam Zheng    def __init__(self):
2179459f754SMarc-André Lureau        self._command = _guess_engine_command()
2184485b04bSFam Zheng        self._instances = []
2194485b04bSFam Zheng        atexit.register(self._kill_instances)
22097cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
22197cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
2224485b04bSFam Zheng
22358bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2244485b04bSFam Zheng        if quiet:
225c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2264485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2274485b04bSFam Zheng
2280b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2290b95ff72SFam Zheng        if quiet:
2300b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2310b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2320b95ff72SFam Zheng
2334485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2344485b04bSFam Zheng        cmd = ["ps", "-q"]
2354485b04bSFam Zheng        if not only_active:
2364485b04bSFam Zheng            cmd.append("-a")
2374485b04bSFam Zheng        for i in self._output(cmd).split():
2384485b04bSFam Zheng            resp = self._output(["inspect", i])
2394485b04bSFam Zheng            labels = json.loads(resp)[0]["Config"]["Labels"]
2404485b04bSFam Zheng            active = json.loads(resp)[0]["State"]["Running"]
2414485b04bSFam Zheng            if not labels:
2424485b04bSFam Zheng                continue
2434485b04bSFam Zheng            instance_uuid = labels.get("com.qemu.instance.uuid", None)
2444485b04bSFam Zheng            if not instance_uuid:
2454485b04bSFam Zheng                continue
2464485b04bSFam Zheng            if only_known and instance_uuid not in self._instances:
2474485b04bSFam Zheng                continue
248f03868bdSEduardo Habkost            print("Terminating", i)
2494485b04bSFam Zheng            if active:
2504485b04bSFam Zheng                self._do(["kill", i])
2514485b04bSFam Zheng            self._do(["rm", i])
2524485b04bSFam Zheng
2534485b04bSFam Zheng    def clean(self):
2544485b04bSFam Zheng        self._do_kill_instances(False, False)
2554485b04bSFam Zheng        return 0
2564485b04bSFam Zheng
25797cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2584485b04bSFam Zheng        return self._do_kill_instances(True)
2594485b04bSFam Zheng
2604485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2614485b04bSFam Zheng        return subprocess.check_output(self._command + cmd,
2624485b04bSFam Zheng                                       stderr=subprocess.STDOUT,
263*4112aff7SAlex Bennée                                       encoding='utf-8',
2644485b04bSFam Zheng                                       **kwargs)
2654485b04bSFam Zheng
266f97da1f7SAlex Bennée    def inspect_tag(self, tag):
267f97da1f7SAlex Bennée        try:
268f97da1f7SAlex Bennée            return self._output(["inspect", tag])
269f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
270f97da1f7SAlex Bennée            return None
271f97da1f7SAlex Bennée
2727b882245SAlex Bennée    def get_image_creation_time(self, info):
2737b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2747b882245SAlex Bennée
2754485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
276f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2774485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2784485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
2794485b04bSFam Zheng
280414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
281438d1168SPhilippe Mathieu-Daudé                    quiet=True, user=False, argv=None, extra_files_cksum=[]):
282432d8ad5SAlex Bennée        if argv is None:
2834485b04bSFam Zheng            argv = []
2844485b04bSFam Zheng
285*4112aff7SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
286*4112aff7SAlex Bennée                                             encoding='utf-8',
287*4112aff7SAlex Bennée                                             dir=docker_dir, suffix=".docker")
2884485b04bSFam Zheng        tmp_df.write(dockerfile)
2894485b04bSFam Zheng
290414a8ce5SAlex Bennée        if user:
291414a8ce5SAlex Bennée            uid = os.getuid()
292414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
293414a8ce5SAlex Bennée            tmp_df.write("\n")
294414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
295414a8ce5SAlex Bennée                         (uname, uid, uname))
296414a8ce5SAlex Bennée
2974485b04bSFam Zheng        tmp_df.write("\n")
2984485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
299f9172822SAlex Bennée                     _text_checksum(_dockerfile_preprocess(dockerfile)))
300f9172822SAlex Bennée        for f, c in extra_files_cksum:
301f9172822SAlex Bennée            tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
302f9172822SAlex Bennée
3034485b04bSFam Zheng        tmp_df.flush()
304a9f8d038SAlex Bennée
305432d8ad5SAlex Bennée        self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv +
306a9f8d038SAlex Bennée                       [docker_dir],
3074485b04bSFam Zheng                       quiet=quiet)
3084485b04bSFam Zheng
3096e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
3106e733da6SAlex Bennée        "Update a tagged image using "
3116e733da6SAlex Bennée
3120b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
3136e733da6SAlex Bennée
3144485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
3154485b04bSFam Zheng        try:
3164485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
3174485b04bSFam Zheng        except Exception:
3184485b04bSFam Zheng            return False
319c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
3204485b04bSFam Zheng
3214485b04bSFam Zheng    def run(self, cmd, keep, quiet):
3224485b04bSFam Zheng        label = uuid.uuid1().hex
3234485b04bSFam Zheng        if not keep:
3244485b04bSFam Zheng            self._instances.append(label)
3250b95ff72SFam Zheng        ret = self._do_check(["run", "--label",
3264485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3274485b04bSFam Zheng                             quiet=quiet)
3284485b04bSFam Zheng        if not keep:
3294485b04bSFam Zheng            self._instances.remove(label)
3304485b04bSFam Zheng        return ret
3314485b04bSFam Zheng
3324b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3334b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3344b08af60SFam Zheng
335432d8ad5SAlex Bennée
3364485b04bSFam Zhengclass SubCommand(object):
3374485b04bSFam Zheng    """A SubCommand template base class"""
3384485b04bSFam Zheng    name = None  # Subcommand name
339432d8ad5SAlex Bennée
3404485b04bSFam Zheng    def shared_args(self, parser):
3414485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
342e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3434485b04bSFam Zheng
3444485b04bSFam Zheng    def args(self, parser):
3454485b04bSFam Zheng        """Setup argument parser"""
3464485b04bSFam Zheng        pass
347432d8ad5SAlex Bennée
3484485b04bSFam Zheng    def run(self, args, argv):
3494485b04bSFam Zheng        """Run command.
3504485b04bSFam Zheng        args: parsed argument by argument parser.
3514485b04bSFam Zheng        argv: remaining arguments from sys.argv.
3524485b04bSFam Zheng        """
3534485b04bSFam Zheng        pass
3544485b04bSFam Zheng
355432d8ad5SAlex Bennée
3564485b04bSFam Zhengclass RunCommand(SubCommand):
3574485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
3584485b04bSFam Zheng    name = "run"
359432d8ad5SAlex Bennée
3604485b04bSFam Zheng    def args(self, parser):
3614485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
3624485b04bSFam Zheng                            help="Don't remove image when command completes")
3632461d80eSMarc-André Lureau        parser.add_argument("--run-as-current-user", action="store_true",
3642461d80eSMarc-André Lureau                            help="Run container using the current user's uid")
365432d8ad5SAlex Bennée
3664485b04bSFam Zheng    def run(self, args, argv):
3672461d80eSMarc-André Lureau        if args.run_as_current_user:
3682461d80eSMarc-André Lureau            uid = os.getuid()
3692461d80eSMarc-André Lureau            argv = [ "-u", str(uid) ] + argv
3709459f754SMarc-André Lureau            docker = Docker()
3719459f754SMarc-André Lureau            if docker._command[0] == "podman":
3729459f754SMarc-André Lureau                argv = [ "--uidmap", "%d:0:1" % uid,
3739459f754SMarc-André Lureau                         "--uidmap", "0:1:%d" % uid,
3749459f754SMarc-André Lureau                         "--uidmap", "%d:%d:64536" % (uid + 1, uid + 1)] + argv
3754485b04bSFam Zheng        return Docker().run(argv, args.keep, quiet=args.quiet)
3764485b04bSFam Zheng
377432d8ad5SAlex Bennée
3784485b04bSFam Zhengclass BuildCommand(SubCommand):
379432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
3804485b04bSFam Zheng    name = "build"
381432d8ad5SAlex Bennée
3824485b04bSFam Zheng    def args(self, parser):
383504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
384504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
385504ca3c2SAlex Bennée                            container together with all its dependent
386504ca3c2SAlex Bennée                            libraries""")
3874c84f662SPhilippe Mathieu-Daudé        parser.add_argument("--extra-files", "-f", nargs='*',
3884c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
3894c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
3904c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
391414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
392414a8ce5SAlex Bennée                            action="store_true",
393414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
3944485b04bSFam Zheng        parser.add_argument("tag",
3954485b04bSFam Zheng                            help="Image Tag")
3964485b04bSFam Zheng        parser.add_argument("dockerfile",
3974485b04bSFam Zheng                            help="Dockerfile name")
3984485b04bSFam Zheng
3994485b04bSFam Zheng    def run(self, args, argv):
400*4112aff7SAlex Bennée        dockerfile = _read_dockerfile(args.dockerfile)
4014485b04bSFam Zheng        tag = args.tag
4024485b04bSFam Zheng
4034485b04bSFam Zheng        dkr = Docker()
4046fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
4056fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
4064485b04bSFam Zheng            if not args.quiet:
407f03868bdSEduardo Habkost                print("Image is up to date.")
408a9f8d038SAlex Bennée        else:
409a9f8d038SAlex Bennée            # Create a docker context directory for the build
410a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
4114485b04bSFam Zheng
41215352decSAlex Bennée            # Validate binfmt_misc will work
41315352decSAlex Bennée            if args.include_executable:
414d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
415d10404b1SAlex Bennée                if not enabled:
41615352decSAlex Bennée                    return 1
41715352decSAlex Bennée
418920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
419920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
420920776eaSAlex Bennée            if os.path.exists(docker_pre):
421f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
422920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
423f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
424920776eaSAlex Bennée                if rc == 3:
425f03868bdSEduardo Habkost                    print("Skip")
426920776eaSAlex Bennée                    return 0
427920776eaSAlex Bennée                elif rc != 0:
428f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
429920776eaSAlex Bennée                    return 1
430920776eaSAlex Bennée
4314c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
4324c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
433438d1168SPhilippe Mathieu-Daudé            cksum = []
434504ca3c2SAlex Bennée            if args.include_executable:
435438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
436438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
437438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
438d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
439d10404b1SAlex Bennée                                       qpath, docker_dir)
440d10404b1SAlex Bennée
4414c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
4424c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
443f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
444504ca3c2SAlex Bennée
44506cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
446*4112aff7SAlex Bennée                     for k, v in os.environ.items()
44706cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
448a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
449438d1168SPhilippe Mathieu-Daudé                            quiet=args.quiet, user=args.user, argv=argv,
450438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
451a9f8d038SAlex Bennée
452a9f8d038SAlex Bennée            rmtree(docker_dir)
453a9f8d038SAlex Bennée
4544485b04bSFam Zheng        return 0
4554485b04bSFam Zheng
456432d8ad5SAlex Bennée
4576e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
458432d8ad5SAlex Bennée    """ Update a docker image with new executables. Args: <tag> <executable>"""
4596e733da6SAlex Bennée    name = "update"
460432d8ad5SAlex Bennée
4616e733da6SAlex Bennée    def args(self, parser):
4626e733da6SAlex Bennée        parser.add_argument("tag",
4636e733da6SAlex Bennée                            help="Image Tag")
4646e733da6SAlex Bennée        parser.add_argument("executable",
4656e733da6SAlex Bennée                            help="Executable to copy")
4666e733da6SAlex Bennée
4676e733da6SAlex Bennée    def run(self, args, argv):
4686e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
4696e733da6SAlex Bennée        # dockerfile for the update
4706e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
4716e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
4726e733da6SAlex Bennée
4737e81d198SAlex Bennée        # Add the executable to the tarball, using the current
474d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
475d10404b1SAlex Bennée        # only need the support libraries copied
476d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
4777e81d198SAlex Bennée
478d10404b1SAlex Bennée        if not enabled:
479d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
480d10404b1SAlex Bennée            return 1
481d10404b1SAlex Bennée
482d10404b1SAlex Bennée        if ff:
4836e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
4846e733da6SAlex Bennée
4856e733da6SAlex Bennée        # Add any associated libraries
4866e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
4876e733da6SAlex Bennée        if libs:
4886e733da6SAlex Bennée            for l in libs:
4896e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
4906e733da6SAlex Bennée
4916e733da6SAlex Bennée        # Create a Docker buildfile
4926e733da6SAlex Bennée        df = StringIO()
4936e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
4946e733da6SAlex Bennée        df.write("ADD . /\n")
4956e733da6SAlex Bennée        df.seek(0)
4966e733da6SAlex Bennée
4976e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
4986e733da6SAlex Bennée        df_tar.size = len(df.buf)
4996e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
5006e733da6SAlex Bennée
5016e733da6SAlex Bennée        tmp_tar.close()
5026e733da6SAlex Bennée
5036e733da6SAlex Bennée        # reset the file pointers
5046e733da6SAlex Bennée        tmp.flush()
5056e733da6SAlex Bennée        tmp.seek(0)
5066e733da6SAlex Bennée
5076e733da6SAlex Bennée        # Run the build with our tarball context
5086e733da6SAlex Bennée        dkr = Docker()
5096e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
5106e733da6SAlex Bennée
5116e733da6SAlex Bennée        return 0
5126e733da6SAlex Bennée
513432d8ad5SAlex Bennée
5144485b04bSFam Zhengclass CleanCommand(SubCommand):
5154485b04bSFam Zheng    """Clean up docker instances"""
5164485b04bSFam Zheng    name = "clean"
517432d8ad5SAlex Bennée
5184485b04bSFam Zheng    def run(self, args, argv):
5194485b04bSFam Zheng        Docker().clean()
5204485b04bSFam Zheng        return 0
5214485b04bSFam Zheng
522432d8ad5SAlex Bennée
5234b08af60SFam Zhengclass ImagesCommand(SubCommand):
5244b08af60SFam Zheng    """Run "docker images" command"""
5254b08af60SFam Zheng    name = "images"
526432d8ad5SAlex Bennée
5274b08af60SFam Zheng    def run(self, args, argv):
5284b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
5294b08af60SFam Zheng
53015df9d37SAlex Bennée
53115df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
53215df9d37SAlex Bennée    """Probe if we can run docker automatically"""
53315df9d37SAlex Bennée    name = "probe"
53415df9d37SAlex Bennée
53515df9d37SAlex Bennée    def run(self, args, argv):
53615df9d37SAlex Bennée        try:
53715df9d37SAlex Bennée            docker = Docker()
53815df9d37SAlex Bennée            if docker._command[0] == "docker":
539f03868bdSEduardo Habkost                print("yes")
54015df9d37SAlex Bennée            elif docker._command[0] == "sudo":
541f03868bdSEduardo Habkost                print("sudo")
5429459f754SMarc-André Lureau            elif docker._command[0] == "podman":
5439459f754SMarc-André Lureau                print("podman")
54415df9d37SAlex Bennée        except Exception:
545f03868bdSEduardo Habkost            print("no")
54615df9d37SAlex Bennée
54715df9d37SAlex Bennée        return
54815df9d37SAlex Bennée
54915df9d37SAlex Bennée
5505e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
5515e03c2d8SAlex Bennée    """Compile sources with cc in images"""
5525e03c2d8SAlex Bennée    name = "cc"
5535e03c2d8SAlex Bennée
5545e03c2d8SAlex Bennée    def args(self, parser):
5555e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
5565e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
55799cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
55899cfdb86SAlex Bennée                            help="The compiler executable to call")
55950b72738SAlex Bennée        parser.add_argument("--user",
56050b72738SAlex Bennée                            help="The user-id to run under")
5615e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
5625e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
5635e03c2d8SAlex Bennée                            reading sources""")
5645e03c2d8SAlex Bennée
5655e03c2d8SAlex Bennée    def run(self, args, argv):
5665e03c2d8SAlex Bennée        if argv and argv[0] == "--":
5675e03c2d8SAlex Bennée            argv = argv[1:]
5685e03c2d8SAlex Bennée        cwd = os.getcwd()
5695e03c2d8SAlex Bennée        cmd = ["--rm", "-w", cwd,
5705e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
5715e03c2d8SAlex Bennée        if args.paths:
5725e03c2d8SAlex Bennée            for p in args.paths:
5735e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
57450b72738SAlex Bennée        if args.user:
57550b72738SAlex Bennée            cmd += ["-u", args.user]
57699cfdb86SAlex Bennée        cmd += [args.image, args.cc]
5775e03c2d8SAlex Bennée        cmd += argv
5785e03c2d8SAlex Bennée        return Docker().command("run", cmd, args.quiet)
5795e03c2d8SAlex Bennée
5805e03c2d8SAlex Bennée
581f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
582f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
583f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
584f97da1f7SAlex Bennée    name = "check"
585f97da1f7SAlex Bennée
586f97da1f7SAlex Bennée    def args(self, parser):
587f97da1f7SAlex Bennée        parser.add_argument("tag",
588f97da1f7SAlex Bennée                            help="Image Tag")
5897b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
5907b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
5917b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
5927b882245SAlex Bennée                            default="checksum", help="check type")
5937b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
5947b882245SAlex Bennée                            help="number of minutes")
595f97da1f7SAlex Bennée
596f97da1f7SAlex Bennée    def run(self, args, argv):
597f97da1f7SAlex Bennée        tag = args.tag
598f97da1f7SAlex Bennée
59943e1b2ffSAlex Bennée        try:
600f97da1f7SAlex Bennée            dkr = Docker()
601432d8ad5SAlex Bennée        except subprocess.CalledProcessError:
60243e1b2ffSAlex Bennée            print("Docker not set up")
60343e1b2ffSAlex Bennée            return 1
60443e1b2ffSAlex Bennée
605f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
606f97da1f7SAlex Bennée        if info is None:
607f97da1f7SAlex Bennée            print("Image does not exist")
608f97da1f7SAlex Bennée            return 1
609f97da1f7SAlex Bennée
6107b882245SAlex Bennée        if args.checktype == "checksum":
6117b882245SAlex Bennée            if not args.dockerfile:
6127b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
6137b882245SAlex Bennée                return 1
6147b882245SAlex Bennée
615*4112aff7SAlex Bennée            dockerfile = _read_dockerfile(args.dockerfile)
6167b882245SAlex Bennée
617f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
618f97da1f7SAlex Bennée                if not args.quiet:
619f97da1f7SAlex Bennée                    print("Image is up to date")
620f97da1f7SAlex Bennée                return 0
621f97da1f7SAlex Bennée            else:
622f97da1f7SAlex Bennée                print("Image needs updating")
623f97da1f7SAlex Bennée                return 1
6247b882245SAlex Bennée        elif args.checktype == "age":
6257b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
6267b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
6277b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
6287b882245SAlex Bennée            if created < past:
6297b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
6307b882245SAlex Bennée                       (timestr, args.olderthan))
6317b882245SAlex Bennée                return 1
6327b882245SAlex Bennée            else:
6337b882245SAlex Bennée                if not args.quiet:
6347b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
6357b882245SAlex Bennée                return 0
636f97da1f7SAlex Bennée
637f97da1f7SAlex Bennée
6384485b04bSFam Zhengdef main():
6399459f754SMarc-André Lureau    global USE_ENGINE
6409459f754SMarc-André Lureau
6414485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
642432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
643432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
6449459f754SMarc-André Lureau    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
6459459f754SMarc-André Lureau                        help="specify which container engine to use")
6464485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
6474485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
6484485b04bSFam Zheng        cmd = cls()
6494485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
6504485b04bSFam Zheng        cmd.shared_args(subp)
6514485b04bSFam Zheng        cmd.args(subp)
6524485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
6534485b04bSFam Zheng    args, argv = parser.parse_known_args()
6549459f754SMarc-André Lureau    USE_ENGINE = args.engine
6554485b04bSFam Zheng    return args.cmdobj.run(args, argv)
6564485b04bSFam Zheng
657432d8ad5SAlex Bennée
6584485b04bSFam Zhengif __name__ == "__main__":
6594485b04bSFam Zheng    sys.exit(main())
660