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 AutoInstModes
{
79 #[derive(Deserialize, Serialize, Debug)]
80 #[serde(rename_all = "lowercase", deny_unknown_fields)]
81 pub struct AutoInstSettings
{
82 pub mode
: AutoInstModes
,
83 pub http_url
: Option
<String
>,
84 pub cert_fingerprint
: Option
<String
>,
87 #[derive(Deserialize, Debug)]
88 struct IpLinksUdevInfo
{
92 /// Returns vec of usable NICs
93 pub fn get_nic_list() -> Result
<Vec
<String
>> {
94 let ip_output
= Command
::new("/usr/sbin/ip")
98 let parsed_links
: Vec
<IpLinksUdevInfo
> = serde_json
::from_slice(&ip_output
.stdout
)?
;
99 let mut links
: Vec
<String
> = Vec
::new();
101 for link
in parsed_links
{
102 if link
.ifname
== *"lo" {
105 links
.push(link
.ifname
);
111 pub fn get_matched_udev_indexes(
112 filter
: &BTreeMap
<String
, String
>,
113 udev_list
: &BTreeMap
<String
, BTreeMap
<String
, String
>>,
115 ) -> Result
<Vec
<String
>> {
116 let mut matches
= vec
![];
117 for (dev
, dev_values
) in udev_list
{
118 let mut did_match_once
= false;
119 let mut did_match_all
= true;
120 for (filter_key
, filter_value
) in filter
{
122 Pattern
::new(filter_value
).context("invalid glob in disk selection")?
;
123 for (udev_key
, udev_value
) in dev_values
{
124 if udev_key
== filter_key
&& filter_pattern
.matches(udev_value
) {
125 did_match_once
= true;
126 } else if udev_key
== filter_key
{
127 did_match_all
= false;
131 if (match_all
&& did_match_all
) || (!match_all
&& did_match_once
) {
132 matches
.push(dev
.clone());
135 if matches
.is_empty() {
136 bail
!("filter did not match any devices");
144 udev_info
: &UdevInfo
,
145 runtime_info
: &RuntimeInfo
,
146 config
: &mut InstallConfig
,
148 match config
.filesys
{
149 FsType
::Ext4
| FsType
::Xfs
=> set_single_disk(answer
, udev_info
, runtime_info
, config
),
150 FsType
::Zfs(_
) | FsType
::Btrfs(_
) => {
151 set_selected_disks(answer
, udev_info
, runtime_info
, config
)
158 udev_info
: &UdevInfo
,
159 runtime_info
: &RuntimeInfo
,
160 config
: &mut InstallConfig
,
162 match &answer
.disks
.disk_selection
{
163 answer
::DiskSelection
::Selection(disk_list
) => {
164 let disk_name
= disk_list
[0].clone();
165 let disk
= runtime_info
168 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
170 Some(disk
) => config
.target_hd
= Some(disk
.clone()),
171 None
=> bail
!("disk in 'disk_selection' not found"),
174 answer
::DiskSelection
::Filter(filter
) => {
175 let disk_index
= get_single_udev_index(filter
, &udev_info
.disks
)?
;
176 let disk
= runtime_info
179 .find(|item
| item
.index
== disk_index
);
180 config
.target_hd
= disk
.cloned();
183 info
!("Selected disk: {}", config
.target_hd
.clone().unwrap().path
);
187 fn set_selected_disks(
189 udev_info
: &UdevInfo
,
190 runtime_info
: &RuntimeInfo
,
191 config
: &mut InstallConfig
,
193 match &answer
.disks
.disk_selection
{
194 answer
::DiskSelection
::Selection(disk_list
) => {
195 info
!("Disk selection found");
196 for disk_name
in disk_list
.clone() {
197 let disk
= runtime_info
200 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
201 if let Some(disk
) = disk
{
204 .insert(disk
.index
.clone(), disk
.index
.clone());
208 answer
::DiskSelection
::Filter(filter
) => {
209 info
!("No disk list found, looking for disk filters");
210 let filter_match
= answer
214 .unwrap_or(answer
::FilterMatch
::Any
);
215 let selected_disk_indexes
= get_matched_udev_indexes(
218 filter_match
== answer
::FilterMatch
::All
,
221 for i
in selected_disk_indexes
.into_iter() {
222 let disk
= runtime_info
225 .find(|item
| item
.index
== i
)
229 .insert(disk
.index
.clone(), disk
.index
.clone());
233 if config
.disk_selection
.is_empty() {
234 bail
!("No disks found matching selection.");
237 let mut selected_disks
: Vec
<String
> = Vec
::new();
238 for i
in config
.disk_selection
.keys() {
243 .find(|item
| item
.index
.as_str() == i
)
250 "Selected disks: {}",
253 .map(|x
| x
.to_string() + " ")
260 pub fn get_first_selected_disk(config
: &InstallConfig
) -> usize {
265 .expect("no disks found")
268 .expect("could not parse key to usize")
271 pub fn verify_locale_settings(answer
: &Answer
, locales
: &LocaleInfo
) -> Result
<()> {
272 info
!("Verifying locale settings");
276 .any(|i
| i
== &answer
.global
.country
)
278 bail
!("country code '{}' is not valid", &answer
.global
.country
);
280 if !locales
.kmap
.keys().any(|i
| i
== &answer
.global
.keyboard
) {
281 bail
!("keyboard layout '{}' is not valid", &answer
.global
.keyboard
);
286 .any(|(_
, zones
)| zones
.contains(&answer
.global
.timezone
))
288 bail
!("timezone '{}' is not valid", &answer
.global
.timezone
);
293 pub fn run_cmds(step
: &str, in_chroot
: bool
, cmds
: &[&str]) {
295 debug
!("Running commands for '{step}':");
299 Ok
::<(), anyhow
::Error
>(())
303 if let Err(err
) = run_cmd("proxmox-chroot prepare") {
304 error
!("Failed to setup chroot for '{step}': {err}");
309 if let Err(err
) = run() {
310 error
!("Running commands for '{step}' failed: {err:?}");
312 debug
!("Running commands in chroot for '{step}' finished");
316 if let Err(err
) = run_cmd("proxmox-chroot cleanup") {
317 error
!("Failed to clean up chroot for '{step}': {err}");
322 fn run_cmd(cmd
: &str) -> Result
<()> {
323 debug
!("Command '{cmd}':");
324 let child
= match Command
::new("/bin/bash")
327 .stdout(Stdio
::piped())
328 .stderr(Stdio
::piped())
332 Err(err
) => bail
!("error running command {cmd}: {err}"),
334 match child
.wait_with_output() {
336 if output
.status
.success() {
337 debug
!("{}", String
::from_utf8(output
.stdout
).unwrap());
339 bail
!("{}", String
::from_utf8(output
.stderr
).unwrap());
342 Err(err
) => bail
!("{err}"),
350 udev_info
: &UdevInfo
,
351 runtime_info
: &RuntimeInfo
,
352 locales
: &LocaleInfo
,
353 setup_info
: &SetupInfo
,
354 ) -> Result
<InstallConfig
> {
355 info
!("Parsing answer file");
356 info
!("Setting File system");
357 let filesystem
= answer
.disks
.fs_type
;
358 info
!("File system selected: {}", filesystem
);
360 let network_settings
= get_network_settings(answer
, udev_info
, runtime_info
, setup_info
)?
;
362 verify_locale_settings(answer
, locales
)?
;
364 let mut config
= InstallConfig
{
374 disk_selection
: BTreeMap
::new(),
377 country
: answer
.global
.country
.clone(),
378 timezone
: answer
.global
.timezone
.clone(),
379 keymap
: answer
.global
.keyboard
.clone(),
381 password
: answer
.global
.root_password
.clone(),
382 mailto
: answer
.global
.mailto
.clone(),
384 mngmt_nic
: network_settings
.ifname
,
386 hostname
: network_settings
.fqdn
.host().unwrap().to_string(),
387 domain
: network_settings
.fqdn
.domain(),
388 cidr
: network_settings
.address
,
389 gateway
: network_settings
.gateway
,
390 dns
: network_settings
.dns_server
,
393 set_disks(answer
, udev_info
, runtime_info
, &mut config
)?
;
394 match &answer
.disks
.fs_options
{
395 answer
::FsOptions
::LVM(lvm
) => {
396 config
.hdsize
= lvm
.hdsize
.unwrap_or(config
.target_hd
.clone().unwrap().size
);
397 config
.swapsize
= lvm
.swapsize
;
398 config
.maxroot
= lvm
.maxroot
;
399 config
.maxvz
= lvm
.maxvz
;
400 config
.minfree
= lvm
.minfree
;
402 answer
::FsOptions
::ZFS(zfs
) => {
403 let first_selected_disk
= get_first_selected_disk(&config
);
407 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
408 config
.zfs_opts
= Some(InstallZfsOption
{
409 ashift
: zfs
.ashift
.unwrap_or(12),
410 arc_max
: zfs
.arc_max
.unwrap_or(2048),
411 compress
: zfs
.compress
.unwrap_or(ZfsCompressOption
::On
),
412 checksum
: zfs
.checksum
.unwrap_or(ZfsChecksumOption
::On
),
413 copies
: zfs
.copies
.unwrap_or(1),
416 answer
::FsOptions
::BTRFS(btrfs
) => {
417 let first_selected_disk
= get_first_selected_disk(&config
);
419 config
.hdsize
= btrfs
421 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
425 // never print the auto reboot text after finishing to avoid the delay, as this is handled by
426 // the auto-installer itself anyway. The auto-installer might still perform some post-install
427 // steps after running the low-level installer.
428 config
.autoreboot
= 0;
432 #[derive(Clone, Debug, Deserialize, PartialEq)]
433 #[serde(tag = "type", rename_all = "lowercase")]
434 pub enum LowLevelMessage
{
435 #[serde(rename = "message")]