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