123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- #!/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 pathlib import Path
- import shutil
- from impl.util import (
- add_common_args,
- confirm,
- cros_repo_root,
- CROSVM_ROOT,
- is_cros_repo,
- is_kiwi_repo,
- kiwi_repo_root,
- is_aosp_repo,
- aosp_repo_root,
- )
- from impl.command import (
- chdir,
- cmd,
- quoted,
- )
- from typing import Optional, List
- import getpass
- 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"
- CROS_IMAGE_NAME = "gcr.io/crosvm-infra/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)
- COMMON_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" if Path("/dev/kvm").is_char_device() else None,
- # Enable terminal colors
- f"--env TERM={os.environ.get('TERM', 'xterm-256color')}",
- ]
- DOCKER_ARGS = [
- *COMMON_ARGS,
- ]
- PODMAN_ARGS = [
- *COMMON_ARGS,
- # Allow access to group permissions of the user (e.g. for kvm access).
- "--group-add keep-groups" if os.name == "posix" else None,
- # Increase number of PIDs the container can spawn (we run a lot of test processes in parallel)
- "--pids-limit=4096" if os.name == "posix" else None,
- ]
- # Environment variables to pass through to the container if they are specified.
- ENV_PASSTHROUGH = [
- "NEXTEST_PROFILE",
- "http_proxy",
- "https_proxy",
- ]
- def machine_is_running(docker: cmd):
- machine_state = docker("machine info").stdout()
- return "MachineState: Running" in machine_state
- 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",
- ]
- elif is_kiwi_repo():
- return [
- f"--volume {quoted(kiwi_repo_root())}:/workspace:rw",
- # We override /scratch because we run out of memory if we use memory to back the
- # `/scratch` mount point.
- f"--volume {quoted(kiwi_repo_root())}/scratch:/scratch/cargo_target:rw",
- "--workdir /workspace/platform/crosvm",
- ]
- elif is_aosp_repo():
- return [
- f"--volume {quoted(aosp_repo_root())}:/workspace:rw",
- "--workdir /workspace/external/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.
- print(f"Starting container...")
- docker(
- f"run --detach --name {container_name(cros)}",
- *docker_args,
- "sleep infinity",
- ).fg(quiet=False)
- cid = container_id(docker, cros)
- else:
- cid = container_id(docker, cros)
- print(f"Using existing container ({cid}).")
- return cid
- def validate_podman(podman: cmd):
- graph_driver_name = podman("info --format={{.Store.GraphDriverName}}").stdout()
- config_file_name = podman("info --format={{.Store.ConfigFile}}").stdout()
- if graph_driver_name == "vfs":
- print("You are using vfs as a storage driver. This will be extremely slow.")
- print("Using the overlay driver is strongly recommended.")
- print("Note: This will delete all existing podman images and containers.")
- if confirm(f"Do you want me to update your config in {config_file_name}?"):
- podman("system reset -f").fg()
- with open(config_file_name, "a") as config_file:
- print("[storage]", file=config_file)
- print('driver = "overlay"', file=config_file)
- if os.name == "posix":
- username = os.environ["USER"]
- subuids = Path("/etc/subuid").read_text()
- if not username in subuids:
- print("Rootless podman requires subuid's to be set up for your user.")
- usermod = cmd(
- "sudo usermod --add-subuids 900000-965535 --add-subgids 900000-965535", username
- )
- print("I can fix that by running:", usermod)
- if confirm("Ok?"):
- usermod.fg()
- podman("system migrate").fg()
- def main(argv: List[str]):
- parser = argparse.ArgumentParser()
- add_common_args(parser)
- parser.add_argument("--stop", action="store_true")
- parser.add_argument("--clean", action="store_true")
- parser.add_argument("--hermetic", action="store_true")
- parser.add_argument("--no-interactive", action="store_true")
- parser.add_argument("--use-docker", action="store_true")
- parser.add_argument("--self-test", action="store_true")
- parser.add_argument("--pull", action="store_true")
- parser.add_argument("--cros", action="store_true")
- parser.add_argument("command", nargs=argparse.REMAINDER)
- args = parser.parse_args(argv)
- chdir(CROSVM_ROOT)
- if CACHE_DIR:
- Path(CACHE_DIR).mkdir(exist_ok=True)
- has_docker = shutil.which("docker") != None
- has_podman = shutil.which("podman") != None
- if not has_podman and not has_docker:
- raise Exception("Please install podman (or docker) to use the dev container.")
- use_docker = args.use_docker
- if has_docker and not has_podman:
- use_docker = True
- # cros container only works in docker
- if args.cros:
- use_docker = True
- if use_docker:
- print(
- "WARNING: Running dev_container with docker may cause root-owned files to be created."
- )
- print("Use podman to prevent this.")
- print()
- docker = cmd("docker")
- docker_args = [
- *DOCKER_ARGS,
- *workspace_mount_args(args.cros),
- ]
- else:
- docker = cmd("podman")
- # On windows, podman uses wsl vm. start the default podman vm for the rest of the script
- # to work properly.
- if os.name == "nt" and not machine_is_running(docker):
- print("Starting podman default machine.")
- docker("machine start").fg(quiet=True)
- docker_args = [
- *PODMAN_ARGS,
- *workspace_mount_args(args.cros),
- ]
- validate_podman(docker)
- if args.cros:
- docker_args.append("--privileged") # cros container requires privileged container
- docker_args.append(CROS_IMAGE_NAME)
- else:
- docker_args.append(DEV_IMAGE_NAME + ":" + DEV_IMAGE_VERSION)
- # Add environment variables to command line
- exec_args: List[str] = []
- for key in ENV_PASSTHROUGH:
- value = os.environ.get(key)
- if value is not None:
- exec_args.append("--env")
- exec_args.append(f"{key}={quoted(value)}")
- if args.self_test:
- TestDevContainer.docker = docker
- suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDevContainer)
- unittest.TextTestRunner().run(suite)
- return
- if args.stop:
- if not delete_container(docker, args.cros):
- print(f"container is not running.")
- return
- if args.clean:
- delete_container(docker, args.cros)
- if args.pull:
- if args.cros:
- docker("pull", CROS_IMAGE_NAME).fg()
- else:
- docker("pull", f"gcr.io/crosvm-infra/crosvm_dev:{DEV_IMAGE_VERSION}").fg()
- return
- command = args.command
- # Default to interactive mode if a tty is present.
- tty_args: List[str] = []
- if sys.stdin.isatty():
- tty_args += ["--tty"]
- if not args.no_interactive:
- tty_args += ["--interactive"]
- # Start an interactive shell by default
- if args.hermetic:
- # cmd is passed to entrypoint
- quoted_cmd = list(map(quoted, command))
- docker(f"run --rm", *tty_args, *docker_args, *exec_args, *quoted_cmd).fg()
- else:
- # cmd is executed directly
- cid = ensure_container_is_alive(docker, docker_args, args.cros)
- if not command:
- command = ("/bin/bash",)
- quoted_cmd = list(map(quoted, command))
- docker("exec", *tty_args, *exec_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__":
- main(sys.argv[1:])
|