1
0

dev_container 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. #!/usr/bin/env python3
  2. # Copyright 2021 The ChromiumOS Authors
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. # Usage:
  6. #
  7. # To get an interactive shell for development:
  8. # ./tools/dev_container
  9. #
  10. # To run a command in the container, e.g. to run presubmits:
  11. # ./tools/dev_container ./tools/presubmit
  12. #
  13. # The state of the container (including build artifacts) are preserved between
  14. # calls. To stop the container call:
  15. # ./tools/dev_container --stop
  16. #
  17. # The dev container can also be called with a fresh container for each call that
  18. # is cleaned up afterwards (e.g. when run by Kokoro):
  19. #
  20. # ./tools/dev_container --hermetic CMD
  21. #
  22. # There's an alternative container which can be used to test crosvm in crOS tree.
  23. # It can be launched with:
  24. # ./tools/dev_container --cros
  25. import argparse
  26. from pathlib import Path
  27. import shutil
  28. from impl.util import (
  29. add_common_args,
  30. confirm,
  31. cros_repo_root,
  32. CROSVM_ROOT,
  33. is_cros_repo,
  34. is_kiwi_repo,
  35. kiwi_repo_root,
  36. is_aosp_repo,
  37. aosp_repo_root,
  38. )
  39. from impl.command import (
  40. chdir,
  41. cmd,
  42. quoted,
  43. )
  44. from typing import Optional, List
  45. import getpass
  46. import sys
  47. import unittest
  48. import os
  49. import zlib
  50. DEV_CONTAINER_NAME = (
  51. f"crosvm_dev_{getpass.getuser()}_{zlib.crc32(os.path.realpath(__file__).encode('utf-8')):x}"
  52. )
  53. CROS_CONTAINER_NAME = (
  54. f"crosvm_cros_{getpass.getuser()}_{zlib.crc32(os.path.realpath(__file__).encode('utf-8')):x}"
  55. )
  56. DEV_IMAGE_NAME = "gcr.io/crosvm-infra/crosvm_dev"
  57. CROS_IMAGE_NAME = "gcr.io/crosvm-infra/crosvm_cros_cloudbuild"
  58. DEV_IMAGE_VERSION = (CROSVM_ROOT / "tools/impl/dev_container/version").read_text().strip()
  59. CACHE_DIR = os.environ.get("CROSVM_CONTAINER_CACHE", None)
  60. COMMON_ARGS = [
  61. # Share cache dir
  62. f"--volume {CACHE_DIR}:/cache:rw" if CACHE_DIR else None,
  63. # Use tmpfs in the container for faster performance.
  64. "--mount type=tmpfs,destination=/tmp",
  65. # KVM is required to run a VM for testing.
  66. "--device /dev/kvm" if Path("/dev/kvm").is_char_device() else None,
  67. # Enable terminal colors
  68. f"--env TERM={os.environ.get('TERM', 'xterm-256color')}",
  69. ]
  70. DOCKER_ARGS = [
  71. *COMMON_ARGS,
  72. ]
  73. PODMAN_ARGS = [
  74. *COMMON_ARGS,
  75. # Allow access to group permissions of the user (e.g. for kvm access).
  76. "--group-add keep-groups" if os.name == "posix" else None,
  77. # Increase number of PIDs the container can spawn (we run a lot of test processes in parallel)
  78. "--pids-limit=4096" if os.name == "posix" else None,
  79. ]
  80. # Environment variables to pass through to the container if they are specified.
  81. ENV_PASSTHROUGH = [
  82. "NEXTEST_PROFILE",
  83. "http_proxy",
  84. "https_proxy",
  85. ]
  86. def machine_is_running(docker: cmd):
  87. machine_state = docker("machine info").stdout()
  88. return "MachineState: Running" in machine_state
  89. def container_name(cros: bool):
  90. if cros:
  91. return CROS_CONTAINER_NAME
  92. else:
  93. return DEV_CONTAINER_NAME
  94. def container_revision(docker: cmd, container_id: str):
  95. image = docker("container inspect -f {{.Config.Image}}", container_id).stdout()
  96. parts = image.split(":")
  97. assert len(parts) == 2, f"Invalid image name {image}"
  98. return parts[1]
  99. def container_id(docker: cmd, cros: bool):
  100. return docker(f"ps -a -q -f name={container_name(cros)}").stdout()
  101. def container_is_running(docker: cmd, cros: bool):
  102. return bool(docker(f"ps -q -f name={container_name(cros)}").stdout())
  103. def delete_container(docker: cmd, cros: bool):
  104. cid = container_id(docker, cros)
  105. if cid:
  106. print(f"Deleting dev-container {cid}.")
  107. docker("rm -f", cid).fg(quiet=True)
  108. return True
  109. return False
  110. def workspace_mount_args(cros: bool):
  111. """
  112. Returns arguments for mounting the crosvm sources to /workspace.
  113. In ChromeOS checkouts the crosvm repo uses a symlink or worktree checkout, which links to a
  114. different folder in the ChromeOS checkout. So we need to mount the whole CrOS checkout.
  115. """
  116. if cros:
  117. return ["--workdir /home/crosvmdev/chromiumos/src/platform/crosvm"]
  118. elif is_cros_repo():
  119. return [
  120. f"--volume {quoted(cros_repo_root())}:/workspace:rw",
  121. "--workdir /workspace/src/platform/crosvm",
  122. ]
  123. elif is_kiwi_repo():
  124. return [
  125. f"--volume {quoted(kiwi_repo_root())}:/workspace:rw",
  126. # We override /scratch because we run out of memory if we use memory to back the
  127. # `/scratch` mount point.
  128. f"--volume {quoted(kiwi_repo_root())}/scratch:/scratch/cargo_target:rw",
  129. "--workdir /workspace/platform/crosvm",
  130. ]
  131. elif is_aosp_repo():
  132. return [
  133. f"--volume {quoted(aosp_repo_root())}:/workspace:rw",
  134. "--workdir /workspace/external/crosvm",
  135. ]
  136. else:
  137. return [
  138. f"--volume {quoted(CROSVM_ROOT)}:/workspace:rw",
  139. ]
  140. def ensure_container_is_alive(docker: cmd, docker_args: List[Optional[str]], cros: bool):
  141. cid = container_id(docker, cros)
  142. if cid and not container_is_running(docker, cros):
  143. print("Existing container is not running.")
  144. delete_container(docker, cros)
  145. elif cid and not cros and container_revision(docker, cid) != DEV_IMAGE_VERSION:
  146. print(f"New image is available.")
  147. delete_container(docker, cros)
  148. if not container_is_running(docker, cros):
  149. # Run neverending sleep to keep container alive while we 'docker exec' commands.
  150. print(f"Starting container...")
  151. docker(
  152. f"run --detach --name {container_name(cros)}",
  153. *docker_args,
  154. "sleep infinity",
  155. ).fg(quiet=False)
  156. cid = container_id(docker, cros)
  157. else:
  158. cid = container_id(docker, cros)
  159. print(f"Using existing container ({cid}).")
  160. return cid
  161. def validate_podman(podman: cmd):
  162. graph_driver_name = podman("info --format={{.Store.GraphDriverName}}").stdout()
  163. config_file_name = podman("info --format={{.Store.ConfigFile}}").stdout()
  164. if graph_driver_name == "vfs":
  165. print("You are using vfs as a storage driver. This will be extremely slow.")
  166. print("Using the overlay driver is strongly recommended.")
  167. print("Note: This will delete all existing podman images and containers.")
  168. if confirm(f"Do you want me to update your config in {config_file_name}?"):
  169. podman("system reset -f").fg()
  170. with open(config_file_name, "a") as config_file:
  171. print("[storage]", file=config_file)
  172. print('driver = "overlay"', file=config_file)
  173. if os.name == "posix":
  174. username = os.environ["USER"]
  175. subuids = Path("/etc/subuid").read_text()
  176. if not username in subuids:
  177. print("Rootless podman requires subuid's to be set up for your user.")
  178. usermod = cmd(
  179. "sudo usermod --add-subuids 900000-965535 --add-subgids 900000-965535", username
  180. )
  181. print("I can fix that by running:", usermod)
  182. if confirm("Ok?"):
  183. usermod.fg()
  184. podman("system migrate").fg()
  185. def main(argv: List[str]):
  186. parser = argparse.ArgumentParser()
  187. add_common_args(parser)
  188. parser.add_argument("--stop", action="store_true")
  189. parser.add_argument("--clean", action="store_true")
  190. parser.add_argument("--hermetic", action="store_true")
  191. parser.add_argument("--no-interactive", action="store_true")
  192. parser.add_argument("--use-docker", action="store_true")
  193. parser.add_argument("--self-test", action="store_true")
  194. parser.add_argument("--pull", action="store_true")
  195. parser.add_argument("--cros", action="store_true")
  196. parser.add_argument("command", nargs=argparse.REMAINDER)
  197. args = parser.parse_args(argv)
  198. chdir(CROSVM_ROOT)
  199. if CACHE_DIR:
  200. Path(CACHE_DIR).mkdir(exist_ok=True)
  201. has_docker = shutil.which("docker") != None
  202. has_podman = shutil.which("podman") != None
  203. if not has_podman and not has_docker:
  204. raise Exception("Please install podman (or docker) to use the dev container.")
  205. use_docker = args.use_docker
  206. if has_docker and not has_podman:
  207. use_docker = True
  208. # cros container only works in docker
  209. if args.cros:
  210. use_docker = True
  211. if use_docker:
  212. print(
  213. "WARNING: Running dev_container with docker may cause root-owned files to be created."
  214. )
  215. print("Use podman to prevent this.")
  216. print()
  217. docker = cmd("docker")
  218. docker_args = [
  219. *DOCKER_ARGS,
  220. *workspace_mount_args(args.cros),
  221. ]
  222. else:
  223. docker = cmd("podman")
  224. # On windows, podman uses wsl vm. start the default podman vm for the rest of the script
  225. # to work properly.
  226. if os.name == "nt" and not machine_is_running(docker):
  227. print("Starting podman default machine.")
  228. docker("machine start").fg(quiet=True)
  229. docker_args = [
  230. *PODMAN_ARGS,
  231. *workspace_mount_args(args.cros),
  232. ]
  233. validate_podman(docker)
  234. if args.cros:
  235. docker_args.append("--privileged") # cros container requires privileged container
  236. docker_args.append(CROS_IMAGE_NAME)
  237. else:
  238. docker_args.append(DEV_IMAGE_NAME + ":" + DEV_IMAGE_VERSION)
  239. # Add environment variables to command line
  240. exec_args: List[str] = []
  241. for key in ENV_PASSTHROUGH:
  242. value = os.environ.get(key)
  243. if value is not None:
  244. exec_args.append("--env")
  245. exec_args.append(f"{key}={quoted(value)}")
  246. if args.self_test:
  247. TestDevContainer.docker = docker
  248. suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDevContainer)
  249. unittest.TextTestRunner().run(suite)
  250. return
  251. if args.stop:
  252. if not delete_container(docker, args.cros):
  253. print(f"container is not running.")
  254. return
  255. if args.clean:
  256. delete_container(docker, args.cros)
  257. if args.pull:
  258. if args.cros:
  259. docker("pull", CROS_IMAGE_NAME).fg()
  260. else:
  261. docker("pull", f"gcr.io/crosvm-infra/crosvm_dev:{DEV_IMAGE_VERSION}").fg()
  262. return
  263. command = args.command
  264. # Default to interactive mode if a tty is present.
  265. tty_args: List[str] = []
  266. if sys.stdin.isatty():
  267. tty_args += ["--tty"]
  268. if not args.no_interactive:
  269. tty_args += ["--interactive"]
  270. # Start an interactive shell by default
  271. if args.hermetic:
  272. # cmd is passed to entrypoint
  273. quoted_cmd = list(map(quoted, command))
  274. docker(f"run --rm", *tty_args, *docker_args, *exec_args, *quoted_cmd).fg()
  275. else:
  276. # cmd is executed directly
  277. cid = ensure_container_is_alive(docker, docker_args, args.cros)
  278. if not command:
  279. command = ("/bin/bash",)
  280. quoted_cmd = list(map(quoted, command))
  281. docker("exec", *tty_args, *exec_args, cid, *quoted_cmd).fg()
  282. class TestDevContainer(unittest.TestCase):
  283. """
  284. Runs live tests using the docker service.
  285. Note: This test is not run by health-check since it cannot be run inside the
  286. container. It is run by infra/recipes/health_check.py before running health checks.
  287. """
  288. docker: cmd
  289. docker_args = [
  290. *workspace_mount_args(cros=False),
  291. *DOCKER_ARGS,
  292. ]
  293. def setUp(self):
  294. # Start with a stopped container for each test.
  295. delete_container(self.docker, cros=False)
  296. def test_stopped_container(self):
  297. # Create but do not run a new container.
  298. self.docker(
  299. f"create --name {DEV_CONTAINER_NAME}", *self.docker_args, "sleep infinity"
  300. ).stdout()
  301. self.assertTrue(container_id(self.docker, cros=False))
  302. self.assertFalse(container_is_running(self.docker, cros=False))
  303. def test_container_reuse(self):
  304. cid = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
  305. cid2 = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
  306. self.assertEqual(cid, cid2)
  307. def test_handling_of_stopped_container(self):
  308. cid = ensure_container_is_alive(self.docker, self.docker_args, cros=False)
  309. self.docker("kill", cid).fg()
  310. # Make sure we can get back into a good state and execute commands.
  311. ensure_container_is_alive(self.docker, self.docker_args, cros=False)
  312. self.assertTrue(container_is_running(self.docker, cros=False))
  313. main(["true"])
  314. if __name__ == "__main__":
  315. main(sys.argv[1:])