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