Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jobs:
- name: Install required packages
run: zypper --non-interactive install
clang-devel
dbus-1-daemon
jq
libopenssl-3-devel
openssl-3
Expand Down
36 changes: 36 additions & 0 deletions rust/agama-lib/src/jobs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! This module implements support for the so-called Jobs. It is a concept hat represents running
//! an external command that may take some time, like formatting a DASD device. It is exposed via
//! D-Bus and, at this time, only the storage service makes use of it.

use std::collections::HashMap;

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

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

pub mod client;

/// Represents a job.
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Job {
/// Artificial job identifier.
pub id: String,
/// Whether the job is running.
pub running: bool,
/// Job exit code.
pub exit_code: u32,
}

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

fn try_from(value: &HashMap<String, OwnedValue>) -> Result<Self, Self::Error> {
Ok(Job {
running: get_property(value, "Running")?,
exit_code: get_property(value, "ExitCode")?,
..Default::default()
})
}
}
53 changes: 53 additions & 0 deletions rust/agama-lib/src/jobs/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Implements a client to access Agama's Jobs API.

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

use crate::error::ServiceError;

use super::Job;

#[derive(Clone)]
pub struct JobsClient<'a> {
object_manager_proxy: ObjectManagerProxy<'a>,
}

impl<'a> JobsClient<'a> {
pub async fn new(
connection: Connection,
destination: &'static str,
path: &'static str,
) -> Result<Self, ServiceError> {
let object_manager_proxy = ObjectManagerProxy::builder(&connection)
.destination(destination)?
.path(path)?
.build()
.await?;

Ok(Self {
object_manager_proxy,
})
}

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

let mut jobs = vec![];
for (path, ifaces) in managed_objects {
let Some(properties) = ifaces.get("org.opensuse.Agama.Storage1.Job") else {
continue;
};

match Job::try_from(properties) {
Ok(mut job) => {
job.id = path.to_string();
jobs.push((path, job));
}
Err(error) => {
log::warn!("Not a valid job: {}", error);
}
}
}

Ok(jobs)
}
}
1 change: 1 addition & 0 deletions rust/agama-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod auth;
pub mod base_http_client;
pub mod error;
pub mod install_settings;
pub mod jobs;
pub mod localization;
pub mod manager;
pub mod network;
Expand Down
26 changes: 26 additions & 0 deletions rust/agama-lib/src/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,29 @@ trait Locale {
/// SetLocale method
fn set_locale(&self, locale: &str) -> zbus::Result<()>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.Job",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1/jobs"
)]
trait Job {
#[dbus_proxy(property)]
fn running(&self) -> zbus::Result<bool>;

#[dbus_proxy(property)]
fn exit_code(&self) -> zbus::Result<u32>;

#[dbus_proxy(signal)]
fn finished(&self, exit_code: u32) -> zbus::Result<()>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.DASD.Format",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1/jobs/1"
)]
trait FormatJob {
#[dbus_proxy(property)]
fn summary(&self) -> zbus::Result<std::collections::HashMap<String, (u32, u32, bool)>>;
}
1 change: 1 addition & 0 deletions rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use zbus::fdo::ObjectManagerProxy;
use zbus::names::{InterfaceName, OwnedInterfaceName};
use zbus::zvariant::{OwnedObjectPath, OwnedValue};
use zbus::Connection;
pub mod dasd;
pub mod iscsi;

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

use zbus::{
fdo::ObjectManagerProxy,
zvariant::{ObjectPath, OwnedObjectPath},
Connection,
};

use crate::{
error::ServiceError,
storage::{model::dasd::DASDDevice, proxies::DASDManagerProxy},
};

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

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

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

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

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

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

pub async fn format(&self, ids: &[&str]) -> Result<String, ServiceError> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

API looks strange, it allows formating multiple DASDs but it return only one path for job.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there is only one job, that is ok, but then we need to check the status for each device, I think the missing part is the job Summary call on formatProgress callback.

https://github.com/openSUSE/agama/blob/master/web/src/client/storage.js#L891

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let selected = self.find_devices(ids).await?;
let references = selected.iter().collect::<Vec<&ObjectPath<'_>>>();
let (exit_code, job_path) = self.manager_proxy.format(&references).await?;
if exit_code != 0 {
return Err(ServiceError::UnsuccessfulAction("DASD format".to_string()));
}

Ok(job_path.to_string())
}

pub async fn enable(&self, ids: &[&str]) -> Result<(), ServiceError> {
let selected = self.find_devices(ids).await?;
let references = selected.iter().collect::<Vec<&ObjectPath<'_>>>();
self.manager_proxy.enable(&references).await?;
Ok(())
}

pub async fn disable(&self, ids: &[&str]) -> Result<(), ServiceError> {
let selected = self.find_devices(ids).await?;
let references = selected.iter().collect::<Vec<&ObjectPath<'_>>>();
self.manager_proxy.disable(&references).await?;
Ok(())
}

pub async fn set_diag(&self, ids: &[&str], diag: bool) -> Result<(), ServiceError> {
let selected = self.find_devices(ids).await?;
let references = selected.iter().collect::<Vec<&ObjectPath<'_>>>();
self.manager_proxy.set_diag(&references, diag).await?;
Ok(())
}

async fn find_devices(&self, ids: &[&str]) -> Result<Vec<ObjectPath<'_>>, ServiceError> {
let devices = self.devices().await?;
let selected: Vec<ObjectPath> = devices
.into_iter()
.filter_map(|(path, device)| {
if ids.contains(&device.id.as_str()) {
Some(path.into_inner())
} else {
None
}
})
.collect();
Ok(selected)
}
}
2 changes: 2 additions & 0 deletions rust/agama-lib/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use zbus::zvariant::{OwnedValue, Value};

use crate::dbus::{get_optional_property, get_property};

pub mod dasd;

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DeviceSid(u32);

Expand Down
46 changes: 46 additions & 0 deletions rust/agama-lib/src/storage/model/dasd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Implements a data model for DASD devices management.
use std::collections::HashMap;

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

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

/// Represents a DASD device (specific to s390x systems).
#[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DASDDevice {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some of the fields in this struct (status, device_type and/or access_type) might deserve an enum.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

btw resulting json is not consistent with others. I think agreemend is snakeCase...so let me change it to #[serde(rename_all = "camelCase")]

pub id: String,
pub enabled: bool,
pub device_name: String,
pub formatted: bool,
pub diag: bool,
pub status: String,
pub device_type: String,
pub access_type: String,
pub partition_info: String,
}
#[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)]
pub struct DASDFormatSummary {
pub total: u32,
pub step: u32,
pub done: bool,
}

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

fn try_from(value: &HashMap<String, OwnedValue>) -> Result<Self, Self::Error> {
Ok(DASDDevice {
id: get_property(value, "Id")?,
enabled: get_property(value, "Enabled")?,
device_name: get_property(value, "DeviceName")?,
formatted: get_property(value, "Formatted")?,
diag: get_property(value, "Diag")?,
status: get_property(value, "Status")?,
device_type: get_property(value, "Type")?,
access_type: get_property(value, "AccessType")?,
partition_info: get_property(value, "PartitionInfo")?,
})
}
}
72 changes: 72 additions & 0 deletions rust/agama-lib/src/storage/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,75 @@ trait Node {
#[dbus_proxy(property)]
fn target(&self) -> zbus::Result<String>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.DASD.Manager",
default_service = "org.opensuse.Agama.Storage1",
default_path = "/org/opensuse/Agama/Storage1"
)]
trait DASDManager {
/// Disable method
fn disable(&self, devices: &[&zbus::zvariant::ObjectPath<'_>]) -> zbus::Result<u32>;

/// Enable method
fn enable(&self, devices: &[&zbus::zvariant::ObjectPath<'_>]) -> zbus::Result<u32>;

/// Format method
fn format(
&self,
devices: &[&zbus::zvariant::ObjectPath<'_>],
) -> zbus::Result<(u32, zbus::zvariant::OwnedObjectPath)>;

/// Probe method
fn probe(&self) -> zbus::Result<()>;

/// SetDiag method
fn set_diag(
&self,
devices: &[&zbus::zvariant::ObjectPath<'_>],
diag: bool,
) -> zbus::Result<u32>;
}

#[dbus_proxy(
interface = "org.opensuse.Agama.Storage1.DASD.Device",
default_service = "org.opensuse.Agama.Storage1",
assume_defaults = true
)]
trait DASDDevice {
/// AccessType property
#[dbus_proxy(property)]
fn access_type(&self) -> zbus::Result<String>;

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

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

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

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

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

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

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

/// Type property
#[dbus_proxy(property)]
fn type_(&self) -> zbus::Result<String>;
}
Loading