Пример #1
0
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
Пример #2
0
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)
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
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."""
                )
Пример #7
0
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")