]> git.proxmox.com Git - pve-installer.git/blob - proxmox-installer-common/src/setup.rs
simplify some code
[pve-installer.git] / proxmox-installer-common / src / setup.rs
1 use std::{
2 cmp,
3 collections::{BTreeMap, HashMap},
4 fmt,
5 fs::File,
6 io::{self, BufReader},
7 net::IpAddr,
8 path::{Path, PathBuf},
9 process::{self, Command, Stdio},
10 };
11
12 use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
13
14 use crate::{
15 options::{
16 BtrfsRaidLevel, Disk, FsType, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption,
17 ZfsRaidLevel,
18 },
19 utils::CidrAddress,
20 };
21
22 #[allow(clippy::upper_case_acronyms)]
23 #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
24 #[serde(rename_all = "lowercase")]
25 pub enum ProxmoxProduct {
26 PVE,
27 PBS,
28 PMG,
29 }
30
31 impl ProxmoxProduct {
32 pub fn default_hostname(self) -> &'static str {
33 match self {
34 Self::PVE => "pve",
35 Self::PMG => "pmg",
36 Self::PBS => "pbs",
37 }
38 }
39 }
40
41 impl fmt::Display for ProxmoxProduct {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.write_str(match self {
44 Self::PVE => "pve",
45 Self::PMG => "pmg",
46 Self::PBS => "pbs",
47 })
48 }
49 }
50
51 #[derive(Debug, Clone, Deserialize, Serialize)]
52 pub struct ProductConfig {
53 pub fullname: String,
54 pub product: ProxmoxProduct,
55 #[serde(deserialize_with = "deserialize_bool_from_int")]
56 pub enable_btrfs: bool,
57 }
58
59 impl ProductConfig {
60 /// A mocked ProductConfig simulating a Proxmox VE environment.
61 pub fn mocked() -> Self {
62 Self {
63 fullname: String::from("Proxmox VE (mocked)"),
64 product: ProxmoxProduct::PVE,
65 enable_btrfs: true,
66 }
67 }
68 }
69
70 #[derive(Debug, Clone, Deserialize, Serialize)]
71 pub struct IsoInfo {
72 pub release: String,
73 pub isorelease: String,
74 }
75
76 impl IsoInfo {
77 /// A mocked IsoInfo with some edge case to convey that this is not necessarily purely numeric.
78 pub fn mocked() -> Self {
79 Self {
80 release: String::from("42.1"),
81 isorelease: String::from("mocked-1"),
82 }
83 }
84 }
85
86 /// Paths in the ISO environment containing installer data.
87 #[derive(Clone, Deserialize)]
88 pub struct IsoLocations {
89 pub iso: PathBuf,
90 }
91
92 impl IsoLocations {
93 /// A mocked location, uses the current working directory by default
94 pub fn mocked() -> Self {
95 Self {
96 iso: std::env::current_dir().unwrap_or("/dev/null".into()),
97 }
98 }
99 }
100
101 #[derive(Clone, Deserialize)]
102 pub struct SetupInfo {
103 #[serde(rename = "product-cfg")]
104 pub config: ProductConfig,
105 #[serde(rename = "iso-info")]
106 pub iso_info: IsoInfo,
107 pub locations: IsoLocations,
108 }
109
110 impl SetupInfo {
111 /// Return a mocked SetupInfo that is very similar to how our actual ones look like and should
112 /// be good enough for testing.
113 pub fn mocked() -> Self {
114 Self {
115 config: ProductConfig::mocked(),
116 iso_info: IsoInfo::mocked(),
117 locations: IsoLocations::mocked(),
118 }
119 }
120 }
121
122 #[derive(Clone, Deserialize)]
123 pub struct CountryInfo {
124 pub name: String,
125 #[serde(default)]
126 pub zone: String,
127 pub kmap: String,
128 }
129
130 #[derive(Clone, Deserialize, Eq, PartialEq)]
131 pub struct KeyboardMapping {
132 pub name: String,
133 #[serde(rename = "kvm")]
134 pub id: String,
135 #[serde(rename = "x11")]
136 pub xkb_layout: String,
137 #[serde(rename = "x11var")]
138 pub xkb_variant: String,
139 }
140
141 impl cmp::PartialOrd for KeyboardMapping {
142 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
143 self.name.partial_cmp(&other.name)
144 }
145 }
146
147 impl cmp::Ord for KeyboardMapping {
148 fn cmp(&self, other: &Self) -> cmp::Ordering {
149 self.name.cmp(&other.name)
150 }
151 }
152
153 #[derive(Clone, Deserialize)]
154 pub struct LocaleInfo {
155 #[serde(deserialize_with = "deserialize_cczones_map")]
156 pub cczones: HashMap<String, Vec<String>>,
157 #[serde(rename = "country")]
158 pub countries: HashMap<String, CountryInfo>,
159 pub kmap: HashMap<String, KeyboardMapping>,
160 }
161
162 /// Fetches basic information needed for the installer which is required to work
163 pub fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo, RuntimeInfo), String> {
164 let base_path = if in_test_mode { "./testdir" } else { "/" };
165 let mut path = PathBuf::from(base_path);
166
167 path.push("run");
168 path.push("proxmox-installer");
169
170 let installer_info: SetupInfo = {
171 let mut path = path.clone();
172 path.push("iso-info.json");
173
174 read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))?
175 };
176
177 let locale_info = {
178 let mut path = path.clone();
179 path.push("locales.json");
180
181 read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
182 };
183
184 let mut runtime_info: RuntimeInfo = {
185 let mut path = path.clone();
186 path.push("run-env-info.json");
187
188 read_json(&path)
189 .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
190 };
191
192 runtime_info.disks.sort();
193 if runtime_info.disks.is_empty() {
194 Err("The installer could not find any supported hard disks.".to_owned())
195 } else {
196 Ok((installer_info, locale_info, runtime_info))
197 }
198 }
199
200 #[derive(Debug, Deserialize, Serialize)]
201 pub struct InstallZfsOption {
202 pub ashift: usize,
203 #[serde(serialize_with = "serialize_as_display")]
204 pub compress: ZfsCompressOption,
205 #[serde(serialize_with = "serialize_as_display")]
206 pub checksum: ZfsChecksumOption,
207 pub copies: usize,
208 pub arc_max: usize,
209 }
210
211 impl From<ZfsBootdiskOptions> for InstallZfsOption {
212 fn from(opts: ZfsBootdiskOptions) -> Self {
213 InstallZfsOption {
214 ashift: opts.ashift,
215 compress: opts.compress,
216 checksum: opts.checksum,
217 copies: opts.copies,
218 arc_max: opts.arc_max,
219 }
220 }
221 }
222
223 pub fn read_json<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T, String> {
224 let file = File::open(path).map_err(|err| err.to_string())?;
225 let reader = BufReader::new(file);
226
227 serde_json::from_reader(reader).map_err(|err| format!("failed to parse JSON: {err}"))
228 }
229
230 fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
231 where
232 D: Deserializer<'de>,
233 {
234 let val: u32 = Deserialize::deserialize(deserializer)?;
235 Ok(val != 0)
236 }
237
238 fn deserialize_cczones_map<'de, D>(
239 deserializer: D,
240 ) -> Result<HashMap<String, Vec<String>>, D::Error>
241 where
242 D: Deserializer<'de>,
243 {
244 let map: HashMap<String, HashMap<String, u32>> = Deserialize::deserialize(deserializer)?;
245
246 let mut result = HashMap::new();
247 for (cc, list) in map.into_iter() {
248 result.insert(cc, list.into_keys().collect());
249 }
250
251 Ok(result)
252 }
253
254 fn deserialize_disks_map<'de, D>(deserializer: D) -> Result<Vec<Disk>, D::Error>
255 where
256 D: Deserializer<'de>,
257 {
258 let disks =
259 <Vec<(usize, String, f64, String, Option<usize>, String)>>::deserialize(deserializer)?;
260 Ok(disks
261 .into_iter()
262 .map(
263 |(index, device, size_mb, model, logical_bsize, _syspath)| Disk {
264 index: index.to_string(),
265 // Linux always reports the size of block devices in sectors, where one sector is
266 // defined as being 2^9 = 512 bytes in size.
267 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/blk_types.h?h=v6.4#n30
268 size: (size_mb * 512.) / 1024. / 1024. / 1024.,
269 block_size: logical_bsize,
270 path: device,
271 model: (!model.is_empty()).then_some(model),
272 },
273 )
274 .collect())
275 }
276
277 fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
278 where
279 D: Deserializer<'de>,
280 {
281 #[derive(Deserialize)]
282 struct CidrDescriptor {
283 address: String,
284 prefix: usize,
285 // family is implied anyway by parsing the address
286 }
287
288 let list: Vec<CidrDescriptor> = Deserialize::deserialize(deserializer)?;
289
290 let mut result = Vec::with_capacity(list.len());
291 for desc in list {
292 let ip_addr = desc
293 .address
294 .parse::<IpAddr>()
295 .map_err(|err| de::Error::custom(format!("{:?}", err)))?;
296
297 result.push(
298 CidrAddress::new(ip_addr, desc.prefix)
299 .map_err(|err| de::Error::custom(format!("{:?}", err)))?,
300 );
301 }
302
303 Ok(Some(result))
304 }
305
306 fn serialize_as_display<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
307 where
308 S: Serializer,
309 T: fmt::Display,
310 {
311 serializer.collect_str(value)
312 }
313
314 #[derive(Clone, Deserialize)]
315 pub struct RuntimeInfo {
316 /// Whether is system was booted in (legacy) BIOS or UEFI mode.
317 pub boot_type: BootType,
318
319 /// Detected country if available.
320 pub country: Option<String>,
321
322 /// Maps devices to their information.
323 #[serde(deserialize_with = "deserialize_disks_map")]
324 pub disks: Vec<Disk>,
325
326 /// Network addresses, gateways and DNS info.
327 pub network: NetworkInfo,
328
329 /// Total memory of the system in MiB.
330 pub total_memory: usize,
331
332 /// Whether the CPU supports hardware-accelerated virtualization
333 #[serde(deserialize_with = "deserialize_bool_from_int")]
334 pub hvm_supported: bool,
335 }
336
337 #[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
338 #[serde(rename_all = "lowercase")]
339 pub enum BootType {
340 Bios,
341 Efi,
342 }
343
344 #[derive(Clone, Deserialize)]
345 pub struct NetworkInfo {
346 pub dns: Dns,
347 pub routes: Option<Routes>,
348
349 /// Maps devices to their configuration, if it has a usable configuration.
350 /// (Contains no entries for devices with only link-local addresses.)
351 #[serde(default)]
352 pub interfaces: BTreeMap<String, Interface>,
353
354 /// The hostname of this machine, if set by the DHCP server.
355 pub hostname: Option<String>,
356 }
357
358 #[derive(Clone, Deserialize)]
359 pub struct Dns {
360 pub domain: Option<String>,
361
362 /// List of stringified IP addresses.
363 #[serde(default)]
364 pub dns: Vec<IpAddr>,
365 }
366
367 #[derive(Clone, Deserialize)]
368 pub struct Routes {
369 /// Ipv4 gateway.
370 pub gateway4: Option<Gateway>,
371
372 /// Ipv6 gateway.
373 pub gateway6: Option<Gateway>,
374 }
375
376 #[derive(Clone, Deserialize)]
377 pub struct Gateway {
378 /// Outgoing network device.
379 pub dev: String,
380
381 /// Stringified gateway IP address.
382 pub gateway: IpAddr,
383 }
384
385 #[derive(Clone, Deserialize)]
386 #[serde(rename_all = "UPPERCASE")]
387 pub enum InterfaceState {
388 Up,
389 Down,
390 #[serde(other)]
391 Unknown,
392 }
393
394 impl InterfaceState {
395 // avoid display trait as this is not the string representation for a serializer
396 pub fn render(&self) -> String {
397 match self {
398 Self::Up => "\u{25CF}",
399 Self::Down | Self::Unknown => " ",
400 }
401 .into()
402 }
403 }
404
405 #[derive(Clone, Deserialize)]
406 pub struct Interface {
407 pub name: String,
408
409 pub index: usize,
410
411 pub mac: String,
412
413 pub state: InterfaceState,
414
415 #[serde(default)]
416 #[serde(deserialize_with = "deserialize_cidr_list")]
417 pub addresses: Option<Vec<CidrAddress>>,
418 }
419
420 impl Interface {
421 // avoid display trait as this is not the string representation for a serializer
422 pub fn render(&self) -> String {
423 format!("{} {}", self.state.render(), self.name)
424 }
425 }
426
427 pub fn spawn_low_level_installer(test_mode: bool) -> io::Result<process::Child> {
428 let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = if test_mode {
429 (
430 "./proxmox-low-level-installer",
431 &["-t", "/dev/null", "start-session-test"],
432 vec![("PERL5LIB", ".")],
433 )
434 } else {
435 ("proxmox-low-level-installer", &["start-session"], vec![])
436 };
437
438 Command::new(path)
439 .args(args)
440 .envs(envs)
441 .stdin(Stdio::piped())
442 .stdout(Stdio::piped())
443 .spawn()
444 }
445
446 /// See Proxmox::Install::Config
447 #[derive(Debug, Deserialize, Serialize)]
448 pub struct InstallConfig {
449 pub autoreboot: usize,
450
451 #[serde(
452 serialize_with = "serialize_fstype",
453 deserialize_with = "deserialize_fs_type"
454 )]
455 pub filesys: FsType,
456 pub hdsize: f64,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub swapsize: Option<f64>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub maxroot: Option<f64>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub minfree: Option<f64>,
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub maxvz: Option<f64>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
467 pub zfs_opts: Option<InstallZfsOption>,
468
469 #[serde(
470 serialize_with = "serialize_disk_opt",
471 skip_serializing_if = "Option::is_none",
472 // only the 'path' property is serialized -> deserialization is problematic
473 // The information would be present in the 'run-env-info-json', but for now there is no
474 // need for it in any code that deserializes the low-level config. Therefore we are
475 // currently skipping it on deserialization
476 skip_deserializing
477 )]
478 pub target_hd: Option<Disk>,
479 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
480 pub disk_selection: BTreeMap<String, String>,
481
482 pub lvm_auto_rename: usize,
483
484 pub country: String,
485 pub timezone: String,
486 pub keymap: String,
487
488 pub password: String,
489 pub mailto: String,
490
491 pub mngmt_nic: String,
492
493 pub hostname: String,
494 pub domain: String,
495 #[serde(serialize_with = "serialize_as_display")]
496 pub cidr: CidrAddress,
497 pub gateway: IpAddr,
498 pub dns: IpAddr,
499 }
500
501 fn serialize_disk_opt<S>(value: &Option<Disk>, serializer: S) -> Result<S::Ok, S::Error>
502 where
503 S: Serializer,
504 {
505 if let Some(disk) = value {
506 serializer.serialize_str(&disk.path)
507 } else {
508 serializer.serialize_none()
509 }
510 }
511
512 fn serialize_fstype<S>(value: &FsType, serializer: S) -> Result<S::Ok, S::Error>
513 where
514 S: Serializer,
515 {
516 use FsType::*;
517 let value = match value {
518 // proxinstall::$fssetup
519 Ext4 => "ext4",
520 Xfs => "xfs",
521 // proxinstall::get_zfs_raid_setup()
522 Zfs(ZfsRaidLevel::Raid0) => "zfs (RAID0)",
523 Zfs(ZfsRaidLevel::Raid1) => "zfs (RAID1)",
524 Zfs(ZfsRaidLevel::Raid10) => "zfs (RAID10)",
525 Zfs(ZfsRaidLevel::RaidZ) => "zfs (RAIDZ-1)",
526 Zfs(ZfsRaidLevel::RaidZ2) => "zfs (RAIDZ-2)",
527 Zfs(ZfsRaidLevel::RaidZ3) => "zfs (RAIDZ-3)",
528 // proxinstall::get_btrfs_raid_setup()
529 Btrfs(BtrfsRaidLevel::Raid0) => "btrfs (RAID0)",
530 Btrfs(BtrfsRaidLevel::Raid1) => "btrfs (RAID1)",
531 Btrfs(BtrfsRaidLevel::Raid10) => "btrfs (RAID10)",
532 };
533
534 serializer.collect_str(value)
535 }
536
537 pub fn deserialize_fs_type<'de, D>(deserializer: D) -> Result<FsType, D::Error>
538 where
539 D: Deserializer<'de>,
540 {
541 use FsType::*;
542 let de_fs: String = Deserialize::deserialize(deserializer)?;
543
544 match de_fs.as_str() {
545 "ext4" => Ok(Ext4),
546 "xfs" => Ok(Xfs),
547 "zfs (RAID0)" => Ok(Zfs(ZfsRaidLevel::Raid0)),
548 "zfs (RAID1)" => Ok(Zfs(ZfsRaidLevel::Raid1)),
549 "zfs (RAID10)" => Ok(Zfs(ZfsRaidLevel::Raid10)),
550 "zfs (RAIDZ-1)" => Ok(Zfs(ZfsRaidLevel::RaidZ)),
551 "zfs (RAIDZ-2)" => Ok(Zfs(ZfsRaidLevel::RaidZ2)),
552 "zfs (RAIDZ-3)" => Ok(Zfs(ZfsRaidLevel::RaidZ3)),
553 "btrfs (RAID0)" => Ok(Btrfs(BtrfsRaidLevel::Raid0)),
554 "btrfs (RAID1)" => Ok(Btrfs(BtrfsRaidLevel::Raid1)),
555 "btrfs (RAID10)" => Ok(Btrfs(BtrfsRaidLevel::Raid10)),
556 _ => Err(de::Error::custom("could not find file system: {de_fs}")),
557 }
558 }