xref: /qemu/tests/docker/docker.py (revision dfae62845961556935c6b8ccbb4285d4688c42b4)
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
594485b04bSFam Zhengdef _text_checksum(text):
604485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
614112aff7SAlex Bennée    return hashlib.sha1(text.encode('utf-8')).hexdigest()
624485b04bSFam Zheng
634112aff7SAlex Bennéedef _read_dockerfile(path):
644112aff7SAlex Bennée    return open(path, 'rt', encoding='utf-8').read()
65432d8ad5SAlex Bennée
66438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
674112aff7SAlex 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 the right data is copied."""
110504ca3c2SAlex Bennée
111504ca3c2SAlex Bennée    libs = []
1125e33f7feSAlex Bennée    ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)")
113504ca3c2SAlex Bennée    try:
114eea2153eSAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8')
115504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
116504ca3c2SAlex Bennée            search = ldd_re.search(line)
1175e33f7feSAlex Bennée            if search:
1185e33f7feSAlex Bennée                try:
1195e33f7feSAlex Bennée                    libs.append(s.group(1))
1205e33f7feSAlex Bennée                except IndexError:
1215e33f7feSAlex Bennée                    pass
122504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
123f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
124504ca3c2SAlex Bennée
125504ca3c2SAlex Bennée    return libs
126504ca3c2SAlex Bennée
127432d8ad5SAlex Bennée
128d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
129d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
130d10404b1SAlex Bennée
131d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
132d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
133d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
134d10404b1SAlex Bennée    is in the image.
135504ca3c2SAlex Bennée
136504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
137d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
138d10404b1SAlex Bennée    """
139504ca3c2SAlex Bennée
140d10404b1SAlex Bennée    if bin_dest:
141d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
142d10404b1SAlex Bennée    else:
143d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
144504ca3c2SAlex Bennée
145504ca3c2SAlex Bennée    libs = _get_so_libs(src)
146504ca3c2SAlex Bennée    if libs:
147504ca3c2SAlex Bennée        for l in libs:
148504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
1495e33f7feSAlex Bennée            real_l = os.path.realpath(l)
1505e33f7feSAlex Bennée            _copy_with_mkdir(real_l, dest_dir, so_path)
151504ca3c2SAlex Bennée
15215352decSAlex Bennée
15315352decSAlex Bennéedef _check_binfmt_misc(executable):
15415352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
15515352decSAlex Bennée
15615352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
15715352decSAlex Bennée    this script but we should at least fail early with a useful
158d10404b1SAlex Bennée    message if it won't work.
159d10404b1SAlex Bennée
160d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
161d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
162d10404b1SAlex Bennée    libraries.
163d10404b1SAlex Bennée    """
16415352decSAlex Bennée
16515352decSAlex Bennée    binary = os.path.basename(executable)
16615352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
16715352decSAlex Bennée
16815352decSAlex Bennée    if not os.path.exists(binfmt_entry):
16915352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
170d10404b1SAlex Bennée        return None, False
17115352decSAlex Bennée
17215352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
17315352decSAlex Bennée
17443c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
175432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
17643c898b7SAlex Bennée              (binary))
177d10404b1SAlex Bennée        return None, True
17843c898b7SAlex Bennée
1797e81d198SAlex Bennée    m = re.search("interpreter (\S+)\n", entry)
1807e81d198SAlex Bennée    interp = m.group(1)
1817e81d198SAlex Bennée    if interp and interp != executable:
1827e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1837e81d198SAlex Bennée              (binary, executable, interp))
18415352decSAlex Bennée
185d10404b1SAlex Bennée    return interp, True
186d10404b1SAlex Bennée
18715352decSAlex Bennée
188c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
189547cb45eSAlex Bennée    # special case for Debian linux-user images
190547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
191547cb45eSAlex Bennée        img_name = "debian-bootstrap"
192547cb45eSAlex Bennée
193c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
194c1958e9dSFam Zheng                      img_name + ".docker")
1954112aff7SAlex Bennée    return _read_dockerfile(df)
196c1958e9dSFam Zheng
197432d8ad5SAlex Bennée
198c1958e9dSFam Zhengdef _dockerfile_preprocess(df):
199c1958e9dSFam Zheng    out = ""
200c1958e9dSFam Zheng    for l in df.splitlines():
201c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
202c1958e9dSFam Zheng            continue
203c1958e9dSFam Zheng        from_pref = "FROM qemu:"
204c1958e9dSFam Zheng        if l.startswith(from_pref):
205c1958e9dSFam Zheng            # TODO: Alternatively we could replace this line with "FROM $ID"
206c1958e9dSFam Zheng            # where $ID is the image's hex id obtained with
207c1958e9dSFam Zheng            #    $ docker images $IMAGE --format="{{.Id}}"
208c1958e9dSFam Zheng            # but unfortunately that's not supported by RHEL 7.
209c1958e9dSFam Zheng            inlining = _read_qemu_dockerfile(l[len(from_pref):])
210c1958e9dSFam Zheng            out += _dockerfile_preprocess(inlining)
211c1958e9dSFam Zheng            continue
212c1958e9dSFam Zheng        out += l + "\n"
213c1958e9dSFam Zheng    return out
214c1958e9dSFam Zheng
215432d8ad5SAlex Bennée
2164485b04bSFam Zhengclass Docker(object):
2174485b04bSFam Zheng    """ Running Docker commands """
2184485b04bSFam Zheng    def __init__(self):
2199459f754SMarc-André Lureau        self._command = _guess_engine_command()
220529994e2SAlex Bennée        self._instance = None
2214485b04bSFam Zheng        atexit.register(self._kill_instances)
22297cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
22397cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
2244485b04bSFam Zheng
22558bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2264485b04bSFam Zheng        if quiet:
227c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2284485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2294485b04bSFam Zheng
2300b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2310b95ff72SFam Zheng        if quiet:
2320b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2330b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2340b95ff72SFam Zheng
2354485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2364485b04bSFam Zheng        cmd = ["ps", "-q"]
2374485b04bSFam Zheng        if not only_active:
2384485b04bSFam Zheng            cmd.append("-a")
239529994e2SAlex Bennée
240529994e2SAlex Bennée        filter = "--filter=label=com.qemu.instance.uuid"
241529994e2SAlex Bennée        if only_known:
242529994e2SAlex Bennée            if self._instance:
243529994e2SAlex Bennée                filter += "=%s" % (self._instance)
244529994e2SAlex Bennée            else:
245529994e2SAlex Bennée                # no point trying to kill, we finished
246529994e2SAlex Bennée                return
247529994e2SAlex Bennée
248529994e2SAlex Bennée        print("filter=%s" % (filter))
249529994e2SAlex Bennée        cmd.append(filter)
2504485b04bSFam Zheng        for i in self._output(cmd).split():
251529994e2SAlex Bennée            self._do(["rm", "-f", 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):
2612d110c11SJohn Snow        try:
2624485b04bSFam Zheng            return subprocess.check_output(self._command + cmd,
2634485b04bSFam Zheng                                           stderr=subprocess.STDOUT,
2644112aff7SAlex Bennée                                           encoding='utf-8',
2654485b04bSFam Zheng                                           **kwargs)
2662d110c11SJohn Snow        except TypeError:
2672d110c11SJohn Snow            # 'encoding' argument was added in 3.6+
268884fcafcSAlex Bennée            return subprocess.check_output(self._command + cmd,
269884fcafcSAlex Bennée                                           stderr=subprocess.STDOUT,
270884fcafcSAlex Bennée                                           **kwargs).decode('utf-8')
271884fcafcSAlex Bennée
2724485b04bSFam Zheng
273f97da1f7SAlex Bennée    def inspect_tag(self, tag):
274f97da1f7SAlex Bennée        try:
275f97da1f7SAlex Bennée            return self._output(["inspect", tag])
276f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
277f97da1f7SAlex Bennée            return None
278f97da1f7SAlex Bennée
2797b882245SAlex Bennée    def get_image_creation_time(self, info):
2807b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2817b882245SAlex Bennée
2824485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
283f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2844485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2854485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
2864485b04bSFam Zheng
287414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
288438d1168SPhilippe Mathieu-Daudé                    quiet=True, user=False, argv=None, extra_files_cksum=[]):
289432d8ad5SAlex Bennée        if argv is None:
2904485b04bSFam Zheng            argv = []
2914485b04bSFam Zheng
2924112aff7SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
2934112aff7SAlex Bennée                                             encoding='utf-8',
2944112aff7SAlex Bennée                                             dir=docker_dir, suffix=".docker")
2954485b04bSFam Zheng        tmp_df.write(dockerfile)
2964485b04bSFam Zheng
297414a8ce5SAlex Bennée        if user:
298414a8ce5SAlex Bennée            uid = os.getuid()
299414a8ce5SAlex Bennée            uname = getpwuid(uid).pw_name
300414a8ce5SAlex Bennée            tmp_df.write("\n")
301414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
302414a8ce5SAlex Bennée                         (uname, uid, uname))
303414a8ce5SAlex Bennée
3044485b04bSFam Zheng        tmp_df.write("\n")
3054485b04bSFam Zheng        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" %
306f9172822SAlex Bennée                     _text_checksum(_dockerfile_preprocess(dockerfile)))
307f9172822SAlex Bennée        for f, c in extra_files_cksum:
308f9172822SAlex Bennée            tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c))
309f9172822SAlex Bennée
3104485b04bSFam Zheng        tmp_df.flush()
311a9f8d038SAlex Bennée
312432d8ad5SAlex Bennée        self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv +
313a9f8d038SAlex Bennée                       [docker_dir],
3144485b04bSFam Zheng                       quiet=quiet)
3154485b04bSFam Zheng
3166e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
3176e733da6SAlex Bennée        "Update a tagged image using "
3186e733da6SAlex Bennée
3190b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
3206e733da6SAlex Bennée
3214485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
3224485b04bSFam Zheng        try:
3234485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
3244485b04bSFam Zheng        except Exception:
3254485b04bSFam Zheng            return False
326c1958e9dSFam Zheng        return checksum == _text_checksum(_dockerfile_preprocess(dockerfile))
3274485b04bSFam Zheng
32871ebbe09SAlex Bennée    def run(self, cmd, keep, quiet, as_user=False):
329529994e2SAlex Bennée        label = uuid.uuid4().hex
3304485b04bSFam Zheng        if not keep:
331529994e2SAlex Bennée            self._instance = label
33271ebbe09SAlex Bennée
33371ebbe09SAlex Bennée        if as_user:
33471ebbe09SAlex Bennée            uid = os.getuid()
33571ebbe09SAlex Bennée            cmd = [ "-u", str(uid) ] + cmd
33671ebbe09SAlex Bennée            # podman requires a bit more fiddling
33771ebbe09SAlex Bennée            if self._command[0] == "podman":
338b3a790beSJohn Snow                cmd.insert(0, '--userns=keep-id')
33971ebbe09SAlex Bennée
3400b95ff72SFam Zheng        ret = self._do_check(["run", "--label",
3414485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3424485b04bSFam Zheng                             quiet=quiet)
3434485b04bSFam Zheng        if not keep:
344529994e2SAlex Bennée            self._instance = None
3454485b04bSFam Zheng        return ret
3464485b04bSFam Zheng
3474b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3484b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3494b08af60SFam Zheng
350432d8ad5SAlex Bennée
3514485b04bSFam Zhengclass SubCommand(object):
3524485b04bSFam Zheng    """A SubCommand template base class"""
3534485b04bSFam Zheng    name = None  # Subcommand name
354432d8ad5SAlex Bennée
3554485b04bSFam Zheng    def shared_args(self, parser):
3564485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
357e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3584485b04bSFam Zheng
3594485b04bSFam Zheng    def args(self, parser):
3604485b04bSFam Zheng        """Setup argument parser"""
3614485b04bSFam Zheng        pass
362432d8ad5SAlex Bennée
3634485b04bSFam Zheng    def run(self, args, argv):
3644485b04bSFam Zheng        """Run command.
3654485b04bSFam Zheng        args: parsed argument by argument parser.
3664485b04bSFam Zheng        argv: remaining arguments from sys.argv.
3674485b04bSFam Zheng        """
3684485b04bSFam Zheng        pass
3694485b04bSFam Zheng
370432d8ad5SAlex Bennée
3714485b04bSFam Zhengclass RunCommand(SubCommand):
3724485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
3734485b04bSFam Zheng    name = "run"
374432d8ad5SAlex Bennée
3754485b04bSFam Zheng    def args(self, parser):
3764485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
3774485b04bSFam Zheng                            help="Don't remove image when command completes")
3782461d80eSMarc-André Lureau        parser.add_argument("--run-as-current-user", action="store_true",
3792461d80eSMarc-André Lureau                            help="Run container using the current user's uid")
380432d8ad5SAlex Bennée
3814485b04bSFam Zheng    def run(self, args, argv):
38271ebbe09SAlex Bennée        return Docker().run(argv, args.keep, quiet=args.quiet,
38371ebbe09SAlex Bennée                            as_user=args.run_as_current_user)
3844485b04bSFam Zheng
385432d8ad5SAlex Bennée
3864485b04bSFam Zhengclass BuildCommand(SubCommand):
387432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
3884485b04bSFam Zheng    name = "build"
389432d8ad5SAlex Bennée
3904485b04bSFam Zheng    def args(self, parser):
391504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
392504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
393504ca3c2SAlex Bennée                            container together with all its dependent
394504ca3c2SAlex Bennée                            libraries""")
395*dfae6284SPaolo Bonzini        parser.add_argument("--extra-files", nargs='*',
3964c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
3974c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
3984c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
399414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
400414a8ce5SAlex Bennée                            action="store_true",
401414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
402*dfae6284SPaolo Bonzini        parser.add_argument("-t", dest="tag",
4034485b04bSFam Zheng                            help="Image Tag")
404*dfae6284SPaolo Bonzini        parser.add_argument("-f", dest="dockerfile",
4054485b04bSFam Zheng                            help="Dockerfile name")
4064485b04bSFam Zheng
4074485b04bSFam Zheng    def run(self, args, argv):
4084112aff7SAlex Bennée        dockerfile = _read_dockerfile(args.dockerfile)
4094485b04bSFam Zheng        tag = args.tag
4104485b04bSFam Zheng
4114485b04bSFam Zheng        dkr = Docker()
4126fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
4136fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
4144485b04bSFam Zheng            if not args.quiet:
415f03868bdSEduardo Habkost                print("Image is up to date.")
416a9f8d038SAlex Bennée        else:
417a9f8d038SAlex Bennée            # Create a docker context directory for the build
418a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
4194485b04bSFam Zheng
42015352decSAlex Bennée            # Validate binfmt_misc will work
42115352decSAlex Bennée            if args.include_executable:
422d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
423d10404b1SAlex Bennée                if not enabled:
42415352decSAlex Bennée                    return 1
42515352decSAlex Bennée
426920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
427920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
428920776eaSAlex Bennée            if os.path.exists(docker_pre):
429f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
430920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
431f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
432920776eaSAlex Bennée                if rc == 3:
433f03868bdSEduardo Habkost                    print("Skip")
434920776eaSAlex Bennée                    return 0
435920776eaSAlex Bennée                elif rc != 0:
436f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
437920776eaSAlex Bennée                    return 1
438920776eaSAlex Bennée
4394c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
4404c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
441438d1168SPhilippe Mathieu-Daudé            cksum = []
442504ca3c2SAlex Bennée            if args.include_executable:
443438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
444438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
445438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
446d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
447d10404b1SAlex Bennée                                       qpath, docker_dir)
448d10404b1SAlex Bennée
4494c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
4504c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
451f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
452504ca3c2SAlex Bennée
45306cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
4544112aff7SAlex Bennée                     for k, v in os.environ.items()
45506cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
456a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
457438d1168SPhilippe Mathieu-Daudé                            quiet=args.quiet, user=args.user, argv=argv,
458438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
459a9f8d038SAlex Bennée
460a9f8d038SAlex Bennée            rmtree(docker_dir)
461a9f8d038SAlex Bennée
4624485b04bSFam Zheng        return 0
4634485b04bSFam Zheng
464432d8ad5SAlex Bennée
4656e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
466432d8ad5SAlex Bennée    """ Update a docker image with new executables. Args: <tag> <executable>"""
4676e733da6SAlex Bennée    name = "update"
468432d8ad5SAlex Bennée
4696e733da6SAlex Bennée    def args(self, parser):
4706e733da6SAlex Bennée        parser.add_argument("tag",
4716e733da6SAlex Bennée                            help="Image Tag")
4726e733da6SAlex Bennée        parser.add_argument("executable",
4736e733da6SAlex Bennée                            help="Executable to copy")
4746e733da6SAlex Bennée
4756e733da6SAlex Bennée    def run(self, args, argv):
4766e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
4776e733da6SAlex Bennée        # dockerfile for the update
4786e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
4796e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
4806e733da6SAlex Bennée
4817e81d198SAlex Bennée        # Add the executable to the tarball, using the current
482d10404b1SAlex Bennée        # configured binfmt_misc path. If we don't get a path then we
483d10404b1SAlex Bennée        # only need the support libraries copied
484d10404b1SAlex Bennée        ff, enabled = _check_binfmt_misc(args.executable)
4857e81d198SAlex Bennée
486d10404b1SAlex Bennée        if not enabled:
487d10404b1SAlex Bennée            print("binfmt_misc not enabled, update disabled")
488d10404b1SAlex Bennée            return 1
489d10404b1SAlex Bennée
490d10404b1SAlex Bennée        if ff:
4916e733da6SAlex Bennée            tmp_tar.add(args.executable, arcname=ff)
4926e733da6SAlex Bennée
4936e733da6SAlex Bennée        # Add any associated libraries
4946e733da6SAlex Bennée        libs = _get_so_libs(args.executable)
4956e733da6SAlex Bennée        if libs:
4966e733da6SAlex Bennée            for l in libs:
4976e733da6SAlex Bennée                tmp_tar.add(os.path.realpath(l), arcname=l)
4986e733da6SAlex Bennée
4996e733da6SAlex Bennée        # Create a Docker buildfile
5006e733da6SAlex Bennée        df = StringIO()
5016e733da6SAlex Bennée        df.write("FROM %s\n" % args.tag)
5026e733da6SAlex Bennée        df.write("ADD . /\n")
5036e733da6SAlex Bennée        df.seek(0)
5046e733da6SAlex Bennée
5056e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
5066e733da6SAlex Bennée        df_tar.size = len(df.buf)
5076e733da6SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df)
5086e733da6SAlex Bennée
5096e733da6SAlex Bennée        tmp_tar.close()
5106e733da6SAlex Bennée
5116e733da6SAlex Bennée        # reset the file pointers
5126e733da6SAlex Bennée        tmp.flush()
5136e733da6SAlex Bennée        tmp.seek(0)
5146e733da6SAlex Bennée
5156e733da6SAlex Bennée        # Run the build with our tarball context
5166e733da6SAlex Bennée        dkr = Docker()
5176e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
5186e733da6SAlex Bennée
5196e733da6SAlex Bennée        return 0
5206e733da6SAlex Bennée
521432d8ad5SAlex Bennée
5224485b04bSFam Zhengclass CleanCommand(SubCommand):
5234485b04bSFam Zheng    """Clean up docker instances"""
5244485b04bSFam Zheng    name = "clean"
525432d8ad5SAlex Bennée
5264485b04bSFam Zheng    def run(self, args, argv):
5274485b04bSFam Zheng        Docker().clean()
5284485b04bSFam Zheng        return 0
5294485b04bSFam Zheng
530432d8ad5SAlex Bennée
5314b08af60SFam Zhengclass ImagesCommand(SubCommand):
5324b08af60SFam Zheng    """Run "docker images" command"""
5334b08af60SFam Zheng    name = "images"
534432d8ad5SAlex Bennée
5354b08af60SFam Zheng    def run(self, args, argv):
5364b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
5374b08af60SFam Zheng
53815df9d37SAlex Bennée
53915df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
54015df9d37SAlex Bennée    """Probe if we can run docker automatically"""
54115df9d37SAlex Bennée    name = "probe"
54215df9d37SAlex Bennée
54315df9d37SAlex Bennée    def run(self, args, argv):
54415df9d37SAlex Bennée        try:
54515df9d37SAlex Bennée            docker = Docker()
54615df9d37SAlex Bennée            if docker._command[0] == "docker":
5478480517dSAlex Bennée                print("docker")
54815df9d37SAlex Bennée            elif docker._command[0] == "sudo":
5498480517dSAlex Bennée                print("sudo docker")
5509459f754SMarc-André Lureau            elif docker._command[0] == "podman":
5519459f754SMarc-André Lureau                print("podman")
55215df9d37SAlex Bennée        except Exception:
553f03868bdSEduardo Habkost            print("no")
55415df9d37SAlex Bennée
55515df9d37SAlex Bennée        return
55615df9d37SAlex Bennée
55715df9d37SAlex Bennée
5585e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
5595e03c2d8SAlex Bennée    """Compile sources with cc in images"""
5605e03c2d8SAlex Bennée    name = "cc"
5615e03c2d8SAlex Bennée
5625e03c2d8SAlex Bennée    def args(self, parser):
5635e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
5645e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
56599cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
56699cfdb86SAlex Bennée                            help="The compiler executable to call")
5675e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
5685e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
5695e03c2d8SAlex Bennée                            reading sources""")
5705e03c2d8SAlex Bennée
5715e03c2d8SAlex Bennée    def run(self, args, argv):
5725e03c2d8SAlex Bennée        if argv and argv[0] == "--":
5735e03c2d8SAlex Bennée            argv = argv[1:]
5745e03c2d8SAlex Bennée        cwd = os.getcwd()
5755e03c2d8SAlex Bennée        cmd = ["--rm", "-w", cwd,
5765e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
5775e03c2d8SAlex Bennée        if args.paths:
5785e03c2d8SAlex Bennée            for p in args.paths:
5795e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
58099cfdb86SAlex Bennée        cmd += [args.image, args.cc]
5815e03c2d8SAlex Bennée        cmd += argv
58271ebbe09SAlex Bennée        return Docker().run(cmd, False, quiet=args.quiet,
58371ebbe09SAlex Bennée                            as_user=True)
5845e03c2d8SAlex Bennée
5855e03c2d8SAlex Bennée
586f97da1f7SAlex Bennéeclass CheckCommand(SubCommand):
587f97da1f7SAlex Bennée    """Check if we need to re-build a docker image out of a dockerfile.
588f97da1f7SAlex Bennée    Arguments: <tag> <dockerfile>"""
589f97da1f7SAlex Bennée    name = "check"
590f97da1f7SAlex Bennée
591f97da1f7SAlex Bennée    def args(self, parser):
592f97da1f7SAlex Bennée        parser.add_argument("tag",
593f97da1f7SAlex Bennée                            help="Image Tag")
5947b882245SAlex Bennée        parser.add_argument("dockerfile", default=None,
5957b882245SAlex Bennée                            help="Dockerfile name", nargs='?')
5967b882245SAlex Bennée        parser.add_argument("--checktype", choices=["checksum", "age"],
5977b882245SAlex Bennée                            default="checksum", help="check type")
5987b882245SAlex Bennée        parser.add_argument("--olderthan", default=60, type=int,
5997b882245SAlex Bennée                            help="number of minutes")
600f97da1f7SAlex Bennée
601f97da1f7SAlex Bennée    def run(self, args, argv):
602f97da1f7SAlex Bennée        tag = args.tag
603f97da1f7SAlex Bennée
60443e1b2ffSAlex Bennée        try:
605f97da1f7SAlex Bennée            dkr = Docker()
606432d8ad5SAlex Bennée        except subprocess.CalledProcessError:
60743e1b2ffSAlex Bennée            print("Docker not set up")
60843e1b2ffSAlex Bennée            return 1
60943e1b2ffSAlex Bennée
610f97da1f7SAlex Bennée        info = dkr.inspect_tag(tag)
611f97da1f7SAlex Bennée        if info is None:
612f97da1f7SAlex Bennée            print("Image does not exist")
613f97da1f7SAlex Bennée            return 1
614f97da1f7SAlex Bennée
6157b882245SAlex Bennée        if args.checktype == "checksum":
6167b882245SAlex Bennée            if not args.dockerfile:
6177b882245SAlex Bennée                print("Need a dockerfile for tag:%s" % (tag))
6187b882245SAlex Bennée                return 1
6197b882245SAlex Bennée
6204112aff7SAlex Bennée            dockerfile = _read_dockerfile(args.dockerfile)
6217b882245SAlex Bennée
622f97da1f7SAlex Bennée            if dkr.image_matches_dockerfile(tag, dockerfile):
623f97da1f7SAlex Bennée                if not args.quiet:
624f97da1f7SAlex Bennée                    print("Image is up to date")
625f97da1f7SAlex Bennée                return 0
626f97da1f7SAlex Bennée            else:
627f97da1f7SAlex Bennée                print("Image needs updating")
628f97da1f7SAlex Bennée                return 1
6297b882245SAlex Bennée        elif args.checktype == "age":
6307b882245SAlex Bennée            timestr = dkr.get_image_creation_time(info).split(".")[0]
6317b882245SAlex Bennée            created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
6327b882245SAlex Bennée            past = datetime.now() - timedelta(minutes=args.olderthan)
6337b882245SAlex Bennée            if created < past:
6347b882245SAlex Bennée                print ("Image created @ %s more than %d minutes old" %
6357b882245SAlex Bennée                       (timestr, args.olderthan))
6367b882245SAlex Bennée                return 1
6377b882245SAlex Bennée            else:
6387b882245SAlex Bennée                if not args.quiet:
6397b882245SAlex Bennée                    print ("Image less than %d minutes old" % (args.olderthan))
6407b882245SAlex Bennée                return 0
641f97da1f7SAlex Bennée
642f97da1f7SAlex Bennée
6434485b04bSFam Zhengdef main():
6449459f754SMarc-André Lureau    global USE_ENGINE
6459459f754SMarc-André Lureau
6464485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
647432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
648432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
6499459f754SMarc-André Lureau    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
6509459f754SMarc-André Lureau                        help="specify which container engine to use")
6514485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
6524485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
6534485b04bSFam Zheng        cmd = cls()
6544485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
6554485b04bSFam Zheng        cmd.shared_args(subp)
6564485b04bSFam Zheng        cmd.args(subp)
6574485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
6584485b04bSFam Zheng    args, argv = parser.parse_known_args()
6598480517dSAlex Bennée    if args.engine:
6609459f754SMarc-André Lureau        USE_ENGINE = args.engine
6614485b04bSFam Zheng    return args.cmdobj.run(args, argv)
6624485b04bSFam Zheng
663432d8ad5SAlex Bennée
6644485b04bSFam Zhengif __name__ == "__main__":
6654485b04bSFam Zheng    sys.exit(main())
666