]> git.proxmox.com Git - pve-installer.git/blame - proxmox-auto-installer/src/utils.rs
auto installer: drop fetch-from auto mode
[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)]
d2c9b9fd 72pub 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)]
80pub 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)]
87struct IpLinksUdevInfo {
88 ifname: String,
89}
90
91/// Returns vec of usable NICs
92pub 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 110pub 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
141pub 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
155fn 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
186fn 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
259pub 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
270pub 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
295pub 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
324fn 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
350pub 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")]
436pub 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}