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 27e336cec3SAlex Bennéefrom io import StringIO, BytesIO 28a9f8d038SAlex Bennéefrom shutil import copy, rmtree 29414a8ce5SAlex Bennéefrom pwd import getpwuid 307b882245SAlex Bennéefrom datetime import datetime, timedelta 314485b04bSFam Zheng 32c9772570SSascha Silbe 3306cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy'] 3406cc3551SPhilippe Mathieu-Daudé 3506cc3551SPhilippe Mathieu-Daudé 36c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb') 37c9772570SSascha Silbe 389459f754SMarc-André Lureauclass EngineEnum(enum.IntEnum): 399459f754SMarc-André Lureau AUTO = 1 409459f754SMarc-André Lureau DOCKER = 2 419459f754SMarc-André Lureau PODMAN = 3 429459f754SMarc-André Lureau 439459f754SMarc-André Lureau def __str__(self): 449459f754SMarc-André Lureau return self.name.lower() 459459f754SMarc-André Lureau 469459f754SMarc-André Lureau def __repr__(self): 479459f754SMarc-André Lureau return str(self) 489459f754SMarc-André Lureau 499459f754SMarc-André Lureau @staticmethod 509459f754SMarc-André Lureau def argparse(s): 519459f754SMarc-André Lureau try: 529459f754SMarc-André Lureau return EngineEnum[s.upper()] 539459f754SMarc-André Lureau except KeyError: 549459f754SMarc-André Lureau return s 559459f754SMarc-André Lureau 569459f754SMarc-André Lureau 579459f754SMarc-André LureauUSE_ENGINE = EngineEnum.AUTO 58c9772570SSascha Silbe 59af509738SPaolo Bonzinidef _bytes_checksum(bytes): 60af509738SPaolo Bonzini """Calculate a digest string unique to the text content""" 61af509738SPaolo Bonzini return hashlib.sha1(bytes).hexdigest() 62af509738SPaolo Bonzini 634485b04bSFam Zhengdef _text_checksum(text): 644485b04bSFam Zheng """Calculate a digest string unique to the text content""" 65af509738SPaolo Bonzini return _bytes_checksum(text.encode('utf-8')) 664485b04bSFam Zheng 674112aff7SAlex Bennéedef _read_dockerfile(path): 684112aff7SAlex Bennée return open(path, 'rt', encoding='utf-8').read() 69432d8ad5SAlex Bennée 70438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename): 71af509738SPaolo Bonzini return _bytes_checksum(open(filename, 'rb').read()) 72438d1168SPhilippe Mathieu-Daudé 73432d8ad5SAlex Bennée 749459f754SMarc-André Lureaudef _guess_engine_command(): 759459f754SMarc-André Lureau """ Guess a working engine command or raise exception if not found""" 769459f754SMarc-André Lureau commands = [] 779459f754SMarc-André Lureau 789459f754SMarc-André Lureau if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]: 799459f754SMarc-André Lureau commands += [["podman"]] 809459f754SMarc-André Lureau if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]: 819459f754SMarc-André Lureau commands += [["docker"], ["sudo", "-n", "docker"]] 824485b04bSFam Zheng for cmd in commands: 830679f98bSEduardo Habkost try: 8483405c45SAlex Bennée # docker version will return the client details in stdout 8583405c45SAlex Bennée # but still report a status of 1 if it can't contact the daemon 8683405c45SAlex Bennée if subprocess.call(cmd + ["version"], 87c9772570SSascha Silbe stdout=DEVNULL, stderr=DEVNULL) == 0: 884485b04bSFam Zheng return cmd 890679f98bSEduardo Habkost except OSError: 900679f98bSEduardo Habkost pass 914485b04bSFam Zheng commands_txt = "\n".join([" " + " ".join(x) for x in commands]) 929459f754SMarc-André Lureau raise Exception("Cannot find working engine command. Tried:\n%s" % 934485b04bSFam Zheng commands_txt) 944485b04bSFam Zheng 95432d8ad5SAlex Bennée 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 1897e81d198SAlex Bennée m = re.search("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 208c1958e9dSFam Zhengdef _dockerfile_preprocess(df): 209c1958e9dSFam Zheng out = "" 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): 215c1958e9dSFam Zheng # TODO: Alternatively we could replace this line with "FROM $ID" 216c1958e9dSFam Zheng # where $ID is the image's hex id obtained with 217c1958e9dSFam Zheng # $ docker images $IMAGE --format="{{.Id}}" 218c1958e9dSFam Zheng # but unfortunately that's not supported by RHEL 7. 219c1958e9dSFam Zheng inlining = _read_qemu_dockerfile(l[len(from_pref):]) 220c1958e9dSFam Zheng out += _dockerfile_preprocess(inlining) 221c1958e9dSFam Zheng continue 222c1958e9dSFam Zheng out += l + "\n" 223c1958e9dSFam Zheng return out 224c1958e9dSFam Zheng 225432d8ad5SAlex Bennée 2264485b04bSFam Zhengclass Docker(object): 2274485b04bSFam Zheng """ Running Docker commands """ 2284485b04bSFam Zheng def __init__(self): 2299459f754SMarc-André Lureau self._command = _guess_engine_command() 230e6f1306bSAlex Bennée 231e6f1306bSAlex Bennée if "docker" in self._command and "TRAVIS" not in os.environ: 232e6f1306bSAlex Bennée os.environ["DOCKER_BUILDKIT"] = "1" 233e6f1306bSAlex Bennée self._buildkit = True 234e6f1306bSAlex Bennée else: 235e6f1306bSAlex Bennée self._buildkit = False 236e6f1306bSAlex Bennée 237529994e2SAlex Bennée self._instance = None 2384485b04bSFam Zheng atexit.register(self._kill_instances) 23997cba1a1SFam Zheng signal.signal(signal.SIGTERM, self._kill_instances) 24097cba1a1SFam Zheng signal.signal(signal.SIGHUP, self._kill_instances) 2414485b04bSFam Zheng 24258bf7b6dSFam Zheng def _do(self, cmd, quiet=True, **kwargs): 2434485b04bSFam Zheng if quiet: 244c9772570SSascha Silbe kwargs["stdout"] = DEVNULL 2454485b04bSFam Zheng return subprocess.call(self._command + cmd, **kwargs) 2464485b04bSFam Zheng 2470b95ff72SFam Zheng def _do_check(self, cmd, quiet=True, **kwargs): 2480b95ff72SFam Zheng if quiet: 2490b95ff72SFam Zheng kwargs["stdout"] = DEVNULL 2500b95ff72SFam Zheng return subprocess.check_call(self._command + cmd, **kwargs) 2510b95ff72SFam Zheng 2524485b04bSFam Zheng def _do_kill_instances(self, only_known, only_active=True): 2534485b04bSFam Zheng cmd = ["ps", "-q"] 2544485b04bSFam Zheng if not only_active: 2554485b04bSFam Zheng cmd.append("-a") 256529994e2SAlex Bennée 257529994e2SAlex Bennée filter = "--filter=label=com.qemu.instance.uuid" 258529994e2SAlex Bennée if only_known: 259529994e2SAlex Bennée if self._instance: 260529994e2SAlex Bennée filter += "=%s" % (self._instance) 261529994e2SAlex Bennée else: 262529994e2SAlex Bennée # no point trying to kill, we finished 263529994e2SAlex Bennée return 264529994e2SAlex Bennée 265529994e2SAlex Bennée print("filter=%s" % (filter)) 266529994e2SAlex Bennée cmd.append(filter) 2674485b04bSFam Zheng for i in self._output(cmd).split(): 268529994e2SAlex Bennée self._do(["rm", "-f", i]) 2694485b04bSFam Zheng 2704485b04bSFam Zheng def clean(self): 2714485b04bSFam Zheng self._do_kill_instances(False, False) 2724485b04bSFam Zheng return 0 2734485b04bSFam Zheng 27497cba1a1SFam Zheng def _kill_instances(self, *args, **kwargs): 2754485b04bSFam Zheng return self._do_kill_instances(True) 2764485b04bSFam Zheng 2774485b04bSFam Zheng def _output(self, cmd, **kwargs): 2782d110c11SJohn Snow try: 2794485b04bSFam Zheng return subprocess.check_output(self._command + cmd, 2804485b04bSFam Zheng stderr=subprocess.STDOUT, 2814112aff7SAlex Bennée encoding='utf-8', 2824485b04bSFam Zheng **kwargs) 2832d110c11SJohn Snow except TypeError: 2842d110c11SJohn Snow # 'encoding' argument was added in 3.6+ 285884fcafcSAlex Bennée return subprocess.check_output(self._command + cmd, 286884fcafcSAlex Bennée stderr=subprocess.STDOUT, 287884fcafcSAlex Bennée **kwargs).decode('utf-8') 288884fcafcSAlex Bennée 2894485b04bSFam Zheng 290f97da1f7SAlex Bennée def inspect_tag(self, tag): 291f97da1f7SAlex Bennée try: 292f97da1f7SAlex Bennée return self._output(["inspect", tag]) 293f97da1f7SAlex Bennée except subprocess.CalledProcessError: 294f97da1f7SAlex Bennée return None 295f97da1f7SAlex Bennée 2967b882245SAlex Bennée def get_image_creation_time(self, info): 2977b882245SAlex Bennée return json.loads(info)[0]["Created"] 2987b882245SAlex Bennée 2994485b04bSFam Zheng def get_image_dockerfile_checksum(self, tag): 300f97da1f7SAlex Bennée resp = self.inspect_tag(tag) 3014485b04bSFam Zheng labels = json.loads(resp)[0]["Config"].get("Labels", {}) 3024485b04bSFam Zheng return labels.get("com.qemu.dockerfile-checksum", "") 3034485b04bSFam Zheng 304414a8ce5SAlex Bennée def build_image(self, tag, docker_dir, dockerfile, 305e6f1306bSAlex Bennée quiet=True, user=False, argv=None, registry=None, 306e6f1306bSAlex Bennée extra_files_cksum=[]): 307432d8ad5SAlex Bennée if argv is None: 3084485b04bSFam Zheng argv = [] 3094485b04bSFam Zheng 310e6f1306bSAlex Bennée # pre-calculate the docker checksum before any 311e6f1306bSAlex Bennée # substitutions we make for caching 312e6f1306bSAlex Bennée checksum = _text_checksum(_dockerfile_preprocess(dockerfile)) 313e6f1306bSAlex Bennée 314e6f1306bSAlex Bennée if registry is not None: 315f73e4852SAlex Bennée sources = re.findall("FROM qemu\/(.*)", dockerfile) 316f73e4852SAlex Bennée # Fetch any cache layers we can, may fail 317f73e4852SAlex Bennée for s in sources: 318f73e4852SAlex Bennée pull_args = ["pull", "%s/qemu/%s" % (registry, s)] 319f73e4852SAlex Bennée if self._do(pull_args, quiet=quiet) != 0: 320f73e4852SAlex Bennée registry = None 321f73e4852SAlex Bennée break 322f73e4852SAlex Bennée # Make substitutions 323f73e4852SAlex Bennée if registry is not None: 324e6f1306bSAlex Bennée dockerfile = dockerfile.replace("FROM qemu/", 325e6f1306bSAlex Bennée "FROM %s/qemu/" % 326e6f1306bSAlex Bennée (registry)) 327e6f1306bSAlex Bennée 3284112aff7SAlex Bennée tmp_df = tempfile.NamedTemporaryFile(mode="w+t", 3294112aff7SAlex Bennée encoding='utf-8', 3304112aff7SAlex Bennée dir=docker_dir, suffix=".docker") 3314485b04bSFam Zheng tmp_df.write(dockerfile) 3324485b04bSFam Zheng 333414a8ce5SAlex Bennée if user: 334414a8ce5SAlex Bennée uid = os.getuid() 335414a8ce5SAlex Bennée uname = getpwuid(uid).pw_name 336414a8ce5SAlex Bennée tmp_df.write("\n") 337414a8ce5SAlex Bennée tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % 338414a8ce5SAlex Bennée (uname, uid, uname)) 339414a8ce5SAlex Bennée 3404485b04bSFam Zheng tmp_df.write("\n") 341e405a3ebSAlessandro Di Federico tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum)) 342f9172822SAlex Bennée for f, c in extra_files_cksum: 343e405a3ebSAlessandro Di Federico tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c)) 344f9172822SAlex Bennée 3454485b04bSFam Zheng tmp_df.flush() 346a9f8d038SAlex Bennée 347e6f1306bSAlex Bennée build_args = ["build", "-t", tag, "-f", tmp_df.name] 348e6f1306bSAlex Bennée if self._buildkit: 349e6f1306bSAlex Bennée build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"] 350e6f1306bSAlex Bennée 351e6f1306bSAlex Bennée if registry is not None: 352f73e4852SAlex Bennée pull_args = ["pull", "%s/%s" % (registry, tag)] 353f73e4852SAlex Bennée self._do(pull_args, quiet=quiet) 354e6f1306bSAlex Bennée cache = "%s/%s" % (registry, tag) 355e6f1306bSAlex Bennée build_args += ["--cache-from", cache] 356e6f1306bSAlex Bennée build_args += argv 357e6f1306bSAlex Bennée build_args += [docker_dir] 358e6f1306bSAlex Bennée 359e6f1306bSAlex Bennée self._do_check(build_args, 3604485b04bSFam Zheng quiet=quiet) 3614485b04bSFam Zheng 3626e733da6SAlex Bennée def update_image(self, tag, tarball, quiet=True): 3636e733da6SAlex Bennée "Update a tagged image using " 3646e733da6SAlex Bennée 3650b95ff72SFam Zheng self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball) 3666e733da6SAlex Bennée 3674485b04bSFam Zheng def image_matches_dockerfile(self, tag, dockerfile): 3684485b04bSFam Zheng try: 3694485b04bSFam Zheng checksum = self.get_image_dockerfile_checksum(tag) 3704485b04bSFam Zheng except Exception: 3714485b04bSFam Zheng return False 372c1958e9dSFam Zheng return checksum == _text_checksum(_dockerfile_preprocess(dockerfile)) 3734485b04bSFam Zheng 37471ebbe09SAlex Bennée def run(self, cmd, keep, quiet, as_user=False): 375529994e2SAlex Bennée label = uuid.uuid4().hex 3764485b04bSFam Zheng if not keep: 377529994e2SAlex Bennée self._instance = label 37871ebbe09SAlex Bennée 37971ebbe09SAlex Bennée if as_user: 38071ebbe09SAlex Bennée uid = os.getuid() 38171ebbe09SAlex Bennée cmd = [ "-u", str(uid) ] + cmd 38271ebbe09SAlex Bennée # podman requires a bit more fiddling 38371ebbe09SAlex Bennée if self._command[0] == "podman": 384b3a790beSJohn Snow cmd.insert(0, '--userns=keep-id') 38571ebbe09SAlex Bennée 38617cd6e2bSPaolo Bonzini ret = self._do_check(["run", "--rm", "--label", 3874485b04bSFam Zheng "com.qemu.instance.uuid=" + label] + cmd, 3884485b04bSFam Zheng quiet=quiet) 3894485b04bSFam Zheng if not keep: 390529994e2SAlex Bennée self._instance = None 3914485b04bSFam Zheng return ret 3924485b04bSFam Zheng 3934b08af60SFam Zheng def command(self, cmd, argv, quiet): 3944b08af60SFam Zheng return self._do([cmd] + argv, quiet=quiet) 3954b08af60SFam Zheng 396432d8ad5SAlex Bennée 3974485b04bSFam Zhengclass SubCommand(object): 3984485b04bSFam Zheng """A SubCommand template base class""" 3994485b04bSFam Zheng name = None # Subcommand name 400432d8ad5SAlex Bennée 4014485b04bSFam Zheng def shared_args(self, parser): 4024485b04bSFam Zheng parser.add_argument("--quiet", action="store_true", 403e50a6121SStefan Weil help="Run quietly unless an error occurred") 4044485b04bSFam Zheng 4054485b04bSFam Zheng def args(self, parser): 4064485b04bSFam Zheng """Setup argument parser""" 4074485b04bSFam Zheng pass 408432d8ad5SAlex Bennée 4094485b04bSFam Zheng def run(self, args, argv): 4104485b04bSFam Zheng """Run command. 4114485b04bSFam Zheng args: parsed argument by argument parser. 4124485b04bSFam Zheng argv: remaining arguments from sys.argv. 4134485b04bSFam Zheng """ 4144485b04bSFam Zheng pass 4154485b04bSFam Zheng 416432d8ad5SAlex Bennée 4174485b04bSFam Zhengclass RunCommand(SubCommand): 4184485b04bSFam Zheng """Invoke docker run and take care of cleaning up""" 4194485b04bSFam Zheng name = "run" 420432d8ad5SAlex Bennée 4214485b04bSFam Zheng def args(self, parser): 4224485b04bSFam Zheng parser.add_argument("--keep", action="store_true", 4234485b04bSFam Zheng help="Don't remove image when command completes") 4242461d80eSMarc-André Lureau parser.add_argument("--run-as-current-user", action="store_true", 4252461d80eSMarc-André Lureau help="Run container using the current user's uid") 426432d8ad5SAlex Bennée 4274485b04bSFam Zheng def run(self, args, argv): 42871ebbe09SAlex Bennée return Docker().run(argv, args.keep, quiet=args.quiet, 42971ebbe09SAlex Bennée as_user=args.run_as_current_user) 4304485b04bSFam Zheng 431432d8ad5SAlex Bennée 4324485b04bSFam Zhengclass BuildCommand(SubCommand): 433432d8ad5SAlex Bennée """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>""" 4344485b04bSFam Zheng name = "build" 435432d8ad5SAlex Bennée 4364485b04bSFam Zheng def args(self, parser): 437504ca3c2SAlex Bennée parser.add_argument("--include-executable", "-e", 438504ca3c2SAlex Bennée help="""Specify a binary that will be copied to the 439504ca3c2SAlex Bennée container together with all its dependent 440504ca3c2SAlex Bennée libraries""") 441*ddd5ed83SAlex Bennée parser.add_argument("--skip-binfmt", 442*ddd5ed83SAlex Bennée action="store_true", 443*ddd5ed83SAlex Bennée help="""Skip binfmt entry check (used for testing)""") 444dfae6284SPaolo Bonzini parser.add_argument("--extra-files", nargs='*', 4454c84f662SPhilippe Mathieu-Daudé help="""Specify files that will be copied in the 4464c84f662SPhilippe Mathieu-Daudé Docker image, fulfilling the ADD directive from the 4474c84f662SPhilippe Mathieu-Daudé Dockerfile""") 448414a8ce5SAlex Bennée parser.add_argument("--add-current-user", "-u", dest="user", 449414a8ce5SAlex Bennée action="store_true", 450414a8ce5SAlex Bennée help="Add the current user to image's passwd") 451e6f1306bSAlex Bennée parser.add_argument("--registry", "-r", 452e6f1306bSAlex Bennée help="cache from docker registry") 453dfae6284SPaolo Bonzini parser.add_argument("-t", dest="tag", 4544485b04bSFam Zheng help="Image Tag") 455dfae6284SPaolo Bonzini parser.add_argument("-f", dest="dockerfile", 4564485b04bSFam Zheng help="Dockerfile name") 4574485b04bSFam Zheng 4584485b04bSFam Zheng def run(self, args, argv): 4594112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 4604485b04bSFam Zheng tag = args.tag 4614485b04bSFam Zheng 4624485b04bSFam Zheng dkr = Docker() 4636fe3ae3fSAlex Bennée if "--no-cache" not in argv and \ 4646fe3ae3fSAlex Bennée dkr.image_matches_dockerfile(tag, dockerfile): 4654485b04bSFam Zheng if not args.quiet: 466f03868bdSEduardo Habkost print("Image is up to date.") 467a9f8d038SAlex Bennée else: 468a9f8d038SAlex Bennée # Create a docker context directory for the build 469a9f8d038SAlex Bennée docker_dir = tempfile.mkdtemp(prefix="docker_build") 4704485b04bSFam Zheng 47115352decSAlex Bennée # Validate binfmt_misc will work 472*ddd5ed83SAlex Bennée if args.skip_binfmt: 473*ddd5ed83SAlex Bennée qpath = args.include_executable 474*ddd5ed83SAlex Bennée elif args.include_executable: 475d10404b1SAlex Bennée qpath, enabled = _check_binfmt_misc(args.include_executable) 476d10404b1SAlex Bennée if not enabled: 47715352decSAlex Bennée return 1 47815352decSAlex Bennée 479920776eaSAlex Bennée # Is there a .pre file to run in the build context? 480920776eaSAlex Bennée docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" 481920776eaSAlex Bennée if os.path.exists(docker_pre): 482f8042deaSSascha Silbe stdout = DEVNULL if args.quiet else None 483920776eaSAlex Bennée rc = subprocess.call(os.path.realpath(docker_pre), 484f8042deaSSascha Silbe cwd=docker_dir, stdout=stdout) 485920776eaSAlex Bennée if rc == 3: 486f03868bdSEduardo Habkost print("Skip") 487920776eaSAlex Bennée return 0 488920776eaSAlex Bennée elif rc != 0: 489f03868bdSEduardo Habkost print("%s exited with code %d" % (docker_pre, rc)) 490920776eaSAlex Bennée return 1 491920776eaSAlex Bennée 4924c84f662SPhilippe Mathieu-Daudé # Copy any extra files into the Docker context. These can be 4934c84f662SPhilippe Mathieu-Daudé # included by the use of the ADD directive in the Dockerfile. 494438d1168SPhilippe Mathieu-Daudé cksum = [] 495504ca3c2SAlex Bennée if args.include_executable: 496438d1168SPhilippe Mathieu-Daudé # FIXME: there is no checksum of this executable and the linked 497438d1168SPhilippe Mathieu-Daudé # libraries, once the image built any change of this executable 498438d1168SPhilippe Mathieu-Daudé # or any library won't trigger another build. 499d10404b1SAlex Bennée _copy_binary_with_libs(args.include_executable, 500d10404b1SAlex Bennée qpath, docker_dir) 501d10404b1SAlex Bennée 5024c84f662SPhilippe Mathieu-Daudé for filename in args.extra_files or []: 5034c84f662SPhilippe Mathieu-Daudé _copy_with_mkdir(filename, docker_dir) 504f9172822SAlex Bennée cksum += [(filename, _file_checksum(filename))] 505504ca3c2SAlex Bennée 50606cc3551SPhilippe Mathieu-Daudé argv += ["--build-arg=" + k.lower() + "=" + v 5074112aff7SAlex Bennée for k, v in os.environ.items() 50806cc3551SPhilippe Mathieu-Daudé if k.lower() in FILTERED_ENV_NAMES] 509a9f8d038SAlex Bennée dkr.build_image(tag, docker_dir, dockerfile, 510e6f1306bSAlex Bennée quiet=args.quiet, user=args.user, 511e6f1306bSAlex Bennée argv=argv, registry=args.registry, 512438d1168SPhilippe Mathieu-Daudé extra_files_cksum=cksum) 513a9f8d038SAlex Bennée 514a9f8d038SAlex Bennée rmtree(docker_dir) 515a9f8d038SAlex Bennée 5164485b04bSFam Zheng return 0 5174485b04bSFam Zheng 518432d8ad5SAlex Bennée 5196e733da6SAlex Bennéeclass UpdateCommand(SubCommand): 520432d8ad5SAlex Bennée """ Update a docker image with new executables. Args: <tag> <executable>""" 5216e733da6SAlex Bennée name = "update" 522432d8ad5SAlex Bennée 5236e733da6SAlex Bennée def args(self, parser): 5246e733da6SAlex Bennée parser.add_argument("tag", 5256e733da6SAlex Bennée help="Image Tag") 5266e733da6SAlex Bennée parser.add_argument("executable", 5276e733da6SAlex Bennée help="Executable to copy") 5286e733da6SAlex Bennée 5296e733da6SAlex Bennée def run(self, args, argv): 5306e733da6SAlex Bennée # Create a temporary tarball with our whole build context and 5316e733da6SAlex Bennée # dockerfile for the update 5326e733da6SAlex Bennée tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") 5336e733da6SAlex Bennée tmp_tar = TarFile(fileobj=tmp, mode='w') 5346e733da6SAlex Bennée 5357e81d198SAlex Bennée # Add the executable to the tarball, using the current 536d10404b1SAlex Bennée # configured binfmt_misc path. If we don't get a path then we 537d10404b1SAlex Bennée # only need the support libraries copied 538d10404b1SAlex Bennée ff, enabled = _check_binfmt_misc(args.executable) 5397e81d198SAlex Bennée 540d10404b1SAlex Bennée if not enabled: 541d10404b1SAlex Bennée print("binfmt_misc not enabled, update disabled") 542d10404b1SAlex Bennée return 1 543d10404b1SAlex Bennée 544d10404b1SAlex Bennée if ff: 5456e733da6SAlex Bennée tmp_tar.add(args.executable, arcname=ff) 5466e733da6SAlex Bennée 5476e733da6SAlex Bennée # Add any associated libraries 5486e733da6SAlex Bennée libs = _get_so_libs(args.executable) 5496e733da6SAlex Bennée if libs: 5506e733da6SAlex Bennée for l in libs: 5516e733da6SAlex Bennée tmp_tar.add(os.path.realpath(l), arcname=l) 5526e733da6SAlex Bennée 5536e733da6SAlex Bennée # Create a Docker buildfile 5546e733da6SAlex Bennée df = StringIO() 555e336cec3SAlex Bennée df.write(u"FROM %s\n" % args.tag) 556e336cec3SAlex Bennée df.write(u"ADD . /\n") 557e336cec3SAlex Bennée 558e336cec3SAlex Bennée df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8")) 5596e733da6SAlex Bennée 5606e733da6SAlex Bennée df_tar = TarInfo(name="Dockerfile") 561e336cec3SAlex Bennée df_tar.size = df_bytes.getbuffer().nbytes 562e336cec3SAlex Bennée tmp_tar.addfile(df_tar, fileobj=df_bytes) 5636e733da6SAlex Bennée 5646e733da6SAlex Bennée tmp_tar.close() 5656e733da6SAlex Bennée 5666e733da6SAlex Bennée # reset the file pointers 5676e733da6SAlex Bennée tmp.flush() 5686e733da6SAlex Bennée tmp.seek(0) 5696e733da6SAlex Bennée 5706e733da6SAlex Bennée # Run the build with our tarball context 5716e733da6SAlex Bennée dkr = Docker() 5726e733da6SAlex Bennée dkr.update_image(args.tag, tmp, quiet=args.quiet) 5736e733da6SAlex Bennée 5746e733da6SAlex Bennée return 0 5756e733da6SAlex Bennée 576432d8ad5SAlex Bennée 5774485b04bSFam Zhengclass CleanCommand(SubCommand): 5784485b04bSFam Zheng """Clean up docker instances""" 5794485b04bSFam Zheng name = "clean" 580432d8ad5SAlex Bennée 5814485b04bSFam Zheng def run(self, args, argv): 5824485b04bSFam Zheng Docker().clean() 5834485b04bSFam Zheng return 0 5844485b04bSFam Zheng 585432d8ad5SAlex Bennée 5864b08af60SFam Zhengclass ImagesCommand(SubCommand): 5874b08af60SFam Zheng """Run "docker images" command""" 5884b08af60SFam Zheng name = "images" 589432d8ad5SAlex Bennée 5904b08af60SFam Zheng def run(self, args, argv): 5914b08af60SFam Zheng return Docker().command("images", argv, args.quiet) 5924b08af60SFam Zheng 59315df9d37SAlex Bennée 59415df9d37SAlex Bennéeclass ProbeCommand(SubCommand): 59515df9d37SAlex Bennée """Probe if we can run docker automatically""" 59615df9d37SAlex Bennée name = "probe" 59715df9d37SAlex Bennée 59815df9d37SAlex Bennée def run(self, args, argv): 59915df9d37SAlex Bennée try: 60015df9d37SAlex Bennée docker = Docker() 60115df9d37SAlex Bennée if docker._command[0] == "docker": 6028480517dSAlex Bennée print("docker") 60315df9d37SAlex Bennée elif docker._command[0] == "sudo": 6048480517dSAlex Bennée print("sudo docker") 6059459f754SMarc-André Lureau elif docker._command[0] == "podman": 6069459f754SMarc-André Lureau print("podman") 60715df9d37SAlex Bennée except Exception: 608f03868bdSEduardo Habkost print("no") 60915df9d37SAlex Bennée 61015df9d37SAlex Bennée return 61115df9d37SAlex Bennée 61215df9d37SAlex Bennée 6135e03c2d8SAlex Bennéeclass CcCommand(SubCommand): 6145e03c2d8SAlex Bennée """Compile sources with cc in images""" 6155e03c2d8SAlex Bennée name = "cc" 6165e03c2d8SAlex Bennée 6175e03c2d8SAlex Bennée def args(self, parser): 6185e03c2d8SAlex Bennée parser.add_argument("--image", "-i", required=True, 6195e03c2d8SAlex Bennée help="The docker image in which to run cc") 62099cfdb86SAlex Bennée parser.add_argument("--cc", default="cc", 62199cfdb86SAlex Bennée help="The compiler executable to call") 6225e03c2d8SAlex Bennée parser.add_argument("--source-path", "-s", nargs="*", dest="paths", 6235e03c2d8SAlex Bennée help="""Extra paths to (ro) mount into container for 6245e03c2d8SAlex Bennée reading sources""") 6255e03c2d8SAlex Bennée 6265e03c2d8SAlex Bennée def run(self, args, argv): 6275e03c2d8SAlex Bennée if argv and argv[0] == "--": 6285e03c2d8SAlex Bennée argv = argv[1:] 6295e03c2d8SAlex Bennée cwd = os.getcwd() 63017cd6e2bSPaolo Bonzini cmd = ["-w", cwd, 6315e03c2d8SAlex Bennée "-v", "%s:%s:rw" % (cwd, cwd)] 6325e03c2d8SAlex Bennée if args.paths: 6335e03c2d8SAlex Bennée for p in args.paths: 6345e03c2d8SAlex Bennée cmd += ["-v", "%s:%s:ro,z" % (p, p)] 63599cfdb86SAlex Bennée cmd += [args.image, args.cc] 6365e03c2d8SAlex Bennée cmd += argv 63771ebbe09SAlex Bennée return Docker().run(cmd, False, quiet=args.quiet, 63871ebbe09SAlex Bennée as_user=True) 6395e03c2d8SAlex Bennée 6405e03c2d8SAlex Bennée 641f97da1f7SAlex Bennéeclass CheckCommand(SubCommand): 642f97da1f7SAlex Bennée """Check if we need to re-build a docker image out of a dockerfile. 643f97da1f7SAlex Bennée Arguments: <tag> <dockerfile>""" 644f97da1f7SAlex Bennée name = "check" 645f97da1f7SAlex Bennée 646f97da1f7SAlex Bennée def args(self, parser): 647f97da1f7SAlex Bennée parser.add_argument("tag", 648f97da1f7SAlex Bennée help="Image Tag") 6497b882245SAlex Bennée parser.add_argument("dockerfile", default=None, 6507b882245SAlex Bennée help="Dockerfile name", nargs='?') 6517b882245SAlex Bennée parser.add_argument("--checktype", choices=["checksum", "age"], 6527b882245SAlex Bennée default="checksum", help="check type") 6537b882245SAlex Bennée parser.add_argument("--olderthan", default=60, type=int, 6547b882245SAlex Bennée help="number of minutes") 655f97da1f7SAlex Bennée 656f97da1f7SAlex Bennée def run(self, args, argv): 657f97da1f7SAlex Bennée tag = args.tag 658f97da1f7SAlex Bennée 65943e1b2ffSAlex Bennée try: 660f97da1f7SAlex Bennée dkr = Docker() 661432d8ad5SAlex Bennée except subprocess.CalledProcessError: 66243e1b2ffSAlex Bennée print("Docker not set up") 66343e1b2ffSAlex Bennée return 1 66443e1b2ffSAlex Bennée 665f97da1f7SAlex Bennée info = dkr.inspect_tag(tag) 666f97da1f7SAlex Bennée if info is None: 667f97da1f7SAlex Bennée print("Image does not exist") 668f97da1f7SAlex Bennée return 1 669f97da1f7SAlex Bennée 6707b882245SAlex Bennée if args.checktype == "checksum": 6717b882245SAlex Bennée if not args.dockerfile: 6727b882245SAlex Bennée print("Need a dockerfile for tag:%s" % (tag)) 6737b882245SAlex Bennée return 1 6747b882245SAlex Bennée 6754112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 6767b882245SAlex Bennée 677f97da1f7SAlex Bennée if dkr.image_matches_dockerfile(tag, dockerfile): 678f97da1f7SAlex Bennée if not args.quiet: 679f97da1f7SAlex Bennée print("Image is up to date") 680f97da1f7SAlex Bennée return 0 681f97da1f7SAlex Bennée else: 682f97da1f7SAlex Bennée print("Image needs updating") 683f97da1f7SAlex Bennée return 1 6847b882245SAlex Bennée elif args.checktype == "age": 6857b882245SAlex Bennée timestr = dkr.get_image_creation_time(info).split(".")[0] 6867b882245SAlex Bennée created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S") 6877b882245SAlex Bennée past = datetime.now() - timedelta(minutes=args.olderthan) 6887b882245SAlex Bennée if created < past: 6897b882245SAlex Bennée print ("Image created @ %s more than %d minutes old" % 6907b882245SAlex Bennée (timestr, args.olderthan)) 6917b882245SAlex Bennée return 1 6927b882245SAlex Bennée else: 6937b882245SAlex Bennée if not args.quiet: 6947b882245SAlex Bennée print ("Image less than %d minutes old" % (args.olderthan)) 6957b882245SAlex Bennée return 0 696f97da1f7SAlex Bennée 697f97da1f7SAlex Bennée 6984485b04bSFam Zhengdef main(): 6999459f754SMarc-André Lureau global USE_ENGINE 7009459f754SMarc-André Lureau 7014485b04bSFam Zheng parser = argparse.ArgumentParser(description="A Docker helper", 702432d8ad5SAlex Bennée usage="%s <subcommand> ..." % 703432d8ad5SAlex Bennée os.path.basename(sys.argv[0])) 7049459f754SMarc-André Lureau parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum), 7059459f754SMarc-André Lureau help="specify which container engine to use") 7064485b04bSFam Zheng subparsers = parser.add_subparsers(title="subcommands", help=None) 7074485b04bSFam Zheng for cls in SubCommand.__subclasses__(): 7084485b04bSFam Zheng cmd = cls() 7094485b04bSFam Zheng subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) 7104485b04bSFam Zheng cmd.shared_args(subp) 7114485b04bSFam Zheng cmd.args(subp) 7124485b04bSFam Zheng subp.set_defaults(cmdobj=cmd) 7134485b04bSFam Zheng args, argv = parser.parse_known_args() 7148480517dSAlex Bennée if args.engine: 7159459f754SMarc-André Lureau USE_ENGINE = args.engine 7164485b04bSFam Zheng return args.cmdobj.run(args, argv) 7174485b04bSFam Zheng 718432d8ad5SAlex Bennée 7194485b04bSFam Zhengif __name__ == "__main__": 7204485b04bSFam Zheng sys.exit(main()) 721