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#[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#[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 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 pub fn username_bytes(mut self, username: impl Into<Vec<u8>>) -> Self {
64 self.username = Some(username.into());
65 self
66 }
67
68 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 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 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 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 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 pub fn privilege_level(mut self, level: PrivilegeLevel) -> Self {
102 self.privilege_level = level;
103 self
104 }
105
106 pub fn timeout(mut self, timeout: Duration) -> Self {
108 self.timeout = timeout;
109 self
110 }
111
112 pub fn retries(mut self, attempts: u32) -> Self {
114 self.retries = attempts;
115 self
116 }
117
118 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 pub fn builder(target: SocketAddr) -> ClientBuilder {
162 ClientBuilder::new(target)
163 }
164
165 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 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 pub async fn get_device_id(&self) -> Result<DeviceId> {
191 self.execute(GetDeviceId).await
192 }
193
194 pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
196 self.execute(GetSelfTestResults).await
197 }
198
199 pub async fn get_system_guid(&self) -> Result<SystemGuid> {
201 self.execute(GetSystemGuid).await
202 }
203
204 pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
206 self.execute(GetChassisStatus).await
207 }
208
209 pub async fn chassis_control(&self, control: ChassisControl) -> Result<()> {
211 self.execute(ChassisControlCommand { control }).await
212 }
213
214 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 pub fn managed_session_id(&self) -> u32 {
231 self.managed_session_id
232 }
233
234 pub fn remote_session_id(&self) -> u32 {
236 self.remote_session_id
237 }
238
239 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 pub fn app(&self) -> AppService {
293 AppService {
294 client: self.clone(),
295 }
296 }
297
298 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#[derive(Clone)]
321pub struct AppService {
322 client: Client,
323}
324
325impl AppService {
326 pub async fn get_device_id(&self) -> Result<DeviceId> {
328 self.client.get_device_id().await
329 }
330
331 pub async fn get_self_test_results(&self) -> Result<SelfTestResult> {
333 self.client.get_self_test_results().await
334 }
335
336 pub async fn get_system_guid(&self) -> Result<SystemGuid> {
338 self.client.get_system_guid().await
339 }
340
341 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#[derive(Clone)]
355pub struct ChassisService {
356 client: Client,
357}
358
359impl ChassisService {
360 pub async fn get_chassis_status(&self) -> Result<ChassisStatus> {
362 self.client.get_chassis_status().await
363 }
364
365 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}