14112aff7SAlex Bennée#!/usr/bin/env python3 24485b04bSFam Zheng# 34485b04bSFam Zheng# Docker controlling module 44485b04bSFam Zheng# 54485b04bSFam Zheng# Copyright (c) 2016 Red Hat Inc. 64485b04bSFam Zheng# 74485b04bSFam Zheng# Authors: 84485b04bSFam Zheng# Fam Zheng <famz@redhat.com> 94485b04bSFam Zheng# 104485b04bSFam Zheng# This work is licensed under the terms of the GNU GPL, version 2 114485b04bSFam Zheng# or (at your option) any later version. See the COPYING file in 124485b04bSFam Zheng# the top-level directory. 134485b04bSFam Zheng 144485b04bSFam Zhengimport os 154485b04bSFam Zhengimport sys 164485b04bSFam Zhengimport subprocess 174485b04bSFam Zhengimport json 184485b04bSFam Zhengimport hashlib 194485b04bSFam Zhengimport atexit 204485b04bSFam Zhengimport uuid 21ae68fdabSEduardo Habkostimport argparse 229459f754SMarc-André Lureauimport enum 234485b04bSFam Zhengimport tempfile 24504ca3c2SAlex Bennéeimport re 2597cba1a1SFam Zhengimport signal 266e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo 277a5d936bSAlex Bennéefrom io import StringIO 28a9f8d038SAlex Bennéefrom shutil import copy, rmtree 29414a8ce5SAlex Bennéefrom pwd import getpwuid 307b882245SAlex Bennéefrom datetime import datetime, timedelta 314485b04bSFam Zheng 32c9772570SSascha Silbe 3306cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy'] 3406cc3551SPhilippe Mathieu-Daudé 3506cc3551SPhilippe Mathieu-Daudé 36c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb') 37c9772570SSascha Silbe 389459f754SMarc-André Lureauclass EngineEnum(enum.IntEnum): 399459f754SMarc-André Lureau AUTO = 1 409459f754SMarc-André Lureau DOCKER = 2 419459f754SMarc-André Lureau PODMAN = 3 429459f754SMarc-André Lureau 439459f754SMarc-André Lureau def __str__(self): 449459f754SMarc-André Lureau return self.name.lower() 459459f754SMarc-André Lureau 469459f754SMarc-André Lureau def __repr__(self): 479459f754SMarc-André Lureau return str(self) 489459f754SMarc-André Lureau 499459f754SMarc-André Lureau @staticmethod 509459f754SMarc-André Lureau def argparse(s): 519459f754SMarc-André Lureau try: 529459f754SMarc-André Lureau return EngineEnum[s.upper()] 539459f754SMarc-André Lureau except KeyError: 549459f754SMarc-André Lureau return s 559459f754SMarc-André Lureau 569459f754SMarc-André Lureau 579459f754SMarc-André LureauUSE_ENGINE = EngineEnum.AUTO 58c9772570SSascha Silbe 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 962499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'): 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 105504ca3c2SAlex Bennée dest_file = "%s/%s" % (dest_dir, os.path.basename(src)) 106504ca3c2SAlex Bennée copy(src, dest_file) 107504ca3c2SAlex Bennée 108504ca3c2SAlex Bennée 109504ca3c2SAlex Bennéedef _get_so_libs(executable): 110504ca3c2SAlex Bennée """Return a list of libraries associated with an executable. 111504ca3c2SAlex Bennée 112504ca3c2SAlex Bennée The paths may be symbolic links which would need to be resolved to 113504ca3c2SAlex Bennée ensure the right data is copied.""" 114504ca3c2SAlex Bennée 115504ca3c2SAlex Bennée libs = [] 1165e33f7feSAlex Bennée ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)") 117504ca3c2SAlex Bennée try: 118eea2153eSAlex Bennée ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8') 119504ca3c2SAlex Bennée for line in ldd_output.split("\n"): 120504ca3c2SAlex Bennée search = ldd_re.search(line) 1215e33f7feSAlex Bennée if search: 1225e33f7feSAlex Bennée try: 1235e33f7feSAlex Bennée libs.append(s.group(1)) 1245e33f7feSAlex Bennée except IndexError: 1255e33f7feSAlex Bennée pass 126504ca3c2SAlex Bennée except subprocess.CalledProcessError: 127f03868bdSEduardo Habkost print("%s had no associated libraries (static build?)" % (executable)) 128504ca3c2SAlex Bennée 129504ca3c2SAlex Bennée return libs 130504ca3c2SAlex Bennée 131432d8ad5SAlex Bennée 132d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir): 133d10404b1SAlex Bennée """Maybe copy a binary and all its dependent libraries. 134d10404b1SAlex Bennée 135d10404b1SAlex Bennée If bin_dest isn't set we only copy the support libraries because 136d10404b1SAlex Bennée we don't need qemu in the docker path to run (due to persistent 137d10404b1SAlex Bennée mapping). Indeed users may get confused if we aren't running what 138d10404b1SAlex Bennée is in the image. 139504ca3c2SAlex Bennée 140504ca3c2SAlex Bennée This does rely on the host file-system being fairly multi-arch 141d10404b1SAlex Bennée aware so the file don't clash with the guests layout. 142d10404b1SAlex Bennée """ 143504ca3c2SAlex Bennée 144d10404b1SAlex Bennée if bin_dest: 145d10404b1SAlex Bennée _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest)) 146d10404b1SAlex Bennée else: 147d10404b1SAlex Bennée print("only copying support libraries for %s" % (src)) 148504ca3c2SAlex Bennée 149504ca3c2SAlex Bennée libs = _get_so_libs(src) 150504ca3c2SAlex Bennée if libs: 151504ca3c2SAlex Bennée for l in libs: 152504ca3c2SAlex Bennée so_path = os.path.dirname(l) 1535e33f7feSAlex Bennée real_l = os.path.realpath(l) 1545e33f7feSAlex Bennée _copy_with_mkdir(real_l, dest_dir, so_path) 155504ca3c2SAlex Bennée 15615352decSAlex Bennée 15715352decSAlex Bennéedef _check_binfmt_misc(executable): 15815352decSAlex Bennée """Check binfmt_misc has entry for executable in the right place. 15915352decSAlex Bennée 16015352decSAlex Bennée The details of setting up binfmt_misc are outside the scope of 16115352decSAlex Bennée this script but we should at least fail early with a useful 162d10404b1SAlex Bennée message if it won't work. 163d10404b1SAlex Bennée 164d10404b1SAlex Bennée Returns the configured binfmt path and a valid flag. For 165d10404b1SAlex Bennée persistent configurations we will still want to copy and dependent 166d10404b1SAlex Bennée libraries. 167d10404b1SAlex Bennée """ 16815352decSAlex Bennée 16915352decSAlex Bennée binary = os.path.basename(executable) 17015352decSAlex Bennée binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary) 17115352decSAlex Bennée 17215352decSAlex Bennée if not os.path.exists(binfmt_entry): 17315352decSAlex Bennée print ("No binfmt_misc entry for %s" % (binary)) 174d10404b1SAlex Bennée return None, False 17515352decSAlex Bennée 17615352decSAlex Bennée with open(binfmt_entry) as x: entry = x.read() 17715352decSAlex Bennée 17843c898b7SAlex Bennée if re.search("flags:.*F.*\n", entry): 179432d8ad5SAlex Bennée print("binfmt_misc for %s uses persistent(F) mapping to host binary" % 18043c898b7SAlex Bennée (binary)) 181d10404b1SAlex Bennée return None, True 18243c898b7SAlex Bennée 1837e81d198SAlex Bennée m = re.search("interpreter (\S+)\n", entry) 1847e81d198SAlex Bennée interp = m.group(1) 1857e81d198SAlex Bennée if interp and interp != executable: 1867e81d198SAlex Bennée print("binfmt_misc for %s does not point to %s, using %s" % 1877e81d198SAlex Bennée (binary, executable, interp)) 18815352decSAlex Bennée 189d10404b1SAlex Bennée return interp, True 190d10404b1SAlex Bennée 19115352decSAlex Bennée 192c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name): 193547cb45eSAlex Bennée # special case for Debian linux-user images 194547cb45eSAlex Bennée if img_name.startswith("debian") and img_name.endswith("user"): 195547cb45eSAlex Bennée img_name = "debian-bootstrap" 196547cb45eSAlex Bennée 197c1958e9dSFam Zheng df = os.path.join(os.path.dirname(__file__), "dockerfiles", 198c1958e9dSFam Zheng img_name + ".docker") 1994112aff7SAlex Bennée return _read_dockerfile(df) 200c1958e9dSFam Zheng 201432d8ad5SAlex Bennée 202c1958e9dSFam Zhengdef _dockerfile_preprocess(df): 203c1958e9dSFam Zheng out = "" 204c1958e9dSFam Zheng for l in df.splitlines(): 205c1958e9dSFam Zheng if len(l.strip()) == 0 or l.startswith("#"): 206c1958e9dSFam Zheng continue 207767b6bd2SAlex Bennée from_pref = "FROM qemu/" 208c1958e9dSFam Zheng if l.startswith(from_pref): 209c1958e9dSFam Zheng # TODO: Alternatively we could replace this line with "FROM $ID" 210c1958e9dSFam Zheng # where $ID is the image's hex id obtained with 211c1958e9dSFam Zheng # $ docker images $IMAGE --format="{{.Id}}" 212c1958e9dSFam Zheng # but unfortunately that's not supported by RHEL 7. 213c1958e9dSFam Zheng inlining = _read_qemu_dockerfile(l[len(from_pref):]) 214c1958e9dSFam Zheng out += _dockerfile_preprocess(inlining) 215c1958e9dSFam Zheng continue 216c1958e9dSFam Zheng out += l + "\n" 217c1958e9dSFam Zheng return out 218c1958e9dSFam Zheng 219432d8ad5SAlex Bennée 2204485b04bSFam Zhengclass Docker(object): 2214485b04bSFam Zheng """ Running Docker commands """ 2224485b04bSFam Zheng def __init__(self): 2239459f754SMarc-André Lureau self._command = _guess_engine_command() 224e6f1306bSAlex Bennée 225e6f1306bSAlex Bennée if "docker" in self._command and "TRAVIS" not in os.environ: 226e6f1306bSAlex Bennée os.environ["DOCKER_BUILDKIT"] = "1" 227e6f1306bSAlex Bennée self._buildkit = True 228e6f1306bSAlex Bennée else: 229e6f1306bSAlex Bennée self._buildkit = False 230e6f1306bSAlex Bennée 231529994e2SAlex Bennée self._instance = None 2324485b04bSFam Zheng atexit.register(self._kill_instances) 23397cba1a1SFam Zheng signal.signal(signal.SIGTERM, self._kill_instances) 23497cba1a1SFam Zheng signal.signal(signal.SIGHUP, self._kill_instances) 2354485b04bSFam Zheng 23658bf7b6dSFam Zheng def _do(self, cmd, quiet=True, **kwargs): 2374485b04bSFam Zheng if quiet: 238c9772570SSascha Silbe kwargs["stdout"] = DEVNULL 2394485b04bSFam Zheng return subprocess.call(self._command + cmd, **kwargs) 2404485b04bSFam Zheng 2410b95ff72SFam Zheng def _do_check(self, cmd, quiet=True, **kwargs): 2420b95ff72SFam Zheng if quiet: 2430b95ff72SFam Zheng kwargs["stdout"] = DEVNULL 2440b95ff72SFam Zheng return subprocess.check_call(self._command + cmd, **kwargs) 2450b95ff72SFam Zheng 2464485b04bSFam Zheng def _do_kill_instances(self, only_known, only_active=True): 2474485b04bSFam Zheng cmd = ["ps", "-q"] 2484485b04bSFam Zheng if not only_active: 2494485b04bSFam Zheng cmd.append("-a") 250529994e2SAlex Bennée 251529994e2SAlex Bennée filter = "--filter=label=com.qemu.instance.uuid" 252529994e2SAlex Bennée if only_known: 253529994e2SAlex Bennée if self._instance: 254529994e2SAlex Bennée filter += "=%s" % (self._instance) 255529994e2SAlex Bennée else: 256529994e2SAlex Bennée # no point trying to kill, we finished 257529994e2SAlex Bennée return 258529994e2SAlex Bennée 259529994e2SAlex Bennée print("filter=%s" % (filter)) 260529994e2SAlex Bennée cmd.append(filter) 2614485b04bSFam Zheng for i in self._output(cmd).split(): 262529994e2SAlex Bennée self._do(["rm", "-f", i]) 2634485b04bSFam Zheng 2644485b04bSFam Zheng def clean(self): 2654485b04bSFam Zheng self._do_kill_instances(False, False) 2664485b04bSFam Zheng return 0 2674485b04bSFam Zheng 26897cba1a1SFam Zheng def _kill_instances(self, *args, **kwargs): 2694485b04bSFam Zheng return self._do_kill_instances(True) 2704485b04bSFam Zheng 2714485b04bSFam Zheng def _output(self, cmd, **kwargs): 2722d110c11SJohn Snow try: 2734485b04bSFam Zheng return subprocess.check_output(self._command + cmd, 2744485b04bSFam Zheng stderr=subprocess.STDOUT, 2754112aff7SAlex Bennée encoding='utf-8', 2764485b04bSFam Zheng **kwargs) 2772d110c11SJohn Snow except TypeError: 2782d110c11SJohn Snow # 'encoding' argument was added in 3.6+ 279884fcafcSAlex Bennée return subprocess.check_output(self._command + cmd, 280884fcafcSAlex Bennée stderr=subprocess.STDOUT, 281884fcafcSAlex Bennée **kwargs).decode('utf-8') 282884fcafcSAlex Bennée 2834485b04bSFam Zheng 284f97da1f7SAlex Bennée def inspect_tag(self, tag): 285f97da1f7SAlex Bennée try: 286f97da1f7SAlex Bennée return self._output(["inspect", tag]) 287f97da1f7SAlex Bennée except subprocess.CalledProcessError: 288f97da1f7SAlex Bennée return None 289f97da1f7SAlex Bennée 2907b882245SAlex Bennée def get_image_creation_time(self, info): 2917b882245SAlex Bennée return json.loads(info)[0]["Created"] 2927b882245SAlex Bennée 2934485b04bSFam Zheng def get_image_dockerfile_checksum(self, tag): 294f97da1f7SAlex Bennée resp = self.inspect_tag(tag) 2954485b04bSFam Zheng labels = json.loads(resp)[0]["Config"].get("Labels", {}) 2964485b04bSFam Zheng return labels.get("com.qemu.dockerfile-checksum", "") 2974485b04bSFam Zheng 298414a8ce5SAlex Bennée def build_image(self, tag, docker_dir, dockerfile, 299e6f1306bSAlex Bennée quiet=True, user=False, argv=None, registry=None, 300e6f1306bSAlex Bennée extra_files_cksum=[]): 301432d8ad5SAlex Bennée if argv is None: 3024485b04bSFam Zheng argv = [] 3034485b04bSFam Zheng 304e6f1306bSAlex Bennée # pre-calculate the docker checksum before any 305e6f1306bSAlex Bennée # substitutions we make for caching 306e6f1306bSAlex Bennée checksum = _text_checksum(_dockerfile_preprocess(dockerfile)) 307e6f1306bSAlex Bennée 308e6f1306bSAlex Bennée if registry is not None: 309*f73e4852SAlex Bennée sources = re.findall("FROM qemu\/(.*)", dockerfile) 310*f73e4852SAlex Bennée # Fetch any cache layers we can, may fail 311*f73e4852SAlex Bennée for s in sources: 312*f73e4852SAlex Bennée pull_args = ["pull", "%s/qemu/%s" % (registry, s)] 313*f73e4852SAlex Bennée if self._do(pull_args, quiet=quiet) != 0: 314*f73e4852SAlex Bennée registry = None 315*f73e4852SAlex Bennée break 316*f73e4852SAlex Bennée # Make substitutions 317*f73e4852SAlex Bennée if registry is not None: 318e6f1306bSAlex Bennée dockerfile = dockerfile.replace("FROM qemu/", 319e6f1306bSAlex Bennée "FROM %s/qemu/" % 320e6f1306bSAlex Bennée (registry)) 321e6f1306bSAlex Bennée 3224112aff7SAlex Bennée tmp_df = tempfile.NamedTemporaryFile(mode="w+t", 3234112aff7SAlex Bennée encoding='utf-8', 3244112aff7SAlex Bennée dir=docker_dir, suffix=".docker") 3254485b04bSFam Zheng tmp_df.write(dockerfile) 3264485b04bSFam Zheng 327414a8ce5SAlex Bennée if user: 328414a8ce5SAlex Bennée uid = os.getuid() 329414a8ce5SAlex Bennée uname = getpwuid(uid).pw_name 330414a8ce5SAlex Bennée tmp_df.write("\n") 331414a8ce5SAlex Bennée tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % 332414a8ce5SAlex Bennée (uname, uid, uname)) 333414a8ce5SAlex Bennée 3344485b04bSFam Zheng tmp_df.write("\n") 335e6f1306bSAlex Bennée tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % (checksum)) 336f9172822SAlex Bennée for f, c in extra_files_cksum: 337f9172822SAlex Bennée tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c)) 338f9172822SAlex Bennée 3394485b04bSFam Zheng tmp_df.flush() 340a9f8d038SAlex Bennée 341e6f1306bSAlex Bennée build_args = ["build", "-t", tag, "-f", tmp_df.name] 342e6f1306bSAlex Bennée if self._buildkit: 343e6f1306bSAlex Bennée build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"] 344e6f1306bSAlex Bennée 345e6f1306bSAlex Bennée if registry is not None: 346*f73e4852SAlex Bennée pull_args = ["pull", "%s/%s" % (registry, tag)] 347*f73e4852SAlex Bennée self._do(pull_args, quiet=quiet) 348e6f1306bSAlex Bennée cache = "%s/%s" % (registry, tag) 349e6f1306bSAlex Bennée build_args += ["--cache-from", cache] 350e6f1306bSAlex Bennée build_args += argv 351e6f1306bSAlex Bennée build_args += [docker_dir] 352e6f1306bSAlex Bennée 353e6f1306bSAlex Bennée self._do_check(build_args, 3544485b04bSFam Zheng quiet=quiet) 3554485b04bSFam Zheng 3566e733da6SAlex Bennée def update_image(self, tag, tarball, quiet=True): 3576e733da6SAlex Bennée "Update a tagged image using " 3586e733da6SAlex Bennée 3590b95ff72SFam Zheng self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball) 3606e733da6SAlex Bennée 3614485b04bSFam Zheng def image_matches_dockerfile(self, tag, dockerfile): 3624485b04bSFam Zheng try: 3634485b04bSFam Zheng checksum = self.get_image_dockerfile_checksum(tag) 3644485b04bSFam Zheng except Exception: 3654485b04bSFam Zheng return False 366c1958e9dSFam Zheng return checksum == _text_checksum(_dockerfile_preprocess(dockerfile)) 3674485b04bSFam Zheng 36871ebbe09SAlex Bennée def run(self, cmd, keep, quiet, as_user=False): 369529994e2SAlex Bennée label = uuid.uuid4().hex 3704485b04bSFam Zheng if not keep: 371529994e2SAlex Bennée self._instance = label 37271ebbe09SAlex Bennée 37371ebbe09SAlex Bennée if as_user: 37471ebbe09SAlex Bennée uid = os.getuid() 37571ebbe09SAlex Bennée cmd = [ "-u", str(uid) ] + cmd 37671ebbe09SAlex Bennée # podman requires a bit more fiddling 37771ebbe09SAlex Bennée if self._command[0] == "podman": 378b3a790beSJohn Snow cmd.insert(0, '--userns=keep-id') 37971ebbe09SAlex Bennée 3800b95ff72SFam Zheng ret = self._do_check(["run", "--label", 3814485b04bSFam Zheng "com.qemu.instance.uuid=" + label] + cmd, 3824485b04bSFam Zheng quiet=quiet) 3834485b04bSFam Zheng if not keep: 384529994e2SAlex Bennée self._instance = None 3854485b04bSFam Zheng return ret 3864485b04bSFam Zheng 3874b08af60SFam Zheng def command(self, cmd, argv, quiet): 3884b08af60SFam Zheng return self._do([cmd] + argv, quiet=quiet) 3894b08af60SFam Zheng 390432d8ad5SAlex Bennée 3914485b04bSFam Zhengclass SubCommand(object): 3924485b04bSFam Zheng """A SubCommand template base class""" 3934485b04bSFam Zheng name = None # Subcommand name 394432d8ad5SAlex Bennée 3954485b04bSFam Zheng def shared_args(self, parser): 3964485b04bSFam Zheng parser.add_argument("--quiet", action="store_true", 397e50a6121SStefan Weil help="Run quietly unless an error occurred") 3984485b04bSFam Zheng 3994485b04bSFam Zheng def args(self, parser): 4004485b04bSFam Zheng """Setup argument parser""" 4014485b04bSFam Zheng pass 402432d8ad5SAlex Bennée 4034485b04bSFam Zheng def run(self, args, argv): 4044485b04bSFam Zheng """Run command. 4054485b04bSFam Zheng args: parsed argument by argument parser. 4064485b04bSFam Zheng argv: remaining arguments from sys.argv. 4074485b04bSFam Zheng """ 4084485b04bSFam Zheng pass 4094485b04bSFam Zheng 410432d8ad5SAlex Bennée 4114485b04bSFam Zhengclass RunCommand(SubCommand): 4124485b04bSFam Zheng """Invoke docker run and take care of cleaning up""" 4134485b04bSFam Zheng name = "run" 414432d8ad5SAlex Bennée 4154485b04bSFam Zheng def args(self, parser): 4164485b04bSFam Zheng parser.add_argument("--keep", action="store_true", 4174485b04bSFam Zheng help="Don't remove image when command completes") 4182461d80eSMarc-André Lureau parser.add_argument("--run-as-current-user", action="store_true", 4192461d80eSMarc-André Lureau help="Run container using the current user's uid") 420432d8ad5SAlex Bennée 4214485b04bSFam Zheng def run(self, args, argv): 42271ebbe09SAlex Bennée return Docker().run(argv, args.keep, quiet=args.quiet, 42371ebbe09SAlex Bennée as_user=args.run_as_current_user) 4244485b04bSFam Zheng 425432d8ad5SAlex Bennée 4264485b04bSFam Zhengclass BuildCommand(SubCommand): 427432d8ad5SAlex Bennée """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>""" 4284485b04bSFam Zheng name = "build" 429432d8ad5SAlex Bennée 4304485b04bSFam Zheng def args(self, parser): 431504ca3c2SAlex Bennée parser.add_argument("--include-executable", "-e", 432504ca3c2SAlex Bennée help="""Specify a binary that will be copied to the 433504ca3c2SAlex Bennée container together with all its dependent 434504ca3c2SAlex Bennée libraries""") 435dfae6284SPaolo Bonzini parser.add_argument("--extra-files", nargs='*', 4364c84f662SPhilippe Mathieu-Daudé help="""Specify files that will be copied in the 4374c84f662SPhilippe Mathieu-Daudé Docker image, fulfilling the ADD directive from the 4384c84f662SPhilippe Mathieu-Daudé Dockerfile""") 439414a8ce5SAlex Bennée parser.add_argument("--add-current-user", "-u", dest="user", 440414a8ce5SAlex Bennée action="store_true", 441414a8ce5SAlex Bennée help="Add the current user to image's passwd") 442e6f1306bSAlex Bennée parser.add_argument("--registry", "-r", 443e6f1306bSAlex Bennée help="cache from docker registry") 444dfae6284SPaolo Bonzini parser.add_argument("-t", dest="tag", 4454485b04bSFam Zheng help="Image Tag") 446dfae6284SPaolo Bonzini parser.add_argument("-f", dest="dockerfile", 4474485b04bSFam Zheng help="Dockerfile name") 4484485b04bSFam Zheng 4494485b04bSFam Zheng def run(self, args, argv): 4504112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 4514485b04bSFam Zheng tag = args.tag 4524485b04bSFam Zheng 4534485b04bSFam Zheng dkr = Docker() 4546fe3ae3fSAlex Bennée if "--no-cache" not in argv and \ 4556fe3ae3fSAlex Bennée dkr.image_matches_dockerfile(tag, dockerfile): 4564485b04bSFam Zheng if not args.quiet: 457f03868bdSEduardo Habkost print("Image is up to date.") 458a9f8d038SAlex Bennée else: 459a9f8d038SAlex Bennée # Create a docker context directory for the build 460a9f8d038SAlex Bennée docker_dir = tempfile.mkdtemp(prefix="docker_build") 4614485b04bSFam Zheng 46215352decSAlex Bennée # Validate binfmt_misc will work 46315352decSAlex Bennée if args.include_executable: 464d10404b1SAlex Bennée qpath, enabled = _check_binfmt_misc(args.include_executable) 465d10404b1SAlex Bennée if not enabled: 46615352decSAlex Bennée return 1 46715352decSAlex Bennée 468920776eaSAlex Bennée # Is there a .pre file to run in the build context? 469920776eaSAlex Bennée docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" 470920776eaSAlex Bennée if os.path.exists(docker_pre): 471f8042deaSSascha Silbe stdout = DEVNULL if args.quiet else None 472920776eaSAlex Bennée rc = subprocess.call(os.path.realpath(docker_pre), 473f8042deaSSascha Silbe cwd=docker_dir, stdout=stdout) 474920776eaSAlex Bennée if rc == 3: 475f03868bdSEduardo Habkost print("Skip") 476920776eaSAlex Bennée return 0 477920776eaSAlex Bennée elif rc != 0: 478f03868bdSEduardo Habkost print("%s exited with code %d" % (docker_pre, rc)) 479920776eaSAlex Bennée return 1 480920776eaSAlex Bennée 4814c84f662SPhilippe Mathieu-Daudé # Copy any extra files into the Docker context. These can be 4824c84f662SPhilippe Mathieu-Daudé # included by the use of the ADD directive in the Dockerfile. 483438d1168SPhilippe Mathieu-Daudé cksum = [] 484504ca3c2SAlex Bennée if args.include_executable: 485438d1168SPhilippe Mathieu-Daudé # FIXME: there is no checksum of this executable and the linked 486438d1168SPhilippe Mathieu-Daudé # libraries, once the image built any change of this executable 487438d1168SPhilippe Mathieu-Daudé # or any library won't trigger another build. 488d10404b1SAlex Bennée _copy_binary_with_libs(args.include_executable, 489d10404b1SAlex Bennée qpath, docker_dir) 490d10404b1SAlex Bennée 4914c84f662SPhilippe Mathieu-Daudé for filename in args.extra_files or []: 4924c84f662SPhilippe Mathieu-Daudé _copy_with_mkdir(filename, docker_dir) 493f9172822SAlex Bennée cksum += [(filename, _file_checksum(filename))] 494504ca3c2SAlex Bennée 49506cc3551SPhilippe Mathieu-Daudé argv += ["--build-arg=" + k.lower() + "=" + v 4964112aff7SAlex Bennée for k, v in os.environ.items() 49706cc3551SPhilippe Mathieu-Daudé if k.lower() in FILTERED_ENV_NAMES] 498a9f8d038SAlex Bennée dkr.build_image(tag, docker_dir, dockerfile, 499e6f1306bSAlex Bennée quiet=args.quiet, user=args.user, 500e6f1306bSAlex Bennée argv=argv, registry=args.registry, 501438d1168SPhilippe Mathieu-Daudé extra_files_cksum=cksum) 502a9f8d038SAlex Bennée 503a9f8d038SAlex Bennée rmtree(docker_dir) 504a9f8d038SAlex Bennée 5054485b04bSFam Zheng return 0 5064485b04bSFam Zheng 507432d8ad5SAlex Bennée 5086e733da6SAlex Bennéeclass UpdateCommand(SubCommand): 509432d8ad5SAlex Bennée """ Update a docker image with new executables. Args: <tag> <executable>""" 5106e733da6SAlex Bennée name = "update" 511432d8ad5SAlex Bennée 5126e733da6SAlex Bennée def args(self, parser): 5136e733da6SAlex Bennée parser.add_argument("tag", 5146e733da6SAlex Bennée help="Image Tag") 5156e733da6SAlex Bennée parser.add_argument("executable", 5166e733da6SAlex Bennée help="Executable to copy") 5176e733da6SAlex Bennée 5186e733da6SAlex Bennée def run(self, args, argv): 5196e733da6SAlex Bennée # Create a temporary tarball with our whole build context and 5206e733da6SAlex Bennée # dockerfile for the update 5216e733da6SAlex Bennée tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") 5226e733da6SAlex Bennée tmp_tar = TarFile(fileobj=tmp, mode='w') 5236e733da6SAlex Bennée 5247e81d198SAlex Bennée # Add the executable to the tarball, using the current 525d10404b1SAlex Bennée # configured binfmt_misc path. If we don't get a path then we 526d10404b1SAlex Bennée # only need the support libraries copied 527d10404b1SAlex Bennée ff, enabled = _check_binfmt_misc(args.executable) 5287e81d198SAlex Bennée 529d10404b1SAlex Bennée if not enabled: 530d10404b1SAlex Bennée print("binfmt_misc not enabled, update disabled") 531d10404b1SAlex Bennée return 1 532d10404b1SAlex Bennée 533d10404b1SAlex Bennée if ff: 5346e733da6SAlex Bennée tmp_tar.add(args.executable, arcname=ff) 5356e733da6SAlex Bennée 5366e733da6SAlex Bennée # Add any associated libraries 5376e733da6SAlex Bennée libs = _get_so_libs(args.executable) 5386e733da6SAlex Bennée if libs: 5396e733da6SAlex Bennée for l in libs: 5406e733da6SAlex Bennée tmp_tar.add(os.path.realpath(l), arcname=l) 5416e733da6SAlex Bennée 5426e733da6SAlex Bennée # Create a Docker buildfile 5436e733da6SAlex Bennée df = StringIO() 5446e733da6SAlex Bennée df.write("FROM %s\n" % args.tag) 5456e733da6SAlex Bennée df.write("ADD . /\n") 5466e733da6SAlex Bennée df.seek(0) 5476e733da6SAlex Bennée 5486e733da6SAlex Bennée df_tar = TarInfo(name="Dockerfile") 5496e733da6SAlex Bennée df_tar.size = len(df.buf) 5506e733da6SAlex Bennée tmp_tar.addfile(df_tar, fileobj=df) 5516e733da6SAlex Bennée 5526e733da6SAlex Bennée tmp_tar.close() 5536e733da6SAlex Bennée 5546e733da6SAlex Bennée # reset the file pointers 5556e733da6SAlex Bennée tmp.flush() 5566e733da6SAlex Bennée tmp.seek(0) 5576e733da6SAlex Bennée 5586e733da6SAlex Bennée # Run the build with our tarball context 5596e733da6SAlex Bennée dkr = Docker() 5606e733da6SAlex Bennée dkr.update_image(args.tag, tmp, quiet=args.quiet) 5616e733da6SAlex Bennée 5626e733da6SAlex Bennée return 0 5636e733da6SAlex Bennée 564432d8ad5SAlex Bennée 5654485b04bSFam Zhengclass CleanCommand(SubCommand): 5664485b04bSFam Zheng """Clean up docker instances""" 5674485b04bSFam Zheng name = "clean" 568432d8ad5SAlex Bennée 5694485b04bSFam Zheng def run(self, args, argv): 5704485b04bSFam Zheng Docker().clean() 5714485b04bSFam Zheng return 0 5724485b04bSFam Zheng 573432d8ad5SAlex Bennée 5744b08af60SFam Zhengclass ImagesCommand(SubCommand): 5754b08af60SFam Zheng """Run "docker images" command""" 5764b08af60SFam Zheng name = "images" 577432d8ad5SAlex Bennée 5784b08af60SFam Zheng def run(self, args, argv): 5794b08af60SFam Zheng return Docker().command("images", argv, args.quiet) 5804b08af60SFam Zheng 58115df9d37SAlex Bennée 58215df9d37SAlex Bennéeclass ProbeCommand(SubCommand): 58315df9d37SAlex Bennée """Probe if we can run docker automatically""" 58415df9d37SAlex Bennée name = "probe" 58515df9d37SAlex Bennée 58615df9d37SAlex Bennée def run(self, args, argv): 58715df9d37SAlex Bennée try: 58815df9d37SAlex Bennée docker = Docker() 58915df9d37SAlex Bennée if docker._command[0] == "docker": 5908480517dSAlex Bennée print("docker") 59115df9d37SAlex Bennée elif docker._command[0] == "sudo": 5928480517dSAlex Bennée print("sudo docker") 5939459f754SMarc-André Lureau elif docker._command[0] == "podman": 5949459f754SMarc-André Lureau print("podman") 59515df9d37SAlex Bennée except Exception: 596f03868bdSEduardo Habkost print("no") 59715df9d37SAlex Bennée 59815df9d37SAlex Bennée return 59915df9d37SAlex Bennée 60015df9d37SAlex Bennée 6015e03c2d8SAlex Bennéeclass CcCommand(SubCommand): 6025e03c2d8SAlex Bennée """Compile sources with cc in images""" 6035e03c2d8SAlex Bennée name = "cc" 6045e03c2d8SAlex Bennée 6055e03c2d8SAlex Bennée def args(self, parser): 6065e03c2d8SAlex Bennée parser.add_argument("--image", "-i", required=True, 6075e03c2d8SAlex Bennée help="The docker image in which to run cc") 60899cfdb86SAlex Bennée parser.add_argument("--cc", default="cc", 60999cfdb86SAlex Bennée help="The compiler executable to call") 6105e03c2d8SAlex Bennée parser.add_argument("--source-path", "-s", nargs="*", dest="paths", 6115e03c2d8SAlex Bennée help="""Extra paths to (ro) mount into container for 6125e03c2d8SAlex Bennée reading sources""") 6135e03c2d8SAlex Bennée 6145e03c2d8SAlex Bennée def run(self, args, argv): 6155e03c2d8SAlex Bennée if argv and argv[0] == "--": 6165e03c2d8SAlex Bennée argv = argv[1:] 6175e03c2d8SAlex Bennée cwd = os.getcwd() 6185e03c2d8SAlex Bennée cmd = ["--rm", "-w", cwd, 6195e03c2d8SAlex Bennée "-v", "%s:%s:rw" % (cwd, cwd)] 6205e03c2d8SAlex Bennée if args.paths: 6215e03c2d8SAlex Bennée for p in args.paths: 6225e03c2d8SAlex Bennée cmd += ["-v", "%s:%s:ro,z" % (p, p)] 62399cfdb86SAlex Bennée cmd += [args.image, args.cc] 6245e03c2d8SAlex Bennée cmd += argv 62571ebbe09SAlex Bennée return Docker().run(cmd, False, quiet=args.quiet, 62671ebbe09SAlex Bennée as_user=True) 6275e03c2d8SAlex Bennée 6285e03c2d8SAlex Bennée 629f97da1f7SAlex Bennéeclass CheckCommand(SubCommand): 630f97da1f7SAlex Bennée """Check if we need to re-build a docker image out of a dockerfile. 631f97da1f7SAlex Bennée Arguments: <tag> <dockerfile>""" 632f97da1f7SAlex Bennée name = "check" 633f97da1f7SAlex Bennée 634f97da1f7SAlex Bennée def args(self, parser): 635f97da1f7SAlex Bennée parser.add_argument("tag", 636f97da1f7SAlex Bennée help="Image Tag") 6377b882245SAlex Bennée parser.add_argument("dockerfile", default=None, 6387b882245SAlex Bennée help="Dockerfile name", nargs='?') 6397b882245SAlex Bennée parser.add_argument("--checktype", choices=["checksum", "age"], 6407b882245SAlex Bennée default="checksum", help="check type") 6417b882245SAlex Bennée parser.add_argument("--olderthan", default=60, type=int, 6427b882245SAlex Bennée help="number of minutes") 643f97da1f7SAlex Bennée 644f97da1f7SAlex Bennée def run(self, args, argv): 645f97da1f7SAlex Bennée tag = args.tag 646f97da1f7SAlex Bennée 64743e1b2ffSAlex Bennée try: 648f97da1f7SAlex Bennée dkr = Docker() 649432d8ad5SAlex Bennée except subprocess.CalledProcessError: 65043e1b2ffSAlex Bennée print("Docker not set up") 65143e1b2ffSAlex Bennée return 1 65243e1b2ffSAlex Bennée 653f97da1f7SAlex Bennée info = dkr.inspect_tag(tag) 654f97da1f7SAlex Bennée if info is None: 655f97da1f7SAlex Bennée print("Image does not exist") 656f97da1f7SAlex Bennée return 1 657f97da1f7SAlex Bennée 6587b882245SAlex Bennée if args.checktype == "checksum": 6597b882245SAlex Bennée if not args.dockerfile: 6607b882245SAlex Bennée print("Need a dockerfile for tag:%s" % (tag)) 6617b882245SAlex Bennée return 1 6627b882245SAlex Bennée 6634112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 6647b882245SAlex Bennée 665f97da1f7SAlex Bennée if dkr.image_matches_dockerfile(tag, dockerfile): 666f97da1f7SAlex Bennée if not args.quiet: 667f97da1f7SAlex Bennée print("Image is up to date") 668f97da1f7SAlex Bennée return 0 669f97da1f7SAlex Bennée else: 670f97da1f7SAlex Bennée print("Image needs updating") 671f97da1f7SAlex Bennée return 1 6727b882245SAlex Bennée elif args.checktype == "age": 6737b882245SAlex Bennée timestr = dkr.get_image_creation_time(info).split(".")[0] 6747b882245SAlex Bennée created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S") 6757b882245SAlex Bennée past = datetime.now() - timedelta(minutes=args.olderthan) 6767b882245SAlex Bennée if created < past: 6777b882245SAlex Bennée print ("Image created @ %s more than %d minutes old" % 6787b882245SAlex Bennée (timestr, args.olderthan)) 6797b882245SAlex Bennée return 1 6807b882245SAlex Bennée else: 6817b882245SAlex Bennée if not args.quiet: 6827b882245SAlex Bennée print ("Image less than %d minutes old" % (args.olderthan)) 6837b882245SAlex Bennée return 0 684f97da1f7SAlex Bennée 685f97da1f7SAlex Bennée 6864485b04bSFam Zhengdef main(): 6879459f754SMarc-André Lureau global USE_ENGINE 6889459f754SMarc-André Lureau 6894485b04bSFam Zheng parser = argparse.ArgumentParser(description="A Docker helper", 690432d8ad5SAlex Bennée usage="%s <subcommand> ..." % 691432d8ad5SAlex Bennée os.path.basename(sys.argv[0])) 6929459f754SMarc-André Lureau parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum), 6939459f754SMarc-André Lureau help="specify which container engine to use") 6944485b04bSFam Zheng subparsers = parser.add_subparsers(title="subcommands", help=None) 6954485b04bSFam Zheng for cls in SubCommand.__subclasses__(): 6964485b04bSFam Zheng cmd = cls() 6974485b04bSFam Zheng subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) 6984485b04bSFam Zheng cmd.shared_args(subp) 6994485b04bSFam Zheng cmd.args(subp) 7004485b04bSFam Zheng subp.set_defaults(cmdobj=cmd) 7014485b04bSFam Zheng args, argv = parser.parse_known_args() 7028480517dSAlex Bennée if args.engine: 7039459f754SMarc-André Lureau USE_ENGINE = args.engine 7044485b04bSFam Zheng return args.cmdobj.run(args, argv) 7054485b04bSFam Zheng 706432d8ad5SAlex Bennée 7074485b04bSFam Zhengif __name__ == "__main__": 7084485b04bSFam Zheng sys.exit(main()) 709