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