-
Notifications
You must be signed in to change notification settings - Fork 68
Storage adapt #1169
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
Changes from 15 commits
79b11a5
ed67ffa
251d0e4
e37c4ca
f4d20be
67a015c
fff5460
bb46e3b
2164ac6
36fdb6d
ba3d250
36ac90d
ca23a36
b4d9ae5
5b23d77
785dae7
fedf5e9
a598fb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,46 @@ | ||
| 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 dbus 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()) | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
| 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 | ||
|
|
@@ -16,18 +21,112 @@ pub struct StorageDevice { | |
| description: String, | ||
| } | ||
|
|
||
| /// Represent single change action done to storage | ||
jreidinger marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #[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 | ||
| #[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 single volume | ||
jreidinger marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct Volume { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| }) | ||
| } | ||
|
|
@@ -40,6 +139,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. | ||
|
|
@@ -55,22 +175,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 | ||
|
|
@@ -140,4 +276,110 @@ 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(interfaces).await?, | ||
| component: None, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we could implement
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking that in the end I will add to |
||
| 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 | ||
| .context("Failed to get managed objects")?; | ||
jreidinger marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| .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<OwnedInterfaceName, HashMap<std::string::String, OwnedValue>>, | ||
| ) -> Result<DeviceInfo, ServiceError> { | ||
| 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!( | ||
jreidinger marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "Device does not implement device info" | ||
| ))) | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.