max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+263 insertions,
-94 deletions
| @@ -1107,6 +1107,27 @@ | |||
| 1107 | 1107 | /// resolvers, where the partition path finally exists. This one is what the | |
| 1108 | 1108 | /// caller hands over, and `Option` is what carries the user's answer: `None` is | |
| 1109 | 1109 | /// an unencrypted install, which is why nothing downstream needs a bool. | |
| 1110 | + | /// Everything the work after the deploy needs to know. | |
| 1111 | + | /// | |
| 1112 | + | /// One struct because [`install_plan`] and [`recover_plan`] need exactly the | |
| 1113 | + | /// same answers: the recovery re-runs the install's own configuration, so a | |
| 1114 | + | /// value either of them could do without is a value neither of them has. | |
| 1115 | + | /// Passing them separately meant two argument lists to keep in step, and the | |
| 1116 | + | /// one that drifted would drift silently. | |
| 1117 | + | /// | |
| 1118 | + | /// Not [`Answers`], which is the wizard's record of what was typed and holds | |
| 1119 | + | /// no password: this is the plan's view of it, borrowed for the moment the plan | |
| 1120 | + | /// is built, with the secrets that deliberately live nowhere else. See | |
| 1121 | + | /// [`Secret`]. | |
| 1122 | + | struct Choices<'a> { | |
| 1123 | + | hostname: &'a str, | |
| 1124 | + | username: &'a str, | |
| 1125 | + | password: &'a str, | |
| 1126 | + | pubkey: Option<&'a str>, | |
| 1127 | + | locate_timezone: bool, | |
| 1128 | + | encryption: Option<&'a EncryptionChoice>, | |
| 1129 | + | } | |
| 1130 | + | ||
| 1110 | 1131 | #[derive(Debug, Clone)] | |
| 1111 | 1132 | struct EncryptionChoice { | |
| 1112 | 1133 | passphrase: String, | |
| @@ -1192,21 +1213,43 @@ | |||
| 1192 | 1213 | /// Nothing recoverable is destroyed. The target is a disk the user consented to | |
| 1193 | 1214 | /// erasing before the install began, and the install had already erased it. | |
| 1194 | 1215 | fn wipe_signatures(target: &str) -> Invocation { | |
| 1195 | - | Invocation::new("wipefs").arg("--all").arg(target) | |
| 1216 | + | Invocation::new("wipefs") | |
| 1217 | + | .arg("--all") | |
| 1218 | + | .arg("--force") | |
| 1219 | + | .arg(target) | |
| 1196 | 1220 | } | |
| 1197 | 1221 | ||
| 1198 | - | /// Say what is about to be destroyed, then destroy it. | |
| 1222 | + | /// Say what is about to be destroyed, tear down what holds it, then destroy it. | |
| 1199 | 1223 | /// | |
| 1200 | 1224 | /// The line matters as much as the command. A user watching the run screen sees | |
| 1201 | 1225 | /// their install fail and then sees a disk being wiped, and without the reason | |
| 1202 | 1226 | /// between them the second reads as the installer compounding the first. | |
| 1203 | - | fn wreck_plan(target: &str, why: &str) -> Vec<Stage> { | |
| 1204 | - | vec![ | |
| 1205 | - | Stage::Run(Invocation::new("echo").arg(format!( | |
| 1206 | - | "{why}. Clearing {target} so this disk cannot be mistaken for an install" | |
| 1207 | - | ))), | |
| 1208 | - | Stage::Run(wipe_signatures(target)), | |
| 1209 | - | ] | |
| 1227 | + | /// | |
| 1228 | + | /// The teardown is not tidiness. `wipefs` opens the device exclusively, so it | |
| 1229 | + | /// refuses a mounted filesystem or a LUKS container whose mapper is open, and | |
| 1230 | + | /// the recovery reaches here having just mounted the target to look at it. | |
| 1231 | + | /// Measured: `wipefs --all` against a container with its mapper open fails with | |
| 1232 | + | /// "probing initialization failed: Device or resource busy". Without this the | |
| 1233 | + | /// wreck would fail in exactly the case it exists for. | |
| 1234 | + | /// | |
| 1235 | + | /// `--force` behind the teardown rather than instead of it, for whatever else | |
| 1236 | + | /// on a live system has picked the device up. This disk is already condemned; | |
| 1237 | + | /// what must not happen is it surviving as something that looks installable. | |
| 1238 | + | fn wreck_plan(partition: &str, device: Option<&str>, why: &str) -> Vec<Stage> { | |
| 1239 | + | let mut stages = vec![Stage::Run(Invocation::new("echo").arg(format!( | |
| 1240 | + | "{why}. Clearing {partition} so this disk cannot be mistaken for an install" | |
| 1241 | + | )))]; | |
| 1242 | + | ||
| 1243 | + | // Attempts: on the dead-end path nothing may be mounted or open yet, and | |
| 1244 | + | // "it was not mounted" is not a reason to leave the trap in place. | |
| 1245 | + | stages.push(Stage::Attempt(unmount(TARGET_MOUNT))); | |
| 1246 | + | if let Some(device) = device.filter(|device| *device != partition) { | |
| 1247 | + | stages.push(Stage::Attempt( | |
| 1248 | + | Invocation::new("cryptsetup").arg("close").arg(device), | |
| 1249 | + | )); | |
| 1250 | + | } | |
| 1251 | + | stages.push(Stage::Run(wipe_signatures(partition))); | |
| 1252 | + | stages | |
| 1210 | 1253 | } | |
| 1211 | 1254 | ||
| 1212 | 1255 | /// Whether the header in `dump` has a keyslot that is not the TPM's. | |
| @@ -1376,6 +1419,16 @@ | |||
| 1376 | 1419 | .collect() | |
| 1377 | 1420 | } | |
| 1378 | 1421 | ||
| 1422 | + | /// What the wreck would have to clear, carried alongside the checks. | |
| 1423 | + | /// | |
| 1424 | + | /// Two values because clearing a LUKS container means closing the mapper | |
| 1425 | + | /// opened inside it first, and only the device knows that mapper's name. | |
| 1426 | + | #[derive(Clone)] | |
| 1427 | + | struct Wreckable { | |
| 1428 | + | partition: String, | |
| 1429 | + | device: String, | |
| 1430 | + | } | |
| 1431 | + | ||
| 1379 | 1432 | /// The same checks as the recovery runs them: a false answer wrecks the disk. | |
| 1380 | 1433 | /// | |
| 1381 | 1434 | /// Each check decides whether the next one is asked, so the first fact that is | |
| @@ -1386,7 +1439,7 @@ | |||
| 1386 | 1439 | /// because a check's verdict is not knowable until its command has run. | |
| 1387 | 1440 | fn verify_or_wreck( | |
| 1388 | 1441 | mut checks: std::collections::VecDeque<Check>, | |
| 1389 | - | target: String, | |
| 1442 | + | target: Wreckable, | |
| 1390 | 1443 | intact: Arc<AtomicBool>, | |
| 1391 | 1444 | ) -> Vec<Stage> { | |
| 1392 | 1445 | let Some(Check { | |
| @@ -1401,7 +1454,11 @@ | |||
| 1401 | 1454 | invocation, | |
| 1402 | 1455 | then: Box::new(move |output| { | |
| 1403 | 1456 | if let Err(message) = verdict(output) { | |
| 1404 | - | return Ok(wreck_plan(&target, &message)); | |
| 1457 | + | return Ok(wreck_plan( | |
| 1458 | + | &target.partition, | |
| 1459 | + | Some(&target.device), | |
| 1460 | + | &message, | |
| 1461 | + | )); | |
| 1405 | 1462 | } | |
| 1406 | 1463 | if checks.is_empty() { | |
| 1407 | 1464 | intact.store(true, Ordering::Relaxed); | |
| @@ -1479,6 +1536,32 @@ | |||
| 1479 | 1536 | } | |
| 1480 | 1537 | } | |
| 1481 | 1538 | ||
| 1539 | + | /// The same stages, with nothing in them able to decide the run. | |
| 1540 | + | /// | |
| 1541 | + | /// For the recovery's repair phase. Re-running the configuration against a disk | |
| 1542 | + | /// that is already in an unknown state will step on things: a command may fail | |
| 1543 | + | /// because the work was already done, or because the state it needed is exactly | |
| 1544 | + | /// what went missing. Neither is an answer about the disk, and the checks that | |
| 1545 | + | /// follow are. | |
| 1546 | + | /// | |
| 1547 | + | /// So a [`Stage::Run`] becomes a [`Stage::Attempt`], and a | |
| 1548 | + | /// [`Stage::Resolve`] that cannot make sense of its output contributes nothing | |
| 1549 | + | /// instead of failing the run. The transformation follows the stages a resolver | |
| 1550 | + | /// produces, because those are configuration too. | |
| 1551 | + | fn best_effort(stages: Vec<Stage>) -> Vec<Stage> { | |
| 1552 | + | stages | |
| 1553 | + | .into_iter() | |
| 1554 | + | .map(|stage| match stage { | |
| 1555 | + | Stage::Run(invocation) | Stage::Attempt(invocation) => Stage::Attempt(invocation), | |
| 1556 | + | Stage::Commit(invocation) => Stage::Commit(invocation), | |
| 1557 | + | Stage::Resolve { invocation, then } => Stage::Resolve { | |
| 1558 | + | invocation, | |
| 1559 | + | then: Box::new(move |output| Ok(best_effort(then(output).unwrap_or_default()))), | |
| 1560 | + | }, | |
| 1561 | + | }) | |
| 1562 | + | .collect() | |
| 1563 | + | } | |
| 1564 | + | ||
| 1482 | 1565 | /// What is owed to the disk when the install fails past its commit point. | |
| 1483 | 1566 | /// | |
| 1484 | 1567 | /// Before the commit point a failure owes nothing: the machine is as it was | |
| @@ -1490,10 +1573,15 @@ | |||
| 1490 | 1573 | /// matter. | |
| 1491 | 1574 | /// | |
| 1492 | 1575 | /// **Repair forward.** Find the disk again and mount it, through | |
| 1493 | - | /// [`deployed_disk_plan`], the same chain the install used, and then enroll the | |
| 1494 | - | /// keyslots bootc wipes. The repair runs as [`Stage::Attempt`]s: one that fails | |
| 1495 | - | /// must not stop the checks, because what decides this is the state of the disk | |
| 1496 | - | /// and not the exit status of a retry. | |
| 1576 | + | /// [`deployed_disk_plan`], and re-run [`configure_plan`] against it: the whole | |
| 1577 | + | /// of the install's post-deploy work, done a second time. A failure at the last | |
| 1578 | + | /// stage of an otherwise good install is then repaired by redoing that stage, | |
| 1579 | + | /// rather than by condemning the machine over it. | |
| 1580 | + | /// | |
| 1581 | + | /// The retry is [`best_effort`], so nothing in it can decide the outcome. That | |
| 1582 | + | /// is not laxity: a command here can fail because its work was already done, or | |
| 1583 | + | /// because the state it needed is exactly what went missing, and neither is an | |
| 1584 | + | /// answer about the disk. | |
| 1497 | 1585 | /// | |
| 1498 | 1586 | /// **Ask whether it is a machine.** [`acceptance_checks`], the same four facts | |
| 1499 | 1587 | /// the install asserts at its end. Using the install's own definition is the | |
| @@ -1508,52 +1596,67 @@ | |||
| 1508 | 1596 | /// cannot even be shown to hold a deployment, it certainly cannot be shown to | |
| 1509 | 1597 | /// hold a machine, and the whole disk is cleared rather than a partition that | |
| 1510 | 1598 | /// was never identified. | |
| 1511 | - | fn recover_plan( | |
| 1512 | - | disk: &str, | |
| 1513 | - | username: &str, | |
| 1514 | - | encryption: Option<&EncryptionChoice>, | |
| 1515 | - | intact: &Arc<AtomicBool>, | |
| 1516 | - | ) -> Vec<Stage> { | |
| 1517 | - | let username = username.to_string(); | |
| 1518 | - | let choice = encryption.cloned(); | |
| 1599 | + | fn recover_plan(disk: &str, choices: &Choices<'_>, intact: &Arc<AtomicBool>) -> Vec<Stage> { | |
| 1600 | + | let hostname = choices.hostname.to_string(); | |
| 1601 | + | let username = choices.username.to_string(); | |
| 1602 | + | let password = choices.password.to_string(); | |
| 1603 | + | let pubkey = choices.pubkey.map(str::to_string); | |
| 1604 | + | let locate_timezone = choices.locate_timezone; | |
| 1605 | + | let panel = crate::display::detect_panel(); | |
| 1606 | + | let choice = choices.encryption.cloned(); | |
| 1519 | 1607 | let intact = Arc::clone(intact); | |
| 1520 | 1608 | ||
| 1521 | 1609 | // Before the root partition is known there is nothing narrower to clear. | |
| 1522 | 1610 | let whole_disk = disk.to_string(); | |
| 1523 | - | let dead_end: DeadEnd = std::rc::Rc::new(move |message| wreck_plan(&whole_disk, &message)); | |
| 1611 | + | let dead_end: DeadEnd = | |
| 1612 | + | std::rc::Rc::new(move |message| wreck_plan(&whole_disk, None, &message)); | |
| 1524 | 1613 | ||
| 1525 | 1614 | deployed_disk_plan( | |
| 1526 | 1615 | disk, | |
| 1527 | 1616 | Some(dead_end), | |
| 1528 | - | Box::new(move |partition, root| { | |
| 1529 | - | let mut stages = Vec::new(); | |
| 1530 | - | ||
| 1531 | - | // The one repair that does not depend on anything the checks are | |
| 1532 | - | // about to test, and the one whose absence is irreversible: a | |
| 1533 | - | // volume with no slot but the TPM's cannot be given one later by | |
| 1534 | - | // anybody, where an account can be added from any live medium. | |
| 1535 | - | if let Some(choice) = &choice { | |
| 1536 | - | stages.extend( | |
| 1537 | - | enroll_plan(partition, &choice.passphrase, &choice.recovery) | |
| 1538 | - | .into_iter() | |
| 1539 | - | .map(|stage| match stage { | |
| 1540 | - | Stage::Run(invocation) => Stage::Attempt(invocation), | |
| 1541 | - | other => other, | |
| 1542 | - | }), | |
| 1543 | - | ); | |
| 1544 | - | } | |
| 1545 | - | ||
| 1617 | + | Box::new(move |partition, device, root| { | |
| 1546 | 1618 | let encryption = choice.as_ref().map(|choice| Encryption { | |
| 1547 | 1619 | partition, | |
| 1548 | 1620 | passphrase: &choice.passphrase, | |
| 1549 | 1621 | recovery: &choice.recovery, | |
| 1550 | 1622 | }); | |
| 1623 | + | ||
| 1624 | + | // Everything the install does after the deploy, run again against | |
| 1625 | + | // the disk as it now stands. A failure at the last stage of an | |
| 1626 | + | // otherwise good install is repaired by redoing the last stage, | |
| 1627 | + | // rather than by condemning the machine over it. | |
| 1628 | + | // | |
| 1629 | + | // Best-effort throughout: none of it may decide the outcome. The | |
| 1630 | + | // checks below do that, by looking. | |
| 1631 | + | let mut stages = best_effort( | |
| 1632 | + | configure_plan( | |
| 1633 | + | &hostname, | |
| 1634 | + | &username, | |
| 1635 | + | &password, | |
| 1636 | + | &HomeSeeds { | |
| 1637 | + | pubkey: pubkey.as_deref(), | |
| 1638 | + | panel: panel.as_ref(), | |
| 1639 | + | }, | |
| 1640 | + | locate_timezone, | |
| 1641 | + | encryption.as_ref(), | |
| 1642 | + | root, | |
| 1643 | + | ) | |
| 1644 | + | .unwrap_or_default(), | |
| 1645 | + | ); | |
| 1646 | + | ||
| 1551 | 1647 | stages.extend(verify_or_wreck( | |
| 1552 | 1648 | acceptance_checks(&username, encryption.as_ref(), root).into(), | |
| 1553 | - | partition.to_string(), | |
| 1649 | + | Wreckable { | |
| 1650 | + | partition: partition.to_string(), | |
| 1651 | + | device: device.to_string(), | |
| 1652 | + | }, | |
| 1554 | 1653 | intact, | |
| 1555 | 1654 | )); | |
| 1556 | 1655 | ||
| 1656 | + | // Tolerated: the wreck unmounts for itself, so on that path this is | |
| 1657 | + | // a second umount of nothing. | |
| 1658 | + | stages.push(Stage::Attempt(unmount(TARGET_MOUNT))); | |
| 1659 | + | ||
| 1557 | 1660 | Ok(stages) | |
| 1558 | 1661 | }), | |
| 1559 | 1662 | ) | |
| @@ -1608,6 +1711,10 @@ | |||
| 1608 | 1711 | let group_root = root.to_string(); | |
| 1609 | 1712 | let group_file = format!("{root}/etc/group"); | |
| 1610 | 1713 | ||
| 1714 | + | let useradd_root = root.to_string(); | |
| 1715 | + | let useradd_user = username.to_string(); | |
| 1716 | + | let useradd_home = installed_home.clone(); | |
| 1717 | + | ||
| 1611 | 1718 | let mut stages = vec![ | |
| 1612 | 1719 | // Before anything is written: a shell the target will not hand out | |
| 1613 | 1720 | // makes an account nobody can log into, and useradd will not catch it. | |
| @@ -1692,14 +1799,31 @@ | |||
| 1692 | 1799 | // their install reads it as a failure. Left alone rather than | |
| 1693 | 1800 | // papered over: a spool nothing on the image delivers to would be | |
| 1694 | 1801 | // invented purely to quiet a warning. | |
| 1695 | - | Stage::Run( | |
| 1696 | - | in_target(root, "useradd") | |
| 1697 | - | .arg("--no-create-home") | |
| 1698 | - | .args(["--home-dir", &installed_home]) | |
| 1699 | - | .args(["--shell", LOGIN_SHELL]) | |
| 1700 | - | .args(["--groups", "wheel,video"]) | |
| 1701 | - | .arg(username), | |
| 1702 | - | ), | |
| 1802 | + | Stage::Resolve { | |
| 1803 | + | invocation: Invocation::new("cat").arg(format!("{useradd_root}/etc/passwd")), | |
| 1804 | + | then: Box::new(move |listing| { | |
| 1805 | + | // useradd exits 9 against an account that already exists, so | |
| 1806 | + | // an unguarded call is a stage that can only run once. The | |
| 1807 | + | // recovery re-runs this whole plan against a disk that may | |
| 1808 | + | // already carry the account, and a stage that fails because | |
| 1809 | + | // its work was already done tells nobody anything. | |
| 1810 | + | // | |
| 1811 | + | // Asked of the target's own passwd file rather than of getent, | |
| 1812 | + | // for the reason the id lookup below states: getent answers | |
| 1813 | + | // about the machine doing the installing. | |
| 1814 | + | if passwd_ids(listing, &useradd_user).is_ok() { | |
| 1815 | + | return Ok(Vec::new()); | |
| 1816 | + | } | |
| 1817 | + | Ok(vec![Stage::Run( | |
| 1818 | + | in_target(&useradd_root, "useradd") | |
| 1819 | + | .arg("--no-create-home") | |
| 1820 | + | .args(["--home-dir", &useradd_home]) | |
| 1821 | + | .args(["--shell", LOGIN_SHELL]) | |
| 1822 | + | .args(["--groups", "wheel,video"]) | |
| 1823 | + | .arg(&useradd_user), | |
| 1824 | + | )]) | |
| 1825 | + | }), | |
| 1826 | + | }, | |
| 1703 | 1827 | // -p because /var/home does not exist yet either. | |
| 1704 | 1828 | Stage::Run(Invocation::new("mkdir").args(["-p", &home])), | |
| 1705 | 1829 | // The dotfiles --create-home would have copied. Without them an | |
| @@ -1833,29 +1957,6 @@ | |||
| 1833 | 1957 | )); | |
| 1834 | 1958 | } | |
| 1835 | 1959 | ||
| 1836 | - | stages.extend(acceptance_plan(username, encryption, root)); | |
| 1837 | - | ||
| 1838 | - | stages.extend([ | |
| 1839 | - | // Upstream: "optional, but recommended to run as the penultimate step | |
| 1840 | - | // before unmounting the target filesystem. This command will perform | |
| 1841 | - | // some basic sanity checks and may also perform fixups on the target | |
| 1842 | - | // root." `--help` confirms the shape: `finalize <ROOT_PATH>`, the | |
| 1843 | - | // mounted root filesystem, which is what is passed. | |
| 1844 | - | // | |
| 1845 | - | // It bills itself as a step of an `install to-filesystem` flow, and | |
| 1846 | - | // `to-disk` already finalizes and unmounts by itself. Against a real | |
| 1847 | - | // to-disk install this exits 0 without output, so it is kept as a | |
| 1848 | - | // cheap sanity check rather than for any fixup it is known to make. | |
| 1849 | - | Stage::Run( | |
| 1850 | - | Invocation::new("bootc") | |
| 1851 | - | .args(["install", "finalize"]) | |
| 1852 | - | .arg(TARGET_MOUNT), | |
| 1853 | - | ), | |
| 1854 | - | // Leaving the target mounted would strand the filesystem dirty across | |
| 1855 | - | // the reboot the user is about to perform. | |
| 1856 | - | Stage::Run(Invocation::new("umount").arg(TARGET_MOUNT)), | |
| 1857 | - | ]); | |
| 1858 | - | ||
| 1859 | 1960 | Ok(stages) | |
| 1860 | 1961 | } | |
| 1861 | 1962 | ||
| @@ -2055,10 +2156,16 @@ | |||
| 2055 | 2156 | ||
| 2056 | 2157 | /// What to do once the disk has been found to hold a deployment. | |
| 2057 | 2158 | /// | |
| 2058 | - | /// Takes the root partition and the deployment directory, the two values that | |
| 2059 | - | /// cannot be known before the deploy has run. The install configures; the | |
| 2060 | - | /// recovery checks. | |
| 2061 | - | type Deployed = Box<dyn FnOnce(&str, &str) -> Result<Vec<Stage>, String>>; | |
| 2159 | + | /// Takes the root partition, the device the filesystem is actually on, and the | |
| 2160 | + | /// deployment directory: the values that cannot be known before the deploy has | |
| 2161 | + | /// run. The install configures; the recovery checks. | |
| 2162 | + | /// | |
| 2163 | + | /// The partition and the device differ exactly when the volume is encrypted, | |
| 2164 | + | /// where the partition is the LUKS container and the device is the mapper | |
| 2165 | + | /// opened inside it. Both are needed: the header being enrolled into is on the | |
| 2166 | + | /// partition, and the mapper is what has to be closed before the partition can | |
| 2167 | + | /// be cleared. | |
| 2168 | + | type Deployed = Box<dyn FnOnce(&str, &str, &str) -> Result<Vec<Stage>, String>>; | |
| 2062 | 2169 | ||
| 2063 | 2170 | /// What to do when the chain cannot go on. | |
| 2064 | 2171 | /// | |
| @@ -2132,6 +2239,7 @@ | |||
| 2132 | 2239 | return or_dead_end(Err(message), contents_end.as_ref()); | |
| 2133 | 2240 | } | |
| 2134 | 2241 | }; | |
| 2242 | + | let opened = device.clone(); | |
| 2135 | 2243 | Ok(vec![ | |
| 2136 | 2244 | // Release whatever bootc left mounted on the device | |
| 2137 | 2245 | // before mounting it. Its own leftover is read-only, | |
| @@ -2160,6 +2268,7 @@ | |||
| 2160 | 2268 | Stage::Resolve { | |
| 2161 | 2269 | invocation: mount_options(TARGET_MOUNT), | |
| 2162 | 2270 | then: Box::new(move |options| { | |
| 2271 | + | let device = opened; | |
| 2163 | 2272 | if let Err(message) = writable_mount(options) { | |
| 2164 | 2273 | return or_dead_end(Err(message), mount_end.as_ref()); | |
| 2165 | 2274 | } | |
| @@ -2178,7 +2287,7 @@ | |||
| 2178 | 2287 | ); | |
| 2179 | 2288 | } | |
| 2180 | 2289 | or_dead_end( | |
| 2181 | - | then(&partition, deployment), | |
| 2290 | + | then(&partition, &device, deployment), | |
| 2182 | 2291 | deployment_end.as_ref(), | |
| 2183 | 2292 | ) | |
| 2184 | 2293 | }), | |
| @@ -2229,19 +2338,13 @@ | |||
| 2229 | 2338 | /// This is the discipline the mount check already stated for itself, applied to | |
| 2230 | 2339 | /// the stage where being wrong is most expensive. `mount` warns and exits 0 when | |
| 2231 | 2340 | /// it falls back to read-only, so its success was never evidence either. | |
| 2232 | - | fn install_plan( | |
| 2233 | - | disk: &str, | |
| 2234 | - | hostname: &str, | |
| 2235 | - | username: &str, | |
| 2236 | - | password: &str, | |
| 2237 | - | pubkey: Option<&str>, | |
| 2238 | - | locate_timezone: bool, | |
| 2239 | - | encryption: Option<EncryptionChoice>, | |
| 2240 | - | ) -> Vec<Stage> { | |
| 2241 | - | let hostname = hostname.to_string(); | |
| 2242 | - | let username = username.to_string(); | |
| 2243 | - | let password = password.to_string(); | |
| 2244 | - | let pubkey = pubkey.map(str::to_string); | |
| 2341 | + | fn install_plan(disk: &str, choices: &Choices<'_>) -> Vec<Stage> { | |
| 2342 | + | let hostname = choices.hostname.to_string(); | |
| 2343 | + | let username = choices.username.to_string(); | |
| 2344 | + | let password = choices.password.to_string(); | |
| 2345 | + | let pubkey = choices.pubkey.map(str::to_string); | |
| 2346 | + | let locate_timezone = choices.locate_timezone; | |
| 2347 | + | let encryption = choices.encryption.cloned(); | |
| 2245 | 2348 | let encrypt = encryption.is_some(); | |
| 2246 | 2349 | // Read here, once, on the machine being installed. The panel does not | |
| 2247 | 2350 | // change while the install runs, and reading it before the disk is touched | |
| @@ -2307,7 +2410,7 @@ | |||
| 2307 | 2410 | stages.extend(deployed_disk_plan( | |
| 2308 | 2411 | disk, | |
| 2309 | 2412 | None, | |
| 2310 | - | Box::new(move |partition, deployment| { | |
| 2413 | + | Box::new(move |partition, _device, deployment| { | |
| 2311 | 2414 | // The container, not the mapper device: the header being enrolled | |
| 2312 | 2415 | // into is on the partition. | |
| 2313 | 2416 | let encryption = encryption.as_ref().map(|choice| Encryption { | |
| @@ -2315,7 +2418,7 @@ | |||
| 2315 | 2418 | passphrase: &choice.passphrase, | |
| 2316 | 2419 | recovery: &choice.recovery, | |
| 2317 | 2420 | }); | |
| 2318 | - | configure_plan( | |
| 2421 | + | finish_plan( | |
| 2319 | 2422 | &hostname, | |
| 2320 | 2423 | &username, | |
| 2321 | 2424 | &password, | |
| @@ -2332,6 +2435,61 @@ | |||
| 2332 | 2435 | stages | |
| 2333 | 2436 | } | |
| 2334 | 2437 | ||
| 2438 | + | /// Configure the deployment, assert it is a machine, and release it. | |
| 2439 | + | /// | |
| 2440 | + | /// The whole of the install's post-deploy half, in the order the three parts | |
| 2441 | + | /// have to happen: nothing can be checked before it is configured, and nothing | |
| 2442 | + | /// can be checked after the mount it is read through is gone. | |
| 2443 | + | #[allow(clippy::too_many_arguments)] | |
| 2444 | + | fn finish_plan( | |
| 2445 | + | hostname: &str, | |
| 2446 | + | username: &str, | |
| 2447 | + | password: &str, | |
| 2448 | + | seeds: &HomeSeeds<'_>, | |
| 2449 | + | locate_timezone: bool, | |
| 2450 | + | encryption: Option<&Encryption<'_>>, | |
| 2451 | + | deployment: &str, | |
| 2452 | + | ) -> Result<Vec<Stage>, String> { | |
| 2453 | + | let mut stages = configure_plan( | |
| 2454 | + | hostname, | |
| 2455 | + | username, | |
| 2456 | + | password, | |
| 2457 | + | seeds, | |
| 2458 | + | locate_timezone, | |
| 2459 | + | encryption, | |
| 2460 | + | deployment, | |
| 2461 | + | )?; | |
| 2462 | + | ||
| 2463 | + | // Asserted before the target is finalized and released, because every | |
| 2464 | + | // check reads it through the mount. | |
| 2465 | + | stages.extend(acceptance_plan(username, encryption, deployment)); | |
| 2466 | + | ||
| 2467 | + | stages.extend([ | |
| 2468 | + | // Upstream: "optional, but recommended to run as the | |
| 2469 | + | // penultimate step before unmounting the target filesystem. | |
| 2470 | + | // This command will perform some basic sanity checks and may | |
| 2471 | + | // also perform fixups on the target root." `--help` confirms | |
| 2472 | + | // the shape: `finalize <ROOT_PATH>`, the mounted root | |
| 2473 | + | // filesystem, which is what is passed. | |
| 2474 | + | // | |
| 2475 | + | // It bills itself as a step of an `install to-filesystem` | |
| 2476 | + | // flow, and `to-disk` already finalizes and unmounts by | |
| 2477 | + | // itself. Against a real to-disk install this exits 0 without | |
| 2478 | + | // output, so it is kept as a cheap sanity check rather than | |
| 2479 | + | // for any fixup it is known to make. | |
| 2480 | + | Stage::Run( | |
| 2481 | + | Invocation::new("bootc") | |
| 2482 | + | .args(["install", "finalize"]) | |
| 2483 | + | .arg(TARGET_MOUNT), | |
| 2484 | + | ), | |
| 2485 | + | // Leaving the target mounted would strand the filesystem dirty | |
| 2486 | + | // across the reboot the user is about to perform. | |
| 2487 | + | Stage::Run(unmount(TARGET_MOUNT)), | |
| 2488 | + | ]); | |
| 2489 | + | ||
| 2490 | + | Ok(stages) | |
| 2491 | + | } | |
| 2492 | + | ||
| 2335 | 2493 | // ---- disks ---- | |
| 2336 | 2494 | ||
| 2337 | 2495 | /// A whole disk, as an install target. | |
| @@ -3724,18 +3882,36 @@ | |||
| 3724 | 3882 | return Vec::new(); | |
| 3725 | 3883 | }; | |
| 3726 | 3884 | ||
| 3885 | + | let encryption = self.answers.encrypt.then(|| EncryptionChoice { | |
| 3886 | + | passphrase: passphrase.to_string(), | |
| 3887 | + | recovery: recovery.to_string(), | |
| 3888 | + | }); | |
| 3889 | + | ||
| 3727 | 3890 | install_plan( | |
| 3728 | 3891 | disk, | |
| 3892 | + | &self.choices(hostname, username, password, encryption.as_ref()), | |
| 3893 | + | ) | |
| 3894 | + | } | |
| 3895 | + | ||
| 3896 | + | /// The answers the plans take, gathered from the view. | |
| 3897 | + | /// |
Lines truncated