Example #1
0
def subprocess_exec(binary, args, env=None):
    # type: (str, List[str], Optional[Mapping[str, str]]) -> int
    if os.getenv("BZL_DEBUG"):
        print(
            "subprocess_exec: {} {}".format(
                binary, " ".join(pipes.quote(s) for s in args[1:])),
            file=sys.stderr,
        )
    try:
        proc = subprocess.Popen(args, executable=binary, env=env)
        exit_code = proc.wait()
    except KeyboardInterrupt:
        # Wait for child process to die on graceful interrupt.
        exit_code = proc.wait()
    metrics.set_extra_attributes("exit_code", str(exit_code))
    if exit_code != 0:
        metrics.set_error(
            "subprocess_exec_bad_exit",
            "subprocess_exec {} returned non-zero exit code {}".format(
                binary, exit_code),
        )
    sys.exit(exit_code)
Example #2
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
Example #3
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)
Example #4
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)