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:
Christoph Heiss 2024-11-11 14:15:07 +01:00 committed by Thomas Lamprecht
parent 5d82b252cb
commit 4be9eb381f
9 changed files with 818 additions and 5 deletions

View File

@ -7,6 +7,7 @@ members = [
"proxmox-fetch-answer",
"proxmox-installer-common",
"proxmox-tui-installer",
"proxmox-post-hook",
]
[workspace.dependencies]

View File

@ -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
View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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,

View File

@ -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

View 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

View 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"
);
}
}