1use std::path::{Path, PathBuf};
18
19use anyhow::Result;
20
21pub use appliance_pid1::net::DhcpSession;
22
23pub const NET_VAR: &str = "GUNNAR_NET";
26pub const T1_OVERRIDE_VAR: &str = "GUNNAR_DHCP_T1_OVERRIDE_SECS";
28pub const HOSTNAME_PREFIX: &str = "gunnar";
30pub const RESOLVER_PROBE: &str = "github.com";
32pub const PANIC_CODE: &str = "GN-99";
34
35pub fn net_plans() -> Result<Vec<appliance_pid1::net::NetConfig>> {
37 appliance_pid1::net::NetConfig::from_env(NET_VAR)
38}
39
40pub fn bring_up_network() -> Result<Vec<DhcpSession>> {
47 let narrate = |note: &str| ship_boot_narration(note);
48 let how = appliance_pid1::net::Bringup {
49 hostname_prefix: HOSTNAME_PREFIX,
50 resolver_probe: Some(RESOLVER_PROBE),
51 narrate: &narrate,
52 };
53 appliance_pid1::net::bring_up_network_with(net_plans()?, &how, late_lease())
54}
55
56pub fn late_lease() -> appliance_pid1::net::LateLease {
59 appliance_pid1::net::LateLease {
60 narrate: ship_boot_narration,
61 t1_override_var: Some(T1_OVERRIDE_VAR),
62 }
63}
64
65pub fn ship_boot_narration(network_note: &str) {
68 appliance_pid1::narration::ship_boot_narration(network_note, &|l: &str| gunnar_common::logqueue::tap_line(l));
69}
70
71#[cfg(feature = "dhcp-client")]
74pub async fn dhcp_renewal(session: DhcpSession) {
75 appliance_pid1::runtime::dhcp_renewal(session, appliance_pid1::runtime::dhcp_t1_override(T1_OVERRIDE_VAR)).await
76}
77
78#[cfg(not(feature = "dhcp-client"))]
81pub async fn dhcp_renewal(session: DhcpSession) {
82 match session {}
83}
84
85pub fn announce_install_mode(data_root: &Path) {
91 if !Path::new(appliance_pid1::install::INSTALL_RECORD).exists() {
92 return;
93 }
94 let set = crate::dashboard::probe::disk_kind(data_root)
95 .and_then(|k| k.set)
96 .map(|s| format!("{}\u{d7}{}", s.members, crate::dashboard::fmt_bytes(s.member_bytes)));
97 appliance_pid1::install::announce_install_mode(set.as_deref(), &install_mode_event);
98}
99
100pub fn install_mode_event(line: &str) {
103 let f = install_mode_fields(line);
104 tracing::info!(
105 event = "install.mode",
106 mode = %f[0],
107 kept = %f[1],
108 kept_partno = %f[2],
109 kept_gib = %f[3],
110 kept_set = %f[4],
111 "install record read off {}",
112 appliance_pid1::install::INSTALL_RECORD
113 );
114}
115
116pub fn install_mode_fields(line: &str) -> [String; 5] {
119 let get = |k: &str| {
120 line.split_whitespace()
121 .find_map(|t| t.strip_prefix(k).and_then(|r| r.strip_prefix('=')))
122 .unwrap_or("-")
123 .to_owned()
124 };
125 [get("mode"), get("kept"), get("kept_partno"), get("kept_gib"), get("kept_set")]
126}
127
128pub fn install_panic_fault(data: Option<PathBuf>, shout: fn(&str)) {
138 appliance_pid1::fault::narration_from(crate::narrate::memory);
141 let p = appliance_pid1::fault::PanicFault {
142 code: PANIC_CODE,
143 meaning: crate::narrate::code_meaning(PANIC_CODE).unwrap_or("the app panicked"),
144 data,
145 };
146 appliance_pid1::fault::install_panic_fault(
147 p,
148 || appliance_pid1::net::is_init() && crate::narrate::narrator().is_some(),
149 move |line: &str| say_panic_line(line, shout),
150 );
151}
152
153fn say_panic_line(line: &str, shout: fn(&str)) {
155 if let Some(text) = line.strip_prefix("PANIC: ") {
156 let text = format!("\ngunnar PANIC (exit 101): {text}\n");
157 shout(&text);
158 if let Some(path) = crate::slow::record_crash(&text) {
162 shout(&format!("gunnar: crash record written to {}\n", path.display()));
163 }
164 return;
165 }
166 match crate::narrate::narrator() {
167 Some(n) => n.say(line),
168 None => shout(&format!("gunnar: {line}\n")),
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
179 fn gunnar_names_its_network_variable_its_host_and_its_panic() {
180 assert_eq!(NET_VAR, "GUNNAR_NET");
181 assert_eq!(T1_OVERRIDE_VAR, "GUNNAR_DHCP_T1_OVERRIDE_SECS");
182 assert_eq!(appliance_pid1::net::hostname_for(HOSTNAME_PREFIX, None, [0, 0x11, 0x22, 0xaa, 0xbb, 0xcc]), "gunnar-aabbcc");
183 assert_eq!(appliance_pid1::net::hostname_for(HOSTNAME_PREFIX, Some("vm1.example"), [0; 6]), "vm1.example");
184 assert_eq!(RESOLVER_PROBE, "github.com");
185 assert_eq!(PANIC_CODE, "GN-99");
186 assert_eq!(crate::narrate::code_meaning(PANIC_CODE), Some("the app panicked"));
187 }
188
189 #[test]
192 fn the_install_line_becomes_the_dashboards_fields() {
193 use appliance_pid1::install::InstallRecord;
194 let p = InstallRecord::parse("mode=upgrade\nkept=korp-data\nkept_partno=3\nkept_bytes=241591910400\n").line(None);
195 assert_eq!(install_mode_fields(&p), ["upgrade", "korp-data", "3", "225.0", "-"].map(String::from));
196 let s = InstallRecord::parse("mode=upgrade\nkept=data-set\n").line(Some("4\u{d7}75G"));
197 assert_eq!(install_mode_fields(&s), ["upgrade", "data-set", "-", "-", "4\u{d7}75G"].map(String::from));
198 let s = InstallRecord::parse("mode=upgrade\nkept=data-set\n").line(None);
199 assert_eq!(install_mode_fields(&s), ["upgrade", "data-set", "-", "-", "-"].map(String::from));
200 let f = InstallRecord::parse("mode=fresh\n").line(None);
201 assert_eq!(install_mode_fields(&format!("{f} (install record read off the ESP)")), ["fresh", "-", "-", "-", "-"].map(String::from));
202 }
203
204 #[test]
212 fn a_late_lease_is_narrated_into_the_installers_boot_log() {
213 let late = late_lease();
214 assert_eq!(late.t1_override_var, Some(T1_OVERRIDE_VAR));
215 #[cfg(feature = "wg-client")]
217 {
218 crate::installer_listener::capture();
219 (late.narrate)("network up: eth0 10.0.2.15/24 gw Some(10.0.2.2) via DHCP from 10.0.2.2 (lease 3600s) — after the boot's failure, 7 asks l10-late");
220 let log = crate::installer_listener::BOOT_LOG.since(0, usize::MAX).lines.join("\n");
221 assert!(log.contains("after the boot's failure, 7 asks l10-late"), "the late line reaches the boot log: {log}");
222 }
223 let src = include_str!("boot.rs");
224 let up = &src[src.find("pub fn bring_up_network()").unwrap()..];
225 let up = &up[..up.find("\n}\n").unwrap()];
226 assert!(up.contains("bring_up_network_with(net_plans()?, &how, late_lease())"), "{up}");
227 assert!(up.contains("resolver_probe: Some(RESOLVER_PROBE)"), "{up}");
228 }
229
230 #[test]
233 fn gunnar_net_is_read_from_its_own_variable() {
234 if std::env::var_os(NET_VAR).is_none() {
236 assert!(net_plans().unwrap().is_empty());
237 }
238 let plans = appliance_pid1::net::NetConfig::parse_list("eth0:81.27.107.63/23:81.27.106.1,eth1:10.13.8.78/22").unwrap();
239 assert_eq!(plans.iter().map(|p| p.interface()).collect::<Vec<_>>(), ["eth0", "eth1"]);
240 }
241}