Skip to content

Commit

Permalink
Storage adapt (#1169)
Browse files Browse the repository at this point in the history
It is the first step in having HTTP based API for storage. As it is
really big API we decided to split it into series of patches for easier
review.
  • Loading branch information
jreidinger committed Apr 30, 2024
2 parents 141a3dd + a598fb5 commit 259b625
Show file tree
Hide file tree
Showing 10 changed files with 569 additions and 17 deletions.
43 changes: 42 additions & 1 deletion rust/agama-lib/src/dbus.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
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<String, HashMap<String, zvariant::OwnedValue>>;

/// 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<String, OwnedValue>,
name: &str,
) -> Result<T, zbus::zvariant::Error>
where
T: TryFrom<Value<'a>>,
<T as TryFrom<Value<'a>>>::Error: Into<zbus::zvariant::Error>,
{
let value: Value = properties
.get(name)
.ok_or(zbus::zvariant::Error::Message(format!(
"Failed to find property '{}'",
name
)))?
.into();

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<String, OwnedValue>,
name: &str,
) -> Result<Option<T>, zbus::zvariant::Error>
where
T: TryFrom<Value<'a>>,
<T as TryFrom<Value<'a>>>::Error: Into<zbus::zvariant::Error>,
{
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())
} else {
Ok(None)
}
}
6 changes: 5 additions & 1 deletion rust/agama-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ use curl;
use serde_json;
use std::io;
use thiserror::Error;
use zbus;
use zbus::{self, zvariant};

#[derive(Error, Debug)]
pub enum ServiceError {
#[error("D-Bus service error: {0}")]
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
// specific error will be printed too
#[error("Error: {0}")]
Expand Down
3 changes: 2 additions & 1 deletion rust/agama-lib/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Implements support for handling the storage settings

mod client;
pub mod client;
pub mod device;
mod proxies;
mod settings;
mod store;
Expand Down
259 changes: 249 additions & 10 deletions rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
//! Implements a client to access Agama's storage service.

use super::proxies::{BlockDeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy};
use super::device::{BlockDevice, Device, DeviceInfo};
use super::proxies::{DeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy};
use super::StorageSettings;
use crate::dbus::{get_optional_property, 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::zvariant::OwnedObjectPath;
use zbus::fdo::ObjectManagerProxy;
use zbus::names::{InterfaceName, OwnedInterfaceName};
use zbus::zvariant::{OwnedObjectPath, OwnedValue};
use zbus::Connection;

/// Represents a storage device
Expand All @@ -16,18 +21,113 @@ pub struct StorageDevice {
description: String,
}

/// Represents a 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,
}

/// 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 {
Default,
NewPartition,
NewVg,
Device,
Filesystem,
}

impl TryFrom<zbus::zvariant::Value<'_>> for VolumeTarget {
type Error = zbus::zvariant::Error;

fn try_from(value: zbus::zvariant::Value) -> Result<Self, zbus::zvariant::Error> {
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 = "camelCase")]
pub struct VolumeOutline {
required: bool,
fs_types: Vec<String>,
support_auto_size: bool,
snapshots_configurable: bool,
snaphosts_affect_sizes: bool,
size_relevant_volumes: Vec<String>,
}

impl TryFrom<zbus::zvariant::Value<'_>> for VolumeOutline {
type Error = zbus::zvariant::Error;

fn try_from(value: zbus::zvariant::Value) -> Result<Self, zbus::zvariant::Error> {
let mvalue: HashMap<String, OwnedValue> = 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 a single volume
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Volume {
mount_path: String,
mount_options: Vec<String>,
target: VolumeTarget,
target_device: Option<String>,
min_size: u64,
max_size: Option<u64>,
auto_size: bool,
snapshots: Option<bool>,
transactional: Option<bool>,
outline: Option<VolumeOutline>,
}

/// D-Bus client for the storage service
#[derive(Clone)]
pub struct StorageClient<'a> {
pub connection: Connection,
calculator_proxy: ProposalCalculatorProxy<'a>,
storage_proxy: Storage1Proxy<'a>,
object_manager_proxy: ObjectManagerProxy<'a>,
proposal_proxy: ProposalProxy<'a>,
}

impl<'a> StorageClient<'a> {
pub async fn new(connection: Connection) -> Result<StorageClient<'a>, ServiceError> {
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?,
proposal_proxy: ProposalProxy::new(&connection).await?,
connection,
})
}
Expand All @@ -40,6 +140,27 @@ impl<'a> StorageClient<'a> {
Ok(ProposalProxy::new(&self.connection).await?)
}

pub async fn devices_dirty_bit(&self) -> Result<bool, ServiceError> {
Ok(self.storage_proxy.deprecated_system().await?)
}

pub async fn actions(&self) -> Result<Vec<Action>, ServiceError> {
let actions = self.proposal_proxy.actions().await?;
let mut result: Vec<Action> = 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.
Expand All @@ -55,22 +176,38 @@ impl<'a> StorageClient<'a> {
join_all(devices).await.into_iter().collect()
}

pub async fn volume_for(&self, mount_path: &str) -> Result<Volume, ServiceError> {
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_optional_property(&volume_hash, "TargetDevice")?,
min_size: get_property(&volume_hash, "MinSize")?,
max_size: get_optional_property(&volume_hash, "MaxSize")?,
auto_size: get_property(&volume_hash, "AutoSize")?,
snapshots: get_optional_property(&volume_hash, "Snapshots")?,
transactional: get_optional_property(&volume_hash, "Transactional")?,
outline: get_optional_property(&volume_hash, "Outline")?,
};

Ok(volume)
}

/// Returns the storage device for the given D-Bus path
async fn storage_device(
&self,
dbus_path: OwnedObjectPath,
) -> Result<StorageDevice, ServiceError> {
let proxy = BlockDeviceProxy::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
Expand Down Expand Up @@ -140,4 +277,106 @@ impl<'a> StorageClient<'a> {

Ok(self.calculator_proxy.calculate(dbus_settings).await?)
}

async fn build_device(
&self,
object: &(
OwnedObjectPath,
HashMap<OwnedInterfaceName, HashMap<std::string::String, OwnedValue>>,
),
) -> Result<Device, ServiceError> {
let interfaces = &object.1;
Ok(Device {
device_info: self.build_device_info(object).await?,
component: None,
drive: None,
block_device: self.build_block_device(interfaces).await?,
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<Vec<Device>, ServiceError> {
let objects = self.object_manager_proxy.get_managed_objects().await?;
let mut result = vec![];
for object in objects {
let path = &object.0;
if !path.as_str().contains("Storage1/system") {
continue;
}

result.push(self.build_device(&object).await?)
}

Ok(result)
}

pub async fn staging_devices(&self) -> Result<Vec<Device>, ServiceError> {
let objects = self.object_manager_proxy.get_managed_objects().await?;
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,
object: &(
OwnedObjectPath,
HashMap<OwnedInterfaceName, HashMap<std::string::String, OwnedValue>>,
),
) -> Result<DeviceInfo, ServiceError> {
let interfaces = &object.1;
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 {
let message =
format!("storage device {} is missing Device interface", object.0).to_string();
Err(zbus::zvariant::Error::Message(message).into())
}
}

async fn build_block_device(
&self,
interfaces: &HashMap<OwnedInterfaceName, HashMap<std::string::String, OwnedValue>>,
) -> Result<Option<BlockDevice>, 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)
}
}
}

0 comments on commit 259b625

Please sign in to comment.