max / alloy
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+66 insertions,
-5 deletions
| @@ -592,6 +592,18 @@ | |||
| 592 | 592 | .args(["install", "to-disk", "--wipe"]) | |
| 593 | 593 | .arg(disk), | |
| 594 | 594 | ), | |
| 595 | + | // bootc returns when the install is done, not when the kernel and | |
| 596 | + | // udev have caught up with the partition table it wrote. In that | |
| 597 | + | // window `lsblk` answers with a `parttype` of null for every | |
| 598 | + | // partition, so the discovery below finds no root and fails with | |
| 599 | + | // "disk not deployed" about a disk that deployed perfectly. Observed | |
| 600 | + | // against a real bootc 1.16.3 install: null for all three partitions | |
| 601 | + | // immediately after, the correct GUIDs a moment later. | |
| 602 | + | // | |
| 603 | + | // Waiting for the queue to drain is the fix rather than a retry loop: | |
| 604 | + | // there is a specific event to wait for, and a retry would just be | |
| 605 | + | // this wait spelled less precisely. | |
| 606 | + | Stage::Run(Invocation::new("udevadm").arg("settle")), | |
| 595 | 607 | Stage::Run(Invocation::new("mkdir").args(["-p", TARGET_MOUNT])), | |
| 596 | 608 | // First discovery: which partition bootc made the root. | |
| 597 | 609 | Stage::Resolve { | |
| @@ -2081,10 +2093,11 @@ | |||
| 2081 | 2093 | let (view, _log) = at_summary(); | |
| 2082 | 2094 | let shown: Vec<String> = view.plan().iter().map(Stage::display).collect(); | |
| 2083 | 2095 | ||
| 2084 | - | assert_eq!(shown.len(), 3, "{shown:#?}"); | |
| 2096 | + | assert_eq!(shown.len(), 4, "{shown:#?}"); | |
| 2085 | 2097 | assert_eq!(shown[0], "bootc install to-disk --wipe /dev/sda"); | |
| 2086 | - | assert_eq!(shown[1], "mkdir -p /mnt/alloy-target"); | |
| 2087 | - | assert!(shown[2].starts_with("lsblk"), "{}", shown[2]); | |
| 2098 | + | assert_eq!(shown[1], "udevadm settle"); | |
| 2099 | + | assert_eq!(shown[2], "mkdir -p /mnt/alloy-target"); | |
| 2100 | + | assert!(shown[3].starts_with("lsblk"), "{}", shown[3]); | |
| 2088 | 2101 | } | |
| 2089 | 2102 | ||
| 2090 | 2103 | // The summary can only show what is known before anything runs. Everything | |
| @@ -2402,6 +2415,54 @@ | |||
| 2402 | 2415 | assert!(root_partition("not json").unwrap_err().contains("JSON")); | |
| 2403 | 2416 | } | |
| 2404 | 2417 | ||
| 2418 | + | // Verbatim from `lsblk -J -o PATH,PARTTYPE` against a disk bootc 1.16.3 | |
| 2419 | + | // had just installed to, once udev had settled. A hand-written fixture | |
| 2420 | + | // cannot show that the GUID the code matches on is the one bootc sets; | |
| 2421 | + | // this can, and it is the whole reason the discovery exists. | |
| 2422 | + | #[test] | |
| 2423 | + | fn a_real_bootc_layout_yields_its_root_partition() { | |
| 2424 | + | let listing = r#"{"blockdevices":[ | |
| 2425 | + | {"path": "/dev/nbd0", "parttype": null}, | |
| 2426 | + | {"path": "/dev/nbd0p1", "parttype": "21686148-6449-6e6f-744e-656564454649"}, | |
| 2427 | + | {"path": "/dev/nbd0p2", "parttype": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"}, | |
| 2428 | + | {"path": "/dev/nbd0p3", "parttype": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709"} | |
| 2429 | + | ]}"#; | |
| 2430 | + | assert_eq!(root_partition(listing).unwrap(), "/dev/nbd0p3"); | |
| 2431 | + | } | |
| 2432 | + | ||
| 2433 | + | // The same disk a moment earlier, before udev caught up with the table | |
| 2434 | + | // bootc had written. Every parttype reads null, so the discovery reports | |
| 2435 | + | // a disk that was in fact deployed as undeployed. install_plan waits on | |
| 2436 | + | // `udevadm settle` to keep this listing from ever reaching the parser; | |
| 2437 | + | // pinned here so the reason that stage exists survives it. | |
| 2438 | + | #[test] | |
| 2439 | + | fn an_unsettled_listing_is_why_the_plan_waits_for_udev() { | |
| 2440 | + | let listing = r#"{"blockdevices":[ | |
| 2441 | + | {"path": "/dev/nbd0", "parttype": null}, | |
| 2442 | + | {"path": "/dev/nbd0p1", "parttype": null}, | |
| 2443 | + | {"path": "/dev/nbd0p2", "parttype": null}, | |
| 2444 | + | {"path": "/dev/nbd0p3", "parttype": null} | |
| 2445 | + | ]}"#; | |
| 2446 | + | assert!(root_partition(listing).is_err()); | |
| 2447 | + | } | |
| 2448 | + | ||
| 2449 | + | // The stage ordering the race turns on: nothing may look at the | |
| 2450 | + | // partition table between bootc writing it and udev settling. | |
| 2451 | + | #[test] | |
| 2452 | + | fn the_plan_settles_udev_before_it_reads_the_partition_table() { | |
| 2453 | + | let plan = install_plan("/dev/sda", "host", "user", "pw"); | |
| 2454 | + | let lines: Vec<String> = plan.iter().map(Stage::display).collect(); | |
| 2455 | + | let settle = lines | |
| 2456 | + | .iter() | |
| 2457 | + | .position(|line| line.contains("udevadm")) | |
| 2458 | + | .expect("the plan waits for udev"); | |
| 2459 | + | let install = lines | |
| 2460 | + | .iter() | |
| 2461 | + | .position(|line| line.contains("install to-disk")) | |
| 2462 | + | .expect("the plan installs"); | |
| 2463 | + | assert!(install < settle, "settle must follow the install: {lines:?}"); | |
| 2464 | + | } | |
| 2465 | + | ||
| 2405 | 2466 | // Missing answers cannot happen from the summary — every step gates on its | |
| 2406 | 2467 | // own validation — but a partial plan would render a command with a hole in | |
| 2407 | 2468 | // it, so it returns nothing instead. | |
| @@ -2424,8 +2485,8 @@ | |||
| 2424 | 2485 | ||
| 2425 | 2486 | let sequence = view.running.as_ref().expect("nothing was queued"); | |
| 2426 | 2487 | assert!(!sequence.is_done()); | |
| 2427 | - | // Three up front; the rest appear as the discoveries resolve. | |
| 2428 | - | assert_eq!(sequence.progress(), (0, 3)); | |
| 2488 | + | // Four up front; the rest appear as the discoveries resolve. | |
| 2489 | + | assert_eq!(sequence.progress(), (0, 4)); | |
| 2429 | 2490 | assert!(sequence.output().is_empty(), "a command ran during confirm"); | |
| 2430 | 2491 | } | |
| 2431 | 2492 |