ipmi/client/
tokio.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3use std::time::Duration;
4use std::time::Instant;
5
6use crate::client::core::ClientCore;
7use crate::commands::{
8    ChassisControlCommand, Command, GetChannelAuthCapabilities, GetChassisStatus, GetDeviceId,
9    GetSelfTestResults, GetSystemGuid,
10};
11use crate::crypto::SecretBytes;
12use crate::error::{Error, Result};
13use crate::session::establish_session_async;
14use crate::transport::AsyncTransport;
15use crate::transport::tokio::UdpTransport;
16use crate::types::{
17    ChannelAuthCapabilities, ChassisControl, ChassisStatus, DeviceId, PrivilegeLevel, RawResponse,
18    SelfTestResult, SystemGuid,
19};
20
21/// A tokio-based IPMI v2.0 RMCP+ client.
22#[derive(Clone)]
23pub struct Client {
24    inner: Arc<tokio::sync::Mutex<Inner>>,
25    managed_session_id: u32,
26    remote_session_id: u32,
27}
28
29struct Inner {
30    transport: Box<dyn AsyncTransport + Send>,
31    core: ClientCore,
32}
33
34/// Builder for [`Client`].
35#[derive(Debug)]
36pub struct ClientBuilder {
37    target: SocketAddr,
38    username: Option<Vec<u8>>,
39    password: Option<SecretBytes>,
40    bmc_key: Option<SecretBytes>,
41    privilege_level: PrivilegeLevel,
42    timeout: Duration,
43    retries: u32,
44}
45
46impl ClientBuilder {
47    /// Create a new builder.
48    pub fn new(target: SocketAddr) -> Self {
49        Self {
50            target,
51            username: None,
52            password: None,
53            bmc_key: None,
54            privilege_level: PrivilegeLevel::Administrator,
55            timeout: Duration::from_secs(1),
56            retries: 3,
57        }
58    }
59
60    /// Set the username (bytes).
61    ///
62    /// IPMI usernames are ASCII in most deployments, but the protocol treats them as raw bytes.
63    pub fn username_bytes(mut self, username: impl Into<Vec<u8>>) -> Self {
64        self.username = Some(username.into());
65        self
66    }
67
68    /// Set the username (UTF-8 string). This is a convenience wrapper around [`Self::username_bytes`].
69    pub fn username(mut self, username: impl AsRef<str>) -> Self {
70        self.username = Some(username.as_ref().as_bytes().to_vec());
71        self
72    }
73
74    /// Set the password (bytes).
75    pub fn password_bytes(mut self, password: impl Into<Vec<u8>>) -> Self {
76        self.password = Some(SecretBytes::new(password.into()));
77        self
78    }
79
80    /// Set the password (UTF-8 string). This is a convenience wrapper around [`Self::password_bytes`].
81    pub fn password(mut self, password: impl AsRef<str>) -> Self {
82        self.password = Some(SecretBytes::new(password.as_ref().as_bytes().to_vec()));
83        self
84    }
85
86    /// Set the optional BMC key (`Kg`) for "two-key" logins.
87    ///
88    /// If not set, the password key is used ("one-key" login), which is common in many BMC default configs.
89    pub fn bmc_key_bytes(mut self, kg: impl Into<Vec<u8>>) -> Self {
90        self.bmc_key = Some(SecretBytes::new(kg.into()));
91        self
92    }
93
94    /// Set the optional BMC key (`Kg`) for "two-key" logins (UTF-8 string).
95    pub fn bmc_key(mut self, kg: impl AsRef<str>) -> Self {
96        self.bmc_key = Some(SecretBytes::new(kg.as_ref().as_bytes().to_vec()));
97        self
98    }
99
100    /// Set requested session privilege level.
101    pub fn privilege_level(mut self, level: PrivilegeLevel) -> Self {
102        self.privilege_level = level;
103        self
104    }
105
106    /// Set UDP read timeout.
107    pub fn timeout(mut self, timeout: Duration) -> Self {
108        self.timeout = timeout;
109        self
110    }
111
112    /// Set number of send attempts per request (including the first attempt).
113    pub fn retries(mut self, attempts: u32) -> Self {
114        self.retries = attempts;
115        self
116    }
117
118    /// Establish the session and build the [`Client`].
119    pub async fn build(self) -> Result<Client> {
120        let username = self
121            .username
122            .ok_or(Error::Protocol("username is required"))?;
123        let password = self
124            .password
125            .ok_or(Error::Protocol("password is required"))?;
126
127        if username.len() > 16 {
128            return Err(Error::InvalidArgument(
129                "username longer than 16 bytes is not widely supported",
130            ));
131        }
132
133        let transport: Box<dyn AsyncTransport + Send> =
134            Box::new(UdpTransport::connect(self.target, self.timeout, self.retries).await?);
135
136        let session = establish_session_async(
137            &*transport,
138            &username,
139            &password,
140            self.bmc_key.as_ref(),
141            self.privilege_level,
142        )
143        .await?;
144
145        let managed_session_id = session.managed_session_id;
146        let remote_session_id = session.remote_session_id;
147
148        Ok(Client {
149            inner: Arc::new(tokio::sync::Mutex::new(Inner {
150                transport,
151                core: ClientCore::new(session),
152            })),
153            managed_session_id,
154            remote_session_id,
155        })
156    }
157}
158
159impl Client {
160    /// Create a [`ClientBuilder`].
161    pub fn builder(target: SocketAddr) -> ClientBuilder {
162        ClientBuilder::new(target)
163    }
164
165    /// Execute a typed command (single request/response).
166    pub async fn execute<C: Command>(&self, command: C) -> Result<C::Output> {
167        let request_data = command.request_data();
168        let response = self.send_raw(C::NETFN, C::CMD, &request_data).await?;
169        command.parse_response(response)
170    }
171
172    /// Send a raw IPMI request and return the raw response.
173    pub async fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Result<RawResponse> {
174        let start = Instant::now();
175        let result = {
176            let mut inner = self.inner.lock().await;
177            send_raw_locked(&mut inner, netfn, cmd, data).await
178        };
179        let elapsed = start.elapsed();
180        match &result {
181            Ok(resp) => {
182                crate::observe::record_ok("async", netfn, cmd, elapsed, resp.completion_code)
183            }
184            Err(err) => crate::observe::record_err("async", netfn, cmd, elapsed, err),
185        }
186        result
187    }
188
189    /// Convenience wrapper for `Get Device ID` (App NetFn, cmd 0x01).
190    pub async fn get_device_id(&self) -> Result<DeviceId> {
191        self.execute(GetDeviceId).await
192    }
193
194    /// Convenience wrapper for `Get Self Test Results` (App NetFn, cmd 0x04).
195    pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
196        self.execute(GetSelfTestResults).await
197    }
198
199    /// Convenience wrapper for `Get System GUID` (App NetFn, cmd 0x37).
200    pub async fn get_system_guid(&self) -> Result<SystemGuid> {
201        self.execute(GetSystemGuid).await
202    }
203
204    /// Convenience wrapper for `Get Chassis Status` (Chassis NetFn, cmd 0x01).
205    pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
206        self.execute(GetChassisStatus).await
207    }
208
209    /// Run `Chassis Control` (Chassis NetFn, cmd 0x02).
210    pub async fn chassis_control(&self, control: ChassisControl) -> Result<()> {
211        self.execute(ChassisControlCommand { control }).await
212    }
213
214    /// Convenience wrapper for `Get Channel Authentication Capabilities`
215    /// (App NetFn, cmd 0x38).
216    pub async fn get_channel_auth_capabilities(
217        &self,
218        channel: u8,
219        privilege: PrivilegeLevel,
220    ) -> Result<ChannelAuthCapabilities> {
221        let cmd = GetChannelAuthCapabilities::new(channel, privilege);
222        match self.execute(cmd).await {
223            Ok(caps) => Ok(caps),
224            Err(Error::CompletionCode { .. }) => self.execute(cmd.without_v2_data()).await,
225            Err(e) => Err(e),
226        }
227    }
228
229    /// Return the managed system (BMC) session ID (SIDC).
230    pub fn managed_session_id(&self) -> u32 {
231        self.managed_session_id
232    }
233
234    /// Return the remote console session ID (SIDM).
235    pub fn remote_session_id(&self) -> u32 {
236        self.remote_session_id
237    }
238
239    /// Close the active RMCP+ session (App NetFn, cmd 0x3C).
240    ///
241    /// This is a best-effort operation. If the BMC does not respond (timeout) the client still
242    /// transitions to a locally closed state and will reject further requests.
243    pub async fn close_session(&self) -> Result<()> {
244        const NETFN_APP: u8 = 0x06;
245        const CMD_CLOSE_SESSION: u8 = 0x3C;
246
247        let mut inner = self.inner.lock().await;
248        if inner.core.is_closed() {
249            return Ok(());
250        }
251
252        let session_id = inner.core.managed_session_id_bytes_le();
253        let start = Instant::now();
254        let result = send_raw_locked(&mut inner, NETFN_APP, CMD_CLOSE_SESSION, &session_id).await;
255        let elapsed = start.elapsed();
256        match &result {
257            Ok(resp) => crate::observe::record_ok(
258                "async",
259                NETFN_APP,
260                CMD_CLOSE_SESSION,
261                elapsed,
262                resp.completion_code,
263            ),
264            Err(err) => {
265                crate::observe::record_err("async", NETFN_APP, CMD_CLOSE_SESSION, elapsed, err)
266            }
267        }
268
269        match result {
270            Ok(resp) => {
271                if resp.completion_code != 0x00 && resp.completion_code != 0x87 {
272                    inner.core.mark_closed();
273                    return Err(Error::CompletionCode {
274                        completion_code: resp.completion_code,
275                    });
276                }
277                inner.core.mark_closed();
278                Ok(())
279            }
280            Err(Error::Timeout) => {
281                inner.core.mark_closed();
282                Ok(())
283            }
284            Err(e) => {
285                inner.core.mark_closed();
286                Err(e)
287            }
288        }
289    }
290
291    /// A service-style grouping for App netfn commands.
292    pub fn app(&self) -> AppService {
293        AppService {
294            client: self.clone(),
295        }
296    }
297
298    /// A service-style grouping for Chassis netfn commands.
299    pub fn chassis(&self) -> ChassisService {
300        ChassisService {
301            client: self.clone(),
302        }
303    }
304}
305
306async fn send_raw_locked(
307    inner: &mut Inner,
308    netfn: u8,
309    cmd: u8,
310    data: &[u8],
311) -> Result<RawResponse> {
312    let (rq_seq, packet) = inner.core.build_rmcpplus_ipmi_request(netfn, cmd, data)?;
313    let response_bytes = inner.transport.send_recv(&packet).await?;
314    inner
315        .core
316        .decode_rmcpplus_ipmi_response(netfn, cmd, rq_seq, &response_bytes)
317}
318
319/// App NetFn service.
320#[derive(Clone)]
321pub struct AppService {
322    client: Client,
323}
324
325impl AppService {
326    /// `Get Device ID` (App NetFn, cmd 0x01).
327    pub async fn get_device_id(&self) -> Result<DeviceId> {
328        self.client.get_device_id().await
329    }
330
331    /// `Get Self Test Results` (App NetFn, cmd 0x04).
332    pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
333        self.client.get_self_test_results().await
334    }
335
336    /// `Get System GUID` (App NetFn, cmd 0x37).
337    pub async fn get_system_guid(&self) -> Result<SystemGuid> {
338        self.client.get_system_guid().await
339    }
340
341    /// `Get Channel Authentication Capabilities` (App NetFn, cmd 0x38).
342    pub async fn get_channel_auth_capabilities(
343        &self,
344        channel: u8,
345        privilege: PrivilegeLevel,
346    ) -> Result<ChannelAuthCapabilities> {
347        self.client
348            .get_channel_auth_capabilities(channel, privilege)
349            .await
350    }
351}
352
353/// Chassis NetFn service.
354#[derive(Clone)]
355pub struct ChassisService {
356    client: Client,
357}
358
359impl ChassisService {
360    /// `Get Chassis Status` (Chassis NetFn, cmd 0x01).
361    pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
362        self.client.get_chassis_status().await
363    }
364
365    /// `Chassis Control` (Chassis NetFn, cmd 0x02).
366    pub async fn chassis_control(&self, control: ChassisControl) -> Result<()> {
367        self.client.chassis_control(control).await
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use core::future::Future;
374    use core::pin::Pin;
375
376    use super::*;
377
378    use crate::session::Session;
379
380    #[derive(Debug, Clone, Copy)]
381    struct TimeoutAsyncTransport;
382
383    impl AsyncTransport for TimeoutAsyncTransport {
384        fn send_recv<'a>(
385            &'a self,
386            _request: &'a [u8],
387        ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>> {
388            Box::pin(async { Err(Error::Timeout) })
389        }
390    }
391
392    fn dummy_session() -> Session {
393        Session::new_test(0x11223344, 0x55667788, false, false)
394    }
395
396    #[tokio::test(flavor = "current_thread")]
397    async fn close_session_timeout_marks_client_closed() {
398        let session = dummy_session();
399        let managed_session_id = session.managed_session_id;
400        let remote_session_id = session.remote_session_id;
401        let client = Client {
402            inner: Arc::new(tokio::sync::Mutex::new(Inner {
403                transport: Box::new(TimeoutAsyncTransport),
404                core: ClientCore::new(session),
405            })),
406            managed_session_id,
407            remote_session_id,
408        };
409
410        client.close_session().await.expect("close_session");
411
412        let err = client
413            .get_device_id()
414            .await
415            .expect_err("expected session-closed error");
416        assert!(matches!(err, Error::Protocol("session is closed")));
417    }
418}