Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/agama-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ serde_with = "3.16.1"
gettext-rs = { version = "0.7.7", features = ["gettext-system"] }
merge = "0.2.0"
agama-proxy = { version = "0.1.0", path = "../agama-proxy" }
tempfile = "3.27.0"

[dev-dependencies]
test-context = "0.4.1"
Expand Down
9 changes: 7 additions & 2 deletions rust/agama-manager/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ use gettextrs::gettext;
use tokio::sync::RwLock;

use crate::{
bootloader, checks, files, hostname, iscsi, l10n, proxy, s390, security, service, software,
storage, users,
bootloader, checks, files, hostname, ipmi::Ipmi, iscsi, l10n, proxy, s390, security, service,
software, storage, users,
};

/// Implements the installation process.
Expand Down Expand Up @@ -422,6 +422,11 @@ impl FinishAction {
}

command.arg("now");

if let Err(e) = Ipmi::default().finished() {
tracing::error!("Ipmi command failed: {}", e);
Comment thread
mchf marked this conversation as resolved.
Outdated
}

match command.output() {
Ok(output) => {
if !output.status.success() {
Expand Down
129 changes: 129 additions & 0 deletions rust/agama-manager/src/ipmi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) [2026] SUSE LLC
//
// All Rights Reserved.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, contact SUSE LLC.
//
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::NamedTempFile;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("ipmitool not found")]
Tool { desc: String },
#[error("ipmitool command call failed")]
Command { desc: String },
}

pub struct Ipmi {
tool: PathBuf,
device: PathBuf,
}

impl Ipmi {
const IPMI_STARTED: u8 = 0x07;
const IPMI_FINISHED: u8 = 0x08;
const IPMI_ABORTED: u8 = 0x09;
const IPMI_FAILED: u8 = 0x0A;

pub fn new(dev: &Path, tool: &Path) -> Self {
let instance = Self {
tool: tool.to_path_buf(),
device: dev.to_path_buf(),
};

tracing::info!("IPMI available: {}", instance.is_available());

instance
}

pub fn started(&self) -> Result<(), Error> {
self.send_command(Self::IPMI_STARTED)
}

pub fn finished(&self) -> Result<(), Error> {
self.send_command(Self::IPMI_FINISHED)
}

pub fn aborted(&self) -> Result<(), Error> {
self.send_command(Self::IPMI_ABORTED)
}

pub fn failed(&self) -> Result<(), Error> {
self.send_command(Self::IPMI_FAILED)
}

fn is_available(&self) -> bool {
fs::metadata(&self.device).is_ok() && fs::metadata(&self.tool).is_ok()
}

fn send_command(&self, code: u8) -> Result<(), Error> {
if !self.is_available() {
return Ok(());
}

// Create a temporary file that is automatically deleted
let mut file = match NamedTempFile::new() {
Ok(f) => f,
Err(e) => {
return Err(Error::Tool {
desc: format!("ipmitool failed, cannot create temporary event file. {}", e),
});
}
};

// Write the event string
let content = format!("0x04 0x1F 0x00 0x6f 0x{:02x} 0x00 0x00\n", code);

if file.write_all(content.as_bytes()).is_err() {
return Err(Error::Tool {
desc: "ipmitool failed, cannot create event file".to_string(),
});
}

// Execute ipmitool
let status = Command::new(&self.tool)
.arg("event")
.arg("file")
.arg(file.path())
.status();

match status {
Ok(s) => {
if !s.success() {
Err(Error::Command {
desc: format!("ipmitool failed, exit code: {:?}", s.code()),
})
} else {
Ok(())
}
}
Err(e) => Err(Error::Command {
desc: format!("ipmitool failed, status: {}", e),
}),
}
}
}

impl Default for Ipmi {
fn default() -> Self {
Self::new(Path::new("/dev/ipmi0"), Path::new("/usr/bin/ipmitool"))
}
}
1 change: 1 addition & 0 deletions rust/agama-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub use service::Service;
pub mod message;

pub mod hardware;
pub mod ipmi;

pub use agama_bootloader as bootloader;
pub use agama_files as files;
Expand Down
20 changes: 17 additions & 3 deletions rust/agama-manager/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
// find current contact information at www.suse.com.

use crate::{
actions::FinishAction, bootloader, checks, files, hardware, hostname, iscsi, l10n, message,
network, proxy, s390, security, software, storage, tasks, users,
actions::FinishAction, bootloader, checks, files, hardware, hostname, ipmi, iscsi, l10n,
message, network, proxy, s390, security, software, storage, tasks, users,
};
use agama_users::PasswordCheckResult;
use agama_utils::{
Expand Down Expand Up @@ -97,6 +97,8 @@ pub enum Error {
Users(#[from] users::service::Error),
#[error(transparent)]
S390(#[from] s390::service::Error),
#[error(transparent)]
Ipmi(#[from] ipmi::Error),
}

pub struct Starter {
Expand Down Expand Up @@ -814,7 +816,19 @@ impl MessageHandler<message::RunAction> for Service {
self.probe_dasd().await?;
}
Action::Install => {
self.tasks.cast(tasks::message::Install)?;
let ipmi = ipmi::Ipmi::default();

if let Err(e) = ipmi.started() {
tracing::error!("Ipmi coommand failed: {}", e);
Comment thread
mchf marked this conversation as resolved.
Outdated
}

if let Err(error) = self.tasks.cast(tasks::message::Install) {
if let Err(e) = ipmi.failed() {
tracing::error!("Ipmi command failed: {}", e);
Comment thread
mchf marked this conversation as resolved.
Outdated
}

return Err(error);
}
}
Action::Finish(method) => {
checks::check_stage(&self.progress, Stage::Finished).await?;
Expand Down
Loading