]>
Commit | Line | Data |
---|---|---|
ea8eea0a | 1 | use anyhow::{bail, Context as _, Result}; |
345fdace | 2 | use clap::ValueEnum; |
c7edc2e1 | 3 | use glob::Pattern; |
9a5b563d CH |
4 | use log::info; |
5 | use std::{collections::BTreeMap, process::Command}; | |
c7edc2e1 AL |
6 | |
7 | use crate::{ | |
8 | answer::{self, Answer}, | |
9 | udevinfo::UdevInfo, | |
10 | }; | |
11 | use proxmox_installer_common::{ | |
12 | options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption}, | |
bdca138e CH |
13 | setup::{ |
14 | InstallConfig, InstallRootPassword, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo, | |
15 | }, | |
c7edc2e1 | 16 | }; |
345fdace | 17 | use serde::{Deserialize, Serialize}; |
c7edc2e1 | 18 | |
c7edc2e1 AL |
19 | pub 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 | ||
42 | pub 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 |
71 | pub enum FetchAnswerFrom { |
72 | Iso, | |
345fdace AL |
73 | Http, |
74 | Partition, | |
75 | } | |
76 | ||
4cf15bd1 TL |
77 | #[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)] |
78 | pub 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)] | |
87 | pub 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)] |
94 | struct IpLinksUdevInfo { | |
95 | ifname: String, | |
96 | } | |
97 | ||
98 | /// Returns vec of usable NICs | |
99 | pub 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 | 117 | pub 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 | ||
148 | pub 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 | ||
162 | fn 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 | ||
193 | fn 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 | ||
266 | pub 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 | ||
277 | pub 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 |
306 | fn 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 |
317 | pub 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")] | |
403 | pub 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 | } |