From 79b11a5346b88987097e9a764e6f58d9b3281dbb Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 19 Apr 2024 17:47:26 +0200 Subject: [PATCH 01/18] initial storage web service --- rust/agama-lib/src/storage/client.rs | 1 + rust/agama-server/src/lib.rs | 1 + rust/agama-server/src/storage.rs | 2 ++ rust/agama-server/src/storage/web.rs | 46 ++++++++++++++++++++++++++++ rust/agama-server/src/web.rs | 5 +++ 5 files changed, 55 insertions(+) create mode 100644 rust/agama-server/src/storage.rs create mode 100644 rust/agama-server/src/storage/web.rs diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 06455992b8..f7679aa2f1 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -17,6 +17,7 @@ pub struct StorageDevice { } /// D-Bus client for the storage service +#[derive(Clone)] pub struct StorageClient<'a> { pub connection: Connection, calculator_proxy: ProposalCalculatorProxy<'a>, diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 8c2602f7b9..46f0cfc909 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -5,6 +5,7 @@ pub mod manager; pub mod network; pub mod questions; pub mod software; +pub mod storage; pub mod users; pub mod web; pub use web::service; diff --git a/rust/agama-server/src/storage.rs b/rust/agama-server/src/storage.rs new file mode 100644 index 0000000000..f5d9826611 --- /dev/null +++ b/rust/agama-server/src/storage.rs @@ -0,0 +1,2 @@ +pub mod web; +pub use web::{storage_service, storage_streams}; \ No newline at end of file diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs new file mode 100644 index 0000000000..1e6024395b --- /dev/null +++ b/rust/agama-server/src/storage/web.rs @@ -0,0 +1,46 @@ +//! This module implements the web API for the storage service. +//! +//! The module offers two public functions: +//! +//! * `storage_service` which returns the Axum service. +//! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. + +use agama_lib::{error::ServiceError, storage::StorageClient}; +use axum::Router; + +use crate::{ + error::Error, + web::{ + common::{issues_router, progress_router, service_status_router, EventStreams}, + Event, + }, +}; + +pub async fn storage_streams(dbus: zbus::Connection) -> Result { + let result: EventStreams = vec![]; // TODO: + Ok(result) +} + +#[derive(Clone)] +struct StorageState<'a> { + client: StorageClient<'a>, +} + +/// Sets up and returns the axum service for the software module. +pub async fn storage_service(dbus: zbus::Connection) -> Result { + const DBUS_SERVICE: &str = "org.opensuse.Agama.Storage1"; + const DBUS_PATH: &str = "/org/opensuse/Agama/Storage1"; + + let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + let progress_router = progress_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + let issues_router = issues_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + + let client = StorageClient::new(dbus.clone()).await?; + let state = StorageState { client }; + let router = Router::new() + .merge(status_router) + .merge(progress_router) + .nest("/issues", issues_router) + .with_state(state); + Ok(router) +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index a7b52d247c..0be99711f5 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -10,6 +10,7 @@ use crate::{ manager::web::{manager_service, manager_stream}, network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, + storage::web::{storage_service, storage_streams}, software::web::{software_service, software_streams}, users::web::{users_service, users_streams}, web::common::{issues_stream, progress_stream, service_status_stream}, @@ -58,6 +59,7 @@ where .add_service("/l10n", l10n_service(dbus.clone(), events.clone()).await?) .add_service("/manager", manager_service(dbus.clone()).await?) .add_service("/software", software_service(dbus.clone()).await?) + .add_service("/storage", storage_service(dbus.clone()).await?) .add_service( "/network", network_service(dbus.clone(), network_adapter).await?, @@ -110,6 +112,9 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res for (id, user_stream) in users_streams(dbus.clone()).await? { stream.insert(id, user_stream); } + for (id, storage_stream) in storage_streams(dbus.clone()).await? { + stream.insert(id, storage_stream); + } for (id, software_stream) in software_streams(dbus.clone()).await? { stream.insert(id, software_stream); } From ed67ffae575bd2972ad756e1766eb5f2b624d3f6 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 19 Apr 2024 23:12:42 +0200 Subject: [PATCH 02/18] implement dirty devices bit in API --- rust/agama-lib/src/storage/client.rs | 4 ++++ rust/agama-server/src/storage/web.rs | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index f7679aa2f1..8aa60de391 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -41,6 +41,10 @@ impl<'a> StorageClient<'a> { Ok(ProposalProxy::new(&self.connection).await?) } + pub async fn devices_dirty_bit(&self) -> Result { + Ok(self.storage_proxy.deprecated_system().await?) + } + /// Returns the available devices /// /// These devices can be used for installing the system. diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 1e6024395b..624c3b6cc5 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -6,7 +6,7 @@ //! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. use agama_lib::{error::ServiceError, storage::StorageClient}; -use axum::Router; +use axum::{extract::State, routing::get, Json, Router}; use crate::{ error::Error, @@ -38,9 +38,14 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result, Error> { + Ok(Json(state.client.devices_dirty_bit().await?)) +} \ No newline at end of file From 251d0e4467edcc502c95e123da0254c1ee210f1d Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Apr 2024 14:25:55 +0200 Subject: [PATCH 03/18] add initial device interface --- rust/agama-lib/src/storage.rs | 3 +- rust/agama-lib/src/storage/device.rs | 80 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 rust/agama-lib/src/storage/device.rs diff --git a/rust/agama-lib/src/storage.rs b/rust/agama-lib/src/storage.rs index dfb9105357..abb02abf37 100644 --- a/rust/agama-lib/src/storage.rs +++ b/rust/agama-lib/src/storage.rs @@ -1,6 +1,7 @@ //! Implements support for handling the storage settings -mod client; +pub mod client; +mod device; mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/storage/device.rs b/rust/agama-lib/src/storage/device.rs new file mode 100644 index 0000000000..2772cef404 --- /dev/null +++ b/rust/agama-lib/src/storage/device.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +/// Information about system device created by composition to reflect different devices on system +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Device { + pub device_info: DeviceInfo, + pub block_device: Option, + pub component: Option, + pub drive: Option, + pub filesystem: Option, + pub lvm_lv: Option, + pub lvm_vg: Option, + pub md: Option, + pub multipath: Option, + pub partition: Option, + pub partition_table: Option, + pub raid: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct DeviceInfo { + pub sid: u32, + pub name: String, + pub description: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct BlockDevice { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Component { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Drive { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Filesystem { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LvmLv { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LvmVg { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MD { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Multipath { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Partition { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PartitionTable { + +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Raid { + +} From e37c4ca04dedd5eff22fe267295f126e5e53bf2e Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Apr 2024 15:34:25 +0200 Subject: [PATCH 04/18] update partly proxies --- rust/agama-lib/src/storage/client.rs | 46 +++++++++++- rust/agama-lib/src/storage/device.rs | 44 +++-------- rust/agama-lib/src/storage/proxies.rs | 101 +++++++++++++++++++++++++- rust/agama-server/src/storage.rs | 2 +- rust/agama-server/src/storage/web.rs | 2 +- rust/agama-server/src/web.rs | 2 +- 6 files changed, 155 insertions(+), 42 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 8aa60de391..37ca991c0a 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,11 +1,14 @@ //! Implements a client to access Agama's storage service. -use super::proxies::{BlockDeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; +use super::device::{Device, DeviceInfo}; +use super::proxies::{BlockProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; use crate::error::ServiceError; +use anyhow::Context; use futures_util::future::join_all; use serde::Serialize; use std::collections::HashMap; +use zbus::fdo::ObjectManagerProxy; use zbus::zvariant::OwnedObjectPath; use zbus::Connection; @@ -22,6 +25,7 @@ pub struct StorageClient<'a> { pub connection: Connection, calculator_proxy: ProposalCalculatorProxy<'a>, storage_proxy: Storage1Proxy<'a>, + object_manager_proxy: ObjectManagerProxy<'a>, } impl<'a> StorageClient<'a> { @@ -29,6 +33,11 @@ impl<'a> StorageClient<'a> { Ok(Self { calculator_proxy: ProposalCalculatorProxy::new(&connection).await?, storage_proxy: Storage1Proxy::new(&connection).await?, + object_manager_proxy: ObjectManagerProxy::builder(&connection) + .destination("org.opensuse.Agama.Storage1")? + .path("/org/opensuse/Agama/Storage1")? + .build() + .await?, connection, }) } @@ -65,7 +74,7 @@ impl<'a> StorageClient<'a> { &self, dbus_path: OwnedObjectPath, ) -> Result { - let proxy = BlockDeviceProxy::builder(&self.connection) + let proxy = BlockProxy::builder(&self.connection) .path(dbus_path)? .build() .await?; @@ -145,4 +154,37 @@ impl<'a> StorageClient<'a> { Ok(self.calculator_proxy.calculate(dbus_settings).await?) } + + pub async fn system_devices(&self) -> Result, ServiceError> { + let objects = self + .object_manager_proxy + .get_managed_objects() + .await + .context("Failed to get managed objects")?; + let result: Vec = objects + .into_iter() + // take ony system devices + .filter(|object| object.0.as_str().contains("Storage1/system")) + .map(|object| Device { + device_info: DeviceInfo { + sid: 0, + name: "TODO".to_string(), + description: "TODO".to_string(), + }, + component: None, + drive: None, + block_device: None, + filesystem: None, + lvm_lv: None, + lvm_vg: None, + md: None, + multipath: None, + partition: None, + partition_table: None, + raid: None, + }) + .collect(); + + Ok(result) + } } diff --git a/rust/agama-lib/src/storage/device.rs b/rust/agama-lib/src/storage/device.rs index 2772cef404..3110832df2 100644 --- a/rust/agama-lib/src/storage/device.rs +++ b/rust/agama-lib/src/storage/device.rs @@ -25,56 +25,34 @@ pub struct DeviceInfo { } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct BlockDevice { - -} +pub struct BlockDevice {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Component { - -} +pub struct Component {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Drive { - -} +pub struct Drive {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Filesystem { - -} +pub struct Filesystem {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct LvmLv { - -} +pub struct LvmLv {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct LvmVg { - -} +pub struct LvmVg {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct MD { - -} +pub struct MD {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Multipath { - -} +pub struct Multipath {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Partition { - -} +pub struct Partition {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct PartitionTable { - -} +pub struct PartitionTable {} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct Raid { - -} +pub struct Raid {} diff --git a/rust/agama-lib/src/storage/proxies.rs b/rust/agama-lib/src/storage/proxies.rs index 29f08c79cd..69018ec1f4 100644 --- a/rust/agama-lib/src/storage/proxies.rs +++ b/rust/agama-lib/src/storage/proxies.rs @@ -103,21 +103,29 @@ trait Proposal { #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Block", - default_service = "org.opensuse.Agama.Storage1" + assume_defaults = true )] -trait BlockDevice { +trait Block { /// Active property #[dbus_proxy(property)] fn active(&self) -> zbus::Result; - /// Name property + /// Encrypted property #[dbus_proxy(property)] - fn name(&self) -> zbus::Result; + fn encrypted(&self) -> zbus::Result; + + /// RecoverableSize property + #[dbus_proxy(property)] + fn recoverable_size(&self) -> zbus::Result; /// Size property #[dbus_proxy(property)] fn size(&self) -> zbus::Result; + /// Start property + #[dbus_proxy(property)] + fn start(&self) -> zbus::Result; + /// Systems property #[dbus_proxy(property)] fn systems(&self) -> zbus::Result>; @@ -130,3 +138,88 @@ trait BlockDevice { #[dbus_proxy(property)] fn udev_paths(&self) -> zbus::Result>; } + +use zbus::proxy; +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.Drive", + assume_defaults = true +)] +trait Drive { + /// Bus property + #[dbus_proxy(property)] + fn bus(&self) -> zbus::Result; + + /// BusId property + #[dbus_proxy(property)] + fn bus_id(&self) -> zbus::Result; + + /// Driver property + #[dbus_proxy(property)] + fn driver(&self) -> zbus::Result>; + + /// Info property + #[dbus_proxy(property)] + fn info(&self) -> zbus::Result>; + + /// Model property + #[dbus_proxy(property)] + fn model(&self) -> zbus::Result; + + /// Transport property + #[dbus_proxy(property)] + fn transport(&self) -> zbus::Result; + + /// Type property + #[dbus_proxy(property)] + fn type_(&self) -> zbus::Result; + + /// Vendor property + #[dbus_proxy(property)] + fn vendor(&self) -> zbus::Result; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.Multipath", + assume_defaults = true +)] +trait Multipath { + /// Wires property + #[dbus_proxy(property)] + fn wires(&self) -> zbus::Result>; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.PartitionTable", + assume_defaults = true +)] +trait PartitionTable { + /// Partitions property + #[dbus_proxy(property)] + fn partitions(&self) -> zbus::Result>; + + /// Type property + #[dbus_proxy(property)] + fn type_(&self) -> zbus::Result; + + /// UnusedSlots property + #[dbus_proxy(property)] + fn unused_slots(&self) -> zbus::Result>; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.Device", + assume_defaults = true +)] +trait Device { + /// Description property + #[dbus_proxy(property)] + fn description(&self) -> zbus::Result; + + /// Name property + #[dbus_proxy(property)] + fn name(&self) -> zbus::Result; + + /// SID property + #[dbus_proxy(property, name = "SID")] + fn sid(&self) -> zbus::Result; +} diff --git a/rust/agama-server/src/storage.rs b/rust/agama-server/src/storage.rs index f5d9826611..22dd60eeea 100644 --- a/rust/agama-server/src/storage.rs +++ b/rust/agama-server/src/storage.rs @@ -1,2 +1,2 @@ pub mod web; -pub use web::{storage_service, storage_streams}; \ No newline at end of file +pub use web::{storage_service, storage_streams}; diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 624c3b6cc5..d9c3cb94ae 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -48,4 +48,4 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result, Error> { Ok(Json(state.client.devices_dirty_bit().await?)) -} \ No newline at end of file +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 0be99711f5..9a9b3c39c4 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -10,8 +10,8 @@ use crate::{ manager::web::{manager_service, manager_stream}, network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, - storage::web::{storage_service, storage_streams}, software::web::{software_service, software_streams}, + storage::web::{storage_service, storage_streams}, users::web::{users_service, users_streams}, web::common::{issues_stream, progress_stream, service_status_stream}, }; From f4d20be4e94519132b1bc2417ce0e83dc0c53a72 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 24 Apr 2024 14:53:47 +0200 Subject: [PATCH 05/18] WIP for listing system devices --- rust/agama-lib/src/storage/client.rs | 25 +++++++++++++------------ rust/agama-lib/src/storage/proxies.rs | 16 ++++++++++------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 37ca991c0a..c7c5ed422c 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,7 +1,7 @@ //! Implements a client to access Agama's storage service. use super::device::{Device, DeviceInfo}; -use super::proxies::{BlockProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; +use super::proxies::{BlockProxy, DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; use crate::error::ServiceError; use anyhow::Context; @@ -74,17 +74,13 @@ impl<'a> StorageClient<'a> { &self, dbus_path: OwnedObjectPath, ) -> Result { - let proxy = BlockProxy::builder(&self.connection) + let proxy = DeviceProxy::builder(&self.connection) .path(dbus_path)? .build() .await?; - let name = proxy.name().await?; - // TODO: The description is not used yet. Decide what info to show, for example the device - // size, see https://crates.io/crates/size. - let description = name.clone(); - Ok(StorageDevice { name, description }) + Ok(StorageDevice { name: proxy.name().await?, description: proxy.description().await? }) } /// Returns the boot device proposal setting @@ -166,11 +162,7 @@ impl<'a> StorageClient<'a> { // take ony system devices .filter(|object| object.0.as_str().contains("Storage1/system")) .map(|object| Device { - device_info: DeviceInfo { - sid: 0, - name: "TODO".to_string(), - description: "TODO".to_string(), - }, + device_info: self.build_device_info(&object.0).await?, component: None, drive: None, block_device: None, @@ -187,4 +179,13 @@ impl<'a> StorageClient<'a> { Ok(result) } + + async fn build_device_info(&self, path: &OwnedObjectPath) -> Result { + let proxy = DeviceProxy::builder(&self.connection).path(path)?.build().await?; + DeviceInfo { + sid: proxy.sid().await?, + name: proxy.name().await?, + description: proxy.description().await?, + } + } } diff --git a/rust/agama-lib/src/storage/proxies.rs b/rust/agama-lib/src/storage/proxies.rs index 69018ec1f4..3bea233564 100644 --- a/rust/agama-lib/src/storage/proxies.rs +++ b/rust/agama-lib/src/storage/proxies.rs @@ -103,7 +103,8 @@ trait Proposal { #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Block", - assume_defaults = true + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" )] trait Block { /// Active property @@ -139,10 +140,10 @@ trait Block { fn udev_paths(&self) -> zbus::Result>; } -use zbus::proxy; #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Drive", - assume_defaults = true + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" )] trait Drive { /// Bus property @@ -180,7 +181,8 @@ trait Drive { #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Multipath", - assume_defaults = true + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" )] trait Multipath { /// Wires property @@ -190,7 +192,8 @@ trait Multipath { #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.PartitionTable", - assume_defaults = true + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" )] trait PartitionTable { /// Partitions property @@ -208,7 +211,8 @@ trait PartitionTable { #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Device", - assume_defaults = true + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" )] trait Device { /// Description property From 67a015ce02420d54396c61251c1572217b07218c Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 24 Apr 2024 15:48:36 +0200 Subject: [PATCH 06/18] use for loop instead of iterators --- rust/agama-lib/src/storage/client.rs | 72 ++++++++++++++++++---------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index c7c5ed422c..27dc3f25be 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,7 +1,9 @@ //! Implements a client to access Agama's storage service. use super::device::{Device, DeviceInfo}; -use super::proxies::{BlockProxy, DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; +use super::proxies::{ + BlockProxy, DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy, +}; use super::StorageSettings; use crate::error::ServiceError; use anyhow::Context; @@ -9,7 +11,8 @@ use futures_util::future::join_all; use serde::Serialize; use std::collections::HashMap; use zbus::fdo::ObjectManagerProxy; -use zbus::zvariant::OwnedObjectPath; +use zbus::names::OwnedInterfaceName; +use zbus::zvariant::{OwnedObjectPath, OwnedValue}; use zbus::Connection; /// Represents a storage device @@ -79,8 +82,10 @@ impl<'a> StorageClient<'a> { .build() .await?; - - Ok(StorageDevice { name: proxy.name().await?, description: proxy.description().await? }) + Ok(StorageDevice { + name: proxy.name().await?, + description: proxy.description().await?, + }) } /// Returns the boot device proposal setting @@ -151,41 +156,56 @@ impl<'a> StorageClient<'a> { Ok(self.calculator_proxy.calculate(dbus_settings).await?) } + async fn build_device( + &self, + object: &( + OwnedObjectPath, + HashMap>, + ), + ) -> Result { + Ok(Device { + device_info: self.build_device_info(&object.0).await?, + component: None, + drive: None, + block_device: None, + filesystem: None, + lvm_lv: None, + lvm_vg: None, + md: None, + multipath: None, + partition: None, + partition_table: None, + raid: None, + }) + } + pub async fn system_devices(&self) -> Result, ServiceError> { let objects = self .object_manager_proxy .get_managed_objects() .await .context("Failed to get managed objects")?; - let result: Vec = objects - .into_iter() - // take ony system devices - .filter(|object| object.0.as_str().contains("Storage1/system")) - .map(|object| Device { - device_info: self.build_device_info(&object.0).await?, - component: None, - drive: None, - block_device: None, - filesystem: None, - lvm_lv: None, - lvm_vg: None, - md: None, - multipath: None, - partition: None, - partition_table: None, - raid: None, - }) - .collect(); + let mut result = vec![]; + for object in objects { + if !object.0.as_str().contains("Storage1/system") { + continue; + } + + result.push(self.build_device(&object).await?) + } Ok(result) } async fn build_device_info(&self, path: &OwnedObjectPath) -> Result { - let proxy = DeviceProxy::builder(&self.connection).path(path)?.build().await?; - DeviceInfo { + let proxy = DeviceProxy::builder(&self.connection) + .path(path)? + .build() + .await?; + Ok(DeviceInfo { sid: proxy.sid().await?, name: proxy.name().await?, description: proxy.description().await?, - } + }) } } From fff5460ec21147f1b0b799aaa2b59af3a0742585 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 25 Apr 2024 13:19:04 +0200 Subject: [PATCH 07/18] use object manager approach for system devices --- rust/agama-lib/src/error.rs | 4 +- rust/agama-lib/src/storage.rs | 2 +- rust/agama-lib/src/storage/client.rs | 62 +++++++++++++++++++--------- rust/agama-lib/src/storage/device.rs | 13 +++++- rust/agama-server/src/storage/web.rs | 7 +++- 5 files changed, 65 insertions(+), 23 deletions(-) diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index b1df6b842e..775336256f 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -2,7 +2,7 @@ use curl; use serde_json; use std::io; use thiserror::Error; -use zbus; +use zbus::{self, zvariant}; #[derive(Error, Debug)] pub enum ServiceError { @@ -10,6 +10,8 @@ pub enum ServiceError { DBus(#[from] zbus::Error), #[error("Could not connect to Agama bus at '{0}': {1}")] DBusConnectionError(String, #[source] zbus::Error), + #[error("Unexpected type on DBus '{0}'")] + ZVariant(#[from] zvariant::Error), // it's fine to say only "Error" because the original // specific error will be printed too #[error("Error: {0}")] diff --git a/rust/agama-lib/src/storage.rs b/rust/agama-lib/src/storage.rs index abb02abf37..a6796ae7f5 100644 --- a/rust/agama-lib/src/storage.rs +++ b/rust/agama-lib/src/storage.rs @@ -1,7 +1,7 @@ //! Implements support for handling the storage settings pub mod client; -mod device; +pub mod device; mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 27dc3f25be..3054ead290 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,20 +1,38 @@ //! Implements a client to access Agama's storage service. -use super::device::{Device, DeviceInfo}; +use super::device::{BlockDevice, Device, DeviceInfo}; use super::proxies::{ - BlockProxy, DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy, + DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy, }; use super::StorageSettings; use crate::error::ServiceError; -use anyhow::Context; +use anyhow::{anyhow, Context}; use futures_util::future::join_all; use serde::Serialize; use std::collections::HashMap; use zbus::fdo::ObjectManagerProxy; -use zbus::names::OwnedInterfaceName; -use zbus::zvariant::{OwnedObjectPath, OwnedValue}; +use zbus::names::{InterfaceName, OwnedInterfaceName}; +use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; use zbus::Connection; +// TODO: move to better place +/// Helper to get property of given type from ManagedObjects map +fn get_property<'a, T>(properties: &'a HashMap, name: &str) -> Result +where +T: TryFrom>, +>>::Error: Into +{ + let value: Value = properties.get(name).context(format!("Failed to find property '{}'", name))?.into(); + match T::try_from(value) { + Ok(v) => Ok(v), + Err(e) => { + let verr : zbus::zvariant::Error = e.into(); + let serr : ServiceError = verr.into(); + Err(serr) + } + } +} + /// Represents a storage device #[derive(Serialize, Debug)] pub struct StorageDevice { @@ -160,11 +178,12 @@ impl<'a> StorageClient<'a> { &self, object: &( OwnedObjectPath, - HashMap>, - ), + HashMap> + ) ) -> Result { + let interfaces = &object.1; Ok(Device { - device_info: self.build_device_info(&object.0).await?, + device_info: self.build_device_info(interfaces).await?, component: None, drive: None, block_device: None, @@ -187,7 +206,8 @@ impl<'a> StorageClient<'a> { .context("Failed to get managed objects")?; let mut result = vec![]; for object in objects { - if !object.0.as_str().contains("Storage1/system") { + let path = &object.0; + if !path.as_str().contains("Storage1/system") { continue; } @@ -197,15 +217,19 @@ impl<'a> StorageClient<'a> { Ok(result) } - async fn build_device_info(&self, path: &OwnedObjectPath) -> Result { - let proxy = DeviceProxy::builder(&self.connection) - .path(path)? - .build() - .await?; - Ok(DeviceInfo { - sid: proxy.sid().await?, - name: proxy.name().await?, - description: proxy.description().await?, - }) + async fn build_device_info(&self, interfaces: &HashMap>) -> Result { + let interface : OwnedInterfaceName = InterfaceName::from_static_str_unchecked("org.opensuse.Agama.Storage1.Device").into(); + let properties = interfaces.get(&interface); + // All devices has to implement device info, so report error if it is not there + if let Some(properties) = properties { + Ok(DeviceInfo { + sid: get_property::(properties, "SID")?, + name: get_property::(properties, "Name")?, + description: get_property(properties, "Description")?, + }) + } else { + Err(ServiceError::Anyhow(anyhow!("Device does not implement device info"))) + } + } } diff --git a/rust/agama-lib/src/storage/device.rs b/rust/agama-lib/src/storage/device.rs index 3110832df2..5a27b7831f 100644 --- a/rust/agama-lib/src/storage/device.rs +++ b/rust/agama-lib/src/storage/device.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; /// Information about system device created by composition to reflect different devices on system #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct Device { pub device_info: DeviceInfo, pub block_device: Option, @@ -25,7 +26,17 @@ pub struct DeviceInfo { } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct BlockDevice {} +#[serde(rename_all = "camelCase")] +pub struct BlockDevice { + pub active: bool, + pub encrypted: bool, + pub recoverable_size: u64, + pub size: u64, + pub start: u64, + pub systems: Vec, + pub udev_ids: Vec, + pub udev_paths: Vec, +} #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct Component {} diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index d9c3cb94ae..921e9f5226 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -5,7 +5,7 @@ //! * `storage_service` which returns the Axum service. //! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. -use agama_lib::{error::ServiceError, storage::StorageClient}; +use agama_lib::{error::ServiceError, storage::StorageClient, storage::device::Device}; use axum::{extract::State, routing::get, Json, Router}; use crate::{ @@ -39,6 +39,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result Result>) -> Result, Error> { Ok(Json(state.client.devices_dirty_bit().await?)) } + +async fn system_devices(State(state): State>) -> Result>, Error> { + Ok(Json(state.client.system_devices().await?)) +} From bb46e3bbd7d14ea175a7c3d3f4a4585a872d5025 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 25 Apr 2024 13:55:05 +0200 Subject: [PATCH 08/18] move helper to better place --- rust/agama-lib/src/dbus.rs | 28 ++++++++++++++++++- rust/agama-lib/src/storage/client.rs | 40 +++++++++------------------- rust/agama-server/src/storage/web.rs | 2 +- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index 863b42cb65..5054aa2821 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -1,7 +1,33 @@ +use anyhow::Context; use std::collections::HashMap; -use zbus::zvariant; +use zbus::zvariant::{self, OwnedValue, Value}; + +use crate::error::ServiceError; /// Nested hash to send to D-Bus. pub type NestedHash<'a> = HashMap<&'a str, HashMap<&'a str, zvariant::Value<'a>>>; /// Nested hash as it comes from D-Bus. pub type OwnedNestedHash = HashMap>; + +/// Helper to get property of given type from ManagedObjects map +pub fn get_property<'a, T>( + properties: &'a HashMap, + name: &str, +) -> Result +where + T: TryFrom>, + >>::Error: Into, +{ + let value: Value = properties + .get(name) + .context(format!("Failed to find property '{}'", name))? + .into(); + match T::try_from(value) { + Ok(v) => Ok(v), + Err(e) => { + let verr: zbus::zvariant::Error = e.into(); + let serr: ServiceError = verr.into(); + Err(serr) + } + } +} diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 3054ead290..12805d0023 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,10 +1,9 @@ //! Implements a client to access Agama's storage service. use super::device::{BlockDevice, Device, DeviceInfo}; -use super::proxies::{ - DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy, -}; +use super::proxies::{DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; +use crate::dbus::get_property; use crate::error::ServiceError; use anyhow::{anyhow, Context}; use futures_util::future::join_all; @@ -15,24 +14,6 @@ use zbus::names::{InterfaceName, OwnedInterfaceName}; use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; use zbus::Connection; -// TODO: move to better place -/// Helper to get property of given type from ManagedObjects map -fn get_property<'a, T>(properties: &'a HashMap, name: &str) -> Result -where -T: TryFrom>, ->>::Error: Into -{ - let value: Value = properties.get(name).context(format!("Failed to find property '{}'", name))?.into(); - match T::try_from(value) { - Ok(v) => Ok(v), - Err(e) => { - let verr : zbus::zvariant::Error = e.into(); - let serr : ServiceError = verr.into(); - Err(serr) - } - } -} - /// Represents a storage device #[derive(Serialize, Debug)] pub struct StorageDevice { @@ -178,8 +159,8 @@ impl<'a> StorageClient<'a> { &self, object: &( OwnedObjectPath, - HashMap> - ) + HashMap>, + ), ) -> Result { let interfaces = &object.1; Ok(Device { @@ -217,8 +198,12 @@ impl<'a> StorageClient<'a> { Ok(result) } - async fn build_device_info(&self, interfaces: &HashMap>) -> Result { - let interface : OwnedInterfaceName = InterfaceName::from_static_str_unchecked("org.opensuse.Agama.Storage1.Device").into(); + async fn build_device_info( + &self, + interfaces: &HashMap>, + ) -> Result { + let interface: OwnedInterfaceName = + InterfaceName::from_static_str_unchecked("org.opensuse.Agama.Storage1.Device").into(); let properties = interfaces.get(&interface); // All devices has to implement device info, so report error if it is not there if let Some(properties) = properties { @@ -228,8 +213,9 @@ impl<'a> StorageClient<'a> { description: get_property(properties, "Description")?, }) } else { - Err(ServiceError::Anyhow(anyhow!("Device does not implement device info"))) + Err(ServiceError::Anyhow(anyhow!( + "Device does not implement device info" + ))) } - } } diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 921e9f5226..c5a8bc193d 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -5,7 +5,7 @@ //! * `storage_service` which returns the Axum service. //! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. -use agama_lib::{error::ServiceError, storage::StorageClient, storage::device::Device}; +use agama_lib::{error::ServiceError, storage::device::Device, storage::StorageClient}; use axum::{extract::State, routing::get, Json, Router}; use crate::{ From 2164ac638409a9278ec4fabc3da30c5c8e261748 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 25 Apr 2024 21:40:12 +0200 Subject: [PATCH 09/18] implement block device interface --- rust/agama-lib/src/storage/client.rs | 31 ++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 12805d0023..e4237aa0ef 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -11,7 +11,7 @@ use serde::Serialize; use std::collections::HashMap; use zbus::fdo::ObjectManagerProxy; use zbus::names::{InterfaceName, OwnedInterfaceName}; -use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; +use zbus::zvariant::{OwnedObjectPath, OwnedValue}; use zbus::Connection; /// Represents a storage device @@ -167,7 +167,7 @@ impl<'a> StorageClient<'a> { device_info: self.build_device_info(interfaces).await?, component: None, drive: None, - block_device: None, + block_device: self.build_block_device(interfaces).await?, filesystem: None, lvm_lv: None, lvm_vg: None, @@ -208,8 +208,8 @@ impl<'a> StorageClient<'a> { // All devices has to implement device info, so report error if it is not there if let Some(properties) = properties { Ok(DeviceInfo { - sid: get_property::(properties, "SID")?, - name: get_property::(properties, "Name")?, + sid: get_property(properties, "SID")?, + name: get_property(properties, "Name")?, description: get_property(properties, "Description")?, }) } else { @@ -218,4 +218,27 @@ impl<'a> StorageClient<'a> { ))) } } + + async fn build_block_device( + &self, + interfaces: &HashMap>, + ) -> Result, ServiceError> { + let interface: OwnedInterfaceName = + InterfaceName::from_static_str_unchecked("org.opensuse.Agama.Storage1.Block").into(); + let properties = interfaces.get(&interface); + if let Some(properties) = properties { + Ok(Some(BlockDevice { + active: get_property(properties, "Active")?, + encrypted: get_property(properties, "Encrypted")?, + recoverable_size: get_property(properties, "RecoverableSize")?, + size: get_property(properties, "Size")?, + start: get_property(properties, "Start")?, + systems: get_property(properties, "Systems")?, + udev_ids: get_property(properties, "UdevIds")?, + udev_paths: get_property(properties, "UdevPaths")?, + })) + } else { + Ok(None) + } + } } From 36fdb6d5507577c518c568cf37dee8196fde6522 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 25 Apr 2024 21:46:01 +0200 Subject: [PATCH 10/18] implement also staging devices route --- rust/agama-lib/src/storage/client.rs | 19 +++++++++++++++++++ rust/agama-server/src/storage/web.rs | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index e4237aa0ef..30a5551c1a 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -198,6 +198,25 @@ impl<'a> StorageClient<'a> { Ok(result) } + pub async fn staging_devices(&self) -> Result, ServiceError> { + let objects = self + .object_manager_proxy + .get_managed_objects() + .await + .context("Failed to get managed objects")?; + let mut result = vec![]; + for object in objects { + let path = &object.0; + if !path.as_str().contains("Storage1/staging") { + continue; + } + + result.push(self.build_device(&object).await?) + } + + Ok(result) + } + async fn build_device_info( &self, interfaces: &HashMap>, diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index c5a8bc193d..6dee3e4552 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -40,6 +40,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result>) -> Result>, Error> { Ok(Json(state.client.system_devices().await?)) } + +async fn staging_devices(State(state): State>) -> Result>, Error> { + Ok(Json(state.client.staging_devices().await?)) +} \ No newline at end of file From ba3d250dcf8a70243fa2b022bb56ee8229752744 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 25 Apr 2024 22:42:00 +0200 Subject: [PATCH 11/18] implement actions --- rust/agama-lib/src/storage/client.rs | 29 ++++++++++++++++++++++++++++ rust/agama-server/src/storage/web.rs | 7 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 30a5551c1a..178345ecbf 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -21,6 +21,16 @@ pub struct StorageDevice { description: String, } +/// Represent single change action done to storage +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Action { + device: String, + text: String, + subvol: bool, + delete: bool, +} + /// D-Bus client for the storage service #[derive(Clone)] pub struct StorageClient<'a> { @@ -28,6 +38,7 @@ pub struct StorageClient<'a> { calculator_proxy: ProposalCalculatorProxy<'a>, storage_proxy: Storage1Proxy<'a>, object_manager_proxy: ObjectManagerProxy<'a>, + proposal_proxy: ProposalProxy<'a>, } impl<'a> StorageClient<'a> { @@ -40,6 +51,7 @@ impl<'a> StorageClient<'a> { .path("/org/opensuse/Agama/Storage1")? .build() .await?, + proposal_proxy: ProposalProxy::new(&connection).await?, connection, }) } @@ -56,6 +68,23 @@ impl<'a> StorageClient<'a> { Ok(self.storage_proxy.deprecated_system().await?) } + pub async fn actions(&self) -> Result, ServiceError> { + let actions = self.proposal_proxy.actions().await?; + let mut result: Vec = Vec::with_capacity(actions.len()); + + for i in actions { + let action = Action { + device: get_property(&i, "Device")?, + text: get_property(&i, "Text")?, + subvol: get_property(&i, "Subvol")?, + delete: get_property(&i, "Delete")?, + }; + result.push(action); + } + + Ok(result) + } + /// Returns the available devices /// /// These devices can be used for installing the system. diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 6dee3e4552..42df61c2e8 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -5,7 +5,7 @@ //! * `storage_service` which returns the Axum service. //! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. -use agama_lib::{error::ServiceError, storage::device::Device, storage::StorageClient}; +use agama_lib::{error::ServiceError, storage::{client::Action, device::Device, StorageClient}}; use axum::{extract::State, routing::get, Json, Router}; use crate::{ @@ -41,6 +41,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result>) -> Result>, Error> { Ok(Json(state.client.staging_devices().await?)) +} + +async fn actions(State(state): State>) -> Result>, Error> { + Ok(Json(state.client.actions().await?)) } \ No newline at end of file From 36ac90db7adf243c6f6593be7a7be51482ae0c4c Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 26 Apr 2024 17:42:52 +0200 Subject: [PATCH 12/18] WIP for volumes --- rust/agama-lib/src/dbus.rs | 25 ++++++++++- rust/agama-lib/src/storage/client.rs | 62 +++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index 5054aa2821..d05d0f74e3 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -9,7 +9,7 @@ pub type NestedHash<'a> = HashMap<&'a str, HashMap<&'a str, zvariant::Value<'a>> /// Nested hash as it comes from D-Bus. pub type OwnedNestedHash = HashMap>; -/// Helper to get property of given type from ManagedObjects map +/// Helper to get property of given type from ManagedObjects map or any generic dbus Hash with variant as value pub fn get_property<'a, T>( properties: &'a HashMap, name: &str, @@ -31,3 +31,26 @@ where } } } + +pub fn get_optional_property<'a, U, T>( + properties: &'a HashMap, + name: &str, +) -> Result +where + T: TryFrom>, + U: Option, + >>::Error: Into, +{ + let value: Value = properties + .get(name) + .context(format!("Failed to find property '{}'", name))? + .into(); + match T::try_from(value) { + Ok(v) => Ok(v), + Err(e) => { + let verr: zbus::zvariant::Error = e.into(); + let serr: ServiceError = verr.into(); + Err(serr) + } + } +} \ No newline at end of file diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 178345ecbf..f846fc1ae5 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -7,7 +7,7 @@ use crate::dbus::get_property; use crate::error::ServiceError; use anyhow::{anyhow, Context}; use futures_util::future::join_all; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use zbus::fdo::ObjectManagerProxy; use zbus::names::{InterfaceName, OwnedInterfaceName}; @@ -31,6 +31,47 @@ pub struct Action { delete: bool, } +/// Represents value for target key of Volume +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum VolumeTarget { + Default, + NewPartition, + NewVg, + Device, + Filesystem, +} + + +/// Represents volume outline aka requirements for volume +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "PascalCase")] +pub struct VolumeOutline { + required: bool, + fs_types: Vec, + support_auto_size: bool, + snapshots_configurable: bool, + snaphosts_affect_sizes: bool, + size_relevant_volumes: Vec, +} + +/// Represents single volume +// TODO: Do we really want to expose PascalCase from dbus or use more consistent snakeCase? +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "PascalCase")] +pub struct Volume { + mount_path: String, + mount_options: Vec, + target: VolumeTarget, + target_device: Option, + min_size: u64, + max_size: Option, + auto_size: bool, + snapshots: Option, + transactional: Option, + outline: Option, +} + /// D-Bus client for the storage service #[derive(Clone)] pub struct StorageClient<'a> { @@ -100,6 +141,25 @@ impl<'a> StorageClient<'a> { join_all(devices).await.into_iter().collect() } + pub async fn volume_for(&self, mount_path: &str) -> Result { + let volume_hash = self.calculator_proxy + .default_volume(mount_path).await?; + let volume = Volume { + mount_path: get_property(&volume_hash, "MountPath")?, + mount_options: get_property(&volume_hash, "MountOptions")?, + target: get_property(&volume_hash, "Target")?, + target_device: get_property(&volume_hash, "TargetDevice")?, + min_size: get_property(&volume_hash, "MinSize")?, + max_size: get_property(&volume_hash, "MaxSize")?, + auto_size: get_property(&volume_hash, "AutoSize")?, + snapshots: get_property(&volume_hash, "Snapshots")?, + transactional: get_property(&volume_hash, "Transactional")?, + outline: get_property(&volume_hash, "Outline"), + }; + + Ok(volume) + } + /// Returns the storage device for the given D-Bus path async fn storage_device( &self, From ca23a36af46e048fcfc625f2eaca5a712b8bd393 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 26 Apr 2024 17:54:16 +0200 Subject: [PATCH 13/18] add get_optional_property --- rust/agama-lib/src/dbus.rs | 26 +++++++++++++------------- rust/agama-lib/src/storage/client.rs | 12 ++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index d05d0f74e3..954e70bccc 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -32,25 +32,25 @@ where } } -pub fn get_optional_property<'a, U, T>( +pub fn get_optional_property<'a, T>( properties: &'a HashMap, name: &str, -) -> Result +) -> Result, ServiceError> where T: TryFrom>, - U: Option, >>::Error: Into, { - let value: Value = properties - .get(name) - .context(format!("Failed to find property '{}'", name))? - .into(); - match T::try_from(value) { - Ok(v) => Ok(v), - Err(e) => { - let verr: zbus::zvariant::Error = e.into(); - let serr: ServiceError = verr.into(); - Err(serr) + if let Some(value) = properties.get(name) { + let value : Value = value.into(); + match T::try_from(value) { + Ok(v) => Ok(Some(v)), + Err(e) => { + let verr: zbus::zvariant::Error = e.into(); + let serr: ServiceError = verr.into(); + Err(serr) + } } + } else { + Ok(None) } } \ No newline at end of file diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index f846fc1ae5..9b92a256fd 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -3,7 +3,7 @@ use super::device::{BlockDevice, Device, DeviceInfo}; use super::proxies::{DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; -use crate::dbus::get_property; +use crate::dbus::{get_optional_property, get_property}; use crate::error::ServiceError; use anyhow::{anyhow, Context}; use futures_util::future::join_all; @@ -148,13 +148,13 @@ impl<'a> StorageClient<'a> { mount_path: get_property(&volume_hash, "MountPath")?, mount_options: get_property(&volume_hash, "MountOptions")?, target: get_property(&volume_hash, "Target")?, - target_device: get_property(&volume_hash, "TargetDevice")?, + target_device: get_optional_property(&volume_hash, "TargetDevice")?, min_size: get_property(&volume_hash, "MinSize")?, - max_size: get_property(&volume_hash, "MaxSize")?, + max_size: get_optional_property(&volume_hash, "MaxSize")?, auto_size: get_property(&volume_hash, "AutoSize")?, - snapshots: get_property(&volume_hash, "Snapshots")?, - transactional: get_property(&volume_hash, "Transactional")?, - outline: get_property(&volume_hash, "Outline"), + snapshots: get_optional_property(&volume_hash, "Snapshots")?, + transactional: get_optional_property(&volume_hash, "Transactional")?, + outline: get_property(&volume_hash, "Outline")?, }; Ok(volume) From b4d9ae5b7554732730d546a65b41e40eeb19c711 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 26 Apr 2024 18:04:56 +0200 Subject: [PATCH 14/18] apply suggestion --- rust/agama-lib/src/dbus.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index 954e70bccc..db418319cc 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -22,14 +22,7 @@ where .get(name) .context(format!("Failed to find property '{}'", name))? .into(); - match T::try_from(value) { - Ok(v) => Ok(v), - Err(e) => { - let verr: zbus::zvariant::Error = e.into(); - let serr: ServiceError = verr.into(); - Err(serr) - } - } + T::try_from(value).map_err(|e| e.into().into()) } pub fn get_optional_property<'a, T>( @@ -42,14 +35,9 @@ where { if let Some(value) = properties.get(name) { let value : Value = value.into(); - match T::try_from(value) { - Ok(v) => Ok(Some(v)), - Err(e) => { - let verr: zbus::zvariant::Error = e.into(); - let serr: ServiceError = verr.into(); - Err(serr) - } - } + T::try_from(value) + .map(|v| Some(v)) + .map_err(|e| e.into().into() ) } else { Ok(None) } From 5b23d77a93a918c057f45d7874676e77ba68b186 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 26 Apr 2024 22:32:14 +0200 Subject: [PATCH 15/18] implement volume for --- rust/agama-lib/src/dbus.rs | 20 +++++++------ rust/agama-lib/src/storage/client.rs | 45 ++++++++++++++++++++++++---- rust/agama-server/src/storage/web.rs | 35 +++++++++++++++++++--- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index db418319cc..faadc06f1d 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -13,32 +13,34 @@ pub type OwnedNestedHash = HashMap pub fn get_property<'a, T>( properties: &'a HashMap, name: &str, -) -> Result +) -> Result where T: TryFrom>, >>::Error: Into, { let value: Value = properties .get(name) - .context(format!("Failed to find property '{}'", name))? + .ok_or(zbus::zvariant::Error::Message(format!( + "Failed to find property '{}'", + name + )))? .into(); - T::try_from(value).map_err(|e| e.into().into()) + + T::try_from(value).map_err(|e| e.into()) } pub fn get_optional_property<'a, T>( properties: &'a HashMap, name: &str, -) -> Result, ServiceError> +) -> Result, zbus::zvariant::Error> where T: TryFrom>, >>::Error: Into, { if let Some(value) = properties.get(name) { - let value : Value = value.into(); - T::try_from(value) - .map(|v| Some(v)) - .map_err(|e| e.into().into() ) + let value: Value = value.into(); + T::try_from(value).map(|v| Some(v)).map_err(|e| e.into()) } else { Ok(None) } -} \ No newline at end of file +} diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 9b92a256fd..4804890bfd 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -42,10 +42,27 @@ pub enum VolumeTarget { Filesystem, } +impl TryFrom> for VolumeTarget { + type Error = zbus::zvariant::Error; + + fn try_from(value: zbus::zvariant::Value) -> Result { + let svalue: String = value.try_into()?; + match svalue.as_str() { + "default" => Ok(VolumeTarget::Default), + "new_partition" => Ok(VolumeTarget::NewPartition), + "new_vg" => Ok(VolumeTarget::NewVg), + "device" => Ok(VolumeTarget::Device), + "filesystem" => Ok(VolumeTarget::Filesystem), + _ => Err(zbus::zvariant::Error::Message( + format!("Wrong value for Target: {}", svalue).to_string(), + )), + } + } +} /// Represents volume outline aka requirements for volume #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "PascalCase")] +#[serde(rename_all = "camelCase")] pub struct VolumeOutline { required: bool, fs_types: Vec, @@ -55,10 +72,27 @@ pub struct VolumeOutline { size_relevant_volumes: Vec, } +impl TryFrom> for VolumeOutline { + type Error = zbus::zvariant::Error; + + fn try_from(value: zbus::zvariant::Value) -> Result { + let mvalue: HashMap = value.try_into()?; + let res = VolumeOutline { + required: get_property(&mvalue, "Required")?, + fs_types: get_property(&mvalue, "FsTypes")?, + support_auto_size: get_property(&mvalue, "SupportAutoSize")?, + snapshots_configurable: get_property(&mvalue, "SnapshotsConfigurable")?, + snaphosts_affect_sizes: get_property(&mvalue, "SnapshotsAffectSizes")?, + size_relevant_volumes: get_property(&mvalue, "SizeRelevantVolumes")?, + }; + + Ok(res) + } +} + /// Represents single volume -// TODO: Do we really want to expose PascalCase from dbus or use more consistent snakeCase? #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "PascalCase")] +#[serde(rename_all = "camelCase")] pub struct Volume { mount_path: String, mount_options: Vec, @@ -142,8 +176,7 @@ impl<'a> StorageClient<'a> { } pub async fn volume_for(&self, mount_path: &str) -> Result { - let volume_hash = self.calculator_proxy - .default_volume(mount_path).await?; + let volume_hash = self.calculator_proxy.default_volume(mount_path).await?; let volume = Volume { mount_path: get_property(&volume_hash, "MountPath")?, mount_options: get_property(&volume_hash, "MountOptions")?, @@ -154,7 +187,7 @@ impl<'a> StorageClient<'a> { auto_size: get_property(&volume_hash, "AutoSize")?, snapshots: get_optional_property(&volume_hash, "Snapshots")?, transactional: get_optional_property(&volume_hash, "Transactional")?, - outline: get_property(&volume_hash, "Outline")?, + outline: get_optional_property(&volume_hash, "Outline")?, }; Ok(volume) diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 42df61c2e8..15c14e3487 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -5,8 +5,22 @@ //! * `storage_service` which returns the Axum service. //! * `storage_stream` which offers an stream that emits the storage events coming from D-Bus. -use agama_lib::{error::ServiceError, storage::{client::Action, device::Device, StorageClient}}; -use axum::{extract::State, routing::get, Json, Router}; +use std::collections::HashMap; + +use agama_lib::{ + error::ServiceError, + storage::{ + client::{Action, Volume}, + device::Device, + StorageClient, + }, +}; +use anyhow::anyhow; +use axum::{ + extract::{Query, State}, + routing::get, + Json, Router, +}; use crate::{ error::Error, @@ -41,6 +55,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result>) -> Result>, Error> { +async fn staging_devices( + State(state): State>, +) -> Result>, Error> { Ok(Json(state.client.staging_devices().await?)) } async fn actions(State(state): State>) -> Result>, Error> { Ok(Json(state.client.actions().await?)) -} \ No newline at end of file +} + +async fn volume_for( + State(state): State>, + Query(params): Query>, +) -> Result, Error> { + let mount_path = params + .get("mount_path") + .ok_or(anyhow!("Missing mount_path parameter"))?; + Ok(Json(state.client.volume_for(mount_path).await?)) +} From 785dae7c97ffd0420cc7957057ef944c046765b7 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 29 Apr 2024 12:36:22 +0200 Subject: [PATCH 16/18] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- rust/agama-lib/src/dbus.rs | 2 +- rust/agama-lib/src/error.rs | 2 +- rust/agama-lib/src/storage/client.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index faadc06f1d..b814f614b6 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -9,7 +9,7 @@ pub type NestedHash<'a> = HashMap<&'a str, HashMap<&'a str, zvariant::Value<'a>> /// Nested hash as it comes from D-Bus. pub type OwnedNestedHash = HashMap>; -/// Helper to get property of given type from ManagedObjects map or any generic dbus Hash with variant as value +/// Helper to get property of given type from ManagedObjects map or any generic D-Bus Hash with variant as value pub fn get_property<'a, T>( properties: &'a HashMap, name: &str, diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index 775336256f..5ef274afe3 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -10,7 +10,7 @@ pub enum ServiceError { DBus(#[from] zbus::Error), #[error("Could not connect to Agama bus at '{0}': {1}")] DBusConnectionError(String, #[source] zbus::Error), - #[error("Unexpected type on DBus '{0}'")] + #[error("Unexpected type on D-Bus '{0}'")] ZVariant(#[from] zvariant::Error), // it's fine to say only "Error" because the original // specific error will be printed too diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 4804890bfd..3c49b2866c 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -21,7 +21,7 @@ pub struct StorageDevice { description: String, } -/// Represent single change action done to storage +/// Represents a single change action done to storage #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Action { @@ -90,7 +90,7 @@ impl TryFrom> for VolumeOutline { } } -/// Represents single volume +/// Represents a single volume #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Volume { From fedf5e919ef1be114b7d1fdec10f6aa989803669 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 29 Apr 2024 13:45:32 +0200 Subject: [PATCH 17/18] fixes from review --- rust/agama-lib/src/dbus.rs | 2 ++ rust/agama-lib/src/error.rs | 2 ++ rust/agama-lib/src/storage/client.rs | 27 ++++++++++++--------------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index b814f614b6..50383f7b6b 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -29,6 +29,8 @@ where T::try_from(value).map_err(|e| e.into()) } +/// It is similar helper like get_property with difference that name does not need to be in HashMap. +/// In such case `None``is returned, so type has to be enclosed in `Option`.` pub fn get_optional_property<'a, T>( properties: &'a HashMap, name: &str, diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index 5ef274afe3..30f61af834 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -10,6 +10,8 @@ pub enum ServiceError { DBus(#[from] zbus::Error), #[error("Could not connect to Agama bus at '{0}': {1}")] DBusConnectionError(String, #[source] zbus::Error), + #[error("D-Bus protocol error: {0}")] + DBusProtocol(#[from] zbus::fdo::Error), #[error("Unexpected type on D-Bus '{0}'")] ZVariant(#[from] zvariant::Error), // it's fine to say only "Error" because the original diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 3c49b2866c..3828c02720 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -32,6 +32,7 @@ pub struct Action { } /// Represents value for target key of Volume +/// It is snake cased when serializing to be compatible with yast2-storage-ng. #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub enum VolumeTarget { @@ -286,7 +287,7 @@ impl<'a> StorageClient<'a> { ) -> Result { let interfaces = &object.1; Ok(Device { - device_info: self.build_device_info(interfaces).await?, + device_info: self.build_device_info(object).await?, component: None, drive: None, block_device: self.build_block_device(interfaces).await?, @@ -302,11 +303,7 @@ impl<'a> StorageClient<'a> { } pub async fn system_devices(&self) -> Result, ServiceError> { - let objects = self - .object_manager_proxy - .get_managed_objects() - .await - .context("Failed to get managed objects")?; + let objects = self.object_manager_proxy.get_managed_objects().await?; let mut result = vec![]; for object in objects { let path = &object.0; @@ -321,11 +318,7 @@ impl<'a> StorageClient<'a> { } pub async fn staging_devices(&self) -> Result, ServiceError> { - let objects = self - .object_manager_proxy - .get_managed_objects() - .await - .context("Failed to get managed objects")?; + let objects = self.object_manager_proxy.get_managed_objects().await?; let mut result = vec![]; for object in objects { let path = &object.0; @@ -341,8 +334,12 @@ impl<'a> StorageClient<'a> { async fn build_device_info( &self, - interfaces: &HashMap>, + object: &( + OwnedObjectPath, + HashMap>, + ), ) -> Result { + let interfaces = &object.1; let interface: OwnedInterfaceName = InterfaceName::from_static_str_unchecked("org.opensuse.Agama.Storage1.Device").into(); let properties = interfaces.get(&interface); @@ -354,9 +351,9 @@ impl<'a> StorageClient<'a> { description: get_property(properties, "Description")?, }) } else { - Err(ServiceError::Anyhow(anyhow!( - "Device does not implement device info" - ))) + let message = + format!("storage device {} is missing Device interface", object.0).to_string(); + Err(zbus::zvariant::Error::Message(message).into()) } } From a598fb5989f9040e0ba0e9f3a4e154a743641a10 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 30 Apr 2024 11:17:31 +0200 Subject: [PATCH 18/18] Update rust/agama-lib/src/dbus.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- rust/agama-lib/src/dbus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index 50383f7b6b..02ca6f302c 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -30,7 +30,7 @@ where } /// It is similar helper like get_property with difference that name does not need to be in HashMap. -/// In such case `None``is returned, so type has to be enclosed in `Option`.` +/// In such case `None` is returned, so type has to be enclosed in `Option`. pub fn get_optional_property<'a, T>( properties: &'a HashMap, name: &str,