1 use anyhow
::{bail, Context as _, Result}
;
4 use log
::{debug, error, info}
;
7 process
::{Command, Stdio}
,
11 answer
::{self, Answer}
,
14 use proxmox_installer_common
::{
15 options
::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption}
,
16 setup
::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo}
,
18 use serde
::{Deserialize, Serialize}
;
20 pub fn get_network_settings(
23 runtime_info
: &RuntimeInfo
,
24 setup_info
: &SetupInfo
,
25 ) -> Result
<NetworkOptions
> {
26 let mut network_options
= NetworkOptions
::defaults_from(setup_info
, &runtime_info
.network
);
28 info
!("Setting network configuration");
30 // Always use the FQDN from the answer file
31 network_options
.fqdn
= answer
.global
.fqdn
.clone();
33 if let answer
::NetworkSettings
::Manual(settings
) = &answer
.network
.network_settings
{
34 network_options
.address
= settings
.cidr
.clone();
35 network_options
.dns_server
= settings
.dns
;
36 network_options
.gateway
= settings
.gateway
;
37 network_options
.ifname
= get_single_udev_index(&settings
.filter
, &udev_info
.nics
)?
;
39 info
!("Network interface used is '{}'", &network_options
.ifname
);
43 pub fn get_single_udev_index(
44 filter
: &BTreeMap
<String
, String
>,
45 udev_list
: &BTreeMap
<String
, BTreeMap
<String
, String
>>,
47 if filter
.is_empty() {
48 bail
!("no filter defined");
50 let mut dev_index
: Option
<String
> = None
;
51 'outer
: for (dev
, dev_values
) in udev_list
{
52 for (filter_key
, filter_value
) in filter
{
54 Pattern
::new(filter_value
).context("invalid glob in disk selection")?
;
55 for (udev_key
, udev_value
) in dev_values
{
56 if udev_key
== filter_key
&& filter_pattern
.matches(udev_value
) {
57 dev_index
= Some(dev
.clone());
58 break 'outer
; // take first match
63 if dev_index
.is_none() {
64 bail
!("filter did not match any device");
67 Ok(dev_index
.unwrap())
70 #[derive(Deserialize, Serialize, Debug, Clone, ValueEnum, PartialEq)]
71 #[serde(rename_all = "lowercase", deny_unknown_fields)]
72 pub enum AutoInstMode
{
78 #[derive(Deserialize, Serialize, Debug)]
79 #[serde(rename_all = "lowercase", deny_unknown_fields)]
80 pub struct AutoInstSettings
{
81 pub mode
: AutoInstMode
,
82 pub http_url
: Option
<String
>,
83 pub cert_fingerprint
: Option
<String
>,
86 #[derive(Deserialize, Debug)]
87 struct IpLinksUdevInfo
{
91 /// Returns vec of usable NICs
92 pub fn get_nic_list() -> Result
<Vec
<String
>> {
93 let ip_output
= Command
::new("/usr/sbin/ip")
97 let parsed_links
: Vec
<IpLinksUdevInfo
> = serde_json
::from_slice(&ip_output
.stdout
)?
;
98 let mut links
: Vec
<String
> = Vec
::new();
100 for link
in parsed_links
{
101 if link
.ifname
== *"lo" {
104 links
.push(link
.ifname
);
110 pub fn get_matched_udev_indexes(
111 filter
: &BTreeMap
<String
, String
>,
112 udev_list
: &BTreeMap
<String
, BTreeMap
<String
, String
>>,
114 ) -> Result
<Vec
<String
>> {
115 let mut matches
= vec
![];
116 for (dev
, dev_values
) in udev_list
{
117 let mut did_match_once
= false;
118 let mut did_match_all
= true;
119 for (filter_key
, filter_value
) in filter
{
121 Pattern
::new(filter_value
).context("invalid glob in disk selection")?
;
122 for (udev_key
, udev_value
) in dev_values
{
123 if udev_key
== filter_key
&& filter_pattern
.matches(udev_value
) {
124 did_match_once
= true;
125 } else if udev_key
== filter_key
{
126 did_match_all
= false;
130 if (match_all
&& did_match_all
) || (!match_all
&& did_match_once
) {
131 matches
.push(dev
.clone());
134 if matches
.is_empty() {
135 bail
!("filter did not match any devices");
143 udev_info
: &UdevInfo
,
144 runtime_info
: &RuntimeInfo
,
145 config
: &mut InstallConfig
,
147 match config
.filesys
{
148 FsType
::Ext4
| FsType
::Xfs
=> set_single_disk(answer
, udev_info
, runtime_info
, config
),
149 FsType
::Zfs(_
) | FsType
::Btrfs(_
) => {
150 set_selected_disks(answer
, udev_info
, runtime_info
, config
)
157 udev_info
: &UdevInfo
,
158 runtime_info
: &RuntimeInfo
,
159 config
: &mut InstallConfig
,
161 match &answer
.disks
.disk_selection
{
162 answer
::DiskSelection
::Selection(disk_list
) => {
163 let disk_name
= disk_list
[0].clone();
164 let disk
= runtime_info
167 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
169 Some(disk
) => config
.target_hd
= Some(disk
.clone()),
170 None
=> bail
!("disk in 'disk_selection' not found"),
173 answer
::DiskSelection
::Filter(filter
) => {
174 let disk_index
= get_single_udev_index(filter
, &udev_info
.disks
)?
;
175 let disk
= runtime_info
178 .find(|item
| item
.index
== disk_index
);
179 config
.target_hd
= disk
.cloned();
182 info
!("Selected disk: {}", config
.target_hd
.clone().unwrap().path
);
186 fn set_selected_disks(
188 udev_info
: &UdevInfo
,
189 runtime_info
: &RuntimeInfo
,
190 config
: &mut InstallConfig
,
192 match &answer
.disks
.disk_selection
{
193 answer
::DiskSelection
::Selection(disk_list
) => {
194 info
!("Disk selection found");
195 for disk_name
in disk_list
.clone() {
196 let disk
= runtime_info
199 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
200 if let Some(disk
) = disk
{
203 .insert(disk
.index
.clone(), disk
.index
.clone());
207 answer
::DiskSelection
::Filter(filter
) => {
208 info
!("No disk list found, looking for disk filters");
209 let filter_match
= answer
213 .unwrap_or(answer
::FilterMatch
::Any
);
214 let selected_disk_indexes
= get_matched_udev_indexes(
217 filter_match
== answer
::FilterMatch
::All
,
220 for i
in selected_disk_indexes
.into_iter() {
221 let disk
= runtime_info
224 .find(|item
| item
.index
== i
)
228 .insert(disk
.index
.clone(), disk
.index
.clone());
232 if config
.disk_selection
.is_empty() {
233 bail
!("No disks found matching selection.");
236 let mut selected_disks
: Vec
<String
> = Vec
::new();
237 for i
in config
.disk_selection
.keys() {
242 .find(|item
| item
.index
.as_str() == i
)
249 "Selected disks: {}",
252 .map(|x
| x
.to_string() + " ")
259 pub fn get_first_selected_disk(config
: &InstallConfig
) -> usize {
264 .expect("no disks found")
267 .expect("could not parse key to usize")
270 pub fn verify_locale_settings(answer
: &Answer
, locales
: &LocaleInfo
) -> Result
<()> {
271 info
!("Verifying locale settings");
275 .any(|i
| i
== &answer
.global
.country
)
277 bail
!("country code '{}' is not valid", &answer
.global
.country
);
279 if !locales
.kmap
.keys().any(|i
| i
== &answer
.global
.keyboard
) {
280 bail
!("keyboard layout '{}' is not valid", &answer
.global
.keyboard
);
286 .any(|(_
, zones
)| zones
.contains(&answer
.global
.timezone
))
287 && answer
.global
.timezone
!= "UTC"
289 bail
!("timezone '{}' is not valid", &answer
.global
.timezone
);
295 pub fn run_cmds(step
: &str, in_chroot
: bool
, cmds
: &[&str]) {
297 debug
!("Running commands for '{step}':");
301 Ok
::<(), anyhow
::Error
>(())
305 if let Err(err
) = run_cmd("proxmox-chroot prepare") {
306 error
!("Failed to setup chroot for '{step}': {err}");
311 if let Err(err
) = run() {
312 error
!("Running commands for '{step}' failed: {err:?}");
314 debug
!("Running commands in chroot for '{step}' finished");
318 if let Err(err
) = run_cmd("proxmox-chroot cleanup") {
319 error
!("Failed to clean up chroot for '{step}': {err}");
324 fn run_cmd(cmd
: &str) -> Result
<()> {
325 debug
!("Command '{cmd}':");
326 let child
= match Command
::new("/bin/bash")
329 .stdout(Stdio
::piped())
330 .stderr(Stdio
::piped())
334 Err(err
) => bail
!("error running command {cmd}: {err}"),
336 match child
.wait_with_output() {
338 if output
.status
.success() {
339 debug
!("{}", String
::from_utf8(output
.stdout
).unwrap());
341 bail
!("{}", String
::from_utf8(output
.stderr
).unwrap());
344 Err(err
) => bail
!("{err}"),
352 udev_info
: &UdevInfo
,
353 runtime_info
: &RuntimeInfo
,
354 locales
: &LocaleInfo
,
355 setup_info
: &SetupInfo
,
356 ) -> Result
<InstallConfig
> {
357 info
!("Parsing answer file");
358 info
!("Setting File system");
359 let filesystem
= answer
.disks
.fs_type
;
360 info
!("File system selected: {}", filesystem
);
362 let network_settings
= get_network_settings(answer
, udev_info
, runtime_info
, setup_info
)?
;
364 verify_locale_settings(answer
, locales
)?
;
366 let mut config
= InstallConfig
{
376 disk_selection
: BTreeMap
::new(),
379 country
: answer
.global
.country
.clone(),
380 timezone
: answer
.global
.timezone
.clone(),
381 keymap
: answer
.global
.keyboard
.clone(),
383 password
: answer
.global
.root_password
.clone(),
384 mailto
: answer
.global
.mailto
.clone(),
386 mngmt_nic
: network_settings
.ifname
,
388 hostname
: network_settings
.fqdn
.host().unwrap().to_string(),
389 domain
: network_settings
.fqdn
.domain(),
390 cidr
: network_settings
.address
,
391 gateway
: network_settings
.gateway
,
392 dns
: network_settings
.dns_server
,
395 set_disks(answer
, udev_info
, runtime_info
, &mut config
)?
;
396 match &answer
.disks
.fs_options
{
397 answer
::FsOptions
::LVM(lvm
) => {
398 config
.hdsize
= lvm
.hdsize
.unwrap_or(config
.target_hd
.clone().unwrap().size
);
399 config
.swapsize
= lvm
.swapsize
;
400 config
.maxroot
= lvm
.maxroot
;
401 config
.maxvz
= lvm
.maxvz
;
402 config
.minfree
= lvm
.minfree
;
404 answer
::FsOptions
::ZFS(zfs
) => {
405 let first_selected_disk
= get_first_selected_disk(&config
);
409 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
410 config
.zfs_opts
= Some(InstallZfsOption
{
411 ashift
: zfs
.ashift
.unwrap_or(12),
412 arc_max
: zfs
.arc_max
.unwrap_or(2048),
413 compress
: zfs
.compress
.unwrap_or(ZfsCompressOption
::On
),
414 checksum
: zfs
.checksum
.unwrap_or(ZfsChecksumOption
::On
),
415 copies
: zfs
.copies
.unwrap_or(1),
418 answer
::FsOptions
::BTRFS(btrfs
) => {
419 let first_selected_disk
= get_first_selected_disk(&config
);
421 config
.hdsize
= btrfs
423 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
427 // never print the auto reboot text after finishing to avoid the delay, as this is handled by
428 // the auto-installer itself anyway. The auto-installer might still perform some post-install
429 // steps after running the low-level installer.
430 config
.autoreboot
= 0;
434 #[derive(Clone, Debug, Deserialize, PartialEq)]
435 #[serde(tag = "type", rename_all = "lowercase")]
436 pub enum LowLevelMessage
{
437 #[serde(rename = "message")]