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