1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973
|
import argparse
import atexit
import cProfile
import collections
import collections.abc
import datetime
import enum
import functools
import hashlib
import io
import locale
import os
import platform
import pstats
import pwd
import re
import shlex
import shutil
import signal
import subprocess
import sys
import time
import traceback
import warnings
# List of dependency problems. This variable needs to be created before we
# import any other Charliecloud stuff to avoid #806.
depfails = [] # đź‘»
import filesystem as fs
import registry as rg
# Lazy-fail so this module can be imported even without building Charliecloud,
# e.g. in build and CI scripts.
try:
import version
except ModuleNotFoundError:
pass
## Enums ##
# Build cache mode.
class Build_Mode(enum.Enum):
ENABLED = "enabled"
DISABLED = "disabled"
REBUILD = "rebuild"
# Download cache mode.
class Download_Mode(enum.Enum):
ENABLED = "enabled"
WRITE_ONLY = "write-only"
# Root emulation mode
class Force_Mode(enum.Enum):
FAKEROOT = "fakeroot"
SECCOMP = "seccomp"
NONE = "none"
# Log level
@functools.total_ordering
class Log_Level(enum.Enum):
TRACE = 3
DEBUG = 2
VERBOSE = 1
INFO = 0
WARNING = -1
STDERR = -2
QUIET_STDERR = -3
# To support comparisons, we need to define at least one “ordering”
# operator. See: https://stackoverflow.com/a/39269589
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
# ch-run exit codes (see also: bin/ch_misc.h)
class Run_Exit(enum.Enum):
OK = 0
ERR_MISC = 31
ERR_CMD = 49
ERR_SQUASH = 84
## Constants ##
# Architectures. This maps the “machine” field returned by uname(2), also
# available as "uname -m" and platform.machine(), into architecture names that
# image registries use. It is incomplete (see e.g. [1], which is itself
# incomplete) but hopefully includes most architectures encountered in
# practice [e.g. 2]. Registry architecture and variant are separated by a
# slash. Note it is *not* 1-to-1: multiple uname(2) architectures map to the
# same registry architecture.
#
# [1]: https://stackoverflow.com/a/45125525
# [2]: https://github.com/docker-library/bashbrew/blob/v0.1.0/vendor/github.com/docker-library/go-dockerlibrary/architecture/oci-platform.go
ARCH_MAP = { "armv5l": "arm/v5",
"armv6l": "arm/v6",
"aarch32": "arm/v7",
"armv7l": "arm/v7",
"aarch64": "arm64/v8",
"armv8l": "arm64/v8",
"i386": "386",
"i686": "386",
"mips64le": "mips64le",
"ppc64le": "ppc64le",
"s390x": "s390x", # a.k.a. IBM Z
"x86_64": "amd64" }
# Some images have oddly specified architecture. For example, as of
# 2022-06-08, on Docker Hub, opensuse/leap:15.1 offers architectures amd64,
# arm/v7, arm64/v8, and ppc64le, while opensuse/leap:15.2 offers amd64, arm,
# arm64, and ppc64le, i.e., with no variants. This maps architectures to a
# sequence of fallback architectures that we hope are equivalent. See class
# Arch_Dict below.
ARCH_MAP_FALLBACK = { "arm/v7": ("arm",),
"arm64/v8": ("arm64",) }
# Incompatible option pairs for the ch-image command line
CLI_INCOMPATIBLE_OPTS = [("quiet", "verbose"),
("xattrs", "no_xattrs")]
# String to use as hint when we throw an error that suggests a bug.
BUG_REPORT_PLZ = "please report this bug: https://github.com/hpc/charliecloud/issues"
# Maximum filename (path component) length, in *characters*. All Linux
# filesystems of note that I could identify support at least 255 *bytes*. The
# problem is filenames with multi-byte characters: you cannot simply truncate
# byte-wise because you might do so in the middle of a character. So this is a
# somewhat random guess with hopefully enough headroom not to cause problems.
FILENAME_MAX_CHARS = 192
# Chunk size in bytes when streaming HTTP. Progress meter is updated once per
# chunk, which means the display is updated roughly every 20s at 100 Kbit/s
# and every 2s at 1Mbit/s; beyond that, the once-per-second display throttling
# takes over.
HTTP_CHUNK_SIZE = 256 * 1024
# Minimum versions. NOTE: Keep in sync with configure.ac.
PYTHON_MIN = (3,6)
RSYNC_MIN = (3,1,0)
## Globals ##
# Compatibility link. Sometimes we load pickled data from when Path was
# defined in this file. This alias lets us still load such pickles.
Path = fs.Path
# Active architecture (both using registry vocabulary)
arch = None # requested by user
arch_host = None # of host
# FIXME: currently set in ch-image :P
CH_BIN = None
CH_RUN = None
# Logging; set using init() below.
log_level = Log_Level(0) # Verbosity level.
log_festoon = False # If true, prepend pid and timestamp to chatter.
log_fp = sys.stderr # File object to print logs to.
trace_fatal = False # Add abbreviated traceback to fatal error hint.
# Warnings to be re-printed when program exits
warns = list()
# True if the download cache is enabled.
dlcache_p = None
# Profiling.
profiling = False
profile = None
# Width of terminal.
term_width = shutil.get_terminal_size(fallback=(sys.maxsize, -1))[0]
## Exceptions ##
class Fatal_Error(Exception):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
class Image_Unavailable_Error(Exception): pass
class No_Fatman_Error(Exception): pass
## Classes ##
class Arch_Dict(collections.UserDict):
"""Dictionary that overloads subscript and “in” to consider
ARCH_MAP_FALLBACK."""
def __contains__(self, k): # “in” operator
if (k in self.data):
return True
try:
return self._fallback_key(k) in self.data
except KeyError:
return False
def __getitem__(self, k):
try:
return self.data.__getitem__(k)
except KeyError:
return self.data.__getitem__(self._fallback_key(k))
def _fallback_key(self, k):
"""Return fallback key corresponding to key k, or raise KeyError if
there is no fallback."""
assert (k not in self.data)
if (k not in ARCH_MAP_FALLBACK):
raise KeyError("no fallbacks: %s" % k)
for f in ARCH_MAP_FALLBACK[k]:
if (f in self.data):
return f
raise KeyError("fallbacks also missing: %s" % k)
def in_warn(self, k):
"""Return True if k in self, False otherwise, just like the “in“
operator, but also log a warning if fallback is used."""
result = k in self
if (result and k not in self.data):
WARNING("arch %s requested but falling back to %s" %
(k, self._fallback_key(k)))
return result
class ArgumentParser(argparse.ArgumentParser):
class HelpFormatter(argparse.HelpFormatter):
# Suppress duplicate metavar printing when option has both short and
# long flavors. E.g., instead of:
#
# -s DIR, --storage DIR set builder internal storage directory to DIR
#
# print:
#
# -s, --storage DIR set builder internal storage directory to DIR
#
# From https://stackoverflow.com/a/31124505.
def _format_action_invocation(self, action):
if (not action.option_strings or action.nargs == 0):
return super()._format_action_invocation(action)
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
return ', '.join(action.option_strings) + ' ' + args_string
def __init__(self, sub_title=None, sub_metavar=None, *args, **kwargs):
super().__init__(formatter_class=self.HelpFormatter, *args, **kwargs)
self._optionals.title = "options" # https://stackoverflow.com/a/16981688
if (sub_title is not None):
self.subs = self.add_subparsers(title=sub_title, metavar=sub_metavar)
def add_parser(self, title, desc, *args, **kwargs):
return self.subs.add_parser(title, help=desc, description=desc,
*args, **kwargs)
def parse_args(self, *args, **kwargs):
cli = super().parse_args(*args, **kwargs)
if (not hasattr(cli, "func")):
self.error("CMD not specified")
# Bring in environment variables that set options.
if (cli.bucache is None and "CH_IMAGE_CACHE" in os.environ):
try:
cli.bucache = Build_Mode(os.environ["CH_IMAGE_CACHE"])
except ValueError:
FATAL("$CH_IMAGE_CACHE: invalid build cache mode: %s"
% os.environ["CH_IMAGE_CACHE"])
return cli
class OrderedSet(collections.abc.MutableSet):
# Note: The superclass provides basic implementations of all the other
# methods. I didn’t evaluate any of these.
__slots__ = ("data",)
def __init__(self, others=None):
self.data = collections.OrderedDict()
if (others is not None):
self.data.update((i, None) for i in others)
def __contains__(self, item):
return (item in self.data)
def __iter__(self):
return iter(self.data.keys())
def __len__(self):
return len(self.data)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, list(iter(self)))
def add(self, x):
self.data[x] = None
def clear(self):
# Superclass provides an implementation but warns it’s slow (and it is).
self.data.clear()
def discard(self, x):
self.data.pop(x, None)
class Progress:
"""Simple progress meter for countable things that updates at most once per
second. Writes first update upon creation. If length is None, then just
count up (this is for registries like Red Hat that sometimes don’t
provide a Content-Length header for blobs).
The purpose of the divisor is to allow counting things that are much
more numerous than what we want to display; for example, to count bytes
but report MiB, use a divisor of 1048576.
By default, moves to a new line at first update, then assumes exclusive
control of this line in the terminal, rewriting the line as needed. If
output is not a TTY or global log_festoon is set, each update is one log
entry with no overwriting."""
__slots__ = ("display_last",
"divisor",
"msg",
"length",
"unit",
"overwrite_p",
"precision",
"progress")
def __init__(self, msg, unit, divisor, length):
self.msg = msg
self.unit = unit
self.divisor = divisor
self.length = length
if (not os.isatty(log_fp.fileno()) or log_festoon):
self.overwrite_p = False # updates all use same line
else:
self.overwrite_p = True # each update on new line
self.precision = 1 if self.divisor >= 1000 else 0
self.progress = 0
self.display_last = float("-inf")
self.update(0)
def done(self):
self.update(0, True)
if (self.overwrite_p):
INFO("") # newline to release display line
def update(self, increment, last=False):
now = time.monotonic()
self.progress += increment
if (last or now - self.display_last > 1):
if (self.length is None):
line = ("%s: %.*f %s"
% (self.msg,
self.precision, self.progress / self.divisor,
self.unit))
else:
ct = "%.*f/%.*f" % (self.precision, self.progress / self.divisor,
self.precision, self.length / self.divisor)
pct = "%d%%" % (100 * self.progress / self.length)
if (ct == "0.0/0.0"):
# too small, don’t print count
line = "%s: %s" % (self.msg, pct)
else:
line = ("%s: %s %s (%s)" % (self.msg, ct, self.unit, pct))
INFO(line, end=("\r" if self.overwrite_p else "\n"))
self.display_last = now
class Progress_Reader:
"""Wrapper around a binary file object to maintain a progress meter while
reading."""
__slots__ = ("fp",
"msg",
"progress")
def __init__(self, fp, msg):
self.fp = fp
self.msg = msg
self.progress = None
def __iter__(self):
return self
def __next__(self):
data = self.read(HTTP_CHUNK_SIZE)
if (len(data) == 0):
raise StopIteration
return data
def close(self):
if (self.progress is not None):
self.progress.done()
close_(self.fp)
def read(self, size=-1):
data = ossafe("can’t read: %s" % self.fp.name, self.fp.read, size)
self.progress.update(len(data))
return data
def seek(self, *args):
raise io.UnsupportedOperation
def start(self):
# Get file size. This seems awkward, but I wasn’t able to find anything
# better. See: https://stackoverflow.com/questions/283707
old_pos = self.fp.tell()
assert (old_pos == 0) # math will be wrong if this isn’t true
length = self.fp.seek(0, os.SEEK_END)
self.fp.seek(old_pos)
self.progress = Progress(self.msg, "MiB", 2**20, length)
class Progress_Writer:
"""Wrapper around a binary file object to maintain a progress meter while
data are written. Overwrite the file if it already exists.
This downloads to a temporary file to ease recovery if the download is
interrupted. This uses a predictable name to support restarts in the
future, which would probably require verification after download. For
now, we just delete any leftover temporary files in Storage.init().
An interesting alternative is to download to an anonymous temporary file
that vanishes if not linked into the filesystem. Recent Linux provides a
very cool procedure to do this -- open(2) with O_TMPFILE followed by
linkat(2) [1] -- but it’s not always supported and the workaround
(create, then immediately unlink(2)) does not support re-linking [2].
This would also not support restarting the download.
[1]: https://man7.org/linux/man-pages/man2/open.2.html
[2]: https://stackoverflow.com/questions/4171713"""
__slots__ = ("fp",
"msg",
"path",
"path_tmp",
"progress")
def __init__(self, path, msg):
self.msg = msg
self.path = path
self.path_tmp = path.with_name("part_" + path.name)
self.progress = None
def close(self):
if (self.progress is not None):
self.progress.done()
close_(self.fp)
self.path.unlink(missing_ok=True)
self.path_tmp.rename(self.path)
def start(self, length):
self.progress = Progress(self.msg, "MiB", 2**20, length)
self.fp = self.path_tmp.open("wb")
def write(self, data):
self.progress.update(len(data))
ossafe("can’t write: %s" % self.path, self.fp.write, data)
class Timer:
__slots__ = ("start")
def __init__(self):
self.start = time.time()
def log(self, msg):
VERBOSE("%s in %.3fs" % (msg, time.time() - self.start))
## Supporting functions ##
def DEBUG(msg, hint=None, **kwargs):
if (log_level >= Log_Level.DEBUG):
log(msg, hint, None, "38;5;6m", "", **kwargs) # dark cyan (same as 36m)
def ERROR(msg, hint=None, trace=None, **kwargs):
log(msg, hint, trace, "1;31m", "error: ", **kwargs) # bold red
def FATAL(msg, hint=None, **kwargs):
if (trace_fatal):
# One-line traceback, skipping top entry (which is always bootstrap code
# calling ch-image.main()) and last entry (this function).
tr = ", ".join("%s:%d:%s" % (os.path.basename(f.filename),
f.lineno, f.name)
for f in reversed(traceback.extract_stack()[1:-1]))
else:
tr = None
raise Fatal_Error(msg, hint, tr, **kwargs)
def ILLERI(msg, hint=None, **kwargs):
# For temporary debugging only. See contributors’ guide.
log(msg, hint, None, "38;5;207m", "", **kwargs) # hot pink
def INFO(msg, hint=None, **kwargs):
"Note: Use print() for output; this function is for logging."
if (log_level >= Log_Level.INFO):
log(msg, hint, None, "33m", "", **kwargs) # yellow
def TRACE(msg, hint=None, **kwargs):
if (log_level >= Log_Level.TRACE):
log(msg, hint, None, "38;5;6m", "", **kwargs) # dark cyan (same as 36m)
def VERBOSE(msg, hint=None, **kwargs):
if (log_level >= Log_Level.VERBOSE):
log(msg, hint, None, "38;5;14m", "", **kwargs) # light cyan (1;36m, not bold)
def WARNING(msg, hint=None, msg_save=True, **kwargs):
if (log_level > Log_Level.STDERR):
if (msg_save):
warns.append(msg)
log(msg, hint, None, "31m", "warning: ", **kwargs) # red
def arch_host_get():
"Return the registry architecture of the host."
arch_uname = platform.machine()
VERBOSE("host architecture from uname: %s" % arch_uname)
try:
arch_registry = ARCH_MAP[arch_uname]
except KeyError:
FATAL("unknown host architecture: %s" % arch_uname, BUG_REPORT_PLZ)
VERBOSE("host architecture for registry: %s" % arch_registry)
return arch_registry
def argv_to_string(argv):
return " ".join(shlex.quote(i).replace("\n", "\\n") for i in argv)
def bytes_hash(data):
"Return the hash of data, as a hex string with no leading algorithm tag."
h = hashlib.sha256()
h.update(data)
return h.hexdigest()
def ch_run_modify(img, args, env, workdir="/", binds=[], ch_run_args=[],
fail_ok=False):
# Note: If you update these arguments, update the ch-image(1) man page too.
args = ( [CH_BIN + "/ch-run"]
+ ch_run_args
+ ["-w", "-u0", "-g0", "--no-passwd", "--cd", workdir, "--unsafe"]
+ sum([["-b", i] for i in binds], [])
+ [img, "--"] + args)
return cmd(args, env=env, stderr=None, fail_ok=fail_ok)
def close_(fp):
try:
path = fp.name
except AttributeError:
path = "(no path)"
ossafe("can’t close: %s" % path, fp.close)
def cmd(argv, fail_ok=False, **kwargs):
"""Run command using cmd_base(). If fail_ok, return the exit code whether
or not the process succeeded; otherwise, return (zero) only if the
process succeeded and exit with fatal error if it failed."""
if (log_level < Log_Level.WARNING):
kwargs["stdout"] = subprocess.DEVNULL
if (log_level <= Log_Level.QUIET_STDERR):
kwargs["stderr"] = subprocess.DEVNULL
cp = cmd_base(argv, fail_ok=fail_ok, **kwargs)
return cp.returncode
def cmd_base(argv, fail_ok=False, **kwargs):
"""Run a command to completion. If not fail_ok, exit with a fatal error if
the command fails (i.e., doesn’t exit with code zero). Return the
CompletedProcess object.
The command’s stderr is suppressed unless (1) logging is DEBUG or higher
or (2) fail_ok is False and the command fails."""
argv = [str(i) for i in argv]
VERBOSE("executing: %s" % argv_to_string(argv))
if ("env" in kwargs):
for (k,v) in sorted(kwargs["env"].items()):
VERBOSE("env: %s=%s" % (k,v))
if ("stderr" not in kwargs):
if (log_level <= Log_Level.INFO): # VERBOSE or lower: capture for printing on fail only
kwargs["stderr"] = subprocess.PIPE
if ("input" not in kwargs):
kwargs["stdin"] = subprocess.DEVNULL
cp = cmd_simple(argv, **kwargs)
if (not fail_ok and cp.returncode != 0):
if (cp.stderr is not None):
if (isinstance(cp.stderr, bytes)):
cp.stderr = cp.stderr.decode("UTF-8")
sys.stderr.write(cp.stderr)
sys.stderr.flush()
FATAL("command failed with code %d: %s"
% (cp.returncode, argv_to_string(argv)))
return cp
def cmd_quiet(argv, **kwargs):
"""Run command using cmd() and return the exit code. If logging is verbose
or lower, discard stdout."""
if (log_level >= Log_Level.DEBUG): # debug or higher
stdout=None
else:
stdout=subprocess.DEVNULL
return cmd(argv, stdout=stdout, **kwargs)
def cmd_simple(argv, **kwargs):
"""Run a command and return its subprocess.CompletedProcess instance. If it
couldn’t be started, log that and return a fake CompletedProcess."""
try:
profile_stop()
cp = subprocess.run(argv, **kwargs)
profile_start()
except OSError as x:
VERBOSE("can’t execute %s: %s" % (argv[0], x.strerror))
# Most common reason we are here is that the command isn’t found, which
# generates a FileNotFoundError. Use fake return value 127; this is
# consistent with the shell [1]. This is a kludge, but we assume the
# caller doesn’t care about the distinction between some problem within
# the subprocess and inability to start the subprocess.
#
# [1]: https://devdocs.io/bash/exit-status#Exit-Status
cp = subprocess.CompletedProcess(argv, 127)
return cp
def cmd_stdout(argv, encoding="UTF-8", **kwargs):
"""Run command using cmd_base(), capturing its standard output. Return the
CompletedProcess object (its stdout is available in the “stdout”
attribute). If logging is debug or higher, print stdout."""
cp = cmd_base(argv, encoding=encoding, stdout=subprocess.PIPE, **kwargs)
if (log_level >= Log_Level.DEBUG): # debug or higher
# just dump to stdout rather than using DEBUG() to match cmd_quiet
sys.stdout.write(cp.stdout)
sys.stdout.flush()
return cp
def color_reset(*fps):
for fp in fps:
color_set("0m", fp)
def color_set(color, fp):
if (fp.isatty()):
print("\033[" + color, end="", flush=True, file=fp)
def dependencies_check():
"""Check more dependencies. If any dependency problems found, here or above
(e.g., lark module checked at import time), then complain and exit."""
# enforce Python minimum version
vsys_py = sys.version_info[:3] # 4th element is a string
if (vsys_py < PYTHON_MIN):
vmin_py_str = ".".join(("%d" % i) for i in PYTHON_MIN)
vsys_py_str = ".".join(("%d" % i) for i in vsys_py)
depfails.append(("bad", ("need Python %s but running under %s: %s"
% (vmin_py_str, vsys_py_str, sys.executable))))
# report problems & exit
for (p, v) in depfails:
ERROR("%s dependency: %s" % (p, v))
if (len(depfails) > 0):
exit(1)
def digest_trim(d):
"""Remove the algorithm tag from digest d and return the rest.
>>> digest_trim("sha256:foobar")
'foobar'
Note: Does not validate the form of the rest."""
try:
return d.split(":", maxsplit=1)[1]
except AttributeError:
FATAL("not a string: %s" % repr(d))
except IndexError:
FATAL("no algorithm tag: %s" % d)
def done_notify():
if (user() == "jogas"):
INFO("!!! KOBE !!!")
else:
INFO("done")
def exit(code):
profile_stop()
profile_dump()
sys.exit(code)
def init(cli):
# logging
global log_festoon, log_fp, log_level, trace_fatal, xattrs_save
incomp_opts = 0
for (x,y) in CLI_INCOMPATIBLE_OPTS:
if (getattr(cli, x) and getattr(cli, y)):
ERROR("“--%s” incompatible with “--%s”" % ((x.replace("_","-"),
y.replace("_","-"))))
incomp_opts += 1
if (incomp_opts > 0):
FATAL("%d incompatible option pair(s)" % incomp_opts)
xattrs_save = ((cli.xattrs) or (("CH_XATTRS" in os.environ) and (not cli.no_xattrs)))
trace_fatal = (cli.debug or bool(os.environ.get("CH_IMAGE_DEBUG", False)))
log_level = Log_Level(cli.verbose - cli.quiet)
assert (-3 <= log_level.value <= 3)
if (log_level <= Log_Level.STDERR):
# suppress writing to stdout (particularly “print”).
sys.stdout = open(os.devnull, 'w')
if ("CH_LOG_FESTOON" in os.environ):
log_festoon = True
file_ = os.getenv("CH_LOG_FILE")
if (file_ is not None):
log_fp = file_.open("at")
atexit.register(color_reset, log_fp)
VERBOSE("version: %s" % version.VERSION)
VERBOSE("verbose level: %d (%s))" % (log_level.value, log_level.name))
VERBOSE("save xattrs: %s" % str(xattrs_save))
# signal handling
signal.signal(signal.SIGINT, sigterm)
signal.signal(signal.SIGTERM, sigterm)
# storage directory
global storage
storage = fs.Storage(cli.storage)
fs.storage_lock = not cli.no_lock
# architecture
global arch, arch_host
assert (cli.arch is not None)
arch_host = arch_host_get()
if (cli.arch == "host"):
arch = arch_host
else:
arch = cli.arch
# download cache
if (cli.always_download):
dlcache = Download_Mode.WRITE_ONLY
else:
dlcache = Download_Mode.ENABLED
global dlcache_p
dlcache_p = (dlcache == Download_Mode.ENABLED)
# registry authentication
if (cli.func.__module__ == "push"):
rg.auth_p = True
elif (cli.auth):
rg.auth_p = True
elif ("CH_IMAGE_AUTH" in os.environ):
rg.auth_p = (os.environ["CH_IMAGE_AUTH"] == "yes")
else:
rg.auth_p = False
VERBOSE("registry authentication: %s" % rg.auth_p)
# Red Hat Python warns about tar bugs, citing CVE-2007-4559.
# We mitigate this already, so suppress the noise. (#1818)
warnings.filterwarnings("ignore", module=r"^tarfile$",
message=( "^The default behavior of tarfile"
+ " extraction has been changed to disallow"
+ " common exploits"))
# misc
global password_many, profiling
password_many = cli.password_many
profiling = cli.profile
if (cli.tls_no_verify):
rg.tls_verify = False
rpu = rg.requests.packages.urllib3
rpu.disable_warnings(rpu.exceptions.InsecureRequestWarning)
def kill_blocking(pid, timeout=10):
"""Kill process pid with SIGTERM (the friendly one) and wait for it to
exit. If timeout (in seconds) is exceeded and it’s still running, exit
with a fatal error. It is *not* an error if pid does not exist, to avoid
race conditions where we decide to kill a process and it exits before we
can send the signal."""
sig = signal.SIGTERM
try:
os.kill(pid, sig)
except ProcessLookupError: # ESRCH, no such process
return
except OSError as x:
FATAL("can’t signal PID %d with %d: %s" % (pid, sig, x.strerror))
for i in range(timeout*2):
try:
os.kill(pid, 0) # no effect on process
except ProcessLookupError: # done
return
except OSError as x:
FATAL("can’t signal PID %s with 0: %s" % (pid, x.strerror))
time.sleep(0.5)
FATAL("timeout of %ds exceeded trying to kill PID %d" % (timeout, pid),
BUG_REPORT_PLZ)
def log(msg, hint, trace, color, prefix, end="\n"):
if (color is not None):
color_set(color, log_fp)
if (log_festoon):
ts = datetime.datetime.now().isoformat(timespec="milliseconds")
festoon = ("%5d %s " % (os.getpid(), ts))
else:
festoon = ""
print(festoon, prefix, msg, sep="", file=log_fp, end=end, flush=True)
if (hint is not None):
print(festoon, "hint: ", hint, sep="", file=log_fp, flush=True)
if (trace is not None):
print(festoon, "trace: ", trace, sep="", file=log_fp, flush=True)
if (color is not None):
color_reset(log_fp)
def monkey_write_streams():
"""Monkey patch to replace problematic characters in stdout and stderr
streams when running Python 3.6. (see #1629)."""
def monkey_write_insert(f):
write_orig = f.write
def write_monkey(text):
text = text.replace("“", "\"").replace("”", "\"").replace("’", "'")
write_orig(text)
f.write = write_monkey
# Try to encode test string of problematic characters. If unsuccessful,
# monkey patch them out.
for stream in sys.stdout, sys.stderr:
for encoding in stream.encoding, locale.getpreferredencoding(), "ASCII":
if (encoding is not None):
try:
"“”’".encode(encoding=encoding)
except UnicodeEncodeError:
monkey_write_insert(stream)
break
def now_utc_iso8601():
return datetime.datetime.utcnow().isoformat(timespec="seconds") + "Z"
def ossafe(msg, f, *args, **kwargs):
"""Call f with args and kwargs. Catch OSError and other problems and fail
with a nice error message."""
try:
return f(*args, **kwargs)
except OSError as x:
FATAL("%s: %s" % (msg, x.strerror))
def positive(x):
"""Convert x to float, then if ≤ 0, change to positive infinity. This is
monstly a convenience function to let 0 express “unlimited”."""
x = float(x)
if (x <= 0):
x = float("inf")
return x
def prefix_path(prefix, path):
""""Return True if prefix is a parent directory of path.
Assume that prefix and path are strings."""
return prefix == path or (prefix + '/' == path[:len(prefix) + 1])
def profile_dump():
"If profiling, dump the profile data."
if (profiling):
INFO("writing profile files ...")
fp = fs.Path("/tmp/chofile.txt").open("wt")
ps = pstats.Stats(profile, stream=fp)
ps.sort_stats(pstats.SortKey.CUMULATIVE)
ps.dump_stats("/tmp/chofile.p")
ps.print_stats()
close_(fp)
def profile_start():
"If profiling, start the profiler."
global profile
if (profiling):
if (profile is None):
INFO("initializing profiler")
profile = cProfile.Profile()
profile.enable()
def profile_stop():
"If profiling, stop the profiler."
if (profiling and profile is not None):
profile.disable()
def si_binary_bytes(ct):
# FIXME: varies between 1 and 3 significant figures
ct = float(ct)
for suffix in ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
if (ct < 1024):
return (ct, suffix)
ct /= 1024
assert False, "unreachable"
def si_decimal(ct):
ct = float(ct)
for suffix in ("", "K", "M", "G", "T", "P", "E", "Z"):
if (ct < 1000):
return (ct, suffix)
ct /= 1000
assert False, "unreachable"
def sigterm(signum, frame):
"Handler for SIGTERM and friends."
# Ignore further signals because we are already cleaning up.
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
# Don’t stomp on progress meter if one is being printed.
print()
signame = signal.Signals(signum).name
ERROR("received %s, exiting" % signame)
FATAL("received %s" % signame)
def user():
"""Return the current username; exit with error if it can’t be obtained.
See #1162. Logic must match username_set() in the C code."""
euid = os.geteuid()
try:
return pwd.getpwuid(euid).pw_name
except KeyError as x:
# AFAICT the “pwd” module does not interpret errno on getpwuid(3), so we
# can’t get any meaningful error reason.
FATAL("can’t get username for EUID %d" % euid)
def variables_sub(s, variables):
if (s is None):
return s
# FIXME: This should go in the grammar rather than being a regex kludge.
#
# Dockerfile spec does not say what to do if substituting a value that’s
# not set. We ignore those substitutions. This is probably wrong (the shell
# substitutes the empty string).
for (k, v) in variables.items():
# FIXME: remove when issue #774 is fixed
m = re.search(r"(?<!\\)\${.+?:[+-].+?}", s)
if (m is not None):
FATAL("modifiers ${foo:+bar} and ${foo:-bar} not yet supported (issue #774)")
s = re.sub(r"(?<!\\)\$({%s}|%s(?=\W|$))" % (k, k), v, s)
return s
def version_check(argv, min_, required=True, regex=r"(\d+)\.(\d+)\.(\d+)"):
"""Return True if the version number of program exectued as argv is at least
min_. Otherwise, including if execution fails, exit with error if
required, otherwise return False. Use regex to extract the version
number from output."""
if (required):
too_old = FATAL
bad_parse = FATAL
else:
too_old = VERBOSE
bad_parse = WARNING
prog = argv[0]
cp = cmd_stdout(argv, fail_ok=True, stderr=subprocess.STDOUT)
if (cp.returncode != 0):
too_old("%s failed with exit code %d, assuming not present"
% (prog, cp.returncode))
return False
m = re.search(regex, cp.stdout)
if (m is None):
bad_parse("can’t parse %s version, assuming not present: %s"
% (prog, cp.stdout))
return False
try:
v = tuple(int(i) for i in m.groups())
except ValueError:
bad_parse("can’t parse %s version part, assuming not present: %s"
% (prog, cp.stdout))
return False
if (min_ > v):
too_old("%s is too old: %d.%d.%d < %d.%d.%d" % ((prog,) + v + min_))
return False
VERBOSE("%s version OK: %d.%d.%d ≥ %d.%d.%d" % ((prog,) + v + min_))
return True
def walk(*args, **kwargs):
"""Wrapper for os.walk(). Return a generator of the files in a directory
tree (root specified in *args). For each directory in said tree, yield a
3-tuple (dirpath, dirnames, filenames), where dirpath is a Path object,
and dirnames and filenames are lists of Path objects. For insight into
these being lists rather than generators, see use of ch.walk() in
Copy_G.copy_src_dir()."""
for (dirpath, dirnames, filenames) in os.walk(*args, **kwargs):
yield (fs.Path(dirpath),
[fs.Path(dirname) for dirname in dirnames],
[fs.Path(filename) for filename in filenames])
def warnings_dump():
if (len(warns) > 0):
WARNING("reprinting %d warning(s)" % len(warns), msg_save=False)
for msg in warns:
WARNING(msg, msg_save=False)
|