crosvm/tools/dev_container
Dennis Kempin 1dab58a2cf Update all copyright headers to match new style
This search/replace updates all copyright notices to drop the
"All rights reserved", Use "ChromiumOS" instead of "Chromium OS"
and drops the trailing dots.

This fulfills the request from legal and unifies our notices.

./tools/health-check has been updated to only accept this style.

BUG=b:246579983
TEST=./tools/health-check

Change-Id: I87a80701dc651f1baf4820e5cc42469d7c5f5bf7
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/3894243
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
Commit-Queue: Dennis Kempin <denniskempin@google.com>
2022-09-13 18:41:29 +00:00

225 lines
7.2 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
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
CONTAINER_NAME = (
f"crosvm_dev_{getpass.getuser()}_{zlib.crc32(os.path.realpath(__file__).encode('utf-8')):x}"
)
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,
# Share devices and syslog
"--device /dev/kvm",
"--volume /dev/log:/dev/log",
"--device /dev/net/tun",
"--device /dev/vhost-net",
"--device /dev/vhost-vsock",
# Use tmpfs in the container for faster performance.
"--mount type=tmpfs,destination=/tmp",
# For plugin process jail
"--mount type=tmpfs,destination=/var/empty",
f"--env OUTSIDE_UID={os.getuid()}",
f"--env OUTSIDE_GID={os.getgid()}",
f"gcr.io/crosvm-infra/crosvm_dev:{IMAGE_VERSION}",
]
PODMAN_IS_DEFAULT = shutil.which("docker") == None
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):
return docker(f"ps -a -q -f name={CONTAINER_NAME}").stdout()
def container_is_running(docker: cmd):
return bool(docker(f"ps -q -f name={CONTAINER_NAME}").stdout())
def delete_container(docker: cmd):
cid = container_id(docker)
if cid:
print(f"Deleting dev-container {cid}.")
docker("rm -f", cid).fg(quiet=True)
return True
return False
def workspace_mount_args():
"""
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 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]]):
cid = container_id(docker)
if cid and not container_is_running(docker):
print("Existing dev-container is not running.")
delete_container(docker)
elif cid and container_revision(docker, cid) != IMAGE_VERSION:
print(f"New image is available.")
delete_container(docker)
if not container_is_running(docker):
# Run neverending sleep to keep container alive while we 'docker exec' commands.
docker(f"run --detach --name {CONTAINER_NAME}", *docker_args, "sleep infinity").stdout()
cid = container_id(docker)
print(f"Started dev-container ({cid}).")
else:
cid = container_id(docker)
print(f"Using existing dev-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,
):
chdir(CROSVM_ROOT)
docker = cmd("podman" if podman else "docker")
docker_args = [
# Podman will not share devices when `--privileged` is specified
"--privileged" if not podman else None,
*workspace_mount_args(),
*DOCKER_ARGS,
]
if podman:
print("WARNING: Running dev_container with podman is not fully supported.")
print("Some crosvm tests require privileges podman cannot provide and may fail.")
print()
if self_test:
TestDevContainer.docker = docker
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDevContainer)
unittest.TextTestRunner().run(suite)
return
if stop:
if not delete_container(docker):
print(f"Dev-container is not running.")
return
if clean:
delete_container(docker)
# 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"]
# 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)
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(),
*DOCKER_ARGS,
]
def setUp(self):
# Start with a stopped container for each test.
delete_container(self.docker)
def test_stopped_container(self):
# Create but do not run a new container.
self.docker(f"create --name {CONTAINER_NAME}", *self.docker_args, "sleep infinity").stdout()
self.assertTrue(container_id(self.docker))
self.assertFalse(container_is_running(self.docker))
def test_container_reuse(self):
cid = ensure_container_is_alive(self.docker, self.docker_args)
cid2 = ensure_container_is_alive(self.docker, self.docker_args)
self.assertEqual(cid, cid2)
def test_handling_of_stopped_container(self):
cid = ensure_container_is_alive(self.docker, self.docker_args)
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)
self.assertTrue(container_is_running(self.docker))
main(("true",))
if __name__ == "__main__":
run_main(main)