use std::net::SocketAddr;
use std::sync::Arc;
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_async;
use crate::transport::AsyncTransport;
use crate::transport::tokio::UdpTransport;
use crate::types::{
ChannelAuthCapabilities, ChassisControl, ChassisStatus, DeviceId, PrivilegeLevel, RawResponse,
SelfTestResult, SystemGuid,
};
#[derive(Clone)]
pub struct Client {
inner: Arc<tokio::sync::Mutex<Inner>>,
managed_session_id: u32,
remote_session_id: u32,
}
struct Inner {
transport: Box<dyn AsyncTransport + 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 async 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 AsyncTransport + Send> =
Box::new(UdpTransport::connect(self.target, self.timeout, self.retries).await?);
let session = establish_session_async(
&*transport,
&username,
&password,
self.bmc_key.as_ref(),
self.privilege_level,
)
.await?;
let managed_session_id = session.managed_session_id;
let remote_session_id = session.remote_session_id;
Ok(Client {
inner: Arc::new(tokio::sync::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 async 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).await?;
command.parse_response(response)
}
pub async fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Result<RawResponse> {
let start = Instant::now();
let result = {
let mut inner = self.inner.lock().await;
send_raw_locked(&mut inner, netfn, cmd, data).await
};
let elapsed = start.elapsed();
match &result {
Ok(resp) => {
crate::observe::record_ok("async", netfn, cmd, elapsed, resp.completion_code)
}
Err(err) => crate::observe::record_err("async", netfn, cmd, elapsed, err),
}
result
}
pub async fn get_device_id(&self) -> Result<DeviceId> {
self.execute(GetDeviceId).await
}
pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
self.execute(GetSelfTestResults).await
}
pub async fn get_system_guid(&self) -> Result<SystemGuid> {
self.execute(GetSystemGuid).await
}
pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
self.execute(GetChassisStatus).await
}
pub async fn chassis_control(&self, control: ChassisControl) -> Result<()> {
self.execute(ChassisControlCommand { control }).await
}
pub async fn get_channel_auth_capabilities(
&self,
channel: u8,
privilege: PrivilegeLevel,
) -> Result<ChannelAuthCapabilities> {
let cmd = GetChannelAuthCapabilities::new(channel, privilege);
match self.execute(cmd).await {
Ok(caps) => Ok(caps),
Err(Error::CompletionCode { .. }) => self.execute(cmd.without_v2_data()).await,
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 async fn close_session(&self) -> Result<()> {
const NETFN_APP: u8 = 0x06;
const CMD_CLOSE_SESSION: u8 = 0x3C;
let mut inner = self.inner.lock().await;
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).await;
let elapsed = start.elapsed();
match &result {
Ok(resp) => crate::observe::record_ok(
"async",
NETFN_APP,
CMD_CLOSE_SESSION,
elapsed,
resp.completion_code,
),
Err(err) => {
crate::observe::record_err("async", 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(),
}
}
}
async 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).await?;
inner
.core
.decode_rmcpplus_ipmi_response(netfn, cmd, rq_seq, &response_bytes)
}
#[derive(Clone)]
pub struct AppService {
client: Client,
}
impl AppService {
pub async fn get_device_id(&self) -> Result<DeviceId> {
self.client.get_device_id().await
}
pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
self.client.get_self_test_results().await
}
pub async fn get_system_guid(&self) -> Result<SystemGuid> {
self.client.get_system_guid().await
}
pub async fn get_channel_auth_capabilities(
&self,
channel: u8,
privilege: PrivilegeLevel,
) -> Result<ChannelAuthCapabilities> {
self.client
.get_channel_auth_capabilities(channel, privilege)
.await
}
}
#[derive(Clone)]
pub struct ChassisService {
client: Client,
}
impl ChassisService {
pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
self.client.get_chassis_status().await
}
pub async fn chassis_control(&self, control: ChassisControl) -> Result<()> {
self.client.chassis_control(control).await
}
}
#[cfg(test)]
mod tests {
use core::future::Future;
use core::pin::Pin;
use super::*;
use crate::session::Session;
#[derive(Debug, Clone, Copy)]
struct TimeoutAsyncTransport;
impl AsyncTransport for TimeoutAsyncTransport {
fn send_recv<'a>(
&'a self,
_request: &'a [u8],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>> {
Box::pin(async { Err(Error::Timeout) })
}
}
fn dummy_session() -> Session {
Session::new_test(0x11223344, 0x55667788, false, false)
}
#[tokio::test(flavor = "current_thread")]
async 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(tokio::sync::Mutex::new(Inner {
transport: Box::new(TimeoutAsyncTransport),
core: ClientCore::new(session),
})),
managed_session_id,
remote_session_id,
};
client.close_session().await.expect("close_session");
let err = client
.get_device_id()
.await
.expect_err("expected session-closed error");
assert!(matches!(err, Error::Protocol("session is closed")));
}
}