+++ /dev/null
-[source]
-[source.debian-packages]
-directory = "/usr/share/cargo/registry"
-[source.crates-io]
-replace-with = "debian-packages"
--- /dev/null
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
-/*.deb
-/*.changes
/*.buildinfo
+/*.changes
+/*.deb
/build/
-/testdir/
+/cd-info.test
+/proxmox-installer-[0-9]*/
/target/
/test*.img
-country.dat
+/testdir/
Cargo.lock
-/cd-info.test
+country.dat
[workspace]
+resolver = "2"
members = [
+ "proxmox-auto-installer",
+ "proxmox-auto-install-assistant",
+ "proxmox-chroot",
+ "proxmox-fetch-answer",
"proxmox-installer-common",
"proxmox-tui-installer",
]
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+ASSISTANT_DEB=proxmox-auto-install-assistant_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
DSC=$(PACKAGE)_$(DEB_VERSION).dsc
CARGO ?= cargo
PREFIX = /usr
BINDIR = $(PREFIX)/bin
-USR_BIN := proxmox-tui-installer
+USR_BIN := \
+ proxmox-chroot\
+ proxmox-tui-installer\
+ proxmox-fetch-answer\
+ proxmox-auto-install-assistant \
+ proxmox-auto-installer
COMPILED_BINS := \
$(addprefix $(CARGO_COMPILEDIR)/,$(USR_BIN))
interfaces \
proxinstall \
proxmox-low-level-installer \
+ proxmox-auto-installer/ \
+ proxmox-auto-install-assistant/ \
+ proxmox-fetch-answer/ \
+ proxmox-chroot \
proxmox-tui-installer/ \
proxmox-installer-common/ \
test/ \
mv country.dat.tmp country.dat
deb: $(DEB)
+$(ASSISTANT_DEB): $(DEB)
$(DEB): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
lintian $(DEB)
HTMLDIR=$(VARLIBDIR)/html/common
.PHONY: install
-install: $(INSTALLER_SOURCES) $(CARGO_COMPILEDIR)/proxmox-tui-installer
+install: $(INSTALLER_SOURCES) $(COMPILED_BINS)
$(MAKE) -C banner install
$(MAKE) -C Proxmox install
install -D -m 644 interfaces $(DESTDIR)/etc/network/interfaces
$(COMPILED_BINS): cargo-build
.PHONY: cargo-build
cargo-build:
- $(CARGO) build --package proxmox-tui-installer --bin proxmox-tui-installer $(CARGO_BUILD_ARGS)
+ $(CARGO) build --package proxmox-tui-installer --bin proxmox-tui-installer \
+ --package proxmox-auto-installer --bin proxmox-auto-installer \
+ --package proxmox-fetch-answer --bin proxmox-fetch-answer \
+ --package proxmox-auto-install-assistant --bin proxmox-auto-install-assistant \
+ --package proxmox-chroot --bin proxmox-chroot $(CARGO_BUILD_ARGS)
%-banner.png: %-banner.svg
rsvg-convert -o $@ $<
.PHONY: upload
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
-upload: $(DEB)
- tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
+upload: $(DEB) $(ASSISTANT_DEB)
+ tar cf - $(DEB) $(ASSISTANT_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
%.img:
truncate -s 2G $@
use Proxmox::Sys::Block qw(get_cached_disks wipe_disk partition_bootable_disk);
use Proxmox::Sys::Command qw(run_command syscmd);
-use Proxmox::Sys::File qw(file_read_all file_read_firstline file_write_all);
+use Proxmox::Sys::File qw(file_read_firstline file_write_all);
use Proxmox::UI;
# TODO: move somewhere better?
$cmd .= " @$hd[1]";
}
} elsif ($filesys eq 'zfs (RAID1)') {
- die "zfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
+ die "zfs (RAID1) needs at least 2 devices\n" if $diskcount < 2;
$cmd .= ' mirror ';
my $hd = @$devlist[0];
my $expected_size = @$hd[2]; # all disks need approximately same size
$cmd .= " @$hd[1]";
}
} elsif ($filesys eq 'zfs (RAID10)') {
- die "zfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
+ die "zfs (RAID10) needs at least 4 devices\n" if $diskcount < 4;
die "zfs (RAID10) needs an even number of devices\n" if $diskcount & 1;
for (my $i = 0; $i < $diskcount; $i+=2) {
if ($filesys eq 'btrfs (RAID0)') {
$mode = 'raid0';
} elsif ($filesys eq 'btrfs (RAID1)') {
- die "btrfs (RAID1) needs at least 2 device\n" if $diskcount < 2;
+ die "btrfs (RAID1) needs at least 2 devices\n" if $diskcount < 2;
$mode = 'raid1';
} elsif ($filesys eq 'btrfs (RAID10)') {
- die "btrfs (RAID10) needs at least 4 device\n" if $diskcount < 4;
+ die "btrfs (RAID10) needs at least 4 devices\n" if $diskcount < 4;
$mode = 'raid10';
} else {
die "unknown btrfs mode '$filesys'\n";
my $duplicate_vgs = get_pv_list_from_vgname($vgname);
return if !$duplicate_vgs;
- my $message = "Detected existing '$vgname' Volume Group(s)! Do you want to:\n";
-
for my $vg_uuid (keys %$duplicate_vgs) {
my $vg = $duplicate_vgs->{$vg_uuid};
# we have a disk with both a "$vgname" and "$vgname-old"...
my $short_uid = sprintf "%08X", rand(0xffffffff);
$vg->{new_vgname} = "$vgname-OLD-$short_uid";
-
- $message .= "rename VG backed by PV '$vg->{pvs}' to '$vg->{new_vgname}'\n";
}
- $message .= "or cancel the installation?";
- my $response_ok = Proxmox::UI::prompt($message);
+ my $response_ok = Proxmox::Install::Config::get_lvm_auto_rename();
+ if (!$response_ok) {
+ my $message = "Detected existing '$vgname' Volume Group(s)! Do you want to:\n";
+
+ for my $vg_uuid (keys %$duplicate_vgs) {
+ my $vg = $duplicate_vgs->{$vg_uuid};
+ $message .= "rename VG backed by PV '$vg->{pvs}' to '$vg->{new_vgname}'\n";
+ }
+ $message .= "or cancel the installation?";
+
+ $response_ok = Proxmox::UI::prompt($message);
+ }
if ($response_ok) {
for my $vg_uuid (keys %$duplicate_vgs) {
}
sub prepare_proxmox_boot_esp {
- my ($espdev, $targetdir) = @_;
+ my ($espdev, $targetdir, $secureboot) = @_;
my $mode = '';
- # detect secure boot being enabled and switch to grub-on-ESP if it is
- if (-d "/sys/firmware/efi") {
- my $content = eval { file_read_all("/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c") };
- if ($@) {
- warn "Failed to read secure boot state: $@\n";
- } else {
- my @secureboot = unpack("CCCCC", $content);
- $mode = 'grub' if $secureboot[4] == 1;
- }
- }
+ # if secure boot is enabled switch to grub-on-ESP
+ $mode = 'grub' if $secureboot;
syscmd("chroot $targetdir proxmox-boot-tool init $espdev $mode") == 0 ||
die "unable to init ESP and install proxmox-boot loader on '$espdev'\n";
die "unable to set zfs properties\n";
}
- update_progress(0.04, 0, $maxper, "create swap space");
if ($swapfile) {
+ update_progress(0.04, 0, $maxper, "create swap space");
syscmd("mkswap -f $swapfile") == 0 ||
die "unable to create swap space\n";
}
create_filesystem($rootdev, 'root', $filesys, 0.05, $maxper, 0, 1);
}
- update_progress(1, 0.05, $maxper, "mounting target $rootdev");
if ($use_zfs) {
# do nothing
} else {
+ update_progress(1, 0.05, $maxper, "mounting target $rootdev");
+
my $mount_opts = 'noatime';
$mount_opts .= ',nobarrier'
if $use_btrfs || $filesys =~ /^ext\d$/;
# upon upgrade - and conflict with each other - install the fitting one only
next if ($deb =~ /grub-pc_/ && $run_env->{boot_type} ne 'bios');
next if ($deb =~ /grub-efi-amd64_/ && $run_env->{boot_type} ne 'efi');
+ next if ($deb =~ /^proxmox-grub/ && $run_env->{boot_type} ne 'efi');
+ next if ($deb =~ /^proxmox-secure-boot-support_/ && !$run_env->{secure_boot});
update_progress($count/$pkg_count, 0.5, 0.75, "extracting $deb");
- print STDERR "extracting: $deb\n";
+
syscmd("chroot $targetdir dpkg $dpkg_opts --force-depends --no-triggers --unpack /tmp/pkg/$deb") == 0
|| die "installation of package $deb failed\n";
update_progress((++$count)/$pkg_count, 0.5, 0.75);
foreach my $di (@$bootdevinfo) {
my $dev = $di->{devname};
if ($use_zfs) {
- prepare_proxmox_boot_esp($di->{esp}, $targetdir);
+ prepare_proxmox_boot_esp($di->{esp}, $targetdir, $run_env->{secure_boot});
} else {
if (!$native_4k_disk_bootable) {
eval {
my $octets = encode("utf-8", Proxmox::Install::Config::get_password());
run_command("chroot $targetdir /usr/sbin/chpasswd", undef, "root:$octets\n");
+ # set root ssh keys
+ my $ssh_keys = Proxmox::Install::Config::get_root_ssh_keys();
+ if (scalar(@$ssh_keys) > 0) {
+ mkdir "$targetdir/root/.ssh";
+ file_write_all("$targetdir/root/.ssh/authorized_keys", join("\n", @$ssh_keys));
+ }
+
my $mailto = Proxmox::Install::Config::get_mailto();
if ($iso_env->{product} eq 'pmg') {
# save admin email
# TODO: single disk selection config
target_hd => undef,
disk_selection => {},
+ lvm_auto_rename => 0,
# locale
country => $country,
# root credentials & details
password => undef,
mailto => 'mail@example.invalid',
+ root_ssh_keys => [],
# network related
mngmt_nic => undef,
sub set_mailto { set_key('mailto', $_[0]); }
sub get_mailto { return get('mailto'); }
+sub set_root_ssh_keys { set_key('root_ssh_keys', $_[0]); }
+sub get_root_ssh_keys { return get('root_ssh_keys'); }
+
sub set_mngmt_nic { set_key('mngmt_nic', $_[0]); }
sub get_mngmt_nic { return get('mngmt_nic'); }
sub set_target_cmdline { set_key('target_cmdline', $_[0]); }
sub get_target_cmdline { return get('target_cmdline'); }
+sub set_lvm_auto_rename { set_key('lvm_auto_rename', $_[0]); }
+sub get_lvm_auto_rename { return get('lvm_auto_rename'); }
1;
use Proxmox::Log;
use Proxmox::Sys::Command qw(run_command CMD_FINISHED);
-use Proxmox::Sys::File qw(file_read_firstline);
+use Proxmox::Sys::File qw(file_read_all file_read_firstline);
use Proxmox::Sys::Block;
use Proxmox::Sys::Net;
$output->{hvm_supported} = query_cpu_hvm_support();
$output->{boot_type} = -d '/sys/firmware/efi' ? 'efi' : 'bios';
+ if ($output->{boot_type} eq 'efi') {
+ my $content = eval { file_read_all("/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c") };
+ if ($@) {
+ log_warn("Failed to read secure boot state: $@\n");
+ } else {
+ my @secureboot = unpack("CCCCC", $content);
+ $output->{secure_boot} = $secureboot[4] == 1;
+ }
+ }
+
my $err;
my $country;
if ($routes->{gateway4}) {
if (defined($country)) {
$output->{country} = $country;
} else {
- warn ($err // "unable to detect country\n");
+ warn ($err || "unable to detect country\n");
}
return $output;
Sys/Command.pm \
Sys/File.pm \
Sys/Net.pm \
+ Sys/Udev.pm \
UI.pm \
UI/Base.pm \
UI/Gtk3.pm \
use Proxmox::Install::ISOEnv;
use Proxmox::Sys::Command qw(syscmd);
use Proxmox::Sys::File qw(file_read_firstline);
+use Proxmox::Sys::Udev;
use Proxmox::UI;
use base qw(Exporter);
next if $bd =~ m|^/sys/block/fd\d+$|;
next if $bd =~ m|^/sys/block/sr\d+$|;
- my $info = `udevadm info --path $bd --query all`;
+ # TODO: switch to get_udev_properties and switch from regex to checking the parsed properties
+ my $info = Proxmox::Sys::Udev::query_udevadm_info($bd);
next if !$info;
next if $info !~ m/^E: DEVTYPE=disk$/m;
next if $info =~ m/^E: ID_CDROM/m;
syscmd("udevadm settle --timeout 10");
};
+sub udevadm_disk_details {
+ my $disks = get_cached_disks();
+ my $result = {};
+ for my $disk_info ($disks->@*) {
+ my ($dev_index, $sys_path) = ($disk_info->[0], $disk_info->[5]);
+ $result->{$dev_index} = Proxmox::Sys::Udev::get_udev_properties($sys_path);
+ }
+ return $result;
+}
+
sub wipe_disk {
my ($disk) = @_;
use strict;
use warnings;
+use Proxmox::Sys::Udev;
+
use base qw(Exporter);
our @EXPORT_OK = qw(parse_ip_address parse_ip_mask parse_fqdn);
}
}
+sub udevadm_netdev_details {
+ my $ip_config = get_ip_config();
+
+ my $result = {};
+ for my $dev (values $ip_config->{ifaces}->%*) {
+ my $name = $dev->{name};
+ $result->{$name} = Proxmox::Sys::Udev::get_udev_properties("/sys/class/net/$name");
+ }
+ return $result;
+}
+
# Tries to detect the FQDN hostname for this system via DHCP, if available.
#
# DHCP server can set option 12 to inform the client about it's hostname [0]. dhclient dumps all
if $text =~ /^[0-9]+(?:\.|$)/;
die "FQDN must only consist of alphanumeric characters and dashes\n"
- if $text !~ m/^${Proxmox::Sys::Net::FQDN_RE}$/;
+ if $text !~ m/^${FQDN_RE}$/;
if ($text =~ m/^([^\.]+)\.(\S+)$/) {
return ($1, $2);
--- /dev/null
+package Proxmox::Sys::Udev;
+
+use strict;
+use warnings;
+
+my $UDEV_REGEX = qr/^E: ([^=]+)=(.*)$/;
+
+sub query_udevadm_info {
+ my ($sys_path) = @_;
+
+ my $info = `udevadm info --path $sys_path --query all`;
+ warn "no details found for device '${sys_path}'\n" if !$info;
+ return $info;
+}
+
+# return hash of E: properties returned by udevadm
+sub parse_udevadm_info {
+ my ($udev_raw) = @_;
+
+ my $details = {};
+ for my $line (split('\n', $udev_raw)) {
+ if ($line =~ $UDEV_REGEX) {
+ $details->{$1} = $2;
+ }
+ }
+ return $details;
+}
+
+sub get_udev_properties {
+ my ($sys_path) = @_;
+ my $info = query_udevadm_info($sys_path) or return;
+ return parse_udevadm_info($info);
+}
+
+1;
+proxmox-installer (8.2.5) bookworm; urgency=medium
+
+ * fetch answer: really try lower-case variant of label too
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 24 Apr 2024 13:11:42 +0200
+
+proxmox-installer (8.2.4) bookworm; urgency=medium
+
+ * auto-installer: shorten partition label to "PROXMOX-AIS" so that it
+ fits in the 11 characters limit for labels when using the FAT file-
+ system
+
+ * assistant: fix newline before ':' in help usage text
+
+ * assistant: updated remaining error messages to reworked CLI
+
+ * answer: perform basic input validation for keyboard
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 24 Apr 2024 10:55:05 +0200
+
+proxmox-installer (8.2.3) bookworm; urgency=medium
+
+ * also skip installing the proxmox-grub meta package if not booted in EFI
+ mode
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 24 Apr 2024 00:16:36 +0200
+
+proxmox-installer (8.2.2) bookworm; urgency=medium
+
+ * auto-installer:
+ - rename `global.password` option to `global.root_password`
+ - move `system.root_ssh_keys` option to `global` section
+ - support UTC as timezone
+ - drop fetch-from auto mode to avoid complexity and querying (potentially
+ untrusted) networks by default
+ - rework the default filename suffix that gets added to the default
+ output filename of the prepared iso
+ - move ssh keys setup to low-level installer, avoiding command injection
+ - rework command line options:
+ - rename 'install-mode' option to 'fetch-from'
+ - rename 'included' fetch-from variant to 'iso'
+ - rename 'source' to 'input' and 'target' to 'output'
+ - drop all short options for now
+ - update help text
+ - report every progress update and include text
+
+ * low level installer:
+ - drop printing about extracting deb packages to stderr
+ - avoid undef warning in progress log when using ZFS
+ - only log about creating SWAP if there's any configured
+
+ * fetch answer tool: allow one to override the fetch-from mode through CLI
+ arguments, which can be useful for debugging or to try the automatic
+ installer without preparing the ISO first
+
+ * installer init: start debug shell if auto-installation is enabled without
+ config
+
+ * assistant: error out on if there's a static network config set together
+ with from-dhcp as source
+
+ * tui: update screen during installation only when necessary
+
+ * less strict regex for matching udev env variables
+
+ * skip new proxmox-secure-boot-support package if secureboot is not enabled
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 23 Apr 2024 21:30:02 +0200
+
+proxmox-installer (8.2.1) bookworm; urgency=medium
+
+ * low-level install: add option to automatically rename LVM volumes
+
+ * auto-installer: always set new `lvm_auto_rename` option to avoid prompt
+ that breaks installation flow
+
+ * assistant: prepare iso: avoid an useless intermediate copy of the answer
+ file
+
+ * d/control: recommend xorriso as dependency for the assistant package
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 22 Apr 2024 17:50:41 +0200
+
+proxmox-installer (8.2.0) bookworm; urgency=medium
+
+ * unconfigured: move terminal size setting before starting debug shell
+
+ * html: pbs: fix missing <br> in template after feature list
+
+ * run env: use default error message if country detection failed with empty
+ string
+
+ * add tech-preview for unattended automatic installation through a premade,
+ but flexible answer file
+
+ * add new proxmox-auto-install-assistant to provide a tool to help with
+ preparing an ISO and answer files for automated installation
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 18 Apr 2024 22:37:04 +0200
+
proxmox-installer (8.1.12) bookworm; urgency=medium
* installation: handle if the clamav-clamonacc.service is already disabled
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: cargo:native,
debhelper-compat (= 12),
+ iproute2,
iso-codes,
libgtk3-perl,
libpve-common-perl,
librsvg2-bin,
+ librust-anyhow-1-dev,
+ librust-clap-4+derive-dev,
librust-cursive+termion-backend-dev (>= 0.20.0),
+ librust-glob-0.3-dev,
+ librust-hex-0.4-dev,
+ librust-native-tls-dev,
+ librust-nix-0.26+default-dev,
librust-regex-1+default-dev (>= 1.7~~),
+ librust-rustls-0.20+dangerous-configuration-dev,
+ librust-rustls-native-certs-dev,
librust-serde-1+default-dev,
librust-serde-json-1+default-dev,
+ librust-serde-plain-1+default-dev,
+ librust-sha2-0.10-dev,
+ librust-toml-0.7-dev,
+ librust-ureq-2.6-dev,
libtest-mockmodule-perl,
perl,
rustc:native,
Architecture: any
Depends: chrony,
geoip-bin,
+ iproute2,
libgtk3-perl,
libgtk3-webkit2-perl,
libjson-perl,
* Proxmox VE.
* Proxmox Mail Gateway.
* Proxmox Backup Server
+
+Package: proxmox-auto-install-assistant
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends},
+Recommends: xorriso,
+Description: Assistant to help with automated installations
+ Provides a helper that can assist with creating an answer file for a automated
+ installation of a Proxmox project, and preparing a official ISO image to use
+ this answer file.
+.Xdefaults
+.spice-vdagent.sh
+.xinitrc
banner/pbs-banner.png /var/lib/proxmox-installer
banner/pmg-banner.png /var/lib/proxmox-installer
banner/pve-banner.png /var/lib/proxmox-installer
+etc
html /var/lib/proxmox-installer/
+sbin
+usr/share
+usr/bin/checktime
+usr/bin/proxinstall
+usr/bin/proxmox-auto-installer
+usr/bin/proxmox-chroot
+usr/bin/proxmox-fetch-answer
+usr/bin/proxmox-low-level-installer
+usr/bin/proxmox-tui-installer
+var
+++ /dev/null
-html-pbs/extract1-license.htm /var/lib/pve-installer/html
-html-pbs/extract2-rulesystem.htm /var/lib/pve-installer/html
-html-pbs/extract3-spam.htm /var/lib/pve-installer/html
-html-pbs/extract4-virus.htm /var/lib/pve-installer/html
-html-pbs/page1.htm /var/lib/pve-installer/html
-html-pbs/passwd.htm /var/lib/pve-installer/html
-pbs-banner.png /var/lib/pve-installer
+++ /dev/null
-html-pmg/extract1-license.htm /var/lib/pve-installer/html
-html-pmg/extract2-rulesystem.htm /var/lib/pve-installer/html
-html-pmg/extract3-spam.htm /var/lib/pve-installer/html
-html-pmg/extract4-virus.htm /var/lib/pve-installer/html
-html-pmg/page1.htm /var/lib/pve-installer/html
-html-pmg/passwd.htm /var/lib/pve-installer/html
-pmg-banner.png /var/lib/pve-installer
--- /dev/null
+usr/bin/proxmox-auto-install-assistant
+++ /dev/null
-html-pve/extract1-license.htm /var/lib/pve-installer/html
-html-pve/extract2-rulesystem.htm /var/lib/pve-installer/html
-html-pve/extract3-spam.htm /var/lib/pve-installer/html
-html-pve/extract4-virus.htm /var/lib/pve-installer/html
-html-pve/page1.htm /var/lib/pve-installer/html
-html-pve/passwd.htm /var/lib/pve-installer/html
-pve-banner.png /var/lib/pve-installer
--- /dev/null
+3.0 (native)
--- /dev/null
+[package]
+name = "proxmox-auto-install-assistant"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Aaron Lauterer <a.lauterer@proxmox.com>",
+ "Proxmox Support Team <support@proxmox.com>",
+]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "4.0", features = ["derive"] }
+glob = "0.3"
+log = "0.4.20"
+proxmox-auto-installer = { path = "../proxmox-auto-installer" }
+regex = "1.7"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+toml = "0.7"
--- /dev/null
+use anyhow::{bail, Result};
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use glob::Pattern;
+use regex::Regex;
+use serde::Serialize;
+use std::{
+ collections::BTreeMap,
+ fs,
+ io::{self, Read},
+ path::{Path, PathBuf},
+ process::{Command, Stdio},
+};
+
+use proxmox_auto_installer::{
+ answer::Answer,
+ answer::FilterMatch,
+ sysinfo::SysInfo,
+ utils::{
+ get_matched_udev_indexes, get_nic_list, get_single_udev_index, AutoInstSettings,
+ FetchAnswerFrom, HttpOptions,
+ },
+};
+
+static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable";
+
+/// This tool can be used to prepare a Proxmox installation ISO for automated installations.
+/// Additional uses are to validate the format of an answer file or to test match filters and
+/// print information on the properties to match against for the current hardware.
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ PrepareIso(CommandPrepareISO),
+ ValidateAnswer(CommandValidateAnswer),
+ DeviceMatch(CommandDeviceMatch),
+ DeviceInfo(CommandDeviceInfo),
+ SystemInfo(CommandSystemInfo),
+}
+
+/// Show device information that can be used for filters
+#[derive(Args, Debug)]
+struct CommandDeviceInfo {
+ /// For which device type information should be shown
+ #[arg(name="type", short, long, value_enum, default_value_t=AllDeviceTypes::All)]
+ device: AllDeviceTypes,
+}
+
+/// Test which devices the given filter matches against
+///
+/// Filters support the following syntax:
+/// ? Match a single character
+/// * Match any number of characters
+/// [a], [0-9] Specifc character or range of characters
+/// [!a] Negate a specific character of range
+///
+/// To avoid globbing characters being interpreted by the shell, use single quotes.
+/// Multiple filters can be defined.
+///
+/// Examples:
+/// Match disks against the serial number and device name, both must match:
+///
+/// proxmox-auto-install-assistant match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*'
+#[derive(Args, Debug)]
+#[command(verbatim_doc_comment)]
+struct CommandDeviceMatch {
+ /// Device type to match the filter against
+ r#type: Devicetype,
+
+ /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string.
+ /// Multiple filters are possible, separated by a space.
+ filter: Vec<String>,
+
+ /// Defines if any filter or all filters must match.
+ #[arg(long, value_enum, default_value_t=FilterMatch::Any)]
+ filter_match: FilterMatch,
+}
+
+/// Validate if an answer file is formatted correctly.
+#[derive(Args, Debug)]
+struct CommandValidateAnswer {
+ /// Path to the answer file
+ path: PathBuf,
+ #[arg(short, long, default_value_t = false)]
+ debug: bool,
+}
+
+/// Prepare an ISO for automated installation.
+///
+/// The behavior of how to fetch an answer file must be set with the '--fetch-from' parameter. The
+/// answer file can be:{n}
+/// * integrated into the ISO itself ('iso'){n}
+/// * present on a partition / file-system with the label 'PROXMOX-AIS' (Proxmox
+/// Automated Installer Source) ('partition'){n}
+/// * requested via an HTTP Post request ('http').
+///
+/// The URL for the HTTP mode can be defined for the ISO with the '--url' argument. If not present,
+/// it will try to get a URL from a DHCP option (250, TXT) or by querying a DNS TXT record for the
+/// domain 'proxmox-auto-installer.{search domain}'.
+///
+/// The TLS certificate fingerprint can either be defined via the '--cert-fingerprint' argument or
+/// alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located at
+/// 'proxmox-auto-installer-cert-fingerprint.{search domain}'.
+///
+/// The latter options to provide the TLS fingerprint will only be used if the same method was used
+/// to retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if
+/// no one was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via
+/// the DNS TXT record.
+#[derive(Args, Debug)]
+struct CommandPrepareISO {
+ /// Path to the source ISO to prepare
+ input: PathBuf,
+
+ /// Path to store the final ISO to, defaults to an auto-generated file name depending on mode
+ /// and the same directory as the source file is located in.
+ #[arg(long)]
+ output: Option<PathBuf>,
+
+ /// Where the automatic installer should fetch the answer file from.
+ #[arg(long, value_enum)]
+ fetch_from: FetchAnswerFrom,
+
+ /// Include the specified answer file in the ISO. Requires the '--fetch-from' parameter
+ /// to be set to 'iso'.
+ #[arg(long)]
+ answer_file: Option<PathBuf>,
+
+ /// Specify URL for fetching the answer file via HTTP
+ #[arg(long)]
+ url: Option<String>,
+
+ /// Pin the ISO to the specified SHA256 TLS certificate fingerprint.
+ #[arg(long)]
+ cert_fingerprint: Option<String>,
+
+ /// Staging directory to use for preparing the new ISO file. Defaults to the directory of the
+ /// input ISO file.
+ #[arg(long)]
+ tmp: Option<String>,
+}
+
+/// Show the system information that can be used to identify a host.
+///
+/// The shown information is sent as POST HTTP request when fetching the answer file for the
+/// automatic installation through HTTP, You can, for example, use this to return a dynamically
+/// assembled answer file.
+#[derive(Args, Debug)]
+struct CommandSystemInfo {}
+
+#[derive(Args, Debug)]
+struct GlobalOpts {
+ /// Output format
+ #[arg(long, short, value_enum)]
+ format: OutputFormat,
+}
+
+#[derive(Clone, Debug, ValueEnum, PartialEq)]
+enum AllDeviceTypes {
+ All,
+ Network,
+ Disk,
+}
+
+#[derive(Clone, Debug, ValueEnum)]
+enum Devicetype {
+ Network,
+ Disk,
+}
+
+#[derive(Clone, Debug, ValueEnum)]
+enum OutputFormat {
+ Pretty,
+ Json,
+}
+
+#[derive(Serialize)]
+struct Devs {
+ disks: Option<BTreeMap<String, BTreeMap<String, String>>>,
+ nics: Option<BTreeMap<String, BTreeMap<String, String>>>,
+}
+
+fn main() {
+ let args = Cli::parse();
+ let res = match &args.command {
+ Commands::PrepareIso(args) => prepare_iso(args),
+ Commands::ValidateAnswer(args) => validate_answer(args),
+ Commands::DeviceInfo(args) => info(args),
+ Commands::DeviceMatch(args) => match_filter(args),
+ Commands::SystemInfo(args) => show_system_info(args),
+ };
+ if let Err(err) = res {
+ eprintln!("{err}");
+ std::process::exit(1);
+ }
+}
+
+fn info(args: &CommandDeviceInfo) -> Result<()> {
+ let mut devs = Devs {
+ disks: None,
+ nics: None,
+ };
+
+ if args.device == AllDeviceTypes::Network || args.device == AllDeviceTypes::All {
+ match get_nics() {
+ Ok(res) => devs.nics = Some(res),
+ Err(err) => bail!("Error getting NIC data: {err}"),
+ }
+ }
+ if args.device == AllDeviceTypes::Disk || args.device == AllDeviceTypes::All {
+ match get_disks() {
+ Ok(res) => devs.disks = Some(res),
+ Err(err) => bail!("Error getting disk data: {err}"),
+ }
+ }
+ println!("{}", serde_json::to_string_pretty(&devs).unwrap());
+ Ok(())
+}
+
+fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
+ let devs: BTreeMap<String, BTreeMap<String, String>> = match args.r#type {
+ Devicetype::Disk => get_disks().unwrap(),
+ Devicetype::Network => get_nics().unwrap(),
+ };
+ // parse filters
+
+ let mut filters: BTreeMap<String, String> = BTreeMap::new();
+
+ for f in &args.filter {
+ match f.split_once('=') {
+ Some((key, value)) => {
+ if key.is_empty() || value.is_empty() {
+ bail!("Filter key or value is empty in filter: '{f}'");
+ }
+ filters.insert(String::from(key), String::from(value));
+ }
+ None => {
+ bail!("Could not find separator '=' in filter: '{f}'");
+ }
+ }
+ }
+
+ // align return values
+ let result = match args.r#type {
+ Devicetype::Disk => {
+ get_matched_udev_indexes(&filters, &devs, args.filter_match == FilterMatch::All)
+ }
+ Devicetype::Network => get_single_udev_index(&filters, &devs).map(|r| vec![r]),
+ };
+
+ match result {
+ Ok(result) => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
+ Err(err) => bail!("Error matching filters: {err}"),
+ }
+ Ok(())
+}
+
+fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
+ let answer = parse_answer(&args.path)?;
+ if args.debug {
+ println!("Parsed data from answer file:\n{:#?}", answer);
+ }
+ Ok(())
+}
+
+fn show_system_info(_args: &CommandSystemInfo) -> Result<()> {
+ match SysInfo::as_json_pretty() {
+ Ok(res) => println!("{res}"),
+ Err(err) => eprintln!("Error fetching system info: {err}"),
+ }
+ Ok(())
+}
+
+fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
+ check_prepare_requirements(args)?;
+
+ if args.fetch_from == FetchAnswerFrom::Iso && args.answer_file.is_none() {
+ bail!("Missing path to the answer file required for the fetch-from 'iso' mode.");
+ }
+ if args.url.is_some() && args.fetch_from != FetchAnswerFrom::Http {
+ bail!(
+ "Setting a URL is incompatible with the fetch-from '{:?}' mode, only works with the 'http' mode",
+ args.fetch_from,
+ );
+ }
+ if args.cert_fingerprint.is_some() && args.fetch_from != FetchAnswerFrom::Http {
+ bail!(
+ "Setting a certificate fingerprint incompatible is fetch-from '{:?}' mode, only works for 'http' mode.",
+ args.fetch_from,
+ );
+ }
+ if args.answer_file.is_some() && args.fetch_from != FetchAnswerFrom::Iso {
+ bail!("You must set '--fetch-from' to 'iso' to place the answer file directly in the ISO.");
+ }
+
+ if let Some(file) = &args.answer_file {
+ println!("Checking provided answer file...");
+ parse_answer(file)?;
+ }
+
+ let iso_target = final_iso_location(args);
+ let iso_target_file_name = match iso_target.file_name() {
+ None => bail!("no base filename in target ISO path found"),
+ Some(source_file_name) => source_file_name.to_string_lossy(),
+ };
+
+ let mut tmp_base = PathBuf::new();
+ match args.tmp.as_ref() {
+ Some(tmp_dir) => tmp_base.push(tmp_dir),
+ None => tmp_base.push(iso_target.parent().unwrap()),
+ }
+
+ let mut tmp_iso = tmp_base.clone();
+ tmp_iso.push(format!("{iso_target_file_name}.tmp",));
+
+ println!("Copying source ISO to temporary location...");
+ fs::copy(&args.input, &tmp_iso)?;
+
+ println!("Preparing ISO...");
+ let config = AutoInstSettings {
+ mode: args.fetch_from.clone(),
+ http: HttpOptions {
+ url: args.url.clone(),
+ cert_fingerprint: args.cert_fingerprint.clone(),
+ },
+ };
+ let mut instmode_file_tmp = tmp_base.clone();
+ instmode_file_tmp.push("auto-installer-mode.toml");
+ fs::write(&instmode_file_tmp, toml::to_string_pretty(&config)?)?;
+
+ inject_file_to_iso(&tmp_iso, &instmode_file_tmp, "/auto-installer-mode.toml")?;
+
+ if let Some(answer_file) = &args.answer_file {
+ inject_file_to_iso(&tmp_iso, answer_file, "/answer.toml")?;
+ }
+
+ println!("Moving prepared ISO to target location...");
+ fs::rename(&tmp_iso, &iso_target)?;
+ println!("Final ISO is available at {iso_target:?}.");
+
+ Ok(())
+}
+
+fn final_iso_location(args: &CommandPrepareISO) -> PathBuf {
+ if let Some(specified) = args.output.clone() {
+ return specified;
+ }
+ let mut suffix: String = match args.fetch_from {
+ FetchAnswerFrom::Http => "auto-from-http",
+ FetchAnswerFrom::Iso => "auto-from-iso",
+ FetchAnswerFrom::Partition => "auto-from-partition",
+ }
+ .into();
+
+ if args.url.is_some() {
+ suffix.push_str("-url");
+ }
+ if args.cert_fingerprint.is_some() {
+ suffix.push_str("-fp");
+ }
+
+ let base = args.input.parent().unwrap();
+ let iso = args.input.file_stem().unwrap();
+
+ let mut target = base.to_path_buf();
+ target.push(format!("{}-{}.iso", iso.to_str().unwrap(), suffix));
+
+ target.to_path_buf()
+}
+
+fn inject_file_to_iso(iso: &PathBuf, file: &PathBuf, location: &str) -> Result<()> {
+ let result = Command::new("xorriso")
+ .arg("--boot_image")
+ .arg("any")
+ .arg("keep")
+ .arg("-dev")
+ .arg(iso)
+ .arg("-map")
+ .arg(file)
+ .arg(location)
+ .output()?;
+ if !result.status.success() {
+ bail!(
+ "Error injecting {file:?} into {iso:?}: {}",
+ String::from_utf8_lossy(&result.stderr)
+ );
+ }
+ Ok(())
+}
+
+fn get_disks() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
+ let unwantend_block_devs = vec![
+ "ram[0-9]*",
+ "loop[0-9]*",
+ "md[0-9]*",
+ "dm-*",
+ "fd[0-9]*",
+ "sr[0-9]*",
+ ];
+
+ // compile Regex here once and not inside the loop
+ let re_disk = Regex::new(r"(?m)^E: DEVTYPE=disk")?;
+ let re_cdrom = Regex::new(r"(?m)^E: ID_CDROM")?;
+ let re_iso9660 = Regex::new(r"(?m)^E: ID_FS_TYPE=iso9660")?;
+
+ let re_name = Regex::new(r"(?m)^N: (.*)$")?;
+ let re_props = Regex::new(r"(?m)^E: ([^=]+)=(.*)$")?;
+
+ let mut disks: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
+
+ 'outer: for entry in fs::read_dir("/sys/block")? {
+ let entry = entry.unwrap();
+ let filename = entry.file_name().into_string().unwrap();
+
+ for p in &unwantend_block_devs {
+ if Pattern::new(p)?.matches(&filename) {
+ continue 'outer;
+ }
+ }
+
+ let output = match get_udev_properties(&entry.path()) {
+ Ok(output) => output,
+ Err(err) => {
+ eprint!("{err}");
+ continue 'outer;
+ }
+ };
+
+ if !re_disk.is_match(&output) {
+ continue 'outer;
+ };
+ if re_cdrom.is_match(&output) {
+ continue 'outer;
+ };
+ if re_iso9660.is_match(&output) {
+ continue 'outer;
+ };
+
+ let mut name = filename;
+ if let Some(cap) = re_name.captures(&output) {
+ if let Some(res) = cap.get(1) {
+ name = String::from(res.as_str());
+ }
+ }
+
+ let mut udev_props: BTreeMap<String, String> = BTreeMap::new();
+
+ for line in output.lines() {
+ if let Some(caps) = re_props.captures(line) {
+ let key = String::from(caps.get(1).unwrap().as_str());
+ let value = String::from(caps.get(2).unwrap().as_str());
+ udev_props.insert(key, value);
+ }
+ }
+
+ disks.insert(name, udev_props);
+ }
+ Ok(disks)
+}
+
+fn get_nics() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
+ let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?;
+ let mut nics: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
+
+ let links = get_nic_list()?;
+ for link in links {
+ let path = format!("/sys/class/net/{link}");
+
+ let output = match get_udev_properties(&PathBuf::from(path)) {
+ Ok(output) => output,
+ Err(err) => {
+ eprint!("{err}");
+ continue;
+ }
+ };
+
+ let mut udev_props: BTreeMap<String, String> = BTreeMap::new();
+
+ for line in output.lines() {
+ if let Some(caps) = re_props.captures(line) {
+ let key = String::from(caps.get(1).unwrap().as_str());
+ let value = String::from(caps.get(2).unwrap().as_str());
+ udev_props.insert(key, value);
+ }
+ }
+
+ nics.insert(link, udev_props);
+ }
+ Ok(nics)
+}
+
+fn get_udev_properties(path: &PathBuf) -> Result<String> {
+ let udev_output = Command::new("udevadm")
+ .arg("info")
+ .arg("--path")
+ .arg(path)
+ .arg("--query")
+ .arg("all")
+ .output()?;
+ if !udev_output.status.success() {
+ bail!("could not run udevadm successfully for {path:?}");
+ }
+ Ok(String::from_utf8(udev_output.stdout)?)
+}
+
+fn parse_answer(path: &PathBuf) -> Result<Answer> {
+ let mut file = match fs::File::open(path) {
+ Ok(file) => file,
+ Err(err) => bail!("Opening answer file {path:?} failed: {err}"),
+ };
+ let mut contents = String::new();
+ if let Err(err) = file.read_to_string(&mut contents) {
+ bail!("Reading from file {path:?} failed: {err}");
+ }
+ match toml::from_str(&contents) {
+ Ok(answer) => {
+ println!("The file was parsed successfully, no syntax errors found!");
+ Ok(answer)
+ }
+ Err(err) => bail!("Error parsing answer file: {err}"),
+ }
+}
+
+fn check_prepare_requirements(args: &CommandPrepareISO) -> Result<()> {
+ match Path::try_exists(&args.input) {
+ Ok(true) => (),
+ Ok(false) => bail!("Source file does not exist."),
+ Err(_) => bail!("Source file does not exist."),
+ }
+
+ match Command::new("xorriso")
+ .arg("-dev")
+ .arg(&args.input)
+ .arg("-find")
+ .arg(PROXMOX_ISO_FLAG)
+ .stderr(Stdio::null())
+ .stdout(Stdio::null())
+ .status()
+ {
+ Ok(v) => {
+ if !v.success() {
+ bail!("The source ISO file is not able to be installed automatically. Please try a more current one.");
+ }
+ }
+ Err(err) if err.kind() == io::ErrorKind::NotFound => {
+ bail!("Could not find the 'xorriso' binary. Please install it.")
+ }
+ Err(err) => bail!("unexpected error when trying to execute 'xorriso' - {err}"),
+ };
+
+ Ok(())
+}
--- /dev/null
+[package]
+name = "proxmox-auto-installer"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Aaron Lauterer <a.lauterer@proxmox.com>",
+ "Proxmox Support Team <support@proxmox.com>",
+]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "4.0", features = ["derive"] }
+glob = "0.3"
+log = "0.4.20"
+proxmox-installer-common = { path = "../proxmox-installer-common" }
+regex = "1.7"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_plain = "1.0"
+toml = "0.7"
--- /dev/null
+use clap::ValueEnum;
+use proxmox_installer_common::{
+ options::{BtrfsRaidLevel, FsType, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel},
+ utils::{CidrAddress, Fqdn},
+};
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeMap, net::IpAddr};
+
+// BTreeMap is used to store filters as the order of the filters will be stable, compared to
+// storing them in a HashMap
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct Answer {
+ pub global: Global,
+ pub network: Network,
+ #[serde(rename = "disk-setup")]
+ pub disks: Disks,
+}
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
+pub struct Global {
+ pub country: String,
+ pub fqdn: Fqdn,
+ pub keyboard: KeyboardLayout,
+ pub mailto: String,
+ pub timezone: String,
+ pub root_password: String,
+ #[serde(default)]
+ pub reboot_on_error: bool,
+ #[serde(default)]
+ pub root_ssh_keys: Vec<String>,
+}
+
+#[derive(Clone, Deserialize, Debug, Default, PartialEq)]
+#[serde(deny_unknown_fields)]
+enum NetworkConfigMode {
+ #[default]
+ #[serde(rename = "from-dhcp")]
+ FromDhcp,
+ #[serde(rename = "from-answer")]
+ FromAnswer,
+}
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
+struct NetworkInAnswer {
+ #[serde(default)]
+ pub source: NetworkConfigMode,
+ pub cidr: Option<CidrAddress>,
+ pub dns: Option<IpAddr>,
+ pub gateway: Option<IpAddr>,
+ pub filter: Option<BTreeMap<String, String>>,
+}
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(try_from = "NetworkInAnswer", deny_unknown_fields)]
+pub struct Network {
+ pub network_settings: NetworkSettings,
+}
+
+impl TryFrom<NetworkInAnswer> for Network {
+ type Error = &'static str;
+
+ fn try_from(network: NetworkInAnswer) -> Result<Self, Self::Error> {
+ if network.source == NetworkConfigMode::FromAnswer {
+ if network.cidr.is_none() {
+ return Err("Field 'cidr' must be set.");
+ }
+ if network.dns.is_none() {
+ return Err("Field 'dns' must be set.");
+ }
+ if network.gateway.is_none() {
+ return Err("Field 'gateway' must be set.");
+ }
+ if network.filter.is_none() {
+ return Err("Field 'filter' must be set.");
+ }
+
+ Ok(Network {
+ network_settings: NetworkSettings::Manual(NetworkManual {
+ cidr: network.cidr.unwrap(),
+ dns: network.dns.unwrap(),
+ gateway: network.gateway.unwrap(),
+ filter: network.filter.unwrap(),
+ }),
+ })
+ } else {
+ if network.cidr.is_some() {
+ return Err("Field 'cidr' not supported for 'from-dhcp' config.");
+ }
+ if network.dns.is_some() {
+ return Err("Field 'dns' not supported for 'from-dhcp' config.");
+ }
+ if network.gateway.is_some() {
+ return Err("Field 'gateway' not supported for 'from-dhcp' config.");
+ }
+ if network.filter.is_some() {
+ return Err("Field 'filter' not supported for 'from-dhcp' config.");
+ }
+
+ Ok(Network {
+ network_settings: NetworkSettings::FromDhcp,
+ })
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum NetworkSettings {
+ FromDhcp,
+ Manual(NetworkManual),
+}
+
+#[derive(Clone, Debug)]
+pub struct NetworkManual {
+ pub cidr: CidrAddress,
+ pub dns: IpAddr,
+ pub gateway: IpAddr,
+ pub filter: BTreeMap<String, String>,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct DiskSetup {
+ pub filesystem: Filesystem,
+ #[serde(default)]
+ pub disk_list: Vec<String>,
+ pub filter: Option<BTreeMap<String, String>>,
+ pub filter_match: Option<FilterMatch>,
+ pub zfs: Option<ZfsOptions>,
+ pub lvm: Option<LvmOptions>,
+ pub btrfs: Option<BtrfsOptions>,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(try_from = "DiskSetup", deny_unknown_fields)]
+pub struct Disks {
+ pub fs_type: FsType,
+ pub disk_selection: DiskSelection,
+ pub filter_match: Option<FilterMatch>,
+ pub fs_options: FsOptions,
+}
+
+impl TryFrom<DiskSetup> for Disks {
+ type Error = &'static str;
+
+ fn try_from(source: DiskSetup) -> Result<Self, Self::Error> {
+ if source.disk_list.is_empty() && source.filter.is_none() {
+ return Err("Need either 'disk_list' or 'filter' set");
+ }
+ if !source.disk_list.is_empty() && source.filter.is_some() {
+ return Err("Cannot use both, 'disk_list' and 'filter'");
+ }
+
+ let disk_selection = if !source.disk_list.is_empty() {
+ DiskSelection::Selection(source.disk_list.clone())
+ } else {
+ DiskSelection::Filter(source.filter.clone().unwrap())
+ };
+
+ let lvm_checks = |source: &DiskSetup| -> Result<(), Self::Error> {
+ if source.zfs.is_some() || source.btrfs.is_some() {
+ return Err("make sure only 'lvm' options are set");
+ }
+ if source.disk_list.len() > 1 {
+ return Err("make sure to define only one disk for ext4 and xfs");
+ }
+ Ok(())
+ };
+ // TODO: improve checks for foreign FS options. E.g. less verbose and handling new FS types
+ // automatically
+ let (fs, fs_options) = match source.filesystem {
+ Filesystem::Xfs => {
+ lvm_checks(&source)?;
+ (FsType::Xfs, FsOptions::LVM(source.lvm.unwrap_or_default()))
+ }
+ Filesystem::Ext4 => {
+ lvm_checks(&source)?;
+ (FsType::Ext4, FsOptions::LVM(source.lvm.unwrap_or_default()))
+ }
+ Filesystem::Zfs => {
+ if source.lvm.is_some() || source.btrfs.is_some() {
+ return Err("make sure only 'zfs' options are set");
+ }
+ match source.zfs {
+ None | Some(ZfsOptions { raid: None, .. }) => {
+ return Err("ZFS raid level 'zfs.raid' must be set")
+ }
+ Some(opts) => (FsType::Zfs(opts.raid.unwrap()), FsOptions::ZFS(opts)),
+ }
+ }
+ Filesystem::Btrfs => {
+ if source.zfs.is_some() || source.lvm.is_some() {
+ return Err("make sure only 'btrfs' options are set");
+ }
+ match source.btrfs {
+ None | Some(BtrfsOptions { raid: None, .. }) => {
+ return Err("BTRFS raid level 'btrfs.raid' must be set")
+ }
+ Some(opts) => (FsType::Btrfs(opts.raid.unwrap()), FsOptions::BTRFS(opts)),
+ }
+ }
+ };
+
+ let res = Disks {
+ fs_type: fs,
+ disk_selection,
+ filter_match: source.filter_match,
+ fs_options,
+ };
+ Ok(res)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum FsOptions {
+ LVM(LvmOptions),
+ ZFS(ZfsOptions),
+ BTRFS(BtrfsOptions),
+}
+
+#[derive(Clone, Debug)]
+pub enum DiskSelection {
+ Selection(Vec<String>),
+ Filter(BTreeMap<String, String>),
+}
+#[derive(Clone, Deserialize, Debug, PartialEq, ValueEnum)]
+#[serde(rename_all = "lowercase", deny_unknown_fields)]
+pub enum FilterMatch {
+ Any,
+ All,
+}
+
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
+#[serde(rename_all = "lowercase", deny_unknown_fields)]
+pub enum Filesystem {
+ Ext4,
+ Xfs,
+ Zfs,
+ Btrfs,
+}
+
+#[derive(Clone, Copy, Default, Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
+pub struct ZfsOptions {
+ pub raid: Option<ZfsRaidLevel>,
+ pub ashift: Option<usize>,
+ pub arc_max: Option<usize>,
+ pub checksum: Option<ZfsChecksumOption>,
+ pub compress: Option<ZfsCompressOption>,
+ pub copies: Option<usize>,
+ pub hdsize: Option<f64>,
+}
+
+#[derive(Clone, Copy, Default, Deserialize, Serialize, Debug)]
+#[serde(deny_unknown_fields)]
+pub struct LvmOptions {
+ pub hdsize: Option<f64>,
+ pub swapsize: Option<f64>,
+ pub maxroot: Option<f64>,
+ pub maxvz: Option<f64>,
+ pub minfree: Option<f64>,
+}
+
+#[derive(Clone, Copy, Default, Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
+pub struct BtrfsOptions {
+ pub hdsize: Option<f64>,
+ pub raid: Option<BtrfsRaidLevel>,
+}
+
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub enum KeyboardLayout {
+ De,
+ DeCh,
+ Dk,
+ EnGb,
+ EnUs,
+ Es,
+ Fi,
+ Fr,
+ FrBe,
+ FrCa,
+ FrCh,
+ Hu,
+ Is,
+ It,
+ Jp,
+ Lt,
+ Mk,
+ Nl,
+ No,
+ Pl,
+ Pt,
+ PtBr,
+ Se,
+ Si,
+ Tr,
+}
+
+serde_plain::derive_display_from_serialize!(KeyboardLayout);
--- /dev/null
+use anyhow::{bail, format_err, Result};
+use log::{error, info, LevelFilter};
+use std::{
+ env,
+ io::{BufRead, BufReader, Write},
+ path::PathBuf,
+ process::ExitCode,
+};
+
+use proxmox_installer_common::setup::{
+ installer_setup, read_json, spawn_low_level_installer, LocaleInfo, RuntimeInfo, SetupInfo,
+};
+
+use proxmox_auto_installer::{
+ answer::Answer,
+ log::AutoInstLogger,
+ udevinfo::UdevInfo,
+ utils::{parse_answer, LowLevelMessage},
+};
+
+static LOGGER: AutoInstLogger = AutoInstLogger;
+
+pub fn init_log() -> Result<()> {
+ AutoInstLogger::init("/tmp/auto_installer.log")?;
+ log::set_logger(&LOGGER)
+ .map(|()| log::set_max_level(LevelFilter::Info))
+ .map_err(|err| format_err!(err))
+}
+
+fn auto_installer_setup(in_test_mode: bool) -> Result<(Answer, UdevInfo)> {
+ let base_path = if in_test_mode { "./testdir" } else { "/" };
+ let mut path = PathBuf::from(base_path);
+
+ path.push("run");
+ path.push("proxmox-installer");
+
+ let udev_info: UdevInfo = {
+ let mut path = path.clone();
+ path.push("run-env-udev.json");
+
+ read_json(&path)
+ .map_err(|err| format_err!("Failed to retrieve udev info details: {err}"))?
+ };
+
+ let mut buffer = String::new();
+ let lines = std::io::stdin().lock().lines();
+ for line in lines {
+ buffer.push_str(&line.unwrap());
+ buffer.push('\n');
+ }
+
+ let answer: Answer =
+ toml::from_str(&buffer).map_err(|err| format_err!("Failed parsing answer file: {err}"))?;
+
+ Ok((answer, udev_info))
+}
+
+fn main() -> ExitCode {
+ if let Err(err) = init_log() {
+ panic!("could not initilize logging: {}", err);
+ }
+
+ let in_test_mode = match env::args().nth(1).as_deref() {
+ Some("-t") => true,
+ // Always force the test directory in debug builds
+ _ => cfg!(debug_assertions),
+ };
+ info!("Starting auto installer");
+
+ let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) {
+ Ok(result) => result,
+ Err(err) => {
+ error!("Installer setup error: {err}");
+ return ExitCode::FAILURE;
+ }
+ };
+
+ let (answer, udevadm_info) = match auto_installer_setup(in_test_mode) {
+ Ok(result) => result,
+ Err(err) => {
+ error!("Autoinstaller setup error: {err}");
+ return ExitCode::FAILURE;
+ }
+ };
+
+ match run_installation(&answer, &locales, &runtime_info, &udevadm_info, &setup_info) {
+ Ok(_) => info!("Installation done."),
+ Err(err) => {
+ error!("Installation failed: {err}");
+ return exit_failure(answer.global.reboot_on_error);
+ }
+ }
+
+ // TODO: (optionally) do a HTTP post with basic system info, like host SSH public key(s) here
+
+ ExitCode::SUCCESS
+}
+
+/// When we exit with a failure, the installer will not automatically reboot.
+/// Default value for reboot_on_error is false
+fn exit_failure(reboot_on_error: bool) -> ExitCode {
+ if reboot_on_error {
+ ExitCode::SUCCESS
+ } else {
+ ExitCode::FAILURE
+ }
+}
+
+fn run_installation(
+ answer: &Answer,
+ locales: &LocaleInfo,
+ runtime_info: &RuntimeInfo,
+ udevadm_info: &UdevInfo,
+ setup_info: &SetupInfo,
+) -> Result<()> {
+ let config = parse_answer(answer, udevadm_info, runtime_info, locales, setup_info)?;
+ info!("Calling low-level installer");
+
+ let mut child = match spawn_low_level_installer(false) {
+ Ok(child) => child,
+ Err(err) => {
+ bail!("Low level installer could not be started: {}", err);
+ }
+ };
+
+ let mut inner = || -> Result<()> {
+ let reader = child
+ .stdout
+ .take()
+ .map(BufReader::new)
+ .ok_or(format_err!("failed to get stdout reader"))?;
+ let mut writer = child
+ .stdin
+ .take()
+ .ok_or(format_err!("failed to get stdin writer"))?;
+
+ serde_json::to_writer(&mut writer, &config)
+ .map_err(|err| format_err!("failed to serialize install config: {err}"))?;
+ writeln!(writer).map_err(|err| format_err!("failed to write install config: {err}"))?;
+
+ for line in reader.lines() {
+ let line = match line {
+ Ok(line) => line,
+ Err(_) => break,
+ };
+ let msg = match serde_json::from_str::<LowLevelMessage>(&line) {
+ Ok(msg) => msg,
+ Err(_) => {
+ // Not a fatal error, so don't abort the installation by returning
+ continue;
+ }
+ };
+
+ match msg.clone() {
+ LowLevelMessage::Info { message } => info!("{message}"),
+ LowLevelMessage::Error { message } => error!("{message}"),
+ LowLevelMessage::Prompt { query } => {
+ bail!("Got interactive prompt I cannot answer: {query}")
+ }
+ LowLevelMessage::Progress { ratio, text } => {
+ let percentage = ratio * 100.;
+ info!("progress {percentage:>5.1} % - {text}");
+ }
+ LowLevelMessage::Finished { state, message } => {
+ if state == "err" {
+ bail!("{message}");
+ }
+ info!("Finished: '{state}' {message}");
+ }
+ };
+ }
+ Ok(())
+ };
+ inner().map_err(|err| format_err!("low level installer returned early: {err}"))
+}
--- /dev/null
+pub mod answer;
+pub mod log;
+pub mod sysinfo;
+pub mod udevinfo;
+pub mod utils;
--- /dev/null
+use anyhow::{bail, Result};
+use log::{Level, Metadata, Record};
+use std::{fs::File, io::Write, sync::Mutex, sync::OnceLock};
+
+pub struct AutoInstLogger;
+static LOGFILE: OnceLock<Mutex<File>> = OnceLock::new();
+
+impl AutoInstLogger {
+ pub fn init(path: &str) -> Result<()> {
+ let f = File::create(path)?;
+ if LOGFILE.set(Mutex::new(f)).is_err() {
+ bail!("Cannot set LOGFILE")
+ }
+ Ok(())
+ }
+}
+
+impl log::Log for AutoInstLogger {
+ fn enabled(&self, metadata: &Metadata) -> bool {
+ metadata.level() <= Level::Info
+ }
+
+ /// Logs to both, stderr and into a log file
+ fn log(&self, record: &Record) {
+ if self.enabled(record.metadata()) {
+ eprintln!("{}: {}", record.level(), record.args());
+ let mut file = LOGFILE
+ .get()
+ .expect("could not get LOGFILE")
+ .lock()
+ .expect("could not get mutex for LOGFILE");
+ writeln!(file, "{}: {}", record.level(), record.args())
+ .expect("could not write to LOGFILE");
+ }
+ }
+
+ fn flush(&self) {
+ LOGFILE
+ .get()
+ .expect("could not get LOGFILE")
+ .lock()
+ .expect("could not get mutex for LOGFILE")
+ .flush()
+ .expect("could not flush LOGFILE");
+ }
+}
--- /dev/null
+use anyhow::{bail, Result};
+use proxmox_installer_common::setup::{IsoInfo, ProductConfig, SetupInfo};
+use serde::Serialize;
+use std::{collections::HashMap, fs, io};
+
+use crate::utils::get_nic_list;
+
+const DMI_PATH: &str = "/sys/devices/virtual/dmi/id";
+
+#[derive(Debug, Serialize)]
+pub struct SysInfo {
+ product: ProductConfig,
+ iso: IsoInfo,
+ dmi: SystemDMI,
+ network_interfaces: Vec<NetdevWithMac>,
+}
+
+impl SysInfo {
+ pub fn get() -> Result<Self> {
+ let setup_info: SetupInfo = match fs::File::open("/run/proxmox-installer/iso-info.json") {
+ Ok(iso_info_file) => {
+ let reader = io::BufReader::new(iso_info_file);
+ serde_json::from_reader(reader)?
+ }
+ Err(err) if err.kind() == io::ErrorKind::NotFound => SetupInfo::mocked(),
+ Err(err) => bail!("failed to open iso-info.json - {err}"),
+ };
+
+ Ok(Self {
+ product: setup_info.config,
+ iso: setup_info.iso_info,
+ network_interfaces: NetdevWithMac::get_all()?,
+ dmi: SystemDMI::get()?,
+ })
+ }
+
+ pub fn as_json_pretty() -> Result<String> {
+ let info = Self::get()?;
+ Ok(serde_json::to_string_pretty(&info)?)
+ }
+
+ pub fn as_json() -> Result<String> {
+ let info = Self::get()?;
+ Ok(serde_json::to_string(&info)?)
+ }
+}
+
+#[derive(Debug, Serialize)]
+struct NetdevWithMac {
+ /// The network link name
+ pub link: String,
+ /// The MAC address of the network device
+ pub mac: String,
+}
+
+impl NetdevWithMac {
+ fn get_all() -> Result<Vec<Self>> {
+ let mut result: Vec<Self> = Vec::new();
+
+ let links = get_nic_list()?;
+ for link in links {
+ let mac = fs::read_to_string(format!("/sys/class/net/{link}/address"))?;
+ let mac = String::from(mac.trim());
+ result.push(Self { link, mac });
+ }
+ Ok(result)
+ }
+}
+
+#[derive(Debug, Serialize)]
+struct SystemDMI {
+ system: HashMap<String, String>,
+ baseboard: HashMap<String, String>,
+ chassis: HashMap<String, String>,
+}
+
+impl SystemDMI {
+ pub(crate) fn get() -> Result<Self> {
+ let system_files = [
+ "product_serial",
+ "product_sku",
+ "product_uuid",
+ "product_name",
+ ];
+ let baseboard_files = ["board_asset_tag", "board_serial", "board_name"];
+ let chassis_files = ["chassis_serial", "chassis_sku", "chassis_asset_tag"];
+
+ Ok(Self {
+ system: Self::get_dmi_infos(&system_files)?,
+ baseboard: Self::get_dmi_infos(&baseboard_files)?,
+ chassis: Self::get_dmi_infos(&chassis_files)?,
+ })
+ }
+
+ fn get_dmi_infos(files: &[&str]) -> Result<HashMap<String, String>> {
+ let mut res: HashMap<String, String> = HashMap::new();
+
+ for file in files {
+ let path = format!("{DMI_PATH}/{file}");
+ let content = match fs::read_to_string(&path) {
+ Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => continue,
+ Err(ref err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
+ bail!("Could not read data. Are you running as root or with sudo?")
+ }
+ Err(err) => bail!("Error: '{err}' on '{path}'"),
+ Ok(content) => content.trim().into(),
+ };
+ let key = file.splitn(2, '_').last().unwrap();
+ res.insert(key.into(), content);
+ }
+
+ Ok(res)
+ }
+}
--- /dev/null
+use serde::Deserialize;
+use std::collections::BTreeMap;
+
+#[derive(Clone, Deserialize, Debug)]
+pub struct UdevInfo {
+ // use BTreeMap to have keys sorted
+ pub disks: BTreeMap<String, BTreeMap<String, String>>,
+ pub nics: BTreeMap<String, BTreeMap<String, String>>,
+}
--- /dev/null
+use anyhow::{bail, Context as _, Result};
+use clap::ValueEnum;
+use glob::Pattern;
+use log::info;
+use std::{collections::BTreeMap, process::Command};
+
+use crate::{
+ answer::{self, Answer},
+ udevinfo::UdevInfo,
+};
+use proxmox_installer_common::{
+ options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption},
+ setup::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo},
+};
+use serde::{Deserialize, Serialize};
+
+pub fn get_network_settings(
+ answer: &Answer,
+ udev_info: &UdevInfo,
+ runtime_info: &RuntimeInfo,
+ setup_info: &SetupInfo,
+) -> Result<NetworkOptions> {
+ let mut network_options = NetworkOptions::defaults_from(setup_info, &runtime_info.network);
+
+ info!("Setting network configuration");
+
+ // Always use the FQDN from the answer file
+ network_options.fqdn = answer.global.fqdn.clone();
+
+ if let answer::NetworkSettings::Manual(settings) = &answer.network.network_settings {
+ network_options.address = settings.cidr.clone();
+ network_options.dns_server = settings.dns;
+ network_options.gateway = settings.gateway;
+ network_options.ifname = get_single_udev_index(&settings.filter, &udev_info.nics)?;
+ }
+ info!("Network interface used is '{}'", &network_options.ifname);
+ Ok(network_options)
+}
+
+pub fn get_single_udev_index(
+ filter: &BTreeMap<String, String>,
+ udev_list: &BTreeMap<String, BTreeMap<String, String>>,
+) -> Result<String> {
+ if filter.is_empty() {
+ bail!("no filter defined");
+ }
+ let mut dev_index: Option<String> = None;
+ 'outer: for (dev, dev_values) in udev_list {
+ for (filter_key, filter_value) in filter {
+ let filter_pattern =
+ Pattern::new(filter_value).context("invalid glob in disk selection")?;
+ for (udev_key, udev_value) in dev_values {
+ if udev_key == filter_key && filter_pattern.matches(udev_value) {
+ dev_index = Some(dev.clone());
+ break 'outer; // take first match
+ }
+ }
+ }
+ }
+ if dev_index.is_none() {
+ bail!("filter did not match any device");
+ }
+
+ Ok(dev_index.unwrap())
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone, ValueEnum, PartialEq)]
+#[serde(rename_all = "lowercase", deny_unknown_fields)]
+pub enum FetchAnswerFrom {
+ Iso,
+ Http,
+ Partition,
+}
+
+#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
+pub struct HttpOptions {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub url: Option<String>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub cert_fingerprint: Option<String>,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "lowercase", deny_unknown_fields)]
+pub struct AutoInstSettings {
+ pub mode: FetchAnswerFrom,
+ #[serde(default)]
+ pub http: HttpOptions,
+}
+
+#[derive(Deserialize, Debug)]
+struct IpLinksUdevInfo {
+ ifname: String,
+}
+
+/// Returns vec of usable NICs
+pub fn get_nic_list() -> Result<Vec<String>> {
+ let ip_output = Command::new("/usr/sbin/ip")
+ .arg("-j")
+ .arg("link")
+ .output()?;
+ let parsed_links: Vec<IpLinksUdevInfo> = serde_json::from_slice(&ip_output.stdout)?;
+ let mut links: Vec<String> = Vec::new();
+
+ for link in parsed_links {
+ if link.ifname == *"lo" {
+ continue;
+ }
+ links.push(link.ifname);
+ }
+
+ Ok(links)
+}
+
+pub fn get_matched_udev_indexes(
+ filter: &BTreeMap<String, String>,
+ udev_list: &BTreeMap<String, BTreeMap<String, String>>,
+ match_all: bool,
+) -> Result<Vec<String>> {
+ let mut matches = vec![];
+ for (dev, dev_values) in udev_list {
+ let mut did_match_once = false;
+ let mut did_match_all = true;
+ for (filter_key, filter_value) in filter {
+ let filter_pattern =
+ Pattern::new(filter_value).context("invalid glob in disk selection")?;
+ for (udev_key, udev_value) in dev_values {
+ if udev_key == filter_key && filter_pattern.matches(udev_value) {
+ did_match_once = true;
+ } else if udev_key == filter_key {
+ did_match_all = false;
+ }
+ }
+ }
+ if (match_all && did_match_all) || (!match_all && did_match_once) {
+ matches.push(dev.clone());
+ }
+ }
+ if matches.is_empty() {
+ bail!("filter did not match any devices");
+ }
+ matches.sort();
+ Ok(matches)
+}
+
+pub fn set_disks(
+ answer: &Answer,
+ udev_info: &UdevInfo,
+ runtime_info: &RuntimeInfo,
+ config: &mut InstallConfig,
+) -> Result<()> {
+ match config.filesys {
+ FsType::Ext4 | FsType::Xfs => set_single_disk(answer, udev_info, runtime_info, config),
+ FsType::Zfs(_) | FsType::Btrfs(_) => {
+ set_selected_disks(answer, udev_info, runtime_info, config)
+ }
+ }
+}
+
+fn set_single_disk(
+ answer: &Answer,
+ udev_info: &UdevInfo,
+ runtime_info: &RuntimeInfo,
+ config: &mut InstallConfig,
+) -> Result<()> {
+ match &answer.disks.disk_selection {
+ answer::DiskSelection::Selection(disk_list) => {
+ let disk_name = disk_list[0].clone();
+ let disk = runtime_info
+ .disks
+ .iter()
+ .find(|item| item.path.ends_with(disk_name.as_str()));
+ match disk {
+ Some(disk) => config.target_hd = Some(disk.clone()),
+ None => bail!("disk in 'disk_selection' not found"),
+ }
+ }
+ answer::DiskSelection::Filter(filter) => {
+ let disk_index = get_single_udev_index(filter, &udev_info.disks)?;
+ let disk = runtime_info
+ .disks
+ .iter()
+ .find(|item| item.index == disk_index);
+ config.target_hd = disk.cloned();
+ }
+ }
+ info!("Selected disk: {}", config.target_hd.clone().unwrap().path);
+ Ok(())
+}
+
+fn set_selected_disks(
+ answer: &Answer,
+ udev_info: &UdevInfo,
+ runtime_info: &RuntimeInfo,
+ config: &mut InstallConfig,
+) -> Result<()> {
+ match &answer.disks.disk_selection {
+ answer::DiskSelection::Selection(disk_list) => {
+ info!("Disk selection found");
+ for disk_name in disk_list.clone() {
+ let disk = runtime_info
+ .disks
+ .iter()
+ .find(|item| item.path.ends_with(disk_name.as_str()));
+ if let Some(disk) = disk {
+ config
+ .disk_selection
+ .insert(disk.index.clone(), disk.index.clone());
+ }
+ }
+ }
+ answer::DiskSelection::Filter(filter) => {
+ info!("No disk list found, looking for disk filters");
+ let filter_match = answer
+ .disks
+ .filter_match
+ .clone()
+ .unwrap_or(answer::FilterMatch::Any);
+ let selected_disk_indexes = get_matched_udev_indexes(
+ filter,
+ &udev_info.disks,
+ filter_match == answer::FilterMatch::All,
+ )?;
+
+ for i in selected_disk_indexes.into_iter() {
+ let disk = runtime_info
+ .disks
+ .iter()
+ .find(|item| item.index == i)
+ .unwrap();
+ config
+ .disk_selection
+ .insert(disk.index.clone(), disk.index.clone());
+ }
+ }
+ }
+ if config.disk_selection.is_empty() {
+ bail!("No disks found matching selection.");
+ }
+
+ let mut selected_disks: Vec<String> = Vec::new();
+ for i in config.disk_selection.keys() {
+ selected_disks.push(
+ runtime_info
+ .disks
+ .iter()
+ .find(|item| item.index.as_str() == i)
+ .unwrap()
+ .clone()
+ .path,
+ );
+ }
+ info!(
+ "Selected disks: {}",
+ selected_disks
+ .iter()
+ .map(|x| x.to_string() + " ")
+ .collect::<String>()
+ );
+
+ Ok(())
+}
+
+pub fn get_first_selected_disk(config: &InstallConfig) -> usize {
+ config
+ .disk_selection
+ .iter()
+ .next()
+ .expect("no disks found")
+ .0
+ .parse::<usize>()
+ .expect("could not parse key to usize")
+}
+
+pub fn verify_locale_settings(answer: &Answer, locales: &LocaleInfo) -> Result<()> {
+ info!("Verifying locale settings");
+ if !locales
+ .countries
+ .keys()
+ .any(|i| i == &answer.global.country)
+ {
+ bail!("country code '{}' is not valid", &answer.global.country);
+ }
+ if !locales
+ .kmap
+ .keys()
+ .any(|i| i == &answer.global.keyboard.to_string())
+ {
+ bail!("keyboard layout '{}' is not valid", &answer.global.keyboard);
+ }
+
+ if !locales
+ .cczones
+ .iter()
+ .any(|(_, zones)| zones.contains(&answer.global.timezone))
+ && answer.global.timezone != "UTC"
+ {
+ bail!("timezone '{}' is not valid", &answer.global.timezone);
+ }
+
+ Ok(())
+}
+
+pub fn parse_answer(
+ answer: &Answer,
+ udev_info: &UdevInfo,
+ runtime_info: &RuntimeInfo,
+ locales: &LocaleInfo,
+ setup_info: &SetupInfo,
+) -> Result<InstallConfig> {
+ info!("Parsing answer file");
+ info!("Setting File system");
+ let filesystem = answer.disks.fs_type;
+ info!("File system selected: {}", filesystem);
+
+ let network_settings = get_network_settings(answer, udev_info, runtime_info, setup_info)?;
+
+ verify_locale_settings(answer, locales)?;
+
+ let mut config = InstallConfig {
+ autoreboot: 1_usize,
+ filesys: filesystem,
+ hdsize: 0.,
+ swapsize: None,
+ maxroot: None,
+ minfree: None,
+ maxvz: None,
+ zfs_opts: None,
+ target_hd: None,
+ disk_selection: BTreeMap::new(),
+ lvm_auto_rename: 1,
+
+ country: answer.global.country.clone(),
+ timezone: answer.global.timezone.clone(),
+ keymap: answer.global.keyboard.to_string(),
+
+ password: answer.global.root_password.clone(),
+ mailto: answer.global.mailto.clone(),
+ root_ssh_keys: answer.global.root_ssh_keys.clone(),
+
+ mngmt_nic: network_settings.ifname,
+
+ hostname: network_settings.fqdn.host().unwrap().to_string(),
+ domain: network_settings.fqdn.domain(),
+ cidr: network_settings.address,
+ gateway: network_settings.gateway,
+ dns: network_settings.dns_server,
+ };
+
+ set_disks(answer, udev_info, runtime_info, &mut config)?;
+ match &answer.disks.fs_options {
+ answer::FsOptions::LVM(lvm) => {
+ config.hdsize = lvm.hdsize.unwrap_or(config.target_hd.clone().unwrap().size);
+ config.swapsize = lvm.swapsize;
+ config.maxroot = lvm.maxroot;
+ config.maxvz = lvm.maxvz;
+ config.minfree = lvm.minfree;
+ }
+ answer::FsOptions::ZFS(zfs) => {
+ let first_selected_disk = get_first_selected_disk(&config);
+
+ config.hdsize = zfs
+ .hdsize
+ .unwrap_or(runtime_info.disks[first_selected_disk].size);
+ config.zfs_opts = Some(InstallZfsOption {
+ ashift: zfs.ashift.unwrap_or(12),
+ arc_max: zfs.arc_max.unwrap_or(2048),
+ compress: zfs.compress.unwrap_or(ZfsCompressOption::On),
+ checksum: zfs.checksum.unwrap_or(ZfsChecksumOption::On),
+ copies: zfs.copies.unwrap_or(1),
+ });
+ }
+ answer::FsOptions::BTRFS(btrfs) => {
+ let first_selected_disk = get_first_selected_disk(&config);
+
+ config.hdsize = btrfs
+ .hdsize
+ .unwrap_or(runtime_info.disks[first_selected_disk].size);
+ }
+ }
+ Ok(config)
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(tag = "type", rename_all = "lowercase")]
+pub enum LowLevelMessage {
+ #[serde(rename = "message")]
+ Info {
+ message: String,
+ },
+ Error {
+ message: String,
+ },
+ Prompt {
+ query: String,
+ },
+ Finished {
+ state: String,
+ message: String,
+ },
+ Progress {
+ ratio: f32,
+ text: String,
+ },
+}
--- /dev/null
+use std::path::PathBuf;
+
+use serde_json::Value;
+use std::fs;
+
+use proxmox_auto_installer::answer;
+use proxmox_auto_installer::answer::Answer;
+use proxmox_auto_installer::udevinfo::UdevInfo;
+use proxmox_auto_installer::utils::parse_answer;
+
+use proxmox_installer_common::setup::{read_json, LocaleInfo, RuntimeInfo, SetupInfo};
+
+fn get_test_resource_path() -> Result<PathBuf, String> {
+ Ok(std::env::current_dir()
+ .expect("current dir failed")
+ .join("tests/resources"))
+}
+fn get_answer(path: PathBuf) -> Result<Answer, String> {
+ let answer_raw = std::fs::read_to_string(path).unwrap();
+ let answer: answer::Answer = toml::from_str(&answer_raw)
+ .map_err(|err| format!("error parsing answer.toml: {err}"))
+ .unwrap();
+
+ Ok(answer)
+}
+
+fn setup_test_basic(path: &PathBuf) -> (SetupInfo, LocaleInfo, RuntimeInfo, UdevInfo) {
+ let installer_info: SetupInfo = {
+ let mut path = path.clone();
+ path.push("iso-info.json");
+
+ read_json(&path)
+ .map_err(|err| format!("Failed to retrieve setup info: {err}"))
+ .unwrap()
+ };
+
+ let locale_info = {
+ let mut path = path.clone();
+ path.push("locales.json");
+
+ read_json(&path)
+ .map_err(|err| format!("Failed to retrieve locale info: {err}"))
+ .unwrap()
+ };
+
+ let mut runtime_info: RuntimeInfo = {
+ let mut path = path.clone();
+ path.push("run-env-info.json");
+
+ read_json(&path)
+ .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))
+ .unwrap()
+ };
+
+ let udev_info: UdevInfo = {
+ let mut path = path.clone();
+ path.push("run-env-udev.json");
+
+ read_json(&path)
+ .map_err(|err| format!("Failed to retrieve udev info details: {err}"))
+ .unwrap()
+ };
+ runtime_info.disks.sort();
+ if runtime_info.disks.is_empty() {
+ panic!("disk list is empty!");
+ }
+ (installer_info, locale_info, runtime_info, udev_info)
+}
+
+#[test]
+fn test_parse_answers() {
+ let path = get_test_resource_path().unwrap();
+ let (setup_info, locales, runtime_info, udev_info) = setup_test_basic(&path);
+ let mut tests_path = path.clone();
+ tests_path.push("parse_answer");
+ let test_dir = fs::read_dir(tests_path.clone()).unwrap();
+
+ for file_entry in test_dir {
+ let file = file_entry.unwrap();
+ if !file.file_type().unwrap().is_file() || file.file_name() == "readme" {
+ continue;
+ }
+ let p = file.path();
+ let name = p.file_stem().unwrap().to_str().unwrap();
+ let extension = p.extension().unwrap().to_str().unwrap();
+ if extension == "toml" {
+ println!("Test: {name}");
+ let answer = get_answer(p.clone()).unwrap();
+ let config =
+ &parse_answer(&answer, &udev_info, &runtime_info, &locales, &setup_info).unwrap();
+ println!("Selected disks: {:#?}", &config.disk_selection);
+ let config_json = serde_json::to_string(config);
+ let config: Value = serde_json::from_str(config_json.unwrap().as_str()).unwrap();
+ let mut path = tests_path.clone();
+ path.push(format!("{name}.json"));
+ let compare_raw = std::fs::read_to_string(&path).unwrap();
+ let compare: Value = serde_json::from_str(&compare_raw).unwrap();
+ if config != compare {
+ panic!(
+ "Test {} failed:\nleft: {:#?}\nright: {:#?}\n",
+ name, config, compare
+ );
+ }
+ }
+ }
+}
--- /dev/null
+{"iso-info":{"isoname":"proxmox-ve","isorelease":"2","product":"pve","productlong":"Proxmox VE","release":"8.0"},"locations":{"iso":"/cdrom","lib":"/var/lib/proxmox-installer","pkg":"/cdrom/proxmox/packages/","run":"/run/proxmox-installer"},"product":"pve","product-cfg":{"bridged_network":1,"enable_btrfs":1,"fullname":"Proxmox VE","port":"8006","product":"pve"},"run-env-cache-file":"/run/proxmox-installer/run-env-info.json"}
--- /dev/null
+{"cczones":{"ad":{"Europe/Andorra":1},"ae":{"Asia/Dubai":1},"af":{"Asia/Kabul":1},"ag":{"America/Antigua":1},"ai":{"America/Anguilla":1},"al":{"Europe/Tirane":1},"am":{"Asia/Yerevan":1},"ao":{"Africa/Luanda":1},"aq":{"Antarctica/Casey":1,"Antarctica/Davis":1,"Antarctica/DumontDUrville":1,"Antarctica/Mawson":1,"Antarctica/McMurdo":1,"Antarctica/Palmer":1,"Antarctica/Rothera":1,"Antarctica/Syowa":1,"Antarctica/Troll":1,"Antarctica/Vostok":1},"ar":{"America/Argentina/Buenos_Aires":1,"America/Argentina/Catamarca":1,"America/Argentina/Cordoba":1,"America/Argentina/Jujuy":1,"America/Argentina/La_Rioja":1,"America/Argentina/Mendoza":1,"America/Argentina/Rio_Gallegos":1,"America/Argentina/Salta":1,"America/Argentina/San_Juan":1,"America/Argentina/San_Luis":1,"America/Argentina/Tucuman":1,"America/Argentina/Ushuaia":1},"as":{"Pacific/Pago_Pago":1},"at":{"Europe/Vienna":1},"au":{"Antarctica/Macquarie":1,"Australia/Adelaide":1,"Australia/Brisbane":1,"Australia/Broken_Hill":1,"Australia/Darwin":1,"Australia/Eucla":1,"Australia/Hobart":1,"Australia/Lindeman":1,"Australia/Lord_Howe":1,"Australia/Melbourne":1,"Australia/Perth":1,"Australia/Sydney":1},"aw":{"America/Aruba":1},"ax":{"Europe/Mariehamn":1},"az":{"Asia/Baku":1},"ba":{"Europe/Sarajevo":1},"bb":{"America/Barbados":1},"bd":{"Asia/Dhaka":1},"be":{"Europe/Brussels":1},"bf":{"Africa/Ouagadougou":1},"bg":{"Europe/Sofia":1},"bh":{"Asia/Bahrain":1},"bi":{"Africa/Bujumbura":1},"bj":{"Africa/Porto-Novo":1},"bl":{"America/St_Barthelemy":1},"bm":{"Atlantic/Bermuda":1},"bn":{"Asia/Brunei":1},"bo":{"America/La_Paz":1},"bq":{"America/Kralendijk":1},"br":{"America/Araguaina":1,"America/Bahia":1,"America/Belem":1,"America/Boa_Vista":1,"America/Campo_Grande":1,"America/Cuiaba":1,"America/Eirunepe":1,"America/Fortaleza":1,"America/Maceio":1,"America/Manaus":1,"America/Noronha":1,"America/Porto_Velho":1,"America/Recife":1,"America/Rio_Branco":1,"America/Santarem":1,"America/Sao_Paulo":1},"bs":{"America/Nassau":1},"bt":{"Asia/Thimphu":1},"bw":{"Africa/Gaborone":1},"by":{"Europe/Minsk":1},"bz":{"America/Belize":1},"ca":{"America/Atikokan":1,"America/Blanc-Sablon":1,"America/Cambridge_Bay":1,"America/Creston":1,"America/Dawson":1,"America/Dawson_Creek":1,"America/Edmonton":1,"America/Fort_Nelson":1,"America/Glace_Bay":1,"America/Goose_Bay":1,"America/Halifax":1,"America/Inuvik":1,"America/Iqaluit":1,"America/Moncton":1,"America/Rankin_Inlet":1,"America/Regina":1,"America/Resolute":1,"America/St_Johns":1,"America/Swift_Current":1,"America/Toronto":1,"America/Vancouver":1,"America/Whitehorse":1,"America/Winnipeg":1},"cc":{"Indian/Cocos":1},"cd":{"Africa/Kinshasa":1,"Africa/Lubumbashi":1},"cf":{"Africa/Bangui":1},"cg":{"Africa/Brazzaville":1},"ch":{"Europe/Zurich":1},"ci":{"Africa/Abidjan":1},"ck":{"Pacific/Rarotonga":1},"cl":{"America/Punta_Arenas":1,"America/Santiago":1,"Pacific/Easter":1},"cm":{"Africa/Douala":1},"cn":{"Asia/Shanghai":1,"Asia/Urumqi":1},"co":{"America/Bogota":1},"cr":{"America/Costa_Rica":1},"cu":{"America/Havana":1},"cv":{"Atlantic/Cape_Verde":1},"cw":{"America/Curacao":1},"cx":{"Indian/Christmas":1},"cy":{"Asia/Famagusta":1,"Asia/Nicosia":1},"cz":{"Europe/Prague":1},"de":{"Europe/Berlin":1,"Europe/Busingen":1},"dj":{"Africa/Djibouti":1},"dk":{"Europe/Copenhagen":1},"dm":{"America/Dominica":1},"do":{"America/Santo_Domingo":1},"dz":{"Africa/Algiers":1},"ec":{"America/Guayaquil":1,"Pacific/Galapagos":1},"ee":{"Europe/Tallinn":1},"eg":{"Africa/Cairo":1},"eh":{"Africa/El_Aaiun":1},"er":{"Africa/Asmara":1},"es":{"Africa/Ceuta":1,"Atlantic/Canary":1,"Europe/Madrid":1},"et":{"Africa/Addis_Ababa":1},"fi":{"Europe/Helsinki":1},"fj":{"Pacific/Fiji":1},"fk":{"Atlantic/Stanley":1},"fm":{"Pacific/Chuuk":1,"Pacific/Kosrae":1,"Pacific/Pohnpei":1},"fo":{"Atlantic/Faroe":1},"fr":{"Europe/Paris":1},"ga":{"Africa/Libreville":1},"gb":{"Europe/London":1},"gd":{"America/Grenada":1},"ge":{"Asia/Tbilisi":1},"gf":{"America/Cayenne":1},"gg":{"Europe/Guernsey":1},"gh":{"Africa/Accra":1},"gi":{"Europe/Gibraltar":1},"gl":{"America/Danmarkshavn":1,"America/Nuuk":1,"America/Scoresbysund":1,"America/Thule":1},"gm":{"Africa/Banjul":1},"gn":{"Africa/Conakry":1},"gp":{"America/Guadeloupe":1},"gq":{"Africa/Malabo":1},"gr":{"Europe/Athens":1},"gs":{"Atlantic/South_Georgia":1},"gt":{"America/Guatemala":1},"gu":{"Pacific/Guam":1},"gw":{"Africa/Bissau":1},"gy":{"America/Guyana":1},"hk":{"Asia/Hong_Kong":1},"hn":{"America/Tegucigalpa":1},"hr":{"Europe/Zagreb":1},"ht":{"America/Port-au-Prince":1},"hu":{"Europe/Budapest":1},"id":{"Asia/Jakarta":1,"Asia/Jayapura":1,"Asia/Makassar":1,"Asia/Pontianak":1},"ie":{"Europe/Dublin":1},"il":{"Asia/Jerusalem":1},"im":{"Europe/Isle_of_Man":1},"in":{"Asia/Kolkata":1},"io":{"Indian/Chagos":1},"iq":{"Asia/Baghdad":1},"ir":{"Asia/Tehran":1},"is":{"Atlantic/Reykjavik":1},"it":{"Europe/Rome":1},"je":{"Europe/Jersey":1},"jm":{"America/Jamaica":1},"jo":{"Asia/Amman":1},"jp":{"Asia/Tokyo":1},"ke":{"Africa/Nairobi":1},"kg":{"Asia/Bishkek":1},"kh":{"Asia/Phnom_Penh":1},"ki":{"Pacific/Kanton":1,"Pacific/Kiritimati":1,"Pacific/Tarawa":1},"km":{"Indian/Comoro":1},"kn":{"America/St_Kitts":1},"kp":{"Asia/Pyongyang":1},"kr":{"Asia/Seoul":1},"kw":{"Asia/Kuwait":1},"ky":{"America/Cayman":1},"kz":{"Asia/Almaty":1,"Asia/Aqtau":1,"Asia/Aqtobe":1,"Asia/Atyrau":1,"Asia/Oral":1,"Asia/Qostanay":1,"Asia/Qyzylorda":1},"la":{"Asia/Vientiane":1},"lb":{"Asia/Beirut":1},"lc":{"America/St_Lucia":1},"li":{"Europe/Vaduz":1},"lk":{"Asia/Colombo":1},"lr":{"Africa/Monrovia":1},"ls":{"Africa/Maseru":1},"lt":{"Europe/Vilnius":1},"lu":{"Europe/Luxembourg":1},"lv":{"Europe/Riga":1},"ly":{"Africa/Tripoli":1},"ma":{"Africa/Casablanca":1},"mc":{"Europe/Monaco":1},"md":{"Europe/Chisinau":1},"me":{"Europe/Podgorica":1},"mf":{"America/Marigot":1},"mg":{"Indian/Antananarivo":1},"mh":{"Pacific/Kwajalein":1,"Pacific/Majuro":1},"mk":{"Europe/Skopje":1},"ml":{"Africa/Bamako":1},"mm":{"Asia/Yangon":1},"mn":{"Asia/Choibalsan":1,"Asia/Hovd":1,"Asia/Ulaanbaatar":1},"mo":{"Asia/Macau":1},"mp":{"Pacific/Saipan":1},"mq":{"America/Martinique":1},"mr":{"Africa/Nouakchott":1},"ms":{"America/Montserrat":1},"mt":{"Europe/Malta":1},"mu":{"Indian/Mauritius":1},"mv":{"Indian/Maldives":1},"mw":{"Africa/Blantyre":1},"mx":{"America/Bahia_Banderas":1,"America/Cancun":1,"America/Chihuahua":1,"America/Ciudad_Juarez":1,"America/Hermosillo":1,"America/Matamoros":1,"America/Mazatlan":1,"America/Merida":1,"America/Mexico_City":1,"America/Monterrey":1,"America/Ojinaga":1,"America/Tijuana":1},"my":{"Asia/Kuala_Lumpur":1,"Asia/Kuching":1},"mz":{"Africa/Maputo":1},"na":{"Africa/Windhoek":1},"nc":{"Pacific/Noumea":1},"ne":{"Africa/Niamey":1},"nf":{"Pacific/Norfolk":1},"ng":{"Africa/Lagos":1},"ni":{"America/Managua":1},"nl":{"Europe/Amsterdam":1},"no":{"Europe/Oslo":1},"np":{"Asia/Kathmandu":1},"nr":{"Pacific/Nauru":1},"nu":{"Pacific/Niue":1},"nz":{"Pacific/Auckland":1,"Pacific/Chatham":1},"om":{"Asia/Muscat":1},"pa":{"America/Panama":1},"pe":{"America/Lima":1},"pf":{"Pacific/Gambier":1,"Pacific/Marquesas":1,"Pacific/Tahiti":1},"pg":{"Pacific/Bougainville":1,"Pacific/Port_Moresby":1},"ph":{"Asia/Manila":1},"pk":{"Asia/Karachi":1},"pl":{"Europe/Warsaw":1},"pm":{"America/Miquelon":1},"pn":{"Pacific/Pitcairn":1},"pr":{"America/Puerto_Rico":1},"ps":{"Asia/Gaza":1,"Asia/Hebron":1},"pt":{"Atlantic/Azores":1,"Atlantic/Madeira":1,"Europe/Lisbon":1},"pw":{"Pacific/Palau":1},"py":{"America/Asuncion":1},"qa":{"Asia/Qatar":1},"re":{"Indian/Reunion":1},"ro":{"Europe/Bucharest":1},"rs":{"Europe/Belgrade":1},"ru":{"Asia/Anadyr":1,"Asia/Barnaul":1,"Asia/Chita":1,"Asia/Irkutsk":1,"Asia/Kamchatka":1,"Asia/Khandyga":1,"Asia/Krasnoyarsk":1,"Asia/Magadan":1,"Asia/Novokuznetsk":1,"Asia/Novosibirsk":1,"Asia/Omsk":1,"Asia/Sakhalin":1,"Asia/Srednekolymsk":1,"Asia/Tomsk":1,"Asia/Ust-Nera":1,"Asia/Vladivostok":1,"Asia/Yakutsk":1,"Asia/Yekaterinburg":1,"Europe/Astrakhan":1,"Europe/Kaliningrad":1,"Europe/Kirov":1,"Europe/Moscow":1,"Europe/Samara":1,"Europe/Saratov":1,"Europe/Ulyanovsk":1,"Europe/Volgograd":1},"rw":{"Africa/Kigali":1},"sa":{"Asia/Riyadh":1},"sb":{"Pacific/Guadalcanal":1},"sc":{"Indian/Mahe":1},"sd":{"Africa/Khartoum":1},"se":{"Europe/Stockholm":1},"sg":{"Asia/Singapore":1},"sh":{"Atlantic/St_Helena":1},"si":{"Europe/Ljubljana":1},"sj":{"Arctic/Longyearbyen":1},"sk":{"Europe/Bratislava":1},"sl":{"Africa/Freetown":1},"sm":{"Europe/San_Marino":1},"sn":{"Africa/Dakar":1},"so":{"Africa/Mogadishu":1},"sr":{"America/Paramaribo":1},"ss":{"Africa/Juba":1},"st":{"Africa/Sao_Tome":1},"sv":{"America/El_Salvador":1},"sx":{"America/Lower_Princes":1},"sy":{"Asia/Damascus":1},"sz":{"Africa/Mbabane":1},"tc":{"America/Grand_Turk":1},"td":{"Africa/Ndjamena":1},"tf":{"Indian/Kerguelen":1},"tg":{"Africa/Lome":1},"th":{"Asia/Bangkok":1},"tj":{"Asia/Dushanbe":1},"tk":{"Pacific/Fakaofo":1},"tl":{"Asia/Dili":1},"tm":{"Asia/Ashgabat":1},"tn":{"Africa/Tunis":1},"to":{"Pacific/Tongatapu":1},"tr":{"Europe/Istanbul":1},"tt":{"America/Port_of_Spain":1},"tv":{"Pacific/Funafuti":1},"tw":{"Asia/Taipei":1},"tz":{"Africa/Dar_es_Salaam":1},"ua":{"Europe/Kyiv":1,"Europe/Simferopol":1},"ug":{"Africa/Kampala":1},"um":{"Pacific/Midway":1,"Pacific/Wake":1},"us":{"America/Adak":1,"America/Anchorage":1,"America/Boise":1,"America/Chicago":1,"America/Denver":1,"America/Detroit":1,"America/Indiana/Indianapolis":1,"America/Indiana/Knox":1,"America/Indiana/Marengo":1,"America/Indiana/Petersburg":1,"America/Indiana/Tell_City":1,"America/Indiana/Vevay":1,"America/Indiana/Vincennes":1,"America/Indiana/Winamac":1,"America/Juneau":1,"America/Kentucky/Louisville":1,"America/Kentucky/Monticello":1,"America/Los_Angeles":1,"America/Menominee":1,"America/Metlakatla":1,"America/New_York":1,"America/Nome":1,"America/North_Dakota/Beulah":1,"America/North_Dakota/Center":1,"America/North_Dakota/New_Salem":1,"America/Phoenix":1,"America/Sitka":1,"America/Yakutat":1,"Pacific/Honolulu":1},"uy":{"America/Montevideo":1},"uz":{"Asia/Samarkand":1,"Asia/Tashkent":1},"va":{"Europe/Vatican":1},"vc":{"America/St_Vincent":1},"ve":{"America/Caracas":1},"vg":{"America/Tortola":1},"vi":{"America/St_Thomas":1},"vn":{"Asia/Ho_Chi_Minh":1},"vu":{"Pacific/Efate":1},"wf":{"Pacific/Wallis":1},"ws":{"Pacific/Apia":1},"ye":{"Asia/Aden":1},"yt":{"Indian/Mayotte":1},"za":{"Africa/Johannesburg":1},"zm":{"Africa/Lusaka":1},"zw":{"Africa/Harare":1}},"country":{"ad":{"kmap":"","mirror":"","name":"Andorra","zone":"Europe/Andorra"},"ae":{"kmap":"","mirror":"","name":"United Arab Emirates","zone":"Asia/Dubai"},"af":{"kmap":"","mirror":"","name":"Afghanistan","zone":"Asia/Kabul"},"ag":{"kmap":"","mirror":"","name":"Antigua and Barbuda","zone":"America/Antigua"},"ai":{"kmap":"","mirror":"","name":"Anguilla","zone":"America/Anguilla"},"al":{"kmap":"","mirror":"","name":"Albania","zone":"Europe/Tirane"},"am":{"kmap":"","mirror":"","name":"Armenia","zone":"Asia/Yerevan"},"ao":{"kmap":"","mirror":"","name":"Angola","zone":"Africa/Luanda"},"aq":{"kmap":"","mirror":"","name":"Antarctica","zone":"Antarctica/McMurdo"},"ar":{"kmap":"","mirror":"","name":"Argentina","zone":"America/Argentina/Buenos_Aires"},"as":{"kmap":"","mirror":"","name":"American Samoa","zone":"Pacific/Pago_Pago"},"at":{"kmap":"de","mirror":"ftp.at.debian.org","name":"Austria","zone":"Europe/Vienna"},"au":{"kmap":"","mirror":"ftp.au.debian.org","name":"Australia","zone":"Australia/Lord_Howe"},"aw":{"kmap":"","mirror":"","name":"Aruba","zone":"America/Aruba"},"ax":{"kmap":"","mirror":"","name":"Åland Islands","zone":"Europe/Mariehamn"},"az":{"kmap":"","mirror":"","name":"Azerbaijan","zone":"Asia/Baku"},"ba":{"kmap":"","mirror":"","name":"Bosnia and Herzegovina","zone":"Europe/Sarajevo"},"bb":{"kmap":"","mirror":"","name":"Barbados","zone":"America/Barbados"},"bd":{"kmap":"","mirror":"","name":"Bangladesh","zone":"Asia/Dhaka"},"be":{"kmap":"fr-be","mirror":"ftp.be.debian.org","name":"Belgium","zone":"Europe/Brussels"},"bf":{"kmap":"","mirror":"","name":"Burkina Faso","zone":"Africa/Ouagadougou"},"bg":{"kmap":"","mirror":"ftp.bg.debian.org","name":"Bulgaria","zone":"Europe/Sofia"},"bh":{"kmap":"","mirror":"","name":"Bahrain","zone":"Asia/Bahrain"},"bi":{"kmap":"","mirror":"","name":"Burundi","zone":"Africa/Bujumbura"},"bj":{"kmap":"","mirror":"","name":"Benin","zone":"Africa/Porto-Novo"},"bl":{"kmap":"","mirror":"","name":"Saint Barthélemy","zone":"America/St_Barthelemy"},"bm":{"kmap":"","mirror":"","name":"Bermuda","zone":"Atlantic/Bermuda"},"bn":{"kmap":"","mirror":"","name":"Brunei Darussalam","zone":"Asia/Brunei"},"bo":{"kmap":"","mirror":"","name":"Bolivia","zone":"America/La_Paz"},"bq":{"kmap":"","mirror":"","name":"Bonaire, Sint Eustatius and Saba","zone":"America/Kralendijk"},"br":{"kmap":"pt-br","mirror":"ftp.br.debian.org","name":"Brazil","zone":"America/Noronha"},"bs":{"kmap":"","mirror":"","name":"Bahamas","zone":"America/Nassau"},"bt":{"kmap":"","mirror":"","name":"Bhutan","zone":"Asia/Thimphu"},"bv":{"kmap":"","mirror":"","name":"Bouvet Island"},"bw":{"kmap":"","mirror":"","name":"Botswana","zone":"Africa/Gaborone"},"by":{"kmap":"","mirror":"","name":"Belarus","zone":"Europe/Minsk"},"bz":{"kmap":"","mirror":"","name":"Belize","zone":"America/Belize"},"ca":{"kmap":"en-us","mirror":"ftp.ca.debian.org","name":"Canada","zone":"America/St_Johns"},"cc":{"kmap":"","mirror":"","name":"Cocos (Keeling) Islands","zone":"Indian/Cocos"},"cd":{"kmap":"","mirror":"","name":"Congo, The Democratic Republic of the","zone":"Africa/Kinshasa"},"cf":{"kmap":"","mirror":"","name":"Central African Republic","zone":"Africa/Bangui"},"cg":{"kmap":"","mirror":"","name":"Congo","zone":"Africa/Brazzaville"},"ch":{"kmap":"de-ch","mirror":"ftp.ch.debian.org","name":"Switzerland","zone":"Europe/Zurich"},"ci":{"kmap":"","mirror":"","name":"Côte d'Ivoire","zone":"Africa/Abidjan"},"ck":{"kmap":"","mirror":"","name":"Cook Islands","zone":"Pacific/Rarotonga"},"cl":{"kmap":"","mirror":"ftp.cl.debian.org","name":"Chile","zone":"America/Santiago"},"cm":{"kmap":"","mirror":"","name":"Cameroon","zone":"Africa/Douala"},"cn":{"kmap":"","mirror":"","name":"China","zone":"Asia/Shanghai"},"co":{"kmap":"","mirror":"","name":"Colombia","zone":"America/Bogota"},"cr":{"kmap":"","mirror":"","name":"Costa Rica","zone":"America/Costa_Rica"},"cu":{"kmap":"","mirror":"","name":"Cuba","zone":"America/Havana"},"cv":{"kmap":"","mirror":"","name":"Cabo Verde","zone":"Atlantic/Cape_Verde"},"cw":{"kmap":"","mirror":"","name":"Curaçao","zone":"America/Curacao"},"cx":{"kmap":"","mirror":"","name":"Christmas Island","zone":"Indian/Christmas"},"cy":{"kmap":"","mirror":"","name":"Cyprus","zone":"Asia/Nicosia"},"cz":{"kmap":"","mirror":"ftp.cz.debian.org","name":"Czechia","zone":"Europe/Prague"},"de":{"kmap":"de","mirror":"ftp.de.debian.org","name":"Germany","zone":"Europe/Berlin"},"dj":{"kmap":"","mirror":"","name":"Djibouti","zone":"Africa/Djibouti"},"dk":{"kmap":"dk","mirror":"ftp.dk.debian.org","name":"Denmark","zone":"Europe/Copenhagen"},"dm":{"kmap":"","mirror":"","name":"Dominica","zone":"America/Dominica"},"do":{"kmap":"","mirror":"","name":"Dominican Republic","zone":"America/Santo_Domingo"},"dz":{"kmap":"","mirror":"","name":"Algeria","zone":"Africa/Algiers"},"ec":{"kmap":"","mirror":"","name":"Ecuador","zone":"America/Guayaquil"},"ee":{"kmap":"","mirror":"ftp.ee.debian.org","name":"Estonia","zone":"Europe/Tallinn"},"eg":{"kmap":"","mirror":"","name":"Egypt","zone":"Africa/Cairo"},"eh":{"kmap":"","mirror":"","name":"Western Sahara","zone":"Africa/El_Aaiun"},"er":{"kmap":"","mirror":"","name":"Eritrea","zone":"Africa/Asmara"},"es":{"kmap":"es","mirror":"ftp.es.debian.org","name":"Spain","zone":"Europe/Madrid"},"et":{"kmap":"","mirror":"","name":"Ethiopia","zone":"Africa/Addis_Ababa"},"fi":{"kmap":"fi","mirror":"ftp.fi.debian.org","name":"Finland","zone":"Europe/Helsinki"},"fj":{"kmap":"","mirror":"","name":"Fiji","zone":"Pacific/Fiji"},"fk":{"kmap":"","mirror":"","name":"Falkland Islands (Malvinas)","zone":"Atlantic/Stanley"},"fm":{"kmap":"","mirror":"","name":"Micronesia, Federated States of","zone":"Pacific/Chuuk"},"fo":{"kmap":"","mirror":"","name":"Faroe Islands","zone":"Atlantic/Faroe"},"fr":{"kmap":"fr","mirror":"ftp.fr.debian.org","name":"France","zone":"Europe/Paris"},"ga":{"kmap":"","mirror":"","name":"Gabon","zone":"Africa/Libreville"},"gb":{"kmap":"en-gb","mirror":"ftp.uk.debian.org","name":"United Kingdom","zone":"Europe/London"},"gd":{"kmap":"","mirror":"","name":"Grenada","zone":"America/Grenada"},"ge":{"kmap":"","mirror":"","name":"Georgia","zone":"Asia/Tbilisi"},"gf":{"kmap":"","mirror":"","name":"French Guiana","zone":"America/Cayenne"},"gg":{"kmap":"","mirror":"","name":"Guernsey","zone":"Europe/Guernsey"},"gh":{"kmap":"","mirror":"","name":"Ghana","zone":"Africa/Accra"},"gi":{"kmap":"es","mirror":"","name":"Gibraltar","zone":"Europe/Gibraltar"},"gl":{"kmap":"","mirror":"","name":"Greenland","zone":"America/Nuuk"},"gm":{"kmap":"","mirror":"","name":"Gambia","zone":"Africa/Banjul"},"gn":{"kmap":"","mirror":"","name":"Guinea","zone":"Africa/Conakry"},"gp":{"kmap":"","mirror":"","name":"Guadeloupe","zone":"America/Guadeloupe"},"gq":{"kmap":"","mirror":"","name":"Equatorial Guinea","zone":"Africa/Malabo"},"gr":{"kmap":"","mirror":"ftp.gr.debian.org","name":"Greece","zone":"Europe/Athens"},"gs":{"kmap":"","mirror":"","name":"South Georgia and the South Sandwich Islands","zone":"Atlantic/South_Georgia"},"gt":{"kmap":"","mirror":"","name":"Guatemala","zone":"America/Guatemala"},"gu":{"kmap":"","mirror":"","name":"Guam","zone":"Pacific/Guam"},"gw":{"kmap":"","mirror":"","name":"Guinea-Bissau","zone":"Africa/Bissau"},"gy":{"kmap":"","mirror":"","name":"Guyana","zone":"America/Guyana"},"hk":{"kmap":"","mirror":"ftp.hk.debian.org","name":"Hong Kong","zone":"Asia/Hong_Kong"},"hm":{"kmap":"","mirror":"","name":"Heard Island and McDonald Islands"},"hn":{"kmap":"","mirror":"","name":"Honduras","zone":"America/Tegucigalpa"},"hr":{"kmap":"","mirror":"ftp.hr.debian.org","name":"Croatia","zone":"Europe/Zagreb"},"ht":{"kmap":"","mirror":"","name":"Haiti","zone":"America/Port-au-Prince"},"hu":{"kmap":"hu","mirror":"ftp.hu.debian.org","name":"Hungary","zone":"Europe/Budapest"},"id":{"kmap":"","mirror":"","name":"Indonesia","zone":"Asia/Jakarta"},"ie":{"kmap":"","mirror":"ftp.ie.debian.org","name":"Ireland","zone":"Europe/Dublin"},"il":{"kmap":"","mirror":"","name":"Israel","zone":"Asia/Jerusalem"},"im":{"kmap":"","mirror":"","name":"Isle of Man","zone":"Europe/Isle_of_Man"},"in":{"kmap":"","mirror":"","name":"India","zone":"Asia/Kolkata"},"io":{"kmap":"","mirror":"","name":"British Indian Ocean Territory","zone":"Indian/Chagos"},"iq":{"kmap":"","mirror":"","name":"Iraq","zone":"Asia/Baghdad"},"ir":{"kmap":"","mirror":"","name":"Iran","zone":"Asia/Tehran"},"is":{"kmap":"is","mirror":"ftp.is.debian.org","name":"Iceland","zone":"Atlantic/Reykjavik"},"it":{"kmap":"it","mirror":"ftp.it.debian.org","name":"Italy","zone":"Europe/Rome"},"je":{"kmap":"","mirror":"","name":"Jersey","zone":"Europe/Jersey"},"jm":{"kmap":"","mirror":"","name":"Jamaica","zone":"America/Jamaica"},"jo":{"kmap":"","mirror":"","name":"Jordan","zone":"Asia/Amman"},"jp":{"kmap":"jp","mirror":"ftp.jp.debian.org","name":"Japan","zone":"Asia/Tokyo"},"ke":{"kmap":"","mirror":"","name":"Kenya","zone":"Africa/Nairobi"},"kg":{"kmap":"","mirror":"","name":"Kyrgyzstan","zone":"Asia/Bishkek"},"kh":{"kmap":"","mirror":"","name":"Cambodia","zone":"Asia/Phnom_Penh"},"ki":{"kmap":"","mirror":"","name":"Kiribati","zone":"Pacific/Tarawa"},"km":{"kmap":"","mirror":"","name":"Comoros","zone":"Indian/Comoro"},"kn":{"kmap":"","mirror":"","name":"Saint Kitts and Nevis","zone":"America/St_Kitts"},"kp":{"kmap":"","mirror":"","name":"North Korea","zone":"Asia/Pyongyang"},"kr":{"kmap":"","mirror":"ftp.kr.debian.org","name":"South Korea","zone":"Asia/Seoul"},"kw":{"kmap":"","mirror":"","name":"Kuwait","zone":"Asia/Kuwait"},"ky":{"kmap":"","mirror":"","name":"Cayman Islands","zone":"America/Cayman"},"kz":{"kmap":"","mirror":"","name":"Kazakhstan","zone":"Asia/Almaty"},"la":{"kmap":"","mirror":"","name":"Laos","zone":"Asia/Vientiane"},"lb":{"kmap":"","mirror":"","name":"Lebanon","zone":"Asia/Beirut"},"lc":{"kmap":"","mirror":"","name":"Saint Lucia","zone":"America/St_Lucia"},"li":{"kmap":"de-ch","mirror":"","name":"Liechtenstein","zone":"Europe/Vaduz"},"lk":{"kmap":"","mirror":"","name":"Sri Lanka","zone":"Asia/Colombo"},"lr":{"kmap":"","mirror":"","name":"Liberia","zone":"Africa/Monrovia"},"ls":{"kmap":"","mirror":"","name":"Lesotho","zone":"Africa/Maseru"},"lt":{"kmap":"lt","mirror":"","name":"Lithuania","zone":"Europe/Vilnius"},"lu":{"kmap":"fr-ch","mirror":"","name":"Luxembourg","zone":"Europe/Luxembourg"},"lv":{"kmap":"","mirror":"","name":"Latvia","zone":"Europe/Riga"},"ly":{"kmap":"","mirror":"","name":"Libya","zone":"Africa/Tripoli"},"ma":{"kmap":"","mirror":"","name":"Morocco","zone":"Africa/Casablanca"},"mc":{"kmap":"","mirror":"","name":"Monaco","zone":"Europe/Monaco"},"md":{"kmap":"","mirror":"","name":"Moldova","zone":"Europe/Chisinau"},"me":{"kmap":"","mirror":"","name":"Montenegro","zone":"Europe/Podgorica"},"mf":{"kmap":"","mirror":"","name":"Saint Martin (French part)","zone":"America/Marigot"},"mg":{"kmap":"","mirror":"","name":"Madagascar","zone":"Indian/Antananarivo"},"mh":{"kmap":"","mirror":"","name":"Marshall Islands","zone":"Pacific/Majuro"},"mk":{"kmap":"mk","mirror":"","name":"North Macedonia","zone":"Europe/Skopje"},"ml":{"kmap":"","mirror":"","name":"Mali","zone":"Africa/Bamako"},"mm":{"kmap":"","mirror":"","name":"Myanmar","zone":"Asia/Yangon"},"mn":{"kmap":"","mirror":"","name":"Mongolia","zone":"Asia/Ulaanbaatar"},"mo":{"kmap":"","mirror":"","name":"Macao","zone":"Asia/Macau"},"mp":{"kmap":"","mirror":"","name":"Northern Mariana Islands","zone":"Pacific/Saipan"},"mq":{"kmap":"","mirror":"","name":"Martinique","zone":"America/Martinique"},"mr":{"kmap":"","mirror":"","name":"Mauritania","zone":"Africa/Nouakchott"},"ms":{"kmap":"","mirror":"","name":"Montserrat","zone":"America/Montserrat"},"mt":{"kmap":"","mirror":"","name":"Malta","zone":"Europe/Malta"},"mu":{"kmap":"","mirror":"","name":"Mauritius","zone":"Indian/Mauritius"},"mv":{"kmap":"","mirror":"","name":"Maldives","zone":"Indian/Maldives"},"mw":{"kmap":"","mirror":"","name":"Malawi","zone":"Africa/Blantyre"},"mx":{"kmap":"","mirror":"ftp.mx.debian.org","name":"Mexico","zone":"America/Mexico_City"},"my":{"kmap":"","mirror":"","name":"Malaysia","zone":"Asia/Kuala_Lumpur"},"mz":{"kmap":"","mirror":"","name":"Mozambique","zone":"Africa/Maputo"},"na":{"kmap":"","mirror":"","name":"Namibia","zone":"Africa/Windhoek"},"nc":{"kmap":"","mirror":"","name":"New Caledonia","zone":"Pacific/Noumea"},"ne":{"kmap":"","mirror":"","name":"Niger","zone":"Africa/Niamey"},"nf":{"kmap":"","mirror":"","name":"Norfolk Island","zone":"Pacific/Norfolk"},"ng":{"kmap":"","mirror":"","name":"Nigeria","zone":"Africa/Lagos"},"ni":{"kmap":"","mirror":"","name":"Nicaragua","zone":"America/Managua"},"nl":{"kmap":"en-us","mirror":"ftp.nl.debian.org","name":"Netherlands","zone":"Europe/Amsterdam"},"no":{"kmap":"no","mirror":"ftp.no.debian.org","name":"Norway","zone":"Europe/Oslo"},"np":{"kmap":"","mirror":"","name":"Nepal","zone":"Asia/Kathmandu"},"nr":{"kmap":"","mirror":"","name":"Nauru","zone":"Pacific/Nauru"},"nu":{"kmap":"","mirror":"","name":"Niue","zone":"Pacific/Niue"},"nz":{"kmap":"","mirror":"ftp.nz.debian.org","name":"New Zealand","zone":"Pacific/Auckland"},"om":{"kmap":"","mirror":"","name":"Oman","zone":"Asia/Muscat"},"pa":{"kmap":"","mirror":"","name":"Panama","zone":"America/Panama"},"pe":{"kmap":"","mirror":"","name":"Peru","zone":"America/Lima"},"pf":{"kmap":"","mirror":"","name":"French Polynesia","zone":"Pacific/Tahiti"},"pg":{"kmap":"","mirror":"","name":"Papua New Guinea","zone":"Pacific/Port_Moresby"},"ph":{"kmap":"","mirror":"","name":"Philippines","zone":"Asia/Manila"},"pk":{"kmap":"","mirror":"","name":"Pakistan","zone":"Asia/Karachi"},"pl":{"kmap":"pl","mirror":"ftp.pl.debian.org","name":"Poland","zone":"Europe/Warsaw"},"pm":{"kmap":"","mirror":"","name":"Saint Pierre and Miquelon","zone":"America/Miquelon"},"pn":{"kmap":"","mirror":"","name":"Pitcairn","zone":"Pacific/Pitcairn"},"pr":{"kmap":"","mirror":"","name":"Puerto Rico","zone":"America/Puerto_Rico"},"ps":{"kmap":"","mirror":"","name":"Palestine, State of","zone":"Asia/Gaza"},"pt":{"kmap":"pt","mirror":"ftp.pt.debian.org","name":"Portugal","zone":"Europe/Lisbon"},"pw":{"kmap":"","mirror":"","name":"Palau","zone":"Pacific/Palau"},"py":{"kmap":"","mirror":"","name":"Paraguay","zone":"America/Asuncion"},"qa":{"kmap":"","mirror":"","name":"Qatar","zone":"Asia/Qatar"},"re":{"kmap":"","mirror":"","name":"Réunion","zone":"Indian/Reunion"},"ro":{"kmap":"","mirror":"ftp.ro.debian.org","name":"Romania","zone":"Europe/Bucharest"},"rs":{"kmap":"","mirror":"","name":"Serbia","zone":"Europe/Belgrade"},"ru":{"kmap":"","mirror":"ftp.ru.debian.org","name":"Russian Federation","zone":"Europe/Kaliningrad"},"rw":{"kmap":"","mirror":"","name":"Rwanda","zone":"Africa/Kigali"},"sa":{"kmap":"","mirror":"","name":"Saudi Arabia","zone":"Asia/Riyadh"},"sb":{"kmap":"","mirror":"","name":"Solomon Islands","zone":"Pacific/Guadalcanal"},"sc":{"kmap":"","mirror":"","name":"Seychelles","zone":"Indian/Mahe"},"sd":{"kmap":"","mirror":"","name":"Sudan","zone":"Africa/Khartoum"},"se":{"kmap":"","mirror":"ftp.se.debian.org","name":"Sweden","zone":"Europe/Stockholm"},"sg":{"kmap":"","mirror":"","name":"Singapore","zone":"Asia/Singapore"},"sh":{"kmap":"","mirror":"","name":"Saint Helena, Ascension and Tristan da Cunha","zone":"Atlantic/St_Helena"},"si":{"kmap":"si","mirror":"ftp.si.debian.org","name":"Slovenia","zone":"Europe/Ljubljana"},"sj":{"kmap":"","mirror":"","name":"Svalbard and Jan Mayen","zone":"Arctic/Longyearbyen"},"sk":{"kmap":"","mirror":"ftp.sk.debian.org","name":"Slovakia","zone":"Europe/Bratislava"},"sl":{"kmap":"","mirror":"","name":"Sierra Leone","zone":"Africa/Freetown"},"sm":{"kmap":"","mirror":"","name":"San Marino","zone":"Europe/San_Marino"},"sn":{"kmap":"","mirror":"","name":"Senegal","zone":"Africa/Dakar"},"so":{"kmap":"","mirror":"","name":"Somalia","zone":"Africa/Mogadishu"},"sr":{"kmap":"","mirror":"","name":"Suriname","zone":"America/Paramaribo"},"ss":{"kmap":"","mirror":"","name":"South Sudan","zone":"Africa/Juba"},"st":{"kmap":"","mirror":"","name":"Sao Tome and Principe","zone":"Africa/Sao_Tome"},"sv":{"kmap":"","mirror":"","name":"El Salvador","zone":"America/El_Salvador"},"sx":{"kmap":"","mirror":"","name":"Sint Maarten (Dutch part)","zone":"America/Lower_Princes"},"sy":{"kmap":"","mirror":"","name":"Syria","zone":"Asia/Damascus"},"sz":{"kmap":"","mirror":"","name":"Eswatini","zone":"Africa/Mbabane"},"tc":{"kmap":"","mirror":"","name":"Turks and Caicos Islands","zone":"America/Grand_Turk"},"td":{"kmap":"","mirror":"","name":"Chad","zone":"Africa/Ndjamena"},"tf":{"kmap":"","mirror":"","name":"French Southern Territories","zone":"Indian/Kerguelen"},"tg":{"kmap":"","mirror":"","name":"Togo","zone":"Africa/Lome"},"th":{"kmap":"","mirror":"","name":"Thailand","zone":"Asia/Bangkok"},"tj":{"kmap":"","mirror":"","name":"Tajikistan","zone":"Asia/Dushanbe"},"tk":{"kmap":"","mirror":"","name":"Tokelau","zone":"Pacific/Fakaofo"},"tl":{"kmap":"","mirror":"","name":"Timor-Leste","zone":"Asia/Dili"},"tm":{"kmap":"","mirror":"","name":"Turkmenistan","zone":"Asia/Ashgabat"},"tn":{"kmap":"","mirror":"","name":"Tunisia","zone":"Africa/Tunis"},"to":{"kmap":"","mirror":"","name":"Tonga","zone":"Pacific/Tongatapu"},"tr":{"kmap":"","mirror":"ftp.tr.debian.org","name":"Türkiye","zone":"Europe/Istanbul"},"tt":{"kmap":"","mirror":"","name":"Trinidad and Tobago","zone":"America/Port_of_Spain"},"tv":{"kmap":"","mirror":"","name":"Tuvalu","zone":"Pacific/Funafuti"},"tw":{"kmap":"","mirror":"ftp.tw.debian.org","name":"Taiwan","zone":"Asia/Taipei"},"tz":{"kmap":"","mirror":"","name":"Tanzania","zone":"Africa/Dar_es_Salaam"},"ua":{"kmap":"","mirror":"","name":"Ukraine","zone":"Europe/Simferopol"},"ug":{"kmap":"","mirror":"","name":"Uganda","zone":"Africa/Kampala"},"um":{"kmap":"","mirror":"","name":"United States Minor Outlying Islands","zone":"Pacific/Midway"},"us":{"kmap":"en-us","mirror":"ftp.us.debian.org","name":"United States","zone":"America/New_York"},"uy":{"kmap":"","mirror":"","name":"Uruguay","zone":"America/Montevideo"},"uz":{"kmap":"","mirror":"","name":"Uzbekistan","zone":"Asia/Samarkand"},"va":{"kmap":"it","mirror":"","name":"Holy See (Vatican City State)","zone":"Europe/Vatican"},"vc":{"kmap":"","mirror":"","name":"Saint Vincent and the Grenadines","zone":"America/St_Vincent"},"ve":{"kmap":"","mirror":"","name":"Venezuela","zone":"America/Caracas"},"vg":{"kmap":"","mirror":"","name":"Virgin Islands, British","zone":"America/Tortola"},"vi":{"kmap":"","mirror":"","name":"Virgin Islands, U.S.","zone":"America/St_Thomas"},"vn":{"kmap":"","mirror":"","name":"Vietnam","zone":"Asia/Ho_Chi_Minh"},"vu":{"kmap":"","mirror":"","name":"Vanuatu","zone":"Pacific/Efate"},"wf":{"kmap":"","mirror":"","name":"Wallis and Futuna","zone":"Pacific/Wallis"},"ws":{"kmap":"","mirror":"","name":"Samoa","zone":"Pacific/Apia"},"ye":{"kmap":"","mirror":"","name":"Yemen","zone":"Asia/Aden"},"yt":{"kmap":"","mirror":"","name":"Mayotte","zone":"Indian/Mayotte"},"za":{"kmap":"","mirror":"","name":"South Africa","zone":"Africa/Johannesburg"},"zm":{"kmap":"","mirror":"","name":"Zambia","zone":"Africa/Lusaka"},"zw":{"kmap":"","mirror":"","name":"Zimbabwe","zone":"Africa/Harare"}},"countryhash":{"afghanistan":"af","albania":"al","algeria":"dz","american samoa":"as","andorra":"ad","angola":"ao","anguilla":"ai","antarctica":"aq","antigua and barbuda":"ag","argentina":"ar","armenia":"am","aruba":"aw","australia":"au","austria":"at","azerbaijan":"az","bahamas":"bs","bahrain":"bh","bangladesh":"bd","barbados":"bb","belarus":"by","belgium":"be","belize":"bz","benin":"bj","bermuda":"bm","bhutan":"bt","bolivia":"bo","bonaire, sint eustatius and saba":"bq","bosnia and herzegovina":"ba","botswana":"bw","bouvet island":"bv","brazil":"br","british indian ocean territory":"io","brunei darussalam":"bn","bulgaria":"bg","burkina faso":"bf","burundi":"bi","cabo verde":"cv","cambodia":"kh","cameroon":"cm","canada":"ca","cayman islands":"ky","central african republic":"cf","chad":"td","chile":"cl","china":"cn","christmas island":"cx","cocos (keeling) islands":"cc","colombia":"co","comoros":"km","congo":"cg","congo, the democratic republic of the":"cd","cook islands":"ck","costa rica":"cr","croatia":"hr","cuba":"cu","curaçao":"cw","cyprus":"cy","czechia":"cz","côte d'ivoire":"ci","denmark":"dk","djibouti":"dj","dominica":"dm","dominican republic":"do","ecuador":"ec","egypt":"eg","el salvador":"sv","equatorial guinea":"gq","eritrea":"er","estonia":"ee","eswatini":"sz","ethiopia":"et","falkland islands (malvinas)":"fk","faroe islands":"fo","fiji":"fj","finland":"fi","france":"fr","french guiana":"gf","french polynesia":"pf","french southern territories":"tf","gabon":"ga","gambia":"gm","georgia":"ge","germany":"de","ghana":"gh","gibraltar":"gi","greece":"gr","greenland":"gl","grenada":"gd","guadeloupe":"gp","guam":"gu","guatemala":"gt","guernsey":"gg","guinea":"gn","guinea-bissau":"gw","guyana":"gy","haiti":"ht","heard island and mcdonald islands":"hm","holy see (vatican city state)":"va","honduras":"hn","hong kong":"hk","hungary":"hu","iceland":"is","india":"in","indonesia":"id","iran":"ir","iraq":"iq","ireland":"ie","isle of man":"im","israel":"il","italy":"it","jamaica":"jm","japan":"jp","jersey":"je","jordan":"jo","kazakhstan":"kz","kenya":"ke","kiribati":"ki","kuwait":"kw","kyrgyzstan":"kg","laos":"la","latvia":"lv","lebanon":"lb","lesotho":"ls","liberia":"lr","libya":"ly","liechtenstein":"li","lithuania":"lt","luxembourg":"lu","macao":"mo","madagascar":"mg","malawi":"mw","malaysia":"my","maldives":"mv","mali":"ml","malta":"mt","marshall islands":"mh","martinique":"mq","mauritania":"mr","mauritius":"mu","mayotte":"yt","mexico":"mx","micronesia, federated states of":"fm","moldova":"md","monaco":"mc","mongolia":"mn","montenegro":"me","montserrat":"ms","morocco":"ma","mozambique":"mz","myanmar":"mm","namibia":"na","nauru":"nr","nepal":"np","netherlands":"nl","new caledonia":"nc","new zealand":"nz","nicaragua":"ni","niger":"ne","nigeria":"ng","niue":"nu","norfolk island":"nf","north korea":"kp","north macedonia":"mk","northern mariana islands":"mp","norway":"no","oman":"om","pakistan":"pk","palau":"pw","palestine, state of":"ps","panama":"pa","papua new guinea":"pg","paraguay":"py","peru":"pe","philippines":"ph","pitcairn":"pn","poland":"pl","portugal":"pt","puerto rico":"pr","qatar":"qa","romania":"ro","russian federation":"ru","rwanda":"rw","réunion":"re","saint barthélemy":"bl","saint helena, ascension and tristan da cunha":"sh","saint kitts and nevis":"kn","saint lucia":"lc","saint martin (french part)":"mf","saint pierre and miquelon":"pm","saint vincent and the grenadines":"vc","samoa":"ws","san marino":"sm","sao tome and principe":"st","saudi arabia":"sa","senegal":"sn","serbia":"rs","seychelles":"sc","sierra leone":"sl","singapore":"sg","sint maarten (dutch part)":"sx","slovakia":"sk","slovenia":"si","solomon islands":"sb","somalia":"so","south africa":"za","south georgia and the south sandwich islands":"gs","south korea":"kr","south sudan":"ss","spain":"es","sri lanka":"lk","sudan":"sd","suriname":"sr","svalbard and jan mayen":"sj","sweden":"se","switzerland":"ch","syria":"sy","taiwan":"tw","tajikistan":"tj","tanzania":"tz","thailand":"th","timor-leste":"tl","togo":"tg","tokelau":"tk","tonga":"to","trinidad and tobago":"tt","tunisia":"tn","turkmenistan":"tm","turks and caicos islands":"tc","tuvalu":"tv","türkiye":"tr","uganda":"ug","ukraine":"ua","united arab emirates":"ae","united kingdom":"gb","united states":"us","united states minor outlying islands":"um","uruguay":"uy","uzbekistan":"uz","vanuatu":"vu","venezuela":"ve","vietnam":"vn","virgin islands, british":"vg","virgin islands, u.s.":"vi","wallis and futuna":"wf","western sahara":"eh","yemen":"ye","zambia":"zm","zimbabwe":"zw","åland islands":"ax"},"kmap":{"de":{"console":"qwertz/de-latin1-nodeadkeys.kmap.gz","kvm":"de","name":"German","x11":"de","x11var":"nodeadkeys"},"de-ch":{"console":"qwertz/sg-latin1.kmap.gz","kvm":"de-ch","name":"Swiss-German","x11":"ch","x11var":"de_nodeadkeys"},"dk":{"console":"qwerty/dk-latin1.kmap.gz","kvm":"da","name":"Danish","x11":"dk","x11var":"nodeadkeys"},"en-gb":{"console":"qwerty/uk.kmap.gz","kvm":"en-gb","name":"United Kingdom","x11":"gb","x11var":""},"en-us":{"console":"qwerty/us-latin1.kmap.gz","kvm":"en-us","name":"U.S. English","x11":"us","x11var":""},"es":{"console":"qwerty/es.kmap.gz","kvm":"es","name":"Spanish","x11":"es","x11var":"nodeadkeys"},"fi":{"console":"qwerty/fi-latin1.kmap.gz","kvm":"fi","name":"Finnish","x11":"fi","x11var":"nodeadkeys"},"fr":{"console":"azerty/fr-latin1.kmap.gz","kvm":"fr","name":"French","x11":"fr","x11var":"nodeadkeys"},"fr-be":{"console":"azerty/be2-latin1.kmap.gz","kvm":"fr-be","name":"Belgium-French","x11":"be","x11var":"nodeadkeys"},"fr-ca":{"console":"qwerty/cf.kmap.gz","kvm":"fr-ca","name":"Canada-French","x11":"ca","x11var":"fr-legacy"},"fr-ch":{"console":"qwertz/fr_CH-latin1.kmap.gz","kvm":"fr-ch","name":"Swiss-French","x11":"ch","x11var":"fr_nodeadkeys"},"hu":{"console":"qwertz/hu.kmap.gz","kvm":"hu","name":"Hungarian","x11":"hu","x11var":""},"is":{"console":"qwerty/is-latin1.kmap.gz","kvm":"is","name":"Icelandic","x11":"is","x11var":"nodeadkeys"},"it":{"console":"qwerty/it2.kmap.gz","kvm":"it","name":"Italian","x11":"it","x11var":"nodeadkeys"},"jp":{"console":"qwerty/jp106.kmap.gz","kvm":"ja","name":"Japanese","x11":"jp","x11var":""},"lt":{"console":"qwerty/lt.kmap.gz","kvm":"lt","name":"Lithuanian","x11":"lt","x11var":"std"},"mk":{"console":"qwerty/mk.kmap.gz","kvm":"mk","name":"Macedonian","x11":"mk","x11var":"nodeadkeys"},"nl":{"console":"qwerty/nl.kmap.gz","kvm":"nl","name":"Dutch","x11":"nl","x11var":""},"no":{"console":"qwerty/no-latin1.kmap.gz","kvm":"no","name":"Norwegian","x11":"no","x11var":"nodeadkeys"},"pl":{"console":"qwerty/pl.kmap.gz","kvm":"pl","name":"Polish","x11":"pl","x11var":""},"pt":{"console":"qwerty/pt-latin1.kmap.gz","kvm":"pt","name":"Portuguese","x11":"pt","x11var":"nodeadkeys"},"pt-br":{"console":"qwerty/br-latin1.kmap.gz","kvm":"pt-br","name":"Brazil-Portuguese","x11":"br","x11var":"nodeadkeys"},"se":{"console":"qwerty/se-latin1.kmap.gz","kvm":"sv","name":"Swedish","x11":"se","x11var":"nodeadkeys"},"si":{"console":"qwertz/slovene.kmap.gz","kvm":"sl","name":"Slovenian","x11":"si","x11var":""},"tr":{"console":"qwerty/trq.kmap.gz","kvm":"tr","name":"Turkish","x11":"tr","x11var":""}},"kmaphash":{"Belgium-French":"fr-be","Brazil-Portuguese":"pt-br","Canada-French":"fr-ca","Danish":"dk","Dutch":"nl","Finnish":"fi","French":"fr","German":"de","Hungarian":"hu","Icelandic":"is","Italian":"it","Japanese":"jp","Lithuanian":"lt","Macedonian":"mk","Norwegian":"no","Polish":"pl","Portuguese":"pt","Slovenian":"si","Spanish":"es","Swedish":"se","Swiss-French":"fr-ch","Swiss-German":"de-ch","Turkish":"tr","U.S. English":"en-us","United Kingdom":"en-gb"},"zones":{"Africa/Abidjan":1,"Africa/Accra":1,"Africa/Addis_Ababa":1,"Africa/Algiers":1,"Africa/Asmara":1,"Africa/Bamako":1,"Africa/Bangui":1,"Africa/Banjul":1,"Africa/Bissau":1,"Africa/Blantyre":1,"Africa/Brazzaville":1,"Africa/Bujumbura":1,"Africa/Cairo":1,"Africa/Casablanca":1,"Africa/Ceuta":1,"Africa/Conakry":1,"Africa/Dakar":1,"Africa/Dar_es_Salaam":1,"Africa/Djibouti":1,"Africa/Douala":1,"Africa/El_Aaiun":1,"Africa/Freetown":1,"Africa/Gaborone":1,"Africa/Harare":1,"Africa/Johannesburg":1,"Africa/Juba":1,"Africa/Kampala":1,"Africa/Khartoum":1,"Africa/Kigali":1,"Africa/Kinshasa":1,"Africa/Lagos":1,"Africa/Libreville":1,"Africa/Lome":1,"Africa/Luanda":1,"Africa/Lubumbashi":1,"Africa/Lusaka":1,"Africa/Malabo":1,"Africa/Maputo":1,"Africa/Maseru":1,"Africa/Mbabane":1,"Africa/Mogadishu":1,"Africa/Monrovia":1,"Africa/Nairobi":1,"Africa/Ndjamena":1,"Africa/Niamey":1,"Africa/Nouakchott":1,"Africa/Ouagadougou":1,"Africa/Porto-Novo":1,"Africa/Sao_Tome":1,"Africa/Tripoli":1,"Africa/Tunis":1,"Africa/Windhoek":1,"America/Adak":1,"America/Anchorage":1,"America/Anguilla":1,"America/Antigua":1,"America/Araguaina":1,"America/Argentina/Buenos_Aires":1,"America/Argentina/Catamarca":1,"America/Argentina/Cordoba":1,"America/Argentina/Jujuy":1,"America/Argentina/La_Rioja":1,"America/Argentina/Mendoza":1,"America/Argentina/Rio_Gallegos":1,"America/Argentina/Salta":1,"America/Argentina/San_Juan":1,"America/Argentina/San_Luis":1,"America/Argentina/Tucuman":1,"America/Argentina/Ushuaia":1,"America/Aruba":1,"America/Asuncion":1,"America/Atikokan":1,"America/Bahia":1,"America/Bahia_Banderas":1,"America/Barbados":1,"America/Belem":1,"America/Belize":1,"America/Blanc-Sablon":1,"America/Boa_Vista":1,"America/Bogota":1,"America/Boise":1,"America/Cambridge_Bay":1,"America/Campo_Grande":1,"America/Cancun":1,"America/Caracas":1,"America/Cayenne":1,"America/Cayman":1,"America/Chicago":1,"America/Chihuahua":1,"America/Ciudad_Juarez":1,"America/Costa_Rica":1,"America/Creston":1,"America/Cuiaba":1,"America/Curacao":1,"America/Danmarkshavn":1,"America/Dawson":1,"America/Dawson_Creek":1,"America/Denver":1,"America/Detroit":1,"America/Dominica":1,"America/Edmonton":1,"America/Eirunepe":1,"America/El_Salvador":1,"America/Fort_Nelson":1,"America/Fortaleza":1,"America/Glace_Bay":1,"America/Goose_Bay":1,"America/Grand_Turk":1,"America/Grenada":1,"America/Guadeloupe":1,"America/Guatemala":1,"America/Guayaquil":1,"America/Guyana":1,"America/Halifax":1,"America/Havana":1,"America/Hermosillo":1,"America/Indiana/Indianapolis":1,"America/Indiana/Knox":1,"America/Indiana/Marengo":1,"America/Indiana/Petersburg":1,"America/Indiana/Tell_City":1,"America/Indiana/Vevay":1,"America/Indiana/Vincennes":1,"America/Indiana/Winamac":1,"America/Inuvik":1,"America/Iqaluit":1,"America/Jamaica":1,"America/Juneau":1,"America/Kentucky/Louisville":1,"America/Kentucky/Monticello":1,"America/Kralendijk":1,"America/La_Paz":1,"America/Lima":1,"America/Los_Angeles":1,"America/Lower_Princes":1,"America/Maceio":1,"America/Managua":1,"America/Manaus":1,"America/Marigot":1,"America/Martinique":1,"America/Matamoros":1,"America/Mazatlan":1,"America/Menominee":1,"America/Merida":1,"America/Metlakatla":1,"America/Mexico_City":1,"America/Miquelon":1,"America/Moncton":1,"America/Monterrey":1,"America/Montevideo":1,"America/Montserrat":1,"America/Nassau":1,"America/New_York":1,"America/Nome":1,"America/Noronha":1,"America/North_Dakota/Beulah":1,"America/North_Dakota/Center":1,"America/North_Dakota/New_Salem":1,"America/Nuuk":1,"America/Ojinaga":1,"America/Panama":1,"America/Paramaribo":1,"America/Phoenix":1,"America/Port-au-Prince":1,"America/Port_of_Spain":1,"America/Porto_Velho":1,"America/Puerto_Rico":1,"America/Punta_Arenas":1,"America/Rankin_Inlet":1,"America/Recife":1,"America/Regina":1,"America/Resolute":1,"America/Rio_Branco":1,"America/Santarem":1,"America/Santiago":1,"America/Santo_Domingo":1,"America/Sao_Paulo":1,"America/Scoresbysund":1,"America/Sitka":1,"America/St_Barthelemy":1,"America/St_Johns":1,"America/St_Kitts":1,"America/St_Lucia":1,"America/St_Thomas":1,"America/St_Vincent":1,"America/Swift_Current":1,"America/Tegucigalpa":1,"America/Thule":1,"America/Tijuana":1,"America/Toronto":1,"America/Tortola":1,"America/Vancouver":1,"America/Whitehorse":1,"America/Winnipeg":1,"America/Yakutat":1,"Antarctica/Casey":1,"Antarctica/Davis":1,"Antarctica/DumontDUrville":1,"Antarctica/Macquarie":1,"Antarctica/Mawson":1,"Antarctica/McMurdo":1,"Antarctica/Palmer":1,"Antarctica/Rothera":1,"Antarctica/Syowa":1,"Antarctica/Troll":1,"Antarctica/Vostok":1,"Arctic/Longyearbyen":1,"Asia/Aden":1,"Asia/Almaty":1,"Asia/Amman":1,"Asia/Anadyr":1,"Asia/Aqtau":1,"Asia/Aqtobe":1,"Asia/Ashgabat":1,"Asia/Atyrau":1,"Asia/Baghdad":1,"Asia/Bahrain":1,"Asia/Baku":1,"Asia/Bangkok":1,"Asia/Barnaul":1,"Asia/Beirut":1,"Asia/Bishkek":1,"Asia/Brunei":1,"Asia/Chita":1,"Asia/Choibalsan":1,"Asia/Colombo":1,"Asia/Damascus":1,"Asia/Dhaka":1,"Asia/Dili":1,"Asia/Dubai":1,"Asia/Dushanbe":1,"Asia/Famagusta":1,"Asia/Gaza":1,"Asia/Hebron":1,"Asia/Ho_Chi_Minh":1,"Asia/Hong_Kong":1,"Asia/Hovd":1,"Asia/Irkutsk":1,"Asia/Jakarta":1,"Asia/Jayapura":1,"Asia/Jerusalem":1,"Asia/Kabul":1,"Asia/Kamchatka":1,"Asia/Karachi":1,"Asia/Kathmandu":1,"Asia/Khandyga":1,"Asia/Kolkata":1,"Asia/Krasnoyarsk":1,"Asia/Kuala_Lumpur":1,"Asia/Kuching":1,"Asia/Kuwait":1,"Asia/Macau":1,"Asia/Magadan":1,"Asia/Makassar":1,"Asia/Manila":1,"Asia/Muscat":1,"Asia/Nicosia":1,"Asia/Novokuznetsk":1,"Asia/Novosibirsk":1,"Asia/Omsk":1,"Asia/Oral":1,"Asia/Phnom_Penh":1,"Asia/Pontianak":1,"Asia/Pyongyang":1,"Asia/Qatar":1,"Asia/Qostanay":1,"Asia/Qyzylorda":1,"Asia/Riyadh":1,"Asia/Sakhalin":1,"Asia/Samarkand":1,"Asia/Seoul":1,"Asia/Shanghai":1,"Asia/Singapore":1,"Asia/Srednekolymsk":1,"Asia/Taipei":1,"Asia/Tashkent":1,"Asia/Tbilisi":1,"Asia/Tehran":1,"Asia/Thimphu":1,"Asia/Tokyo":1,"Asia/Tomsk":1,"Asia/Ulaanbaatar":1,"Asia/Urumqi":1,"Asia/Ust-Nera":1,"Asia/Vientiane":1,"Asia/Vladivostok":1,"Asia/Yakutsk":1,"Asia/Yangon":1,"Asia/Yekaterinburg":1,"Asia/Yerevan":1,"Atlantic/Azores":1,"Atlantic/Bermuda":1,"Atlantic/Canary":1,"Atlantic/Cape_Verde":1,"Atlantic/Faroe":1,"Atlantic/Madeira":1,"Atlantic/Reykjavik":1,"Atlantic/South_Georgia":1,"Atlantic/St_Helena":1,"Atlantic/Stanley":1,"Australia/Adelaide":1,"Australia/Brisbane":1,"Australia/Broken_Hill":1,"Australia/Darwin":1,"Australia/Eucla":1,"Australia/Hobart":1,"Australia/Lindeman":1,"Australia/Lord_Howe":1,"Australia/Melbourne":1,"Australia/Perth":1,"Australia/Sydney":1,"Europe/Amsterdam":1,"Europe/Andorra":1,"Europe/Astrakhan":1,"Europe/Athens":1,"Europe/Belgrade":1,"Europe/Berlin":1,"Europe/Bratislava":1,"Europe/Brussels":1,"Europe/Bucharest":1,"Europe/Budapest":1,"Europe/Busingen":1,"Europe/Chisinau":1,"Europe/Copenhagen":1,"Europe/Dublin":1,"Europe/Gibraltar":1,"Europe/Guernsey":1,"Europe/Helsinki":1,"Europe/Isle_of_Man":1,"Europe/Istanbul":1,"Europe/Jersey":1,"Europe/Kaliningrad":1,"Europe/Kirov":1,"Europe/Kyiv":1,"Europe/Lisbon":1,"Europe/Ljubljana":1,"Europe/London":1,"Europe/Luxembourg":1,"Europe/Madrid":1,"Europe/Malta":1,"Europe/Mariehamn":1,"Europe/Minsk":1,"Europe/Monaco":1,"Europe/Moscow":1,"Europe/Oslo":1,"Europe/Paris":1,"Europe/Podgorica":1,"Europe/Prague":1,"Europe/Riga":1,"Europe/Rome":1,"Europe/Samara":1,"Europe/San_Marino":1,"Europe/Sarajevo":1,"Europe/Saratov":1,"Europe/Simferopol":1,"Europe/Skopje":1,"Europe/Sofia":1,"Europe/Stockholm":1,"Europe/Tallinn":1,"Europe/Tirane":1,"Europe/Ulyanovsk":1,"Europe/Vaduz":1,"Europe/Vatican":1,"Europe/Vienna":1,"Europe/Vilnius":1,"Europe/Volgograd":1,"Europe/Warsaw":1,"Europe/Zagreb":1,"Europe/Zurich":1,"Indian/Antananarivo":1,"Indian/Chagos":1,"Indian/Christmas":1,"Indian/Cocos":1,"Indian/Comoro":1,"Indian/Kerguelen":1,"Indian/Mahe":1,"Indian/Maldives":1,"Indian/Mauritius":1,"Indian/Mayotte":1,"Indian/Reunion":1,"Pacific/Apia":1,"Pacific/Auckland":1,"Pacific/Bougainville":1,"Pacific/Chatham":1,"Pacific/Chuuk":1,"Pacific/Easter":1,"Pacific/Efate":1,"Pacific/Fakaofo":1,"Pacific/Fiji":1,"Pacific/Funafuti":1,"Pacific/Galapagos":1,"Pacific/Gambier":1,"Pacific/Guadalcanal":1,"Pacific/Guam":1,"Pacific/Honolulu":1,"Pacific/Kanton":1,"Pacific/Kiritimati":1,"Pacific/Kosrae":1,"Pacific/Kwajalein":1,"Pacific/Majuro":1,"Pacific/Marquesas":1,"Pacific/Midway":1,"Pacific/Nauru":1,"Pacific/Niue":1,"Pacific/Norfolk":1,"Pacific/Noumea":1,"Pacific/Pago_Pago":1,"Pacific/Palau":1,"Pacific/Pitcairn":1,"Pacific/Pohnpei":1,"Pacific/Port_Moresby":1,"Pacific/Rarotonga":1,"Pacific/Saipan":1,"Pacific/Tahiti":1,"Pacific/Tarawa":1,"Pacific/Tongatapu":1,"Pacific/Wake":1,"Pacific/Wallis":1}}
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "disk_selection": {
+ "6": "6",
+ "7": "7",
+ "8": "8",
+ "9": "9"
+ },
+ "lvm_auto_rename": 1,
+ "filesys": "zfs (RAID10)",
+ "gateway": "192.168.1.1",
+ "hdsize": 223.57088470458984,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "eno1",
+ "password": "123456",
+ "timezone": "Europe/Vienna",
+ "zfs_opts": {
+ "arc_max": 2048,
+ "ashift": 12,
+ "checksum": "on",
+ "compress": "on",
+ "copies": 1
+ }
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "zfs"
+zfs.raid = "raid10"
+#disk_list = ['sda']
+filter.ID_SERIAL = "*MZ7KM240HAGR*"
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "disk_selection": {
+ "9": "9"
+ },
+ "lvm_auto_rename": 1,
+ "filesys": "zfs (RAID0)",
+ "gateway": "192.168.1.1",
+ "hdsize": 223.57088470458984,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "eno1",
+ "password": "123456",
+ "timezone": "Europe/Vienna",
+ "zfs_opts": {
+ "arc_max": 2048,
+ "ashift": 12,
+ "checksum": "on",
+ "compress": "on",
+ "copies": 1
+ }
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "zfs"
+zfs.raid = "raid0"
+filter_match = "all"
+filter.ID_SERIAL = "*MZ7KM240HAGR*"
+filter.ID_SERIAL_SHORT = "S2HRNX0J403419"
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "disk_selection": {
+ "0": "0",
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "6": "6",
+ "7": "7",
+ "8": "8",
+ "9": "9"
+ },
+ "lvm_auto_rename": 1,
+ "filesys": "zfs (RAID10)",
+ "gateway": "192.168.1.1",
+ "hdsize": 2980.820640563965,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "eno1",
+ "password": "123456",
+ "timezone": "Europe/Vienna",
+ "zfs_opts": {
+ "arc_max": 2048,
+ "ashift": 12,
+ "checksum": "on",
+ "compress": "on",
+ "copies": 1
+ }
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "zfs"
+zfs.raid = "raid10"
+filter_match = "any"
+filter.ID_SERIAL = "*MZ7KM240HAGR*"
+filter.ID_MODEL = "Micron_9300*"
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "filesys": "ext4",
+ "gateway": "192.168.1.1",
+ "hdsize": 223.57088470458984,
+ "lvm_auto_rename": 1,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "eno1",
+ "password": "123456",
+ "target_hd": "/dev/sda",
+ "timezone": "Europe/Vienna"
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "10.10.10.10/24",
+ "country": "at",
+ "dns": "10.10.10.1",
+ "domain": "testinstall",
+ "filesys": "ext4",
+ "gateway": "10.10.10.1",
+ "hdsize": 223.57088470458984,
+ "lvm_auto_rename": 1,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "enp65s0f0",
+ "password": "123456",
+ "target_hd": "/dev/sda",
+ "timezone": "Europe/Vienna"
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-answer"
+cidr = "10.10.10.10/24"
+dns = "10.10.10.1"
+gateway = "10.10.10.1"
+filter.ID_NET_NAME_MAC = "*a0369f0ab382"
+
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
--- /dev/null
+the size parameter from /sys/block/{disk}/size is the number of blocks.
+
+to calculate the size as the low level installer needs it:
+size * 512 / 1024 / 1024 / 1024 with 14 digits after the comma
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "10.10.10.10/24",
+ "country": "at",
+ "dns": "10.10.10.1",
+ "domain": "testinstall",
+ "filesys": "ext4",
+ "gateway": "10.10.10.1",
+ "hdsize": 223.57088470458984,
+ "lvm_auto_rename": 1,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "enp129s0f1np1",
+ "password": "123456",
+ "target_hd": "/dev/sda",
+ "timezone": "Europe/Vienna"
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-answer"
+cidr = "10.10.10.10/24"
+dns = "10.10.10.1"
+gateway = "10.10.10.1"
+filter.ID_NET_NAME = "enp129s0f1np1"
+
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
--- /dev/null
+{
+ "autoreboot": 1,
+ "cidr": "192.168.1.114/24",
+ "country": "at",
+ "dns": "192.168.1.254",
+ "domain": "testinstall",
+ "disk_selection": {
+ "6": "6",
+ "7": "7"
+ },
+ "lvm_auto_rename": 1,
+ "filesys": "zfs (RAID1)",
+ "gateway": "192.168.1.1",
+ "hdsize": 80.0,
+ "hostname": "pveauto",
+ "keymap": "de",
+ "mailto": "mail@no.invalid",
+ "mngmt_nic": "eno1",
+ "password": "123456",
+ "timezone": "Europe/Vienna",
+ "zfs_opts": {
+ "arc_max": 2048,
+ "ashift": 12,
+ "checksum": "on",
+ "compress": "lz4",
+ "copies": 2
+ }
+}
--- /dev/null
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "123456"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "zfs"
+zfs.raid = "raid1"
+zfs.ashift = 12
+zfs.checksum = "on"
+zfs.compress = "lz4"
+zfs.copies = 2
+zfs.hdsize = 80
+disk_list = ["sda", "sdb"]
--- /dev/null
+{"boot_type":"efi","country":"at","disks":[[0,"/dev/nvme0n1",6251233968,"Micron_9300_MTFDHAL3T2TDR",4096,"/sys/block/nvme0n1"],[1,"/dev/nvme1n1",6251233968,"Micron_9300_MTFDHAL3T2TDR",4096,"/sys/block/nvme1n1"],[2,"/dev/nvme2n1",6251233968,"Micron_9300_MTFDHAL3T2TDR",4096,"/sys/block/nvme2n1"],[3,"/dev/nvme3n1",6251233968,"Micron_9300_MTFDHAL3T2TDR",4096,"/sys/block/nvme3n1"],[4,"/dev/nvme4n1",976773168,"Samsung SSD 970 EVO Plus 500GB",512,"/sys/block/nvme4n1"],[5,"/dev/nvme5n1",732585168,"INTEL SSDPED1K375GA",512,"/sys/block/nvme5n1"],[6,"/dev/sda",468862128,"SAMSUNG MZ7KM240",512,"/sys/block/sda"],[7,"/dev/sdb",468862128,"SAMSUNG MZ7KM240",512,"/sys/block/sdb"],[8,"/dev/sdc",468862128,"SAMSUNG MZ7KM240",512,"/sys/block/sdc"],[9,"/dev/sdd",468862128,"SAMSUNG MZ7KM240",512,"/sys/block/sdd"]],"hvm_supported":1,"ipconf":{"default":"4","dnsserver":"192.168.1.254","domain":null,"gateway":"192.168.1.1","ifaces":{"10":{"driver":"mlx5_core","flags":"NO-CARRIER,BROADCAST,MULTICAST,UP","mac":"24:8a:07:1e:05:bd","name":"enp193s0f1np1","state":"DOWN"},"2":{"driver":"igb","flags":"NO-CARRIER,BROADCAST,MULTICAST,UP","mac":"a0:36:9f:0a:b3:82","name":"enp65s0f0","state":"DOWN"},"3":{"driver":"igb","flags":"NO-CARRIER,BROADCAST,MULTICAST,UP","mac":"a0:36:9f:0a:b3:83","name":"enp65s0f1","state":"DOWN"},"4":{"driver":"igb","flags":"BROADCAST,MULTICAST,UP,LOWER_UP","inet":{"addr":"192.168.1.114","mask":"255.255.240.0","prefix":20},"mac":"b4:2e:99:ac:ad:b4","name":"eno1","state":"UP"},"5":{"driver":"cdc_ether","flags":"BROADCAST,MULTICAST,UP,LOWER_UP","mac":"5a:47:32:dd:c7:47","name":"enx5a4732ddc747","state":"UNKNOWN"},"6":{"driver":"igb","flags":"BROADCAST,MULTICAST,UP,LOWER_UP","mac":"b4:2e:99:ac:ad:b5","name":"eno2","state":"UP"},"7":{"driver":"mlx5_core","flags":"NO-CARRIER,BROADCAST,MULTICAST,UP","mac":"1c:34:da:5c:5e:24","name":"enp129s0f0np0","state":"DOWN"},"8":{"driver":"mlx5_core","flags":"NO-CARRIER,BROADCAST,MULTICAST,UP","mac":"1c:34:da:5c:5e:25","name":"enp129s0f1np1","state":"DOWN"},"9":{"driver":"mlx5_core","flags":"BROADCAST,MULTICAST,UP,LOWER_UP","mac":"24:8a:07:1e:05:bc","name":"enp193s0f0np0","state":"UP"}}},"kernel_cmdline":"BOOT_IMAGE=/boot/linux26 ro ramdisk_size=16777216 rw splash=verbose proxdebug vga=788","network":{"dns":{"dns":["192.168.1.254"],"domain":null},"interfaces":{"eno1":{"addresses":[{"address":"192.168.1.114","family":"inet","prefix":24}],"index":4,"mac":"b4:2e:99:ac:ad:b4","name":"eno1","state":"UP"},"eno2":{"index":6,"mac":"b4:2e:99:ac:ad:b5","name":"eno2","state":"UP"},"enp129s0f0np0":{"index":7,"mac":"1c:34:da:5c:5e:24","name":"enp129s0f0np0","state":"DOWN"},"enp129s0f1np1":{"index":8,"mac":"1c:34:da:5c:5e:25","name":"enp129s0f1np1","state":"DOWN"},"enp193s0f0np0":{"index":9,"mac":"24:8a:07:1e:05:bc","name":"enp193s0f0np0","state":"UP"},"enp193s0f1np1":{"index":10,"mac":"24:8a:07:1e:05:bd","name":"enp193s0f1np1","state":"DOWN"},"enp65s0f0":{"index":2,"mac":"a0:36:9f:0a:b3:82","name":"enp65s0f0","state":"DOWN"},"enp65s0f1":{"index":3,"mac":"a0:36:9f:0a:b3:83","name":"enp65s0f1","state":"DOWN"},"enx5a4732ddc747":{"index":5,"mac":"5a:47:32:dd:c7:47","name":"enx5a4732ddc747","state":"UNKNOWN"}},"routes":{"gateway4":{"dev":"eno1","gateway":"192.168.1.1"}}},"total_memory":257597}
--- /dev/null
+{"disks":{"0":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-path/pci-0000:01:00.0-nvme-1 /dev/disk/by-id/nvme-Micron_9300_MTFDHAL3T2TDR_19502596FC74 /dev/disk/by-id/lvm-pv-uuid-hl5Cyv-dghE-CcX8-lDCV-6BSj-EbFU-cT4dIP /dev/disk/by-diskseq/16 /dev/disk/by-id/nvme-eui.000000000000001500a075012596fc74","DEVNAME":"/dev/nvme0n1","DEVPATH":"/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1","DEVTYPE":"disk","DISKSEQ":"16","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"hl5Cyv-dghE-CcX8-lDCV-6BSj-EbFU-cT4dIP","ID_FS_UUID_ENC":"hl5Cyv-dghE-CcX8-lDCV-6BSj-EbFU-cT4dIP","ID_FS_VERSION":"LVM2 001","ID_MODEL":"Micron_9300_MTFDHAL3T2TDR","ID_PATH":"pci-0000:01:00.0-nvme-1","ID_PATH_TAG":"pci-0000_01_00_0-nvme-1","ID_REVISION":"11300DN0","ID_SERIAL":"Micron_9300_MTFDHAL3T2TDR_19502596FC74","ID_SERIAL_SHORT":"19502596FC74","ID_WWN":"eui.000000000000001500a075012596fc74","LVM_VG_NAME_COMPLETE":"ceph-67f6a633-8bac-4ba6-a54c-40f0d24a9701","MAJOR":"259","MINOR":"6","SUBSYSTEM":"block","SYSTEMD_READY":"1","TAGS":":systemd:","USEC_INITIALIZED":"45215609"},"1":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-path/pci-0000:02:00.0-nvme-1 /dev/disk/by-id/nvme-eui.000000000000001400a0750125de7a16 /dev/disk/by-diskseq/15 /dev/disk/by-id/nvme-Micron_9300_MTFDHAL3T2TDR_195225DE7A16","DEVNAME":"/dev/nvme1n1","DEVPATH":"/devices/pci0000:00/0000:00:01.2/0000:02:00.0/nvme/nvme1/nvme1n1","DEVTYPE":"disk","DISKSEQ":"15","ID_MODEL":"Micron_9300_MTFDHAL3T2TDR","ID_PATH":"pci-0000:02:00.0-nvme-1","ID_PATH_TAG":"pci-0000_02_00_0-nvme-1","ID_REVISION":"11300DN0","ID_SERIAL":"Micron_9300_MTFDHAL3T2TDR_195225DE7A16","ID_SERIAL_SHORT":"195225DE7A16","ID_WWN":"eui.000000000000001400a0750125de7a16","MAJOR":"259","MINOR":"5","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"43271971"},"2":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-path/pci-0000:03:00.0-nvme-1 /dev/disk/by-diskseq/17 /dev/disk/by-id/lvm-pv-uuid-b92FQw-lExM-2EYR-5UyV-T6cl-yzsM-qRjCOU /dev/disk/by-id/nvme-Micron_9300_MTFDHAL3T2TDR_1945250F206E /dev/disk/by-id/nvme-eui.000000000000001400a07501250f206e","DEVNAME":"/dev/nvme2n1","DEVPATH":"/devices/pci0000:00/0000:00:01.3/0000:03:00.0/nvme/nvme2/nvme2n1","DEVTYPE":"disk","DISKSEQ":"17","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"b92FQw-lExM-2EYR-5UyV-T6cl-yzsM-qRjCOU","ID_FS_UUID_ENC":"b92FQw-lExM-2EYR-5UyV-T6cl-yzsM-qRjCOU","ID_FS_VERSION":"LVM2 001","ID_MODEL":"Micron_9300_MTFDHAL3T2TDR","ID_PATH":"pci-0000:03:00.0-nvme-1","ID_PATH_TAG":"pci-0000_03_00_0-nvme-1","ID_REVISION":"11300DN0","ID_SERIAL":"Micron_9300_MTFDHAL3T2TDR_1945250F206E","ID_SERIAL_SHORT":"1945250F206E","ID_WWN":"eui.000000000000001400a07501250f206e","LVM_VG_NAME_COMPLETE":"ceph-ee820014-6121-458b-a661-889f0901bff6","MAJOR":"259","MINOR":"7","SUBSYSTEM":"block","SYSTEMD_READY":"1","TAGS":":systemd:","USEC_INITIALIZED":"45218640"},"3":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-path/pci-0000:04:00.0-nvme-1 /dev/disk/by-id/lvm-pv-uuid-f56spY-IptZ-fH5e-AqQv-K1cI-3nnt-2UlO17 /dev/disk/by-id/nvme-Micron_9300_MTFDHAL3T2TDR_1945250F20AC /dev/disk/by-diskseq/18 /dev/disk/by-id/nvme-eui.000000000000001400a07501250f20ac","DEVNAME":"/dev/nvme3n1","DEVPATH":"/devices/pci0000:00/0000:00:01.4/0000:04:00.0/nvme/nvme3/nvme3n1","DEVTYPE":"disk","DISKSEQ":"18","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"f56spY-IptZ-fH5e-AqQv-K1cI-3nnt-2UlO17","ID_FS_UUID_ENC":"f56spY-IptZ-fH5e-AqQv-K1cI-3nnt-2UlO17","ID_FS_VERSION":"LVM2 001","ID_MODEL":"Micron_9300_MTFDHAL3T2TDR","ID_PATH":"pci-0000:04:00.0-nvme-1","ID_PATH_TAG":"pci-0000_04_00_0-nvme-1","ID_REVISION":"11300DN0","ID_SERIAL":"Micron_9300_MTFDHAL3T2TDR_1945250F20AC","ID_SERIAL_SHORT":"1945250F20AC","ID_WWN":"eui.000000000000001400a07501250f20ac","LVM_VG_NAME_COMPLETE":"ceph-2928aceb-9300-4175-8640-e227d897d45e","MAJOR":"259","MINOR":"8","SUBSYSTEM":"block","SYSTEMD_READY":"1","TAGS":":systemd:","USEC_INITIALIZED":"45215244"},"4":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-diskseq/13 /dev/disk/by-path/pci-0000:82:00.0-nvme-1 /dev/disk/by-id/lvm-pv-uuid-jFM6eE-KUmT-fTBO-9SWe-4VJG-W4rW-DUQPRd /dev/disk/by-id/nvme-INTEL_SSDPED1K375GA_PHKS746500DK375AGN /dev/disk/by-id/nvme-nvme.8086-50484b53373436353030444b33373541474e-494e54454c20535344504544314b3337354741-00000001","DEVNAME":"/dev/nvme4n1","DEVPATH":"/devices/pci0000:80/0000:80:03.1/0000:82:00.0/nvme/nvme4/nvme4n1","DEVTYPE":"disk","DISKSEQ":"13","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"jFM6eE-KUmT-fTBO-9SWe-4VJG-W4rW-DUQPRd","ID_FS_UUID_ENC":"jFM6eE-KUmT-fTBO-9SWe-4VJG-W4rW-DUQPRd","ID_FS_VERSION":"LVM2 001","ID_MODEL":"INTEL SSDPED1K375GA","ID_PATH":"pci-0000:82:00.0-nvme-1","ID_PATH_TAG":"pci-0000_82_00_0-nvme-1","ID_REVISION":"E2010435","ID_SERIAL":"INTEL_SSDPED1K375GA_PHKS746500DK375AGN","ID_SERIAL_SHORT":"PHKS746500DK375AGN","ID_WWN":"nvme.8086-50484b53373436353030444b33373541474e-494e54454c20535344504544314b3337354741-00000001","LVM_VG_NAME_COMPLETE":"ceph-b4af8112-88e7-4cd4-9cf9-0f4163ca77bd","MAJOR":"259","MINOR":"0","SUBSYSTEM":"block","SYSTEMD_READY":"1","TAGS":":systemd:","USEC_INITIALIZED":"45219471"},"5":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-id/nvme-eui.0025385791b04175 /dev/disk/by-id/nvme-Samsung_SSD_970_EVO_Plus_500GB_S4EVNF0M703256N /dev/disk/by-path/pci-0000:06:00.0-nvme-1 /dev/disk/by-diskseq/14","DEVNAME":"/dev/nvme5n1","DEVPATH":"/devices/pci0000:00/0000:00:03.3/0000:06:00.0/nvme/nvme5/nvme5n1","DEVTYPE":"disk","DISKSEQ":"14","ID_MODEL":"Samsung SSD 970 EVO Plus 500GB","ID_PART_TABLE_TYPE":"gpt","ID_PART_TABLE_UUID":"1c40cb4b-72d8-49ec-804b-e5933e09423d","ID_PATH":"pci-0000:06:00.0-nvme-1","ID_PATH_TAG":"pci-0000_06_00_0-nvme-1","ID_REVISION":"2B2QEXM7","ID_SERIAL":"Samsung_SSD_970_EVO_Plus_500GB_S4EVNF0M703256N","ID_SERIAL_SHORT":"S4EVNF0M703256N","ID_WWN":"eui.0025385791b04175","MAJOR":"259","MINOR":"1","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"43271933"},"6":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-id/lvm-pv-uuid-tMMNAX-noqI-P0oS-9OEJ-7IR5-WoRL-N5K5Cv /dev/disk/by-id/ata-SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403550 /dev/disk/by-path/pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy0-lun-0 /dev/disk/by-diskseq/9 /dev/disk/by-id/wwn-0x5002538c405dbf10","DEVNAME":"/dev/sda","DEVPATH":"/devices/pci0000:00/0000:00:03.1/0000:05:00.0/host8/port-8:0/expander-8:0/port-8:0:0/end_device-8:0:0/target8:0:0/8:0:0:0/block/sda","DEVTYPE":"disk","DISKSEQ":"9","ID_ATA":"1","ID_ATA_DOWNLOAD_MICROCODE":"1","ID_ATA_FEATURE_SET_HPA":"1","ID_ATA_FEATURE_SET_HPA_ENABLED":"1","ID_ATA_FEATURE_SET_PM":"1","ID_ATA_FEATURE_SET_PM_ENABLED":"1","ID_ATA_FEATURE_SET_SECURITY":"1","ID_ATA_FEATURE_SET_SECURITY_ENABLED":"0","ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SMART":"1","ID_ATA_FEATURE_SET_SMART_ENABLED":"1","ID_ATA_ROTATION_RATE_RPM":"0","ID_ATA_SATA":"1","ID_ATA_WRITE_CACHE":"1","ID_ATA_WRITE_CACHE_ENABLED":"1","ID_BUS":"ata","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"tMMNAX-noqI-P0oS-9OEJ-7IR5-WoRL-N5K5Cv","ID_FS_UUID_ENC":"tMMNAX-noqI-P0oS-9OEJ-7IR5-WoRL-N5K5Cv","ID_FS_VERSION":"LVM2 001","ID_MODEL":"SAMSUNG_MZ7KM240HAGR-00005","ID_MODEL_ENC":"SAMSUNG\\x20MZ7KM240HAGR-00005\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20","ID_PATH":"pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy0-lun-0","ID_PATH_TAG":"pci-0000_05_00_0-sas-exp0x500304801f3f7f7f-phy0-lun-0","ID_REVISION":"GXM1103Q","ID_SERIAL":"SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403550","ID_SERIAL_SHORT":"S2HRNX0J403550","ID_TYPE":"disk","ID_WWN":"0x5002538c405dbf10","ID_WWN_WITH_EXTENSION":"0x5002538c405dbf10","MAJOR":"8","MINOR":"0","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"45234812"},"7":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-id/wwn-0x5002538c405dbce5 /dev/disk/by-path/pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy1-lun-0 /dev/disk/by-id/lvm-pv-uuid-oPUG7c-CMh3-oHQy-YRZP-8cNJ-uMIv-ceVPZu /dev/disk/by-diskseq/10 /dev/disk/by-id/ata-SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403335","DEVNAME":"/dev/sdb","DEVPATH":"/devices/pci0000:00/0000:00:03.1/0000:05:00.0/host8/port-8:0/expander-8:0/port-8:0:1/end_device-8:0:1/target8:0:1/8:0:1:0/block/sdb","DEVTYPE":"disk","DISKSEQ":"10","ID_ATA":"1","ID_ATA_DOWNLOAD_MICROCODE":"1","ID_ATA_FEATURE_SET_HPA":"1","ID_ATA_FEATURE_SET_HPA_ENABLED":"1","ID_ATA_FEATURE_SET_PM":"1","ID_ATA_FEATURE_SET_PM_ENABLED":"1","ID_ATA_FEATURE_SET_SECURITY":"1","ID_ATA_FEATURE_SET_SECURITY_ENABLED":"0","ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SMART":"1","ID_ATA_FEATURE_SET_SMART_ENABLED":"1","ID_ATA_ROTATION_RATE_RPM":"0","ID_ATA_SATA":"1","ID_ATA_WRITE_CACHE":"1","ID_ATA_WRITE_CACHE_ENABLED":"1","ID_BUS":"ata","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"oPUG7c-CMh3-oHQy-YRZP-8cNJ-uMIv-ceVPZu","ID_FS_UUID_ENC":"oPUG7c-CMh3-oHQy-YRZP-8cNJ-uMIv-ceVPZu","ID_FS_VERSION":"LVM2 001","ID_MODEL":"SAMSUNG_MZ7KM240HAGR-00005","ID_MODEL_ENC":"SAMSUNG\\x20MZ7KM240HAGR-00005\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20","ID_PATH":"pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy1-lun-0","ID_PATH_TAG":"pci-0000_05_00_0-sas-exp0x500304801f3f7f7f-phy1-lun-0","ID_REVISION":"GXM1103Q","ID_SERIAL":"SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403335","ID_SERIAL_SHORT":"S2HRNX0J403335","ID_TYPE":"disk","ID_WWN":"0x5002538c405dbce5","ID_WWN_WITH_EXTENSION":"0x5002538c405dbce5","MAJOR":"8","MINOR":"16","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"45215406"},"8":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-id/wwn-0x5002538c405dbcd9 /dev/disk/by-diskseq/11 /dev/disk/by-id/ata-SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403333 /dev/disk/by-path/pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy2-lun-0 /dev/disk/by-id/lvm-pv-uuid-tbguYd-sqom-3Okm-aJ0F-0F8N-2ALl-lo7ONW","DEVNAME":"/dev/sdc","DEVPATH":"/devices/pci0000:00/0000:00:03.1/0000:05:00.0/host8/port-8:0/expander-8:0/port-8:0:2/end_device-8:0:2/target8:0:2/8:0:2:0/block/sdc","DEVTYPE":"disk","DISKSEQ":"11","ID_ATA":"1","ID_ATA_DOWNLOAD_MICROCODE":"1","ID_ATA_FEATURE_SET_HPA":"1","ID_ATA_FEATURE_SET_HPA_ENABLED":"1","ID_ATA_FEATURE_SET_PM":"1","ID_ATA_FEATURE_SET_PM_ENABLED":"1","ID_ATA_FEATURE_SET_SECURITY":"1","ID_ATA_FEATURE_SET_SECURITY_ENABLED":"0","ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SMART":"1","ID_ATA_FEATURE_SET_SMART_ENABLED":"1","ID_ATA_ROTATION_RATE_RPM":"0","ID_ATA_SATA":"1","ID_ATA_WRITE_CACHE":"1","ID_ATA_WRITE_CACHE_ENABLED":"1","ID_BUS":"ata","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"tbguYd-sqom-3Okm-aJ0F-0F8N-2ALl-lo7ONW","ID_FS_UUID_ENC":"tbguYd-sqom-3Okm-aJ0F-0F8N-2ALl-lo7ONW","ID_FS_VERSION":"LVM2 001","ID_MODEL":"SAMSUNG_MZ7KM240HAGR-00005","ID_MODEL_ENC":"SAMSUNG\\x20MZ7KM240HAGR-00005\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20","ID_PATH":"pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy2-lun-0","ID_PATH_TAG":"pci-0000_05_00_0-sas-exp0x500304801f3f7f7f-phy2-lun-0","ID_REVISION":"GXM1103Q","ID_SERIAL":"SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403333","ID_SERIAL_SHORT":"S2HRNX0J403333","ID_TYPE":"disk","ID_WWN":"0x5002538c405dbcd9","ID_WWN_WITH_EXTENSION":"0x5002538c405dbcd9","MAJOR":"8","MINOR":"32","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"45198824"},"9":{"CURRENT_TAGS":":systemd:","DEVLINKS":"/dev/disk/by-diskseq/12 /dev/disk/by-id/wwn-0x5002538c405dbdc5 /dev/disk/by-id/lvm-pv-uuid-Lpxa0X-i8MT-EYWV-J7yQ-r5x7-S99u-jLf8bz /dev/disk/by-path/pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy5-lun-0 /dev/disk/by-id/ata-SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403419","DEVNAME":"/dev/sdd","DEVPATH":"/devices/pci0000:00/0000:00:03.1/0000:05:00.0/host8/port-8:0/expander-8:0/port-8:0:3/end_device-8:0:3/target8:0:3/8:0:3:0/block/sdd","DEVTYPE":"disk","DISKSEQ":"12","ID_ATA":"1","ID_ATA_DOWNLOAD_MICROCODE":"1","ID_ATA_FEATURE_SET_HPA":"1","ID_ATA_FEATURE_SET_HPA_ENABLED":"1","ID_ATA_FEATURE_SET_PM":"1","ID_ATA_FEATURE_SET_PM_ENABLED":"1","ID_ATA_FEATURE_SET_SECURITY":"1","ID_ATA_FEATURE_SET_SECURITY_ENABLED":"0","ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN":"32","ID_ATA_FEATURE_SET_SMART":"1","ID_ATA_FEATURE_SET_SMART_ENABLED":"1","ID_ATA_ROTATION_RATE_RPM":"0","ID_ATA_SATA":"1","ID_ATA_WRITE_CACHE":"1","ID_ATA_WRITE_CACHE_ENABLED":"1","ID_BUS":"ata","ID_FS_TYPE":"LVM2_member","ID_FS_USAGE":"raid","ID_FS_UUID":"Lpxa0X-i8MT-EYWV-J7yQ-r5x7-S99u-jLf8bz","ID_FS_UUID_ENC":"Lpxa0X-i8MT-EYWV-J7yQ-r5x7-S99u-jLf8bz","ID_FS_VERSION":"LVM2 001","ID_MODEL":"SAMSUNG_MZ7KM240HAGR-00005","ID_MODEL_ENC":"SAMSUNG\\x20MZ7KM240HAGR-00005\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20","ID_PATH":"pci-0000:05:00.0-sas-exp0x500304801f3f7f7f-phy5-lun-0","ID_PATH_TAG":"pci-0000_05_00_0-sas-exp0x500304801f3f7f7f-phy5-lun-0","ID_REVISION":"GXM1103Q","ID_SERIAL":"SAMSUNG_MZ7KM240HAGR-00005_S2HRNX0J403419","ID_SERIAL_SHORT":"S2HRNX0J403419","ID_TYPE":"disk","ID_WWN":"0x5002538c405dbdc5","ID_WWN_WITH_EXTENSION":"0x5002538c405dbdc5","MAJOR":"8","MINOR":"48","SUBSYSTEM":"block","TAGS":":systemd:","USEC_INITIALIZED":"45215283"}},"nics":{"eno1":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:c0/0000:c0:03.5/0000:c2:00.0/net/eno1","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"I350 Gigabit Network Connection","ID_MODEL_ID":"0x1521","ID_NET_DRIVER":"igb","ID_NET_LABEL_ONBOARD":"Onboard LAN1","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"eno1","ID_NET_NAME_MAC":"enxb42e99acadb4","ID_NET_NAME_ONBOARD":"eno1","ID_NET_NAME_PATH":"enp194s0f0","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"GIGA-BYTE TECHNOLOGY CO.,LTD.","ID_PATH":"pci-0000:c2:00.0","ID_PATH_TAG":"pci-0000_c2_00_0","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Intel Corporation","ID_VENDOR_ID":"0x8086","IFINDEX":"5","INTERFACE":"eno1","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/eno1","TAGS":":systemd:","USEC_INITIALIZED":"45212091"},"eno2":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:c0/0000:c0:03.5/0000:c2:00.1/net/eno2","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"I350 Gigabit Network Connection","ID_MODEL_ID":"0x1521","ID_NET_DRIVER":"igb","ID_NET_LABEL_ONBOARD":"Onboard LAN2","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"eno2","ID_NET_NAME_MAC":"enxb42e99acadb5","ID_NET_NAME_ONBOARD":"eno2","ID_NET_NAME_PATH":"enp194s0f1","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"GIGA-BYTE TECHNOLOGY CO.,LTD.","ID_PATH":"pci-0000:c2:00.1","ID_PATH_TAG":"pci-0000_c2_00_1","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Intel Corporation","ID_VENDOR_ID":"0x8086","IFINDEX":"6","INTERFACE":"eno2","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/eno2","TAGS":":systemd:","USEC_INITIALIZED":"45128159"},"enp129s0f0np0":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:80/0000:80:01.1/0000:81:00.0/net/enp129s0f0np0","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"MT27710 Family [ConnectX-4 Lx] (MCX4421A-ACQN ConnectX-4 Lx EN OCP,2x25G)","ID_MODEL_ID":"0x1015","ID_NET_DRIVER":"mlx5_core","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp129s0f0np0","ID_NET_NAME_MAC":"enx1c34da5c5e24","ID_NET_NAME_PATH":"enp129s0f0np0","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Mellanox Technologies, Inc.","ID_PATH":"pci-0000:81:00.0","ID_PATH_TAG":"pci-0000_81_00_0","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Mellanox Technologies","ID_VENDOR_ID":"0x15b3","IFINDEX":"7","INTERFACE":"enp129s0f0np0","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp129s0f0np0","TAGS":":systemd:","USEC_INITIALIZED":"47752091"},"enp129s0f1np1":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:80/0000:80:01.1/0000:81:00.1/net/enp129s0f1np1","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"MT27710 Family [ConnectX-4 Lx] (MCX4421A-ACQN ConnectX-4 Lx EN OCP,2x25G)","ID_MODEL_ID":"0x1015","ID_NET_DRIVER":"mlx5_core","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp129s0f1np1","ID_NET_NAME_MAC":"enx1c34da5c5e25","ID_NET_NAME_PATH":"enp129s0f1np1","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Mellanox Technologies, Inc.","ID_PATH":"pci-0000:81:00.1","ID_PATH_TAG":"pci-0000_81_00_1","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Mellanox Technologies","ID_VENDOR_ID":"0x15b3","IFINDEX":"8","INTERFACE":"enp129s0f1np1","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp129s0f1np1","TAGS":":systemd:","USEC_INITIALIZED":"47716100"},"enp193s0f0np0":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:c0/0000:c0:01.1/0000:c1:00.0/net/enp193s0f0np0","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"MT27700 Family [ConnectX-4]","ID_MODEL_ID":"0x1013","ID_NET_DRIVER":"mlx5_core","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp193s0f0np0","ID_NET_NAME_MAC":"enx248a071e05bc","ID_NET_NAME_PATH":"enp193s0f0np0","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Mellanox Technologies, Inc.","ID_PATH":"pci-0000:c1:00.0","ID_PATH_TAG":"pci-0000_c1_00_0","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Mellanox Technologies","ID_VENDOR_ID":"0x15b3","IFINDEX":"9","INTERFACE":"enp193s0f0np0","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp193s0f0np0","TAGS":":systemd:","USEC_INITIALIZED":"47784094"},"enp193s0f1np1":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:c0/0000:c0:01.1/0000:c1:00.1/net/enp193s0f1np1","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"MT27700 Family [ConnectX-4]","ID_MODEL_ID":"0x1013","ID_NET_DRIVER":"mlx5_core","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp193s0f1np1","ID_NET_NAME_MAC":"enx248a071e05bd","ID_NET_NAME_PATH":"enp193s0f1np1","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Mellanox Technologies, Inc.","ID_PATH":"pci-0000:c1:00.1","ID_PATH_TAG":"pci-0000_c1_00_1","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Mellanox Technologies","ID_VENDOR_ID":"0x15b3","IFINDEX":"10","INTERFACE":"enp193s0f1np1","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp193s0f1np1","TAGS":":systemd:","USEC_INITIALIZED":"47820155"},"enp65s0f0":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:40/0000:40:03.1/0000:41:00.0/net/enp65s0f0","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"I350 Gigabit Network Connection (Ethernet Server Adapter I350-T2)","ID_MODEL_ID":"0x1521","ID_NET_DRIVER":"igb","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp65s0f0","ID_NET_NAME_MAC":"enxa0369f0ab382","ID_NET_NAME_PATH":"enp65s0f0","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Intel Corporate","ID_PATH":"pci-0000:41:00.0","ID_PATH_TAG":"pci-0000_41_00_0","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Intel Corporation","ID_VENDOR_ID":"0x8086","IFINDEX":"3","INTERFACE":"enp65s0f0","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp65s0f0","TAGS":":systemd:","USEC_INITIALIZED":"45176103"},"enp65s0f1":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:40/0000:40:03.1/0000:41:00.1/net/enp65s0f1","ID_BUS":"pci","ID_MODEL_FROM_DATABASE":"I350 Gigabit Network Connection (Ethernet Server Adapter I350-T2)","ID_MODEL_ID":"0x1521","ID_NET_DRIVER":"igb","ID_NET_LINK_FILE":"/usr/lib/systemd/network/99-default.link","ID_NET_NAME":"enp65s0f1","ID_NET_NAME_MAC":"enxa0369f0ab383","ID_NET_NAME_PATH":"enp65s0f1","ID_NET_NAMING_SCHEME":"v252","ID_OUI_FROM_DATABASE":"Intel Corporate","ID_PATH":"pci-0000:41:00.1","ID_PATH_TAG":"pci-0000_41_00_1","ID_PCI_CLASS_FROM_DATABASE":"Network controller","ID_PCI_SUBCLASS_FROM_DATABASE":"Ethernet controller","ID_VENDOR_FROM_DATABASE":"Intel Corporation","ID_VENDOR_ID":"0x8086","IFINDEX":"4","INTERFACE":"enp65s0f1","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enp65s0f1","TAGS":":systemd:","USEC_INITIALIZED":"45260218"},"enxaa0c304b6362":{"CURRENT_TAGS":":systemd:","DEVPATH":"/devices/pci0000:40/0000:40:08.1/0000:43:00.3/usb3/3-2/3-2.4/3-2.4.3/3-2.4.3:2.0/net/enxaa0c304b6362","ID_BUS":"usb","ID_MODEL":"Virtual_Ethernet","ID_MODEL_ENC":"Virtual\\x20Ethernet","ID_MODEL_ID":"ffb0","ID_NET_DRIVER":"cdc_ether","ID_NET_LINK_FILE":"/usr/lib/systemd/network/73-usb-net-by-mac.link","ID_NET_NAME":"enxaa0c304b6362","ID_NET_NAME_MAC":"enxaa0c304b6362","ID_NET_NAME_PATH":"enp67s0f3u2u4u3c2","ID_NET_NAMING_SCHEME":"v252","ID_PATH":"pci-0000:43:00.3-usb-0:2.4.3:2.0","ID_PATH_TAG":"pci-0000_43_00_3-usb-0_2_4_3_2_0","ID_REVISION":"0100","ID_SERIAL":"American_Megatrends_Inc._Virtual_Ethernet_1234567890","ID_SERIAL_SHORT":"1234567890","ID_TYPE":"generic","ID_USB_CLASS_FROM_DATABASE":"Communications","ID_USB_DRIVER":"cdc_ether","ID_USB_INTERFACES":":0202ff:0a0000:020600:","ID_USB_INTERFACE_NUM":"00","ID_USB_MODEL":"Virtual_Ethernet","ID_USB_MODEL_ENC":"Virtual\\x20Ethernet","ID_USB_MODEL_ID":"ffb0","ID_USB_REVISION":"0100","ID_USB_SERIAL":"American_Megatrends_Inc._Virtual_Ethernet_1234567890","ID_USB_SERIAL_SHORT":"1234567890","ID_USB_TYPE":"generic","ID_USB_VENDOR":"American_Megatrends_Inc.","ID_USB_VENDOR_ENC":"American\\x20Megatrends\\x20Inc.","ID_USB_VENDOR_ID":"046b","ID_VENDOR":"American_Megatrends_Inc.","ID_VENDOR_ENC":"American\\x20Megatrends\\x20Inc.","ID_VENDOR_FROM_DATABASE":"American Megatrends, Inc.","ID_VENDOR_ID":"046b","IFINDEX":"2","INTERFACE":"enxaa0c304b6362","SUBSYSTEM":"net","SYSTEMD_ALIAS":"/sys/subsystem/net/devices/enxaa0c304b6362","TAGS":":systemd:","USEC_INITIALIZED":"44748106"}}}
--- /dev/null
+[package]
+name = "proxmox-chroot"
+version = "0.1.0"
+edition = "2021"
+authors = [ "Aaron Lauterer <a.lauterer@proxmox.com>" ]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "4.0", features = ["derive"] }
+nix = "0.26.1"
+proxmox-installer-common = { path = "../proxmox-installer-common" }
+regex = "1.7"
+serde_json = "1.0"
--- /dev/null
+use std::{fs, io, path, process::Command};
+
+use anyhow::{bail, Result};
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use nix::mount::{mount, umount, MsFlags};
+use proxmox_installer_common::{
+ options::FsType,
+ setup::{InstallConfig, SetupInfo},
+};
+use regex::Regex;
+
+const ANSWER_MP: &str = "answer";
+static BINDMOUNTS: [&str; 4] = ["dev", "proc", "run", "sys"];
+const TARGET_DIR: &str = "/target";
+const ZPOOL_NAME: &str = "rpool";
+
+/// Helper tool to prepare eveything to `chroot` into an installation
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ Prepare(CommandPrepare),
+ Cleanup(CommandCleanup),
+}
+
+/// Mount the root file system and bind mounts in preparation to chroot into the installation
+#[derive(Args, Debug)]
+struct CommandPrepare {
+ /// Filesystem used for the installation. Will try to automatically detect it after a
+ /// successful installation.
+ #[arg(short, long, value_enum)]
+ filesystem: Option<Filesystems>,
+
+ /// Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
+ #[arg(long)]
+ rpool_id: Option<u64>,
+
+ /// UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
+ #[arg(long)]
+ btrfs_uuid: Option<String>,
+}
+
+/// Unmount everything. Use once done with chroot.
+#[derive(Args, Debug)]
+struct CommandCleanup {
+ /// Filesystem used for the installation. Will try to automatically detect it by default.
+ #[arg(short, long, value_enum)]
+ filesystem: Option<Filesystems>,
+}
+
+#[derive(Copy, Clone, Debug, ValueEnum)]
+enum Filesystems {
+ Zfs,
+ Ext4,
+ Xfs,
+ Btrfs,
+}
+
+impl From<FsType> for Filesystems {
+ fn from(fs: FsType) -> Self {
+ match fs {
+ FsType::Xfs => Self::Xfs,
+ FsType::Ext4 => Self::Ext4,
+ FsType::Zfs(_) => Self::Zfs,
+ FsType::Btrfs(_) => Self::Btrfs,
+ }
+ }
+}
+
+fn main() {
+ let args = Cli::parse();
+ let res = match &args.command {
+ Commands::Prepare(args) => prepare(args),
+ Commands::Cleanup(args) => cleanup(args),
+ };
+ if let Err(err) = res {
+ eprintln!("{err}");
+ std::process::exit(1);
+ }
+}
+
+fn prepare(args: &CommandPrepare) -> Result<()> {
+ let fs = get_fs(args.filesystem)?;
+
+ fs::create_dir_all(TARGET_DIR)?;
+
+ match fs {
+ Filesystems::Zfs => mount_zpool(args.rpool_id)?,
+ Filesystems::Xfs => mount_fs()?,
+ Filesystems::Ext4 => mount_fs()?,
+ Filesystems::Btrfs => mount_btrfs(args.btrfs_uuid.clone())?,
+ }
+
+ if let Err(e) = bindmount() {
+ eprintln!("{e}")
+ }
+
+ println!("Done. You can now use 'chroot /target /bin/bash'!");
+ Ok(())
+}
+
+fn cleanup(args: &CommandCleanup) -> Result<()> {
+ let fs = get_fs(args.filesystem)?;
+
+ if let Err(e) = bind_umount() {
+ eprintln!("{e}")
+ }
+
+ match fs {
+ Filesystems::Zfs => umount_zpool(),
+ Filesystems::Xfs => umount_fs()?,
+ Filesystems::Ext4 => umount_fs()?,
+ _ => (),
+ }
+
+ println!("Chroot cleanup done. You can now reboot or leave the shell.");
+ Ok(())
+}
+
+fn get_fs(filesystem: Option<Filesystems>) -> Result<Filesystems> {
+ let fs = match filesystem {
+ None => {
+ let low_level_config = match get_low_level_config() {
+ Ok(c) => c,
+ Err(_) => bail!("Could not fetch config from previous installation. Please specify file system with -f."),
+ };
+ Filesystems::from(low_level_config.filesys)
+ }
+ Some(fs) => fs,
+ };
+
+ Ok(fs)
+}
+
+fn get_low_level_config() -> Result<InstallConfig> {
+ let file = fs::File::open("/tmp/low-level-config.json")?;
+ let reader = io::BufReader::new(file);
+ let config: InstallConfig = serde_json::from_reader(reader)?;
+ Ok(config)
+}
+
+fn get_iso_info() -> Result<SetupInfo> {
+ let file = fs::File::open("/run/proxmox-installer/iso-info.json")?;
+ let reader = io::BufReader::new(file);
+ let setup_info: SetupInfo = serde_json::from_reader(reader)?;
+ Ok(setup_info)
+}
+
+fn mount_zpool(pool_id: Option<u64>) -> Result<()> {
+ println!("importing ZFS pool to {TARGET_DIR}");
+ let mut import = Command::new("zpool");
+ import.arg("import").args(["-R", TARGET_DIR]);
+ match pool_id {
+ None => {
+ import.arg(ZPOOL_NAME);
+ }
+ Some(id) => {
+ import.arg(id.to_string());
+ }
+ }
+ match import.status() {
+ Ok(s) if !s.success() => bail!("Could not import ZFS pool. Abort!"),
+ _ => (),
+ }
+ println!("successfully imported ZFS pool to {TARGET_DIR}");
+ Ok(())
+}
+
+fn umount_zpool() {
+ match Command::new("zpool").arg("export").arg(ZPOOL_NAME).status() {
+ Ok(s) if !s.success() => println!("failure on exporting {ZPOOL_NAME}"),
+ _ => (),
+ }
+}
+
+fn mount_fs() -> Result<()> {
+ let iso_info = get_iso_info()?;
+ let product = iso_info.config.product;
+
+ println!("Activating VG '{product}'");
+ let res = Command::new("vgchange")
+ .arg("-ay")
+ .arg(product.to_string())
+ .output();
+ match res {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!(
+ "successfully activated VG '{product}': {}",
+ String::from_utf8(output.stdout)?
+ );
+ } else {
+ bail!(
+ "activation of VG '{product}' failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ match Command::new("mount")
+ .arg(format!("/dev/mapper/{product}-root"))
+ .arg("/target")
+ .output()
+ {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!("mounted root file system successfully");
+ } else {
+ bail!(
+ "mounting of root file system failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn umount_fs() -> Result<()> {
+ umount(TARGET_DIR)?;
+ Ok(())
+}
+
+fn mount_btrfs(btrfs_uuid: Option<String>) -> Result<()> {
+ let uuid = match btrfs_uuid {
+ Some(uuid) => uuid,
+ None => get_btrfs_uuid()?,
+ };
+
+ match Command::new("mount")
+ .arg("--uuid")
+ .arg(uuid)
+ .arg("/target")
+ .output()
+ {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!("mounted BTRFS root file system successfully");
+ } else {
+ bail!(
+ "mounting of BTRFS root file system failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn get_btrfs_uuid() -> Result<String> {
+ let output = Command::new("btrfs")
+ .arg("filesystem")
+ .arg("show")
+ .output()?;
+ if !output.status.success() {
+ bail!(
+ "Error checking for BTRFS file systems: {}",
+ String::from_utf8(output.stderr)?
+ );
+ }
+ let out = String::from_utf8(output.stdout)?;
+ let mut uuids = Vec::new();
+
+ let re_uuid =
+ Regex::new(r"uuid: ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$")?;
+ for line in out.lines() {
+ if let Some(cap) = re_uuid.captures(line) {
+ if let Some(uuid) = cap.get(1) {
+ uuids.push(uuid.as_str());
+ }
+ }
+ }
+ match uuids.len() {
+ 0 => bail!("Could not find any BTRFS UUID"),
+ i if i > 1 => {
+ let uuid_list = uuids
+ .iter()
+ .fold(String::new(), |acc, &arg| format!("{acc}\n{arg}"));
+ bail!("Found {i} UUIDs:{uuid_list}\nPlease specify the UUID to use with the --btrfs-uuid parameter")
+ }
+ _ => (),
+ }
+ Ok(uuids[0].into())
+}
+
+fn bindmount() -> Result<()> {
+ println!("Bind mounting");
+ // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L19
+ // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L146
+ const NONE: Option<&'static [u8]> = None;
+
+ let flags = MsFlags::MS_BIND;
+ for item in BINDMOUNTS {
+ let source = path::Path::new("/").join(item);
+ let target = path::Path::new(TARGET_DIR).join(item);
+
+ println!("Bindmount {source:?} to {target:?}");
+ mount(Some(source.as_path()), target.as_path(), NONE, flags, NONE)?;
+ }
+
+ let answer_path = path::Path::new("/mnt").join(ANSWER_MP);
+ if answer_path.exists() {
+ let target = path::Path::new(TARGET_DIR).join("mnt").join(ANSWER_MP);
+
+ println!("Create dir {target:?}");
+ fs::create_dir_all(&target)?;
+
+ println!("Bindmount {answer_path:?} to {target:?}");
+ mount(
+ Some(answer_path.as_path()),
+ target.as_path(),
+ NONE,
+ flags,
+ NONE,
+ )?;
+ }
+ Ok(())
+}
+
+fn bind_umount() -> Result<()> {
+ for item in BINDMOUNTS {
+ let target = path::Path::new(TARGET_DIR).join(item);
+ println!("Unmounting {target:?}");
+ if let Err(e) = umount(target.as_path()) {
+ eprintln!("{e}");
+ }
+ }
+
+ let answer_target = path::Path::new(TARGET_DIR).join("mnt").join(ANSWER_MP);
+ if answer_target.exists() {
+ println!("Unmounting and removing answer mountpoint");
+ if let Err(e) = umount(answer_target.as_os_str()) {
+ eprintln!("{e}");
+ }
+ if let Err(e) = fs::remove_dir(answer_target) {
+ eprintln!("{e}");
+ }
+ }
+
+ Ok(())
+}
--- /dev/null
+[package]
+name = "proxmox-fetch-answer"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Aaron Lauterer <a.lauterer@proxmox.com>",
+ "Proxmox Support Team <support@proxmox.com>",
+]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0"
+hex = "0.4"
+log = "0.4.20"
+native-tls = "0.2"
+proxmox-auto-installer = { path = "../proxmox-auto-installer" }
+rustls = { version = "0.20", features = [ "dangerous_configuration" ] }
+rustls-native-certs = "0.6"
+sha2 = "0.10"
+toml = "0.7"
+ureq = { version = "2.6", features = [ "native-certs", "native-tls" ] }
--- /dev/null
+use anyhow::{bail, Result};
+use log::info;
+use std::{
+ fs::{self, read_to_string},
+ process::Command,
+};
+
+use proxmox_auto_installer::{sysinfo::SysInfo, utils::HttpOptions};
+
+static ANSWER_URL_SUBDOMAIN: &str = "proxmox-auto-installer";
+static ANSWER_CERT_FP_SUBDOMAIN: &str = "proxmox-auto-installer-cert-fingerprint";
+
+// It is possible to set custom DHPC options. Option numbers 224 to 254 [0].
+// To use them with dhclient, we need to configure it to request them and what they should be
+// called.
+//
+// e.g. /etc/dhcp/dhclient.conf:
+// ```
+// option proxmox-auto-installer-manifest-url code 250 = text;
+// option proxmox-auto-installer-cert-fingerprint code 251 = text;
+// also request proxmox-auto-installer-manifest-url, proxmox-auto-installer-cert-fingerprint;
+// ```
+//
+// The results will end up in the /var/lib/dhcp/dhclient.leases file from where we can fetch them
+//
+// [0] https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
+static DHCP_URL_OPTION: &str = "proxmox-auto-installer-manifest-url";
+static DHCP_CERT_FP_OPTION: &str = "proxmox-auto-installer-cert-fingerprint";
+static DHCP_LEASE_FILE: &str = "/var/lib/dhcp/dhclient.leases";
+
+pub struct FetchFromHTTP;
+
+impl FetchFromHTTP {
+ /// Will try to fetch the answer.toml by sending a HTTP POST request. The URL can be configured
+ /// either via DHCP or DNS or preconfigured in the ISO.
+ /// If the URL is not defined in the ISO, it will first check DHCP options. The SSL certificate
+ /// needs to be either trusted by the root certs or a SHA256 fingerprint needs to be provided.
+ /// The SHA256 SSL fingerprint can either be defined in the ISO, as DHCP option, or as DNS TXT
+ /// record. If provided, the fingerprint provided in the ISO has preference.
+ pub fn get_answer(settings: &HttpOptions) -> Result<String> {
+ let mut fingerprint: Option<String> = match settings.cert_fingerprint.clone() {
+ Some(fp) => {
+ info!("SSL fingerprint provided through ISO.");
+ Some(fp)
+ }
+ None => None,
+ };
+
+ let answer_url: String;
+ if let Some(url) = settings.url.clone() {
+ info!("URL specified in ISO");
+ answer_url = url;
+ } else {
+ (answer_url, fingerprint) = match Self::fetch_dhcp(fingerprint.clone()) {
+ Ok((url, fp)) => (url, fp),
+ Err(err) => {
+ info!("{err}");
+ Self::fetch_dns(fingerprint.clone())?
+ }
+ };
+ }
+
+ if let Some(fingerprint) = &fingerprint {
+ let _ = fs::write("/tmp/cert_fingerprint", fingerprint);
+ }
+
+ info!("Gathering system information.");
+ let payload = SysInfo::as_json()?;
+ info!("Sending POST request to '{answer_url}'.");
+ let answer = http_post::call(answer_url, fingerprint.as_deref(), payload)?;
+ Ok(answer)
+ }
+
+ /// Fetches search domain from resolv.conf file
+ fn get_search_domain() -> Result<String> {
+ info!("Retrieving default search domain.");
+ for line in read_to_string("/etc/resolv.conf")?.lines() {
+ if let Some((key, value)) = line.split_once(' ') {
+ if key == "search" {
+ return Ok(value.trim().into());
+ }
+ }
+ }
+ bail!("Could not find search domain in resolv.conf.");
+ }
+
+ /// Runs a TXT DNS query on the domain provided
+ fn query_txt_record(query: String) -> Result<String> {
+ info!("Querying TXT record for '{query}'");
+ let url: String;
+ match Command::new("dig")
+ .args(["txt", "+short"])
+ .arg(&query)
+ .output()
+ {
+ Ok(output) => {
+ if output.status.success() {
+ url = String::from_utf8(output.stdout)?
+ .replace('"', "")
+ .trim()
+ .into();
+ if url.is_empty() {
+ bail!("Got empty response.");
+ }
+ } else {
+ bail!(
+ "Error querying DNS record '{query}' : {}",
+ String::from_utf8(output.stderr)?
+ );
+ }
+ }
+ Err(err) => bail!("Error querying DNS record '{query}': {err}"),
+ }
+ info!("Found: '{url}'");
+ Ok(url)
+ }
+
+ /// Tries to fetch answer URL and SSL fingerprint info from DNS
+ fn fetch_dns(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
+ let search_domain = Self::get_search_domain()?;
+
+ let answer_url =
+ match Self::query_txt_record(format!("{ANSWER_URL_SUBDOMAIN}.{search_domain}")) {
+ Ok(url) => url,
+ Err(err) => bail!("{err}"),
+ };
+
+ if fingerprint.is_none() {
+ fingerprint =
+ match Self::query_txt_record(format!("{ANSWER_CERT_FP_SUBDOMAIN}.{search_domain}"))
+ {
+ Ok(fp) => Some(fp),
+ Err(err) => {
+ info!("{err}");
+ None
+ }
+ };
+ }
+ Ok((answer_url, fingerprint))
+ }
+
+ /// Tries to fetch answer URL and SSL fingerprint info from DHCP options
+ fn fetch_dhcp(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
+ info!("Checking DHCP options.");
+ let leases = fs::read_to_string(DHCP_LEASE_FILE)?;
+
+ let mut answer_url: Option<String> = None;
+
+ let url_match = format!("option {DHCP_URL_OPTION}");
+ let fp_match = format!("option {DHCP_CERT_FP_OPTION}");
+
+ for line in leases.lines() {
+ if answer_url.is_none() && line.trim().starts_with(url_match.as_str()) {
+ answer_url = Self::strip_dhcp_option(line.split(' ').nth_back(0));
+ }
+ if fingerprint.is_none() && line.trim().starts_with(fp_match.as_str()) {
+ fingerprint = Self::strip_dhcp_option(line.split(' ').nth_back(0));
+ }
+ }
+
+ let answer_url = match answer_url {
+ None => bail!("No DHCP option found for fetch URL."),
+ Some(url) => {
+ info!("Found URL for answer in DHCP option: '{url}'");
+ url
+ }
+ };
+
+ if let Some(fp) = fingerprint.clone() {
+ info!("Found SSL Fingerprint via DHCP: '{fp}'");
+ }
+
+ Ok((answer_url, fingerprint))
+ }
+
+ /// Clean DHCP option string
+ fn strip_dhcp_option(value: Option<&str>) -> Option<String> {
+ // value is expected to be in format: "value";
+ value.map(|value| String::from(&value[1..value.len() - 2]))
+ }
+}
+
+mod http_post {
+ use anyhow::Result;
+ use rustls::ClientConfig;
+ use sha2::{Digest, Sha256};
+ use std::sync::Arc;
+ use ureq::{Agent, AgentBuilder};
+
+ /// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to
+ /// check the cert against it, instead of the regular cert validation.
+ /// To gather the sha256 fingerprint you can use the following command:
+ /// ```no_compile
+ /// openssl s_client -connect <host>:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout -in /dev/stdin
+ /// ```
+ ///
+ /// # Arguemnts
+ /// * `url` - URL to call
+ /// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
+ /// * `payload` - The payload to send to the server. Expected to be a JSON formatted string.
+ pub fn call(url: String, fingerprint: Option<&str>, payload: String) -> Result<String> {
+ let answer;
+
+ if let Some(fingerprint) = fingerprint {
+ let tls_config = ClientConfig::builder()
+ .with_safe_defaults()
+ .with_custom_certificate_verifier(VerifyCertFingerprint::new(fingerprint)?)
+ .with_no_client_auth();
+
+ let agent: Agent = AgentBuilder::new().tls_config(Arc::new(tls_config)).build();
+
+ answer = agent
+ .post(&url)
+ .set("Content-type", "application/json; charset=utf-")
+ .send_string(&payload)?
+ .into_string()?;
+ } else {
+ let mut roots = rustls::RootCertStore::empty();
+ for cert in rustls_native_certs::load_native_certs()? {
+ roots.add(&rustls::Certificate(cert.0)).unwrap();
+ }
+
+ let tls_config = rustls::ClientConfig::builder()
+ .with_safe_defaults()
+ .with_root_certificates(roots)
+ .with_no_client_auth();
+
+ let agent = AgentBuilder::new()
+ .tls_connector(Arc::new(native_tls::TlsConnector::new()?))
+ .tls_config(Arc::new(tls_config))
+ .build();
+ answer = agent
+ .post(&url)
+ .set("Content-type", "application/json; charset=utf-")
+ .timeout(std::time::Duration::from_secs(60))
+ .send_string(&payload)?
+ .into_string()?;
+ }
+ Ok(answer)
+ }
+
+ struct VerifyCertFingerprint {
+ cert_fingerprint: Vec<u8>,
+ }
+
+ impl VerifyCertFingerprint {
+ fn new<S: AsRef<str>>(cert_fingerprint: S) -> Result<std::sync::Arc<Self>> {
+ let cert_fingerprint = cert_fingerprint.as_ref();
+ let sanitized = cert_fingerprint.replace(':', "");
+ let decoded = hex::decode(sanitized)?;
+ Ok(std::sync::Arc::new(Self {
+ cert_fingerprint: decoded,
+ }))
+ }
+ }
+
+ impl rustls::client::ServerCertVerifier for VerifyCertFingerprint {
+ fn verify_server_cert(
+ &self,
+ end_entity: &rustls::Certificate,
+ _intermediates: &[rustls::Certificate],
+ _server_name: &rustls::ServerName,
+ _scts: &mut dyn Iterator<Item = &[u8]>,
+ _ocsp_response: &[u8],
+ _now: std::time::SystemTime,
+ ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
+ let mut hasher = Sha256::new();
+ hasher.update(end_entity);
+ let result = hasher.finalize();
+
+ if result.as_slice() == self.cert_fingerprint {
+ Ok(rustls::client::ServerCertVerified::assertion())
+ } else {
+ Err(rustls::Error::General("Fingerprint did not match!".into()))
+ }
+ }
+ }
+}
--- /dev/null
+pub(crate) mod http;
+pub(crate) mod partition;
--- /dev/null
+use anyhow::{bail, format_err, Result};
+use log::{info, warn};
+use std::{
+ fs::{self, create_dir_all},
+ path::{Path, PathBuf},
+ process::Command,
+};
+
+static ANSWER_FILE: &str = "answer.toml";
+static ANSWER_MP: &str = "/mnt/answer";
+// FAT can only handle 11 characters, so shorten Automated Installer Source to AIS
+static PARTLABEL: &str = "proxmox-ais";
+static DISK_BY_ID_PATH: &str = "/dev/disk/by-label";
+
+pub struct FetchFromPartition;
+
+impl FetchFromPartition {
+ /// Returns the contents of the answer file
+ pub fn get_answer() -> Result<String> {
+ info!("Checking for answer file on partition.");
+
+ let mut mount_path = PathBuf::from(mount_proxmoxinst_part()?);
+ mount_path.push(ANSWER_FILE);
+ let answer = fs::read_to_string(mount_path)
+ .map_err(|err| format_err!("failed to read answer file - {err}"))?;
+
+ info!("Found answer file on partition.");
+
+ Ok(answer)
+ }
+}
+
+fn path_exists_logged(file_name: &str, search_path: &str) -> Option<PathBuf> {
+ let path = Path::new(search_path).join(&file_name);
+ info!("Testing partition search path {path:?}");
+ match path.try_exists() {
+ Ok(true) => Some(path),
+ Ok(false) => None,
+ Err(err) => {
+ info!("Encountered issue, accessing '{path:?}': {err}");
+ None
+ }
+ }
+}
+
+/// Searches for upper and lower case existence of the partlabel in the search_path
+///
+/// # Arguemnts
+/// * `partlabel_source` - Partition Label, used as upper and lower case
+/// * `search_path` - Path where to search for the partiiton label
+fn scan_partlabels(partlabel: &str, search_path: &str) -> Result<PathBuf> {
+ let partlabel_upper_case = partlabel.to_uppercase();
+ if let Some(path) = path_exists_logged(&partlabel_upper_case, search_path) {
+ info!("Found partition with label '{partlabel_upper_case}'");
+ return Ok(path);
+ }
+
+ let partlabel_lower_case = partlabel.to_lowercase();
+ if let Some(path) = path_exists_logged(&partlabel_lower_case, search_path) {
+ info!("Found partition with label '{partlabel_lower_case}'");
+ return Ok(path);
+ }
+
+ bail!("Could not detect upper or lower case labels for '{partlabel}'");
+}
+
+/// Will search and mount a partition/FS labeled PARTLABEL (proxmox-ais) in lower or uppercase
+/// to ANSWER_MP
+fn mount_proxmoxinst_part() -> Result<String> {
+ if let Ok(true) = check_if_mounted(ANSWER_MP) {
+ info!("Skipping: '{ANSWER_MP}' is already mounted.");
+ return Ok(ANSWER_MP.into());
+ }
+ let part_path = scan_partlabels(PARTLABEL, DISK_BY_ID_PATH)?;
+ info!("Mounting partition at {ANSWER_MP}");
+ // create dir for mountpoint
+ create_dir_all(ANSWER_MP)?;
+ match Command::new("mount")
+ .args(["-o", "ro"])
+ .arg(part_path)
+ .arg(ANSWER_MP)
+ .output()
+ {
+ Ok(output) => {
+ if output.status.success() {
+ Ok(ANSWER_MP.into())
+ } else {
+ warn!("Error mounting: {}", String::from_utf8(output.stderr)?);
+ Ok(ANSWER_MP.into())
+ }
+ }
+ Err(err) => bail!("Error mounting: {err}"),
+ }
+}
+
+fn check_if_mounted(target_path: &str) -> Result<bool> {
+ let mounts = fs::read_to_string("/proc/mounts")?;
+ for line in mounts.lines() {
+ if let Some(mp) = line.split(' ').nth(1) {
+ if mp == target_path {
+ return Ok(true);
+ }
+ }
+ }
+ Ok(false)
+}
--- /dev/null
+use std::process::ExitCode;
+use std::{fs, path::PathBuf};
+
+use anyhow::{bail, format_err, Result};
+use log::{error, info, LevelFilter};
+
+use proxmox_auto_installer::{
+ log::AutoInstLogger,
+ utils::{AutoInstSettings, FetchAnswerFrom, HttpOptions},
+};
+
+use fetch_plugins::{http::FetchFromHTTP, partition::FetchFromPartition};
+
+mod fetch_plugins;
+
+static LOGGER: AutoInstLogger = AutoInstLogger;
+static AUTOINST_MODE_FILE: &str = "/cdrom/auto-installer-mode.toml";
+
+pub fn init_log() -> Result<()> {
+ AutoInstLogger::init("/tmp/fetch_answer.log")?;
+ log::set_logger(&LOGGER)
+ .map(|()| log::set_max_level(LevelFilter::Info))
+ .map_err(|err| format_err!(err))
+}
+
+fn fetch_answer(install_settings: &AutoInstSettings) -> Result<String> {
+ info!("Fetching answer file in mode {:?}:", &install_settings.mode);
+ match install_settings.mode {
+ FetchAnswerFrom::Iso => {
+ let answer_path = PathBuf::from("/cdrom/answer.toml");
+ match fs::read_to_string(answer_path) {
+ Ok(answer) => return Ok(answer),
+ Err(err) => info!("Fetching answer file from ISO failed: {err}"),
+ }
+ }
+ FetchAnswerFrom::Partition => match FetchFromPartition::get_answer() {
+ Ok(answer) => return Ok(answer),
+ Err(err) => info!("Fetching answer file from partition failed: {err}"),
+ },
+ FetchAnswerFrom::Http => match FetchFromHTTP::get_answer(&install_settings.http) {
+ Ok(answer) => return Ok(answer),
+ Err(err) => info!("Fetching answer file via HTTP failed: {err}"),
+ },
+ }
+ bail!("Could not find any answer file!");
+}
+
+fn settings_from_cli_args(args: &[String]) -> Result<AutoInstSettings> {
+ // TODO: this was done in a bit of a hurry, needs tidying up
+ let mode = match args[1].to_lowercase().as_str() {
+ "iso" => FetchAnswerFrom::Iso,
+ "http" => FetchAnswerFrom::Http,
+ "partition" => FetchAnswerFrom::Partition,
+ "-h" | "--help" => bail!(
+ "usage: {} <http|iso|partition> [<http-url>] [<tls-cert-fingerprint>]",
+ args[0]
+ ),
+ _ => bail!("failed to parse fetch-from argument, not one of 'http', 'iso', or 'partition'"),
+ };
+ if args.len() > 4 {
+ } else if args.len() > 2 && mode != FetchAnswerFrom::Http {
+ bail!("only 'http' fetch-from mode supports additional url and cert-fingerprint mode");
+ }
+ Ok(AutoInstSettings {
+ mode,
+ http: HttpOptions {
+ url: args.get(2).cloned(),
+ cert_fingerprint: args.get(3).cloned(),
+ },
+ })
+}
+
+fn do_main() -> Result<()> {
+ if let Err(err) = init_log() {
+ bail!("could not initialize logging: {err}");
+ }
+
+ let args: Vec<String> = std::env::args().collect();
+
+ let install_settings: AutoInstSettings = if args.len() > 1 {
+ settings_from_cli_args(&args)?
+ } else {
+ let raw_install_settings = fs::read_to_string(AUTOINST_MODE_FILE).map_err(|err| {
+ format_err!(
+ "Could not find needed file '{AUTOINST_MODE_FILE}' in live environment: {err}"
+ )
+ })?;
+ toml::from_str(raw_install_settings.as_str())
+ .map_err(|err| format_err!("Failed to parse '{AUTOINST_MODE_FILE}': {err}"))?
+ };
+
+ let answer = fetch_answer(&install_settings).map_err(|err| format_err!("Aborting: {err}"))?;
+ info!("queried answer file for automatic installation successfully");
+
+ println!("{answer}");
+
+ Ok(())
+}
+
+fn main() -> ExitCode {
+ match do_main() {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(err) => {
+ error!("{err}");
+ ExitCode::FAILURE
+ }
+ }
+}
homepage = "https://www.proxmox.com"
[dependencies]
+regex = "1.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
/// * `runinfo` - `RuntimeInfo` instance of currently running system
/// * `disks` - List of disks designated as bootdisk targets.
pub 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) {
+ if boot_type == BootType::Bios && disks.iter().any(|disk| disk.block_size == Some(4096)) {
return Err("Booting from 4Kn drive in legacy BIOS mode is not supported.");
}
+use serde::Deserialize;
use std::net::{IpAddr, Ipv4Addr};
use std::{cmp, fmt};
};
use crate::utils::{CidrAddress, Fqdn};
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
pub enum BtrfsRaidLevel {
Raid0,
Raid1,
}
}
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
pub enum ZfsRaidLevel {
Raid0,
Raid1,
Raid10,
+ #[serde(rename = "raidz-1")]
RaidZ,
+ #[serde(rename = "raidz-2")]
RaidZ2,
+ #[serde(rename = "raidz-3")]
RaidZ3,
}
}
}
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
pub enum FsType {
Ext4,
Xfs,
}
}
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
+#[serde(rename_all(deserialize = "lowercase"))]
pub enum ZfsCompressOption {
#[default]
On,
&[On, Off, Lzjb, Lz4, Zle, Gzip, Zstd]
};
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "kebab-case")]
pub enum ZfsChecksumOption {
#[default]
On,
Btrfs(BtrfsBootdiskOptions),
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Disk {
pub index: String,
pub path: String,
impl cmp::PartialOrd for Disk {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
- self.index.partial_cmp(&other.index)
+ Some(self.cmp(other))
}
}
let timezone = locales
.cczones
.get(&country)
- .and_then(|zones| zones.get(0))
+ .and_then(|zones| zones.first())
.cloned()
.unwrap_or_else(|| "UTC".to_owned());
}
impl NetworkOptions {
- const DEFAULT_DOMAIN: &str = "example.invalid";
+ const DEFAULT_DOMAIN: &'static str = "example.invalid";
pub fn defaults_from(setup: &SetupInfo, network: &NetworkInfo) -> Self {
let mut this = Self {
let mut filled = false;
if let Some(gw) = &routes.gateway4 {
if let Some(iface) = network.interfaces.get(&gw.dev) {
- this.ifname = iface.name.clone();
+ this.ifname.clone_from(&iface.name);
if let Some(addresses) = &iface.addresses {
if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv4()) {
this.gateway = gw.gateway;
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.ifname.clone_from(&iface.name);
this.gateway = gw.gateway;
this.address = addr.clone();
}
use std::{
cmp,
- collections::HashMap,
+ collections::{BTreeMap, HashMap},
fmt,
fs::File,
io::{self, BufReader},
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::{
- options::{Disk, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption},
+ options::{
+ BtrfsRaidLevel, Disk, FsType, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption,
+ ZfsRaidLevel,
+ },
utils::CidrAddress,
};
#[allow(clippy::upper_case_acronyms)]
-#[derive(Clone, Copy, Deserialize, PartialEq)]
+#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProxmoxProduct {
PVE,
}
}
-#[derive(Clone, Deserialize)]
+impl fmt::Display for ProxmoxProduct {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ Self::PVE => "pve",
+ Self::PMG => "pmg",
+ Self::PBS => "pbs",
+ })
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProductConfig {
pub fullname: String,
pub product: ProxmoxProduct,
pub enable_btrfs: bool,
}
-#[derive(Clone, Deserialize)]
+impl ProductConfig {
+ /// A mocked ProductConfig simulating a Proxmox VE environment.
+ pub fn mocked() -> Self {
+ Self {
+ fullname: String::from("Proxmox VE (mocked)"),
+ product: ProxmoxProduct::PVE,
+ enable_btrfs: true,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IsoInfo {
pub release: String,
pub isorelease: String,
}
+impl IsoInfo {
+ /// A mocked IsoInfo with some edge case to convey that this is not necessarily purely numeric.
+ pub fn mocked() -> Self {
+ Self {
+ release: String::from("42.1"),
+ isorelease: String::from("mocked-1"),
+ }
+ }
+}
+
/// Paths in the ISO environment containing installer data.
#[derive(Clone, Deserialize)]
pub struct IsoLocations {
pub iso: PathBuf,
}
+impl IsoLocations {
+ /// A mocked location, uses the current working directory by default
+ pub fn mocked() -> Self {
+ Self {
+ iso: std::env::current_dir().unwrap_or("/dev/null".into()),
+ }
+ }
+}
+
#[derive(Clone, Deserialize)]
pub struct SetupInfo {
#[serde(rename = "product-cfg")]
pub locations: IsoLocations,
}
+impl SetupInfo {
+ /// Return a mocked SetupInfo that is very similar to how our actual ones look like and should
+ /// be good enough for testing.
+ pub fn mocked() -> Self {
+ Self {
+ config: ProductConfig::mocked(),
+ iso_info: IsoInfo::mocked(),
+ locations: IsoLocations::mocked(),
+ }
+ }
+}
+
#[derive(Clone, Deserialize)]
pub struct CountryInfo {
pub name: String,
impl cmp::PartialOrd for KeyboardMapping {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
- self.name.partial_cmp(&other.name)
+ Some(self.cmp(other))
}
}
}
}
-#[derive(Serialize)]
+#[derive(Debug, Deserialize, Serialize)]
pub struct InstallZfsOption {
- ashift: usize,
+ pub ashift: usize,
#[serde(serialize_with = "serialize_as_display")]
- compress: ZfsCompressOption,
+ pub compress: ZfsCompressOption,
#[serde(serialize_with = "serialize_as_display")]
- checksum: ZfsChecksumOption,
- copies: usize,
- arc_max: usize,
+ pub checksum: ZfsChecksumOption,
+ pub copies: usize,
+ pub arc_max: usize,
}
impl From<ZfsBootdiskOptions> for InstallZfsOption {
/// 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>,
+ pub interfaces: BTreeMap<String, Interface>,
/// The hostname of this machine, if set by the DHCP server.
pub hostname: Option<String>,
.stdout(Stdio::piped())
.spawn()
}
+
+/// See Proxmox::Install::Config
+#[derive(Debug, Deserialize, Serialize)]
+pub struct InstallConfig {
+ pub autoreboot: usize,
+
+ #[serde(
+ serialize_with = "serialize_fstype",
+ deserialize_with = "deserialize_fs_type"
+ )]
+ pub filesys: FsType,
+ pub hdsize: f64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub swapsize: Option<f64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub maxroot: Option<f64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub minfree: Option<f64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub maxvz: Option<f64>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub zfs_opts: Option<InstallZfsOption>,
+
+ #[serde(
+ serialize_with = "serialize_disk_opt",
+ skip_serializing_if = "Option::is_none",
+ // only the 'path' property is serialized -> deserialization is problematic
+ // The information would be present in the 'run-env-info-json', but for now there is no
+ // need for it in any code that deserializes the low-level config. Therefore we are
+ // currently skipping it on deserialization
+ skip_deserializing
+ )]
+ pub target_hd: Option<Disk>,
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+ pub disk_selection: BTreeMap<String, String>,
+
+ pub lvm_auto_rename: usize,
+
+ pub country: String,
+ pub timezone: String,
+ pub keymap: String,
+
+ pub password: String,
+ pub mailto: String,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub root_ssh_keys: Vec<String>,
+
+ pub mngmt_nic: String,
+
+ pub hostname: String,
+ pub domain: String,
+ #[serde(serialize_with = "serialize_as_display")]
+ pub cidr: CidrAddress,
+ pub gateway: IpAddr,
+ pub dns: IpAddr,
+}
+
+fn serialize_disk_opt<S>(value: &Option<Disk>, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ if let Some(disk) = value {
+ serializer.serialize_str(&disk.path)
+ } else {
+ serializer.serialize_none()
+ }
+}
+
+fn serialize_fstype<S>(value: &FsType, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ use FsType::*;
+ let value = match value {
+ // proxinstall::$fssetup
+ Ext4 => "ext4",
+ Xfs => "xfs",
+ // proxinstall::get_zfs_raid_setup()
+ Zfs(ZfsRaidLevel::Raid0) => "zfs (RAID0)",
+ Zfs(ZfsRaidLevel::Raid1) => "zfs (RAID1)",
+ Zfs(ZfsRaidLevel::Raid10) => "zfs (RAID10)",
+ Zfs(ZfsRaidLevel::RaidZ) => "zfs (RAIDZ-1)",
+ Zfs(ZfsRaidLevel::RaidZ2) => "zfs (RAIDZ-2)",
+ Zfs(ZfsRaidLevel::RaidZ3) => "zfs (RAIDZ-3)",
+ // proxinstall::get_btrfs_raid_setup()
+ Btrfs(BtrfsRaidLevel::Raid0) => "btrfs (RAID0)",
+ Btrfs(BtrfsRaidLevel::Raid1) => "btrfs (RAID1)",
+ Btrfs(BtrfsRaidLevel::Raid10) => "btrfs (RAID10)",
+ };
+
+ serializer.collect_str(value)
+}
+
+pub fn deserialize_fs_type<'de, D>(deserializer: D) -> Result<FsType, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ use FsType::*;
+ let de_fs: String = Deserialize::deserialize(deserializer)?;
+
+ match de_fs.as_str() {
+ "ext4" => Ok(Ext4),
+ "xfs" => Ok(Xfs),
+ "zfs (RAID0)" => Ok(Zfs(ZfsRaidLevel::Raid0)),
+ "zfs (RAID1)" => Ok(Zfs(ZfsRaidLevel::Raid1)),
+ "zfs (RAID10)" => Ok(Zfs(ZfsRaidLevel::Raid10)),
+ "zfs (RAIDZ-1)" => Ok(Zfs(ZfsRaidLevel::RaidZ)),
+ "zfs (RAIDZ-2)" => Ok(Zfs(ZfsRaidLevel::RaidZ2)),
+ "zfs (RAIDZ-3)" => Ok(Zfs(ZfsRaidLevel::RaidZ3)),
+ "btrfs (RAID0)" => Ok(Btrfs(BtrfsRaidLevel::Raid0)),
+ "btrfs (RAID1)" => Ok(Btrfs(BtrfsRaidLevel::Raid1)),
+ "btrfs (RAID10)" => Ok(Btrfs(BtrfsRaidLevel::Raid10)),
+ _ => Err(de::Error::custom("could not find file system: {de_fs}")),
+ }
+}
}
}
+impl<'de> Deserialize<'de> for CidrAddress {
+ 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 CIDR"))
+ }
+}
+
fn mask_limit(addr: &IpAddr) -> usize {
if addr.is_ipv4() {
32
self.parts.len() > 1
}
- fn validate_single(s: &String) -> bool {
+ fn validate_single(s: &str) -> bool {
!s.is_empty()
&& s.len() <= Self::MAX_LABEL_LENGTH
// First character must be alphanumeric
assert_eq!(Fqdn::from("example.com"), Fqdn::from("example.com"));
assert_eq!(Fqdn::from("example.com"), Fqdn::from("ExAmPle.Com"));
assert_eq!(Fqdn::from("ExAmPle.Com"), Fqdn::from("example.com"));
- assert_ne!(Fqdn::from("subdomain.ExAmPle.Com"), Fqdn::from("example.com"));
+ assert_ne!(
+ Fqdn::from("subdomain.ExAmPle.Com"),
+ Fqdn::from("example.com")
+ );
assert_ne!(Fqdn::from("foo.com"), Fqdn::from("bar.com"));
assert_ne!(Fqdn::from("example.com"), Fqdn::from("example.net"));
}
use Proxmox::Install::ISOEnv;
use Proxmox::Install::RunEnv;
+use Proxmox::Sys::Udev;
use Proxmox::Sys::File qw(file_write_all);
my $commands = {
'dump-env' => 'Dump the current ISO and Hardware environment to base the installer UI on.',
+ 'dump-udev' => 'Dump disk and network device info. Used for the auto installation.',
'start-session' => 'Start an installation session, with command and result transmitted via stdin/out',
'start-session-test' => 'Start an installation TEST session, with command and result transmitted via stdin/out',
'help' => 'Output this usage help.',
Proxmox::Install::Config::merge($config);
log_info("got installation config: ". to_json(Proxmox::Install::Config::get(), { utf8 => 1, canonical => 1 }) ."\n");
+ file_write_all("/tmp/low-level-config.json", to_json(Proxmox::Install::Config::get()));
}
sub send_reboot_ui_message {
my $run_env = Proxmox::Install::RunEnv::query_installation_environment();
my $run_env_serialized = to_json($run_env, {canonical => 1, utf8 => 1}) ."\n";
file_write_all($run_env_file, $run_env_serialized);
+} elsif ($cmd eq 'dump-udev') {
+ my $out_dir = $env->{locations}->{run};
+ make_path($out_dir);
+ die "failed to create output directory '$out_dir'\n" if !-d $out_dir;
+
+ my $output = {
+ disks => Proxmox::Sys::Block::udevadm_disk_details(),
+ nics => Proxmox::Sys::Net::udevadm_netdev_details(),
+ };
+
+ my $output_serialized = to_json($output, {canonical => 1, utf8 => 1}) ."\n";
+ file_write_all("$out_dir/run-env-udev.json", $output_serialized);
} elsif ($cmd eq 'start-session') {
Proxmox::UI::init_stdio({}, $env);
read_and_merge_config();
}
fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
- // Ensure the screen is updated independently of keyboard events and such
- siv.set_autorefresh(true);
-
let state = siv.user_data::<InstallerState>().cloned().unwrap();
InstallerView::with_raw(&state, InstallProgressView::new(siv))
}
utils::{CidrAddress, Fqdn},
};
use std::net::{IpAddr, Ipv4Addr};
- use std::{collections::HashMap, path::PathBuf};
+ use std::{collections::BTreeMap, path::PathBuf};
fn dummy_setup_info() -> SetupInfo {
SetupInfo {
fn network_options_from_setup_network_info() {
let setup = dummy_setup_info();
- let mut interfaces = HashMap::new();
+ let mut interfaces = BTreeMap::new();
interfaces.insert(
"eth0".to_owned(),
Interface {
-use std::{collections::HashMap, fmt, net::IpAddr};
-
-use serde::{Serialize, Serializer};
+use std::collections::BTreeMap;
use crate::options::InstallerOptions;
-use proxmox_installer_common::{
- options::{AdvancedBootdiskOptions, BtrfsRaidLevel, Disk, FsType, ZfsRaidLevel},
- setup::InstallZfsOption,
- utils::CidrAddress,
-};
-
-/// See Proxmox::Install::Config
-#[derive(Serialize)]
-pub struct InstallConfig {
- autoreboot: usize,
-
- #[serde(serialize_with = "serialize_fstype")]
- filesys: FsType,
- hdsize: f64,
- #[serde(skip_serializing_if = "Option::is_none")]
- swapsize: Option<f64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- maxroot: Option<f64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- minfree: Option<f64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- maxvz: Option<f64>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- zfs_opts: Option<InstallZfsOption>,
-
- #[serde(
- serialize_with = "serialize_disk_opt",
- skip_serializing_if = "Option::is_none"
- )]
- target_hd: Option<Disk>,
- #[serde(skip_serializing_if = "HashMap::is_empty")]
- disk_selection: HashMap<String, String>,
-
- country: String,
- timezone: String,
- keymap: String,
-
- password: String,
- mailto: String,
-
- mngmt_nic: String,
-
- hostname: String,
- domain: String,
- #[serde(serialize_with = "serialize_as_display")]
- cidr: CidrAddress,
- gateway: IpAddr,
- dns: IpAddr,
-}
+use proxmox_installer_common::{options::AdvancedBootdiskOptions, setup::InstallConfig};
impl From<InstallerOptions> for InstallConfig {
fn from(options: InstallerOptions) -> Self {
maxvz: None,
zfs_opts: None,
target_hd: None,
- disk_selection: HashMap::new(),
+ disk_selection: BTreeMap::new(),
+ lvm_auto_rename: 0,
country: options.timezone.country,
timezone: options.timezone.timezone,
password: options.password.root_password,
mailto: options.password.email,
+ root_ssh_keys: vec![],
mngmt_nic: options.network.ifname,
config
}
}
-
-fn serialize_disk_opt<S>(value: &Option<Disk>, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- if let Some(disk) = value {
- serializer.serialize_str(&disk.path)
- } else {
- serializer.serialize_none()
- }
-}
-
-fn serialize_fstype<S>(value: &FsType, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- use FsType::*;
- let value = match value {
- // proxinstall::$fssetup
- Ext4 => "ext4",
- Xfs => "xfs",
- // proxinstall::get_zfs_raid_setup()
- Zfs(ZfsRaidLevel::Raid0) => "zfs (RAID0)",
- Zfs(ZfsRaidLevel::Raid1) => "zfs (RAID1)",
- Zfs(ZfsRaidLevel::Raid10) => "zfs (RAID10)",
- Zfs(ZfsRaidLevel::RaidZ) => "zfs (RAIDZ-1)",
- Zfs(ZfsRaidLevel::RaidZ2) => "zfs (RAIDZ-2)",
- Zfs(ZfsRaidLevel::RaidZ3) => "zfs (RAIDZ-3)",
- // proxinstall::get_btrfs_raid_setup()
- Btrfs(BtrfsRaidLevel::Raid0) => "btrfs (RAID0)",
- Btrfs(BtrfsRaidLevel::Raid1) => "btrfs (RAID1)",
- Btrfs(BtrfsRaidLevel::Raid10) => "btrfs (RAID10)",
- };
-
- serializer.collect_str(value)
-}
-
-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)
-}
.unwrap_or_default(),
),
)
- .child("copies", IntegerEditView::new().content(options.copies).max_value(3))
+ .child(
+ "copies",
+ IntegerEditView::new().content(options.copies).max_value(3),
+ )
.child_conditional(
is_pve,
"ARC max size",
use cursive::{
utils::Counter,
view::{Nameable, Resizable, ViewWrapper},
- views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextContent, TextView},
+ views::{Dialog, DummyView, LinearLayout, PaddedView, ProgressBar, TextView},
CbSink, Cursive,
};
use serde::Deserialize;
time::Duration,
};
-use crate::{abort_install_button, prompt_dialog, setup::InstallConfig, InstallerState};
-use proxmox_installer_common::setup::spawn_low_level_installer;
+use crate::{abort_install_button, prompt_dialog, InstallerState};
+use proxmox_installer_common::setup::{spawn_low_level_installer, InstallConfig};
pub struct InstallProgressView {
view: PaddedView<LinearLayout>,
}
impl InstallProgressView {
+ const PROGRESS_TEXT_VIEW_ID: &str = "progress-text";
+
pub fn new(siv: &mut Cursive) -> Self {
let cb_sink = siv.cb_sink().clone();
let state = siv.user_data::<InstallerState>().unwrap();
- let progress_text = TextContent::new("starting the installation ..");
let progress_task = {
- let progress_text = progress_text.clone();
let state = state.clone();
- move |counter: Counter| Self::progress_task(counter, cb_sink, state, progress_text)
+ move |counter: Counter| Self::progress_task(counter, cb_sink, state)
};
let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
LinearLayout::vertical()
.child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
.child(DummyView)
- .child(TextView::new_with_content(progress_text).center())
+ .child(
+ TextView::new("starting the installation ..")
+ .center()
+ .with_name(Self::PROGRESS_TEXT_VIEW_ID),
+ )
.child(PaddedView::lrtb(
1,
1,
Self { view }
}
- fn progress_task(
- counter: Counter,
- cb_sink: CbSink,
- state: InstallerState,
- progress_text: TextContent,
- ) {
+ fn progress_task(counter: Counter, cb_sink: CbSink, state: InstallerState) {
let mut child = match spawn_low_level_installer(state.in_test_mode) {
Ok(child) => child,
Err(err) => {
}),
UiMessage::Progress { ratio, text } => {
counter.set((ratio * 100.).floor() as usize);
- progress_text.set_content(text);
- Ok(())
+ cb_sink.send(Box::new(move |siv| {
+ siv.call_on_name(Self::PROGRESS_TEXT_VIEW_ID, |v: &mut TextView| {
+ v.set_content(text);
+ });
+ }))
}
UiMessage::Finished { state, message } => {
counter.set(100);
- progress_text.set_content(message.to_owned());
cb_sink.send(Box::new(move |siv| {
+ siv.call_on_name(Self::PROGRESS_TEXT_VIEW_ID, |v: &mut TextView| {
+ v.set_content(&message);
+ });
Self::prepare_for_reboot(siv, state == "ok", &message);
}))
}
# NOTE: we nowadays get exec'd by the initrd's PID 1, so we're the new PID 1
parse_cmdline() {
+ start_auto_installer=0
proxdebug=0
proxtui=0
serial=0
# shellcheck disable=SC2013 # per word splitting is wanted here
for par in $(cat /proc/cmdline); do
case $par in
- proxdebug)
+ proxdebug|proxmox-debug)
proxdebug=1
;;
- proxtui)
+ proxtui|proxmox-tui-mode)
proxtui=1
;;
+ proxauto|proxmox-start-auto-installer)
+ start_auto_installer=1
+ ;;
console=ttyS*)
serial=1
;;
debugsh || true
fi
+# add custom DHCP options for auto installer
+if [ $start_auto_installer -ne 0 ]; then
+ echo "Preparing DHCP as potential source to get location of automatic-installation answer file"
+ cat >> /etc/dhcp/dhclient.conf <<EOF
+option proxmox-auto-installer-manifest-url code 250 = text;
+option proxmox-auto-installer-cert-fingerprint code 251 = text;
+also request proxmox-auto-installer-manifest-url, proxmox-auto-installer-cert-fingerprint;
+EOF
+fi
+
# try to get ip config with dhcp
echo -n "Attempting to get DHCP leases... "
dhclient -v
if [ $proxtui -ne 0 ]; then
echo "Starting the TUI installer"
/usr/bin/proxmox-tui-installer 2>/dev/tty2
+elif [ $start_auto_installer -ne 0 ]; then
+ echo "Caching device info from udev"
+ /usr/bin/proxmox-low-level-installer dump-udev
+
+ if [ -f /cdrom/auto-installer-mode.toml ]; then
+ echo "Fetching answers for automatic installation"
+ /usr/bin/proxmox-fetch-answer >/run/automatic-installer-answers
+ else
+ printf "\nAutomatic installation selected but no config for fetching the answer file found!\n"
+ echo "Starting debug shell, to fetch the answer file manually use:"
+ echo " proxmox-fetch-answer MODE >/run/automatic-installer-answers"
+ echo "and enter 'exit' or press 'CTRL' + 'D' when finished."
+ debugsh || true
+ fi
+ echo "Starting automatic installation"
+ /usr/bin/proxmox-auto-installer </run/automatic-installer-answers
else
echo "Starting the installer GUI - see tty2 (CTRL+ALT+F2) for any errors..."
xinit -- -dpi "$DPI" -s 0 >/dev/tty2 2>&1