mirror of
https://git.proxmox.com/git/pve-installer
synced 2025-08-08 05:19:42 +00:00
common: copy common code from tui-installer
Copy code that is common to its own crate. Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com> Reviewed-by: Christoph Heiss <c.heiss@proxmox.com> Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
parent
2f65a616d3
commit
5362c05cd1
@ -8,3 +8,5 @@ exclude = [ "build", "debian" ]
|
|||||||
homepage = "https://www.proxmox.com"
|
homepage = "https://www.proxmox.com"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
237
proxmox-installer-common/src/disk_checks.rs
Normal file
237
proxmox-installer-common/src/disk_checks.rs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::options::{BtrfsRaidLevel, Disk, ZfsRaidLevel};
|
||||||
|
use crate::setup::BootType;
|
||||||
|
|
||||||
|
/// Checks a list of disks for duplicate entries, using their index as key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `disks` - A list of disks to check for duplicates.
|
||||||
|
fn check_for_duplicate_disks(disks: &[Disk]) -> Result<(), &Disk> {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
|
||||||
|
for disk in disks {
|
||||||
|
if !set.insert(&disk.index) {
|
||||||
|
return Err(disk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple wrapper which returns an descriptive error if the list of disks is too short.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `disks` - A list of disks to check the lenght of.
|
||||||
|
/// * `min` - Minimum number of disks
|
||||||
|
fn check_raid_min_disks(disks: &[Disk], min: usize) -> Result<(), String> {
|
||||||
|
if disks.len() < min {
|
||||||
|
Err(format!("Need at least {min} disks"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks all disks for legacy BIOS boot compatibility and reports an error as appropriate. 4Kn
|
||||||
|
/// disks are generally broken with legacy BIOS and cannot be booted from.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `runinfo` - `RuntimeInfo` instance of currently running system
|
||||||
|
/// * `disks` - List of disks designated as bootdisk targets.
|
||||||
|
fn check_disks_4kn_legacy_boot(boot_type: BootType, disks: &[Disk]) -> Result<(), &str> {
|
||||||
|
let is_blocksize_4096 = |disk: &Disk| disk.block_size.map(|s| s == 4096).unwrap_or(false);
|
||||||
|
|
||||||
|
if boot_type == BootType::Bios && disks.iter().any(is_blocksize_4096) {
|
||||||
|
return Err("Booting from 4Kn drive in legacy BIOS mode is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a user-supplied ZFS RAID setup is valid or not, such as disk sizes andminimum
|
||||||
|
/// number of disks.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `level` - The targeted ZFS RAID level by the user.
|
||||||
|
/// * `disks` - List of disks designated as RAID targets.
|
||||||
|
fn check_zfs_raid_config(level: ZfsRaidLevel, disks: &[Disk]) -> Result<(), String> {
|
||||||
|
// See also Proxmox/Install.pm:get_zfs_raid_setup()
|
||||||
|
|
||||||
|
let check_mirror_size = |disk1: &Disk, disk2: &Disk| {
|
||||||
|
if (disk1.size - disk2.size).abs() > disk1.size / 10. {
|
||||||
|
Err(format!(
|
||||||
|
"Mirrored disks must have same size:\n\n * {disk1}\n * {disk2}"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match level {
|
||||||
|
ZfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
|
||||||
|
ZfsRaidLevel::Raid1 => {
|
||||||
|
check_raid_min_disks(disks, 2)?;
|
||||||
|
for disk in disks {
|
||||||
|
check_mirror_size(&disks[0], disk)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ZfsRaidLevel::Raid10 => {
|
||||||
|
check_raid_min_disks(disks, 4)?;
|
||||||
|
// Pairs need to have the same size
|
||||||
|
for i in (0..disks.len()).step_by(2) {
|
||||||
|
check_mirror_size(&disks[i], &disks[i + 1])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For RAID-Z: minimum disks number is level + 2
|
||||||
|
ZfsRaidLevel::RaidZ => {
|
||||||
|
check_raid_min_disks(disks, 3)?;
|
||||||
|
for disk in disks {
|
||||||
|
check_mirror_size(&disks[0], disk)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ZfsRaidLevel::RaidZ2 => {
|
||||||
|
check_raid_min_disks(disks, 4)?;
|
||||||
|
for disk in disks {
|
||||||
|
check_mirror_size(&disks[0], disk)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ZfsRaidLevel::RaidZ3 => {
|
||||||
|
check_raid_min_disks(disks, 5)?;
|
||||||
|
for disk in disks {
|
||||||
|
check_mirror_size(&disks[0], disk)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a user-supplied Btrfs RAID setup is valid or not, such as minimum
|
||||||
|
/// number of disks.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `level` - The targeted Btrfs RAID level by the user.
|
||||||
|
/// * `disks` - List of disks designated as RAID targets.
|
||||||
|
fn check_btrfs_raid_config(level: BtrfsRaidLevel, disks: &[Disk]) -> Result<(), String> {
|
||||||
|
// See also Proxmox/Install.pm:get_btrfs_raid_setup()
|
||||||
|
|
||||||
|
match level {
|
||||||
|
BtrfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
|
||||||
|
BtrfsRaidLevel::Raid1 => check_raid_min_disks(disks, 2)?,
|
||||||
|
BtrfsRaidLevel::Raid10 => check_raid_min_disks(disks, 4)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn dummy_disk(index: usize) -> Disk {
|
||||||
|
Disk {
|
||||||
|
index: index.to_string(),
|
||||||
|
path: format!("/dev/dummy{index}"),
|
||||||
|
model: Some("Dummy disk".to_owned()),
|
||||||
|
size: 1024. * 1024. * 1024. * 8.,
|
||||||
|
block_size: Some(512),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dummy_disks(num: usize) -> Vec<Disk> {
|
||||||
|
(0..num).map(dummy_disk).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_disks() {
|
||||||
|
assert!(check_for_duplicate_disks(&dummy_disks(2)).is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
check_for_duplicate_disks(&[
|
||||||
|
dummy_disk(0),
|
||||||
|
dummy_disk(1),
|
||||||
|
dummy_disk(2),
|
||||||
|
dummy_disk(2),
|
||||||
|
dummy_disk(3),
|
||||||
|
]),
|
||||||
|
Err(&dummy_disk(2)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid_min_disks() {
|
||||||
|
let disks = dummy_disks(10);
|
||||||
|
|
||||||
|
assert!(check_raid_min_disks(&disks[..1], 2).is_err());
|
||||||
|
assert!(check_raid_min_disks(&disks[..1], 1).is_ok());
|
||||||
|
assert!(check_raid_min_disks(&disks, 1).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bios_boot_compat_4kn() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let mut disks = dummy_disks(10);
|
||||||
|
disks[i].block_size = Some(4096);
|
||||||
|
|
||||||
|
// Must fail if /any/ of the disks are 4Kn
|
||||||
|
assert!(check_disks_4kn_legacy_boot(BootType::Bios, &disks).is_err());
|
||||||
|
// For UEFI, we allow it for every configuration
|
||||||
|
assert!(check_disks_4kn_legacy_boot(BootType::Efi, &disks).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn btrfs_raid() {
|
||||||
|
let disks = dummy_disks(10);
|
||||||
|
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &[]).is_err());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks[..1]).is_ok());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &[]).is_err());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..1]).is_err());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..2]).is_ok());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &[]).is_err());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..3]).is_err());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..4]).is_ok());
|
||||||
|
assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zfs_raid() {
|
||||||
|
let disks = dummy_disks(10);
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &disks[..1]).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &disks[..2]).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &dummy_disks(4)).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks[..2]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks[..3]).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks[..3]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks[..4]).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks).is_ok());
|
||||||
|
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &[]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks[..4]).is_err());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks[..5]).is_ok());
|
||||||
|
assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks).is_ok());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
pub mod disk_checks;
|
||||||
|
pub mod options;
|
||||||
|
pub mod setup;
|
||||||
|
pub mod utils;
|
387
proxmox-installer-common/src/options.rs
Normal file
387
proxmox-installer-common/src/options.rs
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
use std::{cmp, fmt};
|
||||||
|
|
||||||
|
use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo};
|
||||||
|
use crate::utils::{CidrAddress, Fqdn};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BtrfsRaidLevel {
|
||||||
|
Raid0,
|
||||||
|
Raid1,
|
||||||
|
Raid10,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BtrfsRaidLevel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use BtrfsRaidLevel::*;
|
||||||
|
match self {
|
||||||
|
Raid0 => write!(f, "RAID0"),
|
||||||
|
Raid1 => write!(f, "RAID1"),
|
||||||
|
Raid10 => write!(f, "RAID10"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ZfsRaidLevel {
|
||||||
|
Raid0,
|
||||||
|
Raid1,
|
||||||
|
Raid10,
|
||||||
|
RaidZ,
|
||||||
|
RaidZ2,
|
||||||
|
RaidZ3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ZfsRaidLevel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use ZfsRaidLevel::*;
|
||||||
|
match self {
|
||||||
|
Raid0 => write!(f, "RAID0"),
|
||||||
|
Raid1 => write!(f, "RAID1"),
|
||||||
|
Raid10 => write!(f, "RAID10"),
|
||||||
|
RaidZ => write!(f, "RAIDZ-1"),
|
||||||
|
RaidZ2 => write!(f, "RAIDZ-2"),
|
||||||
|
RaidZ3 => write!(f, "RAIDZ-3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum FsType {
|
||||||
|
Ext4,
|
||||||
|
Xfs,
|
||||||
|
Zfs(ZfsRaidLevel),
|
||||||
|
Btrfs(BtrfsRaidLevel),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FsType {
|
||||||
|
pub fn is_btrfs(&self) -> bool {
|
||||||
|
matches!(self, FsType::Btrfs(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FsType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use FsType::*;
|
||||||
|
match self {
|
||||||
|
Ext4 => write!(f, "ext4"),
|
||||||
|
Xfs => write!(f, "XFS"),
|
||||||
|
Zfs(level) => write!(f, "ZFS ({level})"),
|
||||||
|
Btrfs(level) => write!(f, "Btrfs ({level})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LvmBootdiskOptions {
|
||||||
|
pub total_size: f64,
|
||||||
|
pub swap_size: Option<f64>,
|
||||||
|
pub max_root_size: Option<f64>,
|
||||||
|
pub max_data_size: Option<f64>,
|
||||||
|
pub min_lvm_free: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LvmBootdiskOptions {
|
||||||
|
pub fn defaults_from(disk: &Disk) -> Self {
|
||||||
|
Self {
|
||||||
|
total_size: disk.size,
|
||||||
|
swap_size: None,
|
||||||
|
max_root_size: None,
|
||||||
|
max_data_size: None,
|
||||||
|
min_lvm_free: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BtrfsBootdiskOptions {
|
||||||
|
pub disk_size: f64,
|
||||||
|
pub selected_disks: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BtrfsBootdiskOptions {
|
||||||
|
/// This panics if the provided slice is empty.
|
||||||
|
pub fn defaults_from(disks: &[Disk]) -> Self {
|
||||||
|
let disk = &disks[0];
|
||||||
|
Self {
|
||||||
|
disk_size: disk.size,
|
||||||
|
selected_disks: (0..disks.len()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum ZfsCompressOption {
|
||||||
|
#[default]
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Lzjb,
|
||||||
|
Lz4,
|
||||||
|
Zle,
|
||||||
|
Gzip,
|
||||||
|
Zstd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ZfsCompressOption {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", format!("{self:?}").to_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ZfsCompressOption> for String {
|
||||||
|
fn from(value: &ZfsCompressOption) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ZFS_COMPRESS_OPTIONS: &[ZfsCompressOption] = {
|
||||||
|
use ZfsCompressOption::*;
|
||||||
|
&[On, Off, Lzjb, Lz4, Zle, Gzip, Zstd]
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum ZfsChecksumOption {
|
||||||
|
#[default]
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Fletcher2,
|
||||||
|
Fletcher4,
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ZfsChecksumOption {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", format!("{self:?}").to_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ZfsChecksumOption> for String {
|
||||||
|
fn from(value: &ZfsChecksumOption) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ZFS_CHECKSUM_OPTIONS: &[ZfsChecksumOption] = {
|
||||||
|
use ZfsChecksumOption::*;
|
||||||
|
&[On, Off, Fletcher2, Fletcher4, Sha256]
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ZfsBootdiskOptions {
|
||||||
|
pub ashift: usize,
|
||||||
|
pub compress: ZfsCompressOption,
|
||||||
|
pub checksum: ZfsChecksumOption,
|
||||||
|
pub copies: usize,
|
||||||
|
pub disk_size: f64,
|
||||||
|
pub selected_disks: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZfsBootdiskOptions {
|
||||||
|
/// This panics if the provided slice is empty.
|
||||||
|
pub fn defaults_from(disks: &[Disk]) -> Self {
|
||||||
|
let disk = &disks[0];
|
||||||
|
Self {
|
||||||
|
ashift: 12,
|
||||||
|
compress: ZfsCompressOption::default(),
|
||||||
|
checksum: ZfsChecksumOption::default(),
|
||||||
|
copies: 1,
|
||||||
|
disk_size: disk.size,
|
||||||
|
selected_disks: (0..disks.len()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AdvancedBootdiskOptions {
|
||||||
|
Lvm(LvmBootdiskOptions),
|
||||||
|
Zfs(ZfsBootdiskOptions),
|
||||||
|
Btrfs(BtrfsBootdiskOptions),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Disk {
|
||||||
|
pub index: String,
|
||||||
|
pub path: String,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub size: f64,
|
||||||
|
pub block_size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Disk {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// TODO: Format sizes properly with `proxmox-human-byte` once merged
|
||||||
|
// https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html
|
||||||
|
f.write_str(&self.path)?;
|
||||||
|
if let Some(model) = &self.model {
|
||||||
|
// FIXME: ellipsize too-long names?
|
||||||
|
write!(f, " ({model})")?;
|
||||||
|
}
|
||||||
|
write!(f, " ({:.2} GiB)", self.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Disk> for String {
|
||||||
|
fn from(value: &Disk) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::Eq for Disk {}
|
||||||
|
|
||||||
|
impl cmp::PartialOrd for Disk {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
self.index.partial_cmp(&other.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::Ord for Disk {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
self.index.cmp(&other.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BootdiskOptions {
|
||||||
|
pub disks: Vec<Disk>,
|
||||||
|
pub fstype: FsType,
|
||||||
|
pub advanced: AdvancedBootdiskOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootdiskOptions {
|
||||||
|
pub fn defaults_from(disk: &Disk) -> Self {
|
||||||
|
Self {
|
||||||
|
disks: vec![disk.clone()],
|
||||||
|
fstype: FsType::Ext4,
|
||||||
|
advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(disk)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TimezoneOptions {
|
||||||
|
pub country: String,
|
||||||
|
pub timezone: String,
|
||||||
|
pub kb_layout: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimezoneOptions {
|
||||||
|
pub fn defaults_from(runtime: &RuntimeInfo, locales: &LocaleInfo) -> Self {
|
||||||
|
let country = runtime.country.clone().unwrap_or_else(|| "at".to_owned());
|
||||||
|
|
||||||
|
let timezone = locales
|
||||||
|
.cczones
|
||||||
|
.get(&country)
|
||||||
|
.and_then(|zones| zones.get(0))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "UTC".to_owned());
|
||||||
|
|
||||||
|
let kb_layout = locales
|
||||||
|
.countries
|
||||||
|
.get(&country)
|
||||||
|
.and_then(|c| {
|
||||||
|
if c.kmap.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(c.kmap.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "en-us".to_owned());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
country,
|
||||||
|
timezone,
|
||||||
|
kb_layout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PasswordOptions {
|
||||||
|
pub email: String,
|
||||||
|
pub root_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PasswordOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
email: "mail@example.invalid".to_string(),
|
||||||
|
root_password: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct NetworkOptions {
|
||||||
|
pub ifname: String,
|
||||||
|
pub fqdn: Fqdn,
|
||||||
|
pub address: CidrAddress,
|
||||||
|
pub gateway: IpAddr,
|
||||||
|
pub dns_server: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkOptions {
|
||||||
|
const DEFAULT_DOMAIN: &str = "example.invalid";
|
||||||
|
|
||||||
|
pub fn defaults_from(setup: &SetupInfo, network: &NetworkInfo) -> Self {
|
||||||
|
let mut this = Self {
|
||||||
|
ifname: String::new(),
|
||||||
|
fqdn: Self::construct_fqdn(network, setup.config.product.default_hostname()),
|
||||||
|
// Safety: The provided mask will always be valid.
|
||||||
|
address: CidrAddress::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(),
|
||||||
|
gateway: Ipv4Addr::UNSPECIFIED.into(),
|
||||||
|
dns_server: Ipv4Addr::UNSPECIFIED.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ip) = network.dns.dns.first() {
|
||||||
|
this.dns_server = *ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(routes) = &network.routes {
|
||||||
|
let mut filled = false;
|
||||||
|
if let Some(gw) = &routes.gateway4 {
|
||||||
|
if let Some(iface) = network.interfaces.get(&gw.dev) {
|
||||||
|
this.ifname = iface.name.clone();
|
||||||
|
if let Some(addresses) = &iface.addresses {
|
||||||
|
if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv4()) {
|
||||||
|
this.gateway = gw.gateway;
|
||||||
|
this.address = addr.clone();
|
||||||
|
filled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !filled {
|
||||||
|
if let Some(gw) = &routes.gateway6 {
|
||||||
|
if let Some(iface) = network.interfaces.get(&gw.dev) {
|
||||||
|
if let Some(addresses) = &iface.addresses {
|
||||||
|
if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv6()) {
|
||||||
|
this.ifname = iface.name.clone();
|
||||||
|
this.gateway = gw.gateway;
|
||||||
|
this.address = addr.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_fqdn(network: &NetworkInfo, default_hostname: &str) -> Fqdn {
|
||||||
|
let hostname = network.hostname.as_deref().unwrap_or(default_hostname);
|
||||||
|
|
||||||
|
let domain = network
|
||||||
|
.dns
|
||||||
|
.domain
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(Self::DEFAULT_DOMAIN);
|
||||||
|
|
||||||
|
Fqdn::from(&format!("{hostname}.{domain}")).unwrap_or_else(|_| {
|
||||||
|
// Safety: This will always result in a valid FQDN, as we control & know
|
||||||
|
// the values of default_hostname (one of "pve", "pmg" or "pbs") and
|
||||||
|
// constant-defined DEFAULT_DOMAIN.
|
||||||
|
Fqdn::from(&format!("{}.{}", default_hostname, Self::DEFAULT_DOMAIN)).unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
330
proxmox-installer-common/src/setup.rs
Normal file
330
proxmox-installer-common/src/setup.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
use std::{
|
||||||
|
cmp,
|
||||||
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
fs::File,
|
||||||
|
io::BufReader,
|
||||||
|
net::IpAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
options::{Disk, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption},
|
||||||
|
utils::CidrAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
#[derive(Clone, Copy, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ProxmoxProduct {
|
||||||
|
PVE,
|
||||||
|
PBS,
|
||||||
|
PMG,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxmoxProduct {
|
||||||
|
pub fn default_hostname(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::PVE => "pve",
|
||||||
|
Self::PMG => "pmg",
|
||||||
|
Self::PBS => "pbs",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct ProductConfig {
|
||||||
|
pub fullname: String,
|
||||||
|
pub product: ProxmoxProduct,
|
||||||
|
#[serde(deserialize_with = "deserialize_bool_from_int")]
|
||||||
|
pub enable_btrfs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct IsoInfo {
|
||||||
|
pub release: String,
|
||||||
|
pub isorelease: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paths in the ISO environment containing installer data.
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct IsoLocations {
|
||||||
|
pub iso: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct SetupInfo {
|
||||||
|
#[serde(rename = "product-cfg")]
|
||||||
|
pub config: ProductConfig,
|
||||||
|
#[serde(rename = "iso-info")]
|
||||||
|
pub iso_info: IsoInfo,
|
||||||
|
pub locations: IsoLocations,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct CountryInfo {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub zone: String,
|
||||||
|
pub kmap: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct KeyboardMapping {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "kvm")]
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "x11")]
|
||||||
|
pub xkb_layout: String,
|
||||||
|
#[serde(rename = "x11var")]
|
||||||
|
pub xkb_variant: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::PartialOrd for KeyboardMapping {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
self.name.partial_cmp(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::Ord for KeyboardMapping {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
self.name.cmp(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct LocaleInfo {
|
||||||
|
#[serde(deserialize_with = "deserialize_cczones_map")]
|
||||||
|
pub cczones: HashMap<String, Vec<String>>,
|
||||||
|
#[serde(rename = "country")]
|
||||||
|
pub countries: HashMap<String, CountryInfo>,
|
||||||
|
pub kmap: HashMap<String, KeyboardMapping>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct InstallZfsOption {
|
||||||
|
ashift: usize,
|
||||||
|
#[serde(serialize_with = "serialize_as_display")]
|
||||||
|
compress: ZfsCompressOption,
|
||||||
|
#[serde(serialize_with = "serialize_as_display")]
|
||||||
|
checksum: ZfsChecksumOption,
|
||||||
|
copies: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ZfsBootdiskOptions> for InstallZfsOption {
|
||||||
|
fn from(opts: ZfsBootdiskOptions) -> Self {
|
||||||
|
InstallZfsOption {
|
||||||
|
ashift: opts.ashift,
|
||||||
|
compress: opts.compress,
|
||||||
|
checksum: opts.checksum,
|
||||||
|
copies: opts.copies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_json<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T, String> {
|
||||||
|
let file = File::open(path).map_err(|err| err.to_string())?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
serde_json::from_reader(reader).map_err(|err| format!("failed to parse JSON: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let val: u32 = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(val != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_cczones_map<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<HashMap<String, Vec<String>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let map: HashMap<String, HashMap<String, u32>> = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
for (cc, list) in map.into_iter() {
|
||||||
|
result.insert(cc, list.into_keys().collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_disks_map<'de, D>(deserializer: D) -> Result<Vec<Disk>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let disks =
|
||||||
|
<Vec<(usize, String, f64, String, Option<usize>, String)>>::deserialize(deserializer)?;
|
||||||
|
Ok(disks
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|(index, device, size_mb, model, logical_bsize, _syspath)| Disk {
|
||||||
|
index: index.to_string(),
|
||||||
|
// Linux always reports the size of block devices in sectors, where one sector is
|
||||||
|
// defined as being 2^9 = 512 bytes in size.
|
||||||
|
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/blk_types.h?h=v6.4#n30
|
||||||
|
size: (size_mb * 512.) / 1024. / 1024. / 1024.,
|
||||||
|
block_size: logical_bsize,
|
||||||
|
path: device,
|
||||||
|
model: (!model.is_empty()).then_some(model),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CidrDescriptor {
|
||||||
|
address: String,
|
||||||
|
prefix: usize,
|
||||||
|
// family is implied anyway by parsing the address
|
||||||
|
}
|
||||||
|
|
||||||
|
let list: Vec<CidrDescriptor> = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(list.len());
|
||||||
|
for desc in list {
|
||||||
|
let ip_addr = desc
|
||||||
|
.address
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.map_err(|err| de::Error::custom(format!("{:?}", err)))?;
|
||||||
|
|
||||||
|
result.push(
|
||||||
|
CidrAddress::new(ip_addr, desc.prefix)
|
||||||
|
.map_err(|err| de::Error::custom(format!("{:?}", err)))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_as_display<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
serializer.collect_str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct RuntimeInfo {
|
||||||
|
/// Whether is system was booted in (legacy) BIOS or UEFI mode.
|
||||||
|
pub boot_type: BootType,
|
||||||
|
|
||||||
|
/// Detected country if available.
|
||||||
|
pub country: Option<String>,
|
||||||
|
|
||||||
|
/// Maps devices to their information.
|
||||||
|
#[serde(deserialize_with = "deserialize_disks_map")]
|
||||||
|
pub disks: Vec<Disk>,
|
||||||
|
|
||||||
|
/// Network addresses, gateways and DNS info.
|
||||||
|
pub network: NetworkInfo,
|
||||||
|
|
||||||
|
/// Total memory of the system in MiB.
|
||||||
|
pub total_memory: usize,
|
||||||
|
|
||||||
|
/// Whether the CPU supports hardware-accelerated virtualization
|
||||||
|
#[serde(deserialize_with = "deserialize_bool_from_int")]
|
||||||
|
pub hvm_supported: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum BootType {
|
||||||
|
Bios,
|
||||||
|
Efi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct NetworkInfo {
|
||||||
|
pub dns: Dns,
|
||||||
|
pub routes: Option<Routes>,
|
||||||
|
|
||||||
|
/// Maps devices to their configuration, if it has a usable configuration.
|
||||||
|
/// (Contains no entries for devices with only link-local addresses.)
|
||||||
|
#[serde(default)]
|
||||||
|
pub interfaces: HashMap<String, Interface>,
|
||||||
|
|
||||||
|
/// The hostname of this machine, if set by the DHCP server.
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Dns {
|
||||||
|
pub domain: Option<String>,
|
||||||
|
|
||||||
|
/// List of stringified IP addresses.
|
||||||
|
#[serde(default)]
|
||||||
|
pub dns: Vec<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Routes {
|
||||||
|
/// Ipv4 gateway.
|
||||||
|
pub gateway4: Option<Gateway>,
|
||||||
|
|
||||||
|
/// Ipv6 gateway.
|
||||||
|
pub gateway6: Option<Gateway>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Gateway {
|
||||||
|
/// Outgoing network device.
|
||||||
|
pub dev: String,
|
||||||
|
|
||||||
|
/// Stringified gateway IP address.
|
||||||
|
pub gateway: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum InterfaceState {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceState {
|
||||||
|
// avoid display trait as this is not the string representation for a serializer
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Up => "\u{25CF}",
|
||||||
|
Self::Down | Self::Unknown => " ",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Interface {
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub index: usize,
|
||||||
|
|
||||||
|
pub mac: String,
|
||||||
|
|
||||||
|
pub state: InterfaceState,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(deserialize_with = "deserialize_cidr_list")]
|
||||||
|
pub addresses: Option<Vec<CidrAddress>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interface {
|
||||||
|
// avoid display trait as this is not the string representation for a serializer
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
format!("{} {}", self.state.render(), self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
268
proxmox-installer-common/src/utils.rs
Normal file
268
proxmox-installer-common/src/utils.rs
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
net::{AddrParseError, IpAddr},
|
||||||
|
num::ParseIntError,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Possible errors that might occur when parsing CIDR addresses.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CidrAddressParseError {
|
||||||
|
/// No delimiter for separating address and mask was found.
|
||||||
|
NoDelimiter,
|
||||||
|
/// The IP address part could not be parsed.
|
||||||
|
InvalidAddr(AddrParseError),
|
||||||
|
/// The mask could not be parsed.
|
||||||
|
InvalidMask(Option<ParseIntError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An IP address (IPv4 or IPv6), including network mask.
|
||||||
|
///
|
||||||
|
/// See the [`IpAddr`] type for more information how IP addresses are handled.
|
||||||
|
/// The mask is appropriately enforced to be `0 <= mask <= 32` for IPv4 or
|
||||||
|
/// `0 <= mask <= 128` for IPv6 addresses.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
/// let ipv4 = CidrAddress::new(Ipv4Addr::new(192, 168, 0, 1), 24).unwrap();
|
||||||
|
/// let ipv6 = CidrAddress::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc0a8, 1), 32).unwrap();
|
||||||
|
///
|
||||||
|
/// assert_eq!(ipv4.to_string(), "192.168.0.1/24");
|
||||||
|
/// assert_eq!(ipv6.to_string(), "2001:db8::c0a8:1/32");
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct CidrAddress {
|
||||||
|
addr: IpAddr,
|
||||||
|
mask: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CidrAddress {
|
||||||
|
/// Constructs a new CIDR address.
|
||||||
|
///
|
||||||
|
/// It fails if the mask is invalid for the given IP address.
|
||||||
|
pub fn new<T: Into<IpAddr>>(addr: T, mask: usize) -> Result<Self, CidrAddressParseError> {
|
||||||
|
let addr = addr.into();
|
||||||
|
|
||||||
|
if mask > mask_limit(&addr) {
|
||||||
|
Err(CidrAddressParseError::InvalidMask(None))
|
||||||
|
} else {
|
||||||
|
Ok(Self { addr, mask })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns only the IP address part of the address.
|
||||||
|
pub fn addr(&self) -> IpAddr {
|
||||||
|
self.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this address is an IPv4 address, `false` otherwise.
|
||||||
|
pub fn is_ipv4(&self) -> bool {
|
||||||
|
self.addr.is_ipv4()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this address is an IPv6 address, `false` otherwise.
|
||||||
|
pub fn is_ipv6(&self) -> bool {
|
||||||
|
self.addr.is_ipv6()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns only the mask part of the address.
|
||||||
|
pub fn mask(&self) -> usize {
|
||||||
|
self.mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CidrAddress {
|
||||||
|
type Err = CidrAddressParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (addr, mask) = s
|
||||||
|
.split_once('/')
|
||||||
|
.ok_or(CidrAddressParseError::NoDelimiter)?;
|
||||||
|
|
||||||
|
let addr = addr.parse().map_err(CidrAddressParseError::InvalidAddr)?;
|
||||||
|
|
||||||
|
let mask = mask
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| CidrAddressParseError::InvalidMask(Some(err)))?;
|
||||||
|
|
||||||
|
if mask > mask_limit(&addr) {
|
||||||
|
Err(CidrAddressParseError::InvalidMask(None))
|
||||||
|
} else {
|
||||||
|
Ok(Self { addr, mask })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CidrAddress {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}/{}", self.addr, self.mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_limit(addr: &IpAddr) -> usize {
|
||||||
|
if addr.is_ipv4() {
|
||||||
|
32
|
||||||
|
} else {
|
||||||
|
128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible errors that might occur when parsing FQDNs.
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum FqdnParseError {
|
||||||
|
MissingHostname,
|
||||||
|
NumericHostname,
|
||||||
|
InvalidPart(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FqdnParseError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use FqdnParseError::*;
|
||||||
|
match self {
|
||||||
|
MissingHostname => write!(f, "missing hostname part"),
|
||||||
|
NumericHostname => write!(f, "hostname cannot be purely numeric"),
|
||||||
|
InvalidPart(part) => write!(
|
||||||
|
f,
|
||||||
|
"FQDN must only consist of alphanumeric characters and dashes. Invalid part: '{part}'",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Fqdn {
|
||||||
|
parts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fqdn {
|
||||||
|
pub fn from(fqdn: &str) -> Result<Self, FqdnParseError> {
|
||||||
|
let parts = fqdn
|
||||||
|
.split('.')
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
for part in &parts {
|
||||||
|
if !Self::validate_single(part) {
|
||||||
|
return Err(FqdnParseError::InvalidPart(part.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.len() < 2 {
|
||||||
|
Err(FqdnParseError::MissingHostname)
|
||||||
|
} else if parts[0].chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
// Not allowed/supported on Debian systems.
|
||||||
|
Err(FqdnParseError::NumericHostname)
|
||||||
|
} else {
|
||||||
|
Ok(Self { parts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host(&self) -> Option<&str> {
|
||||||
|
self.has_host().then_some(&self.parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domain(&self) -> String {
|
||||||
|
let parts = if self.has_host() {
|
||||||
|
&self.parts[1..]
|
||||||
|
} else {
|
||||||
|
&self.parts
|
||||||
|
};
|
||||||
|
|
||||||
|
parts.join(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the FQDN has a hostname associated with it, i.e. is has more than 1 part.
|
||||||
|
fn has_host(&self) -> bool {
|
||||||
|
self.parts.len() > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_single(s: &String) -> bool {
|
||||||
|
!s.is_empty()
|
||||||
|
// First character must be alphanumeric
|
||||||
|
&& s.chars()
|
||||||
|
.next()
|
||||||
|
.map(|c| c.is_ascii_alphanumeric())
|
||||||
|
.unwrap_or_default()
|
||||||
|
// .. last character as well,
|
||||||
|
&& s.chars()
|
||||||
|
.last()
|
||||||
|
.map(|c| c.is_ascii_alphanumeric())
|
||||||
|
.unwrap_or_default()
|
||||||
|
// and anything between must be alphanumeric or -
|
||||||
|
&& s.chars()
|
||||||
|
.skip(1)
|
||||||
|
.take(s.len().saturating_sub(2))
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Fqdn {
|
||||||
|
type Err = FqdnParseError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::from(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Fqdn {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.parts.join("."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Fqdn {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
s.parse()
|
||||||
|
.map_err(|_| serde::de::Error::custom("invalid FQDN"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fqdn_construct() {
|
||||||
|
use FqdnParseError::*;
|
||||||
|
assert!(Fqdn::from("foo.example.com").is_ok());
|
||||||
|
assert!(Fqdn::from("foo-bar.com").is_ok());
|
||||||
|
assert!(Fqdn::from("a-b.com").is_ok());
|
||||||
|
|
||||||
|
assert_eq!(Fqdn::from("foo"), Err(MissingHostname));
|
||||||
|
|
||||||
|
assert_eq!(Fqdn::from("-foo.com"), Err(InvalidPart("-foo".to_owned())));
|
||||||
|
assert_eq!(Fqdn::from("foo-.com"), Err(InvalidPart("foo-".to_owned())));
|
||||||
|
assert_eq!(Fqdn::from("foo.com-"), Err(InvalidPart("com-".to_owned())));
|
||||||
|
assert_eq!(Fqdn::from("-o-.com"), Err(InvalidPart("-o-".to_owned())));
|
||||||
|
|
||||||
|
assert_eq!(Fqdn::from("123.com"), Err(NumericHostname));
|
||||||
|
assert!(Fqdn::from("foo123.com").is_ok());
|
||||||
|
assert!(Fqdn::from("123foo.com").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fqdn_parts() {
|
||||||
|
let fqdn = Fqdn::from("pve.example.com").unwrap();
|
||||||
|
assert_eq!(fqdn.host().unwrap(), "pve");
|
||||||
|
assert_eq!(fqdn.domain(), "example.com");
|
||||||
|
assert_eq!(
|
||||||
|
fqdn.parts,
|
||||||
|
&["pve".to_owned(), "example".to_owned(), "com".to_owned()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fqdn_display() {
|
||||||
|
assert_eq!(
|
||||||
|
Fqdn::from("foo.example.com").unwrap().to_string(),
|
||||||
|
"foo.example.com"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user