mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
feat: 实现 Redfish API 标准接口;支持通过前端开关控制 Redfish 服务
This commit is contained in:
84
src/redfish/auth.rs
Normal file
84
src/redfish/auth.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::{header, Method, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use base64::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::schema::RedfishError;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn redfish_auth_middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
if !state.config.is_initialized() {
|
||||
let body = RedfishError::service_unavailable("System not initialized");
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(body)).into_response();
|
||||
}
|
||||
|
||||
let path = request.uri().path();
|
||||
if is_redfish_public_endpoint(path, request.method()) {
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
if let Some(token) = request.headers().get("X-Auth-Token") {
|
||||
if let Ok(token_str) = token.to_str() {
|
||||
if state.is_session_revoked(token_str).await {
|
||||
let body = RedfishError::invalid_credentials();
|
||||
return (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response();
|
||||
}
|
||||
if let Ok(Some(session)) = state.sessions.get(token_str).await {
|
||||
request.extensions_mut().insert(session);
|
||||
return next.run(request).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(auth_header) = request.headers().get(header::AUTHORIZATION) {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
if let Some(credentials) = auth_str.strip_prefix("Basic ") {
|
||||
if let Some((username, password)) = decode_basic_auth(credentials) {
|
||||
match state.users.verify(&username, &password).await {
|
||||
Ok(Some(user)) => {
|
||||
request.extensions_mut().insert(user);
|
||||
return next.run(request).await;
|
||||
}
|
||||
_ => {
|
||||
let body = RedfishError::invalid_credentials();
|
||||
return (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let body = RedfishError::authentication_required();
|
||||
(StatusCode::UNAUTHORIZED, axum::Json(body)).into_response()
|
||||
}
|
||||
|
||||
fn is_redfish_public_endpoint(path: &str, method: &Method) -> bool {
|
||||
matches!(
|
||||
path,
|
||||
"/" | "/v1" | "/v1/" | "/v1/odata"
|
||||
) || path.starts_with("/v1/$metadata")
|
||||
|| (path == "/v1/SessionService/Sessions" && *method == Method::POST)
|
||||
}
|
||||
|
||||
fn decode_basic_auth(encoded: &str) -> Option<(String, String)> {
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.ok()?;
|
||||
let credentials = String::from_utf8(decoded).ok()?;
|
||||
let mut parts = credentials.splitn(2, ':');
|
||||
let username = parts.next()?.to_string();
|
||||
let password = parts.next()?.to_string();
|
||||
if username.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((username, password))
|
||||
}
|
||||
3
src/redfish/mod.rs
Normal file
3
src/redfish/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
155
src/redfish/routes/account.rs
Normal file
155
src/redfish/routes/account.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{empty_collection, resource_not_found};
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/AccountService", get(account_service))
|
||||
.route(
|
||||
"/v1/AccountService/Accounts",
|
||||
get(account_list),
|
||||
)
|
||||
.route(
|
||||
"/v1/AccountService/Accounts/{account_id}",
|
||||
get(account_detail),
|
||||
)
|
||||
.route(
|
||||
"/v1/AccountService/Roles",
|
||||
get(roles_stub),
|
||||
)
|
||||
.route(
|
||||
"/v1/AccountService/Roles/{role_id}",
|
||||
get(role_detail_stub),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn account_service() -> Json<AccountService> {
|
||||
Json(AccountService {
|
||||
odata_type: "#AccountService.v1_13_0.AccountService".to_string(),
|
||||
odata_id: "/redfish/v1/AccountService".to_string(),
|
||||
odata_context: "/redfish/v1/$metadata#AccountService.AccountService".to_string(),
|
||||
id: "AccountService".to_string(),
|
||||
name: "Account Service".to_string(),
|
||||
description: "Account Service".to_string(),
|
||||
service_enabled: true,
|
||||
accounts: odata_ref("/redfish/v1/AccountService/Accounts"),
|
||||
roles: odata_ref("/redfish/v1/AccountService/Roles"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn account_list(State(state): State<Arc<AppState>>) -> Response {
|
||||
let user = match state.users.single_user().await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
return Json(empty_collection(
|
||||
"#ManagerAccountCollection.ManagerAccountCollection",
|
||||
"/redfish/v1/AccountService/Accounts",
|
||||
"/redfish/v1/$metadata#ManagerAccountCollection.ManagerAccountCollection",
|
||||
"Accounts Collection",
|
||||
"Collection of Accounts",
|
||||
vec![],
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
return (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
Json(empty_collection(
|
||||
"#ManagerAccountCollection.ManagerAccountCollection",
|
||||
"/redfish/v1/AccountService/Accounts",
|
||||
"/redfish/v1/$metadata#ManagerAccountCollection.ManagerAccountCollection",
|
||||
"Accounts Collection",
|
||||
"Collection of Accounts",
|
||||
vec![odata_ref(&format!("/redfish/v1/AccountService/Accounts/{}", user.id))],
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn account_detail(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(account_id): Path<String>,
|
||||
) -> Response {
|
||||
let user = match state.users.single_user().await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return resource_not_found(),
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
if user.id != account_id {
|
||||
return resource_not_found();
|
||||
}
|
||||
|
||||
Json(ManagerAccount {
|
||||
odata_type: "#ManagerAccount.v1_12_0.ManagerAccount".to_string(),
|
||||
odata_id: format!("/redfish/v1/AccountService/Accounts/{}", user.id),
|
||||
odata_context: "/redfish/v1/$metadata#ManagerAccount.ManagerAccount".to_string(),
|
||||
id: user.id,
|
||||
name: format!("Account {}", user.username),
|
||||
description: "User Account".to_string(),
|
||||
enabled: true,
|
||||
user_name: user.username,
|
||||
role_id: "Administrator".to_string(),
|
||||
locked: false,
|
||||
links: ManagerAccountLinks {
|
||||
role: odata_ref("/redfish/v1/AccountService/Roles/Administrator"),
|
||||
},
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn roles_stub() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({
|
||||
"@odata.type": "#RoleCollection.RoleCollection",
|
||||
"@odata.id": "/redfish/v1/AccountService/Roles",
|
||||
"@odata.context": "/redfish/v1/$metadata#RoleCollection.RoleCollection",
|
||||
"Name": "Roles Collection",
|
||||
"Description": "Collection of Roles",
|
||||
"Members@odata.count": 1,
|
||||
"Members": [{ "@odata.id": "/redfish/v1/AccountService/Roles/Administrator" }]
|
||||
}))
|
||||
}
|
||||
|
||||
async fn role_detail_stub(Path(role_id): Path<String>) -> Response {
|
||||
if role_id != "Administrator" {
|
||||
return resource_not_found();
|
||||
}
|
||||
|
||||
Json(serde_json::json!({
|
||||
"@odata.type": "#Role.v1_3_1.Role",
|
||||
"@odata.id": "/redfish/v1/AccountService/Roles/Administrator",
|
||||
"@odata.context": "/redfish/v1/$metadata#Role.Role",
|
||||
"Id": "Administrator",
|
||||
"Name": "Administrator Role",
|
||||
"Description": "Administrator role with full access",
|
||||
"IsPredefined": true,
|
||||
"AssignedPrivileges": [
|
||||
"Login", "ConfigureManager", "ConfigureUsers",
|
||||
"ConfigureSelf", "ConfigureComponents"
|
||||
],
|
||||
"OemPrivileges": []
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
92
src/redfish/routes/chassis.rs
Normal file
92
src/redfish/routes/chassis.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{empty_collection, get_power_state, validate_id, RESOURCE_ID};
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/Chassis", get(chassis_collection))
|
||||
.route("/v1/Chassis/{chassis_id}", get(chassis_detail))
|
||||
.route("/v1/Chassis/{chassis_id}/Power", get(chassis_power))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn chassis_collection() -> Json<Collection<ODataLink>> {
|
||||
Json(empty_collection(
|
||||
"#ChassisCollection.ChassisCollection",
|
||||
"/redfish/v1/Chassis",
|
||||
"/redfish/v1/$metadata#ChassisCollection.ChassisCollection",
|
||||
"Chassis Collection",
|
||||
"Collection of Chassis",
|
||||
vec![odata_ref("/redfish/v1/Chassis/1")],
|
||||
))
|
||||
}
|
||||
|
||||
async fn chassis_detail(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(chassis_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&chassis_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
let power_state = get_power_state(&state).await;
|
||||
|
||||
Json(Chassis {
|
||||
odata_type: "#Chassis.v1_25_0.Chassis".to_string(),
|
||||
odata_id: format!("/redfish/v1/Chassis/{}", chassis_id),
|
||||
odata_context: "/redfish/v1/$metadata#Chassis.Chassis".to_string(),
|
||||
id: chassis_id.clone(),
|
||||
name: "One-KVM Chassis".to_string(),
|
||||
description: "The physical chassis managed by One-KVM".to_string(),
|
||||
chassis_type: "RackMount".to_string(),
|
||||
asset_tag: String::new(),
|
||||
manufacturer: "One-KVM".to_string(),
|
||||
model: "Virtual".to_string(),
|
||||
serial_number: String::new(),
|
||||
part_number: String::new(),
|
||||
power_state: power_state.to_string(),
|
||||
status: Status::enabled_ok(),
|
||||
power: odata_ref(&format!("/redfish/v1/Chassis/{}/Power", chassis_id)),
|
||||
links: ChassisLinks {
|
||||
computer_systems: vec![odata_ref(&format!("/redfish/v1/Systems/{}", RESOURCE_ID))],
|
||||
managed_by: vec![odata_ref(&format!("/redfish/v1/Managers/{}", RESOURCE_ID))],
|
||||
},
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn chassis_power(
|
||||
Path(chassis_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&chassis_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Json(Power {
|
||||
odata_type: "#Power.v1_7_3.Power".to_string(),
|
||||
odata_id: format!("/redfish/v1/Chassis/{}/Power", chassis_id),
|
||||
odata_context: "/redfish/v1/$metadata#Power.Power".to_string(),
|
||||
id: "Power".to_string(),
|
||||
name: "Power".to_string(),
|
||||
power_control: vec![PowerControl {
|
||||
odata_id: format!("/redfish/v1/Chassis/{}/Power#/PowerControl/0", chassis_id),
|
||||
member_id: "0".to_string(),
|
||||
name: "System Power Control".to_string(),
|
||||
power_consumed_watts: None,
|
||||
power_capacity_watts: None,
|
||||
power_requested_watts: None,
|
||||
power_metrics: None,
|
||||
status: Status::enabled_ok(),
|
||||
}],
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
102
src/redfish/routes/event.rs
Normal file
102
src/redfish/routes/event.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{convert::Infallible, sync::Arc, time::Duration};
|
||||
use tracing::info;
|
||||
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/EventService", get(event_service))
|
||||
.route("/v1/EventService/SSE", get(event_service_sse))
|
||||
.route(
|
||||
"/v1/EventService/Actions/EventService.SubmitTestEvent",
|
||||
post(event_submit_test),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn event_service() -> Json<EventService> {
|
||||
Json(EventService {
|
||||
odata_type: "#EventService.v1_8_1.EventService".to_string(),
|
||||
odata_id: "/redfish/v1/EventService".to_string(),
|
||||
odata_context: "/redfish/v1/$metadata#EventService.EventService".to_string(),
|
||||
id: "EventService".to_string(),
|
||||
name: "Event Service".to_string(),
|
||||
description: "Event Service".to_string(),
|
||||
service_enabled: true,
|
||||
delivery_retry_attempts: 3,
|
||||
delivery_retry_interval_seconds: 30,
|
||||
event_format_types: vec!["Event".to_string(), "MetricReport".to_string()],
|
||||
registry_prefixes: vec!["Base".to_string()],
|
||||
subordinate_resources: true,
|
||||
sse_filter_properties_supported: SseFilterPropertiesSupported {
|
||||
event_format_type: false,
|
||||
message_id: false,
|
||||
metric_report_definition: false,
|
||||
origin_resource: false,
|
||||
registry_prefix: false,
|
||||
resource_type: false,
|
||||
},
|
||||
server_sent_event_uri: Some("/redfish/v1/EventService/SSE".to_string()),
|
||||
actions: EventServiceActions {
|
||||
submit_test_event: ActionTarget {
|
||||
target: "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent"
|
||||
.to_string(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn event_service_sse(State(state): State<Arc<AppState>>) -> Response {
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
|
||||
let mut device_info_rx = state.subscribe_device_info();
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
loop {
|
||||
match device_info_rx.changed().await {
|
||||
Ok(()) => {
|
||||
let payload = json!({
|
||||
"@odata.type": "#Event.v1_7_0.Event",
|
||||
"Id": uuid::Uuid::new_v4().to_string(),
|
||||
"Name": "One-KVM Event",
|
||||
"Context": "One-KVM",
|
||||
"Events": [{
|
||||
"EventType": "ResourceUpdated",
|
||||
"EventId": uuid::Uuid::new_v4().to_string(),
|
||||
"Severity": "OK",
|
||||
"Message": "Device state updated",
|
||||
"MessageId": "ResourceUpdated.1.0.0.ResourceUpdated"
|
||||
}]
|
||||
});
|
||||
|
||||
let event = Event::default()
|
||||
.data(serde_json::to_string(&payload).unwrap_or_default());
|
||||
yield Ok::<_, Infallible>(event);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Sse::new(Box::pin(stream))
|
||||
.keep_alive(
|
||||
KeepAlive::new()
|
||||
.interval(Duration::from_secs(30))
|
||||
.text(":\n"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn event_submit_test() -> StatusCode {
|
||||
info!("Redfish: SubmitTestEvent received (no-op)");
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
120
src/redfish/routes/managers.rs
Normal file
120
src/redfish/routes/managers.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{empty_collection, validate_id, RESOURCE_ID};
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/Managers", get(managers_collection))
|
||||
.route("/v1/Managers/{manager_id}", get(manager_detail))
|
||||
.route(
|
||||
"/v1/Managers/{manager_id}/NetworkProtocol",
|
||||
get(network_protocol),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn managers_collection() -> Json<Collection<ODataLink>> {
|
||||
Json(empty_collection(
|
||||
"#ManagerCollection.ManagerCollection",
|
||||
"/redfish/v1/Managers",
|
||||
"/redfish/v1/$metadata#ManagerCollection.ManagerCollection",
|
||||
"Manager Collection",
|
||||
"Collection of Managers",
|
||||
vec![odata_ref("/redfish/v1/Managers/1")],
|
||||
))
|
||||
}
|
||||
|
||||
async fn manager_detail(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(manager_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let datetime = now
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_default();
|
||||
let offset = now.offset();
|
||||
let offset_str = format!(
|
||||
"{:+03}{:02}",
|
||||
offset.whole_hours(),
|
||||
offset.minutes_past_hour().abs()
|
||||
);
|
||||
|
||||
let mgr_uuid = "00000000-0000-0000-0000-000000000001".to_string();
|
||||
|
||||
Json(Manager {
|
||||
odata_type: "#Manager.v1_15_0.Manager".to_string(),
|
||||
odata_id: format!("/redfish/v1/Managers/{}", manager_id),
|
||||
odata_context: "/redfish/v1/$metadata#Manager.Manager".to_string(),
|
||||
id: manager_id.clone(),
|
||||
name: "One-KVM Manager".to_string(),
|
||||
description: "One-KVM Management Controller".to_string(),
|
||||
manager_type: "BMC".to_string(),
|
||||
status: Status::enabled_ok(),
|
||||
firmware_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
manufacturer: "One-KVM".to_string(),
|
||||
model: "One-KVM".to_string(),
|
||||
date_time: datetime,
|
||||
date_time_local_offset: offset_str,
|
||||
service_entry_point_uuid: mgr_uuid,
|
||||
command_shell: CommandShell {
|
||||
service_enabled: state
|
||||
.extensions
|
||||
.check_available(crate::extensions::ExtensionId::Ttyd),
|
||||
max_concurrent_sessions: 1,
|
||||
connect_types_supported: vec!["WebUI".to_string()],
|
||||
},
|
||||
graphical_console: GraphicalConsole {
|
||||
service_enabled: true,
|
||||
max_concurrent_sessions: 4,
|
||||
connect_types_supported: vec!["KVMIP".to_string()],
|
||||
},
|
||||
virtual_media: odata_ref(&format!("/redfish/v1/Managers/{}/VirtualMedia", manager_id)),
|
||||
links: ManagerLinks {
|
||||
manager_for_servers: vec![odata_ref(&format!("/redfish/v1/Systems/{}", RESOURCE_ID))],
|
||||
manager_for_chassis: vec![odata_ref(&format!("/redfish/v1/Chassis/{}", RESOURCE_ID))],
|
||||
},
|
||||
network_protocol: odata_ref(&format!("/redfish/v1/Managers/{}/NetworkProtocol", manager_id)),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn network_protocol(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(manager_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
let config = state.config.get();
|
||||
let http_port = config.web.http_port;
|
||||
let https_enabled = config.web.https_enabled;
|
||||
let https_port = config.web.https_port;
|
||||
|
||||
Json(serde_json::json!({
|
||||
"@odata.type": "#ManagerNetworkProtocol.v1_10_0.ManagerNetworkProtocol",
|
||||
"@odata.id": format!("/redfish/v1/Managers/{}/NetworkProtocol", manager_id),
|
||||
"@odata.context": "/redfish/v1/$metadata#ManagerNetworkProtocol.ManagerNetworkProtocol",
|
||||
"Id": "NetworkProtocol",
|
||||
"Name": "Manager Network Protocol",
|
||||
"Description": "Network protocol settings",
|
||||
"Status": { "State": "Enabled", "Health": "OK" },
|
||||
"HTTP": { "ProtocolEnabled": !https_enabled, "Port": http_port },
|
||||
"HTTPS": { "ProtocolEnabled": https_enabled, "Port": https_port },
|
||||
"SSDP": { "ProtocolEnabled": false }
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
211
src/redfish/routes/mod.rs
Normal file
211
src/redfish/routes/mod.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
mod account;
|
||||
mod chassis;
|
||||
mod event;
|
||||
mod managers;
|
||||
mod session;
|
||||
mod systems;
|
||||
mod virtual_media;
|
||||
|
||||
use axum::{
|
||||
http::{HeaderName, HeaderValue},
|
||||
middleware,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use super::auth::redfish_auth_middleware;
|
||||
use super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) const REDFISH_VERSION: &str = "1.18.1";
|
||||
pub(crate) const RESOURCE_ID: &str = "1";
|
||||
|
||||
pub(crate) fn resource_not_found() -> Response {
|
||||
(
|
||||
axum::http::StatusCode::NOT_FOUND,
|
||||
axum::Json(RedfishError::resource_not_found()),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) fn validate_id(id: &str) -> Option<Response> {
|
||||
if id != RESOURCE_ID {
|
||||
return Some(resource_not_found());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn service_unavailable(msg: &str) -> Response {
|
||||
(
|
||||
axum::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
axum::Json(RedfishError::service_unavailable(msg)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) fn empty_collection(
|
||||
odata_type: &str,
|
||||
odata_id: &str,
|
||||
odata_context: &str,
|
||||
name: &str,
|
||||
description: &str,
|
||||
members: Vec<ODataLink>,
|
||||
) -> Collection<ODataLink> {
|
||||
Collection {
|
||||
odata_type: odata_type.to_string(),
|
||||
odata_id: odata_id.to_string(),
|
||||
odata_context: odata_context.to_string(),
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
members_count: members.len() as u64,
|
||||
members,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_power_state(state: &Arc<AppState>) -> &'static str {
|
||||
let guard = state.atx.read().await;
|
||||
match guard.as_ref() {
|
||||
Some(atx) => match atx.power_status().await {
|
||||
crate::atx::PowerStatus::On => "On",
|
||||
crate::atx::PowerStatus::Off => "Off",
|
||||
crate::atx::PowerStatus::Unknown => "Unknown",
|
||||
},
|
||||
None => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
async fn service_root_redirect() -> Response {
|
||||
axum::response::Redirect::permanent("/redfish/v1/").into_response()
|
||||
}
|
||||
|
||||
async fn service_root() -> Json<ServiceRoot> {
|
||||
let uuid = "00000000-0000-0000-0000-000000000001".to_string();
|
||||
|
||||
Json(ServiceRoot {
|
||||
odata_type: "#ServiceRoot.v1_17_0.ServiceRoot".to_string(),
|
||||
odata_id: "/redfish/v1".to_string(),
|
||||
odata_context: "/redfish/v1/$metadata#ServiceRoot.ServiceRoot".to_string(),
|
||||
id: "RootService".to_string(),
|
||||
name: "One-KVM Redfish Service".to_string(),
|
||||
redfish_version: REDFISH_VERSION.to_string(),
|
||||
uuid,
|
||||
protocol_features_supported: ProtocolFeaturesSupported {
|
||||
excerpt_query: false,
|
||||
expand_query: ExpandQuery {
|
||||
expand_all: false,
|
||||
levels: false,
|
||||
max_levels: 0,
|
||||
no_links: false,
|
||||
top: false,
|
||||
},
|
||||
filter_query: false,
|
||||
only_member_query: true,
|
||||
select_query: false,
|
||||
},
|
||||
systems: odata_ref("/redfish/v1/Systems"),
|
||||
chassis: odata_ref("/redfish/v1/Chassis"),
|
||||
managers: odata_ref("/redfish/v1/Managers"),
|
||||
session_service: odata_ref("/redfish/v1/SessionService"),
|
||||
account_service: odata_ref("/redfish/v1/AccountService"),
|
||||
event_service: odata_ref("/redfish/v1/EventService"),
|
||||
links: ServiceRootLinks {
|
||||
sessions: odata_ref("/redfish/v1/SessionService/Sessions"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn odata_document() -> Json<serde_json::Value> {
|
||||
Json(json!({
|
||||
"@odata.context": "/redfish/v1/$metadata",
|
||||
"value": [
|
||||
{ "name": "ServiceRoot", "kind": "Singleton", "url": "/redfish/v1" },
|
||||
{ "name": "Systems", "kind": "Collection", "url": "/redfish/v1/Systems" },
|
||||
{ "name": "Chassis", "kind": "Collection", "url": "/redfish/v1/Chassis" },
|
||||
{ "name": "Managers", "kind": "Collection", "url": "/redfish/v1/Managers" }
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
async fn metadata() -> Response {
|
||||
(
|
||||
[(axum::http::header::CONTENT_TYPE, "application/xml")],
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/ServiceRoot_v1.xml">
|
||||
<edmx:Include Namespace="ServiceRoot"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/ComputerSystem_v1.xml">
|
||||
<edmx:Include Namespace="ComputerSystem"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/Manager_v1.xml">
|
||||
<edmx:Include Namespace="Manager"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/Chassis_v1.xml">
|
||||
<edmx:Include Namespace="Chassis"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/Power_v1.xml">
|
||||
<edmx:Include Namespace="Power"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/VirtualMedia_v1.xml">
|
||||
<edmx:Include Namespace="VirtualMedia"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/SessionService_v1.xml">
|
||||
<edmx:Include Namespace="SessionService"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/AccountService_v1.xml">
|
||||
<edmx:Include Namespace="AccountService"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/EventService_v1.xml">
|
||||
<edmx:Include Namespace="EventService"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/ManagerNetworkProtocol_v1.xml">
|
||||
<edmx:Include Namespace="ManagerNetworkProtocol"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://redfish.dmtf.org/schemas/v1/Role_v1.xml">
|
||||
<edmx:Include Namespace="Role"/>
|
||||
</edmx:Reference>
|
||||
<edmx:Reference Uri="http://docs.oasis-open.org/odata/ns/edm">
|
||||
<edmx:Include Namespace="Edm" />
|
||||
</edmx:Reference>
|
||||
<edmx:DataServices>
|
||||
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="OneKVM">
|
||||
<EntityContainer Name="Service" Extends="ServiceRoot.v1_17_0.ServiceContainer"/>
|
||||
</Schema>
|
||||
</edmx:DataServices>
|
||||
</edmx:Edmx>"#,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub fn create_redfish_router(state: Arc<AppState>) -> Router {
|
||||
let redfish_routes = Router::new()
|
||||
.route("/", get(service_root))
|
||||
.route("/v1", get(service_root_redirect))
|
||||
.route("/v1/", get(service_root))
|
||||
.route("/v1/odata", get(odata_document))
|
||||
.route("/v1/$metadata", get(metadata))
|
||||
.merge(systems::router(state.clone()))
|
||||
.merge(chassis::router(state.clone()))
|
||||
.merge(managers::router(state.clone()))
|
||||
.merge(virtual_media::router(state.clone()))
|
||||
.merge(session::router(state.clone()))
|
||||
.merge(account::router(state.clone()))
|
||||
.merge(event::router(state.clone()))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
redfish_auth_middleware,
|
||||
));
|
||||
|
||||
Router::new()
|
||||
.route("/redfish", get(service_root_redirect))
|
||||
.nest("/redfish/", redfish_routes)
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("odata-version"),
|
||||
HeaderValue::from_static("4.0"),
|
||||
))
|
||||
.with_state(state)
|
||||
}
|
||||
160
src/redfish/routes/session.rs
Normal file
160
src/redfish/routes/session.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{delete, get},
|
||||
Json, Router,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::empty_collection;
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/SessionService", get(session_service))
|
||||
.route(
|
||||
"/v1/SessionService/Sessions",
|
||||
get(session_list).post(session_create),
|
||||
)
|
||||
.route(
|
||||
"/v1/SessionService/Sessions/{session_id}",
|
||||
delete(session_delete),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn session_service() -> Json<SessionService> {
|
||||
Json(SessionService {
|
||||
odata_type: "#SessionService.v1_1_8.SessionService".to_string(),
|
||||
odata_id: "/redfish/v1/SessionService".to_string(),
|
||||
odata_context: "/redfish/v1/$metadata#SessionService.SessionService".to_string(),
|
||||
id: "SessionService".to_string(),
|
||||
name: "Session Service".to_string(),
|
||||
description: "Session Service".to_string(),
|
||||
service_enabled: true,
|
||||
session_timeout: "PT24H".to_string(),
|
||||
sessions: odata_ref("/redfish/v1/SessionService/Sessions"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn session_list(State(state): State<Arc<AppState>>) -> Response {
|
||||
let session_ids = match state.sessions.list_ids().await {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let mut members = Vec::new();
|
||||
for id in &session_ids {
|
||||
if state.sessions.get(id).await.ok().flatten().is_some() {
|
||||
members.push(odata_ref(&format!("/redfish/v1/SessionService/Sessions/{}", id)));
|
||||
}
|
||||
}
|
||||
|
||||
Json(empty_collection(
|
||||
"#SessionCollection.SessionCollection",
|
||||
"/redfish/v1/SessionService/Sessions",
|
||||
"/redfish/v1/$metadata#SessionCollection.SessionCollection",
|
||||
"Session Collection",
|
||||
"Collection of Sessions",
|
||||
members,
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn session_create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SessionCreateRequest>,
|
||||
) -> Response {
|
||||
let user = match state.users.verify(&req.user_name, &req.password).await {
|
||||
Ok(Some(user)) => user,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(RedfishError::invalid_credentials()),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
if !state.config.get().auth.single_user_allow_multiple_sessions {
|
||||
let revoked_ids = state.sessions.list_ids().await.unwrap_or_default();
|
||||
let _ = state.sessions.delete_all().await;
|
||||
state.remember_revoked_sessions(revoked_ids).await;
|
||||
}
|
||||
|
||||
let session = match state.sessions.create(&user.id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
info!("Redfish: Session created for user '{}'", user.username);
|
||||
|
||||
let location = format!("/redfish/v1/SessionService/Sessions/{}", session.id);
|
||||
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
[
|
||||
("X-Auth-Token", session.id.clone()),
|
||||
("Location", location.clone()),
|
||||
],
|
||||
Json(Session {
|
||||
odata_type: "#Session.v1_0_0.Session".to_string(),
|
||||
odata_id: location,
|
||||
odata_context: "/redfish/v1/$metadata#Session.Session".to_string(),
|
||||
id: session.id,
|
||||
name: format!("Session for {}", user.username),
|
||||
description: "Manager User Session".to_string(),
|
||||
user_name: user.username,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn session_delete(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Response {
|
||||
match state.sessions.get(&session_id).await {
|
||||
Ok(Some(_)) => {
|
||||
if let Err(e) = state.sessions.delete(&session_id).await {
|
||||
tracing::warn!("Redfish: Session delete failed: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
info!("Redfish: Session {} deleted", session_id);
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(RedfishError::resource_not_found()),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::warn!("Redfish: Session delete failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
220
src/redfish/routes/systems.rs
Normal file
220
src/redfish/routes/systems.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
use super::{get_power_state, validate_id, service_unavailable, empty_collection, RESOURCE_ID};
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/v1/Systems", get(systems_collection))
|
||||
.route(
|
||||
"/v1/Systems/{system_id}",
|
||||
get(system_detail).patch(system_patch),
|
||||
)
|
||||
.route(
|
||||
"/v1/Systems/{system_id}/Actions/ComputerSystem.Reset",
|
||||
post(system_reset),
|
||||
)
|
||||
.route(
|
||||
"/v1/Systems/{system_id}/Actions/ComputerSystem.SetDefaultBootOrder",
|
||||
post(system_set_default_boot_order),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
fn build_computer_system(system_id: &str, power_state: &str, boot: Boot) -> ComputerSystem {
|
||||
ComputerSystem {
|
||||
odata_type: "#ComputerSystem.v1_20_0.ComputerSystem".to_string(),
|
||||
odata_id: format!("/redfish/v1/Systems/{}", system_id),
|
||||
odata_context: "/redfish/v1/$metadata#ComputerSystem.ComputerSystem".to_string(),
|
||||
odata_etag: "W/\"168\"".to_string(),
|
||||
id: system_id.to_string(),
|
||||
name: "Managed System".to_string(),
|
||||
description: "The managed computer system connected via One-KVM".to_string(),
|
||||
system_type: "Physical".to_string(),
|
||||
asset_tag: String::new(),
|
||||
manufacturer: "Unknown".to_string(),
|
||||
model: "Unknown".to_string(),
|
||||
serial_number: String::new(),
|
||||
part_number: String::new(),
|
||||
power_state: power_state.to_string(),
|
||||
bios_version: "Unknown".to_string(),
|
||||
status: Status::enabled_ok(),
|
||||
boot,
|
||||
processor_summary: ProcessorSummary {
|
||||
count: None,
|
||||
logical_processor_count: None,
|
||||
model: "Unknown".to_string(),
|
||||
status: Status::enabled_ok(),
|
||||
},
|
||||
memory_summary: MemorySummary {
|
||||
total_system_memory_gi_b: None,
|
||||
status: Status::enabled_ok(),
|
||||
},
|
||||
trusted_modules: vec![],
|
||||
actions: ComputerSystemActions {
|
||||
reset: ActionTarget {
|
||||
target: format!(
|
||||
"/redfish/v1/Systems/{}/Actions/ComputerSystem.Reset",
|
||||
system_id
|
||||
),
|
||||
},
|
||||
set_default_boot_order: ActionTarget {
|
||||
target: format!(
|
||||
"/redfish/v1/Systems/{}/Actions/ComputerSystem.SetDefaultBootOrder",
|
||||
system_id
|
||||
),
|
||||
},
|
||||
},
|
||||
links: ComputerSystemLinks {
|
||||
chassis: vec![odata_ref(&format!("/redfish/v1/Chassis/{}", RESOURCE_ID))],
|
||||
managed_by: vec![odata_ref(&format!("/redfish/v1/Managers/{}", RESOURCE_ID))],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn systems_collection() -> Json<Collection<ODataLink>> {
|
||||
Json(empty_collection(
|
||||
"#ComputerSystemCollection.ComputerSystemCollection",
|
||||
"/redfish/v1/Systems",
|
||||
"/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection",
|
||||
"Computer System Collection",
|
||||
"Collection of Computer Systems",
|
||||
vec![odata_ref("/redfish/v1/Systems/1")],
|
||||
))
|
||||
}
|
||||
|
||||
async fn system_detail(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(system_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&system_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
let power_state = get_power_state(&state).await;
|
||||
let system = build_computer_system(
|
||||
&system_id,
|
||||
power_state,
|
||||
Boot {
|
||||
boot_source_override_enabled: "Disabled".to_string(),
|
||||
boot_source_override_mode: None,
|
||||
boot_source_override_target: None,
|
||||
uefi_target_boot_source_override: None,
|
||||
},
|
||||
);
|
||||
|
||||
Json(system).into_response()
|
||||
}
|
||||
|
||||
async fn system_patch(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(system_id): Path<String>,
|
||||
Json(req): Json<ComputerSystemPatchRequest>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&system_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
if let Some(boot) = &req.boot {
|
||||
if let Some(target) = &boot.boot_source_override_target {
|
||||
info!(
|
||||
"Redfish: PATCH Systems/{} BootSourceOverrideTarget='{}' (accepted, no-op)",
|
||||
system_id, target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let power_state = get_power_state(&state).await;
|
||||
let boot = match req.boot {
|
||||
Some(b) => Boot {
|
||||
boot_source_override_enabled: b
|
||||
.boot_source_override_enabled
|
||||
.unwrap_or_else(|| "Disabled".to_string()),
|
||||
boot_source_override_mode: b.boot_source_override_mode,
|
||||
boot_source_override_target: b.boot_source_override_target,
|
||||
uefi_target_boot_source_override: b.uefi_target_boot_source_override,
|
||||
},
|
||||
None => Boot {
|
||||
boot_source_override_enabled: "Disabled".to_string(),
|
||||
boot_source_override_mode: None,
|
||||
boot_source_override_target: None,
|
||||
uefi_target_boot_source_override: None,
|
||||
},
|
||||
};
|
||||
|
||||
let system = build_computer_system(&system_id, power_state, boot);
|
||||
Json(system).into_response()
|
||||
}
|
||||
|
||||
async fn system_reset(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(system_id): Path<String>,
|
||||
Json(req): Json<ResetRequest>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&system_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
let result = {
|
||||
let guard = state.atx.read().await;
|
||||
let atx = match guard.as_ref() {
|
||||
Some(atx) => atx,
|
||||
None => return service_unavailable("ATX power control not available"),
|
||||
};
|
||||
|
||||
match req.reset_type.as_str() {
|
||||
"On" | "ForceOn" | "PushPowerButton" => atx.power_short().await,
|
||||
"ForceOff" | "GracefulShutdown" => atx.power_long().await,
|
||||
"ForceRestart" | "GracefulRestart" | "PowerCycle" => atx.reset().await,
|
||||
"Nmi" => {
|
||||
return (
|
||||
StatusCode::NOT_ACCEPTABLE,
|
||||
Json(RedfishError::action_not_supported("Nmi")),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::NOT_ACCEPTABLE,
|
||||
Json(RedfishError::action_not_supported(&req.reset_type)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("Redfish: System reset '{}' executed", req.reset_type);
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Redfish: System reset failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn system_set_default_boot_order(
|
||||
Path(system_id): Path<String>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&system_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
info!("Redfish: SetDefaultBootOrder for system {} (accepted, no-op)", system_id);
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
224
src/redfish/routes/virtual_media.rs
Normal file
224
src/redfish/routes/virtual_media.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{empty_collection, validate_id, service_unavailable, resource_not_found, RESOURCE_ID};
|
||||
use super::super::schema::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/v1/Managers/{manager_id}/VirtualMedia",
|
||||
get(virtual_media_collection),
|
||||
)
|
||||
.route(
|
||||
"/v1/Managers/{manager_id}/VirtualMedia/{media_id}",
|
||||
get(virtual_media_detail),
|
||||
)
|
||||
.route(
|
||||
"/v1/Managers/{manager_id}/VirtualMedia/{media_id}/Actions/VirtualMedia.InsertMedia",
|
||||
post(virtual_media_insert),
|
||||
)
|
||||
.route(
|
||||
"/v1/Managers/{manager_id}/VirtualMedia/{media_id}/Actions/VirtualMedia.EjectMedia",
|
||||
post(virtual_media_eject),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn virtual_media_collection(Path(manager_id): Path<String>) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Json(empty_collection(
|
||||
"#VirtualMediaCollection.VirtualMediaCollection",
|
||||
&format!("/redfish/v1/Managers/{}/VirtualMedia", manager_id),
|
||||
"/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection",
|
||||
"Virtual Media Collection",
|
||||
"Collection of Virtual Media",
|
||||
vec![odata_ref(&format!(
|
||||
"/redfish/v1/Managers/{}/VirtualMedia/{}",
|
||||
manager_id, RESOURCE_ID
|
||||
))],
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn virtual_media_detail(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((manager_id, media_id)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
if media_id != RESOURCE_ID {
|
||||
return resource_not_found();
|
||||
}
|
||||
|
||||
let (inserted, image_name, connected_via) = {
|
||||
let guard = state.msd.read().await;
|
||||
match guard.as_ref() {
|
||||
Some(msd) => {
|
||||
let msd_state = msd.state().await;
|
||||
let img_name = msd_state
|
||||
.current_image
|
||||
.as_ref()
|
||||
.map(|i| i.name.clone())
|
||||
.or_else(|| {
|
||||
msd_state
|
||||
.drive_info
|
||||
.as_ref()
|
||||
.map(|_| "Virtual Drive".to_string())
|
||||
});
|
||||
(
|
||||
msd_state.connected,
|
||||
img_name,
|
||||
if msd_state.connected {
|
||||
Some("Applet".to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
}
|
||||
None => (false, None, None),
|
||||
}
|
||||
};
|
||||
|
||||
Json(VirtualMedia {
|
||||
odata_type: "#VirtualMedia.v1_6_2.VirtualMedia".to_string(),
|
||||
odata_id: format!("/redfish/v1/Managers/{}/VirtualMedia/{}", manager_id, media_id),
|
||||
odata_context: "/redfish/v1/$metadata#VirtualMedia.VirtualMedia".to_string(),
|
||||
id: media_id.clone(),
|
||||
name: "Virtual Media 1".to_string(),
|
||||
description: "Virtual Media Device".to_string(),
|
||||
media_types: vec!["CD".to_string(), "USBStick".to_string()],
|
||||
connected_via: connected_via,
|
||||
inserted: inserted,
|
||||
image: None,
|
||||
image_name: image_name,
|
||||
write_protected: true,
|
||||
transfer_method: None,
|
||||
transfer_protocol_type: None,
|
||||
status: if inserted {
|
||||
Status::enabled_ok()
|
||||
} else {
|
||||
Status::disabled_ok()
|
||||
},
|
||||
actions: VirtualMediaActions {
|
||||
insert_media: ActionTarget {
|
||||
target: format!(
|
||||
"/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.InsertMedia",
|
||||
manager_id, media_id
|
||||
),
|
||||
},
|
||||
eject_media: ActionTarget {
|
||||
target: format!(
|
||||
"/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.EjectMedia",
|
||||
manager_id, media_id
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn virtual_media_insert(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((manager_id, media_id)): Path<(String, String)>,
|
||||
Json(req): Json<InsertMediaRequest>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
if media_id != RESOURCE_ID {
|
||||
return resource_not_found();
|
||||
}
|
||||
|
||||
let result = {
|
||||
let guard = state.msd.read().await;
|
||||
let msd = match guard.as_ref() {
|
||||
Some(msd) => msd,
|
||||
None => return service_unavailable("MSD not available"),
|
||||
};
|
||||
|
||||
if msd.state().await.connected {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(RedfishError::general_error("Virtual media already inserted")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
info!("Redfish: VirtualMedia.InsertMedia image='{}'", req.image);
|
||||
msd.connect_drive().await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("Redfish: VirtualMedia.InsertMedia executed");
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Redfish: VirtualMedia.InsertMedia failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn virtual_media_eject(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((manager_id, media_id)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
if let Some(resp) = validate_id(&manager_id) {
|
||||
return resp;
|
||||
}
|
||||
if media_id != RESOURCE_ID {
|
||||
return resource_not_found();
|
||||
}
|
||||
|
||||
let result = {
|
||||
let guard = state.msd.read().await;
|
||||
let msd = match guard.as_ref() {
|
||||
Some(msd) => msd,
|
||||
None => return service_unavailable("MSD not available"),
|
||||
};
|
||||
|
||||
if !msd.state().await.connected {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(RedfishError::general_error("No virtual media inserted")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
msd.disconnect().await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("Redfish: VirtualMedia.EjectMedia executed");
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Redfish: VirtualMedia.EjectMedia failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RedfishError::general_error(&e.to_string())),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
627
src/redfish/schema.rs
Normal file
627
src/redfish/schema.rs
Normal file
@@ -0,0 +1,627 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn odata_ref(id: &str) -> ODataLink {
|
||||
ODataLink {
|
||||
odata_id: id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ODataLink {
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Status {
|
||||
pub state: String,
|
||||
pub health: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub health_rollup: Option<String>,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn enabled_ok() -> Self {
|
||||
Self {
|
||||
state: "Enabled".to_string(),
|
||||
health: "OK".to_string(),
|
||||
health_rollup: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled_health(health: &str) -> Self {
|
||||
Self {
|
||||
state: "Enabled".to_string(),
|
||||
health: health.to_string(),
|
||||
health_rollup: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled_ok() -> Self {
|
||||
Self {
|
||||
state: "Disabled".to_string(),
|
||||
health: "OK".to_string(),
|
||||
health_rollup: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offline_ok() -> Self {
|
||||
Self {
|
||||
state: "Offline".to_string(),
|
||||
health: "OK".to_string(),
|
||||
health_rollup: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ServiceRoot {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub redfish_version: String,
|
||||
#[serde(rename = "UUID")]
|
||||
pub uuid: String,
|
||||
pub protocol_features_supported: ProtocolFeaturesSupported,
|
||||
pub systems: ODataLink,
|
||||
pub chassis: ODataLink,
|
||||
pub managers: ODataLink,
|
||||
pub session_service: ODataLink,
|
||||
pub account_service: ODataLink,
|
||||
pub event_service: ODataLink,
|
||||
pub links: ServiceRootLinks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ProtocolFeaturesSupported {
|
||||
pub excerpt_query: bool,
|
||||
pub expand_query: ExpandQuery,
|
||||
pub filter_query: bool,
|
||||
pub only_member_query: bool,
|
||||
pub select_query: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ExpandQuery {
|
||||
pub expand_all: bool,
|
||||
pub levels: bool,
|
||||
pub max_levels: u32,
|
||||
pub no_links: bool,
|
||||
pub top: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ServiceRootLinks {
|
||||
pub sessions: ODataLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Collection<T: Serialize> {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(rename = "Members@odata.count")]
|
||||
pub members_count: u64,
|
||||
pub members: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ComputerSystem {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
#[serde(rename = "@odata.etag")]
|
||||
pub odata_etag: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub system_type: String,
|
||||
pub asset_tag: String,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub serial_number: String,
|
||||
pub part_number: String,
|
||||
pub power_state: String,
|
||||
pub bios_version: String,
|
||||
pub status: Status,
|
||||
pub boot: Boot,
|
||||
pub processor_summary: ProcessorSummary,
|
||||
pub memory_summary: MemorySummary,
|
||||
pub trusted_modules: Vec<Value>,
|
||||
pub actions: ComputerSystemActions,
|
||||
pub links: ComputerSystemLinks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Boot {
|
||||
pub boot_source_override_enabled: String,
|
||||
pub boot_source_override_mode: Option<String>,
|
||||
pub boot_source_override_target: Option<String>,
|
||||
pub uefi_target_boot_source_override: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ProcessorSummary {
|
||||
pub count: Option<u32>,
|
||||
pub logical_processor_count: Option<u32>,
|
||||
pub model: String,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct MemorySummary {
|
||||
pub total_system_memory_gi_b: Option<f64>,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ComputerSystemActions {
|
||||
#[serde(rename = "#ComputerSystem.Reset")]
|
||||
pub reset: ActionTarget,
|
||||
#[serde(rename = "#ComputerSystem.SetDefaultBootOrder")]
|
||||
pub set_default_boot_order: ActionTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ActionTarget {
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ComputerSystemLinks {
|
||||
pub chassis: Vec<ODataLink>,
|
||||
pub managed_by: Vec<ODataLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ResetRequest {
|
||||
#[serde(default = "default_reset_type")]
|
||||
pub reset_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ComputerSystemPatchRequest {
|
||||
#[serde(default)]
|
||||
pub boot: Option<BootPatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct BootPatch {
|
||||
#[serde(default)]
|
||||
pub boot_source_override_enabled: Option<String>,
|
||||
#[serde(default)]
|
||||
pub boot_source_override_target: Option<String>,
|
||||
#[serde(default)]
|
||||
pub boot_source_override_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub uefi_target_boot_source_override: Option<String>,
|
||||
}
|
||||
|
||||
fn default_reset_type() -> String {
|
||||
"ForceRestart".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Manager {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub manager_type: String,
|
||||
pub status: Status,
|
||||
pub firmware_version: String,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub date_time: String,
|
||||
pub date_time_local_offset: String,
|
||||
pub service_entry_point_uuid: String,
|
||||
pub command_shell: CommandShell,
|
||||
pub graphical_console: GraphicalConsole,
|
||||
pub virtual_media: ODataLink,
|
||||
pub links: ManagerLinks,
|
||||
pub network_protocol: ODataLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct CommandShell {
|
||||
pub service_enabled: bool,
|
||||
pub max_concurrent_sessions: u32,
|
||||
pub connect_types_supported: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct GraphicalConsole {
|
||||
pub service_enabled: bool,
|
||||
pub max_concurrent_sessions: u32,
|
||||
pub connect_types_supported: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ManagerLinks {
|
||||
pub manager_for_servers: Vec<ODataLink>,
|
||||
pub manager_for_chassis: Vec<ODataLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct VirtualMedia {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub media_types: Vec<String>,
|
||||
pub connected_via: Option<String>,
|
||||
pub inserted: bool,
|
||||
pub image: Option<String>,
|
||||
pub image_name: Option<String>,
|
||||
pub write_protected: bool,
|
||||
pub transfer_method: Option<String>,
|
||||
pub transfer_protocol_type: Option<String>,
|
||||
pub status: Status,
|
||||
pub actions: VirtualMediaActions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct VirtualMediaActions {
|
||||
#[serde(rename = "#VirtualMedia.InsertMedia")]
|
||||
pub insert_media: ActionTarget,
|
||||
#[serde(rename = "#VirtualMedia.EjectMedia")]
|
||||
pub eject_media: ActionTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct InsertMediaRequest {
|
||||
pub image: String,
|
||||
#[serde(default)]
|
||||
pub write_protected: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub transfer_method: Option<String>,
|
||||
#[serde(default)]
|
||||
pub transfer_protocol_type: Option<String>,
|
||||
pub media_types: Option<Vec<String>>,
|
||||
pub inserted: Option<bool>,
|
||||
pub user_name: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Chassis {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub chassis_type: String,
|
||||
pub asset_tag: String,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub serial_number: String,
|
||||
pub part_number: String,
|
||||
pub power_state: String,
|
||||
pub status: Status,
|
||||
pub power: ODataLink,
|
||||
pub links: ChassisLinks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ChassisLinks {
|
||||
pub computer_systems: Vec<ODataLink>,
|
||||
pub managed_by: Vec<ODataLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Power {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub power_control: Vec<PowerControl>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct PowerControl {
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
pub member_id: String,
|
||||
pub name: String,
|
||||
pub power_consumed_watts: Option<f64>,
|
||||
pub power_capacity_watts: Option<f64>,
|
||||
pub power_requested_watts: Option<f64>,
|
||||
pub power_metrics: Option<PowerMetric>,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct PowerMetric {
|
||||
pub interval_in_min: u32,
|
||||
pub min_consumed_watts: Option<f64>,
|
||||
pub max_consumed_watts: Option<f64>,
|
||||
pub average_consumed_watts: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct SessionService {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub service_enabled: bool,
|
||||
pub session_timeout: String,
|
||||
pub sessions: ODataLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Session {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub user_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct SessionCreateRequest {
|
||||
pub user_name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AccountService {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub service_enabled: bool,
|
||||
pub accounts: ODataLink,
|
||||
pub roles: ODataLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ManagerAccount {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub enabled: bool,
|
||||
pub user_name: String,
|
||||
pub role_id: String,
|
||||
pub locked: bool,
|
||||
pub links: ManagerAccountLinks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ManagerAccountLinks {
|
||||
pub role: ODataLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct EventService {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
#[serde(rename = "@odata.id")]
|
||||
pub odata_id: String,
|
||||
#[serde(rename = "@odata.context")]
|
||||
pub odata_context: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub service_enabled: bool,
|
||||
pub delivery_retry_attempts: u32,
|
||||
pub delivery_retry_interval_seconds: u32,
|
||||
pub event_format_types: Vec<String>,
|
||||
pub registry_prefixes: Vec<String>,
|
||||
pub subordinate_resources: bool,
|
||||
#[serde(rename = "SSEFilterPropertiesSupported")]
|
||||
pub sse_filter_properties_supported: SseFilterPropertiesSupported,
|
||||
pub server_sent_event_uri: Option<String>,
|
||||
pub actions: EventServiceActions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct SseFilterPropertiesSupported {
|
||||
pub event_format_type: bool,
|
||||
pub message_id: bool,
|
||||
pub metric_report_definition: bool,
|
||||
pub origin_resource: bool,
|
||||
pub registry_prefix: bool,
|
||||
pub resource_type: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EventServiceActions {
|
||||
#[serde(rename = "#EventService.SubmitTestEvent")]
|
||||
pub submit_test_event: ActionTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RedfishError {
|
||||
pub error: RedfishErrorBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RedfishErrorBody {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
#[serde(rename = "@Message.ExtendedInfo", skip_serializing_if = "Vec::is_empty")]
|
||||
pub extended_info: Vec<RedfishExtendedInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct RedfishExtendedInfo {
|
||||
#[serde(rename = "@odata.type")]
|
||||
pub odata_type: String,
|
||||
pub message_id: String,
|
||||
pub message: String,
|
||||
pub severity: String,
|
||||
pub resolution: String,
|
||||
}
|
||||
|
||||
impl RedfishError {
|
||||
pub fn general_error(message: &str) -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.GeneralError".to_string(),
|
||||
message: message.to_string(),
|
||||
extended_info: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authentication_required() -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.AuthenticationRequired".to_string(),
|
||||
message: "Authentication is required to access this resource".to_string(),
|
||||
extended_info: vec![RedfishExtendedInfo {
|
||||
odata_type: "#Message.v1_2_1.Message".to_string(),
|
||||
message_id: "Base.1.18.AuthenticationRequired".to_string(),
|
||||
message: "Authentication is required to access this resource".to_string(),
|
||||
severity: "Critical".to_string(),
|
||||
resolution: "Authenticate using HTTP Basic auth or create a session via POST /redfish/v1/SessionService/Sessions".to_string(),
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_credentials() -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.AuthenticationRequired".to_string(),
|
||||
message: "Invalid username or password".to_string(),
|
||||
extended_info: vec![RedfishExtendedInfo {
|
||||
odata_type: "#Message.v1_2_1.Message".to_string(),
|
||||
message_id: "Base.1.18.InvalidCredentials".to_string(),
|
||||
message: "Invalid username or password".to_string(),
|
||||
severity: "Critical".to_string(),
|
||||
resolution: "Correct the credentials and retry".to_string(),
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_not_found() -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.ResourceNotFound".to_string(),
|
||||
message: "The requested resource was not found".to_string(),
|
||||
extended_info: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_not_supported(action: &str) -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.ActionNotSupported".to_string(),
|
||||
message: format!("Action '{}' is not supported", action),
|
||||
extended_info: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn property_missing(property: &str) -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.PropertyMissing".to_string(),
|
||||
message: format!("Property '{}' is required", property),
|
||||
extended_info: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn service_unavailable(msg: &str) -> Self {
|
||||
Self {
|
||||
error: RedfishErrorBody {
|
||||
code: "Base.1.18.ServiceUnavailable".to_string(),
|
||||
message: msg.to_string(),
|
||||
extended_info: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user