run.rs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. use std::{
  2. env::consts::{ARCH, OS},
  3. ffi::OsString,
  4. fmt::Write as _,
  5. fs::{copy, create_dir_all, OpenOptions},
  6. io::{BufRead as _, BufReader, ErrorKind, Write as _},
  7. path::{Path, PathBuf},
  8. process::{Child, ChildStdin, Command, Output, Stdio},
  9. sync::{Arc, Mutex},
  10. thread,
  11. };
  12. use anyhow::{anyhow, bail, Context as _, Result};
  13. use cargo_metadata::{Artifact, CompilerMessage, Message, Target};
  14. use clap::Parser;
  15. use xtask::{exec, Errors, AYA_BUILD_INTEGRATION_BPF};
  16. #[derive(Parser)]
  17. enum Environment {
  18. /// Runs the integration tests locally.
  19. Local {
  20. /// The command used to wrap your application.
  21. #[clap(short, long, default_value = "sudo -E")]
  22. runner: String,
  23. },
  24. /// Runs the integration tests in a VM.
  25. VM {
  26. /// The kernel images to use.
  27. ///
  28. /// You can download some images with:
  29. ///
  30. /// wget --accept-regex '.*/linux-image-[0-9\.-]+-cloud-.*-unsigned*' \
  31. /// --recursive ftp://ftp.us.debian.org/debian/pool/main/l/linux/
  32. ///
  33. /// You can then extract them with:
  34. ///
  35. /// find . -name '*.deb' -print0 \
  36. /// | xargs -0 -I {} sh -c "dpkg --fsys-tarfile {} \
  37. /// | tar --wildcards --extract '*vmlinuz*' --file -"
  38. #[clap(required = true)]
  39. kernel_image: Vec<PathBuf>,
  40. },
  41. }
  42. #[derive(Parser)]
  43. pub struct Options {
  44. #[clap(subcommand)]
  45. environment: Environment,
  46. /// Arguments to pass to your application.
  47. #[clap(global = true, last = true)]
  48. run_args: Vec<OsString>,
  49. }
  50. pub fn build<F>(target: Option<&str>, f: F) -> Result<Vec<(String, PathBuf)>>
  51. where
  52. F: FnOnce(&mut Command) -> &mut Command,
  53. {
  54. // Always use rust-lld and -Zbuild-std in case we're cross-compiling.
  55. let mut cmd = Command::new("cargo");
  56. cmd.args(["build", "--message-format=json"]);
  57. if let Some(target) = target {
  58. let config = format!("target.{target}.linker = \"rust-lld\"");
  59. cmd.args(["--target", target, "--config", &config]);
  60. }
  61. f(&mut cmd);
  62. let mut child = cmd
  63. .stdout(Stdio::piped())
  64. .spawn()
  65. .with_context(|| format!("failed to spawn {cmd:?}"))?;
  66. let Child { stdout, .. } = &mut child;
  67. let stdout = stdout.take().unwrap();
  68. let stdout = BufReader::new(stdout);
  69. let mut executables = Vec::new();
  70. for message in Message::parse_stream(stdout) {
  71. #[allow(clippy::collapsible_match)]
  72. match message.context("valid JSON")? {
  73. Message::CompilerArtifact(Artifact {
  74. executable,
  75. target: Target { name, .. },
  76. ..
  77. }) => {
  78. if let Some(executable) = executable {
  79. executables.push((name, executable.into()));
  80. }
  81. }
  82. Message::CompilerMessage(CompilerMessage { message, .. }) => {
  83. for line in message.rendered.unwrap_or_default().split('\n') {
  84. println!("cargo:warning={line}");
  85. }
  86. }
  87. Message::TextLine(line) => {
  88. println!("{line}");
  89. }
  90. _ => {}
  91. }
  92. }
  93. let status = child
  94. .wait()
  95. .with_context(|| format!("failed to wait for {cmd:?}"))?;
  96. if status.code() != Some(0) {
  97. bail!("{cmd:?} failed: {status:?}")
  98. }
  99. Ok(executables)
  100. }
  101. /// Build and run the project.
  102. pub fn run(opts: Options) -> Result<()> {
  103. let Options {
  104. environment,
  105. run_args,
  106. } = opts;
  107. type Binary = (String, PathBuf);
  108. fn binaries(target: Option<&str>) -> Result<Vec<(&str, Vec<Binary>)>> {
  109. ["dev", "release"]
  110. .into_iter()
  111. .map(|profile| {
  112. let binaries = build(target, |cmd| {
  113. cmd.env(AYA_BUILD_INTEGRATION_BPF, "true").args([
  114. "--package",
  115. "integration-test",
  116. "--tests",
  117. "--profile",
  118. profile,
  119. ])
  120. })?;
  121. anyhow::Ok((profile, binaries))
  122. })
  123. .collect()
  124. }
  125. // Use --test-threads=1 to prevent tests from interacting with shared
  126. // kernel state due to the lack of inter-test isolation.
  127. let default_args = [OsString::from("--test-threads=1")];
  128. let run_args = default_args.iter().chain(run_args.iter());
  129. match environment {
  130. Environment::Local { runner } => {
  131. let mut args = runner.trim().split_terminator(' ');
  132. let runner = args.next().ok_or(anyhow!("no first argument"))?;
  133. let args = args.collect::<Vec<_>>();
  134. let binaries = binaries(None)?;
  135. let mut failures = String::new();
  136. for (profile, binaries) in binaries {
  137. for (name, binary) in binaries {
  138. let mut cmd = Command::new(runner);
  139. let cmd = cmd.args(args.iter()).arg(binary).args(run_args.clone());
  140. println!("{profile}:{name} running {cmd:?}");
  141. let status = cmd
  142. .status()
  143. .with_context(|| format!("failed to run {cmd:?}"))?;
  144. if status.code() != Some(0) {
  145. writeln!(&mut failures, "{profile}:{name} failed: {status:?}")
  146. .context("String write failed")?
  147. }
  148. }
  149. }
  150. if failures.is_empty() {
  151. Ok(())
  152. } else {
  153. Err(anyhow!("failures:\n{}", failures))
  154. }
  155. }
  156. Environment::VM { kernel_image } => {
  157. // The user has asked us to run the tests on a VM. This is involved; strap in.
  158. //
  159. // We need tools to build the initramfs; we use gen_init_cpio from the Linux repository,
  160. // taking care to cache it.
  161. //
  162. // Then we iterate the kernel images, using the `file` program to guess the target
  163. // architecture. We then build the init program and our test binaries for that
  164. // architecture, and use gen_init_cpio to build an initramfs containing the test
  165. // binaries. We're almost ready to run the VM.
  166. //
  167. // We consult our OS, our architecture, and the target architecture to determine if
  168. // hardware acceleration is available, and then start QEMU with the provided kernel
  169. // image and the initramfs we built.
  170. //
  171. // We consume the output of QEMU, looking for the output of our init program. This is
  172. // the only way to distinguish success from failure. We batch up the errors across all
  173. // VM images and report to the user. The end.
  174. let cache_dir = Path::new("test/.tmp");
  175. create_dir_all(cache_dir).context("failed to create cache dir")?;
  176. let gen_init_cpio = cache_dir.join("gen_init_cpio");
  177. if !gen_init_cpio
  178. .try_exists()
  179. .context("failed to check existence of gen_init_cpio")?
  180. {
  181. let mut curl = Command::new("curl");
  182. curl.args([
  183. "-sfSL",
  184. "https://raw.githubusercontent.com/torvalds/linux/master/usr/gen_init_cpio.c",
  185. ]);
  186. let mut curl_child = curl
  187. .stdout(Stdio::piped())
  188. .spawn()
  189. .with_context(|| format!("failed to spawn {curl:?}"))?;
  190. let Child { stdout, .. } = &mut curl_child;
  191. let curl_stdout = stdout.take().unwrap();
  192. let mut clang = Command::new("clang");
  193. let clang = exec(
  194. clang
  195. .args(["-g", "-O2", "-x", "c", "-", "-o"])
  196. .arg(&gen_init_cpio)
  197. .stdin(curl_stdout),
  198. );
  199. let output = curl_child
  200. .wait_with_output()
  201. .with_context(|| format!("failed to wait for {curl:?}"))?;
  202. let Output { status, .. } = &output;
  203. if status.code() != Some(0) {
  204. bail!("{curl:?} failed: {output:?}")
  205. }
  206. // Check the result of clang *after* checking curl; in case the download failed,
  207. // only curl's output will be useful.
  208. clang?;
  209. }
  210. let mut errors = Vec::new();
  211. for kernel_image in kernel_image {
  212. // Guess the guest architecture.
  213. let mut cmd = Command::new("file");
  214. let output = cmd
  215. .arg("--brief")
  216. .arg(&kernel_image)
  217. .output()
  218. .with_context(|| format!("failed to run {cmd:?}"))?;
  219. let Output { status, .. } = &output;
  220. if status.code() != Some(0) {
  221. bail!("{cmd:?} failed: {output:?}")
  222. }
  223. let Output { stdout, .. } = output;
  224. // Now parse the output of the file command, which looks something like
  225. //
  226. // - Linux kernel ARM64 boot executable Image, little-endian, 4K pages
  227. //
  228. // - Linux kernel x86 boot executable bzImage, version 6.1.0-10-cloud-amd64 [..]
  229. let stdout = String::from_utf8(stdout)
  230. .with_context(|| format!("invalid UTF-8 in {cmd:?} stdout"))?;
  231. let (_, stdout) = stdout
  232. .split_once("Linux kernel")
  233. .ok_or_else(|| anyhow!("failed to parse {cmd:?} stdout: {stdout}"))?;
  234. let (guest_arch, _) = stdout
  235. .split_once("boot executable")
  236. .ok_or_else(|| anyhow!("failed to parse {cmd:?} stdout: {stdout}"))?;
  237. let guest_arch = guest_arch.trim();
  238. let (guest_arch, machine, cpu) = match guest_arch {
  239. "ARM64" => ("aarch64", Some("virt"), Some("cortex-a57")),
  240. "x86" => ("x86_64", Some("q35"), Some("qemu64")),
  241. guest_arch => (guest_arch, None, None),
  242. };
  243. let target = format!("{guest_arch}-unknown-linux-musl");
  244. // Build our init program. The contract is that it will run anything it finds in /bin.
  245. let init = build(Some(&target), |cmd| {
  246. cmd.args(["--package", "init", "--profile", "release"])
  247. })
  248. .context("building init program failed")?;
  249. let init = match &*init {
  250. [(name, init)] => {
  251. if name != "init" {
  252. bail!("expected init program to be named init, found {name}")
  253. }
  254. init
  255. }
  256. init => bail!("expected exactly one init program, found {init:?}"),
  257. };
  258. let binaries = binaries(Some(&target))?;
  259. let tmp_dir = tempfile::tempdir().context("tempdir failed")?;
  260. let initrd_image = tmp_dir.path().join("qemu-initramfs.img");
  261. let initrd_image_file = OpenOptions::new()
  262. .create_new(true)
  263. .write(true)
  264. .open(&initrd_image)
  265. .with_context(|| {
  266. format!("failed to create {} for writing", initrd_image.display())
  267. })?;
  268. let mut gen_init_cpio = Command::new(&gen_init_cpio);
  269. let mut gen_init_cpio_child = gen_init_cpio
  270. .arg("-")
  271. .stdin(Stdio::piped())
  272. .stdout(initrd_image_file)
  273. .spawn()
  274. .with_context(|| format!("failed to spawn {gen_init_cpio:?}"))?;
  275. let Child { stdin, .. } = &mut gen_init_cpio_child;
  276. let mut stdin = stdin.take().unwrap();
  277. use std::os::unix::ffi::OsStrExt as _;
  278. // Send input into gen_init_cpio which looks something like
  279. //
  280. // file /init path-to-init 0755 0 0
  281. // dir /bin 0755 0 0
  282. // file /bin/foo path-to-foo 0755 0 0
  283. // file /bin/bar path-to-bar 0755 0 0
  284. for bytes in [
  285. "file /init ".as_bytes(),
  286. init.as_os_str().as_bytes(),
  287. " 0755 0 0\n".as_bytes(),
  288. "dir /bin 0755 0 0\n".as_bytes(),
  289. ] {
  290. stdin.write_all(bytes).expect("write");
  291. }
  292. for (profile, binaries) in binaries {
  293. for (name, binary) in binaries {
  294. let name = format!("{}-{}", profile, name);
  295. let path = tmp_dir.path().join(&name);
  296. copy(&binary, &path).with_context(|| {
  297. format!("copy({}, {}) failed", binary.display(), path.display())
  298. })?;
  299. for bytes in [
  300. "file /bin/".as_bytes(),
  301. name.as_bytes(),
  302. " ".as_bytes(),
  303. path.as_os_str().as_bytes(),
  304. " 0755 0 0\n".as_bytes(),
  305. ] {
  306. stdin.write_all(bytes).expect("write");
  307. }
  308. }
  309. }
  310. // Must explicitly close to signal EOF.
  311. drop(stdin);
  312. let output = gen_init_cpio_child
  313. .wait_with_output()
  314. .with_context(|| format!("failed to wait for {gen_init_cpio:?}"))?;
  315. let Output { status, .. } = &output;
  316. if status.code() != Some(0) {
  317. bail!("{gen_init_cpio:?} failed: {output:?}")
  318. }
  319. let mut qemu = Command::new(format!("qemu-system-{guest_arch}"));
  320. if let Some(machine) = machine {
  321. qemu.args(["-machine", machine]);
  322. }
  323. if guest_arch == ARCH {
  324. match OS {
  325. "linux" => {
  326. const KVM: &str = "/dev/kvm";
  327. match OpenOptions::new().read(true).write(true).open(KVM) {
  328. Ok(_file) => {
  329. qemu.args(["-accel", "kvm"]);
  330. }
  331. Err(error) => match error.kind() {
  332. ErrorKind::NotFound | ErrorKind::PermissionDenied => {}
  333. _kind => {
  334. return Err(error)
  335. .with_context(|| format!("failed to open {KVM}"));
  336. }
  337. },
  338. }
  339. }
  340. "macos" => {
  341. qemu.args(["-accel", "hvf"]);
  342. }
  343. os => bail!("unsupported OS: {os}"),
  344. }
  345. } else if let Some(cpu) = cpu {
  346. qemu.args(["-cpu", cpu]);
  347. }
  348. let console = OsString::from("ttyS0");
  349. let mut kernel_args = std::iter::once(("console", &console))
  350. .chain(run_args.clone().map(|run_arg| ("init.arg", run_arg)))
  351. .enumerate()
  352. .fold(OsString::new(), |mut acc, (i, (k, v))| {
  353. if i != 0 {
  354. acc.push(" ");
  355. }
  356. acc.push(k);
  357. acc.push("=");
  358. acc.push(v);
  359. acc
  360. });
  361. // We sometimes see kernel panics containing:
  362. //
  363. // [ 0.064000] Kernel panic - not syncing: IO-APIC + timer doesn't work! Boot with apic=debug and send a report. Then try booting with the 'noapic' option.
  364. //
  365. // Heed the advice and boot with noapic. We don't know why this happens.
  366. kernel_args.push(" noapic");
  367. qemu.args(["-no-reboot", "-nographic", "-m", "512M", "-smp", "2"])
  368. .arg("-append")
  369. .arg(kernel_args)
  370. .arg("-kernel")
  371. .arg(&kernel_image)
  372. .arg("-initrd")
  373. .arg(&initrd_image);
  374. if guest_arch == "aarch64" {
  375. match OS {
  376. "linux" => {
  377. let mut cmd = Command::new("locate");
  378. let output = cmd
  379. .arg("QEMU_EFI.fd")
  380. .output()
  381. .with_context(|| format!("failed to run {cmd:?}"))?;
  382. let Output { status, .. } = &output;
  383. if status.code() != Some(0) {
  384. bail!("{qemu:?} failed: {output:?}")
  385. }
  386. let Output { stdout, .. } = output;
  387. let bios = String::from_utf8(stdout)
  388. .with_context(|| format!("failed to parse output of {cmd:?}"))?;
  389. qemu.args(["-bios", bios.trim()]);
  390. }
  391. "macos" => {
  392. let mut cmd = Command::new("brew");
  393. let output = cmd
  394. .args(["list", "qemu", "-1", "-v"])
  395. .output()
  396. .with_context(|| format!("failed to run {cmd:?}"))?;
  397. let Output { status, .. } = &output;
  398. if status.code() != Some(0) {
  399. bail!("{qemu:?} failed: {output:?}")
  400. }
  401. let Output { stdout, .. } = output;
  402. let output = String::from_utf8(stdout)
  403. .with_context(|| format!("failed to parse output of {cmd:?}"))?;
  404. const NAME: &str = "edk2-aarch64-code.fd";
  405. let bios = output.lines().find(|line| line.contains(NAME)).ok_or_else(
  406. || anyhow!("failed to find {NAME} in output of {cmd:?}: {output}"),
  407. )?;
  408. qemu.args(["-bios", bios.trim()]);
  409. }
  410. os => bail!("unsupported OS: {os}"),
  411. };
  412. }
  413. let mut qemu_child = qemu
  414. .stdin(Stdio::piped())
  415. .stdout(Stdio::piped())
  416. .stderr(Stdio::piped())
  417. .spawn()
  418. .with_context(|| format!("failed to spawn {qemu:?}"))?;
  419. let Child {
  420. stdin,
  421. stdout,
  422. stderr,
  423. ..
  424. } = &mut qemu_child;
  425. let stdin = stdin.take().unwrap();
  426. let stdin = Arc::new(Mutex::new(stdin));
  427. let stdout = stdout.take().unwrap();
  428. let stdout = BufReader::new(stdout);
  429. let stderr = stderr.take().unwrap();
  430. let stderr = BufReader::new(stderr);
  431. const TERMINATE_AFTER_COUNT: &[(&str, usize)] = &[
  432. ("end Kernel panic", 0),
  433. ("rcu: RCU grace-period kthread stack dump:", 0),
  434. ("watchdog: BUG: soft lockup", 1),
  435. ];
  436. let mut counts = [0; TERMINATE_AFTER_COUNT.len()];
  437. let mut terminate_if_kernel_hang =
  438. move |line: &str, stdin: &Arc<Mutex<ChildStdin>>| -> anyhow::Result<()> {
  439. if let Some(i) = TERMINATE_AFTER_COUNT
  440. .iter()
  441. .position(|(marker, _)| line.contains(marker))
  442. {
  443. counts[i] += 1;
  444. let (marker, max) = TERMINATE_AFTER_COUNT[i];
  445. if counts[i] > max {
  446. println!("{marker} detected > {max} times; terminating QEMU");
  447. let mut stdin = stdin.lock().unwrap();
  448. stdin
  449. .write_all(&[0x01, b'x'])
  450. .context("failed to write to stdin")?;
  451. println!("waiting for QEMU to terminate");
  452. }
  453. }
  454. Ok(())
  455. };
  456. let stderr = {
  457. let stdin = stdin.clone();
  458. thread::Builder::new()
  459. .spawn(move || {
  460. for line in stderr.lines() {
  461. let line = line.context("failed to read line from stderr")?;
  462. eprintln!("{}", line);
  463. terminate_if_kernel_hang(&line, &stdin)?;
  464. }
  465. anyhow::Ok(())
  466. })
  467. .unwrap()
  468. };
  469. let mut outcome = None;
  470. for line in stdout.lines() {
  471. let line = line.context("failed to read line from stdout")?;
  472. println!("{}", line);
  473. terminate_if_kernel_hang(&line, &stdin)?;
  474. // The init program will print "init: success" or "init: failure" to indicate
  475. // the outcome of running the binaries it found in /bin.
  476. if let Some(line) = line.strip_prefix("init: ") {
  477. let previous = match line {
  478. "success" => outcome.replace(Ok(())),
  479. "failure" => outcome.replace(Err(())),
  480. line => bail!("unexpected init output: {}", line),
  481. };
  482. if let Some(previous) = previous {
  483. bail!("multiple exit status: previous={previous:?}, current={line}");
  484. }
  485. }
  486. }
  487. let output = qemu_child
  488. .wait_with_output()
  489. .with_context(|| format!("failed to wait for {qemu:?}"))?;
  490. let Output { status, .. } = &output;
  491. if status.code() != Some(0) {
  492. bail!("{qemu:?} failed: {output:?}")
  493. }
  494. stderr.join().unwrap()?;
  495. let outcome = outcome.ok_or(anyhow!("init did not exit"))?;
  496. match outcome {
  497. Ok(()) => {}
  498. Err(()) => {
  499. errors.push(anyhow!("VM binaries failed on {}", kernel_image.display()))
  500. }
  501. }
  502. }
  503. if errors.is_empty() {
  504. Ok(())
  505. } else {
  506. Err(Errors::new(errors).into())
  507. }
  508. }
  509. }
  510. }