Skip to content

Commit

Permalink
refactor(web): bring back zFCP support (new HTTP / JSON API, queries,…
Browse files Browse the repository at this point in the history
… and TypeScript) (#1570)

Trello: https://trello.com/c/KMtaVltF

This PR could be seen as a follow up of https://trello.com/c/2LC8JEZR
but in this case bringing back the support for managing
zFCP devices on s390x architectures.

It includes:
- Extending the HTTP API to expose zFCP devices management and zFCP
related events.
  - Adapting the WEB UI for using Tanstack queries and the new HTTP API.
  - Adopting Typescript
  - Move to route pages.

---------

Co-authored-by: Knut Anderssen <[email protected]>
Co-authored-by: Imobach González Sosa <[email protected]>
Co-authored-by: David Díaz González <[email protected]>
Co-authored-by: Knut Alejandro Anderssen González <[email protected]>
  • Loading branch information
5 people authored Sep 18, 2024
1 parent eb86857 commit 0b66de2
Show file tree
Hide file tree
Showing 45 changed files with 2,436 additions and 1,293 deletions.
1 change: 1 addition & 0 deletions rust/agama-lib/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod store;

pub use client::{
iscsi::{ISCSIAuth, ISCSIClient, ISCSIInitiator, ISCSINode},
zfcp::ZFCPClient,
StorageClient,
};
pub use settings::StorageSettings;
Expand Down
1 change: 1 addition & 0 deletions rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use zbus::zvariant::{OwnedObjectPath, OwnedValue};
use zbus::Connection;
pub mod dasd;
pub mod iscsi;
pub mod zfcp;

type DBusObject = (
OwnedObjectPath,
Expand Down
190 changes: 190 additions & 0 deletions rust/agama-lib/src/storage/client/zfcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//! Implements a client to access Agama's D-Bus API related to zFCP management.

use std::collections::HashMap;

use futures_util::future::join_all;
use zbus::{fdo::ObjectManagerProxy, zvariant::OwnedObjectPath, Connection};

use crate::{
dbus::{extract_id_from_path, get_property},
error::ServiceError,
storage::{
model::zfcp::{ZFCPController, ZFCPDisk},
proxies::{ZFCPControllerProxy, ZFCPManagerProxy},
},
};

const ZFCP_CONTROLLER_PREFIX: &'static str = "/org/opensuse/Agama/Storage1/zfcp_controllers";

/// Client to connect to Agama's D-Bus API for zFCP management.
#[derive(Clone)]
pub struct ZFCPClient<'a> {
manager_proxy: ZFCPManagerProxy<'a>,
object_manager_proxy: ObjectManagerProxy<'a>,
connection: Connection,
}

impl<'a> ZFCPClient<'a> {
pub async fn new(connection: Connection) -> Result<Self, ServiceError> {
let manager_proxy = ZFCPManagerProxy::new(&connection).await?;
let object_manager_proxy = ObjectManagerProxy::builder(&connection)
.destination("org.opensuse.Agama.Storage1")?
.path("/org/opensuse/Agama/Storage1")?
.build()
.await?;
Ok(Self {
manager_proxy,
object_manager_proxy,
connection,
})
}

pub async fn supported(&self) -> Result<bool, ServiceError> {
let introspect = self.manager_proxy.introspect().await?;
// simply check if introspection contain given interface
Ok(introspect.contains("org.opensuse.Agama.Storage1.ZFCP.Manager"))
}

pub async fn is_lun_scan_allowed(&self) -> Result<bool, ServiceError> {
let allowed = self.manager_proxy.allow_lunscan().await?;
// simply check if introspection contain given interface
Ok(allowed)
}

pub async fn probe(&self) -> Result<(), ServiceError> {
Ok(self.manager_proxy.probe().await?)
}

pub async fn get_disks(&self) -> Result<Vec<(OwnedObjectPath, ZFCPDisk)>, ServiceError> {
let managed_objects = self.object_manager_proxy.get_managed_objects().await?;

let mut devices: Vec<(OwnedObjectPath, ZFCPDisk)> = vec![];
for (path, ifaces) in managed_objects {
if let Some(properties) = ifaces.get("org.opensuse.Agama.Storage1.ZFCP.Disk") {
match ZFCPDisk::try_from(properties) {
Ok(device) => {
devices.push((path, device));
}
Err(error) => {
log::warn!("Not a valid zFCP disk: {}", error);
}
}
}
}
Ok(devices)
}

pub async fn get_controllers(
&self,
) -> Result<Vec<(OwnedObjectPath, ZFCPController)>, ServiceError> {
let managed_objects = self.object_manager_proxy.get_managed_objects().await?;

let mut devices: Vec<(OwnedObjectPath, ZFCPController)> = vec![];
for (path, ifaces) in managed_objects {
if let Some(properties) = ifaces.get("org.opensuse.Agama.Storage1.ZFCP.Controller") {
let id = extract_id_from_path(&path)?.to_string();
devices.push((
path,
ZFCPController {
id: id.clone(),
channel: get_property(properties, "Channel")?,
lun_scan: get_property(properties, "LUNScan")?,
active: get_property(properties, "Active")?,
luns_map: self.get_luns_map(id.as_str()).await?,
},
))
}
}
Ok(devices)
}

async fn get_controller_proxy(
&self,
controller_id: &str,
) -> Result<ZFCPControllerProxy, ServiceError> {
let dbus = ZFCPControllerProxy::builder(&self.connection)
.path(ZFCP_CONTROLLER_PREFIX.to_string() + "/" + controller_id)?
.build()
.await?;
Ok(dbus)
}

pub async fn activate_controller(&self, controller_id: &str) -> Result<(), ServiceError> {
let controller = self.get_controller_proxy(controller_id).await?;
controller.activate().await?;
Ok(())
}

pub async fn get_wwpns(&self, controller_id: &str) -> Result<Vec<String>, ServiceError> {
let controller = self.get_controller_proxy(controller_id).await?;
let result = controller.get_wwpns().await?;
Ok(result)
}

pub async fn get_luns(
&self,
controller_id: &str,
wwpn: &str,
) -> Result<Vec<String>, ServiceError> {
let controller = self.get_controller_proxy(controller_id).await?;
let result = controller.get_luns(wwpn).await?;
Ok(result)
}

/// Obtains a LUNs map for the given controller
///
/// Given a controller id it returns a HashMap with each of its WWPNs as keys and the list of
/// LUNS corresponding to that specific WWPN as values.
///
/// Arguments:
///
/// `controller_id`: controller id
pub async fn get_luns_map(
&self,
controller_id: &str,
) -> Result<HashMap<String, Vec<String>>, ServiceError> {
let wwpns = self.get_wwpns(controller_id).await?;
let aresult = wwpns.into_iter().map(|wwpn| async move {
Ok((
wwpn.clone(),
self.get_luns(controller_id, wwpn.as_str()).await?,
))
});
let sresult = join_all(aresult).await;
sresult
.into_iter()
.collect::<Result<HashMap<String, Vec<String>>, _>>()
}

pub async fn activate_disk(
&self,
controller_id: &str,
wwpn: &str,
lun: &str,
) -> Result<(), ServiceError> {
let controller = self.get_controller_proxy(controller_id).await?;
let result = controller.activate_disk(wwpn, lun).await?;
if result == 0 {
Ok(())
} else {
let text = format!("Failed to activate disk. chzdev exit code {}", result);
Err(ServiceError::UnsuccessfulAction(text))
}
}

pub async fn deactivate_disk(
&self,
controller_id: &str,
wwpn: &str,
lun: &str,
) -> Result<(), ServiceError> {
let controller = self.get_controller_proxy(controller_id).await?;
let result = controller.deactivate_disk(wwpn, lun).await?;
if result == 0 {
Ok(())
} else {
let text = format!("Failed to deactivate disk. chzdev exit code {}", result);
Err(ServiceError::UnsuccessfulAction(text))
}
}
}
1 change: 1 addition & 0 deletions rust/agama-lib/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use zbus::zvariant::{OwnedValue, Value};
use crate::dbus::{get_optional_property, get_property};

pub mod dasd;
pub mod zfcp;

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DeviceSid(u32);
Expand Down
50 changes: 50 additions & 0 deletions rust/agama-lib/src/storage/model/zfcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Implements a data model for zFCP devices management.
use std::collections::HashMap;

use serde::Serialize;
use zbus::zvariant::OwnedValue;

use crate::{dbus::get_property, error::ServiceError};

/// Represents a zFCP disk (specific to s390x systems).
#[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ZFCPDisk {
/// Name of the zFCP device (e.g., /dev/sda)
pub name: String,
/// zFCP controller channel id (e.g., 0.0.fa00)
pub channel: String,
/// WWPN of the targer port (e.g., 0x500507630300c562)
pub wwpn: String,
/// LUN of the SCSI device (e.g. 0x4010403300000000)
pub lun: String,
}

impl TryFrom<&HashMap<String, OwnedValue>> for ZFCPDisk {
type Error = ServiceError;

fn try_from(value: &HashMap<String, OwnedValue>) -> Result<Self, Self::Error> {
Ok(ZFCPDisk {
name: get_property(value, "Name")?,
channel: get_property(value, "Channel")?,
wwpn: get_property(value, "WWPN")?,
lun: get_property(value, "LUN")?,
})
}
}

/// Represents a zFCP controller (specific to s390x systems).
#[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ZFCPController {
/// unique internal ID for given controller
pub id: String,
/// zFCP controller channel id (e.g., 0.0.fa00)
pub channel: String,
/// flag whenever channel is performing LUN auto scan
pub lun_scan: bool,
/// flag whenever channel is active
pub active: bool,
/// map of associated WWPNs and its LUNs
pub luns_map: HashMap<String, Vec<String>>,
}
73 changes: 73 additions & 0 deletions rust/agama-lib/src/storage/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,76 @@ trait DASDDevice {
#[dbus_proxy(property)]
fn type_(&self) -> zbus::Result<String>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.ZFCP.Manager",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1"
)]
trait ZFCPManager {
/// Probe method
fn probe(&self) -> zbus::Result<()>;

/// AllowLUNScan property
#[dbus_proxy(property, name = "AllowLUNScan")]
fn allow_lunscan(&self) -> zbus::Result<bool>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.ZFCP.Controller",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1"
)]
trait ZFCPController {
/// Activate method
fn activate(&self) -> zbus::Result<u32>;

/// ActivateDisk method
fn activate_disk(&self, wwpn: &str, lun: &str) -> zbus::Result<u32>;

/// DeactivateDisk method
fn deactivate_disk(&self, wwpn: &str, lun: &str) -> zbus::Result<u32>;

/// GetLUNs method
#[dbus_proxy(name = "GetLUNs")]
fn get_luns(&self, wwpn: &str) -> zbus::Result<Vec<String>>;

/// GetWWPNs method
#[dbus_proxy(name = "GetWWPNs")]
fn get_wwpns(&self) -> zbus::Result<Vec<String>>;

/// Active property
#[dbus_proxy(property)]
fn active(&self) -> zbus::Result<bool>;

/// Channel property
#[dbus_proxy(property)]
fn channel(&self) -> zbus::Result<String>;

/// LUNScan property
#[dbus_proxy(property, name = "LUNScan")]
fn lunscan(&self) -> zbus::Result<bool>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.ZFCP.Disk",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1"
)]
trait Disk {
/// Channel property
#[dbus_proxy(property)]
fn channel(&self) -> zbus::Result<String>;

/// LUN property
#[dbus_proxy(property, name = "LUN")]
fn lun(&self) -> zbus::Result<String>;

/// Name property
#[dbus_proxy(property)]
fn name(&self) -> zbus::Result<String>;

/// WWPN property
#[dbus_proxy(property, name = "WWPN")]
fn wwpn(&self) -> zbus::Result<String>;
}
6 changes: 6 additions & 0 deletions rust/agama-server/src/storage/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use tokio_stream::{Stream, StreamExt};
use zfcp::{zfcp_service, zfcp_stream};

pub mod dasd;
pub mod iscsi;
pub mod zfcp;

use crate::{
error::Error,
Expand All @@ -45,9 +47,11 @@ pub async fn storage_streams(dbus: zbus::Connection) -> Result<EventStreams, Err
)];
let mut iscsi = iscsi_stream(&dbus).await?;
let mut dasd = dasd_stream(&dbus).await?;
let mut zfcp = zfcp_stream(&dbus).await?;

result.append(&mut iscsi);
result.append(&mut dasd);
result.append(&mut zfcp);
Ok(result)
}

Expand Down Expand Up @@ -82,6 +86,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result<Router, ServiceEr
let issues_router = issues_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?;
let iscsi_router = iscsi_service(&dbus).await?;
let dasd_router = dasd_service(&dbus).await?;
let zfcp_router = zfcp_service(&dbus).await?;
let jobs_router = jobs_service(&dbus, DBUS_DESTINATION, DBUS_PATH).await?;

let client = StorageClient::new(dbus.clone()).await?;
Expand All @@ -106,6 +111,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result<Router, ServiceEr
.nest("/issues", issues_router)
.nest("/iscsi", iscsi_router)
.nest("/dasd", dasd_router)
.nest("/zfcp", zfcp_router)
.with_state(state);
Ok(router)
}
Expand Down
Loading

0 comments on commit 0b66de2

Please sign in to comment.