[pve-devel] [PATCH installer v2 4/5] fix #5579: auto-installer: add optional first-boot hook script
Christoph Heiss
c.heiss at proxmox.com
Mon Nov 18 13:38:40 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>
---
Changes v1 -> v2:
* adapt to new low-level format
* add settable ordering of service
proxmox-auto-installer/Cargo.toml | 2 +-
proxmox-auto-installer/src/answer.rs | 59 +++++++++++++++++++
.../src/bin/proxmox-auto-installer.rs | 49 +++++++++++++--
proxmox-auto-installer/src/utils.rs | 22 ++++++-
4 files changed, 126 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 73e5869..c206fcc 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 post_installation_webhook: Option<PostNotificationHookInfo>,
+ #[serde(default)]
+ pub first_boot: Option<FirstBootHookInfo>,
}
impl Answer {
@@ -62,6 +64,63 @@ 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,
+}
+
+/// Possible orderings for the `proxmox-first-boot` systemd service.
+///
+/// Determines the final value of `Unit.Before` and `Unit.Wants` in the service
+/// file.
+// Must be kept in sync with Proxmox::Install::Config and the service files in the
+// proxmox-first-boot package.
+#[derive(Clone, Default, Deserialize, Debug, PartialEq)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub enum FirstBootHookServiceOrdering {
+ /// Needed for bringing up the network itself, runs before any networking is attempted.
+ BeforeNetwork,
+ /// Network needs to be already online, runs after networking was brought up.
+ NetworkOnline,
+ /// Runs after the system has successfully booted up completely.
+ #[default]
+ FullyUp,
+}
+
+impl FirstBootHookServiceOrdering {
+ /// Maps the enum to the appropriate systemd target name, without the '.target' suffix.
+ pub fn as_systemd_target_name(&self) -> &str {
+ match self {
+ FirstBootHookServiceOrdering::BeforeNetwork => "network-pre",
+ FirstBootHookServiceOrdering::NetworkOnline => "network-online",
+ FirstBootHookServiceOrdering::FullyUp => "multi-user",
+ }
+ }
+}
+
+/// 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,
+ /// Determines the service order when the hook will run on first boot.
+ #[serde(default)]
+ pub ordering: FirstBootHookServiceOrdering,
+ /// 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..4b9d73d 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_MAX_SIZE, FIRST_BOOT_EXEC_NAME, RUNTIME_DIR,
};
use proxmox_auto_installer::{
- answer::Answer,
+ answer::{Answer, FirstBootHookInfo, FirstBootHookSourceMode},
log::AutoInstLogger,
udevinfo::UdevInfo,
utils::{parse_answer, LowLevelMessage},
@@ -27,6 +31,38 @@ 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 {
+ FirstBootHookSourceMode::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!");
+ }
+ }
+ FirstBootHookSourceMode::FromIso => Some(fs::read_to_string(format!(
+ "/cdrom/{FIRST_BOOT_EXEC_NAME}"
+ ))?),
+ };
+
+ if let Some(content) = content {
+ if content.len() > FIRST_BOOT_EXEC_MAX_SIZE {
+ bail!(
+ "Maximum file size for first-boot executable file is {} MiB",
+ FIRST_BOOT_EXEC_MAX_SIZE / 1024 / 1024
+ )
+ }
+
+ 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 +79,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 9c399a5..ea7176a 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, FirstBootHookSourceMode},
udevinfo::UdevInfo,
};
use proxmox_installer_common::{
@@ -325,6 +325,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 == FirstBootHookSourceMode::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,
@@ -341,6 +353,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,
@@ -419,6 +432,13 @@ pub fn parse_answer(
})
}
}
+
+ if let Some(first_boot) = &answer.first_boot {
+ config.first_boot.enabled = true;
+ config.first_boot.ordering_target =
+ Some(first_boot.ordering.as_systemd_target_name().to_owned());
+ }
+
Ok(config)
}
--
2.47.0
More information about the pve-devel
mailing list