auto-installer: add fetch answer binary

it is supposed to be run first and fetch an answer file.

The initial implementation searches for a partition/filesystem called
'proxmoxinst' or 'PROXMOXINST' with an 'answer.toml' file in the root
directory.

Once it has an answer file, it will call the 'proxmox-auto-installer'
and pipe in the contents via stdin.

Tested-by: Christoph Heiss <c.heiss@proxmox.com>
Reviewed-by: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Aaron Lauterer 2024-04-17 14:30:47 +02:00 committed by Thomas Lamprecht
parent 781f879edc
commit 2949f82539
6 changed files with 190 additions and 1 deletions

View File

@ -20,6 +20,7 @@ PREFIX = /usr
BINDIR = $(PREFIX)/bin
USR_BIN := \
proxmox-tui-installer\
proxmox-fetch-answer\
proxmox-auto-installer
COMPILED_BINS := \
@ -121,7 +122,8 @@ $(COMPILED_BINS): cargo-build
.PHONY: cargo-build
cargo-build:
$(CARGO) build --package proxmox-tui-installer --bin proxmox-tui-installer \
--package proxmox-auto-installer --bin proxmox-auto-installer $(CARGO_BUILD_ARGS)
--package proxmox-auto-installer --bin proxmox-auto-installer \
--bin proxmox-fetch-answer $(CARGO_BUILD_ARGS)
%-banner.png: %-banner.svg
rsvg-convert -o $@ $<

View File

@ -0,0 +1,71 @@
use anyhow::{anyhow, Error, Result};
use log::{error, info, LevelFilter};
use proxmox_auto_installer::{fetch_plugins::partition::FetchFromPartition, log::AutoInstLogger};
use std::io::Write;
use std::process::{Command, ExitCode, Stdio};
static LOGGER: AutoInstLogger = AutoInstLogger;
pub fn init_log() -> Result<()> {
AutoInstLogger::init("/tmp/fetch_answer.log")?;
log::set_logger(&LOGGER)
.map(|()| log::set_max_level(LevelFilter::Info))
.map_err(|err| anyhow!(err))
}
fn fetch_answer() -> Result<String> {
match FetchFromPartition::get_answer() {
Ok(answer) => return Ok(answer),
Err(err) => info!("Fetching answer file from partition failed: {err}"),
}
// TODO: add more options to get an answer file, e.g. download from url where url could be
// fetched via txt records on predefined subdomain, kernel param, dhcp option, ...
Err(Error::msg("Could not find any answer file!"))
}
fn main() -> ExitCode {
if let Err(err) = init_log() {
panic!("could not initialize logging: {err}");
}
info!("Fetching answer file");
let answer = match fetch_answer() {
Ok(answer) => answer,
Err(err) => {
error!("Aborting: {}", err);
return ExitCode::FAILURE;
}
};
let mut child = match Command::new("proxmox-auto-installer")
.stdout(Stdio::inherit())
.stdin(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => child,
Err(err) => panic!("Failed to start automatic installation: {err}"),
};
let mut stdin = child.stdin.take().expect("Failed to open stdin");
std::thread::spawn(move || {
stdin
.write_all(answer.as_bytes())
.expect("Failed to write to stdin");
});
match child.wait() {
Ok(status) => {
if status.success() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE // Will be trapped
}
}
Err(err) => {
error!("Auto installer exited: {err}");
ExitCode::FAILURE
}
}
}

View File

@ -0,0 +1,2 @@
pub mod partition;
mod utils;

View File

@ -0,0 +1,32 @@
use anyhow::{Error, Result};
use std::{fs::read_to_string, path::Path};
use log::info;
use crate::fetch_plugins::utils::mount_proxmoxinst_part;
static ANSWER_FILE: &str = "answer.toml";
pub struct FetchFromPartition;
impl FetchFromPartition {
/// Returns the contents of the answer file
pub fn get_answer() -> Result<String> {
info!("Checking for answer file on partition.");
let mount_path = mount_proxmoxinst_part()?;
let answer = Self::get_answer_file(&mount_path)?;
info!("Found answer file on partition.");
Ok(answer)
}
/// Searches for answer file and returns contents if found
fn get_answer_file(mount_path: &str) -> Result<String> {
let answer_path = Path::new(mount_path).join(ANSWER_FILE);
match answer_path.try_exists() {
Ok(true) => Ok(read_to_string(answer_path)?),
_ => Err(Error::msg(format!(
"could not find answer file expected at: {}",
answer_path.display()
))),
}
}
}

View File

@ -0,0 +1,81 @@
use anyhow::{bail, Error, Result};
use log::{info, warn};
use std::{
fs::{self, create_dir_all},
path::{Path, PathBuf},
process::Command,
};
static ANSWER_MP: &str = "/mnt/answer";
static PARTLABEL: &str = "proxmoxinst";
static SEARCH_PATH: &str = "/dev/disk/by-label";
/// Searches for upper and lower case existence of the partlabel in the search_path
///
/// # Arguemnts
/// * `partlabel_source` - Partition Label, used as upper and lower case
/// * `search_path` - Path where to search for the partiiton label
pub fn scan_partlabels(partlabel_source: &str, search_path: &str) -> Result<PathBuf> {
let partlabel = partlabel_source.to_uppercase();
let path = Path::new(search_path).join(&partlabel);
match path.try_exists() {
Ok(true) => {
info!("Found partition with label '{partlabel}'");
return Ok(path);
}
Ok(false) => info!("Did not detect partition with label '{partlabel}'"),
Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err),
}
let partlabel = partlabel_source.to_lowercase();
let path = Path::new(search_path).join(&partlabel);
match path.try_exists() {
Ok(true) => {
info!("Found partition with label '{partlabel}'");
return Ok(path);
}
Ok(false) => info!("Did not detect partition with label '{partlabel}'"),
Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err),
}
bail!("Could not detect upper or lower case labels for '{partlabel_source}'");
}
/// Will search and mount a partition/FS labeled proxmoxinst in lower or uppercase to ANSWER_MP;
pub fn mount_proxmoxinst_part() -> Result<String> {
if let Ok(true) = check_if_mounted(ANSWER_MP) {
info!("Skipping: '{ANSWER_MP}' is already mounted.");
return Ok(ANSWER_MP.into());
}
let part_path = scan_partlabels(PARTLABEL, SEARCH_PATH)?;
info!("Mounting partition at {ANSWER_MP}");
// create dir for mountpoint
create_dir_all(ANSWER_MP)?;
match Command::new("mount")
.args(["-o", "ro"])
.arg(part_path)
.arg(ANSWER_MP)
.output()
{
Ok(output) => {
if output.status.success() {
Ok(ANSWER_MP.into())
} else {
warn!("Error mounting: {}", String::from_utf8(output.stderr)?);
Ok(ANSWER_MP.into())
}
}
Err(err) => Err(Error::msg(format!("Error mounting: {err}"))),
}
}
fn check_if_mounted(target_path: &str) -> Result<bool> {
let mounts = fs::read_to_string("/proc/mounts")?;
for line in mounts.lines() {
if let Some(mp) = line.split(' ').nth(1) {
if mp == target_path {
return Ok(true);
}
}
}
Ok(false)
}

View File

@ -1,4 +1,5 @@
pub mod answer;
pub mod fetch_plugins;
pub mod log;
pub mod udevinfo;
pub mod utils;