mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2024-11-28 09:33:01 +00:00
988858b66b
Add a new builder to build crosvm in crOS tree, and all the depencies of this new builder. BUG=b:240692674 TESTED=led get-builder luci.crosvm.ci:chromeos_amd64-generic | led edit-cr-cl https://chromium-review.googlesource.com/c/crosvm/crosvm/+/3966928 | led edit-recipe-bundle | led edit -r build_chromeos_hatch | led launch Change-Id: Id2f284139922916edd2dd584f576da9fb3445518 Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/3966928 Reviewed-by: Dennis Kempin <denniskempin@google.com> Commit-Queue: Zihan Chen <zihanchen@google.com>
300 lines
9.6 KiB
Python
Executable file
300 lines
9.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright 2021 The ChromiumOS Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
# Usage:
|
|
#
|
|
# To get an interactive shell for development:
|
|
# ./tools/dev_container
|
|
#
|
|
# To run a command in the container, e.g. to run presubmits:
|
|
# ./tools/dev_container ./tools/presubmit
|
|
#
|
|
# The state of the container (including build artifacts) are preserved between
|
|
# calls. To stop the container call:
|
|
# ./tools/dev_container --stop
|
|
#
|
|
# The dev container can also be called with a fresh container for each call that
|
|
# is cleaned up afterwards (e.g. when run by Kokoro):
|
|
#
|
|
# ./tools/dev_container --hermetic CMD
|
|
#
|
|
# There's an alternative container which can be used to test crosvm in crOS tree.
|
|
# It can be launched with:
|
|
# ./tools/dev_container --cros
|
|
|
|
import argparse
|
|
from argh import arg # type: ignore
|
|
from impl.common import CROSVM_ROOT, cmd, chdir, cros_repo_root, is_cros_repo, quoted, run_main
|
|
from typing import Optional, Tuple, List
|
|
import getpass
|
|
import shutil
|
|
import sys
|
|
import unittest
|
|
import os
|
|
import zlib
|
|
|
|
DEV_CONTAINER_NAME = (
|
|
f"crosvm_dev_{getpass.getuser()}_{zlib.crc32(os.path.realpath(__file__).encode('utf-8')):x}"
|
|
)
|
|
CROS_CONTAINER_NAME = (
|
|
f"crosvm_cros_{getpass.getuser()}_{zlib.crc32(os.path.realpath(__file__).encode('utf-8')):x}"
|
|
)
|
|
|
|
DEV_IMAGE_NAME = "gcr.io/crosvm-infra/crosvm_dev_user"
|
|
CROS_IMAGE_NAME = "gcr.io/crosvm-infra-experimental/crosvm_cros_cloudbuild"
|
|
DEV_IMAGE_VERSION = (CROSVM_ROOT / "tools/impl/dev_container/version").read_text().strip()
|
|
|
|
CACHE_DIR = os.environ.get("CROSVM_CONTAINER_CACHE", None)
|
|
|
|
DOCKER_ARGS = [
|
|
# Share cache dir
|
|
f"--volume {CACHE_DIR}:/cache:rw" if CACHE_DIR else None,
|
|
# Use tmpfs in the container for faster performance.
|
|
"--mount type=tmpfs,destination=/tmp",
|
|
# KVM is required to run a VM for testing.
|
|
"--device /dev/kvm",
|
|
f"--env OUTSIDE_UID={os.getuid()}",
|
|
f"--env OUTSIDE_GID={os.getgid()}",
|
|
]
|
|
|
|
PODMAN_ARGS = [
|
|
# Share cache dir
|
|
f"--volume {CACHE_DIR}:/cache:rw" if CACHE_DIR else None,
|
|
# Use tmpfs in the container for faster performance.
|
|
"--mount type=tmpfs,destination=/tmp",
|
|
# KVM is required to run a VM for testing.
|
|
"--device /dev/kvm",
|
|
]
|
|
|
|
PRIVILEGED_ARGS = [
|
|
# Share devices and syslog
|
|
"--volume /dev/log:/dev/log",
|
|
"--device /dev/net/tun",
|
|
"--device /dev/vhost-net",
|
|
"--device /dev/vhost-vsock",
|
|
# For plugin process jail
|
|
"--mount type=tmpfs,destination=/var/empty",
|
|
]
|
|
|
|
|
|
PODMAN_IS_DEFAULT = shutil.which("docker") == None
|
|
|
|
|
|
def container_name(cros: bool):
|
|
if cros:
|
|
return CROS_CONTAINER_NAME
|
|
else:
|
|
return DEV_CONTAINER_NAME
|
|
|
|
|
|
def container_revision(docker: cmd, container_id: str):
|
|
image = docker("container inspect -f {{.Config.Image}}", container_id).stdout()
|
|
parts = image.split(":")
|
|
assert len(parts) == 2, f"Invalid image name {image}"
|
|
return parts[1]
|
|
|
|
|
|
def container_id(docker: cmd, cros: bool):
|
|
return docker(f"ps -a -q -f name={container_name(cros)}").stdout()
|
|
|
|
|
|
def container_is_running(docker: cmd, cros: bool):
|
|
return bool(docker(f"ps -q -f name={container_name(cros)}").stdout())
|
|
|
|
|
|
def delete_container(docker: cmd, cros: bool):
|
|
cid = container_id(docker, cros)
|
|
if cid:
|
|
print(f"Deleting dev-container {cid}.")
|
|
docker("rm -f", cid).fg(quiet=True)
|
|
return True
|
|
return False
|
|
|
|
|
|
def workspace_mount_args(cros: bool):
|
|
"""
|
|
Returns arguments for mounting the crosvm sources to /workspace.
|
|
|
|
In ChromeOS checkouts the crosvm repo uses a symlink or worktree checkout, which links to a
|
|
different folder in the ChromeOS checkout. So we need to mount the whole CrOS checkout.
|
|
"""
|
|
if cros:
|
|
return ["--workdir /home/crosvmdev/chromiumos/src/platform/crosvm"]
|
|
elif is_cros_repo():
|
|
return [
|
|
f"--volume {quoted(cros_repo_root())}:/workspace:rw",
|
|
"--workdir /workspace/src/platform/crosvm",
|
|
]
|
|
else:
|
|
return [
|
|
f"--volume {quoted(CROSVM_ROOT)}:/workspace:rw",
|
|
]
|
|
|
|
|
|
def ensure_container_is_alive(docker: cmd, docker_args: List[Optional[str]], cros: bool):
|
|
cid = container_id(docker, cros)
|
|
if cid and not container_is_running(docker, cros):
|
|
print("Existing container is not running.")
|
|
delete_container(docker, cros)
|
|
elif cid and not cros and container_revision(docker, cid) != DEV_IMAGE_VERSION:
|
|
print(f"New image is available.")
|
|
delete_container(docker, cros)
|
|
|
|
if not container_is_running(docker, cros):
|
|
# Run neverending sleep to keep container alive while we 'docker exec' commands.
|
|
docker(
|
|
f"run --detach --name {container_name(cros)}",
|
|
*docker_args,
|
|
"sleep infinity",
|
|
).fg(quiet=True)
|
|
cid = container_id(docker, cros)
|
|
print(f"Started container ({cid}).")
|
|
else:
|
|
cid = container_id(docker, cros)
|
|
print(f"Using existing container ({cid}).")
|
|
return cid
|
|
|
|
|
|
@arg("command", nargs=argparse.REMAINDER)
|
|
def main(
|
|
command: Tuple[str, ...],
|
|
stop: bool = False,
|
|
clean: bool = False,
|
|
hermetic: bool = False,
|
|
interactive: bool = False,
|
|
podman: bool = PODMAN_IS_DEFAULT,
|
|
self_test: bool = False,
|
|
pull: bool = False,
|
|
unprivileged: bool = False,
|
|
cros: bool = False,
|
|
):
|
|
chdir(CROSVM_ROOT)
|
|
|
|
if cros and unprivileged:
|
|
print("ERROR: crOS container must be run in privileged mode")
|
|
sys.exit(1)
|
|
|
|
if unprivileged:
|
|
print("WARNING: Running dev_container with --unprivileged is a work in progress.")
|
|
print("Not all tests are expected to pass.")
|
|
print()
|
|
|
|
docker_args = [
|
|
*workspace_mount_args(cros),
|
|
*(PRIVILEGED_ARGS if not unprivileged else []),
|
|
]
|
|
if podman:
|
|
print("WARNING: Running dev_container with podman is experimental.")
|
|
print("It is strongly recommended to use docker.")
|
|
print()
|
|
docker = cmd("podman")
|
|
docker_args += [*PODMAN_ARGS]
|
|
else:
|
|
docker = cmd("docker")
|
|
docker_args += [
|
|
"--privileged" if not unprivileged else None,
|
|
*DOCKER_ARGS,
|
|
]
|
|
|
|
if cros:
|
|
docker_args.append(CROS_IMAGE_NAME)
|
|
else:
|
|
docker_args.append(DEV_IMAGE_NAME + ":" + DEV_IMAGE_VERSION)
|
|
|
|
if self_test:
|
|
TestDevContainer.docker = docker
|
|
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDevContainer)
|
|
unittest.TextTestRunner().run(suite)
|
|
return
|
|
|
|
if stop:
|
|
if not delete_container(docker, cros):
|
|
print(f"container is not running.")
|
|
return
|
|
|
|
if clean:
|
|
delete_container(docker, cros)
|
|
|
|
if pull:
|
|
if cros:
|
|
docker("pull", "gcr.io/crosvm-infra-experimental/crosvm_cros_cloudbuild").fg()
|
|
else:
|
|
docker("pull", f"gcr.io/crosvm-infra/crosvm_dev:{DEV_IMAGE_VERSION}").fg()
|
|
docker("pull", f"gcr.io/crosvm-infra/crosvm_dev_user:{DEV_IMAGE_VERSION}").fg()
|
|
return
|
|
|
|
# If a command is provided run non-interactive unless explicitly asked for.
|
|
tty_args = []
|
|
if not command or interactive:
|
|
if not sys.stdin.isatty():
|
|
raise Exception("Trying to run an interactive session in a non-interactive terminal.")
|
|
tty_args = ["--interactive", "--tty"]
|
|
elif sys.stdin.isatty():
|
|
# Even if run non-interactively, we do want to pass along a tty for proper output.
|
|
tty_args = ["--tty"]
|
|
|
|
# Start an interactive shell by default
|
|
if hermetic:
|
|
# cmd is passed to entrypoint
|
|
quoted_cmd = list(map(quoted, command))
|
|
docker(f"run --rm", *tty_args, *docker_args, *quoted_cmd).fg()
|
|
else:
|
|
# cmd is executed directly
|
|
cid = ensure_container_is_alive(docker, docker_args, cros)
|
|
if podman:
|
|
if not command:
|
|
command = ("/bin/bash",)
|
|
else:
|
|
if not command:
|
|
command = ("/tools/entrypoint.sh",)
|
|
else:
|
|
command = ("/tools/entrypoint.sh",) + tuple(command)
|
|
quoted_cmd = list(map(quoted, command))
|
|
docker("exec", *tty_args, cid, *quoted_cmd).fg()
|
|
|
|
|
|
class TestDevContainer(unittest.TestCase):
|
|
"""
|
|
Runs live tests using the docker service.
|
|
|
|
Note: This test is not run by health-check since it cannot be run inside the
|
|
container. It is run by infra/recipes/health_check.py before running health checks.
|
|
"""
|
|
|
|
docker: cmd
|
|
docker_args = [
|
|
*workspace_mount_args(cros=False),
|
|
*DOCKER_ARGS,
|
|
]
|
|
|
|
def setUp(self):
|
|
# Start with a stopped container for each test.
|
|
delete_container(self.docker, cros=False)
|
|
|
|
def test_stopped_container(self):
|
|
# Create but do not run a new container.
|
|
self.docker(
|
|
f"create --name {DEV_CONTAINER_NAME}", *self.docker_args, "sleep infinity"
|
|
).stdout()
|
|
self.assertTrue(container_id(self.docker, cros=False))
|
|
self.assertFalse(container_is_running(self.docker, cros=False))
|
|
|
|
def test_container_reuse(self):
|
|
cid = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
|
|
cid2 = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
|
|
self.assertEqual(cid, cid2)
|
|
|
|
def test_handling_of_stopped_container(self):
|
|
cid = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
|
|
self.docker("kill", cid).fg()
|
|
|
|
# Make sure we can get back into a good state and execute commands.
|
|
ensure_container_is_alive(self.docker, self.docker_args, cros=False)
|
|
self.assertTrue(container_is_running(self.docker, cros=False))
|
|
main(("true",))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_main(main)
|