def run_build_tool(bazel_path, target, targets, squelch_output=False): workspace = find_workspace() if not workspace: return try: if not os.environ.get("BZL_SKIP_BOOTSTRAP"): # If we can bootstrap a new version, do it once. with metrics.create_and_register_timer("bzl_bootstrap_ms") as t: bzl_script = build_tool(bazel_path, target, targets, squelch_output=squelch_output) bzl_script_path = os.path.join(workspace, bzl_script) argv = [bzl_script_path] + list(sys.argv[1:]) os.environ["BZL_SKIP_BOOTSTRAP"] = "1" os.environ["BZL_BOOTSTRAP_MS"] = str(t.get_interval_ms()) os.environ["BZL_RUNNING_REBUILT_BZL"] = "1" exec_wrapper.execv(bzl_script_path, argv) else: # Propagate stats forward so we can sort of track the full metrics of itest. bootstrap_ms = int(os.environ.get("BZL_BOOTSTRAP_MS", 0)) metrics.create_and_register_timer("bzl_bootstrap_ms", interval_ms=bootstrap_ms) except subprocess.CalledProcessError: print( "WARN: Failed to build %s, continuing without self-update." % target, file=sys.stderr, ) # If something goes wrong during rebuild, just run this version. pass
def _build_target(args, bazel_args, mode_args, target): with metrics.create_and_register_timer("bazel_build_ms"): cmd = ([args.bazel_path] + bazel_args + ["build"] + mode_args + ["@dbx_build_tools//build_tools:bzl", SVCCTL_TARGET, target]) if os.environ.get("BZL_BOOTSTRAP_BUILD") == " ".join(cmd[1:]): # If an exact match normalized command was executed as part of bootstrap we can skip # this no-op build. This is fragile, so don't do anything foolhardy like reordering # or tweaking any cmd args above without investigating the self-build code in bzl.py. return if os.environ.get("BZL_DEBUG"): print("exec:", " ".join(cmd), file=sys.stderr) subprocess.check_call(cmd)
def _get_itest_target(bazel_path, target, use_implicit_output=False): """ Turn a target pattern into a ITestTarget object. If `bazel build` was called on the target before this function, then pass use_implicit_output=True to turn on some heuristics to avoid using `bazel query`. """ with metrics.create_and_register_timer("bazel_query_ms"): itest_target = _get_itest_target_body( bazel_path, target, use_implicit_output=use_implicit_output) metrics.set_extra_attributes("target", itest_target.name) if itest_target.has_services: metrics.set_extra_attributes("has_services", "true") else: metrics.set_extra_attributes("has_services", "false") return itest_target
def _cmd_itest_reload(args, bazel_args, mode_args): _raise_on_glob_target(args.target) _build_target(args, bazel_args, mode_args, args.target) itest_target = _get_itest_target(args.bazel_path, args.target, use_implicit_output=True) container_name = _get_container_name_for_target(itest_target.name) _verify_args(args, itest_target, container_should_be_running=True) host_data_dir = os.path.join(HOST_DATA_DIR_PREFIX, container_name) on_host_test_binary = os.path.join(host_data_dir, RUN_TEST_BIN_NAME) in_container_test_binary = os.path.join(IN_CONTAINER_DATA_DIR, RUN_TEST_BIN_NAME) if not os.path.exists(on_host_test_binary): # this means that the container was started from before `bzl itest` started creating # a run-test script # TODO(naphat) remove this after 09/30 message = """The run-test wrapper does not exist for this target, most likely because the container was creating using an old version of `bzl itest`. Please run the following to recreate the container: bzl itest-stop {target} && bzl itest-run {target}""".format( target=itest_target.name) sys.exit(message) test_cmd_str = " ".join( pipes.quote(x) for x in [in_container_test_binary] + args.test_arg) service_restart_cmd_str = "/bin/true" if itest_target.has_services: service_restart_cmd_str = "svcctl auto-restart | tee {}".format( os.path.join(IN_CONTAINER_DATA_DIR, SVCCTL_RESTART_OUTPUT_FILE)) service_version_check_cmd_str = "/bin/true" if itest_target.has_services: service_version_check_cmd_str = "svcctl version-check" docker_exec_args = [args.docker_path, "exec"] if sys.stdin.isatty(): docker_exec_args += ["--interactive", "--tty"] docker_exec_args += [container_name] workspace = bazel_utils.find_workspace() script = """ set -eu set -o pipefail if [[ ! -d {workspace} ]]; then echo 'Your current workspace ({workspace}) is not mounted into the existing `bzl itest` container. If you have multiple checkouts, are you running from the correct checkout? If you want to terminate the current container and start a new one, try running: bzl itest-stop {target} && bzl itest-run {target}' >&2 exit 1 fi if ! {service_version_check_cmd_str} >/dev/null 2>&1; then echo 'ERROR: Service definitions are stale or the service controller has changed. Please run the following to terminate and recreate your container:' >&2 echo '' >&2 echo 'bzl itest-stop {target} && bzl itest-run {target}' >&2 exit 1 fi {service_restart_cmd_str} {test_cmd_str} """.format( workspace=workspace, service_restart_cmd_str=service_restart_cmd_str, service_version_check_cmd_str=service_version_check_cmd_str, target=itest_target.name, test_cmd_str=test_cmd_str, ) with metrics.create_and_register_timer("service_restart_ms"): return_code = subprocess.call(docker_exec_args + ["/bin/bash", "-c", script]) if return_code == 0: if itest_target.has_services: services_restarted = [] with open(os.path.join(host_data_dir, SVCCTL_RESTART_OUTPUT_FILE), "r") as f: for line in f: if line.startswith("restart successful:"): services_restarted.append(line.split()[-1]) services_restarted.sort() metrics.set_extra_attributes("services_restarted", ",".join(services_restarted)) metrics.set_gauge("services_restarted_count", len(services_restarted)) sys.exit(return_code)
def cmd_itest_run(args, bazel_args, mode_args): _raise_on_glob_target(args.target) _build_target(args, bazel_args, mode_args, args.target) itest_target = _get_itest_target(args.bazel_path, args.target, use_implicit_output=True) container_name = _get_container_name_for_target(itest_target.name) _verify_args(args, itest_target, container_should_be_running=False) tmpdir_name = "test_tmpdir" if args.persist_tmpdir: tmpdir_name = "persistent_test_tmpdir" host_data_dir = os.path.join(HOST_DATA_DIR_PREFIX, container_name) host_tmpdir = os.path.join(host_data_dir, tmpdir_name) for dirname in [host_tmpdir, HOST_HOME_DIR]: if not os.path.exists(dirname): os.makedirs(dirname) container_tmpdir = os.path.join(IN_CONTAINER_DATA_DIR, tmpdir_name) workspace = bazel_utils.find_workspace() cwd = workspace # order matters here. The last command shows up as the last thing the user ran, i.e. # the first command they see when they hit "up" history_cmds = [] # type: ignore[var-annotated] if itest_target.has_services: history_cmds = [ "svcctl --help", "svcctl status", 'svcctl status -format "{{.CPUTime}} {{.Name}}" | sort -rgb | head', ] test_bin = os.path.join(host_data_dir, RUN_TEST_BIN_NAME) with open(test_bin, "w") as f: f.write("""#!/bin/bash -eu cd {cwd} exec {test} "$@" """.format( cwd=itest_target.executable_path + ".runfiles/__main__", test=" ".join(itest_target.test_cmd), )) os.chmod(test_bin, 0o755) test_cmd_str = " ".join( pipes.quote(x) for x in [os.path.join(IN_CONTAINER_DATA_DIR, RUN_TEST_BIN_NAME)] + args.test_arg) history_cmds.append(test_cmd_str) launch_cmd = itest_target.service_launch_cmd if args.verbose: launch_cmd += ["--svc.verbose"] default_paths = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".split( ":") itest_paths = [ os.path.join( workspace, os.path.dirname(bazel_utils.executable_for_label(SVCCTL_TARGET))), os.path.join(workspace, "build_tools/itest"), ] env = { "DROPBOX_SERVER_TEST": "1", "PATH": ":".join(itest_paths + default_paths), "TEST_TMPDIR": container_tmpdir, "HOST_TEST_TMPDIR": host_tmpdir, "HOME": IN_CONTAINER_HOME_DIR, # override HOME since we can't readily edit /etc/passwd "LAUNCH_CMD": " ".join(launch_cmd), "TEST_CMD": test_cmd_str, # Set how much directory to clean up on startup. Pass this into the container so it gets # cleaned up as root. "CLEANDIR": os.path.join(container_tmpdir, "logs") if args.persist_tmpdir else container_tmpdir, } history_file = os.path.join(HOST_HOME_DIR, ".bash_history") bash_history.merge_history([history_file], history_cmds, history_file) bashrc_file_src = runfiles.data_path( "@dbx_build_tools//build_tools/bzl_lib/itest/bashrc") if args.build_image: docker_image = args.build_image else: docker_image = os.path.join(args.docker_registry, DEFAULT_IMAGE) init_cmd_args = [ runfiles.data_path( "@dbx_build_tools//build_tools/bzl_lib/itest/bzl-itest-init") ] # Set a fail-safe limit for an itest container to keep it from detonating the whole # machine. RSS limits are a funny thing in docker. Most likely the oom-killer will # start killing things inside the container rendering it unstable. # FIXME(msolo) It would be nice to teardown the container on out-of-memory and leave # some sort of note. mem_limit_kb = _guess_mem_limit_kb() docker_run_args = [ args.docker_path, "run", "--net=host", "--name", container_name, "--workdir", cwd, "--detach", "--memory", "%dK" % mem_limit_kb, # Swap is disabled anyway, so squelch a spurious warning. "--memory-swap", "-1", # Store target name in config, to be able to reload it later. "--label", "itest-target=%s" % args.target, ] if args.privileged: docker_run_args += ["--privileged"] # set env variables. This will also set it for subsequent `docker exec` commands for k, v in env.items(): docker_run_args += ["-e", "{}={}".format(k, v)] with metrics.create_and_register_timer("bazel_info_ms"): with open(os.devnull, "w") as dev_null: output_base = subprocess.check_output( [args.bazel_path, "info", "output_base"], stderr=dev_null).strip() install_base = subprocess.check_output( [args.bazel_path, "info", "install_base"], stderr=dev_null).strip() mounts = [ (os.fsencode(workspace), b"ro"), (b"/sqpkg", b"ro"), (output_base, b"ro"), (install_base, b"ro"), (b"/etc/ssl", b"ro"), (b"/usr/share/ca-certificates", b"ro"), # We bind mount /run/dropbox/sock-drawer/ as read-write so that services outside # itest (ie ULXC jails) can publish sockets here that can be used from the inside # (bind mount), and so that services inside itest (ie RivieraFS) can publish # sockets here (read-write) that can be used from the outside (DEFAULT_SOCKET_DIRECTORY_PATH, b"rw"), ] for path, perms in mounts: # Docker will happily create a mount source that is nonexistent, but it may not have the # right permissions. Better to just mount nothing. if not os.path.exists(path): print("missing mount point:", path, file=sys.stderr) continue src = os.path.realpath(path) docker_run_args += ["-v", b"%s:%s:%s" % (src, path, perms)] # Allow bzl itest containers to observe external changes to the mount table. if os.path.exists("/mnt/sqpkg"): docker_run_args += ["-v", "/mnt/sqpkg:/mnt/sqpkg:rslave"] if sys.stdin.isatty(): # otherwise text wrapping on subsequent shells is messed up docker_run_args += ["--tty"] docker_run_args += [ "-v", "{}:{}:rw".format(host_data_dir, IN_CONTAINER_DATA_DIR) ] docker_run_args += [ "-v", "{}:{}:rw".format(HOST_HOME_DIR, IN_CONTAINER_HOME_DIR) ] docker_run_args += [ "-v", "{}:{}:ro".format(bashrc_file_src, "/etc/bash.bashrc") ] docker_run_args += [docker_image] docker_run_args += init_cmd_args with metrics.create_and_register_timer("services_start_ms"): with open(os.devnull, "w") as f: subprocess.check_call(docker_run_args, stdout=f) docker_exec_args = [args.docker_path, "exec"] if sys.stdin.isatty(): docker_exec_args += ["--interactive", "--tty"] docker_exec_args += [container_name] exit_code = subprocess.call(docker_exec_args + [ runfiles.data_path( "@dbx_build_tools//build_tools/bzl_lib/itest/bzl-itest-wait") ]) if exit_code == 0: # run the test command with metrics.create_and_register_timer("test_ms"): # NOT check_call. Even if this script doesn't exit with 0 (e.g. test fails), # we want to keep going subprocess.call(docker_exec_args + ["/bin/bash", "-c", test_cmd_str]) if itest_target.has_services: services_started = (subprocess.check_output( [ args.docker_path, "exec", container_name, "svcctl", "status", "--all", "--format={{.Name}}", ], universal_newlines=True, ).strip().split("\n")) metrics.set_extra_attributes("services_started", ",".join(services_started)) metrics.set_gauge("services_started_count", len(services_started)) # report metrics now, instead of after the interactive session since # we don't want to measure that metrics.report_metrics() if args.detach: # display message of the day then exit exec_wrapper.execv(args.docker_path, docker_exec_args + ["cat", "/etc/motd"]) else: exit_code = subprocess.call(docker_exec_args + ["/bin/bash"]) with open(os.devnull, "w") as devnull: subprocess.check_call( [args.docker_path, "rm", "-f", container_name], stdout=devnull, stderr=devnull, ) sys.exit(exit_code)
def _verify_args(args, itest_target, container_should_be_running): """ Verify that the command the user requested is valid before continuing. This checks that valid combination of flags are passed, and that a `bzl itest` container already exists (or doesn't), based on the mode requested. """ with metrics.create_and_register_timer("verify_args_ms"): container_name = _get_container_name_for_target(itest_target.name) # Load all containers because you want neither port nor container name conflicts. # note: this shells out to docker so it's potentially slow existing_containers = _get_all_containers(args.docker_path) container_running = container_name in existing_containers error_info = dict(name=container_name, target=itest_target.name) if existing_containers and not container_should_be_running: if not args.allow_multiple_containers: # if the only container running is the container for this target, then we are ok # here and handle the error better below. if not container_running or len(existing_containers) > 1: message = """There are existing docker containers. Please run: Stop all `bzl itest` containers: bzl itest-stop-all""" sys.exit(message) if container_running and not container_should_be_running: message = """Container {name} already exists. Try one of the following: Rebuild and restart any changed services: bzl itest-reload {target} Get a shell into the container: bzl itest-exec {target} /bin/bash Stop and remove this container: bzl itest-stop {target}""".format(**error_info) sys.exit(message) elif not container_running and container_should_be_running: if existing_containers: message = """A `bzl itest` container must already be running for target {target}. Additionally, there are existing `bzl itest` containers. Try running the following to remove all containers and start a new container for {target}: bzl itest-stop-all && bzl itest-run {target}""".format( target=itest_target.name) sys.exit(message) else: message = """A `bzl itest` container must already be running for target {target}. Try running `bzl itest-run {target}` instead.""".format( target=itest_target.name) sys.exit(message) if os.path.exists("/etc/devbox-release"): # on devbox if multiprocessing.cpu_count( ) < 4 and itest_target.name.startswith( ("//services/metaserver", "//paper/services")): sys.exit( """This Devbox is an infra-sized instance not meant for metaserver or Paper development. Please see https://app.dropboxer.net/docs/devbox/devbox_self-serve#recreate_instance-upgrade_instance for upgrade instructions.""" )
def main(ap, self_target): try: workspace = bazel_utils.find_workspace() except bazel_utils.BazelError as e: sys.exit("Bazel Error: {}".format(e)) test_args = None try: # Hedge that we might not need to rebuild and exec. If for any # reason this fails, fall back to correct behavior. stdout, stderr = sys.stdout, sys.stderr with open("/dev/null", "w") as devnull: sys.stdout, sys.stderr = devnull, devnull test_args, unknown_args = ap.parse_known_args() # No built-in Bazel mode requires bzl to be up-to-date. rebuild_and_exec = test_args.mode not in bazel_modes except (SystemExit, AttributeError): rebuild_and_exec = True finally: sys.stdout, sys.stderr = stdout, stderr if os.environ.get("BZL_SKIP_BOOTSTRAP"): rebuild_and_exec = False # Propagate stats forward so we can sort of track the full metrics of itest. bootstrap_ms = int(os.environ.get("BZL_BOOTSTRAP_MS", 0)) metrics.create_and_register_timer("bzl_bootstrap_ms", interval_ms=bootstrap_ms) if rebuild_and_exec: metrics.set_mode("_bzl_bootstrap") # If the tool requires an update, build it and re-exec. Do this before we parse args in # case we have defined a newer mode. targets = [] # Pass in targets that we are going to build. On average this minimizes target flapping # within bazel and saves time on small incremental updates without sacrificing correct # behavior. # do this for some itest modes and if there are no unknown args (as those can be # bazel flags that causes worse build flapping) if (test_args and test_args.mode in ("itest-run", "itest-start", "itest-reload") and not unknown_args): targets.append(itest.SVCCTL_TARGET) targets.append(test_args.target) # also do this for tool modes, so we can avoid an extra bazel build if test_args and test_args.mode in ("tool", "fmt"): targets.append(test_args.target) squelch_output = test_args and test_args.mode in ("tool", "go", "go-env") run_build_tool( os.environ.get("BAZEL_PATH_FOR_BZL_REBUILD", "bazel"), self_target, targets, squelch_output=squelch_output, ) args, remaining_args = ap.parse_known_args() metrics.set_mode(args.mode) subparser_map = ap._subparsers._group_actions[0].choices if remaining_args and (args.mode is None or not getattr( subparser_map[args.mode], "bzl_allow_unknown_args", False)): print( f"ERROR: unknown args for mode {args.mode}: {remaining_args}", file=sys.stderr, ) sys.exit(2) bazel_args, mode_args = parse_bazel_args(remaining_args) if args.mode in (None, "help"): if not mode_args: ap.print_help() print() elif len(mode_args) == 1 and mode_args[0] not in bazel_modes: help_mode_parser = subparser_map[mode_args[0]] help_mode_parser.print_help() sys.stdout.flush() sys.exit(1 if args.mode is None else 0) if args.build_image and not args.build_image.startswith( args.docker_registry): args.build_image = os.path.join(args.docker_registry, args.build_image) try: if hasattr(args, "targets"): targets = args.targets require_build_file = not getattr(args, "missing_build_file_ok", False) targets = bazel_utils.expand_bazel_targets( workspace, targets, require_build_file=require_build_file) if not targets: sys.exit("No targets specified.") args.targets = targets args.func(args, bazel_args, mode_args) except bazel_utils.BazelError as e: if os.environ.get("BZL_DEBUG"): raise sys.exit("ERROR: " + str(e)) except subprocess.CalledProcessError as e: print(e, file=sys.stderr) if e.output: print(e.output, file=sys.stderr) if os.environ.get("BZL_DEBUG"): raise sys.exit(e.returncode) except KeyboardInterrupt: sys.exit("ERROR: interrupted")