]>
Commit | Line | Data |
---|---|---|
ea8eea0a | 1 | use anyhow::{bail, Context as _, Result}; |
345fdace | 2 | use clap::ValueEnum; |
c7edc2e1 | 3 | use glob::Pattern; |
dd29e294 | 4 | use log::{debug, error, info}; |
c7edc2e1 AL |
5 | use std::{ |
6 | collections::BTreeMap, | |
7 | process::{Command, Stdio}, | |
8 | }; | |
9 | ||
10 | use crate::{ | |
11 | answer::{self, Answer}, | |
12 | udevinfo::UdevInfo, | |
13 | }; | |
14 | use proxmox_installer_common::{ | |
15 | options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption}, | |
16 | setup::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo}, | |
17 | }; | |
345fdace | 18 | use serde::{Deserialize, Serialize}; |
c7edc2e1 | 19 | |
c7edc2e1 AL |
20 | pub fn get_network_settings( |
21 | answer: &Answer, | |
22 | udev_info: &UdevInfo, | |
23 | runtime_info: &RuntimeInfo, | |
24 | setup_info: &SetupInfo, | |
25 | ) -> Result<NetworkOptions> { | |
26 | let mut network_options = NetworkOptions::defaults_from(setup_info, &runtime_info.network); | |
27 | ||
28 | info!("Setting network configuration"); | |
29 | ||
30 | // Always use the FQDN from the answer file | |
31 | network_options.fqdn = answer.global.fqdn.clone(); | |
32 | ||
33 | if let answer::NetworkSettings::Manual(settings) = &answer.network.network_settings { | |
34 | network_options.address = settings.cidr.clone(); | |
35 | network_options.dns_server = settings.dns; | |
36 | network_options.gateway = settings.gateway; | |
2a777841 | 37 | network_options.ifname = get_single_udev_index(&settings.filter, &udev_info.nics)?; |
c7edc2e1 AL |
38 | } |
39 | info!("Network interface used is '{}'", &network_options.ifname); | |
40 | Ok(network_options) | |
41 | } | |
42 | ||
43 | pub fn get_single_udev_index( | |
2a777841 | 44 | filter: &BTreeMap<String, String>, |
c7edc2e1 AL |
45 | udev_list: &BTreeMap<String, BTreeMap<String, String>>, |
46 | ) -> Result<String> { | |
47 | if filter.is_empty() { | |
48 | bail!("no filter defined"); | |
49 | } | |
50 | let mut dev_index: Option<String> = None; | |
51 | 'outer: for (dev, dev_values) in udev_list { | |
2a777841 | 52 | for (filter_key, filter_value) in filter { |
ea8eea0a WB |
53 | let filter_pattern = |
54 | Pattern::new(filter_value).context("invalid glob in disk selection")?; | |
c7edc2e1 | 55 | for (udev_key, udev_value) in dev_values { |
ea8eea0a | 56 | if udev_key == filter_key && filter_pattern.matches(udev_value) { |
c7edc2e1 AL |
57 | dev_index = Some(dev.clone()); |
58 | break 'outer; // take first match | |
59 | } | |
60 | } | |
61 | } | |
62 | } | |
63 | if dev_index.is_none() { | |
64 | bail!("filter did not match any device"); | |
65 | } | |
66 | ||
67 | Ok(dev_index.unwrap()) | |
68 | } | |
69 | ||
345fdace AL |
70 | #[derive(Deserialize, Serialize, Debug, Clone, ValueEnum, PartialEq)] |
71 | #[serde(rename_all = "lowercase", deny_unknown_fields)] | |
72 | pub enum AutoInstModes { | |
73 | Auto, | |
74 | Included, | |
75 | Http, | |
76 | Partition, | |
77 | } | |
78 | ||
79 | #[derive(Deserialize, Serialize, Debug)] | |
80 | #[serde(rename_all = "lowercase", deny_unknown_fields)] | |
81 | pub struct AutoInstSettings { | |
82 | pub mode: AutoInstModes, | |
83 | pub http_url: Option<String>, | |
84 | pub cert_fingerprint: Option<String>, | |
85 | } | |
86 | ||
eedc6521 AL |
87 | #[derive(Deserialize, Debug)] |
88 | struct IpLinksUdevInfo { | |
89 | ifname: String, | |
90 | } | |
91 | ||
92 | /// Returns vec of usable NICs | |
93 | pub fn get_nic_list() -> Result<Vec<String>> { | |
94 | let ip_output = Command::new("/usr/sbin/ip") | |
95 | .arg("-j") | |
96 | .arg("link") | |
97 | .output()?; | |
15ba8a15 | 98 | let parsed_links: Vec<IpLinksUdevInfo> = serde_json::from_slice(&ip_output.stdout)?; |
eedc6521 AL |
99 | let mut links: Vec<String> = Vec::new(); |
100 | ||
101 | for link in parsed_links { | |
102 | if link.ifname == *"lo" { | |
103 | continue; | |
104 | } | |
105 | links.push(link.ifname); | |
106 | } | |
107 | ||
108 | Ok(links) | |
109 | } | |
110 | ||
c7edc2e1 | 111 | pub fn get_matched_udev_indexes( |
2a777841 | 112 | filter: &BTreeMap<String, String>, |
c7edc2e1 AL |
113 | udev_list: &BTreeMap<String, BTreeMap<String, String>>, |
114 | match_all: bool, | |
115 | ) -> Result<Vec<String>> { | |
116 | let mut matches = vec![]; | |
117 | for (dev, dev_values) in udev_list { | |
118 | let mut did_match_once = false; | |
119 | let mut did_match_all = true; | |
2a777841 | 120 | for (filter_key, filter_value) in filter { |
ea8eea0a WB |
121 | let filter_pattern = |
122 | Pattern::new(filter_value).context("invalid glob in disk selection")?; | |
c7edc2e1 | 123 | for (udev_key, udev_value) in dev_values { |
ea8eea0a | 124 | if udev_key == filter_key && filter_pattern.matches(udev_value) { |
c7edc2e1 AL |
125 | did_match_once = true; |
126 | } else if udev_key == filter_key { | |
127 | did_match_all = false; | |
128 | } | |
129 | } | |
130 | } | |
131 | if (match_all && did_match_all) || (!match_all && did_match_once) { | |
132 | matches.push(dev.clone()); | |
133 | } | |
134 | } | |
135 | if matches.is_empty() { | |
136 | bail!("filter did not match any devices"); | |
137 | } | |
138 | matches.sort(); | |
139 | Ok(matches) | |
140 | } | |
141 | ||
142 | pub fn set_disks( | |
143 | answer: &Answer, | |
144 | udev_info: &UdevInfo, | |
145 | runtime_info: &RuntimeInfo, | |
146 | config: &mut InstallConfig, | |
147 | ) -> Result<()> { | |
148 | match config.filesys { | |
149 | FsType::Ext4 | FsType::Xfs => set_single_disk(answer, udev_info, runtime_info, config), | |
150 | FsType::Zfs(_) | FsType::Btrfs(_) => { | |
151 | set_selected_disks(answer, udev_info, runtime_info, config) | |
152 | } | |
153 | } | |
154 | } | |
155 | ||
156 | fn set_single_disk( | |
157 | answer: &Answer, | |
158 | udev_info: &UdevInfo, | |
159 | runtime_info: &RuntimeInfo, | |
160 | config: &mut InstallConfig, | |
161 | ) -> Result<()> { | |
162 | match &answer.disks.disk_selection { | |
163 | answer::DiskSelection::Selection(disk_list) => { | |
164 | let disk_name = disk_list[0].clone(); | |
165 | let disk = runtime_info | |
166 | .disks | |
167 | .iter() | |
168 | .find(|item| item.path.ends_with(disk_name.as_str())); | |
169 | match disk { | |
170 | Some(disk) => config.target_hd = Some(disk.clone()), | |
171 | None => bail!("disk in 'disk_selection' not found"), | |
172 | } | |
173 | } | |
174 | answer::DiskSelection::Filter(filter) => { | |
2a777841 | 175 | let disk_index = get_single_udev_index(filter, &udev_info.disks)?; |
c7edc2e1 AL |
176 | let disk = runtime_info |
177 | .disks | |
178 | .iter() | |
179 | .find(|item| item.index == disk_index); | |
180 | config.target_hd = disk.cloned(); | |
181 | } | |
182 | } | |
183 | info!("Selected disk: {}", config.target_hd.clone().unwrap().path); | |
184 | Ok(()) | |
185 | } | |
186 | ||
187 | fn set_selected_disks( | |
188 | answer: &Answer, | |
189 | udev_info: &UdevInfo, | |
190 | runtime_info: &RuntimeInfo, | |
191 | config: &mut InstallConfig, | |
192 | ) -> Result<()> { | |
193 | match &answer.disks.disk_selection { | |
194 | answer::DiskSelection::Selection(disk_list) => { | |
195 | info!("Disk selection found"); | |
196 | for disk_name in disk_list.clone() { | |
197 | let disk = runtime_info | |
198 | .disks | |
199 | .iter() | |
200 | .find(|item| item.path.ends_with(disk_name.as_str())); | |
201 | if let Some(disk) = disk { | |
202 | config | |
203 | .disk_selection | |
204 | .insert(disk.index.clone(), disk.index.clone()); | |
205 | } | |
206 | } | |
207 | } | |
208 | answer::DiskSelection::Filter(filter) => { | |
209 | info!("No disk list found, looking for disk filters"); | |
210 | let filter_match = answer | |
211 | .disks | |
212 | .filter_match | |
213 | .clone() | |
214 | .unwrap_or(answer::FilterMatch::Any); | |
c7edc2e1 | 215 | let selected_disk_indexes = get_matched_udev_indexes( |
2a777841 | 216 | filter, |
c7edc2e1 AL |
217 | &udev_info.disks, |
218 | filter_match == answer::FilterMatch::All, | |
219 | )?; | |
220 | ||
221 | for i in selected_disk_indexes.into_iter() { | |
222 | let disk = runtime_info | |
223 | .disks | |
224 | .iter() | |
225 | .find(|item| item.index == i) | |
226 | .unwrap(); | |
227 | config | |
228 | .disk_selection | |
229 | .insert(disk.index.clone(), disk.index.clone()); | |
230 | } | |
231 | } | |
232 | } | |
233 | if config.disk_selection.is_empty() { | |
234 | bail!("No disks found matching selection."); | |
235 | } | |
236 | ||
237 | let mut selected_disks: Vec<String> = Vec::new(); | |
238 | for i in config.disk_selection.keys() { | |
239 | selected_disks.push( | |
240 | runtime_info | |
241 | .disks | |
242 | .iter() | |
243 | .find(|item| item.index.as_str() == i) | |
244 | .unwrap() | |
245 | .clone() | |
246 | .path, | |
247 | ); | |
248 | } | |
249 | info!( | |
250 | "Selected disks: {}", | |
251 | selected_disks | |
252 | .iter() | |
253 | .map(|x| x.to_string() + " ") | |
254 | .collect::<String>() | |
255 | ); | |
256 | ||
257 | Ok(()) | |
258 | } | |
259 | ||
260 | pub fn get_first_selected_disk(config: &InstallConfig) -> usize { | |
261 | config | |
262 | .disk_selection | |
263 | .iter() | |
264 | .next() | |
265 | .expect("no disks found") | |
266 | .0 | |
267 | .parse::<usize>() | |
268 | .expect("could not parse key to usize") | |
269 | } | |
270 | ||
271 | pub fn verify_locale_settings(answer: &Answer, locales: &LocaleInfo) -> Result<()> { | |
272 | info!("Verifying locale settings"); | |
273 | if !locales | |
274 | .countries | |
275 | .keys() | |
276 | .any(|i| i == &answer.global.country) | |
277 | { | |
278 | bail!("country code '{}' is not valid", &answer.global.country); | |
279 | } | |
280 | if !locales.kmap.keys().any(|i| i == &answer.global.keyboard) { | |
281 | bail!("keyboard layout '{}' is not valid", &answer.global.keyboard); | |
282 | } | |
283 | if !locales | |
284 | .cczones | |
285 | .iter() | |
286 | .any(|(_, zones)| zones.contains(&answer.global.timezone)) | |
287 | { | |
288 | bail!("timezone '{}' is not valid", &answer.global.timezone); | |
289 | } | |
290 | Ok(()) | |
291 | } | |
292 | ||
dd29e294 CH |
293 | pub fn run_cmds(step: &str, in_chroot: bool, cmds: &[&str]) { |
294 | let run = || { | |
295 | debug!("Running commands for '{step}':"); | |
296 | for cmd in cmds { | |
297 | run_cmd(cmd)?; | |
298 | } | |
299 | Ok::<(), anyhow::Error>(()) | |
300 | }; | |
301 | ||
302 | if in_chroot { | |
303 | if let Err(err) = run_cmd("proxmox-chroot prepare") { | |
304 | error!("Failed to setup chroot for '{step}': {err}"); | |
305 | return; | |
306 | } | |
307 | } | |
308 | ||
309 | if let Err(err) = run() { | |
310 | error!("Running commands for '{step}' failed: {err:?}"); | |
311 | } else { | |
312 | debug!("Running commands in chroot for '{step}' finished"); | |
313 | } | |
314 | ||
315 | if in_chroot { | |
316 | if let Err(err) = run_cmd("proxmox-chroot cleanup") { | |
317 | error!("Failed to clean up chroot for '{step}': {err}"); | |
c7edc2e1 AL |
318 | } |
319 | } | |
c7edc2e1 AL |
320 | } |
321 | ||
dd29e294 CH |
322 | fn run_cmd(cmd: &str) -> Result<()> { |
323 | debug!("Command '{cmd}':"); | |
324 | let child = match Command::new("/bin/bash") | |
325 | .arg("-c") | |
326 | .arg(cmd) | |
327 | .stdout(Stdio::piped()) | |
328 | .stderr(Stdio::piped()) | |
329 | .spawn() | |
330 | { | |
331 | Ok(child) => child, | |
332 | Err(err) => bail!("error running command {cmd}: {err}"), | |
333 | }; | |
334 | match child.wait_with_output() { | |
335 | Ok(output) => { | |
336 | if output.status.success() { | |
337 | debug!("{}", String::from_utf8(output.stdout).unwrap()); | |
338 | } else { | |
339 | bail!("{}", String::from_utf8(output.stderr).unwrap()); | |
e0cac816 | 340 | } |
c7edc2e1 | 341 | } |
dd29e294 | 342 | Err(err) => bail!("{err}"), |
c7edc2e1 AL |
343 | } |
344 | ||
345 | Ok(()) | |
346 | } | |
347 | ||
348 | pub fn parse_answer( | |
349 | answer: &Answer, | |
350 | udev_info: &UdevInfo, | |
351 | runtime_info: &RuntimeInfo, | |
352 | locales: &LocaleInfo, | |
353 | setup_info: &SetupInfo, | |
354 | ) -> Result<InstallConfig> { | |
355 | info!("Parsing answer file"); | |
356 | info!("Setting File system"); | |
357 | let filesystem = answer.disks.fs_type; | |
358 | info!("File system selected: {}", filesystem); | |
359 | ||
360 | let network_settings = get_network_settings(answer, udev_info, runtime_info, setup_info)?; | |
361 | ||
362 | verify_locale_settings(answer, locales)?; | |
363 | ||
364 | let mut config = InstallConfig { | |
5878dc4a | 365 | autoreboot: 0, |
c7edc2e1 AL |
366 | filesys: filesystem, |
367 | hdsize: 0., | |
368 | swapsize: None, | |
369 | maxroot: None, | |
370 | minfree: None, | |
371 | maxvz: None, | |
372 | zfs_opts: None, | |
373 | target_hd: None, | |
374 | disk_selection: BTreeMap::new(), | |
ec880798 | 375 | lvm_auto_rename: 1, |
c7edc2e1 AL |
376 | |
377 | country: answer.global.country.clone(), | |
378 | timezone: answer.global.timezone.clone(), | |
379 | keymap: answer.global.keyboard.clone(), | |
380 | ||
77ca432f | 381 | password: answer.global.root_password.clone(), |
c7edc2e1 AL |
382 | mailto: answer.global.mailto.clone(), |
383 | ||
384 | mngmt_nic: network_settings.ifname, | |
385 | ||
386 | hostname: network_settings.fqdn.host().unwrap().to_string(), | |
387 | domain: network_settings.fqdn.domain(), | |
388 | cidr: network_settings.address, | |
389 | gateway: network_settings.gateway, | |
390 | dns: network_settings.dns_server, | |
391 | }; | |
392 | ||
393 | set_disks(answer, udev_info, runtime_info, &mut config)?; | |
394 | match &answer.disks.fs_options { | |
395 | answer::FsOptions::LVM(lvm) => { | |
396 | config.hdsize = lvm.hdsize.unwrap_or(config.target_hd.clone().unwrap().size); | |
397 | config.swapsize = lvm.swapsize; | |
398 | config.maxroot = lvm.maxroot; | |
399 | config.maxvz = lvm.maxvz; | |
400 | config.minfree = lvm.minfree; | |
401 | } | |
402 | answer::FsOptions::ZFS(zfs) => { | |
403 | let first_selected_disk = get_first_selected_disk(&config); | |
404 | ||
405 | config.hdsize = zfs | |
406 | .hdsize | |
407 | .unwrap_or(runtime_info.disks[first_selected_disk].size); | |
408 | config.zfs_opts = Some(InstallZfsOption { | |
409 | ashift: zfs.ashift.unwrap_or(12), | |
410 | arc_max: zfs.arc_max.unwrap_or(2048), | |
411 | compress: zfs.compress.unwrap_or(ZfsCompressOption::On), | |
412 | checksum: zfs.checksum.unwrap_or(ZfsChecksumOption::On), | |
413 | copies: zfs.copies.unwrap_or(1), | |
414 | }); | |
415 | } | |
416 | answer::FsOptions::BTRFS(btrfs) => { | |
417 | let first_selected_disk = get_first_selected_disk(&config); | |
418 | ||
419 | config.hdsize = btrfs | |
420 | .hdsize | |
421 | .unwrap_or(runtime_info.disks[first_selected_disk].size); | |
422 | } | |
423 | } | |
5878dc4a CH |
424 | |
425 | // never print the auto reboot text after finishing to avoid the delay, as this is handled by | |
426 | // the auto-installer itself anyway. The auto-installer might still perform some post-install | |
427 | // steps after running the low-level installer. | |
428 | config.autoreboot = 0; | |
c7edc2e1 AL |
429 | Ok(config) |
430 | } | |
431 | ||
432 | #[derive(Clone, Debug, Deserialize, PartialEq)] | |
433 | #[serde(tag = "type", rename_all = "lowercase")] | |
434 | pub enum LowLevelMessage { | |
435 | #[serde(rename = "message")] | |
436 | Info { | |
437 | message: String, | |
438 | }, | |
439 | Error { | |
440 | message: String, | |
441 | }, | |
442 | Prompt { | |
443 | query: String, | |
444 | }, | |
445 | Finished { | |
446 | state: String, | |
447 | message: String, | |
448 | }, | |
449 | Progress { | |
450 | ratio: f32, | |
451 | text: String, | |
452 | }, | |
453 | } |