]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/setup.rs
tui: use EULA path from ISO info instead of hard-coding
[pve-installer.git] / proxmox-tui-installer / src / setup.rs
1 use std::{
2 cmp,
3 collections::HashMap,
4 fmt,
5 fs::File,
6 io::BufReader,
7 net::IpAddr,
8 path::{Path, PathBuf},
9 };
10
11 use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
12
13 use crate::{
14 options::{
15 AdvancedBootdiskOptions, BtrfsRaidLevel, Disk, FsType, InstallerOptions,
16 ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
17 },
18 utils::{CidrAddress, Fqdn},
19 };
20
21 #[allow(clippy::upper_case_acronyms)]
22 #[derive(Clone, Copy, Deserialize, PartialEq)]
23 #[serde(rename_all = "lowercase")]
24 pub enum ProxmoxProduct {
25 PVE,
26 PBS,
27 PMG,
28 }
29
30 impl ProxmoxProduct {
31 pub fn default_hostname(self) -> &'static str {
32 match self {
33 Self::PVE => "pve",
34 Self::PMG => "pmg",
35 Self::PBS => "pbs",
36 }
37 }
38 }
39
40 #[derive(Clone, Deserialize)]
41 pub struct ProductConfig {
42 pub fullname: String,
43 pub product: ProxmoxProduct,
44 #[serde(deserialize_with = "deserialize_bool_from_int")]
45 pub enable_btrfs: bool,
46 }
47
48 #[derive(Clone, Deserialize)]
49 pub struct IsoInfo {
50 pub release: String,
51 pub isorelease: String,
52 }
53
54 /// Paths in the ISO environment containing installer data.
55 #[derive(Clone, Deserialize)]
56 pub struct IsoLocations {
57 pub iso: PathBuf,
58 }
59
60 #[derive(Clone, Deserialize)]
61 pub struct SetupInfo {
62 #[serde(rename = "product-cfg")]
63 pub config: ProductConfig,
64 #[serde(rename = "iso-info")]
65 pub iso_info: IsoInfo,
66 pub locations: IsoLocations,
67 }
68
69 #[derive(Clone, Deserialize)]
70 pub struct CountryInfo {
71 pub name: String,
72 #[serde(default)]
73 pub zone: String,
74 pub kmap: String,
75 }
76
77 #[derive(Clone, Deserialize, Eq, PartialEq)]
78 pub struct KeyboardMapping {
79 pub name: String,
80 #[serde(rename = "kvm")]
81 pub id: String,
82 #[serde(rename = "x11")]
83 pub xkb_layout: String,
84 #[serde(rename = "x11var")]
85 pub xkb_variant: String,
86 }
87
88 impl cmp::PartialOrd for KeyboardMapping {
89 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
90 self.name.partial_cmp(&other.name)
91 }
92 }
93
94 impl cmp::Ord for KeyboardMapping {
95 fn cmp(&self, other: &Self) -> cmp::Ordering {
96 self.name.cmp(&other.name)
97 }
98 }
99
100 #[derive(Clone, Deserialize)]
101 pub struct LocaleInfo {
102 #[serde(deserialize_with = "deserialize_cczones_map")]
103 pub cczones: HashMap<String, Vec<String>>,
104 #[serde(rename = "country")]
105 pub countries: HashMap<String, CountryInfo>,
106 pub kmap: HashMap<String, KeyboardMapping>,
107 }
108
109 #[derive(Serialize)]
110 struct InstallZfsOption {
111 ashift: usize,
112 #[serde(serialize_with = "serialize_as_display")]
113 compress: ZfsCompressOption,
114 #[serde(serialize_with = "serialize_as_display")]
115 checksum: ZfsChecksumOption,
116 copies: usize,
117 }
118
119 impl From<ZfsBootdiskOptions> for InstallZfsOption {
120 fn from(opts: ZfsBootdiskOptions) -> Self {
121 InstallZfsOption {
122 ashift: opts.ashift,
123 compress: opts.compress,
124 checksum: opts.checksum,
125 copies: opts.copies,
126 }
127 }
128 }
129
130 /// See Proxmox::Install::Config
131 #[derive(Serialize)]
132 pub struct InstallConfig {
133 autoreboot: usize,
134
135 #[serde(serialize_with = "serialize_fstype")]
136 filesys: FsType,
137 hdsize: f64,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 swapsize: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 maxroot: Option<f64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 minfree: Option<f64>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 maxvz: Option<f64>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
148 zfs_opts: Option<InstallZfsOption>,
149
150 #[serde(
151 serialize_with = "serialize_disk_opt",
152 skip_serializing_if = "Option::is_none"
153 )]
154 target_hd: Option<Disk>,
155 #[serde(skip_serializing_if = "HashMap::is_empty")]
156 disk_selection: HashMap<String, String>,
157
158 country: String,
159 timezone: String,
160 keymap: String,
161
162 password: String,
163 mailto: String,
164
165 mngmt_nic: String,
166
167 hostname: String,
168 domain: String,
169 #[serde(serialize_with = "serialize_as_display")]
170 cidr: CidrAddress,
171 gateway: IpAddr,
172 dns: IpAddr,
173 }
174
175 impl From<InstallerOptions> for InstallConfig {
176 fn from(options: InstallerOptions) -> Self {
177 let mut config = Self {
178 autoreboot: options.autoreboot as usize,
179
180 filesys: options.bootdisk.fstype,
181 hdsize: 0.,
182 swapsize: None,
183 maxroot: None,
184 minfree: None,
185 maxvz: None,
186 zfs_opts: None,
187 target_hd: None,
188 disk_selection: HashMap::new(),
189
190 country: options.timezone.country,
191 timezone: options.timezone.timezone,
192 keymap: options.timezone.kb_layout,
193
194 password: options.password.root_password,
195 mailto: options.password.email,
196
197 mngmt_nic: options.network.ifname,
198
199 hostname: options
200 .network
201 .fqdn
202 .host()
203 .unwrap_or_else(|| crate::current_product().default_hostname())
204 .to_owned(),
205 domain: options.network.fqdn.domain(),
206 cidr: options.network.address,
207 gateway: options.network.gateway,
208 dns: options.network.dns_server,
209 };
210
211 match &options.bootdisk.advanced {
212 AdvancedBootdiskOptions::Lvm(lvm) => {
213 config.hdsize = lvm.total_size;
214 config.target_hd = Some(options.bootdisk.disks[0].clone());
215 config.swapsize = lvm.swap_size;
216 config.maxroot = lvm.max_root_size;
217 config.minfree = lvm.min_lvm_free;
218 config.maxvz = lvm.max_data_size;
219 }
220 AdvancedBootdiskOptions::Zfs(zfs) => {
221 config.hdsize = zfs.disk_size;
222 config.zfs_opts = Some(zfs.clone().into());
223
224 for (i, disk) in options.bootdisk.disks.iter().enumerate() {
225 config
226 .disk_selection
227 .insert(i.to_string(), disk.index.clone());
228 }
229 }
230 AdvancedBootdiskOptions::Btrfs(btrfs) => {
231 config.hdsize = btrfs.disk_size;
232
233 for (i, disk) in options.bootdisk.disks.iter().enumerate() {
234 config
235 .disk_selection
236 .insert(i.to_string(), disk.index.clone());
237 }
238 }
239 }
240
241 config
242 }
243 }
244
245 pub fn read_json<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T, String> {
246 let file = File::open(path).map_err(|err| err.to_string())?;
247 let reader = BufReader::new(file);
248
249 serde_json::from_reader(reader).map_err(|err| format!("failed to parse JSON: {err}"))
250 }
251
252 fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
253 where
254 D: Deserializer<'de>,
255 {
256 let val: u32 = Deserialize::deserialize(deserializer)?;
257 Ok(val != 0)
258 }
259
260 fn deserialize_cczones_map<'de, D>(
261 deserializer: D,
262 ) -> Result<HashMap<String, Vec<String>>, D::Error>
263 where
264 D: Deserializer<'de>,
265 {
266 let map: HashMap<String, HashMap<String, u32>> = Deserialize::deserialize(deserializer)?;
267
268 let mut result = HashMap::new();
269 for (cc, list) in map.into_iter() {
270 result.insert(cc, list.into_keys().collect());
271 }
272
273 Ok(result)
274 }
275
276 fn deserialize_disks_map<'de, D>(deserializer: D) -> Result<Vec<Disk>, D::Error>
277 where
278 D: Deserializer<'de>,
279 {
280 let disks = <Vec<(usize, String, f64, String, f64, String)>>::deserialize(deserializer)?;
281 Ok(disks
282 .into_iter()
283 .map(
284 |(index, device, size_mb, model, logical_bsize, _syspath)| Disk {
285 index: index.to_string(),
286 size: (size_mb * logical_bsize) / 1024. / 1024. / 1024.,
287 path: device,
288 model: (!model.is_empty()).then_some(model),
289 },
290 )
291 .collect())
292 }
293
294 fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
295 where
296 D: Deserializer<'de>,
297 {
298 #[derive(Deserialize)]
299 struct CidrDescriptor {
300 address: String,
301 prefix: usize,
302 // family is implied anyway by parsing the address
303 }
304
305 let list: Vec<CidrDescriptor> = Deserialize::deserialize(deserializer)?;
306
307 let mut result = Vec::with_capacity(list.len());
308 for desc in list {
309 let ip_addr = desc
310 .address
311 .parse::<IpAddr>()
312 .map_err(|err| de::Error::custom(format!("{:?}", err)))?;
313
314 result.push(
315 CidrAddress::new(ip_addr, desc.prefix)
316 .map_err(|err| de::Error::custom(format!("{:?}", err)))?,
317 );
318 }
319
320 Ok(Some(result))
321 }
322
323 fn serialize_disk_opt<S>(value: &Option<Disk>, serializer: S) -> Result<S::Ok, S::Error>
324 where
325 S: Serializer,
326 {
327 if let Some(disk) = value {
328 serializer.serialize_str(&disk.path)
329 } else {
330 serializer.serialize_none()
331 }
332 }
333
334 fn serialize_fstype<S>(value: &FsType, serializer: S) -> Result<S::Ok, S::Error>
335 where
336 S: Serializer,
337 {
338 use FsType::*;
339 let value = match value {
340 // proxinstall::$fssetup
341 Ext4 => "ext4",
342 Xfs => "xfs",
343 // proxinstall::get_zfs_raid_setup()
344 Zfs(ZfsRaidLevel::Raid0) => "zfs (RAID0)",
345 Zfs(ZfsRaidLevel::Raid1) => "zfs (RAID1)",
346 Zfs(ZfsRaidLevel::Raid10) => "zfs (RAID10)",
347 Zfs(ZfsRaidLevel::RaidZ) => "zfs (RAIDZ-1)",
348 Zfs(ZfsRaidLevel::RaidZ2) => "zfs (RAIDZ-2)",
349 Zfs(ZfsRaidLevel::RaidZ3) => "zfs (RAIDZ-3)",
350 // proxinstall::get_btrfs_raid_setup()
351 Btrfs(BtrfsRaidLevel::Raid0) => "btrfs (RAID0)",
352 Btrfs(BtrfsRaidLevel::Raid1) => "btrfs (RAID1)",
353 Btrfs(BtrfsRaidLevel::Raid10) => "btrfs (RAID10)",
354 };
355
356 serializer.collect_str(value)
357 }
358
359 fn serialize_as_display<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
360 where
361 S: Serializer,
362 T: fmt::Display,
363 {
364 serializer.collect_str(value)
365 }
366
367 #[derive(Clone, Deserialize)]
368 pub struct RuntimeInfo {
369 /// Detected country if available.
370 pub country: Option<String>,
371
372 /// Maps devices to their information.
373 #[serde(deserialize_with = "deserialize_disks_map")]
374 pub disks: Vec<Disk>,
375
376 /// Network addresses, gateways and DNS info.
377 pub network: NetworkInfo,
378
379 /// Total memory of the system in MiB.
380 pub total_memory: usize,
381
382 /// Whether the CPU supports hardware-accelerated virtualization
383 #[serde(deserialize_with = "deserialize_bool_from_int")]
384 pub hvm_supported: bool,
385 }
386
387 #[derive(Clone, Deserialize)]
388 pub struct NetworkInfo {
389 pub dns: Dns,
390 pub routes: Option<Routes>,
391
392 /// Maps devices to their configuration, if it has a usable configuration.
393 /// (Contains no entries for devices with only link-local addresses.)
394 #[serde(default)]
395 pub interfaces: HashMap<String, Interface>,
396 }
397
398 #[derive(Clone, Deserialize)]
399 pub struct Dns {
400 #[serde(deserialize_with = "deserialize_invalid_value_as_none")]
401 pub domain: Option<Fqdn>,
402
403 /// List of stringified IP addresses.
404 #[serde(default)]
405 pub dns: Vec<IpAddr>,
406 }
407
408 #[derive(Clone, Deserialize)]
409 pub struct Routes {
410 /// Ipv4 gateway.
411 pub gateway4: Option<Gateway>,
412
413 /// Ipv6 gateway.
414 pub gateway6: Option<Gateway>,
415 }
416
417 #[derive(Clone, Deserialize)]
418 pub struct Gateway {
419 /// Outgoing network device.
420 pub dev: String,
421
422 /// Stringified gateway IP address.
423 pub gateway: IpAddr,
424 }
425
426 #[derive(Clone, Deserialize)]
427 pub struct Interface {
428 pub name: String,
429
430 pub index: usize,
431
432 pub mac: String,
433
434 #[serde(default)]
435 #[serde(deserialize_with = "deserialize_cidr_list")]
436 pub addresses: Option<Vec<CidrAddress>>,
437 }
438
439 fn deserialize_invalid_value_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
440 where
441 D: Deserializer<'de>,
442 T: Deserialize<'de>,
443 {
444 Ok(Deserialize::deserialize(deserializer).ok())
445 }