use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::time::Instant;
use crate::client::core::ClientCore;
use crate::commands::{
ChassisControlCommand, Command, GetChannelAuthCapabilities, GetChassisStatus, GetDeviceId,
GetSelfTestResults, GetSystemGuid,
};
use crate::crypto::SecretBytes;
use crate::error::{Error, Result};
use crate::session::establish_session;
use crate::transport::Transport;
use crate::transport::blocking::UdpTransport;
use crate::types::{
ChannelAuthCapabilities, ChassisControl, ChassisStatus, DeviceId, PrivilegeLevel, RawResponse,
SelfTestResult, SystemGuid,
};
#[derive(Clone)]
pub struct Client {
inner: Arc<Mutex<Inner>>,
managed_session_id: u32,
remote_session_id: u32,
}
struct Inner {
transport: Box<dyn Transport + Send>,
core: ClientCore,
}
#[derive(Debug)]
pub struct ClientBuilder {
target: SocketAddr,
username: Option<Vec<u8>>,
password: Option<SecretBytes>,
bmc_key: Option<SecretBytes>,
privilege_level: PrivilegeLevel,
timeout: Duration,
retries: u32,
}
impl ClientBuilder {
pub fn new(target: SocketAddr) -> Self {
Self {
target,
username: None,
password: None,
bmc_key: None,
privilege_level: PrivilegeLevel::Administrator,
timeout: Duration::from_secs(1),
retries: 3,
}
}
pub fn username_bytes(mut self, username: impl Into<Vec<u8>>) -> Self {
self.username = Some(username.into());
self
}
pub fn username(mut self, username: impl AsRef<str>) -> Self {
self.username = Some(username.as_ref().as_bytes().to_vec());
self
}
pub fn password_bytes(mut self, password: impl Into<Vec<u8>>) -> Self {
self.password = Some(SecretBytes::new(password.into()));
self
}
pub fn password(mut self, password: impl AsRef<str>) -> Self {
self.password = Some(SecretBytes::new(password.as_ref().as_bytes().to_vec()));
self
}
pub fn bmc_key_bytes(mut self, kg: impl Into<Vec<u8>>) -> Self {
self.bmc_key = Some(SecretBytes::new(kg.into()));
self
}
pub fn bmc_key(mut self, kg: impl AsRef<str>) -> Self {
self.bmc_key = Some(SecretBytes::new(kg.as_ref().as_bytes().to_vec()));
self
}
pub fn privilege_level(mut self, level: PrivilegeLevel) -> Self {
self.privilege_level = level;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn retries(mut self, attempts: u32) -> Self {
self.retries = attempts;
self
}
pub fn build(self) -> Result<Client> {
let username = self
.username
.ok_or(Error::Protocol("username is required"))?;
let password = self
.password
.ok_or(Error::Protocol("password is required"))?;
if username.len() > 16 {
return Err(Error::InvalidArgument(
"username longer than 16 bytes is not widely supported",
));
}
let transport: Box<dyn Transport + Send> = Box::new(UdpTransport::connect(
self.target,
self.timeout,
self.retries,
)?);
let session = establish_session(
&*transport,
&username,
&password,
self.bmc_key.as_ref(),
self.privilege_level,
)?;
let managed_session_id = session.managed_session_id;
let remote_session_id = session.remote_session_id;
Ok(Client {
inner: Arc::new(Mutex::new(Inner {
transport,
core: ClientCore::new(session),
})),
managed_session_id,
remote_session_id,
})
}
}
impl Client {
pub fn builder(target: SocketAddr) -> ClientBuilder {
ClientBuilder::new(target)
}
pub fn execute<C: Command>(&self, command: C) -> Result<C::Output> {
let request_data = command.request_data();
let response = self.send_raw(C::NETFN, C::CMD, &request_data)?;
command.parse_response(response)
}
pub fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Result<RawResponse> {
let start = Instant::now();
let result = {
let mut inner = self.lock_inner()?;
send_raw_locked(&mut inner, netfn, cmd, data)
};
let elapsed = start.elapsed();
match &result {
Ok(resp) => {
crate::observe::record_ok("blocking", netfn, cmd, elapsed, resp.completion_code)
}
Err(err) => crate::observe::record_err("blocking", netfn, cmd, elapsed, err),
}
result
}
pub fn get_device_id(&self) -> Result<DeviceId> {
self.execute(GetDeviceId)
}
pub fn get_self_test_results(&self) -> Result<SelfTestResult> {
self.execute(GetSelfTestResults)
}
pub fn get_system_guid(&self) -> Result<SystemGuid> {
self.execute(GetSystemGuid)
}
pub fn get_chassis_status(&self) -> Result<ChassisStatus> {
self.execute(GetChassisStatus)
}
pub fn chassis_control(&self, control: ChassisControl) -> Result<()> {
self.execute(ChassisControlCommand { control })
}
pub fn get_channel_auth_capabilities(
&self,
channel: u8,
privilege: PrivilegeLevel,
) -> Result<ChannelAuthCapabilities> {
let cmd = GetChannelAuthCapabilities::new(channel, privilege);
match self.execute(cmd) {
Ok(caps) => Ok(caps),
Err(Error::CompletionCode { .. }) => self.execute(cmd.without_v2_data()),
Err(e) => Err(e),
}
}
pub fn managed_session_id(&self) -> u32 {
self.managed_session_id
}
pub fn remote_session_id(&self) -> u32 {
self.remote_session_id
}
pub fn close_session(&self) -> Result<()> {
const NETFN_APP: u8 = 0x06;
const CMD_CLOSE_SESSION: u8 = 0x3C;
let mut inner = self.lock_inner()?;
if inner.core.is_closed() {
return Ok(());
}
let session_id = inner.core.managed_session_id_bytes_le();
let start = Instant::now();
let result = send_raw_locked(&mut inner, NETFN_APP, CMD_CLOSE_SESSION, &session_id);
let elapsed = start.elapsed();
match &result {
Ok(resp) => crate::observe::record_ok(
"blocking",
NETFN_APP,
CMD_CLOSE_SESSION,
elapsed,
resp.completion_code,
),
Err(err) => {
crate::observe::record_err("blocking", NETFN_APP, CMD_CLOSE_SESSION, elapsed, err)
}
}
match result {
Ok(resp) => {
if resp.completion_code != 0x00 && resp.completion_code != 0x87 {
inner.core.mark_closed();
return Err(Error::CompletionCode {
completion_code: resp.completion_code,
});
}
inner.core.mark_closed();
Ok(())
}
Err(Error::Timeout) => {
inner.core.mark_closed();
Ok(())
}
Err(e) => {
inner.core.mark_closed();
Err(e)
}
}
}
pub fn app(&self) -> AppService {
AppService {
client: self.clone(),
}
}
pub fn chassis(&self) -> ChassisService {
ChassisService {
client: self.clone(),
}
}
fn lock_inner(&self) -> Result<std::sync::MutexGuard<'_, Inner>> {
self.inner
.lock()
.map_err(|_| Error::Protocol("client lock poisoned"))
}
}
fn send_raw_locked(inner: &mut Inner, netfn: u8, cmd: u8, data: &[u8]) -> Result<RawResponse> {
let (rq_seq, packet) = inner.core.build_rmcpplus_ipmi_request(netfn, cmd, data)?;
let response_bytes = inner.transport.send_recv(&packet)?;
inner
.core
.decode_rmcpplus_ipmi_response(netfn, cmd, rq_seq, &response_bytes)
}
#[derive(Clone)]
pub struct AppService {
client: Client,
}
impl AppService {
pub fn get_device_id(&self) -> Result<DeviceId> {
self.client.get_device_id()
}
pub fn get_self_test_results(&self) -> Result<SelfTestResult> {
self.client.get_self_test_results()
}
pub fn get_system_guid(&self) -> Result<SystemGuid> {
self.client.get_system_guid()
}
pub fn get_channel_auth_capabilities(
&self,
channel: u8,
privilege: PrivilegeLevel,
) -> Result<ChannelAuthCapabilities> {
self.client
.get_channel_auth_capabilities(channel, privilege)
}
}
#[derive(Clone)]
pub struct ChassisService {
client: Client,
}
impl ChassisService {
pub fn get_chassis_status(&self) -> Result<ChassisStatus> {
self.client.get_chassis_status()
}
pub fn chassis_control(&self, control: ChassisControl) -> Result<()> {
self.client.chassis_control(control)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::Session;
#[derive(Debug, Clone, Copy)]
struct TimeoutTransport;
impl Transport for TimeoutTransport {
fn send_recv(&self, _request: &[u8]) -> Result<Vec<u8>> {
Err(Error::Timeout)
}
}
fn dummy_session() -> Session {
Session::new_test(0x11223344, 0x55667788, false, false)
}
#[test]
fn close_session_timeout_marks_client_closed() {
let session = dummy_session();
let managed_session_id = session.managed_session_id;
let remote_session_id = session.remote_session_id;
let client = Client {
inner: Arc::new(Mutex::new(Inner {
transport: Box::new(TimeoutTransport),
core: ClientCore::new(session),
})),
managed_session_id,
remote_session_id,
};
client.close_session().expect("close_session");
let err = client
.get_device_id()
.expect_err("expected session-closed error");
assert!(matches!(err, Error::Protocol("session is closed")));
}
}