]> git.proxmox.com Git - pve-installer.git/blob - proxmox-installer-common/src/setup.rs
tui: add missing argument for low-level installer test-session
[pve-installer.git] / proxmox-installer-common / src / setup.rs
1 use std::{
2 cmp,
3 collections::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::{Disk, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption},
16 utils::CidrAddress,
17 };
18
19 #[allow(clippy::upper_case_acronyms)]
20 #[derive(Clone, Copy, Deserialize, PartialEq)]
21 #[serde(rename_all = "lowercase")]
22 pub enum ProxmoxProduct {
23 PVE,
24 PBS,
25 PMG,
26 }
27
28 impl ProxmoxProduct {
29 pub fn default_hostname(self) -> &'static str {
30 match self {
31 Self::PVE => "pve",
32 Self::PMG => "pmg",
33 Self::PBS => "pbs",
34 }
35 }
36 }
37
38 #[derive(Clone, Deserialize)]
39 pub struct ProductConfig {
40 pub fullname: String,
41 pub product: ProxmoxProduct,
42 #[serde(deserialize_with = "deserialize_bool_from_int")]
43 pub enable_btrfs: bool,
44 }
45
46 #[derive(Clone, Deserialize)]
47 pub struct IsoInfo {
48 pub release: String,
49 pub isorelease: String,
50 }
51
52 /// Paths in the ISO environment containing installer data.
53 #[derive(Clone, Deserialize)]
54 pub struct IsoLocations {
55 pub iso: PathBuf,
56 }
57
58 #[derive(Clone, Deserialize)]
59 pub struct SetupInfo {
60 #[serde(rename = "product-cfg")]
61 pub config: ProductConfig,
62 #[serde(rename = "iso-info")]
63 pub iso_info: IsoInfo,
64 pub locations: IsoLocations,
65 }
66
67 #[derive(Clone, Deserialize)]
68 pub struct CountryInfo {
69 pub name: String,
70 #[serde(default)]
71 pub zone: String,
72 pub kmap: String,
73 }
74
75 #[derive(Clone, Deserialize, Eq, PartialEq)]
76 pub struct KeyboardMapping {
77 pub name: String,
78 #[serde(rename = "kvm")]
79 pub id: String,
80 #[serde(rename = "x11")]
81 pub xkb_layout: String,
82 #[serde(rename = "x11var")]
83 pub xkb_variant: String,
84 }
85
86 impl cmp::PartialOrd for KeyboardMapping {
87 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
88 self.name.partial_cmp(&other.name)
89 }
90 }
91
92 impl cmp::Ord for KeyboardMapping {
93 fn cmp(&self, other: &Self) -> cmp::Ordering {
94 self.name.cmp(&other.name)
95 }
96 }
97
98 #[derive(Clone, Deserialize)]
99 pub struct LocaleInfo {
100 #[serde(deserialize_with = "deserialize_cczones_map")]
101 pub cczones: HashMap<String, Vec<String>>,
102 #[serde(rename = "country")]
103 pub countries: HashMap<String, CountryInfo>,
104 pub kmap: HashMap<String, KeyboardMapping>,
105 }
106
107 /// Fetches basic information needed for the installer which is required to work
108 pub fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo, RuntimeInfo), String> {
109 let base_path = if in_test_mode { "./testdir" } else { "/" };
110 let mut path = PathBuf::from(base_path);
111
112 path.push("run");
113 path.push("proxmox-installer");
114
115 let installer_info: SetupInfo = {
116 let mut path = path.clone();
117 path.push("iso-info.json");
118
119 read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))?
120 };
121
122 let locale_info = {
123 let mut path = path.clone();
124 path.push("locales.json");
125
126 read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
127 };
128
129 let mut runtime_info: RuntimeInfo = {
130 let mut path = path.clone();
131 path.push("run-env-info.json");
132
133 read_json(&path)
134 .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
135 };
136
137 runtime_info.disks.sort();
138 if runtime_info.disks.is_empty() {
139 Err("The installer could not find any supported hard disks.".to_owned())
140 } else {
141 Ok((installer_info, locale_info, runtime_info))
142 }
143 }
144
145 #[derive(Serialize)]
146 pub struct InstallZfsOption {
147 ashift: usize,
148 #[serde(serialize_with = "serialize_as_display")]
149 compress: ZfsCompressOption,
150 #[serde(serialize_with = "serialize_as_display")]
151 checksum: ZfsChecksumOption,
152 copies: usize,
153 arc_max: usize,
154 }
155
156 impl From<ZfsBootdiskOptions> for InstallZfsOption {
157 fn from(opts: ZfsBootdiskOptions) -> Self {
158 InstallZfsOption {
159 ashift: opts.ashift,
160 compress: opts.compress,
161 checksum: opts.checksum,
162 copies: opts.copies,
163 arc_max: opts.arc_max,
164 }
165 }
166 }
167
168 pub fn read_json<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T, String> {
169 let file = File::open(path).map_err(|err| err.to_string())?;
170 let reader = BufReader::new(file);
171
172 serde_json::from_reader(reader).map_err(|err| format!("failed to parse JSON: {err}"))
173 }
174
175 fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
176 where
177 D: Deserializer<'de>,
178 {
179 let val: u32 = Deserialize::deserialize(deserializer)?;
180 Ok(val != 0)
181 }
182
183 fn deserialize_cczones_map<'de, D>(
184 deserializer: D,
185 ) -> Result<HashMap<String, Vec<String>>, D::Error>
186 where
187 D: Deserializer<'de>,
188 {
189 let map: HashMap<String, HashMap<String, u32>> = Deserialize::deserialize(deserializer)?;
190
191 let mut result = HashMap::new();
192 for (cc, list) in map.into_iter() {
193 result.insert(cc, list.into_keys().collect());
194 }
195
196 Ok(result)
197 }
198
199 fn deserialize_disks_map<'de, D>(deserializer: D) -> Result<Vec<Disk>, D::Error>
200 where
201 D: Deserializer<'de>,
202 {
203 let disks =
204 <Vec<(usize, String, f64, String, Option<usize>, String)>>::deserialize(deserializer)?;
205 Ok(disks
206 .into_iter()
207 .map(
208 |(index, device, size_mb, model, logical_bsize, _syspath)| Disk {
209 index: index.to_string(),
210 // Linux always reports the size of block devices in sectors, where one sector is
211 // defined as being 2^9 = 512 bytes in size.
212 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/blk_types.h?h=v6.4#n30
213 size: (size_mb * 512.) / 1024. / 1024. / 1024.,
214 block_size: logical_bsize,
215 path: device,
216 model: (!model.is_empty()).then_some(model),
217 },
218 )
219 .collect())
220 }
221
222 fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
223 where
224 D: Deserializer<'de>,
225 {
226 #[derive(Deserialize)]
227 struct CidrDescriptor {
228 address: String,
229 prefix: usize,
230 // family is implied anyway by parsing the address
231 }
232
233 let list: Vec<CidrDescriptor> = Deserialize::deserialize(deserializer)?;
234
235 let mut result = Vec::with_capacity(list.len());
236 for desc in list {
237 let ip_addr = desc
238 .address
239 .parse::<IpAddr>()
240 .map_err(|err| de::Error::custom(format!("{:?}", err)))?;
241
242 result.push(
243 CidrAddress::new(ip_addr, desc.prefix)
244 .map_err(|err| de::Error::custom(format!("{:?}", err)))?,
245 );
246 }
247
248 Ok(Some(result))
249 }
250
251 fn serialize_as_display<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
252 where
253 S: Serializer,
254 T: fmt::Display,
255 {
256 serializer.collect_str(value)
257 }
258
259 #[derive(Clone, Deserialize)]
260 pub struct RuntimeInfo {
261 /// Whether is system was booted in (legacy) BIOS or UEFI mode.
262 pub boot_type: BootType,
263
264 /// Detected country if available.
265 pub country: Option<String>,
266
267 /// Maps devices to their information.
268 #[serde(deserialize_with = "deserialize_disks_map")]
269 pub disks: Vec<Disk>,
270
271 /// Network addresses, gateways and DNS info.
272 pub network: NetworkInfo,
273
274 /// Total memory of the system in MiB.
275 pub total_memory: usize,
276
277 /// Whether the CPU supports hardware-accelerated virtualization
278 #[serde(deserialize_with = "deserialize_bool_from_int")]
279 pub hvm_supported: bool,
280 }
281
282 #[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
283 #[serde(rename_all = "lowercase")]
284 pub enum BootType {
285 Bios,
286 Efi,
287 }
288
289 #[derive(Clone, Deserialize)]
290 pub struct NetworkInfo {
291 pub dns: Dns,
292 pub routes: Option<Routes>,
293
294 /// Maps devices to their configuration, if it has a usable configuration.
295 /// (Contains no entries for devices with only link-local addresses.)
296 #[serde(default)]
297 pub interfaces: HashMap<String, Interface>,
298
299 /// The hostname of this machine, if set by the DHCP server.
300 pub hostname: Option<String>,
301 }
302
303 #[derive(Clone, Deserialize)]
304 pub struct Dns {
305 pub domain: Option<String>,
306
307 /// List of stringified IP addresses.
308 #[serde(default)]
309 pub dns: Vec<IpAddr>,
310 }
311
312 #[derive(Clone, Deserialize)]
313 pub struct Routes {
314 /// Ipv4 gateway.
315 pub gateway4: Option<Gateway>,
316
317 /// Ipv6 gateway.
318 pub gateway6: Option<Gateway>,
319 }
320
321 #[derive(Clone, Deserialize)]
322 pub struct Gateway {
323 /// Outgoing network device.
324 pub dev: String,
325
326 /// Stringified gateway IP address.
327 pub gateway: IpAddr,
328 }
329
330 #[derive(Clone, Deserialize)]
331 #[serde(rename_all = "UPPERCASE")]
332 pub enum InterfaceState {
333 Up,
334 Down,
335 #[serde(other)]
336 Unknown,
337 }
338
339 impl InterfaceState {
340 // avoid display trait as this is not the string representation for a serializer
341 pub fn render(&self) -> String {
342 match self {
343 Self::Up => "\u{25CF}",
344 Self::Down | Self::Unknown => " ",
345 }
346 .into()
347 }
348 }
349
350 #[derive(Clone, Deserialize)]
351 pub struct Interface {
352 pub name: String,
353
354 pub index: usize,
355
356 pub mac: String,
357
358 pub state: InterfaceState,
359
360 #[serde(default)]
361 #[serde(deserialize_with = "deserialize_cidr_list")]
362 pub addresses: Option<Vec<CidrAddress>>,
363 }
364
365 impl Interface {
366 // avoid display trait as this is not the string representation for a serializer
367 pub fn render(&self) -> String {
368 format!("{} {}", self.state.render(), self.name)
369 }
370 }
371
372 pub fn spawn_low_level_installer(test_mode: bool) -> io::Result<process::Child> {
373 let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = if test_mode {
374 (
375 "./proxmox-low-level-installer",
376 &["-t", "/dev/null", "start-session-test"],
377 vec![("PERL5LIB", ".")],
378 )
379 } else {
380 ("proxmox-low-level-installer", &["start-session"], vec![])
381 };
382
383 Command::new(path)
384 .args(args)
385 .envs(envs)
386 .stdin(Stdio::piped())
387 .stdout(Stdio::piped())
388 .spawn()
389 }