]> git.proxmox.com Git - pve-installer.git/blame - proxmox-auto-installer/src/utils.rs
drop glob helper and tests
[pve-installer.git] / proxmox-auto-installer / src / utils.rs
CommitLineData
ea8eea0a 1use anyhow::{bail, Context as _, Result};
345fdace 2use clap::ValueEnum;
c7edc2e1 3use glob::Pattern;
dd29e294 4use log::{debug, error, info};
c7edc2e1
AL
5use std::{
6 collections::BTreeMap,
7 process::{Command, Stdio},
8};
9
10use crate::{
11 answer::{self, Answer},
12 udevinfo::UdevInfo,
13};
14use proxmox_installer_common::{
15 options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption},
16 setup::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo},
17};
345fdace 18use serde::{Deserialize, Serialize};
c7edc2e1 19
c7edc2e1
AL
20pub 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
43pub 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)]
72pub 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)]
81pub 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)]
88struct IpLinksUdevInfo {
89 ifname: String,
90}
91
92/// Returns vec of usable NICs
93pub 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 111pub 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
142pub 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
156fn 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
187fn 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
260pub 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
271pub 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
293pub 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
322fn 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
348pub 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")]
434pub 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}