/usr/lib/python3/dist-packages/reprotest/build.py is in reprotest 0.7.7.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 | # Licensed under the GPL: https://www.gnu.org/licenses/gpl-3.0.en.html
# For details: reprotest/debian/copyright
import collections
import functools
import getpass
import grp
import logging
import os
import shlex
import shutil
import random
import time
import types
from reprotest import environ
from reprotest import mdiffconf
from reprotest import shell_syn
from reprotest.utils import AttributeReplacer
logger = logging.getLogger(__name__)
def tool_required(*tools):
def wrap(f):
@functools.wraps(f)
def wf(*args, **kwargs):
return f(*args, **kwargs)
wf.tool_required = tools
return wf
return wrap
def tool_missing(f):
if not hasattr(f, "tool_required"):
return []
return [t for t in f.tool_required if shutil.which(t) is None]
def dirname(p):
# works more intuitively for paths with a trailing /
return os.path.normpath(os.path.dirname(os.path.normpath(p)))
def basename(p):
# works more intuitively for paths with a trailing /
return os.path.normpath(os.path.basename(os.path.normpath(p)))
class Build(collections.namedtuple('_Build', 'build_command setup cleanup env tree aux_tree')):
'''Holds the shell ASTs and various other data, used to execute each build.
Fields:
build_command (shell_syn.Command): The build command itself, including
wrapper commands like setarch and sudo that never need cleanup.
setup (shell_syn.AndList): These are shell commands that change the
shell environment and need to be run as part of the same script as
the main build command but don't take other commands as arguments.
These execute conditionally because if one command fails,
the whole script should fail. Examples: cd, umask.
cleanup (shell_syn.List): All commands that have to be run to return
the testbed to its initial state, before the testbed does its own
cleanup. These execute one after another regardless of failure,
because all cleanup commands should be attempted irrespective of
whether others succeed. Examples: fileordering. This is *not* run
if no_clean_on_error is given and setup or build_command failed.
env (types.MappingProxyType): Immutable mapping of the environment.
tree (str): Path to the source root where the build should take place.
aux_tree (str): Path where auxilliary files are stored by reprotest.
When using cls.from_command(), this is automatically created and
cleaned up by the build script.
'''
@classmethod
def from_command(cls, build_command, env, tree):
aux_tree = os.path.join(dirname(tree), basename(tree) + '-aux')
_ = cls(
build_command = shell_syn.Command.make(
"sh", "-ec", shlex.quote(str(build_command))),
setup = shell_syn.AndList(),
cleanup = shell_syn.List(),
env = env,
tree = tree,
aux_tree = aux_tree,
)
_ = _.append_setup_exec('mkdir', '-p', aux_tree)
_ = _.prepend_cleanup_exec('rm', '-rf', aux_tree)
return _
def add_env(self, key, value):
'''Helper function for adding a key-value pair to an immutable mapping.'''
new_mapping = self.env.copy()
new_mapping[key] = value
return self._replace(env=types.MappingProxyType(new_mapping))
def modify_env(self, add, rem):
'''Helper function for adding a key-value pair to an immutable mapping.'''
new_mapping = self.env.copy()
for k, v in add:
new_mapping[k] = v
for k in rem:
del new_mapping[k]
return self._replace(env=types.MappingProxyType(new_mapping))
def prepend_to_build_command(self, *prefix):
'''Prepend a wrapper command onto the build_command.'''
return self.prepend_to_build_command_raw(*map(shlex.quote, prefix))
def prepend_to_build_command_raw(self, *prefix):
new_command = shell_syn.Command(
cmd_prefix=shell_syn.CmdPrefix(prefix),
cmd_suffix=self.build_command)
return self._replace(build_command=new_command)
def append_setup(self, command):
'''Adds a command to the setup phase.'''
new_setup = self.setup + shell_syn.AndList([command])
return self._replace(setup=new_setup)
def append_setup_exec(self, *args):
return self.append_setup_exec_raw(*map(shlex.quote, args))
def append_setup_exec_raw(self, *args):
return self.append_setup(shell_syn.Command.make(*args))
def prepend_cleanup(self, command):
'''Adds a command to the cleanup phase.'''
# if this command fails, save the exit code but keep executing
# we run with -e, so it would fail otherwise
new_cleanup = shell_syn.List.make("{0} || __c=$?".format(command))
return self._replace(cleanup=new_cleanup + self.cleanup)
def prepend_cleanup_exec(self, *args):
return self.prepend_cleanup_exec_raw(*map(shlex.quote, args))
def prepend_cleanup_exec_raw(self, *args):
return self.prepend_cleanup(shell_syn.Command.make(*args))
def move_tree(self, source, target, set_tree):
new_build = self.append_setup_exec(
'mv', source, target).prepend_cleanup_exec(
'mv', target, source)
if set_tree:
return new_build._replace(tree = os.path.join(target, ''))
else:
return new_build
def to_script(self, no_clean_on_error):
'''Generates the shell code for the script.
The build command is only executed if all the setup commands
finish without errors. The setup and build commands are
executed in a subshell so that changes they make to the shell
don't affect the cleanup commands. (This avoids the problem
with the disorderfs mount being kept open as a current working
directory when the cleanup tries to unmount it.)
'''
subshell = self.setup + shell_syn.AndList([self.build_command])
if self.cleanup:
cleanup = shell_syn.List.make("__c=0") + self.cleanup + \
shell_syn.List.make("exit $__c")
# TODO: the below can be extended with a custom command. shell
# doesn't work yet though; we need to hook into autopkgtest better.
whether_to_clean = '! ' + str(bool(no_clean_on_error)).lower()
main_script = """\
trap '( cleanup )' HUP INT QUIT ABRT TERM PIPE # FIXME doesn't quite work reliably yet
if ( run_build ); then ( cleanup ); else
__x=$?; # save the exit code of run_build
if ( {0} ); then
if ( cleanup ); then :; else echo >&2 "cleanup failed with exit code $?"; fi;
fi
exit $__x
fi
""".format(whether_to_clean)
return """\
run_build() {{
{0}
}}
cleanup() {{
{1}
}}
{2}
""".format(subshell.__str__(4), cleanup.__str__(4), main_script.rstrip()).rstrip()
else:
return str(subshell)
# time zone, locales, disorderfs, host name, user/group, shell, CPU
# number, architecture for uname (using linux64), umask, HOME, see
# also: https://tests.reproducible-builds.org/index_variations.html
# TODO: the below ideally should *read the current value*, and pick
# something that's different for the experiment.
def environment(ctx, build, vary):
if not vary:
return build
added, removed = [], []
for k, v in environ.parse_environ_templates(ctx.spec.environment.variables):
if v is None:
removed += [k]
else:
added += [(k, v)]
return build.modify_env(added, removed)
def domain_host(ctx, build, vary):
if not vary:
return build
hostname = "reprotest-capture-hostname"
domainname = "reprotest-capture-domainname"
_ = build
# TODO: below only works on linux, of course..
if ctx.spec.domain_host.use_sudo:
ns_uts = '%s/ns-%s' % (build.aux_tree, "uts")
_ = _.append_setup_exec('touch', ns_uts)
# create our unshare
ns_args = ['--uts=%s' % ns_uts]
_ = _.append_setup_exec(*SUDO, 'unshare', *ns_args, 'true')
_ = _.prepend_cleanup_exec(*SUDO, 'umount', ns_uts)
# configure our unshare
nsenter = SUDO + ['nsenter'] + ns_args
_ = _.append_setup_exec(*nsenter, 'hostname', hostname)
_ = _.append_setup_exec(*nsenter, 'domainname', domainname)
# wrap our build command
_ = _.prepend_to_build_command(*SUDO, '-E', *(nsenter[len(SUDO):]), *make_sudo_command(*current_user_group()))
else:
logger.warn("Not using sudo for domain_host; your build may fail. See man page for other options.")
logger.warn("Be sure to `echo 1 > /proc/sys/kernel/unprivileged_userns_clone` if on a Debian system.")
if "user_group" in ctx.spec and ctx.spec.user_group.available:
logger.error("Incompatible variations: domain_host.use_sudo False, user_group.available non-empty.")
raise ValueError("Incompatible variations; check the log for details.")
_ = _.prepend_to_build_command(*"unshare -r --uts".split(),
"sh", "-ec", r"""
hostname {1}
domainname "{2}"
""".format(build.aux_tree, hostname, domainname) + '"$@"', "-")
return _
# Note: this has to go before fileordering because we can't move mountpoints
# TODO: this variation makes it impossible to parallelise the build, for most
# of the current virtual servers. (It's theoretically possible to make it work)
def build_path(ctx, build, vary):
if vary:
return build
const_path = os.path.join(dirname(build.tree), 'const_build_path')
return build.move_tree(build.tree, const_path, True)
@tool_required("disorderfs")
def fileordering(ctx, build, vary):
if not vary:
return build
old_tree = os.path.join(dirname(build.tree), basename(build.tree) + '-before-disorderfs', '')
_ = build.move_tree(build.tree, old_tree, False)
_ = _.append_setup_exec('mkdir', '-p', build.tree)
_ = _.prepend_cleanup_exec('rmdir', build.tree)
disorderfs = ['disorderfs'] + (['>&2'] if ctx.verbosity >= 2 else ['-q'])
_ = _.append_setup_exec_raw(*disorderfs, *map(shlex.quote, ['--shuffle-dirents=yes', old_tree, build.tree]))
_ = _.prepend_cleanup_exec('fusermount', '-u', build.tree)
# the "user_group" variation hacks PATH to run "sudo -u XXX" instead of various tools, pick it up here
binpath = os.path.join(dirname(build.tree), 'bin')
_ = _.prepend_cleanup_exec_raw('export', 'PATH="%s:$PATH"' % binpath)
return _
# Note: this has to go after anything that might modify 'tree' e.g. build_path
def home(ctx, build, vary):
if not vary:
# choose an existent HOME, see Debian bug #860428
return build.add_env('HOME', build.tree)
else:
return build.add_env('HOME', '/nonexistent/second-build')
# TODO: uname is a POSIX standard. The related Linux command
# (setarch) only affects uname at the moment according to the docs.
# FreeBSD changes uname with environment variables. Wikipedia has a
# reference to a setname command on another Unix variant:
# https://en.wikipedia.org/wiki/Uname
def kernel(ctx, build, vary):
_ = build
if not vary:
_ = _.append_setup_exec_raw('SETARCH_ARCH=$(uname -m)')
else:
_ = _.append_setup_exec_raw('SETARCH_ARCH=$(setarch --list | grep -vF "$(uname -m)" | shuf | head -n1)')
_ = _.append_setup_exec_raw('KERNEL_VERSION=$(uname -r)')
_ = _.append_setup_exec_raw('if [ ${KERNEL_VERSION#2.6} = $KERNEL_VERSION ]; then SETARCH_OPTS=--uname-2.6; fi')
return _.prepend_to_build_command_raw('setarch', '$SETARCH_ARCH', '$SETARCH_OPTS')
def aslr(ctx, build, vary):
if vary:
return build
return build.append_setup_exec_raw('SETARCH_OPTS="$SETARCH_OPTS -R"')
def num_cpus(ctx, build, vary):
_ = build
_ = _.append_setup_exec_raw('CPU_MAX=$(nproc)')
_ = _.append_setup_exec_raw('CPU_MIN=$({ echo $CPU_MAX; echo %s; } | sort -n | head -n1)' % ctx.min_cpus)
if ctx.min_cpus <= 0:
raise ValueError("--min-cpus must be a positive integer: " % ctx.min_cpus)
if not vary:
_ = _.append_setup_exec_raw('CPU_NUM=$CPU_MIN')
else:
# random number between min_cpus and $(nproc)
_ = _.append_setup_exec_raw('CPU_NUM=$(if [ $CPU_MIN = $CPU_MAX ]; \
then echo $CPU_MIN; echo >&2 "only 1 CPU is available; num_cpus is ineffective"; \
else shuf -i$((CPU_MIN + 1))-$CPU_MAX -n1; fi)')
# select CPU_NUM random cpus from the range 0..$((CPU_MAX-1))
cpu_list = "$(echo $(shuf -i0-$((CPU_MAX - 1)) -n$CPU_NUM) | tr ' ' ,)"
return _.prepend_to_build_command_raw('taskset', '-a', '-c', cpu_list)
# TODO: if this locale doesn't exist on the system, Python's
# locales.getlocale() will return (None, None) rather than this
# locale. I imagine it will also probably cause false positives with
# builds being reproducible when they aren't because of locale-based
# issues if this locale isn't installed. The right solution here is
# for this locale to be encoded into the dependencies so installing it
# installs the right locale. A weaker but still reasonable solution
# is to figure out what locales are installed (how?) and use another
# locale if this one isn't installed.
# TODO: what exact locales and how to many test is probably a mailing
# list question.
def locales(ctx, build, vary):
if not vary:
return build.add_env('LANG', 'C.UTF-8').add_env('LANGUAGE', 'en_US:en')
else:
# if there is an issue with this being random, we could instead select it
# based on a deterministic hash of the inputs
loc = random.choice(['fr_CH.UTF-8', 'es_ES', 'ru_RU.CP1251', 'kk_KZ.RK1048', 'zh_CN'])
return build.add_env('LANG', loc).add_env('LC_ALL', loc).add_env('LANGUAGE', '%s:fr' % loc)
def exec_path(ctx, build, vary):
if not vary:
return build
return build.add_env('PATH', build.env['PATH'] + ':/i_capture_the_path')
# This doesn't require superuser privileges, but the chsh command
# affects all user shells, which would be bad.
# # def shell(ctx, script, env, tree):
# return script, env, tree
# FIXME: also test differences with /bin/sh as bash vs dash
def timezone(ctx, build, vary):
# These time zones are theoretically in the POSIX time zone format
# (http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08),
# so they should be cross-platform compatible.
if not vary:
return build.add_env('TZ', 'GMT+12')
else:
return build.add_env('TZ', 'GMT-14')
@tool_required("faketime")
def faketime(ctx, build, vary):
if not vary:
# fix the time at base_faketime
faket = ctx.base_faketime
elif ctx.spec.time.faketimes:
faket = random.choice(ctx.spec.time.faketimes)
else:
now = time.time()
base = int(ctx.base_faketime[1:]) if ctx.base_faketime.startswith("@") else now
# 15552000 = 180 days
if base < now - 15552000 and not random.randint(0, 1):
# if ctx.base_faketime is far in the past, with 1/2 probability
# reuse the current time and don't fake it
return build
else:
# otherwise use a date far in the future
faket = '+%sdays+%shours+%sminutes' % (
random.randint(180, 540), random.randint(0, 23), random.randint(0, 59))
# faketime's manpages are stupidly misleading; it also modifies file timestamps.
# this is only mentioned in the README. we do not want this, it really really
# messes with GNU make and other buildsystems that look at timestamps.
# set NO_FAKE_STAT=1 avoids this.
return build.add_env('NO_FAKE_STAT', '1').prepend_to_build_command('faketime', faket)
def umask(ctx, build, vary):
if not vary:
return build.append_setup_exec('umask', '0022')
else:
return build.append_setup_exec('umask', '0002')
def current_user_group():
return getpass.getuser(), grp.getgrgid(os.getgid()).gr_name
# -h localhost otherwise we get annoying messages about "can't resolve host"
# especially when doing the domain_host variation
SUDO = ['sudo', '-h', 'localhost']
def make_sudo_command(user, group):
assert user or group
userarg = ['-u', user] if user else []
grouparg = ['-g', group] if group else []
return SUDO + ['-E'] + userarg + grouparg + ['env',
'-u', 'SUDO_COMMAND', '-u', 'SUDO_GID', '-u', 'SUDO_UID', '-u', 'SUDO_USER']
def parse_user_group(user_group):
if not user_group or user_group == ':':
raise ValueError("user_group is empty: '%s'" % user_group)
if ":" in user_group:
user, group = user_group.split(":", 1)
if user:
return user, group
else:
return None, group
else:
return user_group, None
# Note: this needs to go before anything that might need to run setup commands
# as the other user (e.g. due to permissions).
@tool_required("sudo")
def user_group(ctx, build, vary):
if not vary:
return build
if not ctx.spec.user_group.available:
logger.warn("IGNORING user_group variation; supply more usergroups "
"with --variations=user_group.available+=USER1:GROUP1;USER2:GROUP2 or "
"alternatively, suppress this warning with --variations=-user_group")
return build
olduser, oldgroup = current_user_group()
user_group = random.choice(list(set(ctx.spec.user_group.available) - set([(olduser, oldgroup)])))
user, group = parse_user_group(user_group)
sudo_command = make_sudo_command(user, group)
if not user:
user = olduser
binpath = os.path.join(dirname(build.tree), 'bin')
_ = build.prepend_to_build_command(*sudo_command)
# disorderfs needs to run as a different user.
# we prefer that to running it as root, principle of least-privilege.
_ = _.append_setup_exec('sh', '-ec', r'''
mkdir -p "{0}"
printf '#!/bin/sh\n{1} /usr/bin/disorderfs "$@"\n' > "{0}"/disorderfs
chmod +x "{0}"/disorderfs
printf '#!/bin/sh\n{1} /bin/mkdir "$@"\n' > "{0}"/mkdir
chmod +x "{0}"/mkdir
printf '#!/bin/sh\n{1} /bin/fusermount "$@"\n' > "{0}"/fusermount
chmod +x "{0}"/fusermount
'''.format(binpath, " ".join(map(shlex.quote, sudo_command))))
_ = _.prepend_cleanup_exec('sh', '-ec',
'cd "{0}" && rm -f disorderfs mkdir fusermount'.format(binpath))
_ = _.append_setup_exec_raw('export', 'PATH="%s:$PATH"' % binpath)
if user != olduser:
_ = _.append_setup_exec(*SUDO, 'chown', '-h', '-R', '--from=%s' % olduser, user, build.tree)
# TODO: artifacts probably shouldn't be chown'd back
_ = _.prepend_cleanup_exec(*SUDO, 'chown', '-h', '-R', '--from=%s' % user, olduser, build.tree)
return _
# The order of the variations *is* important, because the command to
# be executed in the container needs to be built from the inside out.
VARIATIONS = collections.OrderedDict([
('environment', environment),
('build_path', build_path),
('kernel', kernel),
('aslr', aslr), # needs to run after kernel which runs "setarch"
# but also as close to the build command as possible, (i.e. earlier in this list)
# otherwise other variations below can affect the address layout
('num_cpus', num_cpus),
('time', faketime), # needs to go before sudo (user_group), closer to the build command
('user_group', user_group),
('fileordering', fileordering),
('domain_host', domain_host), # needs to run after all other mounts have been set
('home', home),
('locales', locales),
# ('namespace', namespace),
('exec_path', exec_path),
# ('shell', shell),
('timezone', timezone),
('umask', umask),
])
def auto_source_date_epoch(source_root):
# Get the latest modification date of all the files in the source root.
# This tries hard to avoid bad interactions with faketime and make(1) etc.
# However if you're building this too soon after changing one of the source
# files then the effect of this variation is not very great.
filemtimes = (os.lstat(os.path.join(root, f)).st_mtime
for root, dirs, files in os.walk(source_root)
for f in files)
return int(max(filemtimes, default=1))
class TimeVariation(collections.namedtuple('_TimeVariation', 'faketimes auto_faketimes')):
@classmethod
def default(cls):
return cls(mdiffconf.strlist_set(";"), mdiffconf.strlist_set(";", ['SOURCE_DATE_EPOCH']))
@classmethod
def empty(cls):
return cls(mdiffconf.strlist_set(";"), mdiffconf.strlist_set(";"))
class EnvironmentVariation(collections.namedtuple("_EnvironmentVariation", "variables")):
@classmethod
def default(cls):
return cls(mdiffconf.strlist_set(";", ["REPROTEST_CAPTURE_ENVIRONMENT"]))
def extend_variables(self, *ks):
return self._replace(variables=self.variables + list(ks))
class UserGroupVariation(collections.namedtuple('_UserGroupVariation', 'available')):
@classmethod
def default(cls):
return cls(mdiffconf.strlist_set(";"))
class DomainHostVariation(collections.namedtuple('_DomainHostVariation', 'use_sudo')):
@classmethod
def default(cls):
return cls(0)
class VariationSpec(mdiffconf.ImmutableNamespace):
@classmethod
def default(cls, variations=VARIATIONS):
default_overrides = {
"environment": EnvironmentVariation.default(),
"user_group": UserGroupVariation.default(),
"time": TimeVariation.default(),
"domain_host": DomainHostVariation.default(),
}
return cls(**{k: default_overrides.get(k, True) for k in variations})
@classmethod
def default_long_string(cls):
actions = cls.default().actions()
return ", ".join("+" + a[0] for a in actions)
@classmethod
def empty(cls):
return cls()
@classmethod
def all_names(cls):
return list(VARIATIONS.keys())
def variations(self):
return [k for k in VARIATIONS.keys() if k in self.__dict__]
aliases = { ("@+-", "all"): list(VARIATIONS.keys()) }
def extend(self, actions):
one = self.default()
return mdiffconf.parse_all(self, actions, one, one, self.aliases, sep=",")
def __contains__(self, k):
return k in self.__dict__
def __getitem__(self, k):
return self.__dict__[k]
def actions(self):
return [(k, k in self.__dict__, v) for k, v in VARIATIONS.items()]
class Variations(collections.namedtuple('_Variations', 'spec verbosity min_cpus base_faketime')):
@classmethod
def of(cls, *specs, zero=VariationSpec.empty(), verbosity=0, min_cpus=1, base_faketime="@0"):
return [cls(spec, verbosity, min_cpus, base_faketime) for spec in [zero] + list(specs)]
@property
def replace(self):
return AttributeReplacer(self, [])
def print_sudoers(spec):
logger.warn("This feature is EXPERIMENTAL, use at your own risk.")
logger.warn("The output may be out-of-date, please file bugs if it doesn't work...")
user, group = current_user_group()
a = "[a-zA-Z0-9]"
b = "/tmp/reprotest.{0}{0}{0}{0}{0}{0}".format(a)
variables = {
"user": user,
"group": group,
"base": b,
}
experiments = [os.path.join(b, x) for x in [
"build-experiment-[1-9]",
"build-experiment-[1-9][0-9]",
"build-experiment-blacklist",
"build-experiment-non-whitelist",
] + ["build-experiment-%s" % k for k in VariationSpec.all_names()]]
if "user_group" in spec and spec.user_group.available:
user_groups = [parse_user_group(user_group) for user_group in spec.user_group.available]
users = sorted(set(user for user, group in user_groups if user))
for otheruser in users:
newvars = dict(**variables, otheruser=otheruser)
print("""\
# Rules for varying user_group with user %(otheruser)s
%(user)s ALL = (%(otheruser)s) NOPASSWD: ALL
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(otheruser)s %(user)s %(base)s/const_build_path/
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(user)s %(otheruser)s %(base)s/const_build_path/
""".rstrip() % newvars)
for base_ex in experiments:
print("""\
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(otheruser)s %(user)s %(base_ex)s/
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(otheruser)s %(user)s %(base_ex)s-before-disorderfs/
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(user)s %(otheruser)s %(base_ex)s/
%(user)s ALL = NOPASSWD: /bin/chown -h -R --from=%(user)s %(otheruser)s %(base_ex)s-before-disorderfs/
""".rstrip() % dict(**newvars, base_ex=base_ex))
print()
if "domain_host" in spec and spec.domain_host.use_sudo:
print("""# Rules for varying domain_host""")
for base_ex in experiments:
print("""\
%(user)s ALL = NOPASSWD: /usr/bin/unshare --uts=%(base_ex)s-aux/ns-uts true
%(user)s ALL = NOPASSWD: /usr/bin/nsenter --uts=%(base_ex)s-aux/ns-uts hostname reprotest-*
%(user)s ALL = NOPASSWD: /usr/bin/nsenter --uts=%(base_ex)s-aux/ns-uts domainname reprotest-*
%(user)s ALL = NOPASSWD:SETENV: /usr/bin/nsenter --uts=%(base_ex)s-aux/ns-uts sudo -h localhost -E -u %(user)s -g %(group)s env *
%(user)s ALL = NOPASSWD: /bin/umount %(base_ex)s-aux/ns-uts
""".rstrip() % dict(**variables, base_ex=base_ex))
print()
if __name__ == "__main__":
import sys
d = VariationSpec()
for s in sys.argv[1:]:
d = d.extend([s])
print(s)
print(">>>", d)
print("result", d)
|