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