all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer 12/14] fix #5536: add post-hook utility for sending notifications after auto-install
Date: Wed, 10 Jul 2024 15:27:51 +0200	[thread overview]
Message-ID: <20240710132756.1149508-13-c.heiss@proxmox.com> (raw)
In-Reply-To: <20240710132756.1149508-1-c.heiss@proxmox.com>

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Cargo.toml                                    |   1 +
 Makefile                                      |   8 +-
 debian/install                                |   1 +
 proxmox-auto-installer/src/answer.rs          |  16 +-
 .../src/bin/proxmox-auto-installer.rs         |  13 +-
 .../src/fetch_plugins/http.rs                 |   2 +-
 .../src/fetch_plugins/partition.rs            |   2 +-
 proxmox-installer-common/src/http.rs          |   6 +-
 proxmox-installer-common/src/options.rs       |   4 +
 proxmox-installer-common/src/setup.rs         |   6 +-
 proxmox-installer-common/src/utils.rs         |   2 +
 proxmox-post-hook/Cargo.toml                  |  19 +
 proxmox-post-hook/src/main.rs                 | 372 ++++++++++++++++++
 13 files changed, 429 insertions(+), 23 deletions(-)
 create mode 100644 proxmox-post-hook/Cargo.toml
 create mode 100644 proxmox-post-hook/src/main.rs

diff --git a/Cargo.toml b/Cargo.toml
index 94a4dec..6d1e667 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
     "proxmox-fetch-answer",
     "proxmox-installer-common",
     "proxmox-tui-installer",
+    "proxmox-post-hook",
 ]
 
 [workspace.dependencies]
diff --git a/Makefile b/Makefile
index e96a0f2..9dc4c22 100644
--- a/Makefile
+++ b/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 $@ $<
diff --git a/debian/install b/debian/install
index bb91da7..b64c8ec 100644
--- a/debian/install
+++ b/debian/install
@@ -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
diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs
index e27a321..58c8136 100644
--- a/proxmox-auto-installer/src/answer.rs
+++ b/proxmox-auto-installer/src/answer.rs
@@ -1,10 +1,11 @@
+use anyhow::{format_err, Result};
 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};
+use std::{collections::BTreeMap, io::BufRead, net::IpAddr};
 
 // BTreeMap is used to store filters as the order of the filters will be stable, compared to
 // storing them in a HashMap
@@ -20,6 +21,19 @@ pub struct Answer {
     pub posthook: Option<PostNotificationHookInfo>,
 }
 
+impl Answer {
+    pub fn from_reader(reader: impl BufRead) -> Result<Self> {
+        let mut buffer = String::new();
+        let lines = reader.lines();
+        for line in lines {
+            buffer.push_str(&line.unwrap());
+            buffer.push('\n');
+        }
+
+        toml::from_str(&buffer).map_err(|err| format_err!("Failed parsing answer file: {err}"))
+    }
+}
+
 #[derive(Clone, Deserialize, Debug)]
 #[serde(deny_unknown_fields)]
 pub struct Global {
diff --git a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
index bf6f8fb..aab0f1f 100644
--- a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
+++ b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
@@ -42,16 +42,7 @@ fn auto_installer_setup(in_test_mode: bool) -> Result<(Answer, UdevInfo)> {
             .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}"))?;
-
+    let answer = Answer::from_reader(std::io::stdin().lock())?;
     Ok((answer, udev_info))
 }
 
@@ -91,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
 }
 
diff --git a/proxmox-fetch-answer/src/fetch_plugins/http.rs b/proxmox-fetch-answer/src/fetch_plugins/http.rs
index a6a8de0..4317430 100644
--- a/proxmox-fetch-answer/src/fetch_plugins/http.rs
+++ b/proxmox-fetch-answer/src/fetch_plugins/http.rs
@@ -68,7 +68,7 @@ impl FetchFromHTTP {
         let payload = SysInfo::as_json()?;
         info!("Sending POST request to '{answer_url}'.");
         let answer =
-            proxmox_installer_common::http::post(answer_url, fingerprint.as_deref(), payload)?;
+            proxmox_installer_common::http::post(&answer_url, fingerprint.as_deref(), payload)?;
         Ok(answer)
     }
 
diff --git a/proxmox-fetch-answer/src/fetch_plugins/partition.rs b/proxmox-fetch-answer/src/fetch_plugins/partition.rs
index 4472922..f07389b 100644
--- a/proxmox-fetch-answer/src/fetch_plugins/partition.rs
+++ b/proxmox-fetch-answer/src/fetch_plugins/partition.rs
@@ -31,7 +31,7 @@ impl FetchFromPartition {
 }
 
 fn path_exists_logged(file_name: &str, search_path: &str) -> Option<PathBuf> {
-    let path = Path::new(search_path).join(&file_name);
+    let path = Path::new(search_path).join(file_name);
     info!("Testing partition search path {path:?}");
     match path.try_exists() {
         Ok(true) => Some(path),
diff --git a/proxmox-installer-common/src/http.rs b/proxmox-installer-common/src/http.rs
index 4a5d444..b754ed8 100644
--- a/proxmox-installer-common/src/http.rs
+++ b/proxmox-installer-common/src/http.rs
@@ -15,7 +15,7 @@ use ureq::{Agent, AgentBuilder};
 /// * `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 post(url: String, fingerprint: Option<&str>, payload: String) -> Result<String> {
+pub fn post(url: &str, fingerprint: Option<&str>, payload: String) -> Result<String> {
     let answer;
 
     if let Some(fingerprint) = fingerprint {
@@ -27,7 +27,7 @@ pub fn post(url: String, fingerprint: Option<&str>, payload: String) -> Result<S
         let agent: Agent = AgentBuilder::new().tls_config(Arc::new(tls_config)).build();
 
         answer = agent
-            .post(&url)
+            .post(url)
             .set("Content-Type", "application/json; charset=utf-8")
             .send_string(&payload)?
             .into_string()?;
@@ -47,7 +47,7 @@ pub fn post(url: String, fingerprint: Option<&str>, payload: String) -> Result<S
             .tls_config(Arc::new(tls_config))
             .build();
         answer = agent
-            .post(&url)
+            .post(url)
             .set("Content-Type", "application/json; charset=utf-8")
             .timeout(std::time::Duration::from_secs(60))
             .send_string(&payload)?
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 9f6131b..b209587 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -45,6 +45,10 @@ impl FsType {
     pub fn is_btrfs(&self) -> bool {
         matches!(self, FsType::Btrfs(_))
     }
+
+    pub fn is_lvm(&self) -> bool {
+        matches!(self, FsType::Ext4 | FsType::Xfs)
+    }
 }
 
 impl fmt::Display for FsType {
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index 29137bf..479a3b5 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -335,7 +335,7 @@ pub struct RuntimeInfo {
     pub hvm_supported: bool,
 }
 
-#[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
+#[derive(Copy, Clone, Eq, Deserialize, PartialEq, Serialize)]
 #[serde(rename_all = "lowercase")]
 pub enum BootType {
     Bios,
@@ -383,7 +383,7 @@ pub struct Gateway {
     pub gateway: IpAddr,
 }
 
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Serialize)]
 #[serde(rename_all = "UPPERCASE")]
 pub enum InterfaceState {
     Up,
@@ -403,7 +403,7 @@ impl InterfaceState {
     }
 }
 
-#[derive(Clone, Deserialize)]
+#[derive(Clone, Deserialize, Serialize)]
 pub struct Interface {
     pub name: String,
 
diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
index 57b1753..2579c80 100644
--- a/proxmox-installer-common/src/utils.rs
+++ b/proxmox-installer-common/src/utils.rs
@@ -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
diff --git a/proxmox-post-hook/Cargo.toml b/proxmox-post-hook/Cargo.toml
new file mode 100644
index 0000000..ee4f679
--- /dev/null
+++ b/proxmox-post-hook/Cargo.toml
@@ -0,0 +1,19 @@
+[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
+toml.workspace = true
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
new file mode 100644
index 0000000..9e5680b
--- /dev/null
+++ b/proxmox-post-hook/src/main.rs
@@ -0,0 +1,372 @@
+//! 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::BTreeMap,
+    fs::{self, File},
+    io::BufReader,
+    path::PathBuf,
+    process::{Command, ExitCode},
+};
+
+use anyhow::{anyhow, bail, Context, Result};
+use proxmox_auto_installer::{
+    answer::{Answer, PostNotificationHookInfo},
+    udevinfo::UdevInfo,
+};
+use proxmox_installer_common::{
+    options::{Disk, FsType},
+    setup::{
+        load_installer_setup_files, BootType, InstallConfig, ProxmoxProduct, RuntimeInfo, SetupInfo,
+    },
+    utils::CidrAddress,
+};
+use serde::{Deserialize, Serialize};
+
+/// 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,
+}
+
+/// A single disk configured as boot disk.
+#[derive(Clone, Serialize)]
+#[serde(rename_all = "kebab-case")]
+struct BootDiskInfo {
+    /// Size in bytes
+    size: usize,
+    /// Properties as given by udev
+    udev_properties: BTreeMap<String, String>,
+}
+
+/// 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
+    address: CidrAddress,
+    /// Properties as given by udev
+    udev_properties: BTreeMap<String, String>,
+}
+
+/// 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_version: String,
+    /// Installed kernel version
+    kernel_version: String,
+    /// Either `bios` or `efi`
+    boot_type: BootType,
+    /// 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,
+    /// Boot disks selected during installation as used for installation of the system
+    bootdisk: Vec<BootDiskInfo>,
+    /// Primary management network interface chosen during installation
+    management_nic: NetworkInterfaceInfo,
+    /// Public parts of SSH host keys of the installed system
+    ssh_public_host_keys: SshPublicHostKeys,
+}
+
+/// Used for deserializing the output of `proxmox-backup-manager version --output-format json`
+/// Only contains the properties we are interested in for simplicity sake.
+#[derive(Deserialize)]
+#[serde(rename_all(deserialize = "PascalCase"))]
+struct PbsVersionInfo {
+    /// Actual package name as reported by `proxmox-backup-manager`
+    package: String,
+    /// Installed package version as reported by `proxmox-backup-manager`
+    version: String,
+}
+
+/// 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 (optionally) sending
+    /// it to a specified server.
+    ///
+    /// # Arguments
+    ///
+    /// * `answer` - Answer file as provided by the user
+    fn gather(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(&PathBuf::from(proxmox_installer_common::RUNTIME_DIR))
+                .map_err(|err| anyhow!(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)?))?
+        };
+
+        with_chroot(|target_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(|| path.to_owned())
+            };
+
+            // 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)?))
+            };
+
+            let arch = run_cmd(&["dpkg", "--print-architecture"])?;
+
+            let kernel_version = run_cmd(&[
+                "dpkg-query",
+                "--showformat",
+                "${db:Status-Abbrev}|${Architecture}|${Package}\\n",
+                "--show",
+                "proxmox-kernel-[0-9]*",
+            ])
+            .and_then(|s| Self::parse_dpkg_query_kernel_output(&s, arch.trim()))?;
+
+            Ok(Self {
+                debian_version: read_file("/etc/debian_version")?,
+                product_version: Self::gather_product_version(&setup_info, &run_cmd)?,
+                kernel_version,
+                boot_type: run_env.boot_type,
+                filesystem: answer.disks.fs_type,
+                fqdn: answer.global.fqdn.to_string(),
+                machine_id: read_file("/etc/machine-id")?,
+                bootdisk: Self::gather_disks(&config, &run_env, &udev)?,
+                management_nic: 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<BootDiskInfo>> {
+        if config.filesys.is_lvm() {
+            Ok(udev
+                .disks
+                .values()
+                .find(|props| props.get("DEVNAME") == config.target_hd.as_ref())
+                .map(|props| BootDiskInfo {
+                    size: (config.hdsize * (SIZE_GIB as f64)) as usize,
+                    udev_properties: props.clone(),
+                })
+                .as_slice()
+                .to_vec())
+        } else {
+            Ok(config
+                .disk_selection
+                .values()
+                .filter_map(|index| Some(run_env.disks[index.parse::<usize>().ok()?].to_owned()))
+                .filter_map(|disk: Disk| {
+                    let props = udev
+                        .disks
+                        .values()
+                        .find(|props| props.get("DEVNAME") == Some(&disk.path))?;
+
+                    Some(BootDiskInfo {
+                        size: (config.hdsize * (SIZE_GIB as f64)) as usize,
+                        udev_properties: props.clone(),
+                    })
+                })
+                .collect())
+        }
+    }
+
+    /// 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<NetworkInterfaceInfo> {
+        let nic = run_env
+            .network
+            .interfaces
+            .get(&config.mngmt_nic)
+            .ok_or_else(|| anyhow!("could not find network interface '{}'", config.mngmt_nic))?;
+
+        // Use the actual IP address from the low-level install config, as the runtime info
+        // contains the original IP address from DHCP.
+        let address = config.cidr.clone();
+
+        let props = udev
+            .nics
+            .values()
+            .find(|props| props.get("INTERFACE") == Some(&nic.name))
+            .ok_or_else(|| anyhow!("could not find udev information for NIC '{}'", nic.name))?;
+
+        Ok(NetworkInterfaceInfo {
+            mac: nic.mac.clone(),
+            address,
+            udev_properties: props.clone(),
+        })
+    }
+
+    /// 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_version(
+        setup_info: &SetupInfo,
+        run_cmd: &dyn Fn(&[&str]) -> Result<String>,
+    ) -> Result<String> {
+        match setup_info.config.product {
+            ProxmoxProduct::PVE => run_cmd(&["pveversion"])?
+                .split_once(' ')
+                .map(|(ver, _)| ver.to_owned())
+                .ok_or(anyhow!("failed to parse `pveversion` output")),
+            ProxmoxProduct::PMG => run_cmd(&["pmgversion"])?
+                .split_once(' ')
+                .map(|(ver, _)| ver.to_owned())
+                .ok_or(anyhow!("failed to parse `pveversion` output")),
+            ProxmoxProduct::PBS => {
+                let info: Vec<PbsVersionInfo> = serde_json::from_str(&run_cmd(&[
+                    "proxmox-backup-manager",
+                    "version",
+                    "--output-format",
+                    "json",
+                ])?)
+                .context("failed to parse json output from 'proxmox-backup-manager'")?;
+
+                if info.is_empty() {
+                    bail!("got empty version information");
+                }
+
+                Ok(format!("{}/{}", info[0].package, info[0].version))
+            }
+        }
+    }
+
+    /// Tries to parses `dpkg-query` output generated from the following command:
+    /// dpkg-query --showformat '${db:Status-Abbrev}|${Architecture}|${Package}\n' --show 'proxmox-kernel-[0-9]*'
+    /// and report the version of the actual kernel package.
+    ///
+    /// 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
+    fn parse_dpkg_query_kernel_output(output: &str, dpkg_arch: &str) -> Result<String> {
+        for pkg in output.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 kernel package")
+    }
+}
+
+/// 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.
+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
+    let result = callback("/target");
+
+    let ec = Command::new("proxmox-chroot").arg("cleanup").status();
+    if ec.is_err() || !ec.map(|ec| ec.success()).unwrap_or(false) {
+        eprintln!("failed to clean up chroot for installed system");
+    }
+
+    result
+}
+
+fn do_main() -> Result<()> {
+    let answer = Answer::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 = serde_json::to_string(&PostHookInfo::gather(&answer)?)?;
+        proxmox_installer_common::http::post(url, cert_fingerprint.as_deref(), 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
+        }
+    }
+}
-- 
2.45.1



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  parent reply	other threads:[~2024-07-10 13:29 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-07-10 13:27 [pve-devel] [PATCH installer 00/14] fix #5536: implement post-(auto-)installation notification mechanism Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 01/14] chroot: print full anyhow message Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 02/14] tree-wide: fix some typos Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 03/14] tree-wide: collect hardcoded installer runtime directory strings into constant Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 04/14] common: simplify filesystem type serializing & Display trait impl Christoph Heiss
2024-07-11 14:32   ` Stefan Hanreich
2024-07-10 13:27 ` [pve-devel] [PATCH installer 05/14] common: setup: serialize `target_hd` as string explicitly Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 06/14] common: split out installer setup files loading functionality Christoph Heiss
2024-07-11 15:06   ` Stefan Hanreich
2024-07-10 13:27 ` [pve-devel] [PATCH installer 07/14] debian: strip unused library dependencies Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 08/14] fetch-answer: move http-related code to gated module in installer-common Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 09/14] tree-wide: convert some more crates to use workspace dependencies Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 10/14] auto-installer: tests: replace left/right with got/expected in output Christoph Heiss
2024-07-11 15:03   ` Stefan Hanreich
2024-07-10 13:27 ` [pve-devel] [PATCH installer 11/14] auto-installer: answer: add `posthook` section Christoph Heiss
2024-07-10 13:27 ` Christoph Heiss [this message]
2024-07-11 15:54   ` [pve-devel] [PATCH installer 12/14] fix #5536: add post-hook utility for sending notifications after auto-install Stefan Hanreich
2024-07-10 13:27 ` [pve-devel] [PATCH installer 13/14] fix #5536: post-hook: add some unit tests Christoph Heiss
2024-07-10 13:27 ` [pve-devel] [PATCH installer 14/14] unconfigured.sh: run proxmox-post-hook after successful auto-install Christoph Heiss
2024-07-11 16:49 ` [pve-devel] [PATCH installer 00/14] fix #5536: implement post-(auto-)installation notification mechanism Stefan Hanreich
2024-07-12  9:11   ` Christoph Heiss
2024-07-15 10:42 ` Thomas Lamprecht
2024-07-15 14:31   ` Christoph Heiss
2024-07-16 16:09     ` Thomas Lamprecht
2024-07-17  7:25       ` Christoph Heiss

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240710132756.1149508-13-c.heiss@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal