diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index 7ae2e2c1ab..321e4a02e8 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -5,9 +5,7 @@ use std::{ }; use crate::show_progress; -use agama_lib::{ - auth::AuthToken, connection, install_settings::InstallSettings, Store as SettingsStore, -}; +use agama_lib::{auth::AuthToken, install_settings::InstallSettings, Store as SettingsStore}; use anyhow::anyhow; use clap::Subcommand; use std::io::Write; @@ -48,7 +46,7 @@ pub async fn run(subcommand: ConfigCommands) -> anyhow::Result<()> { }; let client = agama_lib::http_client(token.as_str())?; - let store = SettingsStore::new(connection().await?, client).await?; + let store = SettingsStore::new(client).await?; match subcommand { ConfigCommands::Show => { diff --git a/rust/agama-cli/src/profile.rs b/rust/agama-cli/src/profile.rs index d2f0aa559b..ee20b6a823 100644 --- a/rust/agama-cli/src/profile.rs +++ b/rust/agama-cli/src/profile.rs @@ -1,6 +1,5 @@ use agama_lib::{ auth::AuthToken, - connection, install_settings::InstallSettings, profile::{AutoyastProfile, ProfileEvaluator, ProfileValidator, ValidationResult}, Store as SettingsStore, @@ -149,7 +148,7 @@ async fn import(url_string: String, dir: Option) -> anyhow::Result<()> async fn store_settings>(path: P) -> anyhow::Result<()> { let token = AuthToken::find().context("You are not logged in")?; let client = agama_lib::http_client(token.as_str())?; - let store = SettingsStore::new(connection().await?, client).await?; + let store = SettingsStore::new(client).await?; let settings = InstallSettings::from_file(&path)?; store.store(&settings).await?; Ok(()) diff --git a/rust/agama-lib/src/storage.rs b/rust/agama-lib/src/storage.rs index 66940a3599..e371b36e68 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 pub mod client; +pub mod http_client; pub mod model; pub mod proxies; mod settings; diff --git a/rust/agama-lib/src/storage/http_client.rs b/rust/agama-lib/src/storage/http_client.rs new file mode 100644 index 0000000000..2f623a142b --- /dev/null +++ b/rust/agama-lib/src/storage/http_client.rs @@ -0,0 +1,28 @@ +//! Implements a client to access Agama's storage service. +use crate::base_http_client::BaseHTTPClient; +use crate::storage::StorageSettings; +use crate::ServiceError; + +pub struct StorageHTTPClient { + client: BaseHTTPClient, +} + +impl StorageHTTPClient { + pub fn new() -> Result { + Ok(Self { + client: BaseHTTPClient::new()?, + }) + } + + pub fn new_with_base(base: BaseHTTPClient) -> Self { + Self { client: base } + } + + pub async fn get_config(&self) -> Result { + self.client.get("/storage/config").await + } + + pub async fn set_config(&self, config: &StorageSettings) -> Result<(), ServiceError> { + self.client.put_void("/storage/config", config).await + } +} diff --git a/rust/agama-lib/src/storage/store.rs b/rust/agama-lib/src/storage/store.rs index ae5b68cc49..36ca36b207 100644 --- a/rust/agama-lib/src/storage/store.rs +++ b/rust/agama-lib/src/storage/store.rs @@ -1,18 +1,18 @@ //! Implements the store for the storage settings. -use super::{StorageClient, StorageSettings}; +use super::StorageSettings; use crate::error::ServiceError; -use zbus::Connection; +use crate::storage::http_client::StorageHTTPClient; -/// Loads and stores the storage settings from/to the D-Bus service. -pub struct StorageStore<'a> { - storage_client: StorageClient<'a>, +/// Loads and stores the storage settings from/to the HTTP service. +pub struct StorageStore { + storage_client: StorageHTTPClient, } -impl<'a> StorageStore<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { +impl StorageStore { + pub fn new() -> Result { Ok(Self { - storage_client: StorageClient::new(connection).await?, + storage_client: StorageHTTPClient::new()?, }) } @@ -20,8 +20,83 @@ impl<'a> StorageStore<'a> { Ok(self.storage_client.get_config().await?) } - pub async fn store(&self, settings: StorageSettings) -> Result<(), ServiceError> { + pub async fn store(&self, settings: &StorageSettings) -> Result<(), ServiceError> { self.storage_client.set_config(settings).await?; Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::base_http_client::BaseHTTPClient; + use httpmock::prelude::*; + use std::error::Error; + use tokio::test; // without this, "error: async functions cannot be used for tests" + + fn storage_store(mock_server_url: String) -> StorageStore { + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = mock_server_url; + let client = StorageHTTPClient::new_with_base(bhc); + StorageStore { + storage_client: client, + } + } + + #[test] + async fn test_getting_storage() -> Result<(), Box> { + let server = MockServer::start(); + let storage_mock = server.mock(|when, then| { + when.method(GET).path("/api/storage/config"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "storage": { "some": "stuff" } + }"#, + ); + }); + let url = server.url("/api"); + + let store = storage_store(url); + let settings = store.load().await?; + + // main assertion + assert_eq!(settings.storage.unwrap().get(), r#"{ "some": "stuff" }"#); + assert!(settings.storage_autoyast.is_none()); + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + storage_mock.assert(); + Ok(()) + } + + #[test] + async fn test_setting_storage_ok() -> Result<(), Box> { + let server = MockServer::start(); + let storage_mock = server.mock(|when, then| { + when.method(PUT) + .path("/api/storage/config") + .header("content-type", "application/json") + .body(r#"{"legacyAutoyastStorage":{ "some" : "data" }}"#); + then.status(200); + }); + let url = server.url("/api"); + + let store = storage_store(url); + let boxed_raw_value = + serde_json::value::RawValue::from_string(r#"{ "some" : "data" }"#.to_owned())?; + let settings = StorageSettings { + storage: None, + storage_autoyast: Some(boxed_raw_value), + }; + + let result = store.store(&settings).await; + + // main assertion + result?; + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + storage_mock.assert(); + Ok(()) + } +} diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 8a043e6dcb..ad7cb10f90 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -7,7 +7,6 @@ use crate::{ localization::LocalizationStore, network::NetworkStore, product::ProductStore, software::SoftwareStore, storage::StorageStore, users::UsersStore, }; -use zbus::Connection; /// Struct that loads/stores the settings from/to the D-Bus services. /// @@ -15,27 +14,24 @@ use zbus::Connection; /// settings for each service. /// /// This struct uses the default connection built by [connection function](super::connection). -pub struct Store<'a> { +pub struct Store { users: UsersStore, network: NetworkStore, product: ProductStore, software: SoftwareStore, - storage: StorageStore<'a>, + storage: StorageStore, localization: LocalizationStore, } -impl<'a> Store<'a> { - pub async fn new( - connection: Connection, - http_client: reqwest::Client, - ) -> Result, ServiceError> { +impl Store { + pub async fn new(http_client: reqwest::Client) -> Result { Ok(Self { localization: LocalizationStore::new()?, users: UsersStore::new()?, network: NetworkStore::new(http_client).await?, product: ProductStore::new()?, software: SoftwareStore::new()?, - storage: StorageStore::new(connection).await?, + storage: StorageStore::new()?, }) } @@ -77,7 +73,7 @@ impl<'a> Store<'a> { self.users.store(user).await?; } if settings.storage.is_some() || settings.storage_autoyast.is_some() { - self.storage.store(settings.into()).await? + self.storage.store(&settings.into()).await? } Ok(()) } diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 0fc935f346..191a6ddbab 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -10,12 +10,12 @@ use agama_lib::{ storage::{ model::{Action, Device, DeviceSid, ProposalSettings, ProposalSettingsPatch, Volume}, proxies::Storage1Proxy, - StorageClient, + StorageClient, StorageSettings, }, }; use axum::{ extract::{Query, State}, - routing::{get, post}, + routing::{get, post, put}, Json, Router, }; use serde::{Deserialize, Serialize}; @@ -71,7 +71,7 @@ struct StorageState<'a> { client: StorageClient<'a>, } -/// Sets up and returns the axum service for the software module. +/// Sets up and returns the axum service for the storage 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"; @@ -87,6 +87,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result Result>) -> Result, Error> { + // StorageSettings is just a wrapper over serde_json::value::RawValue + let settings = state.client.get_config().await.map_err(Error::Service)?; + Ok(Json(settings)) +} + +/// Sets the storage configuration. +/// +/// * `state`: service state. +/// * `config`: storage configuration. +#[utoipa::path( + put, + path = "/config", + context_path = "/api/storage", + operation_id = "set_storage_config", + responses( + (status = 200, description = "Set the storage configuration"), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] +async fn set_config( + State(state): State>, + Json(settings): Json, +) -> Result, Error> { + let _status: u32 = state + .client + .set_config(settings) + .await + .map_err(Error::Service)?; + Ok(Json(())) +} + /// Probes the storage devices. #[utoipa::path( post, diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 78dd2970ff..8d2137f993 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Sep 18 08:27:13 UTC 2024 - Martin Vidner + +- For CLI, use HTTP clients instead of D-Bus clients, + final piece: Storage (gh#openSUSE/agama#1600) + - added StorageHTTPClient + ------------------------------------------------------------------- Wed Sep 13 12:25:28 UTC 2024 - Jorik Cronenberg