crosvm/tools/dev_container
Zhenyu Wang 7d57013ab8 dev_container: Pass proxy setting
If running behind proxy, need to pass proxy setting for container as well,
otherwise can't pull crates properly. So this tries to add 'http_proxy'
and 'https_proxy' in environment for container.

Change-Id: I897703f571d9fe5bd03b819fc6b13a9e04384e6f
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/5232200
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
Commit-Queue: Daniel Verkamp <dverkamp@chromium.org>
2024-01-24 18:36:22 +00:00

375 lines
12 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 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:])