[pve-devel] [RFC PATCH installer 4/5] fix #5579: auto-installer: add optional first-boot hook script
Christoph Heiss
c.heiss at proxmox.com
Wed Nov 13 14:59:06 CET 2024
Users can specifying an optional file - either fetched from an URL or
backed into the ISO - to execute on the first boot after the
installation, using the 'proxmox-first-boot' oneshot service.
Essentially adds an (optional) `[first-boot]` section to the answer
file. If specified, the `source` key must be at least set, which gives
the location of the hook script.
Signed-off-by: Christoph Heiss <c.heiss at proxmox.com>
---
proxmox-auto-installer/Cargo.toml | 2 +-
proxmox-auto-installer/src/answer.rs | 27 ++++++++++++
.../src/bin/proxmox-auto-installer.rs | 42 +++++++++++++++++--
proxmox-auto-installer/src/utils.rs | 15 ++++++-
4 files changed, 80 insertions(+), 6 deletions(-)
diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml
index 21ed538..7e3d90c 100644
--- a/proxmox-auto-installer/Cargo.toml
+++ b/proxmox-auto-installer/Cargo.toml
@@ -13,7 +13,7 @@ homepage = "https://www.proxmox.com"
[dependencies]
anyhow.workspace = true
log.workspace = true
-proxmox-installer-common.workspace = true
+proxmox-installer-common = { workspace = true, features = ["http"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_plain.workspace = true
diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs
index c23f1f3..23d6878 100644
--- a/proxmox-auto-installer/src/answer.rs
+++ b/proxmox-auto-installer/src/answer.rs
@@ -22,6 +22,8 @@ pub struct Answer {
pub disks: Disks,
#[serde(default)]
pub posthook: Option<PostNotificationHookInfo>,
+ #[serde(default)]
+ pub first_boot: Option<FirstBootHookInfo>,
}
impl Answer {
@@ -62,6 +64,31 @@ pub struct PostNotificationHookInfo {
pub cert_fingerprint: Option<String>,
}
+/// Possible sources for the optional first-boot hook script/executable file.
+#[derive(Clone, Deserialize, Debug, PartialEq)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub enum FirstBootHookSourceMode {
+ /// Fetch the executable file from an URL, specified in the parent.
+ FromUrl,
+ /// The executable file has been baked into the ISO at a known location,
+ /// and should be retrieved from there.
+ FromIso,
+}
+
+/// Describes from where to fetch the first-boot hook script, either being baked into the ISO or
+/// from a URL.
+#[derive(Clone, Deserialize, Debug)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct FirstBootHookInfo {
+ /// Mode how to retrieve the first-boot executable file, either from an URL or from the ISO if
+ /// it has been baked-in.
+ pub source: FirstBootHookSourceMode,
+ /// Retrieve the post-install script from a URL, if source == "from-url".
+ pub url: Option<String>,
+ /// SHA256 cert fingerprint if certificate pinning should be used, if source == "from-url".
+ pub cert_fingerprint: Option<String>,
+}
+
#[derive(Clone, Deserialize, Debug, Default, PartialEq)]
#[serde(deny_unknown_fields)]
enum NetworkConfigMode {
diff --git a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
index ea45c29..9d8a2e5 100644
--- a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
+++ b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
@@ -1,18 +1,22 @@
use anyhow::{bail, format_err, Result};
use log::{error, info, LevelFilter};
use std::{
- env,
+ env, fs,
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_installer_common::{
+ http,
+ setup::{
+ installer_setup, read_json, spawn_low_level_installer, LocaleInfo, RuntimeInfo, SetupInfo,
+ },
+ FIRST_BOOT_EXEC_NAME, RUNTIME_DIR,
};
use proxmox_auto_installer::{
- answer::Answer,
+ answer::{Answer, FirstBootHookInfo, FirstBootSourceMode},
log::AutoInstLogger,
udevinfo::UdevInfo,
utils::{parse_answer, LowLevelMessage},
@@ -27,6 +31,31 @@ pub fn init_log() -> Result<()> {
.map_err(|err| format_err!(err))
}
+fn setup_first_boot_executable(first_boot: &FirstBootHookInfo) -> Result<()> {
+ let content = match first_boot.source {
+ FirstBootSourceMode::FromUrl => {
+ if let Some(url) = &first_boot.url {
+ info!("Fetching first-boot hook from {url} ..");
+ Some(http::get(url, first_boot.cert_fingerprint.as_deref())?)
+ } else {
+ bail!("first-boot hook source set to URL, but none specified!");
+ }
+ }
+ FirstBootSourceMode::FromIso => Some(fs::read_to_string(format!(
+ "/cdrom/{FIRST_BOOT_EXEC_NAME}"
+ ))?),
+ };
+
+ if let Some(content) = content {
+ Ok(fs::write(
+ format!("/{RUNTIME_DIR}/{FIRST_BOOT_EXEC_NAME}"),
+ content,
+ )?)
+ } else {
+ Ok(())
+ }
+}
+
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);
@@ -43,6 +72,11 @@ fn auto_installer_setup(in_test_mode: bool) -> Result<(Answer, UdevInfo)> {
};
let answer = Answer::try_from_reader(std::io::stdin().lock())?;
+
+ if let Some(first_boot) = &answer.first_boot {
+ setup_first_boot_executable(first_boot)?;
+ }
+
Ok((answer, udev_info))
}
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 83f3f12..25f537d 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -5,7 +5,7 @@ use log::info;
use std::{collections::BTreeMap, process::Command};
use crate::{
- answer::{self, Answer},
+ answer::{self, Answer, FirstBootSourceMode},
udevinfo::UdevInfo,
};
use proxmox_installer_common::{
@@ -320,6 +320,18 @@ fn verify_email_and_root_password_settings(answer: &Answer) -> Result<()> {
}
}
+fn verify_first_boot_settings(answer: &Answer) -> Result<()> {
+ info!("Verifying first boot settings");
+
+ if let Some(first_boot) = &answer.first_boot {
+ if first_boot.source == FirstBootSourceMode::FromUrl && first_boot.url.is_none() {
+ bail!("first-boot executable source set to URL, but none specified!");
+ }
+ }
+
+ Ok(())
+}
+
pub fn parse_answer(
answer: &Answer,
udev_info: &UdevInfo,
@@ -336,6 +348,7 @@ pub fn parse_answer(
verify_locale_settings(answer, locales)?;
verify_email_and_root_password_settings(answer)?;
+ verify_first_boot_settings(answer)?;
let mut config = InstallConfig {
autoreboot: 1_usize,
--
2.47.0
More information about the pve-devel
mailing list