blob: 17f9bf8ccc6f62f294d093a207cee7e66b007d60 [file] [log] [blame]
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module handles chassis-related operations, including chassis collection, individual chassis,
//! sensors collection, and individual sensors. It also includes functionality for event subscription
//! and processing.
// This is for using Json name directly
#![allow(non_snake_case)]
use axum::body::to_bytes;
use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use dashmap::DashMap;
use futures::future::join_all;
use serde_json::json;
use serde_json::Value;
use std::convert::Infallible;
use std::sync::Arc;
use axum::{extract::OriginalUri, http::Uri};
use redfish_codegen::models::odata_v4;
use redfish_codegen::models::{
chassis::v1_23_0::Actions as ChassisActions, chassis::v1_23_0::Chassis as ChassisModel,
chassis::v1_23_0::ChassisType, chassis::v1_23_0::Reset as ChassisReset,
chassis_collection::ChassisCollection as ChassisCollectionModel, resource,
sensor::v1_7_0::Sensor as SensorModel,
sensor_collection::SensorCollection as SensorCollectionModel,
};
use crate::dbus_client::{get_objects_with_interface, get_one_sensor, get_sensors_under_a_chassis};
use crate::app_state::AppState;
use crate::composite_query::{RequestResponse, SubRequest};
/// Represents the state of the chassis, including a cache for chassis data.
#[derive(Debug, Default)]
pub struct ChassisState {
pub cache: DashMap<String, serde_json::Value>,
}
/// Represents the query parameters for expanding resources.
#[derive(serde::Deserialize, Debug, Default)]
struct ExpandQuery {
#[serde(rename = "$expand")]
expand: Option<String>,
}
/// Retrieves all chassis from the system.
///
/// # Returns
///
/// A Result containing a vector of chassis names or an error.
async fn get_all_chassis() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let result = get_objects_with_interface(
Vec::new(),
"/xyz/openbmc_project",
"xyz.openbmc_project.Inventory.Item.Board",
)
.await?;
let mut chassis: Vec<String> = Vec::new();
for (_service, objects) in result {
for object_path in objects {
let path_segments: Vec<&str> = object_path.split('/').collect();
if let Some(last_segment) = path_segments.last() {
chassis.push(last_segment.to_string());
}
}
}
Ok(chassis)
}
/// Handles requests for the chassis collection.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `uri` - The original URI of the request.
/// * `params` - Query parameters for expansion.
async fn chassis_collection(
State(state): State<Arc<AppState>>,
uri: OriginalUri,
Query(params): Query<ExpandQuery>,
) -> impl IntoResponse {
let params_str = params.expand.clone().unwrap_or_default();
let cache_key = format!("{}?{}", uri.path(), params_str);
if let Some(cached_response) = state.chassis_state.cache.get(&cache_key) {
return (StatusCode::OK, Json(cached_response.value().clone()));
}
let chassis_collection = get_all_chassis().await.unwrap_or_default();
let members = chassis_collection
.into_iter()
.map(|chassis_id| format!("{}/{}", uri.path(), chassis_id))
.map(|chassis_uri| odata_v4::IdRef {
odata_id: Some(odata_v4::Id(chassis_uri)),
})
.collect::<Vec<_>>();
let response = ChassisCollectionModel {
odata_id: odata_v4::Id(uri.path().to_string()),
members_odata_count: odata_v4::Count(members.len() as i64),
members,
name: resource::Name("Chassis Collection".to_string()),
..Default::default()
};
let mut response_json = serde_json::to_value(response).unwrap_or_default();
let expand = params.expand.as_deref() == Some(".($levels=1)");
if expand {
let member_uris: Vec<String> = response_json["Members"]
.as_array()
.unwrap_or(&Vec::new())
.iter()
.filter_map(|m| m["@odata.id"].as_str().map(ToString::to_string))
.collect();
let tasks = member_uris
.into_iter()
.map(|odata_id| {
let state = state.clone();
tokio::spawn(async move {
let uri: Uri = odata_id.parse().unwrap_or(Uri::default());
let chassis_id = odata_id.split('/').last().unwrap_or("");
let response = chassis(
State(state),
OriginalUri(uri),
Path(chassis_id.to_string()),
Query::default(),
)
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default()
})
})
.collect::<Vec<_>>();
let results = futures::future::join_all(tasks).await;
if let Some(members) = response_json
.get_mut("Members")
.and_then(|m| m.as_array_mut())
{
for (member, result) in members.iter_mut().zip(results) {
match result {
Ok(expanded_sensor) => *member = expanded_sensor,
Err(_e) => {
*member = json!({"error": "Failed to expand sensor"});
}
}
}
}
}
// TODO: create async task to monitoring the changes related to this cache entry
state
.chassis_state
.cache
.insert(cache_key, response_json.clone());
(StatusCode::OK, Json(response_json))
}
/// Handles requests for individual chassis.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `uri` - The original URI of the request.
/// * `chassis_id` - The ID of the chassis.
/// * `params` - Query parameters for expansion.
async fn chassis(
State(state): State<Arc<AppState>>,
uri: OriginalUri,
Path(chassis_id): Path<String>,
Query(params): Query<ExpandQuery>,
) -> impl IntoResponse {
let params_str = params.expand.clone().unwrap_or_default();
let cache_key = format!("{}?{}", uri.path(), params_str);
if let Some(cached_response) = state.chassis_state.cache.get(&cache_key) {
return (StatusCode::OK, Json(cached_response.value().clone()));
}
let response = ChassisModel {
odata_id: odata_v4::Id(uri.path().to_string()),
actions: Some(ChassisActions {
chassis_reset: Some(ChassisReset {
target: Some(format!("{}/{}", uri.path(), "Actions/Chassis.Reset")),
..Default::default()
}),
..Default::default()
}),
sensors: Some(odata_v4::IdRef {
odata_id: Some(odata_v4::Id(format!("{}/{}", uri.path(), "Sensors"))),
}),
chassis_type: ChassisType::RackMount,
id: resource::Id(chassis_id.clone()),
name: resource::Name(format!("Chassis {}", chassis_id)),
..Default::default()
};
// TODO: create async task to monitoring the changes related to this cache entry
let response_json = serde_json::to_value(response).unwrap_or_default();
state
.chassis_state
.cache
.insert(cache_key, response_json.clone());
(StatusCode::OK, Json(response_json))
}
/// Retrieves the object path for all sensors under a chassis.
///
/// # Arguments
///
/// * `sensor_collection_uri` - The URI of the sensor collection.
///
/// # Returns
///
/// A string representing the object path for all sensors.
fn get_all_sensors_object_path(sensor_collection_uri: &OriginalUri) -> String {
let path = sensor_collection_uri.path().to_string();
let parts: Vec<&str> = path.split('/').collect();
if let Some(chassis_index) = parts.iter().position(|&r| r == "Chassis") {
if let Some(chassis_name) = parts.get(chassis_index + 1) {
return format!(
"/xyz/openbmc_project/inventory/system/board/{}/all_sensors",
chassis_name
);
}
}
String::new()
}
/// Handles requests for the sensors collection under a chassis.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `uri` - The original URI of the request.
/// * `chassis_id` - The ID of the chassis.
/// * `params` - Query parameters for expansion.
async fn sensors_collection(
State(state): State<Arc<AppState>>,
uri: OriginalUri,
Path(chassis_id): Path<String>,
Query(params): Query<ExpandQuery>,
) -> impl IntoResponse {
let params_str = params.expand.clone().unwrap_or_default();
let cache_key = format!("{}?{}", uri.path(), params_str);
if let Some(cached_response) = state.chassis_state.cache.get(&cache_key) {
return (StatusCode::OK, Json(cached_response.value().clone()));
}
let sensors = get_sensors_under_a_chassis(&get_all_sensors_object_path(&uri))
.await
.unwrap_or_default();
let members = sensors
.into_iter()
.map(|sensor_id| format!("{}/{}", uri.path(), sensor_id))
.map(|sensor_uri| odata_v4::IdRef {
odata_id: Some(odata_v4::Id(sensor_uri)),
})
.collect::<Vec<_>>();
let response = SensorCollectionModel {
odata_id: odata_v4::Id(uri.path().to_string()),
members_odata_count: odata_v4::Count(members.len() as i64),
members,
name: resource::Name("Sensor Collection".to_string()),
..Default::default()
};
let mut response_json = serde_json::to_value(response).unwrap_or_default();
let expand = params.expand.as_deref() == Some(".($levels=1)");
if expand {
let member_uris: Vec<String> = response_json["Members"]
.as_array()
.unwrap_or(&Vec::new())
.iter()
.filter_map(|m| m["@odata.id"].as_str().map(ToString::to_string))
.collect();
let tasks = member_uris
.into_iter()
.map(|odata_id| {
let state = state.clone();
let chassis_id = chassis_id.clone();
tokio::spawn(async move {
let uri: Uri = odata_id.parse().unwrap_or(Uri::default());
let sensor_id = odata_id.split('/').last().unwrap_or("");
let response = sensor(
State(state),
OriginalUri(uri),
Path((chassis_id.to_string(), sensor_id.to_string())),
Query::default(),
)
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default()
})
})
.collect::<Vec<_>>();
let results = futures::future::join_all(tasks).await;
if let Some(members) = response_json
.get_mut("Members")
.and_then(|m| m.as_array_mut())
{
for (member, result) in members.iter_mut().zip(results) {
match result {
Ok(expanded_sensor) => *member = expanded_sensor,
Err(_e) => {
*member = json!({"error": "Failed to expand sensoris"});
}
}
}
}
}
// TODO: create async task to monitoring the changes related to this cache entry
state
.chassis_state
.cache
.insert(cache_key, response_json.clone());
(StatusCode::OK, Json(response_json))
}
/// Formats a sensor name by removing underscores and trimming.
///
/// # Arguments
///
/// * `sensor_name` - The original sensor name.
///
/// # Returns
///
/// A formatted string representing the sensor name.
fn format_sensor_name(sensor_name: &str) -> String {
let trimmed_name = sensor_name
.find('_')
.map(|index| &sensor_name[index + 1..])
.unwrap_or(sensor_name);
trimmed_name.replace('_', " ")
}
/// Extracts the chassis ID from a base URL.
///
/// # Arguments
///
/// * `base_url` - The base URL containing the chassis ID.
///
/// # Returns
///
/// An Option containing the chassis ID as a String, if found.
fn extract_chassis_id(base_url: &str) -> Option<String> {
let segments: Vec<&str> = base_url.split('/').collect();
segments
.iter()
.position(|&segment| segment == "Chassis")
.and_then(|index| segments.get(index + 1))
.map(|&segment| segment.to_string())
}
/// Handles a single request in the context of a composite query.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `sub_request` - The sub-request to handle.
/// * `context` - The context for storing request results.
///
/// # Returns
///
/// A Result containing the RequestResponse or an Infallible error.
async fn handle_one_request(
state: &Arc<AppState>,
sub_request: &mut SubRequest,
context: &mut Arc<DashMap<String, Value>>,
) -> Result<RequestResponse, Infallible> {
let (base_url, _) = sub_request
.url
.split_at(sub_request.url.find('?').unwrap_or(sub_request.url.len()));
let segments: Vec<&str> = base_url.split('/').collect();
match sub_request.method.as_str() {
"GET" if base_url.ends_with("/Chassis") => {
if let Ok(parsed_url) = url::Url::parse(&format!("http://localhost{}", sub_request.url))
{
let query: String = parsed_url.query().unwrap_or("").to_string();
let expanded_query: ExpandQuery = serde_qs::from_str(&query).unwrap_or_default();
let url = sub_request.url.parse().unwrap_or_default();
let url = OriginalUri(url);
let response = chassis_collection(State(state.clone()), url, Query(expanded_query))
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
let json = serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default();
context.insert(sub_request.referenceId.clone(), json.clone());
Ok(RequestResponse::Json(json))
} else {
Ok(RequestResponse::Json(
json!({"error": "Invalid URL format"}),
))
}
}
"GET" if segments.len() > 2 && segments[segments.len() - 2] == "Chassis" => {
let url = sub_request.url.parse().unwrap_or_default();
let url = OriginalUri(url);
let chassis_id = extract_chassis_id(base_url).unwrap_or_default();
let state = state.clone();
let response = chassis(
State(state),
url,
Path(chassis_id.to_string()),
Query::default(),
)
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
let json = serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default();
context.insert(sub_request.referenceId.clone(), json.clone());
Ok(RequestResponse::Json(json))
}
"GET" if base_url.ends_with("/Sensors") => {
if let Ok(parsed_url) = url::Url::parse(&format!("http://localhost{}", sub_request.url))
{
let query: String = parsed_url.query().unwrap_or("").to_string();
// Deserialize the query string into ExpandQuery
let expanded_query: ExpandQuery = serde_qs::from_str(&query).unwrap_or_default();
let url = sub_request.url.parse().unwrap_or_default();
let url = OriginalUri(url);
let chassis_id = extract_chassis_id(base_url).unwrap_or_default();
let response = sensors_collection(
State(state.clone()),
url,
Path(chassis_id),
Query(expanded_query),
)
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
let json = serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default();
context.insert(sub_request.referenceId.clone(), json.clone());
Ok(RequestResponse::Json(json))
} else {
Ok(RequestResponse::Json(
json!({"error": "Invalid URL format"}),
))
}
}
"GET" if segments.len() > 2 && segments[segments.len() - 2] == "Sensors" => {
let url = sub_request.url.parse().unwrap_or_default();
let url = OriginalUri(url);
let chassis_id = extract_chassis_id(base_url).unwrap_or_default();
let sensor_id = base_url.split('/').last().unwrap_or("");
let state = state.clone();
let response = sensor(
State(state),
url,
Path((chassis_id.to_string(), sensor_id.to_string())),
Query::default(),
)
.await
.into_response();
let body_bytes = to_bytes(response.into_body(), usize::MAX)
.await
.unwrap_or_default();
let json = serde_json::from_slice::<Value>(&body_bytes).unwrap_or_default();
context.insert(sub_request.referenceId.clone(), json.clone());
Ok(RequestResponse::Json(json))
}
_ => Ok::<RequestResponse, Infallible>(RequestResponse::Json(
json!({"error": "Unsupported request method or URL"}),
)),
}
}
/// Retrieves a SensorModel, either from cache or by creating a new one.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `cache_key` - The key for caching the sensor model.
/// * `odata_id` - The odata ID of the sensor.
///
/// # Returns
///
/// A Result containing the SensorModel or an Infallible error.
async fn get_sensor_model(
state: &Arc<AppState>,
cache_key: &str,
odata_id: &str,
) -> Result<SensorModel, Infallible> {
match state.chassis_state.cache.get(cache_key) {
Some(cached_response) => {
Ok(serde_json::from_value(cached_response.value().clone()).unwrap_or_default())
}
None => {
let uri: Uri = odata_id.parse().unwrap_or(Uri::default());
let uri = OriginalUri(uri);
let sensor_id = uri.path().split('/').last().unwrap_or_default().to_string();
let mut sensor_model = SensorModel {
odata_id: odata_v4::Id(odata_id.to_string()),
odata_type: odata_v4::Type("#Sensor.v1_7_0.Sensor".to_string()),
id: resource::Id(sensor_id.to_string()),
name: resource::Name(format_sensor_name(&sensor_id)),
..Default::default()
};
get_one_sensor(&sensor_id, &mut sensor_model)
.await
.unwrap_or_default();
Ok(sensor_model)
}
}
}
/// Handles requests for individual sensors.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `uri` - The original URI of the request.
/// * `_chassis_id` - The ID of the chassis (unused).
/// * `_sensor_id` - The ID of the sensor (unused).
/// * `_params` - Query parameters for expansion (unused).
async fn sensor(
State(state): State<Arc<AppState>>,
uri: OriginalUri,
Path((_chassis_id, _sensor_id)): Path<(String, String)>,
Query(_params): Query<ExpandQuery>,
) -> impl IntoResponse {
let cache_key = uri.path().to_string();
let sensor_model = get_sensor_model(&state, &cache_key, &cache_key)
.await
.unwrap_or_default();
let response_json = serde_json::to_value(sensor_model).unwrap_or_default();
state
.chassis_state
.cache
.insert(cache_key, response_json.clone());
(StatusCode::OK, Json(response_json))
}
/// Handles an XPath segment in the context of URL expansion.
///
/// # Arguments
///
/// * `urls` - The vector of URLs to modify.
/// * `_segment` - The XPath segment (unused).
/// * `state` - The application state.
///
/// # Returns
///
/// A Result containing a vector of RequestResponse objects or an Infallible error.
pub async fn handle_xpath_segment(
urls: &mut Vec<String>,
_segment: &str,
state: &Arc<AppState>,
) -> Result<Vec<RequestResponse>, Infallible> {
let context = Arc::new(DashMap::new());
let futures: Vec<_> = urls
.iter()
.map(|url| {
let state_clone = state.clone();
let mut context_clone = context.clone();
async move {
let mut request = SubRequest {
method: "GET".to_string(),
url: url.to_string(),
..Default::default()
};
let response =
handle_one_request(&state_clone, &mut request, &mut context_clone).await;
let new_urls = match &response {
Ok(RequestResponse::Json(json)) => {
// TODO: use segment as query here
let query = "$.Members[*]['@odata.id']";
jsonpath_lib::select(json, query)
.unwrap_or_else(|_| vec![])
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<String>>()
}
_ => vec![],
};
(response, new_urls)
}
})
.collect();
let results = join_all(futures).await;
*urls = vec![];
let mut responses = vec![];
for (response, new_urls) in results {
urls.extend(new_urls);
responses.push(response.unwrap_or_default());
}
Ok(responses)
}
/// Appends a segment to each URL in the provided vector.
///
/// # Arguments
///
/// * `urls` - The vector of URLs to modify.
/// * `segment` - The segment to append to each URL.
pub fn append_to_urls(urls: &mut [String], segment: &str) {
urls.iter_mut().for_each(|url| {
if !url.ends_with('/') && !segment.starts_with('/') {
url.push('/');
}
url.push_str(segment);
});
}