mirror of
https://git.proxmox.com/git/pve-installer
synced 2025-04-28 16:34:37 +00:00
fix #5536: post-hook: add utility for sending notifications after auto-install
This utility can be called with the low-level install config after a successful installation to send a notification via a HTTP POST request, if the user has configured an endpoint for that in the answer file. Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
This commit is contained in:
parent
5d82b252cb
commit
4be9eb381f
@ -7,6 +7,7 @@ members = [
|
||||
"proxmox-fetch-answer",
|
||||
"proxmox-installer-common",
|
||||
"proxmox-tui-installer",
|
||||
"proxmox-post-hook",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
8
Makefile
8
Makefile
@ -24,7 +24,8 @@ USR_BIN := \
|
||||
proxmox-tui-installer\
|
||||
proxmox-fetch-answer\
|
||||
proxmox-auto-install-assistant \
|
||||
proxmox-auto-installer
|
||||
proxmox-auto-installer \
|
||||
proxmox-post-hook
|
||||
|
||||
COMPILED_BINS := \
|
||||
$(addprefix $(CARGO_COMPILEDIR)/,$(USR_BIN))
|
||||
@ -59,6 +60,7 @@ $(BUILDDIR):
|
||||
proxmox-chroot \
|
||||
proxmox-tui-installer/ \
|
||||
proxmox-installer-common/ \
|
||||
proxmox-post-hook \
|
||||
test/ \
|
||||
$(SHELL_SCRIPTS) \
|
||||
$@.tmp
|
||||
@ -132,7 +134,9 @@ cargo-build:
|
||||
--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)
|
||||
--package proxmox-chroot --bin proxmox-chroot \
|
||||
--package proxmox-post-hook --bin proxmox-post-hook \
|
||||
$(CARGO_BUILD_ARGS)
|
||||
|
||||
%-banner.png: %-banner.svg
|
||||
rsvg-convert -o $@ $<
|
||||
|
1
debian/install
vendored
1
debian/install
vendored
@ -15,4 +15,5 @@ usr/bin/proxmox-chroot
|
||||
usr/bin/proxmox-fetch-answer
|
||||
usr/bin/proxmox-low-level-installer
|
||||
usr/bin/proxmox-tui-installer
|
||||
usr/bin/proxmox-post-hook
|
||||
var
|
||||
|
@ -82,8 +82,6 @@ fn main() -> ExitCode {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: (optionally) do a HTTP post with basic system info, like host SSH public key(s) here
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,11 @@ impl FsType {
|
||||
pub fn is_btrfs(&self) -> bool {
|
||||
matches!(self, FsType::Btrfs(_))
|
||||
}
|
||||
|
||||
/// Returns true if the filesystem is used on top of LVM, e.g. ext4 or XFS.
|
||||
pub fn is_lvm(&self) -> bool {
|
||||
matches!(self, FsType::Ext4 | FsType::Xfs)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FsType {
|
||||
|
@ -363,7 +363,7 @@ pub struct RuntimeInfo {
|
||||
pub secure_boot: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
|
||||
#[derive(Copy, Clone, Eq, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BootType {
|
||||
Bios,
|
||||
|
@ -114,6 +114,8 @@ impl<'de> Deserialize<'de> for CidrAddress {
|
||||
}
|
||||
}
|
||||
|
||||
serde_plain::derive_serialize_from_display!(CidrAddress);
|
||||
|
||||
fn mask_limit(addr: &IpAddr) -> usize {
|
||||
if addr.is_ipv4() {
|
||||
32
|
||||
|
18
proxmox-post-hook/Cargo.toml
Normal file
18
proxmox-post-hook/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "proxmox-post-hook"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Christoph Heiss <c.heiss@proxmox.com>",
|
||||
"Proxmox Support Team <support@proxmox.com>",
|
||||
]
|
||||
license = "AGPL-3"
|
||||
exclude = [ "build", "debian" ]
|
||||
homepage = "https://www.proxmox.com"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
proxmox-auto-installer.workspace = true
|
||||
proxmox-installer-common = { workspace = true, features = ["http"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
784
proxmox-post-hook/src/main.rs
Normal file
784
proxmox-post-hook/src/main.rs
Normal file
@ -0,0 +1,784 @@
|
||||
//! Post installation hook for the Proxmox installer, mainly for combination
|
||||
//! with the auto-installer.
|
||||
//!
|
||||
//! If a `[posthook]` section is specified in the given answer file, it will
|
||||
//! send a HTTP POST request to that URL, with an optional certificate fingerprint
|
||||
//! for usage with (self-signed) TLS certificates.
|
||||
//! In the body of the request, information about the newly installed system is sent.
|
||||
//!
|
||||
//! Relies on `proxmox-chroot` as an external dependency to (bind-)mount the
|
||||
//! previously installed system.
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::CStr,
|
||||
fs::{self, File},
|
||||
io::BufReader,
|
||||
os::unix::fs::FileExt,
|
||||
path::PathBuf,
|
||||
process::{Command, ExitCode},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use proxmox_auto_installer::{
|
||||
answer::{Answer, PostNotificationHookInfo},
|
||||
udevinfo::{UdevInfo, UdevProperties},
|
||||
};
|
||||
use proxmox_installer_common::{
|
||||
options::{Disk, FsType},
|
||||
setup::{
|
||||
load_installer_setup_files, BootType, InstallConfig, IsoInfo, ProxmoxProduct, RuntimeInfo,
|
||||
SetupInfo,
|
||||
},
|
||||
sysinfo::SystemDMI,
|
||||
utils::CidrAddress,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Information about the system boot status.
|
||||
#[derive(Serialize)]
|
||||
struct BootInfo {
|
||||
/// Whether the system is booted using UEFI or legacy BIOS.
|
||||
mode: BootType,
|
||||
/// Whether SecureBoot is enabled for the installation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
secureboot: Option<bool>,
|
||||
}
|
||||
|
||||
/// Holds all the public keys for the different algorithms available.
|
||||
#[derive(Serialize)]
|
||||
struct SshPublicHostKeys {
|
||||
// ECDSA-based public host key
|
||||
ecdsa: String,
|
||||
// ED25519-based public host key
|
||||
ed25519: String,
|
||||
// RSA-based public host key
|
||||
rsa: String,
|
||||
}
|
||||
|
||||
/// Holds information about a single disk in the system.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct DiskInfo {
|
||||
/// Size in bytes
|
||||
size: usize,
|
||||
/// Set to true if the disk is used for booting.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_bootdisk: Option<bool>,
|
||||
/// Properties about the device as given by udev.
|
||||
udev_properties: UdevProperties,
|
||||
}
|
||||
|
||||
/// Holds information about the management network interface.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct NetworkInterfaceInfo {
|
||||
/// MAC address of the interface
|
||||
mac: String,
|
||||
/// (Designated) IP address of the interface
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
address: Option<CidrAddress>,
|
||||
/// Set to true if the interface is the chosen management interface during
|
||||
/// installation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_management: Option<bool>,
|
||||
/// Properties about the device as given by udev.
|
||||
udev_properties: UdevProperties,
|
||||
}
|
||||
|
||||
/// Information about the installed product itself.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ProductInfo {
|
||||
/// Full name of the product
|
||||
fullname: String,
|
||||
/// Product abbreviation
|
||||
short: ProxmoxProduct,
|
||||
/// Version of the installed product
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// The current kernel version.
|
||||
/// Aligns with the format as used by the /nodes/<node>/status API of each product.
|
||||
#[derive(Serialize)]
|
||||
struct KernelVersionInformation {
|
||||
/// The systemname/nodename
|
||||
pub sysname: String,
|
||||
/// The kernel release number
|
||||
pub release: String,
|
||||
/// The kernel version
|
||||
pub version: String,
|
||||
/// The machine architecture
|
||||
pub machine: String,
|
||||
}
|
||||
|
||||
/// Information about the CPU(s) installed in the system
|
||||
#[derive(Serialize)]
|
||||
struct CpuInfo {
|
||||
/// Number of physical CPU cores.
|
||||
cores: usize,
|
||||
/// Number of logical CPU cores aka. threads.
|
||||
cpus: usize,
|
||||
/// CPU feature flag set as a space-delimited list.
|
||||
flags: String,
|
||||
/// Whether hardware-accelerated virtualization is supported.
|
||||
hvm: bool,
|
||||
/// Reported model of the CPU(s)
|
||||
model: String,
|
||||
/// Number of physical CPU sockets
|
||||
sockets: usize,
|
||||
}
|
||||
|
||||
/// All data sent as request payload with the post-hook POST request.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct PostHookInfo {
|
||||
/// major.minor version of Debian as installed, retrieved from /etc/debian_version
|
||||
debian_version: String,
|
||||
/// PVE/PMG/PBS version as reported by `pveversion`, `pmgversion` or
|
||||
/// `proxmox-backup-manager version`, respectively.
|
||||
product: ProductInfo,
|
||||
/// Release information for the ISO used for the installation.
|
||||
iso: IsoInfo,
|
||||
/// Installed kernel version
|
||||
kernel_version: KernelVersionInformation,
|
||||
/// Describes the boot mode of the machine and the SecureBoot status.
|
||||
boot_info: BootInfo,
|
||||
/// Information about the installed CPU(s)
|
||||
cpu_info: CpuInfo,
|
||||
/// DMI information about the system
|
||||
dmi: SystemDMI,
|
||||
/// Filesystem used for boot disk(s)
|
||||
filesystem: FsType,
|
||||
/// Fully qualified domain name of the installed system
|
||||
fqdn: String,
|
||||
/// Unique systemd-id128 identifier of the installed system (128-bit, 16 bytes)
|
||||
machine_id: String,
|
||||
/// All disks detected on the system.
|
||||
disks: Vec<DiskInfo>,
|
||||
/// All network interfaces detected on the system.
|
||||
network_interfaces: Vec<NetworkInterfaceInfo>,
|
||||
/// Public parts of SSH host keys of the installed system
|
||||
ssh_public_host_keys: SshPublicHostKeys,
|
||||
}
|
||||
|
||||
/// Defines the size of a gibibyte in bytes.
|
||||
const SIZE_GIB: usize = 1024 * 1024 * 1024;
|
||||
|
||||
impl PostHookInfo {
|
||||
/// Gathers all needed information about the newly installed system for sending
|
||||
/// it to a specified server.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target_path` - Path to where the chroot environment root is mounted
|
||||
/// * `answer` - Answer file as provided by the user
|
||||
fn gather(target_path: &str, answer: &Answer) -> Result<Self> {
|
||||
println!("Gathering installed system data ...");
|
||||
|
||||
let config: InstallConfig =
|
||||
serde_json::from_reader(BufReader::new(File::open("/tmp/low-level-config.json")?))?;
|
||||
|
||||
let (setup_info, _, run_env) =
|
||||
load_installer_setup_files(proxmox_installer_common::RUNTIME_DIR)
|
||||
.map_err(|err| anyhow!("Failed to load setup files: {err}"))?;
|
||||
|
||||
let udev: UdevInfo = {
|
||||
let path =
|
||||
PathBuf::from(proxmox_installer_common::RUNTIME_DIR).join("run-env-udev.json");
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
};
|
||||
|
||||
// Opens a file, specified by an absolute path _inside_ the chroot
|
||||
// from the target.
|
||||
let open_file = |path: &str| {
|
||||
File::open(format!("{}/{}", target_path, path))
|
||||
.with_context(|| format!("failed to open '{path}'"))
|
||||
};
|
||||
|
||||
// Reads a file, specified by an absolute path _inside_ the chroot
|
||||
// from the target.
|
||||
let read_file = |path: &str| {
|
||||
fs::read_to_string(format!("{}/{}", target_path, path))
|
||||
.map(|s| s.trim().to_owned())
|
||||
.with_context(|| format!("failed to read '{path}'"))
|
||||
};
|
||||
|
||||
// Runs a command inside the target chroot.
|
||||
let run_cmd = |cmd: &[&str]| {
|
||||
Command::new("chroot")
|
||||
.arg(target_path)
|
||||
.args(cmd)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run '{cmd:?}'"))
|
||||
.and_then(|r| Ok(String::from_utf8(r.stdout)?))
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
debian_version: read_file("/etc/debian_version")?,
|
||||
product: Self::gather_product_info(&setup_info, &run_cmd)?,
|
||||
iso: setup_info.iso_info.clone(),
|
||||
kernel_version: Self::gather_kernel_version(&run_cmd, &open_file)?,
|
||||
boot_info: BootInfo {
|
||||
mode: run_env.boot_type,
|
||||
secureboot: run_env.secure_boot,
|
||||
},
|
||||
cpu_info: Self::gather_cpu_info(&run_env)?,
|
||||
dmi: SystemDMI::get()?,
|
||||
filesystem: answer.disks.fs_type,
|
||||
fqdn: answer.global.fqdn.to_string(),
|
||||
machine_id: read_file("/etc/machine-id")?,
|
||||
disks: Self::gather_disks(&config, &run_env, &udev)?,
|
||||
network_interfaces: Self::gather_nic(&config, &run_env, &udev)?,
|
||||
ssh_public_host_keys: SshPublicHostKeys {
|
||||
ecdsa: read_file("/etc/ssh/ssh_host_ecdsa_key.pub")?,
|
||||
ed25519: read_file("/etc/ssh/ssh_host_ed25519_key.pub")?,
|
||||
rsa: read_file("/etc/ssh/ssh_host_rsa_key.pub")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves all needed information about the boot disks that were selected during
|
||||
/// installation, most notable the udev properties.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - Low-level installation configuration
|
||||
/// * `run_env` - Runtime envirornment information gathered by the installer at the start
|
||||
/// * `udev` - udev information for all system devices
|
||||
fn gather_disks(
|
||||
config: &InstallConfig,
|
||||
run_env: &RuntimeInfo,
|
||||
udev: &UdevInfo,
|
||||
) -> Result<Vec<DiskInfo>> {
|
||||
let get_udev_properties = |disk: &Disk| {
|
||||
udev.disks
|
||||
.get(&disk.index)
|
||||
.with_context(|| {
|
||||
format!("could not find udev information for disk '{}'", disk.path)
|
||||
})
|
||||
.cloned()
|
||||
};
|
||||
|
||||
let disks = if config.filesys.is_lvm() {
|
||||
// If the filesystem is LVM, there is only boot disk. The path (aka. /dev/..)
|
||||
// can be found in `config.target_hd`.
|
||||
run_env
|
||||
.disks
|
||||
.iter()
|
||||
.flat_map(|disk| {
|
||||
let is_bootdisk = config
|
||||
.target_hd
|
||||
.as_ref()
|
||||
.and_then(|hd| (*hd == disk.path).then_some(true));
|
||||
|
||||
anyhow::Ok(DiskInfo {
|
||||
size: (config.hdsize * (SIZE_GIB as f64)) as usize,
|
||||
is_bootdisk,
|
||||
udev_properties: get_udev_properties(disk)?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
// If the filesystem is not LVM-based (thus Btrfs or ZFS), `config.disk_selection`
|
||||
// contains a list of indices identifiying the boot disks, as given by udev.
|
||||
let selected_disks_indices: Vec<&String> = config.disk_selection.values().collect();
|
||||
|
||||
run_env
|
||||
.disks
|
||||
.iter()
|
||||
.flat_map(|disk| {
|
||||
let is_bootdisk = selected_disks_indices
|
||||
.contains(&&disk.index)
|
||||
.then_some(true);
|
||||
|
||||
anyhow::Ok(DiskInfo {
|
||||
size: (config.hdsize * (SIZE_GIB as f64)) as usize,
|
||||
is_bootdisk,
|
||||
udev_properties: get_udev_properties(disk)?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
Ok(disks)
|
||||
}
|
||||
|
||||
/// Retrieves all needed information about the management network interface that was selected
|
||||
/// during installation, most notable the udev properties.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - Low-level installation configuration
|
||||
/// * `run_env` - Runtime envirornment information gathered by the installer at the start
|
||||
/// * `udev` - udev information for all system devices
|
||||
fn gather_nic(
|
||||
config: &InstallConfig,
|
||||
run_env: &RuntimeInfo,
|
||||
udev: &UdevInfo,
|
||||
) -> Result<Vec<NetworkInterfaceInfo>> {
|
||||
Ok(run_env
|
||||
.network
|
||||
.interfaces
|
||||
.values()
|
||||
.flat_map(|nic| {
|
||||
let udev_properties = udev
|
||||
.nics
|
||||
.get(&nic.name)
|
||||
.with_context(|| {
|
||||
format!("could not find udev information for NIC '{}'", nic.name)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
if config.mngmt_nic == nic.name {
|
||||
// Use the actual IP address from the low-level install config, as the runtime info
|
||||
// contains the original IP address from DHCP.
|
||||
anyhow::Ok(NetworkInterfaceInfo {
|
||||
mac: nic.mac.clone(),
|
||||
address: Some(config.cidr.clone()),
|
||||
is_management: Some(true),
|
||||
udev_properties,
|
||||
})
|
||||
} else {
|
||||
anyhow::Ok(NetworkInterfaceInfo {
|
||||
mac: nic.mac.clone(),
|
||||
address: None,
|
||||
is_management: None,
|
||||
udev_properties,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Retrieves the version of the installed product from the chroot.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `setup_info` - Filled-out struct with information about the product
|
||||
/// * `run_cmd` - Callback to run a command inside the target chroot.
|
||||
fn gather_product_info(
|
||||
setup_info: &SetupInfo,
|
||||
run_cmd: &dyn Fn(&[&str]) -> Result<String>,
|
||||
) -> Result<ProductInfo> {
|
||||
let package = match setup_info.config.product {
|
||||
ProxmoxProduct::PVE => "pve-manager",
|
||||
ProxmoxProduct::PMG => "pmg-api",
|
||||
ProxmoxProduct::PBS => "proxmox-backup-server",
|
||||
};
|
||||
|
||||
let version = run_cmd(&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${Version}",
|
||||
"--show",
|
||||
package,
|
||||
])
|
||||
.with_context(|| format!("failed to retrieve version of {package}"))?;
|
||||
|
||||
Ok(ProductInfo {
|
||||
fullname: setup_info.config.fullname.clone(),
|
||||
short: setup_info.config.product,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extracts the version string from the *installed* kernel image.
|
||||
///
|
||||
/// First, it determines the exact path to the kernel image (aka. `/boot/vmlinuz-<version>`)
|
||||
/// by looking at the installed kernel package, then reads the string directly from the image
|
||||
/// from the well-defined kernel header. See also [0] for details.
|
||||
///
|
||||
/// [0] https://www.kernel.org/doc/html/latest/arch/x86/boot.html
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `run_cmd` - Callback to run a command inside the target chroot.
|
||||
/// * `open_file` - Callback to open a file inside the target chroot.
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn gather_kernel_version(
|
||||
run_cmd: &dyn Fn(&[&str]) -> Result<String>,
|
||||
open_file: &dyn Fn(&str) -> Result<File>,
|
||||
) -> Result<KernelVersionInformation> {
|
||||
let file = open_file(&Self::find_kernel_image_path(run_cmd)?)?;
|
||||
|
||||
// Read the 2-byte `kernel_version` field at offset 0x20e [0] from the file ..
|
||||
// https://www.kernel.org/doc/html/latest/arch/x86/boot.html#the-real-mode-kernel-header
|
||||
let mut buffer = [0u8; 2];
|
||||
file.read_exact_at(&mut buffer, 0x20e)
|
||||
.context("could not read kernel_version offset from image")?;
|
||||
|
||||
// .. which gives us the offset of the kernel version string inside the image, minus 0x200.
|
||||
// https://www.kernel.org/doc/html/latest/arch/x86/boot.html#details-of-header-fields
|
||||
let offset = u16::from_le_bytes(buffer) + 0x200;
|
||||
|
||||
// The string is usually somewhere around 80-100 bytes long, so 256 bytes is more than
|
||||
// enough to cover all cases.
|
||||
let mut buffer = [0u8; 256];
|
||||
file.read_exact_at(&mut buffer, offset.into())
|
||||
.context("could not read kernel version string from image")?;
|
||||
|
||||
// Now just consume the buffer until the NUL byte
|
||||
let kernel_version = CStr::from_bytes_until_nul(&buffer)
|
||||
.context("did not find a NUL-terminator in kernel version string")?
|
||||
.to_str()
|
||||
.context("could not convert kernel version string")?;
|
||||
|
||||
// The version string looks like:
|
||||
// 6.8.4-2-pve (build@proxmox) #1 SMP PREEMPT_DYNAMIC PMX 6.8.4-2 (2024-04-10T17:36Z) x86_64 GNU/Linux
|
||||
//
|
||||
// Thus split it into three parts, as we are interested in the release version
|
||||
// and everything starting at the build number
|
||||
let parts: Vec<&str> = kernel_version.splitn(3, ' ').collect();
|
||||
|
||||
if parts.len() != 3 {
|
||||
bail!("failed to split kernel version string");
|
||||
}
|
||||
|
||||
Ok(KernelVersionInformation {
|
||||
machine: std::env::consts::ARCH.to_owned(),
|
||||
sysname: "Linux".to_owned(),
|
||||
release: parts
|
||||
.first()
|
||||
.context("kernel release not found")?
|
||||
.to_string(),
|
||||
version: parts
|
||||
.get(2)
|
||||
.context("kernel version not found")?
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the absolute path to the kernel image (aka. `/boot/vmlinuz-<version>`)
|
||||
/// inside the chroot by looking at the file list installed by the kernel package.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `run_cmd` - Callback to run a command inside the target chroot.
|
||||
fn find_kernel_image_path(run_cmd: &dyn Fn(&[&str]) -> Result<String>) -> Result<String> {
|
||||
let pkg_name = Self::find_kernel_package_name(run_cmd)?;
|
||||
|
||||
let all_files = run_cmd(&["dpkg-query", "--listfiles", &pkg_name])?;
|
||||
for file in all_files.lines() {
|
||||
if file.starts_with("/boot/vmlinuz-") {
|
||||
return Ok(file.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to find installed kernel image path")
|
||||
}
|
||||
|
||||
/// Retrieves the full name of the kernel package installed inside the chroot.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `run_cmd` - Callback to run a command inside the target chroot.
|
||||
fn find_kernel_package_name(run_cmd: &dyn Fn(&[&str]) -> Result<String>) -> Result<String> {
|
||||
let dpkg_arch = run_cmd(&["dpkg", "--print-architecture"])?
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let kernel_pkgs = run_cmd(&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${db:Status-Abbrev}|${Architecture}|${Package}\\n",
|
||||
"--show",
|
||||
"proxmox-kernel-[0-9]*",
|
||||
])?;
|
||||
|
||||
// The output to parse looks like this:
|
||||
// ii |all|proxmox-kernel-6.8
|
||||
// un ||proxmox-kernel-6.8.8-2-pve
|
||||
// ii |amd64|proxmox-kernel-6.8.8-2-pve-signed
|
||||
for pkg in kernel_pkgs.lines() {
|
||||
let parts = pkg.split('|').collect::<Vec<&str>>();
|
||||
|
||||
if let [status, arch, name] = parts[..] {
|
||||
if status.trim() == "ii" && arch.trim() == dpkg_arch {
|
||||
return Ok(name.trim().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to find installed kernel package")
|
||||
}
|
||||
|
||||
/// Retrieves some basic information about the CPU in the running system,
|
||||
/// reading them from /proc/cpuinfo.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `run_env` - Runtime envirornment information gathered by the installer at the start
|
||||
fn gather_cpu_info(run_env: &RuntimeInfo) -> Result<CpuInfo> {
|
||||
let mut result = CpuInfo {
|
||||
cores: 0,
|
||||
cpus: 0,
|
||||
flags: String::new(),
|
||||
hvm: run_env.hvm_supported,
|
||||
model: String::new(),
|
||||
sockets: 0,
|
||||
};
|
||||
let mut sockets = HashSet::new();
|
||||
let mut cores = HashSet::new();
|
||||
|
||||
// Does not matter if we read the file from inside the chroot or directly on the host.
|
||||
let cpuinfo = fs::read_to_string("/proc/cpuinfo")?;
|
||||
for line in cpuinfo.lines() {
|
||||
match line.split_once(':') {
|
||||
Some((key, _)) if key.trim() == "processor" => {
|
||||
result.cpus += 1;
|
||||
}
|
||||
Some((key, value)) if key.trim() == "core id" => {
|
||||
cores.insert(value);
|
||||
}
|
||||
Some((key, value)) if key.trim() == "physical id" => {
|
||||
sockets.insert(value);
|
||||
}
|
||||
Some((key, value)) if key.trim() == "flags" => {
|
||||
value.trim().clone_into(&mut result.flags);
|
||||
}
|
||||
Some((key, value)) if key.trim() == "model name" => {
|
||||
value.trim().clone_into(&mut result.model);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
result.cores = cores.len();
|
||||
result.sockets = sockets.len();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the specified callback with the mounted chroot, passing along the
|
||||
/// absolute path to where / is mounted.
|
||||
/// The callback is *not* run inside the chroot itself, that is left to the caller.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `callback` - Callback to call with the absolute path where the chroot environment root is
|
||||
/// mounted.
|
||||
fn with_chroot<R, F: FnOnce(&str) -> Result<R>>(callback: F) -> Result<R> {
|
||||
let ec = Command::new("proxmox-chroot")
|
||||
.arg("prepare")
|
||||
.status()
|
||||
.context("failed to run proxmox-chroot")?;
|
||||
|
||||
if !ec.success() {
|
||||
bail!("failed to create chroot for installed system");
|
||||
}
|
||||
|
||||
// See also proxmox-chroot/src/main.rs w.r.t to the path, which is hard-coded there
|
||||
let result = callback("/target");
|
||||
|
||||
let ec = Command::new("proxmox-chroot").arg("cleanup").status();
|
||||
// We do not want to necessarily fail here, as the install environment is about
|
||||
// to be teared down completely anyway.
|
||||
if ec.is_err() || !ec.map(|ec| ec.success()).unwrap_or(false) {
|
||||
eprintln!("failed to clean up chroot for installed system");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Reads the answer file from stdin, checks for a configured post-hook URL (+ optional certificate
|
||||
/// fingerprint for HTTPS). If configured, retrieves all relevant information about the installed
|
||||
/// system and sends them to the given endpoint.
|
||||
fn do_main() -> Result<()> {
|
||||
let answer = Answer::try_from_reader(std::io::stdin().lock())?;
|
||||
|
||||
if let Some(PostNotificationHookInfo {
|
||||
url,
|
||||
cert_fingerprint,
|
||||
}) = &answer.posthook
|
||||
{
|
||||
println!("Found posthook; sending POST request to '{url}'.");
|
||||
|
||||
let info = with_chroot(|target_path| PostHookInfo::gather(target_path, &answer))?;
|
||||
|
||||
proxmox_installer_common::http::post(
|
||||
url,
|
||||
cert_fingerprint.as_deref(),
|
||||
serde_json::to_string(&info)?,
|
||||
)?;
|
||||
} else {
|
||||
println!("No posthook found; skipping");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match do_main() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(err) => {
|
||||
eprintln!("\nError occurred during posthook:");
|
||||
eprintln!("{err:#}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::PostHookInfo;
|
||||
|
||||
#[test]
|
||||
fn finds_correct_kernel_package_name() {
|
||||
let mocked_run_cmd = |cmd: &[&str]| {
|
||||
if cmd[0] == "dpkg" {
|
||||
assert_eq!(cmd, &["dpkg", "--print-architecture"]);
|
||||
Ok("amd64\n".to_owned())
|
||||
} else {
|
||||
assert_eq!(
|
||||
cmd,
|
||||
&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${db:Status-Abbrev}|${Architecture}|${Package}\\n",
|
||||
"--show",
|
||||
"proxmox-kernel-[0-9]*",
|
||||
]
|
||||
);
|
||||
Ok(r#"ii |all|proxmox-kernel-6.8
|
||||
un ||proxmox-kernel-6.8.8-2-pve
|
||||
ii |amd64|proxmox-kernel-6.8.8-2-pve-signed
|
||||
"#
|
||||
.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
PostHookInfo::find_kernel_package_name(&mocked_run_cmd).unwrap(),
|
||||
"proxmox-kernel-6.8.8-2-pve-signed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_kernel_package_name_fails_on_wrong_architecture() {
|
||||
let mocked_run_cmd = |cmd: &[&str]| {
|
||||
if cmd[0] == "dpkg" {
|
||||
assert_eq!(cmd, &["dpkg", "--print-architecture"]);
|
||||
Ok("arm64\n".to_owned())
|
||||
} else {
|
||||
assert_eq!(
|
||||
cmd,
|
||||
&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${db:Status-Abbrev}|${Architecture}|${Package}\\n",
|
||||
"--show",
|
||||
"proxmox-kernel-[0-9]*",
|
||||
]
|
||||
);
|
||||
Ok(r#"ii |all|proxmox-kernel-6.8
|
||||
un ||proxmox-kernel-6.8.8-2-pve
|
||||
ii |amd64|proxmox-kernel-6.8.8-2-pve-signed
|
||||
"#
|
||||
.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
PostHookInfo::find_kernel_package_name(&mocked_run_cmd)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"failed to find installed kernel package"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_kernel_package_name_fails_on_missing_package() {
|
||||
let mocked_run_cmd = |cmd: &[&str]| {
|
||||
if cmd[0] == "dpkg" {
|
||||
assert_eq!(cmd, &["dpkg", "--print-architecture"]);
|
||||
Ok("amd64\n".to_owned())
|
||||
} else {
|
||||
assert_eq!(
|
||||
cmd,
|
||||
&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${db:Status-Abbrev}|${Architecture}|${Package}\\n",
|
||||
"--show",
|
||||
"proxmox-kernel-[0-9]*",
|
||||
]
|
||||
);
|
||||
Ok(r#"ii |all|proxmox-kernel-6.8
|
||||
un ||proxmox-kernel-6.8.8-2-pve
|
||||
"#
|
||||
.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
PostHookInfo::find_kernel_package_name(&mocked_run_cmd)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"failed to find installed kernel package"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_correct_absolute_kernel_image_path() {
|
||||
let mocked_run_cmd = |cmd: &[&str]| {
|
||||
if cmd[0] == "dpkg" {
|
||||
assert_eq!(cmd, &["dpkg", "--print-architecture"]);
|
||||
Ok("amd64\n".to_owned())
|
||||
} else if cmd[0..=1] == ["dpkg-query", "--showformat"] {
|
||||
assert_eq!(
|
||||
cmd,
|
||||
&[
|
||||
"dpkg-query",
|
||||
"--showformat",
|
||||
"${db:Status-Abbrev}|${Architecture}|${Package}\\n",
|
||||
"--show",
|
||||
"proxmox-kernel-[0-9]*",
|
||||
]
|
||||
);
|
||||
Ok(r#"ii |all|proxmox-kernel-6.8
|
||||
un ||proxmox-kernel-6.8.8-2-pve
|
||||
ii |amd64|proxmox-kernel-6.8.8-2-pve-signed
|
||||
"#
|
||||
.to_owned())
|
||||
} else {
|
||||
assert_eq!(
|
||||
cmd,
|
||||
[
|
||||
"dpkg-query",
|
||||
"--listfiles",
|
||||
"proxmox-kernel-6.8.8-2-pve-signed"
|
||||
]
|
||||
);
|
||||
Ok(r#"
|
||||
/.
|
||||
/boot
|
||||
/boot/System.map-6.8.8-2-pve
|
||||
/boot/config-6.8.8-2-pve
|
||||
/boot/vmlinuz-6.8.8-2-pve
|
||||
/lib
|
||||
/lib/modules
|
||||
/lib/modules/6.8.8-2-pve
|
||||
/lib/modules/6.8.8-2-pve/kernel
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aegis128-aesni.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aesni-intel.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx-x86_64.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx2-x86_64.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-gfni-avx512-x86_64.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/blowfish-x86_64.ko
|
||||
/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/camellia-aesni-avx-x86_64.ko
|
||||
"#
|
||||
.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
PostHookInfo::find_kernel_image_path(&mocked_run_cmd).unwrap(),
|
||||
"/boot/vmlinuz-6.8.8-2-pve"
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user