xref: /qemu/tests/docker/docker.py (revision 604b70f6a4d072ddf7f00d3fdb5b977111fdeef1)
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
26e387ef47SMarc-André Lureauimport getpass
276e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo
28e336cec3SAlex Bennéefrom io import StringIO, BytesIO
29a9f8d038SAlex Bennéefrom shutil import copy, rmtree
307b882245SAlex Bennéefrom datetime import datetime, timedelta
314485b04bSFam Zheng
32c9772570SSascha Silbe
3306cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy']
3406cc3551SPhilippe Mathieu-Daudé
3506cc3551SPhilippe Mathieu-Daudé
36c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb')
37c9772570SSascha Silbe
389459f754SMarc-André Lureauclass EngineEnum(enum.IntEnum):
399459f754SMarc-André Lureau    AUTO = 1
409459f754SMarc-André Lureau    DOCKER = 2
419459f754SMarc-André Lureau    PODMAN = 3
429459f754SMarc-André Lureau
439459f754SMarc-André Lureau    def __str__(self):
449459f754SMarc-André Lureau        return self.name.lower()
459459f754SMarc-André Lureau
469459f754SMarc-André Lureau    def __repr__(self):
479459f754SMarc-André Lureau        return str(self)
489459f754SMarc-André Lureau
499459f754SMarc-André Lureau    @staticmethod
509459f754SMarc-André Lureau    def argparse(s):
519459f754SMarc-André Lureau        try:
529459f754SMarc-André Lureau            return EngineEnum[s.upper()]
539459f754SMarc-André Lureau        except KeyError:
549459f754SMarc-André Lureau            return s
559459f754SMarc-André Lureau
569459f754SMarc-André Lureau
579459f754SMarc-André LureauUSE_ENGINE = EngineEnum.AUTO
58c9772570SSascha Silbe
59af509738SPaolo Bonzinidef _bytes_checksum(bytes):
60af509738SPaolo Bonzini    """Calculate a digest string unique to the text content"""
61af509738SPaolo Bonzini    return hashlib.sha1(bytes).hexdigest()
62af509738SPaolo Bonzini
634485b04bSFam Zhengdef _text_checksum(text):
644485b04bSFam Zheng    """Calculate a digest string unique to the text content"""
65af509738SPaolo Bonzini    return _bytes_checksum(text.encode('utf-8'))
664485b04bSFam Zheng
674112aff7SAlex Bennéedef _read_dockerfile(path):
684112aff7SAlex Bennée    return open(path, 'rt', encoding='utf-8').read()
69432d8ad5SAlex Bennée
70438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename):
71af509738SPaolo Bonzini    return _bytes_checksum(open(filename, 'rb').read())
72438d1168SPhilippe Mathieu-Daudé
73432d8ad5SAlex Bennée
749459f754SMarc-André Lureaudef _guess_engine_command():
759459f754SMarc-André Lureau    """ Guess a working engine command or raise exception if not found"""
769459f754SMarc-André Lureau    commands = []
779459f754SMarc-André Lureau
789459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]:
799459f754SMarc-André Lureau        commands += [["podman"]]
809459f754SMarc-André Lureau    if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]:
819459f754SMarc-André Lureau        commands += [["docker"], ["sudo", "-n", "docker"]]
824485b04bSFam Zheng    for cmd in commands:
830679f98bSEduardo Habkost        try:
8483405c45SAlex Bennée            # docker version will return the client details in stdout
8583405c45SAlex Bennée            # but still report a status of 1 if it can't contact the daemon
8683405c45SAlex Bennée            if subprocess.call(cmd + ["version"],
87c9772570SSascha Silbe                               stdout=DEVNULL, stderr=DEVNULL) == 0:
884485b04bSFam Zheng                return cmd
890679f98bSEduardo Habkost        except OSError:
900679f98bSEduardo Habkost            pass
914485b04bSFam Zheng    commands_txt = "\n".join(["  " + " ".join(x) for x in commands])
929459f754SMarc-André Lureau    raise Exception("Cannot find working engine command. Tried:\n%s" %
934485b04bSFam Zheng                    commands_txt)
944485b04bSFam Zheng
95432d8ad5SAlex Bennée
963971c70fSAlex Bennéedef _copy_with_mkdir(src, root_dir, sub_path='.', name=None):
97504ca3c2SAlex Bennée    """Copy src into root_dir, creating sub_path as needed."""
98504ca3c2SAlex Bennée    dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path))
99504ca3c2SAlex Bennée    try:
100504ca3c2SAlex Bennée        os.makedirs(dest_dir)
101504ca3c2SAlex Bennée    except OSError:
102504ca3c2SAlex Bennée        # we can safely ignore already created directories
103504ca3c2SAlex Bennée        pass
104504ca3c2SAlex Bennée
1053971c70fSAlex Bennée    dest_file = "%s/%s" % (dest_dir, name if name else os.path.basename(src))
106dffccf3dSAlex Bennée
107dffccf3dSAlex Bennée    try:
108504ca3c2SAlex Bennée        copy(src, dest_file)
109dffccf3dSAlex Bennée    except FileNotFoundError:
110dffccf3dSAlex Bennée        print("Couldn't copy %s to %s" % (src, dest_file))
111dffccf3dSAlex Bennée        pass
112504ca3c2SAlex Bennée
113504ca3c2SAlex Bennée
114504ca3c2SAlex Bennéedef _get_so_libs(executable):
115504ca3c2SAlex Bennée    """Return a list of libraries associated with an executable.
116504ca3c2SAlex Bennée
117504ca3c2SAlex Bennée    The paths may be symbolic links which would need to be resolved to
118504ca3c2SAlex Bennée    ensure the right data is copied."""
119504ca3c2SAlex Bennée
120504ca3c2SAlex Bennée    libs = []
1215e33f7feSAlex Bennée    ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)")
122504ca3c2SAlex Bennée    try:
123eea2153eSAlex Bennée        ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8')
124504ca3c2SAlex Bennée        for line in ldd_output.split("\n"):
125504ca3c2SAlex Bennée            search = ldd_re.search(line)
1265e33f7feSAlex Bennée            if search:
1275e33f7feSAlex Bennée                try:
1284d8f6309SPhilippe Mathieu-Daudé                    libs.append(search.group(1))
1295e33f7feSAlex Bennée                except IndexError:
1305e33f7feSAlex Bennée                    pass
131504ca3c2SAlex Bennée    except subprocess.CalledProcessError:
132f03868bdSEduardo Habkost        print("%s had no associated libraries (static build?)" % (executable))
133504ca3c2SAlex Bennée
134504ca3c2SAlex Bennée    return libs
135504ca3c2SAlex Bennée
136432d8ad5SAlex Bennée
137d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir):
138d10404b1SAlex Bennée    """Maybe copy a binary and all its dependent libraries.
139d10404b1SAlex Bennée
140d10404b1SAlex Bennée    If bin_dest isn't set we only copy the support libraries because
141d10404b1SAlex Bennée    we don't need qemu in the docker path to run (due to persistent
142d10404b1SAlex Bennée    mapping). Indeed users may get confused if we aren't running what
143d10404b1SAlex Bennée    is in the image.
144504ca3c2SAlex Bennée
145504ca3c2SAlex Bennée    This does rely on the host file-system being fairly multi-arch
146d10404b1SAlex Bennée    aware so the file don't clash with the guests layout.
147d10404b1SAlex Bennée    """
148504ca3c2SAlex Bennée
149d10404b1SAlex Bennée    if bin_dest:
150d10404b1SAlex Bennée        _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest))
151d10404b1SAlex Bennée    else:
152d10404b1SAlex Bennée        print("only copying support libraries for %s" % (src))
153504ca3c2SAlex Bennée
154504ca3c2SAlex Bennée    libs = _get_so_libs(src)
155504ca3c2SAlex Bennée    if libs:
156504ca3c2SAlex Bennée        for l in libs:
157504ca3c2SAlex Bennée            so_path = os.path.dirname(l)
1583971c70fSAlex Bennée            name = os.path.basename(l)
1595e33f7feSAlex Bennée            real_l = os.path.realpath(l)
1603971c70fSAlex Bennée            _copy_with_mkdir(real_l, dest_dir, so_path, name)
161504ca3c2SAlex Bennée
16215352decSAlex Bennée
16315352decSAlex Bennéedef _check_binfmt_misc(executable):
16415352decSAlex Bennée    """Check binfmt_misc has entry for executable in the right place.
16515352decSAlex Bennée
16615352decSAlex Bennée    The details of setting up binfmt_misc are outside the scope of
16715352decSAlex Bennée    this script but we should at least fail early with a useful
168d10404b1SAlex Bennée    message if it won't work.
169d10404b1SAlex Bennée
170d10404b1SAlex Bennée    Returns the configured binfmt path and a valid flag. For
171d10404b1SAlex Bennée    persistent configurations we will still want to copy and dependent
172d10404b1SAlex Bennée    libraries.
173d10404b1SAlex Bennée    """
17415352decSAlex Bennée
17515352decSAlex Bennée    binary = os.path.basename(executable)
17615352decSAlex Bennée    binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary)
17715352decSAlex Bennée
17815352decSAlex Bennée    if not os.path.exists(binfmt_entry):
17915352decSAlex Bennée        print ("No binfmt_misc entry for %s" % (binary))
180d10404b1SAlex Bennée        return None, False
18115352decSAlex Bennée
18215352decSAlex Bennée    with open(binfmt_entry) as x: entry = x.read()
18315352decSAlex Bennée
18443c898b7SAlex Bennée    if re.search("flags:.*F.*\n", entry):
185432d8ad5SAlex Bennée        print("binfmt_misc for %s uses persistent(F) mapping to host binary" %
18643c898b7SAlex Bennée              (binary))
187d10404b1SAlex Bennée        return None, True
18843c898b7SAlex Bennée
189*a5e3cb3bSPaolo Bonzini    m = re.search(r"interpreter (\S+)\n", entry)
1907e81d198SAlex Bennée    interp = m.group(1)
1917e81d198SAlex Bennée    if interp and interp != executable:
1927e81d198SAlex Bennée        print("binfmt_misc for %s does not point to %s, using %s" %
1937e81d198SAlex Bennée              (binary, executable, interp))
19415352decSAlex Bennée
195d10404b1SAlex Bennée    return interp, True
196d10404b1SAlex Bennée
19715352decSAlex Bennée
198c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name):
199547cb45eSAlex Bennée    # special case for Debian linux-user images
200547cb45eSAlex Bennée    if img_name.startswith("debian") and img_name.endswith("user"):
201547cb45eSAlex Bennée        img_name = "debian-bootstrap"
202547cb45eSAlex Bennée
203c1958e9dSFam Zheng    df = os.path.join(os.path.dirname(__file__), "dockerfiles",
204c1958e9dSFam Zheng                      img_name + ".docker")
2054112aff7SAlex Bennée    return _read_dockerfile(df)
206c1958e9dSFam Zheng
207432d8ad5SAlex Bennée
20807056db1SAlex Bennéedef _dockerfile_verify_flat(df):
20907056db1SAlex Bennée    "Verify we do not include other qemu/ layers"
210c1958e9dSFam Zheng    for l in df.splitlines():
211c1958e9dSFam Zheng        if len(l.strip()) == 0 or l.startswith("#"):
212c1958e9dSFam Zheng            continue
213767b6bd2SAlex Bennée        from_pref = "FROM qemu/"
214c1958e9dSFam Zheng        if l.startswith(from_pref):
21507056db1SAlex Bennée            print("We no longer support multiple QEMU layers.")
21607056db1SAlex Bennée            print("Dockerfiles should be flat, ideally created by lcitool")
21707056db1SAlex Bennée            return False
21807056db1SAlex Bennée    return True
219c1958e9dSFam Zheng
220432d8ad5SAlex Bennée
2214485b04bSFam Zhengclass Docker(object):
2224485b04bSFam Zheng    """ Running Docker commands """
2234485b04bSFam Zheng    def __init__(self):
2249459f754SMarc-André Lureau        self._command = _guess_engine_command()
225e6f1306bSAlex Bennée
2266ddc3dc7SDaniel P. Berrangé        if ("docker" in self._command and
2276ddc3dc7SDaniel P. Berrangé            "TRAVIS" not in os.environ and
2286ddc3dc7SDaniel P. Berrangé            "GITLAB_CI" not in os.environ):
229e6f1306bSAlex Bennée            os.environ["DOCKER_BUILDKIT"] = "1"
230e6f1306bSAlex Bennée            self._buildkit = True
231e6f1306bSAlex Bennée        else:
232e6f1306bSAlex Bennée            self._buildkit = False
233e6f1306bSAlex Bennée
234529994e2SAlex Bennée        self._instance = None
2354485b04bSFam Zheng        atexit.register(self._kill_instances)
23697cba1a1SFam Zheng        signal.signal(signal.SIGTERM, self._kill_instances)
23797cba1a1SFam Zheng        signal.signal(signal.SIGHUP, self._kill_instances)
2384485b04bSFam Zheng
23958bf7b6dSFam Zheng    def _do(self, cmd, quiet=True, **kwargs):
2404485b04bSFam Zheng        if quiet:
241c9772570SSascha Silbe            kwargs["stdout"] = DEVNULL
2424485b04bSFam Zheng        return subprocess.call(self._command + cmd, **kwargs)
2434485b04bSFam Zheng
2440b95ff72SFam Zheng    def _do_check(self, cmd, quiet=True, **kwargs):
2450b95ff72SFam Zheng        if quiet:
2460b95ff72SFam Zheng            kwargs["stdout"] = DEVNULL
2470b95ff72SFam Zheng        return subprocess.check_call(self._command + cmd, **kwargs)
2480b95ff72SFam Zheng
2494485b04bSFam Zheng    def _do_kill_instances(self, only_known, only_active=True):
2504485b04bSFam Zheng        cmd = ["ps", "-q"]
2514485b04bSFam Zheng        if not only_active:
2524485b04bSFam Zheng            cmd.append("-a")
253529994e2SAlex Bennée
254529994e2SAlex Bennée        filter = "--filter=label=com.qemu.instance.uuid"
255529994e2SAlex Bennée        if only_known:
256529994e2SAlex Bennée            if self._instance:
257529994e2SAlex Bennée                filter += "=%s" % (self._instance)
258529994e2SAlex Bennée            else:
259529994e2SAlex Bennée                # no point trying to kill, we finished
260529994e2SAlex Bennée                return
261529994e2SAlex Bennée
262529994e2SAlex Bennée        print("filter=%s" % (filter))
263529994e2SAlex Bennée        cmd.append(filter)
2644485b04bSFam Zheng        for i in self._output(cmd).split():
265529994e2SAlex Bennée            self._do(["rm", "-f", i])
2664485b04bSFam Zheng
2674485b04bSFam Zheng    def clean(self):
2684485b04bSFam Zheng        self._do_kill_instances(False, False)
2694485b04bSFam Zheng        return 0
2704485b04bSFam Zheng
27197cba1a1SFam Zheng    def _kill_instances(self, *args, **kwargs):
2724485b04bSFam Zheng        return self._do_kill_instances(True)
2734485b04bSFam Zheng
2744485b04bSFam Zheng    def _output(self, cmd, **kwargs):
2752d110c11SJohn Snow        try:
2764485b04bSFam Zheng            return subprocess.check_output(self._command + cmd,
2774485b04bSFam Zheng                                           stderr=subprocess.STDOUT,
2784112aff7SAlex Bennée                                           encoding='utf-8',
2794485b04bSFam Zheng                                           **kwargs)
2802d110c11SJohn Snow        except TypeError:
2812d110c11SJohn Snow            # 'encoding' argument was added in 3.6+
282884fcafcSAlex Bennée            return subprocess.check_output(self._command + cmd,
283884fcafcSAlex Bennée                                           stderr=subprocess.STDOUT,
284884fcafcSAlex Bennée                                           **kwargs).decode('utf-8')
285884fcafcSAlex Bennée
2864485b04bSFam Zheng
287f97da1f7SAlex Bennée    def inspect_tag(self, tag):
288f97da1f7SAlex Bennée        try:
289f97da1f7SAlex Bennée            return self._output(["inspect", tag])
290f97da1f7SAlex Bennée        except subprocess.CalledProcessError:
291f97da1f7SAlex Bennée            return None
292f97da1f7SAlex Bennée
2937b882245SAlex Bennée    def get_image_creation_time(self, info):
2947b882245SAlex Bennée        return json.loads(info)[0]["Created"]
2957b882245SAlex Bennée
2964485b04bSFam Zheng    def get_image_dockerfile_checksum(self, tag):
297f97da1f7SAlex Bennée        resp = self.inspect_tag(tag)
2984485b04bSFam Zheng        labels = json.loads(resp)[0]["Config"].get("Labels", {})
2994485b04bSFam Zheng        return labels.get("com.qemu.dockerfile-checksum", "")
3004485b04bSFam Zheng
301414a8ce5SAlex Bennée    def build_image(self, tag, docker_dir, dockerfile,
302e6f1306bSAlex Bennée                    quiet=True, user=False, argv=None, registry=None,
303e6f1306bSAlex Bennée                    extra_files_cksum=[]):
304432d8ad5SAlex Bennée        if argv is None:
3054485b04bSFam Zheng            argv = []
3064485b04bSFam Zheng
30707056db1SAlex Bennée        if not _dockerfile_verify_flat(dockerfile):
30807056db1SAlex Bennée            return -1
309e6f1306bSAlex Bennée
31007056db1SAlex Bennée        checksum = _text_checksum(dockerfile)
311e6f1306bSAlex Bennée
3124112aff7SAlex Bennée        tmp_df = tempfile.NamedTemporaryFile(mode="w+t",
3134112aff7SAlex Bennée                                             encoding='utf-8',
3144112aff7SAlex Bennée                                             dir=docker_dir, suffix=".docker")
3154485b04bSFam Zheng        tmp_df.write(dockerfile)
3164485b04bSFam Zheng
317414a8ce5SAlex Bennée        if user:
318414a8ce5SAlex Bennée            uid = os.getuid()
319e387ef47SMarc-André Lureau            uname = getpass.getuser()
320414a8ce5SAlex Bennée            tmp_df.write("\n")
321414a8ce5SAlex Bennée            tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
322414a8ce5SAlex Bennée                         (uname, uid, uname))
323414a8ce5SAlex Bennée
3244485b04bSFam Zheng        tmp_df.write("\n")
325e405a3ebSAlessandro Di Federico        tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum))
326f9172822SAlex Bennée        for f, c in extra_files_cksum:
327e405a3ebSAlessandro Di Federico            tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c))
328f9172822SAlex Bennée
3294485b04bSFam Zheng        tmp_df.flush()
330a9f8d038SAlex Bennée
331e6f1306bSAlex Bennée        build_args = ["build", "-t", tag, "-f", tmp_df.name]
332e6f1306bSAlex Bennée        if self._buildkit:
333e6f1306bSAlex Bennée            build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"]
334e6f1306bSAlex Bennée
335e6f1306bSAlex Bennée        if registry is not None:
336f73e4852SAlex Bennée            pull_args = ["pull", "%s/%s" % (registry, tag)]
337f73e4852SAlex Bennée            self._do(pull_args, quiet=quiet)
338e6f1306bSAlex Bennée            cache = "%s/%s" % (registry, tag)
339e6f1306bSAlex Bennée            build_args += ["--cache-from", cache]
340e6f1306bSAlex Bennée        build_args += argv
341e6f1306bSAlex Bennée        build_args += [docker_dir]
342e6f1306bSAlex Bennée
343e6f1306bSAlex Bennée        self._do_check(build_args,
3444485b04bSFam Zheng                       quiet=quiet)
3454485b04bSFam Zheng
3466e733da6SAlex Bennée    def update_image(self, tag, tarball, quiet=True):
3476e733da6SAlex Bennée        "Update a tagged image using "
3486e733da6SAlex Bennée
3490b95ff72SFam Zheng        self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball)
3506e733da6SAlex Bennée
3514485b04bSFam Zheng    def image_matches_dockerfile(self, tag, dockerfile):
3524485b04bSFam Zheng        try:
3534485b04bSFam Zheng            checksum = self.get_image_dockerfile_checksum(tag)
3544485b04bSFam Zheng        except Exception:
3554485b04bSFam Zheng            return False
35607056db1SAlex Bennée        return checksum == _text_checksum(dockerfile)
3574485b04bSFam Zheng
35871ebbe09SAlex Bennée    def run(self, cmd, keep, quiet, as_user=False):
359529994e2SAlex Bennée        label = uuid.uuid4().hex
3604485b04bSFam Zheng        if not keep:
361529994e2SAlex Bennée            self._instance = label
36271ebbe09SAlex Bennée
36371ebbe09SAlex Bennée        if as_user:
36471ebbe09SAlex Bennée            uid = os.getuid()
36571ebbe09SAlex Bennée            cmd = [ "-u", str(uid) ] + cmd
36671ebbe09SAlex Bennée            # podman requires a bit more fiddling
36771ebbe09SAlex Bennée            if self._command[0] == "podman":
368b3a790beSJohn Snow                cmd.insert(0, '--userns=keep-id')
36971ebbe09SAlex Bennée
37017cd6e2bSPaolo Bonzini        ret = self._do_check(["run", "--rm", "--label",
3714485b04bSFam Zheng                             "com.qemu.instance.uuid=" + label] + cmd,
3724485b04bSFam Zheng                             quiet=quiet)
3734485b04bSFam Zheng        if not keep:
374529994e2SAlex Bennée            self._instance = None
3754485b04bSFam Zheng        return ret
3764485b04bSFam Zheng
3774b08af60SFam Zheng    def command(self, cmd, argv, quiet):
3784b08af60SFam Zheng        return self._do([cmd] + argv, quiet=quiet)
3794b08af60SFam Zheng
380432d8ad5SAlex Bennée
3814485b04bSFam Zhengclass SubCommand(object):
3824485b04bSFam Zheng    """A SubCommand template base class"""
3834485b04bSFam Zheng    name = None  # Subcommand name
384432d8ad5SAlex Bennée
3854485b04bSFam Zheng    def shared_args(self, parser):
3864485b04bSFam Zheng        parser.add_argument("--quiet", action="store_true",
387e50a6121SStefan Weil                            help="Run quietly unless an error occurred")
3884485b04bSFam Zheng
3894485b04bSFam Zheng    def args(self, parser):
3904485b04bSFam Zheng        """Setup argument parser"""
3914485b04bSFam Zheng        pass
392432d8ad5SAlex Bennée
3934485b04bSFam Zheng    def run(self, args, argv):
3944485b04bSFam Zheng        """Run command.
3954485b04bSFam Zheng        args: parsed argument by argument parser.
3964485b04bSFam Zheng        argv: remaining arguments from sys.argv.
3974485b04bSFam Zheng        """
3984485b04bSFam Zheng        pass
3994485b04bSFam Zheng
400432d8ad5SAlex Bennée
4014485b04bSFam Zhengclass RunCommand(SubCommand):
4024485b04bSFam Zheng    """Invoke docker run and take care of cleaning up"""
4034485b04bSFam Zheng    name = "run"
404432d8ad5SAlex Bennée
4054485b04bSFam Zheng    def args(self, parser):
4064485b04bSFam Zheng        parser.add_argument("--keep", action="store_true",
4074485b04bSFam Zheng                            help="Don't remove image when command completes")
4082461d80eSMarc-André Lureau        parser.add_argument("--run-as-current-user", action="store_true",
4092461d80eSMarc-André Lureau                            help="Run container using the current user's uid")
410432d8ad5SAlex Bennée
4114485b04bSFam Zheng    def run(self, args, argv):
41271ebbe09SAlex Bennée        return Docker().run(argv, args.keep, quiet=args.quiet,
41371ebbe09SAlex Bennée                            as_user=args.run_as_current_user)
4144485b04bSFam Zheng
415432d8ad5SAlex Bennée
4164485b04bSFam Zhengclass BuildCommand(SubCommand):
417432d8ad5SAlex Bennée    """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>"""
4184485b04bSFam Zheng    name = "build"
419432d8ad5SAlex Bennée
4204485b04bSFam Zheng    def args(self, parser):
421504ca3c2SAlex Bennée        parser.add_argument("--include-executable", "-e",
422504ca3c2SAlex Bennée                            help="""Specify a binary that will be copied to the
423504ca3c2SAlex Bennée                            container together with all its dependent
424504ca3c2SAlex Bennée                            libraries""")
425ddd5ed83SAlex Bennée        parser.add_argument("--skip-binfmt",
426ddd5ed83SAlex Bennée                            action="store_true",
427ddd5ed83SAlex Bennée                            help="""Skip binfmt entry check (used for testing)""")
428dfae6284SPaolo Bonzini        parser.add_argument("--extra-files", nargs='*',
4294c84f662SPhilippe Mathieu-Daudé                            help="""Specify files that will be copied in the
4304c84f662SPhilippe Mathieu-Daudé                            Docker image, fulfilling the ADD directive from the
4314c84f662SPhilippe Mathieu-Daudé                            Dockerfile""")
432414a8ce5SAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
433414a8ce5SAlex Bennée                            action="store_true",
434414a8ce5SAlex Bennée                            help="Add the current user to image's passwd")
435e6f1306bSAlex Bennée        parser.add_argument("--registry", "-r",
436e6f1306bSAlex Bennée                            help="cache from docker registry")
437dfae6284SPaolo Bonzini        parser.add_argument("-t", dest="tag",
4384485b04bSFam Zheng                            help="Image Tag")
439dfae6284SPaolo Bonzini        parser.add_argument("-f", dest="dockerfile",
4404485b04bSFam Zheng                            help="Dockerfile name")
4414485b04bSFam Zheng
4424485b04bSFam Zheng    def run(self, args, argv):
4434112aff7SAlex Bennée        dockerfile = _read_dockerfile(args.dockerfile)
4444485b04bSFam Zheng        tag = args.tag
4454485b04bSFam Zheng
4464485b04bSFam Zheng        dkr = Docker()
4476fe3ae3fSAlex Bennée        if "--no-cache" not in argv and \
4486fe3ae3fSAlex Bennée           dkr.image_matches_dockerfile(tag, dockerfile):
4494485b04bSFam Zheng            if not args.quiet:
450f03868bdSEduardo Habkost                print("Image is up to date.")
451a9f8d038SAlex Bennée        else:
452a9f8d038SAlex Bennée            # Create a docker context directory for the build
453a9f8d038SAlex Bennée            docker_dir = tempfile.mkdtemp(prefix="docker_build")
4544485b04bSFam Zheng
45515352decSAlex Bennée            # Validate binfmt_misc will work
456ddd5ed83SAlex Bennée            if args.skip_binfmt:
457ddd5ed83SAlex Bennée                qpath = args.include_executable
458ddd5ed83SAlex Bennée            elif args.include_executable:
459d10404b1SAlex Bennée                qpath, enabled = _check_binfmt_misc(args.include_executable)
460d10404b1SAlex Bennée                if not enabled:
46115352decSAlex Bennée                    return 1
46215352decSAlex Bennée
463920776eaSAlex Bennée            # Is there a .pre file to run in the build context?
464920776eaSAlex Bennée            docker_pre = os.path.splitext(args.dockerfile)[0]+".pre"
465920776eaSAlex Bennée            if os.path.exists(docker_pre):
466f8042deaSSascha Silbe                stdout = DEVNULL if args.quiet else None
467920776eaSAlex Bennée                rc = subprocess.call(os.path.realpath(docker_pre),
468f8042deaSSascha Silbe                                     cwd=docker_dir, stdout=stdout)
469920776eaSAlex Bennée                if rc == 3:
470f03868bdSEduardo Habkost                    print("Skip")
471920776eaSAlex Bennée                    return 0
472920776eaSAlex Bennée                elif rc != 0:
473f03868bdSEduardo Habkost                    print("%s exited with code %d" % (docker_pre, rc))
474920776eaSAlex Bennée                    return 1
475920776eaSAlex Bennée
4764c84f662SPhilippe Mathieu-Daudé            # Copy any extra files into the Docker context. These can be
4774c84f662SPhilippe Mathieu-Daudé            # included by the use of the ADD directive in the Dockerfile.
478438d1168SPhilippe Mathieu-Daudé            cksum = []
479504ca3c2SAlex Bennée            if args.include_executable:
480438d1168SPhilippe Mathieu-Daudé                # FIXME: there is no checksum of this executable and the linked
481438d1168SPhilippe Mathieu-Daudé                # libraries, once the image built any change of this executable
482438d1168SPhilippe Mathieu-Daudé                # or any library won't trigger another build.
483d10404b1SAlex Bennée                _copy_binary_with_libs(args.include_executable,
484d10404b1SAlex Bennée                                       qpath, docker_dir)
485d10404b1SAlex Bennée
4864c84f662SPhilippe Mathieu-Daudé            for filename in args.extra_files or []:
4874c84f662SPhilippe Mathieu-Daudé                _copy_with_mkdir(filename, docker_dir)
488f9172822SAlex Bennée                cksum += [(filename, _file_checksum(filename))]
489504ca3c2SAlex Bennée
49006cc3551SPhilippe Mathieu-Daudé            argv += ["--build-arg=" + k.lower() + "=" + v
4914112aff7SAlex Bennée                     for k, v in os.environ.items()
49206cc3551SPhilippe Mathieu-Daudé                     if k.lower() in FILTERED_ENV_NAMES]
493a9f8d038SAlex Bennée            dkr.build_image(tag, docker_dir, dockerfile,
494e6f1306bSAlex Bennée                            quiet=args.quiet, user=args.user,
495e6f1306bSAlex Bennée                            argv=argv, registry=args.registry,
496438d1168SPhilippe Mathieu-Daudé                            extra_files_cksum=cksum)
497a9f8d038SAlex Bennée
498a9f8d038SAlex Bennée            rmtree(docker_dir)
499a9f8d038SAlex Bennée
5004485b04bSFam Zheng        return 0
5014485b04bSFam Zheng
502c3ad9043SAlex Bennéeclass FetchCommand(SubCommand):
503c3ad9043SAlex Bennée    """ Fetch a docker image from the registry. Args: <tag> <registry>"""
504c3ad9043SAlex Bennée    name = "fetch"
505c3ad9043SAlex Bennée
506c3ad9043SAlex Bennée    def args(self, parser):
507c3ad9043SAlex Bennée        parser.add_argument("tag",
508c3ad9043SAlex Bennée                            help="Local tag for image")
509c3ad9043SAlex Bennée        parser.add_argument("registry",
510c3ad9043SAlex Bennée                            help="Docker registry")
511c3ad9043SAlex Bennée
512c3ad9043SAlex Bennée    def run(self, args, argv):
513c3ad9043SAlex Bennée        dkr = Docker()
514c3ad9043SAlex Bennée        dkr.command(cmd="pull", quiet=args.quiet,
515c3ad9043SAlex Bennée                    argv=["%s/%s" % (args.registry, args.tag)])
516c3ad9043SAlex Bennée        dkr.command(cmd="tag", quiet=args.quiet,
517c3ad9043SAlex Bennée                    argv=["%s/%s" % (args.registry, args.tag), args.tag])
518c3ad9043SAlex Bennée
519432d8ad5SAlex Bennée
5206e733da6SAlex Bennéeclass UpdateCommand(SubCommand):
521bf46c0eeSAlex Bennée    """ Update a docker image. Args: <tag> <actions>"""
5226e733da6SAlex Bennée    name = "update"
523432d8ad5SAlex Bennée
5246e733da6SAlex Bennée    def args(self, parser):
5256e733da6SAlex Bennée        parser.add_argument("tag",
5266e733da6SAlex Bennée                            help="Image Tag")
5278d628d07SAlex Bennée        parser.add_argument("--executable",
5286e733da6SAlex Bennée                            help="Executable to copy")
529bf46c0eeSAlex Bennée        parser.add_argument("--add-current-user", "-u", dest="user",
530bf46c0eeSAlex Bennée                            action="store_true",
531bf46c0eeSAlex Bennée                            help="Add the current user to image's passwd")
5326e733da6SAlex Bennée
5336e733da6SAlex Bennée    def run(self, args, argv):
5346e733da6SAlex Bennée        # Create a temporary tarball with our whole build context and
5356e733da6SAlex Bennée        # dockerfile for the update
5366e733da6SAlex Bennée        tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz")
5376e733da6SAlex Bennée        tmp_tar = TarFile(fileobj=tmp, mode='w')
5386e733da6SAlex Bennée
5398d628d07SAlex Bennée        # Create a Docker buildfile
5408d628d07SAlex Bennée        df = StringIO()
5418d628d07SAlex Bennée        df.write(u"FROM %s\n" % args.tag)
5428d628d07SAlex Bennée
5438d628d07SAlex Bennée        if args.executable:
5447e81d198SAlex Bennée            # Add the executable to the tarball, using the current
545d10404b1SAlex Bennée            # configured binfmt_misc path. If we don't get a path then we
546d10404b1SAlex Bennée            # only need the support libraries copied
547d10404b1SAlex Bennée            ff, enabled = _check_binfmt_misc(args.executable)
5487e81d198SAlex Bennée
549d10404b1SAlex Bennée            if not enabled:
550d10404b1SAlex Bennée                print("binfmt_misc not enabled, update disabled")
551d10404b1SAlex Bennée                return 1
552d10404b1SAlex Bennée
553d10404b1SAlex Bennée            if ff:
5546e733da6SAlex Bennée                tmp_tar.add(args.executable, arcname=ff)
5556e733da6SAlex Bennée
5566e733da6SAlex Bennée            # Add any associated libraries
5576e733da6SAlex Bennée            libs = _get_so_libs(args.executable)
5586e733da6SAlex Bennée            if libs:
5596e733da6SAlex Bennée                for l in libs:
5603218d829SAlex Bennée                    so_path = os.path.dirname(l)
5613218d829SAlex Bennée                    name = os.path.basename(l)
5623218d829SAlex Bennée                    real_l = os.path.realpath(l)
5633218d829SAlex Bennée                    try:
5643218d829SAlex Bennée                        tmp_tar.add(real_l, arcname="%s/%s" % (so_path, name))
5653218d829SAlex Bennée                    except FileNotFoundError:
5663218d829SAlex Bennée                        print("Couldn't add %s/%s to archive" % (so_path, name))
5673218d829SAlex Bennée                        pass
5686e733da6SAlex Bennée
569e336cec3SAlex Bennée            df.write(u"ADD . /\n")
570e336cec3SAlex Bennée
571bf46c0eeSAlex Bennée        if args.user:
572bf46c0eeSAlex Bennée            uid = os.getuid()
573e387ef47SMarc-André Lureau            uname = getpass.getuser()
574bf46c0eeSAlex Bennée            df.write("\n")
575bf46c0eeSAlex Bennée            df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" %
576bf46c0eeSAlex Bennée                     (uname, uid, uname))
577bf46c0eeSAlex Bennée
578e336cec3SAlex Bennée        df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8"))
5796e733da6SAlex Bennée
5806e733da6SAlex Bennée        df_tar = TarInfo(name="Dockerfile")
581e336cec3SAlex Bennée        df_tar.size = df_bytes.getbuffer().nbytes
582e336cec3SAlex Bennée        tmp_tar.addfile(df_tar, fileobj=df_bytes)
5836e733da6SAlex Bennée
5846e733da6SAlex Bennée        tmp_tar.close()
5856e733da6SAlex Bennée
5866e733da6SAlex Bennée        # reset the file pointers
5876e733da6SAlex Bennée        tmp.flush()
5886e733da6SAlex Bennée        tmp.seek(0)
5896e733da6SAlex Bennée
5906e733da6SAlex Bennée        # Run the build with our tarball context
5916e733da6SAlex Bennée        dkr = Docker()
5926e733da6SAlex Bennée        dkr.update_image(args.tag, tmp, quiet=args.quiet)
5936e733da6SAlex Bennée
5946e733da6SAlex Bennée        return 0
5956e733da6SAlex Bennée
596432d8ad5SAlex Bennée
5974485b04bSFam Zhengclass CleanCommand(SubCommand):
5984485b04bSFam Zheng    """Clean up docker instances"""
5994485b04bSFam Zheng    name = "clean"
600432d8ad5SAlex Bennée
6014485b04bSFam Zheng    def run(self, args, argv):
6024485b04bSFam Zheng        Docker().clean()
6034485b04bSFam Zheng        return 0
6044485b04bSFam Zheng
605432d8ad5SAlex Bennée
6064b08af60SFam Zhengclass ImagesCommand(SubCommand):
6074b08af60SFam Zheng    """Run "docker images" command"""
6084b08af60SFam Zheng    name = "images"
609432d8ad5SAlex Bennée
6104b08af60SFam Zheng    def run(self, args, argv):
6114b08af60SFam Zheng        return Docker().command("images", argv, args.quiet)
6124b08af60SFam Zheng
61315df9d37SAlex Bennée
61415df9d37SAlex Bennéeclass ProbeCommand(SubCommand):
61515df9d37SAlex Bennée    """Probe if we can run docker automatically"""
61615df9d37SAlex Bennée    name = "probe"
61715df9d37SAlex Bennée
61815df9d37SAlex Bennée    def run(self, args, argv):
61915df9d37SAlex Bennée        try:
62015df9d37SAlex Bennée            docker = Docker()
62115df9d37SAlex Bennée            if docker._command[0] == "docker":
6228480517dSAlex Bennée                print("docker")
62315df9d37SAlex Bennée            elif docker._command[0] == "sudo":
6248480517dSAlex Bennée                print("sudo docker")
6259459f754SMarc-André Lureau            elif docker._command[0] == "podman":
6269459f754SMarc-André Lureau                print("podman")
62715df9d37SAlex Bennée        except Exception:
628f03868bdSEduardo Habkost            print("no")
62915df9d37SAlex Bennée
63015df9d37SAlex Bennée        return
63115df9d37SAlex Bennée
63215df9d37SAlex Bennée
6335e03c2d8SAlex Bennéeclass CcCommand(SubCommand):
6345e03c2d8SAlex Bennée    """Compile sources with cc in images"""
6355e03c2d8SAlex Bennée    name = "cc"
6365e03c2d8SAlex Bennée
6375e03c2d8SAlex Bennée    def args(self, parser):
6385e03c2d8SAlex Bennée        parser.add_argument("--image", "-i", required=True,
6395e03c2d8SAlex Bennée                            help="The docker image in which to run cc")
64099cfdb86SAlex Bennée        parser.add_argument("--cc", default="cc",
64199cfdb86SAlex Bennée                            help="The compiler executable to call")
6425e03c2d8SAlex Bennée        parser.add_argument("--source-path", "-s", nargs="*", dest="paths",
6435e03c2d8SAlex Bennée                            help="""Extra paths to (ro) mount into container for
6445e03c2d8SAlex Bennée                            reading sources""")
6455e03c2d8SAlex Bennée
6465e03c2d8SAlex Bennée    def run(self, args, argv):
6475e03c2d8SAlex Bennée        if argv and argv[0] == "--":
6485e03c2d8SAlex Bennée            argv = argv[1:]
6495e03c2d8SAlex Bennée        cwd = os.getcwd()
65017cd6e2bSPaolo Bonzini        cmd = ["-w", cwd,
6515e03c2d8SAlex Bennée               "-v", "%s:%s:rw" % (cwd, cwd)]
6525e03c2d8SAlex Bennée        if args.paths:
6535e03c2d8SAlex Bennée            for p in args.paths:
6545e03c2d8SAlex Bennée                cmd += ["-v", "%s:%s:ro,z" % (p, p)]
65599cfdb86SAlex Bennée        cmd += [args.image, args.cc]
6565e03c2d8SAlex Bennée        cmd += argv
65771ebbe09SAlex Bennée        return Docker().run(cmd, False, quiet=args.quiet,
65871ebbe09SAlex Bennée                            as_user=True)
6595e03c2d8SAlex Bennée
6605e03c2d8SAlex Bennée
6614485b04bSFam Zhengdef main():
6629459f754SMarc-André Lureau    global USE_ENGINE
6639459f754SMarc-André Lureau
6644485b04bSFam Zheng    parser = argparse.ArgumentParser(description="A Docker helper",
665432d8ad5SAlex Bennée                                     usage="%s <subcommand> ..." %
666432d8ad5SAlex Bennée                                     os.path.basename(sys.argv[0]))
6679459f754SMarc-André Lureau    parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum),
6689459f754SMarc-André Lureau                        help="specify which container engine to use")
6694485b04bSFam Zheng    subparsers = parser.add_subparsers(title="subcommands", help=None)
6704485b04bSFam Zheng    for cls in SubCommand.__subclasses__():
6714485b04bSFam Zheng        cmd = cls()
6724485b04bSFam Zheng        subp = subparsers.add_parser(cmd.name, help=cmd.__doc__)
6734485b04bSFam Zheng        cmd.shared_args(subp)
6744485b04bSFam Zheng        cmd.args(subp)
6754485b04bSFam Zheng        subp.set_defaults(cmdobj=cmd)
6764485b04bSFam Zheng    args, argv = parser.parse_known_args()
6778480517dSAlex Bennée    if args.engine:
6789459f754SMarc-André Lureau        USE_ENGINE = args.engine
6794485b04bSFam Zheng    return args.cmdobj.run(args, argv)
6804485b04bSFam Zheng
681432d8ad5SAlex Bennée
6824485b04bSFam Zhengif __name__ == "__main__":
6834485b04bSFam Zheng    sys.exit(main())
684