extract related logic into an OkdIpxeScore
Some checks failed
Run Check Script / check (pull_request) Failing after 33s

This commit is contained in:
2025-08-29 09:52:11 -04:00
parent 7bb3602ab8
commit b857412151
10 changed files with 162 additions and 87 deletions

View File

@@ -69,6 +69,7 @@ base64.workspace = true
once_cell = "1.21.3"
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
askama = "0.14.0"
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -0,0 +1,148 @@
use askama::Template;
use async_trait::async_trait;
use derive_new::new;
use serde::Serialize;
use std::net::IpAddr;
use crate::{
data::{FileContent, FilePath, Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::{dhcp::DhcpScore, http::StaticFilesHttpScore, tftp::TftpScore},
score::Score,
topology::{DhcpServer, HttpServer, Router, TftpServer, Topology, Url},
};
#[derive(Debug, new, Clone, Serialize)]
pub struct OkdIpxeScore {
pub kickstart_filename: String,
pub harmony_inventory_agent: String,
pub cluster_pubkey_filename: String,
}
impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Score<T> for OkdIpxeScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(IpxeInterpret::new(self.clone()))
}
fn name(&self) -> String {
"OkdIpxeScore".to_string()
}
}
#[derive(Debug, new, Clone)]
pub struct IpxeInterpret {
score: OkdIpxeScore,
}
#[async_trait]
impl<T: Topology + DhcpServer + TftpServer + HttpServer + Router> Interpret<T> for IpxeInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let gateway_ip = topology.get_gateway();
let scores: Vec<Box<dyn Score<T>>> = vec![
Box::new(DhcpScore {
host_binding: vec![],
next_server: Some(topology.get_gateway()),
boot_filename: None,
filename: Some("undionly.kpxe".to_string()),
filename64: Some("ipxe.efi".to_string()),
filenameipxe: Some(format!("http://{gateway_ip}:8080/boot.ipxe").to_string()),
}),
Box::new(TftpScore {
files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()),
}),
Box::new(StaticFilesHttpScore {
// TODO The current russh based copy is way too slow, check for a lib update or use scp
// when available
//
// For now just run :
// scp -r data/pxe/okd/http_files/* root@192.168.1.1:/usr/local/http/
//
folder_to_serve: None,
// folder_to_serve: Some(Url::LocalFolder("./data/pxe/okd/http_files/".to_string())),
files: vec![
FileContent {
path: FilePath::Relative("boot.ipxe".to_string()),
content: BootIpxeTpl {
gateway_ip: &gateway_ip,
}
.to_string(),
},
FileContent {
path: FilePath::Relative(self.score.kickstart_filename.clone()),
content: InventoryKickstartTpl {
gateway_ip: &gateway_ip,
harmony_inventory_agent: &self.score.harmony_inventory_agent,
cluster_pubkey_filename: &self.score.cluster_pubkey_filename,
}
.to_string(),
},
FileContent {
path: FilePath::Relative("fallback.ipxe".to_string()),
content: FallbackIpxeTpl {
gateway_ip: &gateway_ip,
kickstart_filename: &self.score.kickstart_filename,
}
.to_string(),
},
],
}),
];
for score in scores {
let result = score.interpret(inventory, topology).await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => continue,
InterpretStatus::NOOP => continue,
_ => return Err(InterpretError::new(outcome.message)),
},
Err(e) => return Err(e),
};
}
Ok(Outcome::success("Ipxe installed".to_string()))
}
fn get_name(&self) -> InterpretName {
InterpretName::Ipxe
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}
#[derive(Template)]
#[template(path = "boot.ipxe.j2")]
struct BootIpxeTpl<'a> {
gateway_ip: &'a IpAddr,
}
#[derive(Template)]
#[template(path = "fallback.ipxe.j2")]
struct FallbackIpxeTpl<'a> {
gateway_ip: &'a IpAddr,
kickstart_filename: &'a str,
}
#[derive(Template)]
#[template(path = "inventory.kickstart.j2")]
struct InventoryKickstartTpl<'a> {
gateway_ip: &'a IpAddr,
cluster_pubkey_filename: &'a str,
harmony_inventory_agent: &'a str,
}

View File

@@ -2,5 +2,6 @@ pub mod bootstrap_dhcp;
pub mod bootstrap_load_balancer;
pub mod dhcp;
pub mod dns;
pub mod ipxe;
pub mod load_balancer;
pub mod upgrade;

View File

@@ -0,0 +1,6 @@
#!ipxe
set base-url http://{{ gateway_ip }}:8080
set hostfile ${base-url}/byMAC/01-${mac:hexhyp}.ipxe
chain ${hostfile} || chain ${base-url}/fallback.ipxe

View File

@@ -0,0 +1,40 @@
#!ipxe
# =================================================================
# Harmony Discovery Agent - Default Boot Script (default.ipxe)
# =================================================================
#
# This script boots the CentOS Stream live environment for the
# purpose of hardware inventory. It loads the kernel and initramfs
# directly and passes a Kickstart URL for full automation.
#
# --- Configuration
# Set the base URL for where the CentOS kernel/initrd are hosted.
set os_base_url http://{{gateway_ip}}:8080/os/centos-stream-9
# Set the URL for the Kickstart file.
set ks_url http://{{ gateway_ip }}:8080/{{ kickstart_filename }}
# --- Boot Process
echo "Harmony: Starting automated node discovery..."
echo "Fetching kernel from ${os_base_url}/vmlinuz..."
kernel ${os_base_url}/vmlinuz
echo "Fetching initramfs from ${os_base_url}/initrd.img..."
initrd ${os_base_url}/initrd.img
echo "Configuring kernel boot arguments..."
# Kernel Arguments Explained:
# - initrd=initrd.img: Specifies the initial ramdisk to use.
# - inst.stage2: Points to the OS source. For a live boot, the base URL is sufficient.
# - inst.ks: CRITICAL: Points to our Kickstart file for automation.
# - ip=dhcp: Ensures the live environment configures its network.
# - console=...: Provides boot output on both serial and graphical consoles for debugging.
imgargs vmlinuz initrd=initrd.img inst.sshd inst.stage2=${os_base_url} inst.ks=${ks_url} ip=dhcp console=ttyS0,115200 console=tty1
echo "Booting into CentOS Stream 9 live environment..."
boot || goto failed
:failed
echo "Boot failed. Dropping to iPXE shell."
shell

View File

@@ -0,0 +1,127 @@
# --- Pre-Boot Scripting (The Main Goal) ---
# This section runs after the live environment has booted into RAM.
# It sets up SSH and downloads/runs the harmony-inventory-agent.
%pre --log=/root/ks-pre.log
echo "Harmony Kickstart: Pre-boot script started."
# 1. Configure SSH Access for Root
# Create the .ssh directory and set correct permissions.
echo " - Setting up SSH authorized_keys for root..."
mkdir -p /root/.ssh
chmod 700 /root/.ssh
# Download the public key from the provisioning server.
# The -sS flags make curl silent but show errors. -L follows redirects.
curl -vSL "http://{{ gateway_ip }}:8080/{{ cluster_pubkey_filename }}" -o /root/.ssh/authorized_keys
if [ $? -ne 0 ]; then
echo " - ERROR: Failed to download SSH public key."
else
echo " - SSH key downloaded successfully."
chmod 600 /root/.ssh/authorized_keys
fi
# 2. Download the Harmony Inventory Agent
echo " - Downloading harmony-inventory-agent..."
curl -vSL "http://{{ gateway_ip }}:8080/{{ harmony_inventory_agent }}" -o /usr/bin/harmony-inventory-agent
if [ $? -ne 0 ]; then
echo " - ERROR: Failed to download harmony_inventory_agent."
else
echo " - Agent binary downloaded successfully."
chmod +x /usr/bin/harmony-inventory-agent
fi
# 3. Create a systemd service to run the agent persistently.
# This is the most robust method to ensure the agent stays running.
echo " - Creating systemd service for the agent..."
cat > /etc/systemd/system/harmony-agent.service << EOF
[Unit]
Description=Harmony Inventory Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/harmony-inventory-agent
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# 4. Enable and start the service
# The 'systemctl' commands will work correctly within the chroot environment of the %pre script.
echo " - Enabling and starting harmony-agent.service..."
systemctl daemon-reload
systemctl enable --now harmony-agent.service
# Check if the service started correctly
systemctl is-active --quiet harmony-agent.service
if [ $? -eq 0 ]; then
echo " - Harmony Inventory Agent service is now running."
else
echo " - ERROR: Harmony Inventory Agent service failed to start."
fi
echo "Harmony Kickstart: Pre-boot script finished. The machine is ready for inventory."
echo "Running cat - to pause system indefinitely"
cat -
%end
# =================================================================
# Harmony Discovery Agent - Kickstart File (NON-INSTALL, LIVE BOOT)
# =================================================================
#
# This file achieves a fully automated, non-interactive boot into a
# live CentOS environment. It does NOT install to disk.
#
# --- Automation and Interaction Control ---
# Perform the installation in command-line mode. This is critical for
# preventing Anaconda from starting a UI and halting for input.
cmdline
# Accept the End User License Agreement to prevent a prompt.
eula --agreed
# --- Core System Configuration (Required by Anaconda) ---
# Set keyboard and language. These are mandatory.
keyboard --vckeymap=us --xlayouts='us'
lang en_US.UTF-8
# Configure networking. This is essential for the %post script to work.
# The --activate flag ensures this device is brought up in the installer environment.
network --bootproto=dhcp --device=link --activate
# Set a locked root password. This is a mandatory command.
rootpw --lock
# Set the timezone. This is a mandatory command.
timezone UTC
# --- Disable Installation-Specific Features ---
# CRITICAL: Do not install a bootloader. The --disabled flag prevents
# this step and avoids errors about where to install it.
bootloader --disabled
# CRITICAL: Ignore all disks. This prevents Anaconda from stopping at the
# "Installation Destination" screen asking where to install.
# ignoredisk --drives /dev/sda
# Do not run the Initial Setup wizard on first boot.
firstboot --disable
# --- Package Selection ---
# We are not installing, so this section can be minimal.
# An empty %packages section is valid and ensures no time is wasted
# resolving dependencies for an installation that will not happen.
%packages
%end
# IMPORTANT: Do not include a final action command like 'reboot' or 'poweroff'.
# The default action is 'halt', which in cmdline mode will leave the system
# running in the live environment with the agent active, which is the desired state.