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, Clone, Default, PartialEq, Debug)]
79 pub struct HttpOptions
{
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub url
: Option
<String
>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub cert_fingerprint
: Option
<String
>,
86 #[derive(Deserialize, Serialize, Debug)]
87 #[serde(rename_all = "lowercase", deny_unknown_fields)]
88 pub struct AutoInstSettings
{
89 pub mode
: AutoInstMode
,
91 pub http
: HttpOptions
,
94 #[derive(Deserialize, Debug)]
95 struct IpLinksUdevInfo
{
99 /// Returns vec of usable NICs
100 pub fn get_nic_list() -> Result
<Vec
<String
>> {
101 let ip_output
= Command
::new("/usr/sbin/ip")
105 let parsed_links
: Vec
<IpLinksUdevInfo
> = serde_json
::from_slice(&ip_output
.stdout
)?
;
106 let mut links
: Vec
<String
> = Vec
::new();
108 for link
in parsed_links
{
109 if link
.ifname
== *"lo" {
112 links
.push(link
.ifname
);
118 pub fn get_matched_udev_indexes(
119 filter
: &BTreeMap
<String
, String
>,
120 udev_list
: &BTreeMap
<String
, BTreeMap
<String
, String
>>,
122 ) -> Result
<Vec
<String
>> {
123 let mut matches
= vec
![];
124 for (dev
, dev_values
) in udev_list
{
125 let mut did_match_once
= false;
126 let mut did_match_all
= true;
127 for (filter_key
, filter_value
) in filter
{
129 Pattern
::new(filter_value
).context("invalid glob in disk selection")?
;
130 for (udev_key
, udev_value
) in dev_values
{
131 if udev_key
== filter_key
&& filter_pattern
.matches(udev_value
) {
132 did_match_once
= true;
133 } else if udev_key
== filter_key
{
134 did_match_all
= false;
138 if (match_all
&& did_match_all
) || (!match_all
&& did_match_once
) {
139 matches
.push(dev
.clone());
142 if matches
.is_empty() {
143 bail
!("filter did not match any devices");
151 udev_info
: &UdevInfo
,
152 runtime_info
: &RuntimeInfo
,
153 config
: &mut InstallConfig
,
155 match config
.filesys
{
156 FsType
::Ext4
| FsType
::Xfs
=> set_single_disk(answer
, udev_info
, runtime_info
, config
),
157 FsType
::Zfs(_
) | FsType
::Btrfs(_
) => {
158 set_selected_disks(answer
, udev_info
, runtime_info
, config
)
165 udev_info
: &UdevInfo
,
166 runtime_info
: &RuntimeInfo
,
167 config
: &mut InstallConfig
,
169 match &answer
.disks
.disk_selection
{
170 answer
::DiskSelection
::Selection(disk_list
) => {
171 let disk_name
= disk_list
[0].clone();
172 let disk
= runtime_info
175 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
177 Some(disk
) => config
.target_hd
= Some(disk
.clone()),
178 None
=> bail
!("disk in 'disk_selection' not found"),
181 answer
::DiskSelection
::Filter(filter
) => {
182 let disk_index
= get_single_udev_index(filter
, &udev_info
.disks
)?
;
183 let disk
= runtime_info
186 .find(|item
| item
.index
== disk_index
);
187 config
.target_hd
= disk
.cloned();
190 info
!("Selected disk: {}", config
.target_hd
.clone().unwrap().path
);
194 fn set_selected_disks(
196 udev_info
: &UdevInfo
,
197 runtime_info
: &RuntimeInfo
,
198 config
: &mut InstallConfig
,
200 match &answer
.disks
.disk_selection
{
201 answer
::DiskSelection
::Selection(disk_list
) => {
202 info
!("Disk selection found");
203 for disk_name
in disk_list
.clone() {
204 let disk
= runtime_info
207 .find(|item
| item
.path
.ends_with(disk_name
.as_str()));
208 if let Some(disk
) = disk
{
211 .insert(disk
.index
.clone(), disk
.index
.clone());
215 answer
::DiskSelection
::Filter(filter
) => {
216 info
!("No disk list found, looking for disk filters");
217 let filter_match
= answer
221 .unwrap_or(answer
::FilterMatch
::Any
);
222 let selected_disk_indexes
= get_matched_udev_indexes(
225 filter_match
== answer
::FilterMatch
::All
,
228 for i
in selected_disk_indexes
.into_iter() {
229 let disk
= runtime_info
232 .find(|item
| item
.index
== i
)
236 .insert(disk
.index
.clone(), disk
.index
.clone());
240 if config
.disk_selection
.is_empty() {
241 bail
!("No disks found matching selection.");
244 let mut selected_disks
: Vec
<String
> = Vec
::new();
245 for i
in config
.disk_selection
.keys() {
250 .find(|item
| item
.index
.as_str() == i
)
257 "Selected disks: {}",
260 .map(|x
| x
.to_string() + " ")
267 pub fn get_first_selected_disk(config
: &InstallConfig
) -> usize {
272 .expect("no disks found")
275 .expect("could not parse key to usize")
278 pub fn verify_locale_settings(answer
: &Answer
, locales
: &LocaleInfo
) -> Result
<()> {
279 info
!("Verifying locale settings");
283 .any(|i
| i
== &answer
.global
.country
)
285 bail
!("country code '{}' is not valid", &answer
.global
.country
);
287 if !locales
.kmap
.keys().any(|i
| i
== &answer
.global
.keyboard
) {
288 bail
!("keyboard layout '{}' is not valid", &answer
.global
.keyboard
);
294 .any(|(_
, zones
)| zones
.contains(&answer
.global
.timezone
))
295 && answer
.global
.timezone
!= "UTC"
297 bail
!("timezone '{}' is not valid", &answer
.global
.timezone
);
303 pub fn run_cmds(step
: &str, in_chroot
: bool
, cmds
: &[&str]) {
305 debug
!("Running commands for '{step}':");
309 Ok
::<(), anyhow
::Error
>(())
313 if let Err(err
) = run_cmd("proxmox-chroot prepare") {
314 error
!("Failed to setup chroot for '{step}': {err}");
319 if let Err(err
) = run() {
320 error
!("Running commands for '{step}' failed: {err:?}");
322 debug
!("Running commands in chroot for '{step}' finished");
326 if let Err(err
) = run_cmd("proxmox-chroot cleanup") {
327 error
!("Failed to clean up chroot for '{step}': {err}");
332 fn run_cmd(cmd
: &str) -> Result
<()> {
333 debug
!("Command '{cmd}':");
334 let child
= match Command
::new("/bin/bash")
337 .stdout(Stdio
::piped())
338 .stderr(Stdio
::piped())
342 Err(err
) => bail
!("error running command {cmd}: {err}"),
344 match child
.wait_with_output() {
346 if output
.status
.success() {
347 debug
!("{}", String
::from_utf8(output
.stdout
).unwrap());
349 bail
!("{}", String
::from_utf8(output
.stderr
).unwrap());
352 Err(err
) => bail
!("{err}"),
360 udev_info
: &UdevInfo
,
361 runtime_info
: &RuntimeInfo
,
362 locales
: &LocaleInfo
,
363 setup_info
: &SetupInfo
,
364 ) -> Result
<InstallConfig
> {
365 info
!("Parsing answer file");
366 info
!("Setting File system");
367 let filesystem
= answer
.disks
.fs_type
;
368 info
!("File system selected: {}", filesystem
);
370 let network_settings
= get_network_settings(answer
, udev_info
, runtime_info
, setup_info
)?
;
372 verify_locale_settings(answer
, locales
)?
;
374 let mut config
= InstallConfig
{
384 disk_selection
: BTreeMap
::new(),
387 country
: answer
.global
.country
.clone(),
388 timezone
: answer
.global
.timezone
.clone(),
389 keymap
: answer
.global
.keyboard
.clone(),
391 password
: answer
.global
.root_password
.clone(),
392 mailto
: answer
.global
.mailto
.clone(),
394 mngmt_nic
: network_settings
.ifname
,
396 hostname
: network_settings
.fqdn
.host().unwrap().to_string(),
397 domain
: network_settings
.fqdn
.domain(),
398 cidr
: network_settings
.address
,
399 gateway
: network_settings
.gateway
,
400 dns
: network_settings
.dns_server
,
403 set_disks(answer
, udev_info
, runtime_info
, &mut config
)?
;
404 match &answer
.disks
.fs_options
{
405 answer
::FsOptions
::LVM(lvm
) => {
406 config
.hdsize
= lvm
.hdsize
.unwrap_or(config
.target_hd
.clone().unwrap().size
);
407 config
.swapsize
= lvm
.swapsize
;
408 config
.maxroot
= lvm
.maxroot
;
409 config
.maxvz
= lvm
.maxvz
;
410 config
.minfree
= lvm
.minfree
;
412 answer
::FsOptions
::ZFS(zfs
) => {
413 let first_selected_disk
= get_first_selected_disk(&config
);
417 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
418 config
.zfs_opts
= Some(InstallZfsOption
{
419 ashift
: zfs
.ashift
.unwrap_or(12),
420 arc_max
: zfs
.arc_max
.unwrap_or(2048),
421 compress
: zfs
.compress
.unwrap_or(ZfsCompressOption
::On
),
422 checksum
: zfs
.checksum
.unwrap_or(ZfsChecksumOption
::On
),
423 copies
: zfs
.copies
.unwrap_or(1),
426 answer
::FsOptions
::BTRFS(btrfs
) => {
427 let first_selected_disk
= get_first_selected_disk(&config
);
429 config
.hdsize
= btrfs
431 .unwrap_or(runtime_info
.disks
[first_selected_disk
].size
);
435 // never print the auto reboot text after finishing to avoid the delay, as this is handled by
436 // the auto-installer itself anyway. The auto-installer might still perform some post-install
437 // steps after running the low-level installer.
438 config
.autoreboot
= 0;
442 #[derive(Clone, Debug, Deserialize, PartialEq)]
443 #[serde(tag = "type", rename_all = "lowercase")]
444 pub enum LowLevelMessage
{
445 #[serde(rename = "message")]