Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storage adapt #1169

Merged
merged 18 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>(
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
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")]
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but we should start thinking about moving these structs to some model module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I already did it with device and its interfaces.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could implement Default or, if not possible, have some kind of constructor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking that in the end I will add to Device struct method try_from that will construct itself from that Object Manager struct. and here will be just Ok(object.into())

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)
}
}
}