mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 15:31:46 +08:00
featt: 增加 redfish 身份验证,完善 redfish 虚拟媒体资源管理能力
This commit is contained in:
@@ -80,3 +80,30 @@ fn decode_basic_auth(encoded: &str) -> Option<(String, String)> {
|
|||||||
}
|
}
|
||||||
Some((username, password))
|
Some((username, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_service_discovery_and_session_creation_are_public() {
|
||||||
|
assert!(is_redfish_public_endpoint("/v1/", &Method::GET));
|
||||||
|
assert!(is_redfish_public_endpoint(
|
||||||
|
"/v1/$metadata",
|
||||||
|
&Method::GET
|
||||||
|
));
|
||||||
|
assert!(is_redfish_public_endpoint(
|
||||||
|
"/v1/SessionService/Sessions",
|
||||||
|
&Method::POST
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(!is_redfish_public_endpoint(
|
||||||
|
"/v1/Managers/1/VirtualMedia",
|
||||||
|
&Method::GET
|
||||||
|
));
|
||||||
|
assert!(!is_redfish_public_endpoint(
|
||||||
|
"/v1/Managers/1/VirtualMedia/1/Actions/VirtualMedia.EjectMedia",
|
||||||
|
&Method::POST
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async fn event_service() -> Json<EventService> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn event_service_sse(State(state): State<Arc<AppState>>) -> Response {
|
async fn event_service_sse(State(state): State<Arc<AppState>>) -> Response {
|
||||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
use axum::response::sse::{Event, Sse};
|
||||||
|
|
||||||
let mut device_info_rx = state.subscribe_device_info();
|
let mut device_info_rx = state.subscribe_device_info();
|
||||||
|
|
||||||
@@ -87,15 +87,25 @@ async fn event_service_sse(State(state): State<Arc<AppState>>) -> Response {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Sse::new(Box::pin(stream))
|
Sse::new(Box::pin(stream))
|
||||||
.keep_alive(
|
.keep_alive(redfish_keep_alive())
|
||||||
KeepAlive::new()
|
|
||||||
.interval(Duration::from_secs(30))
|
|
||||||
.text(":\n"),
|
|
||||||
)
|
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn redfish_keep_alive() -> axum::response::sse::KeepAlive {
|
||||||
|
axum::response::sse::KeepAlive::new().interval(Duration::from_secs(30))
|
||||||
|
}
|
||||||
|
|
||||||
async fn event_submit_test() -> StatusCode {
|
async fn event_submit_test() -> StatusCode {
|
||||||
info!("Redfish: SubmitTestEvent received (no-op)");
|
info!("Redfish: SubmitTestEvent received (no-op)");
|
||||||
StatusCode::NO_CONTENT
|
StatusCode::NO_CONTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keep_alive_configuration_does_not_panic() {
|
||||||
|
let _ = redfish_keep_alive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,15 +194,16 @@ pub fn create_redfish_router(state: Arc<AppState>) -> Router {
|
|||||||
.merge(managers::router(state.clone()))
|
.merge(managers::router(state.clone()))
|
||||||
.merge(session::router(state.clone()))
|
.merge(session::router(state.clone()))
|
||||||
.merge(account::router(state.clone()))
|
.merge(account::router(state.clone()))
|
||||||
.merge(event::router(state.clone()))
|
.merge(event::router(state.clone()));
|
||||||
.layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
redfish_auth_middleware,
|
|
||||||
));
|
|
||||||
|
|
||||||
#[cfg(all(unix, not(feature = "android")))]
|
#[cfg(all(unix, not(feature = "android")))]
|
||||||
let redfish_routes = redfish_routes.merge(virtual_media::router(state.clone()));
|
let redfish_routes = redfish_routes.merge(virtual_media::router(state.clone()));
|
||||||
|
|
||||||
|
let redfish_routes = redfish_routes.layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
redfish_auth_middleware,
|
||||||
|
));
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/redfish", get(service_root_redirect))
|
.route("/redfish", get(service_root_redirect))
|
||||||
.nest("/redfish/", redfish_routes)
|
.nest("/redfish/", redfish_routes)
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ use axum::{
|
|||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::super::schema::*;
|
use super::super::schema::*;
|
||||||
use super::{empty_collection, resource_not_found, service_unavailable, validate_id, RESOURCE_ID};
|
use super::{empty_collection, resource_not_found, service_unavailable, validate_id};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::msd::{ImageInfo, ImageManager, MountedMedia, MountedMediaKind};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
@@ -34,21 +35,37 @@ pub(crate) fn router(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
|||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn virtual_media_collection(Path(manager_id): Path<String>) -> Response {
|
async fn virtual_media_collection(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(manager_id): Path<String>,
|
||||||
|
) -> Response {
|
||||||
if let Some(resp) = validate_id(&manager_id) {
|
if let Some(resp) = validate_id(&manager_id) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let capacity = {
|
||||||
|
let guard = state.msd.read().await;
|
||||||
|
let Some(msd) = guard.as_ref() else {
|
||||||
|
return service_unavailable("MSD not available");
|
||||||
|
};
|
||||||
|
msd.state().await.disk_mode.capacity()
|
||||||
|
};
|
||||||
|
let members = (1..=capacity)
|
||||||
|
.map(|slot| {
|
||||||
|
odata_ref(&format!(
|
||||||
|
"/redfish/v1/Managers/{}/VirtualMedia/{}",
|
||||||
|
manager_id, slot
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Json(empty_collection(
|
Json(empty_collection(
|
||||||
"#VirtualMediaCollection.VirtualMediaCollection",
|
"#VirtualMediaCollection.VirtualMediaCollection",
|
||||||
&format!("/redfish/v1/Managers/{}/VirtualMedia", manager_id),
|
&format!("/redfish/v1/Managers/{}/VirtualMedia", manager_id),
|
||||||
"/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection",
|
"/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection",
|
||||||
"Virtual Media Collection",
|
"Virtual Media Collection",
|
||||||
"Collection of Virtual Media",
|
"Collection of Virtual Media",
|
||||||
vec![odata_ref(&format!(
|
members,
|
||||||
"/redfish/v1/Managers/{}/VirtualMedia/{}",
|
|
||||||
manager_id, RESOURCE_ID
|
|
||||||
))],
|
|
||||||
))
|
))
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
@@ -60,50 +77,58 @@ async fn virtual_media_detail(
|
|||||||
if let Some(resp) = validate_id(&manager_id) {
|
if let Some(resp) = validate_id(&manager_id) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if media_id != RESOURCE_ID {
|
|
||||||
return resource_not_found();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (inserted, image_name, connected_via) = {
|
let (msd_state, lun) = {
|
||||||
let guard = state.msd.read().await;
|
let guard = state.msd.read().await;
|
||||||
match guard.as_ref() {
|
let Some(msd) = guard.as_ref() else {
|
||||||
Some(msd) => {
|
return service_unavailable("MSD not available");
|
||||||
let msd_state = msd.state().await;
|
};
|
||||||
let img_name = msd_state
|
let msd_state = msd.state().await;
|
||||||
.mounted_media
|
let Some(lun) = parse_slot_id(&media_id, msd_state.disk_mode.capacity()) else {
|
||||||
.first()
|
return resource_not_found();
|
||||||
.map(|media| media.name.clone());
|
};
|
||||||
(
|
(msd_state, lun)
|
||||||
!msd_state.mounted_media.is_empty(),
|
};
|
||||||
img_name,
|
let media = msd_state
|
||||||
if !msd_state.mounted_media.is_empty() {
|
.mounted_media
|
||||||
Some("Applet".to_string())
|
.iter()
|
||||||
} else {
|
.find(|media| media.lun == lun);
|
||||||
None
|
|
||||||
},
|
Json(virtual_media_resource(&manager_id, &media_id, media)).into_response()
|
||||||
)
|
}
|
||||||
}
|
|
||||||
None => (false, None, None),
|
fn virtual_media_resource(
|
||||||
}
|
manager_id: &str,
|
||||||
|
media_id: &str,
|
||||||
|
media: Option<&MountedMedia>,
|
||||||
|
) -> VirtualMedia {
|
||||||
|
let inserted = media.is_some();
|
||||||
|
let is_image = media.is_some_and(|media| media.kind == MountedMediaKind::Image);
|
||||||
|
let media_types = match media {
|
||||||
|
Some(media) if media.cdrom => vec!["CD".to_string(), "DVD".to_string()],
|
||||||
|
Some(_) => vec!["USBStick".to_string()],
|
||||||
|
None => vec!["CD".to_string(), "DVD".to_string(), "USBStick".to_string()],
|
||||||
};
|
};
|
||||||
|
|
||||||
Json(VirtualMedia {
|
VirtualMedia {
|
||||||
odata_type: "#VirtualMedia.v1_6_2.VirtualMedia".to_string(),
|
odata_type: "#VirtualMedia.v1_6_2.VirtualMedia".to_string(),
|
||||||
odata_id: format!(
|
odata_id: format!(
|
||||||
"/redfish/v1/Managers/{}/VirtualMedia/{}",
|
"/redfish/v1/Managers/{}/VirtualMedia/{}",
|
||||||
manager_id, media_id
|
manager_id, media_id
|
||||||
),
|
),
|
||||||
odata_context: "/redfish/v1/$metadata#VirtualMedia.VirtualMedia".to_string(),
|
odata_context: "/redfish/v1/$metadata#VirtualMedia.VirtualMedia".to_string(),
|
||||||
id: media_id.clone(),
|
id: media_id.to_string(),
|
||||||
name: "Virtual Media 1".to_string(),
|
name: format!("Virtual Media Slot {}", media_id),
|
||||||
description: "Virtual Media Device".to_string(),
|
description: "Virtual Media Slot".to_string(),
|
||||||
media_types: vec!["CD".to_string(), "USBStick".to_string()],
|
media_types,
|
||||||
connected_via: connected_via,
|
connected_via: media.map(|_| if is_image { "URI" } else { "Applet" }.to_string()),
|
||||||
inserted: inserted,
|
inserted,
|
||||||
image: None,
|
image: media
|
||||||
image_name: image_name,
|
.filter(|_| is_image)
|
||||||
write_protected: true,
|
.map(|media| format!("/api/msd/images/{}", media.id)),
|
||||||
transfer_method: None,
|
image_name: media.map(|media| media.name.clone()),
|
||||||
|
write_protected: media.is_none_or(|media| media.read_only),
|
||||||
|
transfer_method: is_image.then(|| "Upload".to_string()),
|
||||||
transfer_protocol_type: None,
|
transfer_protocol_type: None,
|
||||||
status: if inserted {
|
status: if inserted {
|
||||||
Status::enabled_ok()
|
Status::enabled_ok()
|
||||||
@@ -124,8 +149,7 @@ async fn virtual_media_detail(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn virtual_media_insert(
|
async fn virtual_media_insert(
|
||||||
@@ -136,43 +160,53 @@ async fn virtual_media_insert(
|
|||||||
if let Some(resp) = validate_id(&manager_id) {
|
if let Some(resp) = validate_id(&manager_id) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if media_id != RESOURCE_ID {
|
|
||||||
return resource_not_found();
|
let lun = {
|
||||||
|
let guard = state.msd.read().await;
|
||||||
|
let Some(msd) = guard.as_ref() else {
|
||||||
|
return service_unavailable("MSD not available");
|
||||||
|
};
|
||||||
|
let msd_state = msd.state().await;
|
||||||
|
let Some(lun) = parse_slot_id(&media_id, msd_state.disk_mode.capacity()) else {
|
||||||
|
return resource_not_found();
|
||||||
|
};
|
||||||
|
if msd_state.mounted_media.iter().any(|media| media.lun == lun) {
|
||||||
|
return redfish_error(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"Virtual media slot is already occupied",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lun
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = validate_insert_request(&req) {
|
||||||
|
return app_error_response(error);
|
||||||
}
|
}
|
||||||
|
let image = match resolve_image(&state, &req).await {
|
||||||
|
Ok(image) => image,
|
||||||
|
Err(error) => return app_error_response(error),
|
||||||
|
};
|
||||||
|
let (cdrom, read_only) = match mount_options(&req, &image.name) {
|
||||||
|
Ok(options) => options,
|
||||||
|
Err(error) => return app_error_response(error),
|
||||||
|
};
|
||||||
|
|
||||||
let result = {
|
let result = {
|
||||||
let guard = state.msd.read().await;
|
let guard = state.msd.read().await;
|
||||||
let msd = match guard.as_ref() {
|
let Some(msd) = guard.as_ref() else {
|
||||||
Some(msd) => msd,
|
return service_unavailable("MSD not available");
|
||||||
None => return service_unavailable("MSD not available"),
|
|
||||||
};
|
};
|
||||||
|
msd.mount_image_at_lun(&image, cdrom, read_only, lun).await
|
||||||
if !msd.state().await.mounted_media.is_empty() {
|
|
||||||
return (
|
|
||||||
StatusCode::CONFLICT,
|
|
||||||
Json(RedfishError::general_error(
|
|
||||||
"Virtual media already inserted",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Redfish: VirtualMedia.InsertMedia image='{}'", req.image);
|
|
||||||
msd.mount_drive().await
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
info!("Redfish: VirtualMedia.InsertMedia executed");
|
info!(slot = %media_id, image = %image.name, "Redfish virtual media inserted");
|
||||||
StatusCode::NO_CONTENT.into_response()
|
StatusCode::NO_CONTENT.into_response()
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(error) => {
|
||||||
warn!("Redfish: VirtualMedia.InsertMedia failed: {}", e);
|
warn!(slot = %media_id, %error, "Redfish virtual media insert failed");
|
||||||
(
|
app_error_response(error)
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
Json(RedfishError::general_error(&e.to_string())),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,40 +218,207 @@ async fn virtual_media_eject(
|
|||||||
if let Some(resp) = validate_id(&manager_id) {
|
if let Some(resp) = validate_id(&manager_id) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
if media_id != RESOURCE_ID {
|
|
||||||
return resource_not_found();
|
let lun = {
|
||||||
}
|
let guard = state.msd.read().await;
|
||||||
|
let Some(msd) = guard.as_ref() else {
|
||||||
|
return service_unavailable("MSD not available");
|
||||||
|
};
|
||||||
|
let capacity = msd.state().await.disk_mode.capacity();
|
||||||
|
let Some(lun) = parse_slot_id(&media_id, capacity) else {
|
||||||
|
return resource_not_found();
|
||||||
|
};
|
||||||
|
lun
|
||||||
|
};
|
||||||
|
|
||||||
let result = {
|
let result = {
|
||||||
let guard = state.msd.read().await;
|
let guard = state.msd.read().await;
|
||||||
let msd = match guard.as_ref() {
|
let Some(msd) = guard.as_ref() else {
|
||||||
Some(msd) => msd,
|
return service_unavailable("MSD not available");
|
||||||
None => return service_unavailable("MSD not available"),
|
|
||||||
};
|
};
|
||||||
|
msd.unmount_lun(lun).await
|
||||||
if msd.state().await.mounted_media.is_empty() {
|
|
||||||
return (
|
|
||||||
StatusCode::CONFLICT,
|
|
||||||
Json(RedfishError::general_error("No virtual media inserted")),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
msd.disconnect().await
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(true) => {
|
||||||
info!("Redfish: VirtualMedia.EjectMedia executed");
|
info!(slot = %media_id, "Redfish virtual media ejected");
|
||||||
StatusCode::NO_CONTENT.into_response()
|
StatusCode::NO_CONTENT.into_response()
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Ok(false) => redfish_error(
|
||||||
warn!("Redfish: VirtualMedia.EjectMedia failed: {}", e);
|
StatusCode::CONFLICT,
|
||||||
(
|
"No virtual media inserted in this slot",
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
),
|
||||||
Json(RedfishError::general_error(&e.to_string())),
|
Err(error) => {
|
||||||
)
|
warn!(slot = %media_id, %error, "Redfish virtual media eject failed");
|
||||||
.into_response()
|
app_error_response(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_slot_id(media_id: &str, capacity: u8) -> Option<u8> {
|
||||||
|
media_id
|
||||||
|
.parse::<u8>()
|
||||||
|
.ok()?
|
||||||
|
.checked_sub(1)
|
||||||
|
.filter(|lun| *lun < capacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_insert_request(req: &InsertMediaRequest) -> Result<(), AppError> {
|
||||||
|
if req.inserted == Some(false) {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Inserted=false is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if req
|
||||||
|
.transfer_method
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|method| !method.eq_ignore_ascii_case("Upload"))
|
||||||
|
{
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Only TransferMethod=Upload is supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_options(req: &InsertMediaRequest, image_name: &str) -> Result<(bool, bool), AppError> {
|
||||||
|
let requested_type = req
|
||||||
|
.media_types
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|types| types.first())
|
||||||
|
.map(|value| value.as_str());
|
||||||
|
let cdrom = match requested_type {
|
||||||
|
Some(value) if value.eq_ignore_ascii_case("CD") || value.eq_ignore_ascii_case("DVD") => {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Some(value) if value.eq_ignore_ascii_case("USBStick") => false,
|
||||||
|
Some(value) => {
|
||||||
|
return Err(AppError::BadRequest(format!(
|
||||||
|
"Unsupported virtual media type: {value}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
None => image_name.to_ascii_lowercase().ends_with(".iso"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((cdrom, cdrom || req.write_protected.unwrap_or(true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_image(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
req: &InsertMediaRequest,
|
||||||
|
) -> Result<ImageInfo, AppError> {
|
||||||
|
if req.user_name.is_some() || req.password.is_some() {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Authenticated virtual media URIs are not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = state.config.get();
|
||||||
|
let manager = ImageManager::new(config.msd.images_dir());
|
||||||
|
if req.image.starts_with("http://") || req.image.starts_with("https://") {
|
||||||
|
if let Some(protocol) = req.transfer_protocol_type.as_deref() {
|
||||||
|
let expected = if req.image.starts_with("https://") {
|
||||||
|
"HTTPS"
|
||||||
|
} else {
|
||||||
|
"HTTP"
|
||||||
|
};
|
||||||
|
if !protocol.eq_ignore_ascii_case(expected) {
|
||||||
|
return Err(AppError::BadRequest(format!(
|
||||||
|
"TransferProtocolType must be {expected} for this Image URI"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return manager.download_from_url(&req.image, None, |_, _| {}).await;
|
||||||
|
}
|
||||||
|
if req.transfer_protocol_type.is_some() {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"TransferProtocolType is only valid for remote Image URIs".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_id = req
|
||||||
|
.image
|
||||||
|
.strip_prefix("/api/msd/images/")
|
||||||
|
.unwrap_or(&req.image)
|
||||||
|
.split(['?', '#'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
if image_id.is_empty() || image_id.contains('/') {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Image must be an HTTP(S) URI, image ID, or /api/msd/images/{id}".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
manager.get(image_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_error_response(error: AppError) -> Response {
|
||||||
|
let status = match &error {
|
||||||
|
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||||
|
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
AppError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
redfish_error(status, &error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redfish_error(status: StatusCode, message: &str) -> Response {
|
||||||
|
(status, Json(RedfishError::general_error(message))).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn request(image: &str) -> InsertMediaRequest {
|
||||||
|
InsertMediaRequest {
|
||||||
|
image: image.to_string(),
|
||||||
|
write_protected: None,
|
||||||
|
transfer_method: None,
|
||||||
|
transfer_protocol_type: None,
|
||||||
|
media_types: None,
|
||||||
|
inserted: None,
|
||||||
|
user_name: None,
|
||||||
|
password: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slot_ids_map_to_zero_based_luns() {
|
||||||
|
assert_eq!(parse_slot_id("1", 1), Some(0));
|
||||||
|
assert_eq!(parse_slot_id("8", 8), Some(7));
|
||||||
|
assert_eq!(parse_slot_id("0", 8), None);
|
||||||
|
assert_eq!(parse_slot_id("2", 1), None);
|
||||||
|
assert_eq!(parse_slot_id("invalid", 8), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mount_options_follow_media_type_and_redfish_write_protect_default() {
|
||||||
|
assert_eq!(
|
||||||
|
mount_options(&request("opaque-id"), "image.iso").unwrap(),
|
||||||
|
(true, true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mount_options(&request("opaque-id"), "image.img").unwrap(),
|
||||||
|
(false, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut writable = request("image.img");
|
||||||
|
writable.write_protected = Some(false);
|
||||||
|
writable.media_types = Some(vec!["USBStick".to_string()]);
|
||||||
|
assert_eq!(
|
||||||
|
mount_options(&writable, "image.img").unwrap(),
|
||||||
|
(false, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_transfer_and_non_inserted_media_are_rejected() {
|
||||||
|
let mut stream = request("image.iso");
|
||||||
|
stream.transfer_method = Some("Stream".to_string());
|
||||||
|
assert!(validate_insert_request(&stream).is_err());
|
||||||
|
|
||||||
|
let mut not_inserted = request("image.iso");
|
||||||
|
not_inserted.inserted = Some(false);
|
||||||
|
assert!(validate_insert_request(¬_inserted).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct ErrorResponse {
|
|||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
|
let status = status_code(&self);
|
||||||
let body = ErrorResponse {
|
let body = ErrorResponse {
|
||||||
success: false,
|
success: false,
|
||||||
message: self.to_string(),
|
message: self.to_string(),
|
||||||
@@ -25,7 +26,49 @@ impl IntoResponse for AppError {
|
|||||||
"Request failed"
|
"Request failed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Always return 200 OK - success/failure is indicated by the success field
|
(status, Json(body)).into_response()
|
||||||
(StatusCode::OK, Json(body)).into_response()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_code(error: &AppError) -> StatusCode {
|
||||||
|
match error {
|
||||||
|
AppError::AuthError(_) | AppError::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||||
|
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||||
|
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
AppError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_client_and_availability_errors_to_http_statuses() {
|
||||||
|
assert_eq!(
|
||||||
|
status_code(&AppError::BadRequest("invalid".to_string())),
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status_code(&AppError::AuthError("invalid".to_string())),
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status_code(&AppError::NotFound("missing".to_string())),
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
status_code(&AppError::ServiceUnavailable("offline".to_string())),
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_internal_errors_to_server_error() {
|
||||||
|
assert_eq!(
|
||||||
|
status_code(&AppError::Internal("failed".to_string())),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user