From cf8ce2e9350694791f7bb86ec71b71aa40d43e12 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 13 Mar 2024 16:52:40 +0100 Subject: [PATCH 01/11] first draft of questions API in http --- rust/agama-lib/src/proxies.rs | 2 +- rust/agama-server/src/questions.rs | 3 +- rust/agama-server/src/questions/web.rs | 151 +++++++++++++++++++++++++ rust/agama-server/src/web.rs | 1 + rust/agama-server/src/web/event.rs | 1 + 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 rust/agama-server/src/questions/web.rs diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index a2d8673835..aefea02df9 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -89,7 +89,7 @@ trait Questions1 { /// New method #[dbus_proxy(name = "New")] - fn new_quetion( + fn new_question( &self, class: &str, text: &str, diff --git a/rust/agama-server/src/questions.rs b/rust/agama-server/src/questions.rs index fda860bf6d..8b4e6eb148 100644 --- a/rust/agama-server/src/questions.rs +++ b/rust/agama-server/src/questions.rs @@ -6,6 +6,7 @@ use log; use zbus::{dbus_interface, fdo::ObjectManager, zvariant::ObjectPath, Connection}; mod answers; +pub mod web; #[derive(Clone, Debug)] struct GenericQuestionObject(questions::GenericQuestion); @@ -94,7 +95,7 @@ trait AnswerStrategy { /// /// I gets as argument the question to answer. Returned value is pair /// of `answer` and `password` properties. If `None` is used in any - /// position it means that this object does not respond to given property. + /// position it means thatpam-devel this object does not respond to given property. /// /// It is object responsibility to provide correct pair. For example if /// possible answer can be "Ok" and "Cancel". Then for `Ok` password value diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs new file mode 100644 index 0000000000..73639814a4 --- /dev/null +++ b/rust/agama-server/src/questions/web.rs @@ -0,0 +1,151 @@ +//! This module implements the web API for the software module. +//! +//! The module offers two public functions: +//! +//! * `questions_service` which returns the Axum service. +//! * `questions_stream` which offers an stream that emits questions related signals. + +use std::collections::HashMap; +use crate::{error::Error, web::Event}; +use agama_lib::{ + error::ServiceError, proxies::Questions1Proxy, +}; +use axum::{ + extract::{State, Path}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, put}, + Json, Router, +}; +use tokio_stream::{Stream, StreamExt}; +use zbus::fdo::ObjectManagerProxy; +use thiserror::Error; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +// TODO: move to lib +#[derive(Clone)] +struct QuestionsClient<'a> { + connection: zbus::Connection, + questions_proxy: Questions1Proxy<'a>, + objects_proxy: ObjectManagerProxy<'a>, +} + +impl<'a> QuestionsClient<'a> { + pub async fn new(dbus: zbus::Connection) -> Result { + Ok(Self { + connection: dbus.clone(), + questions_proxy: Questions1Proxy::new(&dbus).await?, + objects_proxy: ObjectManagerProxy::new(&dbus).await? + }) + } + + pub async fn questions(self) -> Result, ServiceError> { + // TODO: real call to dbus + Ok(vec![]) + } +} + +#[derive(Error, Debug)] +pub enum QuestionsError { + #[error("Question service error: {0}")] + Error(#[from] ServiceError), +} + +impl IntoResponse for QuestionsError { + fn into_response(self) -> Response { + let body = json!({ + "error": self.to_string() + }); + (StatusCode::BAD_REQUEST, Json(body)).into_response() + } +} + +#[derive(Clone)] +struct QuestionsState<'a> { + questions: QuestionsClient<'a>, +} + +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Question { + generic: GenericQuestion, + with_password: Option, +} + +/// Facade of agama_lib::questions::GenericQuestion +/// For fields details see it. +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct GenericQuestion { + id: u32, + class: String, + text: String, + options: Vec, + default_option: String, + data: HashMap +} + +/// Facade of agama_lib::questions::WithPassword +/// For fields details see it. +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct QuestionWithPassword { + password: String +} + +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Answer { + generic: GenericAnswer, + with_password: Option, +} + +/// Answer needed for GenericQuestion +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct GenericAnswer { + answer: String +} + +/// Answer needed for Password specific questions. +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PasswordAnswer { + password: String +} +/// Sets up and returns the axum service for the questions module. +pub async fn questions_service(dbus: zbus::Connection) -> Result { + let questions = QuestionsClient::new(dbus.clone()).await?; + let state = QuestionsState { questions }; + let router = Router::new() + .route("/questions", get(list_questions)) + .route("/questions/:id/answer", put(answer)) + .with_state(state); + Ok(router) +} + +pub async fn questions_stream(dbus: zbus::Connection) -> Result, Error> { + let proxy = ObjectManagerProxy::new(&dbus).await?; + let add_stream = proxy + .receive_interfaces_added() + .await? + .then(|_| async move { + Event::QuestionsChanged + }); + let remove_stream = proxy + .receive_interfaces_removed() + .await? + .then(|_| async move { + Event::QuestionsChanged + }); + Ok(StreamExt::merge(add_stream, remove_stream)) +} + +async fn list_questions(State(state): State> + ) -> Result>, QuestionsError> { + Ok(Json(state.questions.questions().await?)) +} + +async fn answer( +State(state): State>, +Path(question_id): Path, +Json(answer): Json +) -> Result<(), QuestionsError> { + //TODO: real answer + Ok(()) +} \ No newline at end of file diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index b088528589..c5e754df4e 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -9,6 +9,7 @@ use crate::{ error::Error, l10n::web::l10n_service, software::web::{software_service, software_stream}, + questions::web::{questions_service, questions_stream} }; use axum::Router; diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index a84956c2f3..9ccac0209e 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -10,6 +10,7 @@ pub enum Event { Progress(Progress), ProductChanged { id: String }, PatternsChanged(HashMap), + QuestionsChanged } pub type EventsSender = Sender; From 76caa2f3919c8798ae6d65030b4a640d32e6588b Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 15 Mar 2024 09:53:59 +0100 Subject: [PATCH 02/11] add proxies for question interfaces --- rust/agama-lib/src/proxies.rs | 48 ++++++++++++++++++++++++++ rust/agama-server/src/questions.rs | 2 +- rust/agama-server/src/questions/web.rs | 16 ++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index aefea02df9..a57711037e 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -114,3 +114,51 @@ trait Questions1 { #[dbus_proxy(property)] fn set_interactive(&self, value: bool) -> zbus::Result<()>; } + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Questions.Generic", + assume_defaults = true +)] +trait GenericQuestion { + /// Answer property + #[dbus_proxy(property)] + fn answer(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_answer(&self, value: &str) -> zbus::Result<()>; + + /// Class property + #[dbus_proxy(property)] + fn class(&self) -> zbus::Result; + + /// Data property + #[dbus_proxy(property)] + fn data(&self) -> zbus::Result>; + + /// DefaultOption property + #[dbus_proxy(property)] + fn default_option(&self) -> zbus::Result; + + /// Id property + #[dbus_proxy(property)] + fn id(&self) -> zbus::Result; + + /// Options property + #[dbus_proxy(property)] + fn options(&self) -> zbus::Result>; + + /// Text property + #[dbus_proxy(property)] + fn text(&self) -> zbus::Result; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Questions.WithPassword", + assume_defaults = true +)] +trait QuestionWithPassword { + /// Password property + #[dbus_proxy(property)] + fn password(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_password(&self, value: &str) -> zbus::Result<()>; +} diff --git a/rust/agama-server/src/questions.rs b/rust/agama-server/src/questions.rs index 8b4e6eb148..bc547b3ae5 100644 --- a/rust/agama-server/src/questions.rs +++ b/rust/agama-server/src/questions.rs @@ -95,7 +95,7 @@ trait AnswerStrategy { /// /// I gets as argument the question to answer. Returned value is pair /// of `answer` and `password` properties. If `None` is used in any - /// position it means thatpam-devel this object does not respond to given property. + /// position it means that this object does not respond to given property. /// /// It is object responsibility to provide correct pair. For example if /// possible answer can be "Ok" and "Cancel". Then for `Ok` password value diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 73639814a4..74bfb9ea4d 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -1,4 +1,4 @@ -//! This module implements the web API for the software module. +//! This module implements the web API for the questions module. //! //! The module offers two public functions: //! @@ -10,6 +10,7 @@ use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, proxies::Questions1Proxy, }; +use anyhow::Context; use axum::{ extract::{State, Path}, http::StatusCode, @@ -19,6 +20,8 @@ use axum::{ }; use tokio_stream::{Stream, StreamExt}; use zbus::fdo::ObjectManagerProxy; +use zbus::zvariant::OwnedObjectPath; +use zbus::zvariant::ObjectPath; use thiserror::Error; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -44,6 +47,17 @@ impl<'a> QuestionsClient<'a> { // TODO: real call to dbus Ok(vec![]) } + + pub async fn answer(self, id: u32, answer: Answer) -> Result<(), ServiceError> { + let objects = self.objects_proxy.get_managed_objects().await + .context("failed to get managed object with Object Manager")?; + let question_path = OwnedObjectPath::from( + ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) + .context("Failed to create dbus path")? + ); + let question = objects.get(&question_path); + Ok(()) + } } #[derive(Error, Debug)] From 9de4004be0280dbec6439848e06d16e8f0573da5 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 19 Mar 2024 21:29:06 +0100 Subject: [PATCH 03/11] initial implementation of real service without attachment --- rust/agama-lib/src/proxies.rs | 6 ++- rust/agama-server/src/questions/web.rs | 69 +++++++++++++++++++++----- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index a57711037e..870e281406 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -117,7 +117,8 @@ trait Questions1 { #[dbus_proxy( interface = "org.opensuse.Agama1.Questions.Generic", - assume_defaults = true + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Questions" )] trait GenericQuestion { /// Answer property @@ -153,7 +154,8 @@ trait GenericQuestion { #[dbus_proxy( interface = "org.opensuse.Agama1.Questions.WithPassword", - assume_defaults = true + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Questions" )] trait QuestionWithPassword { /// Password property diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 74bfb9ea4d..01f5a38e3c 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use crate::{error::Error, web::Event}; use agama_lib::{ - error::ServiceError, proxies::Questions1Proxy, + error::ServiceError, proxies::{GenericQuestionProxy, QuestionWithPasswordProxy}, }; use anyhow::Context; use axum::{ @@ -19,7 +19,7 @@ use axum::{ Json, Router, }; use tokio_stream::{Stream, StreamExt}; -use zbus::fdo::ObjectManagerProxy; +use zbus::{fdo::ObjectManagerProxy, names::{InterfaceName, OwnedInterfaceName}}; use zbus::zvariant::OwnedObjectPath; use zbus::zvariant::ObjectPath; use thiserror::Error; @@ -30,7 +30,6 @@ use serde_json::json; #[derive(Clone)] struct QuestionsClient<'a> { connection: zbus::Connection, - questions_proxy: Questions1Proxy<'a>, objects_proxy: ObjectManagerProxy<'a>, } @@ -38,24 +37,70 @@ impl<'a> QuestionsClient<'a> { pub async fn new(dbus: zbus::Connection) -> Result { Ok(Self { connection: dbus.clone(), - questions_proxy: Questions1Proxy::new(&dbus).await?, objects_proxy: ObjectManagerProxy::new(&dbus).await? }) } - pub async fn questions(self) -> Result, ServiceError> { - // TODO: real call to dbus - Ok(vec![]) - } - - pub async fn answer(self, id: u32, answer: Answer) -> Result<(), ServiceError> { + pub async fn questions(&self) -> Result, ServiceError> { let objects = self.objects_proxy.get_managed_objects().await .context("failed to get managed object with Object Manager")?; + let mut result: Vec = Vec::with_capacity(objects.len()); + let password_interface = OwnedInterfaceName::from( + InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword") + .context("Failed to create interface name for question with password")? + ); + for (path, interfaces_hash) in objects.iter() { + if interfaces_hash.contains_key(&password_interface) { + result.push(self.create_question_with_password(&path).await?) + } else { + result.push(self.create_generic_question(&path).await?) + } + } + Ok(result) + } + + async fn create_generic_question(&self, path: &OwnedObjectPath) -> Result { + let dbus_question = GenericQuestionProxy::builder(&self.connection) + .path(path)?.cache_properties(zbus::CacheProperties::No).build().await?; + let result = Question { + generic: GenericQuestion { + id: dbus_question.id().await?, + class: dbus_question.class().await?, + text: dbus_question.text().await?, + options: dbus_question.options().await?, + default_option: dbus_question.default_option().await?, + data: dbus_question.data().await? + }, + with_password: None + }; + + Ok(result) + } + + async fn create_question_with_password(&self, path: &OwnedObjectPath) -> Result { + let dbus_question = QuestionWithPasswordProxy::builder(&self.connection) + .path(path)?.cache_properties(zbus::CacheProperties::No).build().await?; + let mut result = self.create_generic_question(path).await?; + result.with_password = Some(QuestionWithPassword{ + password: dbus_question.password().await? + }); + + Ok(result) + } + + pub async fn answer(&self, id: u32, answer: Answer) -> Result<(), ServiceError> { let question_path = OwnedObjectPath::from( ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) .context("Failed to create dbus path")? ); - let question = objects.get(&question_path); + if let Some(password) = answer.with_password { + let dbus_password = QuestionWithPasswordProxy::builder(&self.connection) + .path(&question_path)?.cache_properties(zbus::CacheProperties::No).build().await?; + dbus_password.set_password(password.password.as_str()).await? + } + let dbus_generic = GenericQuestionProxy::builder(&self.connection) + .path(&question_path)?.cache_properties(zbus::CacheProperties::No).build().await?; + dbus_generic.set_answer(answer.generic.answer.as_str()).await?; Ok(()) } } @@ -160,6 +205,6 @@ State(state): State>, Path(question_id): Path, Json(answer): Json ) -> Result<(), QuestionsError> { - //TODO: real answer + state.questions.answer(question_id, answer).await?; Ok(()) } \ No newline at end of file From eebbbd444d11b7b8ec6a3fbae6fd3fb11e1610ce Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 07:37:54 +0100 Subject: [PATCH 04/11] fix routing of questions --- rust/agama-server/src/questions/web.rs | 12 +++++++----- rust/agama-server/src/web.rs | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 01f5a38e3c..47645495ba 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -5,7 +5,7 @@ //! * `questions_service` which returns the Axum service. //! * `questions_stream` which offers an stream that emits questions related signals. -use std::collections::HashMap; +use std::{collections::HashMap,pin::Pin}; use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, proxies::{GenericQuestionProxy, QuestionWithPasswordProxy}, @@ -172,13 +172,14 @@ pub async fn questions_service(dbus: zbus::Connection) -> Result Result, Error> { +pub async fn questions_stream(dbus: zbus::Connection) -> + Result + Send>>, Error> { let proxy = ObjectManagerProxy::new(&dbus).await?; let add_stream = proxy .receive_interfaces_added() @@ -192,7 +193,8 @@ pub async fn questions_stream(dbus: zbus::Connection) -> Result> diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 11eeb5e989..d142cb0d6b 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -9,7 +9,7 @@ use crate::{ l10n::web::l10n_service, manager::web::{manager_service, manager_stream}, software::web::{software_service, software_stream}, - questions::web::{questions_service, questions_stream} + questions::web::{questions_service, questions_stream}, web::common::{progress_stream, service_status_stream}, }; use axum::Router; @@ -51,7 +51,8 @@ where let router = MainServiceBuilder::new(events.clone(), web_ui_dir) .add_service("/l10n", l10n_service(events.clone())) .add_service("/manager", manager_service(dbus.clone()).await?) - .add_service("/software", software_service(dbus).await?) + .add_service("/software", software_service(dbus.clone()).await?) + .add_service("/questions", questions_service(dbus).await?) .with_config(config) .build(); Ok(router) @@ -115,6 +116,7 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res ) .await?, ); + stream.insert("questions", questions_stream(dbus.clone()).await?); tokio::pin!(stream); let e = events.clone(); From 22a936c53e9a3bee2b918fa4d63c08db6bb5f6e6 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 08:00:12 +0100 Subject: [PATCH 05/11] apply formatter and fix creating object proxy --- rust/agama-server/src/questions/web.rs | 131 ++++++++++++++++--------- rust/agama-server/src/web.rs | 2 +- 2 files changed, 87 insertions(+), 46 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 47645495ba..66a92e19b2 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -5,26 +5,30 @@ //! * `questions_service` which returns the Axum service. //! * `questions_stream` which offers an stream that emits questions related signals. -use std::{collections::HashMap,pin::Pin}; use crate::{error::Error, web::Event}; use agama_lib::{ - error::ServiceError, proxies::{GenericQuestionProxy, QuestionWithPasswordProxy}, + error::ServiceError, + proxies::{GenericQuestionProxy, QuestionWithPasswordProxy}, }; use anyhow::Context; use axum::{ - extract::{State, Path}, + extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, put}, Json, Router, }; -use tokio_stream::{Stream, StreamExt}; -use zbus::{fdo::ObjectManagerProxy, names::{InterfaceName, OwnedInterfaceName}}; -use zbus::zvariant::OwnedObjectPath; -use zbus::zvariant::ObjectPath; -use thiserror::Error; use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{collections::HashMap, pin::Pin}; +use thiserror::Error; +use tokio_stream::{Stream, StreamExt}; +use zbus::zvariant::ObjectPath; +use zbus::zvariant::OwnedObjectPath; +use zbus::{ + fdo::ObjectManagerProxy, + names::{InterfaceName, OwnedInterfaceName}, +}; // TODO: move to lib #[derive(Clone)] @@ -35,19 +39,27 @@ struct QuestionsClient<'a> { impl<'a> QuestionsClient<'a> { pub async fn new(dbus: zbus::Connection) -> Result { + let question_path = + OwnedObjectPath::from(ObjectPath::try_from("/org/opensuse/Agama1/Questions")?); Ok(Self { connection: dbus.clone(), - objects_proxy: ObjectManagerProxy::new(&dbus).await? + objects_proxy: ObjectManagerProxy::builder(&dbus) + .path(question_path)? + .build() + .await?, }) } pub async fn questions(&self) -> Result, ServiceError> { - let objects = self.objects_proxy.get_managed_objects().await + let objects = self + .objects_proxy + .get_managed_objects() + .await .context("failed to get managed object with Object Manager")?; let mut result: Vec = Vec::with_capacity(objects.len()); let password_interface = OwnedInterfaceName::from( InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword") - .context("Failed to create interface name for question with password")? + .context("Failed to create interface name for question with password")?, ); for (path, interfaces_hash) in objects.iter() { if interfaces_hash.contains_key(&password_interface) { @@ -59,9 +71,15 @@ impl<'a> QuestionsClient<'a> { Ok(result) } - async fn create_generic_question(&self, path: &OwnedObjectPath) -> Result { + async fn create_generic_question( + &self, + path: &OwnedObjectPath, + ) -> Result { let dbus_question = GenericQuestionProxy::builder(&self.connection) - .path(path)?.cache_properties(zbus::CacheProperties::No).build().await?; + .path(path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?; let result = Question { generic: GenericQuestion { id: dbus_question.id().await?, @@ -69,20 +87,26 @@ impl<'a> QuestionsClient<'a> { text: dbus_question.text().await?, options: dbus_question.options().await?, default_option: dbus_question.default_option().await?, - data: dbus_question.data().await? + data: dbus_question.data().await?, }, - with_password: None + with_password: None, }; Ok(result) } - async fn create_question_with_password(&self, path: &OwnedObjectPath) -> Result { + async fn create_question_with_password( + &self, + path: &OwnedObjectPath, + ) -> Result { let dbus_question = QuestionWithPasswordProxy::builder(&self.connection) - .path(path)?.cache_properties(zbus::CacheProperties::No).build().await?; + .path(path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?; let mut result = self.create_generic_question(path).await?; - result.with_password = Some(QuestionWithPassword{ - password: dbus_question.password().await? + result.with_password = Some(QuestionWithPassword { + password: dbus_question.password().await?, }); Ok(result) @@ -91,16 +115,26 @@ impl<'a> QuestionsClient<'a> { pub async fn answer(&self, id: u32, answer: Answer) -> Result<(), ServiceError> { let question_path = OwnedObjectPath::from( ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) - .context("Failed to create dbus path")? - ); + .context("Failed to create dbus path")?, + ); if let Some(password) = answer.with_password { let dbus_password = QuestionWithPasswordProxy::builder(&self.connection) - .path(&question_path)?.cache_properties(zbus::CacheProperties::No).build().await?; - dbus_password.set_password(password.password.as_str()).await? + .path(&question_path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?; + dbus_password + .set_password(password.password.as_str()) + .await? } let dbus_generic = GenericQuestionProxy::builder(&self.connection) - .path(&question_path)?.cache_properties(zbus::CacheProperties::No).build().await?; - dbus_generic.set_answer(answer.generic.answer.as_str()).await?; + .path(&question_path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?; + dbus_generic + .set_answer(answer.generic.answer.as_str()) + .await?; Ok(()) } } @@ -140,14 +174,14 @@ pub struct GenericQuestion { text: String, options: Vec, default_option: String, - data: HashMap + data: HashMap, } /// Facade of agama_lib::questions::WithPassword /// For fields details see it. #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct QuestionWithPassword { - password: String + password: String, } #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] @@ -159,13 +193,13 @@ pub struct Answer { /// Answer needed for GenericQuestion #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct GenericAnswer { - answer: String + answer: String, } /// Answer needed for Password specific questions. #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct PasswordAnswer { - password: String + password: String, } /// Sets up and returns the axum service for the questions module. pub async fn questions_service(dbus: zbus::Connection) -> Result { @@ -178,35 +212,42 @@ pub async fn questions_service(dbus: zbus::Connection) -> Result - Result + Send>>, Error> { - let proxy = ObjectManagerProxy::new(&dbus).await?; +pub async fn questions_stream( + dbus: zbus::Connection, +) -> Result + Send>>, Error> { + let question_path = OwnedObjectPath::from( + ObjectPath::try_from("/org/opensuse/Agama1/Questions") + .context("failed to create object path")?, + ); + let proxy = ObjectManagerProxy::builder(&dbus) + .path(question_path) + .context("Failed to create object manager path")? + .build() + .await + .context("Failed to create Object MAnager proxy")?; let add_stream = proxy .receive_interfaces_added() .await? - .then(|_| async move { - Event::QuestionsChanged - }); + .then(|_| async move { Event::QuestionsChanged }); let remove_stream = proxy .receive_interfaces_removed() .await? - .then(|_| async move { - Event::QuestionsChanged - }); + .then(|_| async move { Event::QuestionsChanged }); let stream = StreamExt::merge(add_stream, remove_stream); Ok(Box::pin(stream)) } -async fn list_questions(State(state): State> - ) -> Result>, QuestionsError> { - Ok(Json(state.questions.questions().await?)) +async fn list_questions( + State(state): State>, +) -> Result>, QuestionsError> { + Ok(Json(state.questions.questions().await?)) } async fn answer( -State(state): State>, -Path(question_id): Path, -Json(answer): Json + State(state): State>, + Path(question_id): Path, + Json(answer): Json, ) -> Result<(), QuestionsError> { state.questions.answer(question_id, answer).await?; Ok(()) -} \ No newline at end of file +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index d142cb0d6b..fb7c8ede6c 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -8,8 +8,8 @@ use crate::{ error::Error, l10n::web::l10n_service, manager::web::{manager_service, manager_stream}, - software::web::{software_service, software_stream}, questions::web::{questions_service, questions_stream}, + software::web::{software_service, software_stream}, web::common::{progress_stream, service_status_stream}, }; use axum::Router; From 281e053e04e17133170791223f5816c0d415c641 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 12:01:48 +0100 Subject: [PATCH 06/11] add also proper destination for object manager --- rust/agama-server/src/questions/web.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 66a92e19b2..8694eb88dc 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -45,6 +45,7 @@ impl<'a> QuestionsClient<'a> { connection: dbus.clone(), objects_proxy: ObjectManagerProxy::builder(&dbus) .path(question_path)? + .destination("org.opensuse.Agama1.Questions")? .build() .await?, }) @@ -222,6 +223,7 @@ pub async fn questions_stream( let proxy = ObjectManagerProxy::builder(&dbus) .path(question_path) .context("Failed to create object manager path")? + .destination("org.opensuse.Agama1.Questions")? .build() .await .context("Failed to create Object MAnager proxy")?; From ba5a4dd242455a902bc1810854ffaab51defa2f7 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 12:25:55 +0100 Subject: [PATCH 07/11] another fix for destination --- rust/agama-server/src/questions/web.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 8694eb88dc..109082161d 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -45,7 +45,7 @@ impl<'a> QuestionsClient<'a> { connection: dbus.clone(), objects_proxy: ObjectManagerProxy::builder(&dbus) .path(question_path)? - .destination("org.opensuse.Agama1.Questions")? + .destination("org.opensuse.Agama1")? .build() .await?, }) @@ -223,7 +223,7 @@ pub async fn questions_stream( let proxy = ObjectManagerProxy::builder(&dbus) .path(question_path) .context("Failed to create object manager path")? - .destination("org.opensuse.Agama1.Questions")? + .destination("org.opensuse.Agama1")? .build() .await .context("Failed to create Object MAnager proxy")?; From da4a3437832e17a43eb7a274947a925b9da7ac5f Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 21:57:39 +0100 Subject: [PATCH 08/11] provide openapi documentation --- rust/agama-server/src/questions/web.rs | 16 ++++++++++++++++ rust/agama-server/src/web/docs.rs | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 109082161d..22ecf54212 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -239,12 +239,28 @@ pub async fn questions_stream( Ok(Box::pin(stream)) } +/// Returns the list of questions that waits for answer. +/// +/// * `state`: service state. +#[utoipa::path(get, path = "/questions", responses( + (status = 200, description = "List of open questions", body = Vec), + (status = 400, description = "The D-Bus service could not perform the action") +))] async fn list_questions( State(state): State>, ) -> Result>, QuestionsError> { Ok(Json(state.questions.questions().await?)) } +/// Provide answer to question. +/// +/// * `state`: service state. +/// * `questions_id`: id of question +/// * `answer`: struct with answer and possible other data needed for answer like password +#[utoipa::path(put, path = "/questions/:id/answer", responses( + (status = 200, description = "answer question"), + (status = 400, description = "The D-Bus service could not perform the action") +))] async fn answer( State(state): State>, Path(question_id): Path, diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 34ded88acf..af8309be15 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -16,6 +16,8 @@ use utoipa::OpenApi; crate::manager::web::install_action, crate::manager::web::finish_action, crate::manager::web::installer_status, + crate::questions::web::list_questions, + crate::questions::web::answer, super::http::ping, ), components( @@ -30,6 +32,12 @@ use utoipa::OpenApi; schemas(crate::software::web::SoftwareConfig), schemas(crate::software::web::SoftwareProposal), schemas(crate::manager::web::InstallerStatus), + schemas(crate::questions::web::Question), + schemas(crate::questions::web::GenericQuestion), + schemas(crate::questions::web::QuestionWithPassword), + schemas(crate::questions::web::Answer), + schemas(crate::questions::web::GenericAnswer), + schemas(crate::questions::web::PasswordAnswer), schemas(super::http::PingResponse), ) )] From 189f24ea81e2bc7db825426e3f36395e59bc0494 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 21 Mar 2024 22:31:25 +0100 Subject: [PATCH 09/11] apply clippy suggestions --- rust/agama-cli/src/auth.rs | 8 ++++---- rust/agama-lib/src/manager.rs | 2 +- rust/agama-server/src/agama-web-server.rs | 2 +- rust/agama-server/src/l10n.rs | 6 +++--- rust/agama-server/src/l10n/web.rs | 2 +- rust/agama-server/src/manager/web.rs | 4 ++-- rust/agama-server/src/questions/web.rs | 4 ++-- rust/agama-server/src/software/web.rs | 8 ++++---- rust/agama-server/src/web/common.rs | 8 ++++---- rust/agama-server/tests/common/mod.rs | 6 ++++++ 10 files changed, 28 insertions(+), 22 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 2d7321fd89..a93441bcd8 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -1,5 +1,5 @@ use clap::{arg, Args, Subcommand}; -use home; + use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::fs; use std::fs::File; @@ -36,7 +36,7 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { /// Reads stored token and returns it fn jwt() -> anyhow::Result { if let Some(file) = jwt_file() { - if let Ok(token) = read_line_from_file(&file.as_path()) { + if let Ok(token) = read_line_from_file(file.as_path()) { return Ok(token); } } @@ -93,7 +93,7 @@ impl Credentials for KnownCredentials { impl Credentials for FileCredentials { fn password(&self) -> io::Result { - read_line_from_file(&self.path.as_path()) + read_line_from_file(self.path.as_path()) } } @@ -119,7 +119,7 @@ fn read_line_from_file(path: &Path) -> io::Result { )); } - if let Ok(file) = File::open(&path) { + if let Ok(file) = File::open(path) { // cares only of first line, take everything. No comments // or something like that supported let raw = BufReader::new(file).lines().next(); diff --git a/rust/agama-lib/src/manager.rs b/rust/agama-lib/src/manager.rs index 2c4e3e9ada..ef7d9f7bc2 100644 --- a/rust/agama-lib/src/manager.rs +++ b/rust/agama-lib/src/manager.rs @@ -61,7 +61,7 @@ impl<'a> ManagerClient<'a> { /// Returns the current installation phase. pub async fn current_installation_phase(&self) -> Result { let phase = self.manager_proxy.current_installation_phase().await?; - Ok(phase.try_into()?) + phase.try_into() } /// Starts the probing process. diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 46c14797d0..215250ede5 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -13,7 +13,7 @@ use tokio::sync::broadcast::channel; use tracing_subscriber::prelude::*; use utoipa::OpenApi; -const DEFAULT_WEB_UI_DIR: &'static str = "/usr/share/agama/web_ui"; +const DEFAULT_WEB_UI_DIR: &str = "/usr/share/agama/web_ui"; #[derive(Subcommand, Debug)] enum Commands { diff --git a/rust/agama-server/src/l10n.rs b/rust/agama-server/src/l10n.rs index e130c01a46..afa4db98b6 100644 --- a/rust/agama-server/src/l10n.rs +++ b/rust/agama-server/src/l10n.rs @@ -40,9 +40,9 @@ impl Locale { #[dbus_interface(property)] fn set_locales(&mut self, locales: Vec) -> zbus::fdo::Result<()> { if locales.is_empty() { - return Err(zbus::fdo::Error::Failed(format!( - "The locales list cannot be empty" - ))); + return Err(zbus::fdo::Error::Failed( + "The locales list cannot be empty".to_string(), + )); } for loc in &locales { if !self.locales_db.exists(loc.as_str()) { diff --git a/rust/agama-server/src/l10n/web.rs b/rust/agama-server/src/l10n/web.rs index 5cb2a7f777..271a2bd846 100644 --- a/rust/agama-server/src/l10n/web.rs +++ b/rust/agama-server/src/l10n/web.rs @@ -153,7 +153,7 @@ async fn set_config( .output() .map_err(LocaleError::Commit)?; Command::new("/usr/bin/setxkbmap") - .arg(&ui_keymap) + .arg(ui_keymap) .env("DISPLAY", ":0") .output() .map_err(LocaleError::Commit)?; diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index ce3ebe64d5..3564dd787b 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -77,8 +77,8 @@ pub async fn manager_stream( /// Sets up and returns the axum service for the manager module pub async fn manager_service(dbus: zbus::Connection) -> Result { - const DBUS_SERVICE: &'static str = "org.opensuse.Agama.Manager1"; - const DBUS_PATH: &'static str = "/org/opensuse/Agama/Manager1"; + const DBUS_SERVICE: &str = "org.opensuse.Agama.Manager1"; + const DBUS_PATH: &str = "/org/opensuse/Agama/Manager1"; let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; let progress_router = progress_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 22ecf54212..b8ee3142fc 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -64,9 +64,9 @@ impl<'a> QuestionsClient<'a> { ); for (path, interfaces_hash) in objects.iter() { if interfaces_hash.contains_key(&password_interface) { - result.push(self.create_question_with_password(&path).await?) + result.push(self.create_question_with_password(path).await?) } else { - result.push(self.create_generic_question(&path).await?) + result.push(self.create_generic_question(path).await?) } } Ok(result) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index f5c7bd6cb5..e8f7effb4e 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -112,9 +112,9 @@ fn reason_to_selected_by( /// Sets up and returns the axum service for the software module. pub async fn software_service(dbus: zbus::Connection) -> Result { - const DBUS_SERVICE: &'static str = "org.opensuse.Agama.Software1"; - const DBUS_PATH: &'static str = "/org/opensuse/Agama/Software1"; - const DBUS_PRODUCT_PATH: &'static str = "/org/opensuse/Agama/Software1/Product"; + const DBUS_SERVICE: &str = "org.opensuse.Agama.Software1"; + const DBUS_PATH: &str = "/org/opensuse/Agama/Software1"; + const DBUS_PRODUCT_PATH: &str = "/org/opensuse/Agama/Software1/Product"; let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; let progress_router = progress_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; @@ -229,7 +229,7 @@ async fn get_config(State(state): State>) -> Result( destination: &str, path: &str, ) -> Result, zbus::Error> { - let proxy = ServiceStatusProxy::builder(&dbus) + let proxy = ServiceStatusProxy::builder(dbus) .destination(destination.to_string())? .path(path.to_string())? .build() @@ -205,7 +205,7 @@ impl<'a> Stream for ProgressStream<'a> { let pinned = self.project(); match pinned.inner.poll_next(cx) { Poll::Pending => Poll::Pending, - Poll::Ready(_change) => match Progress::from_cached_proxy(&pinned.proxy) { + Poll::Ready(_change) => match Progress::from_cached_proxy(pinned.proxy) { Some(progress) => { let event = Event::Progress { progress, @@ -224,7 +224,7 @@ async fn build_progress_proxy<'a>( destination: &str, path: &str, ) -> Result, zbus::Error> { - let proxy = ProgressProxy::builder(&dbus) + let proxy = ProgressProxy::builder(dbus) .destination(destination.to_string())? .path(path.to_string())? .build() @@ -345,7 +345,7 @@ async fn build_issues_proxy<'a>( destination: &str, path: &str, ) -> Result, zbus::Error> { - let proxy = IssuesProxy::builder(&dbus) + let proxy = IssuesProxy::builder(dbus) .destination(destination.to_string())? .path(path.to_string())? .build() diff --git a/rust/agama-server/tests/common/mod.rs b/rust/agama-server/tests/common/mod.rs index cd77539774..4525c96fa4 100644 --- a/rust/agama-server/tests/common/mod.rs +++ b/rust/agama-server/tests/common/mod.rs @@ -41,6 +41,12 @@ pub trait ServerState {} impl ServerState for Started {} impl ServerState for Stopped {} +impl Default for DBusServer { + fn default() -> Self { + Self::new() + } +} + impl DBusServer { pub fn new() -> Self { let uuid = Uuid::new_v4(); From bccf78cf030518a852b89ec4b6f5326e3a192bf1 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 22 Mar 2024 16:39:23 +0100 Subject: [PATCH 10/11] apply suggestions --- rust/agama-server/src/questions/web.rs | 36 ++++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index b8ee3142fc..44d09b2ecb 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -13,21 +13,16 @@ use agama_lib::{ use anyhow::Context; use axum::{ extract::{Path, State}, - http::StatusCode, - response::{IntoResponse, Response}, routing::{get, put}, Json, Router, }; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::{collections::HashMap, pin::Pin}; -use thiserror::Error; use tokio_stream::{Stream, StreamExt}; -use zbus::zvariant::ObjectPath; -use zbus::zvariant::OwnedObjectPath; use zbus::{ fdo::ObjectManagerProxy, names::{InterfaceName, OwnedInterfaceName}, + zvariant::{ObjectPath,OwnedObjectPath} }; // TODO: move to lib @@ -140,21 +135,6 @@ impl<'a> QuestionsClient<'a> { } } -#[derive(Error, Debug)] -pub enum QuestionsError { - #[error("Question service error: {0}")] - Error(#[from] ServiceError), -} - -impl IntoResponse for QuestionsError { - fn into_response(self) -> Response { - let body = json!({ - "error": self.to_string() - }); - (StatusCode::BAD_REQUEST, Json(body)).into_response() - } -} - #[derive(Clone)] struct QuestionsState<'a> { questions: QuestionsClient<'a>, @@ -168,6 +148,11 @@ pub struct Question { /// Facade of agama_lib::questions::GenericQuestion /// For fields details see it. +/// Reason why it does not use directly GenericQuestion from lib +/// is that it contain both question and answer. It works for dbus +/// API which has both as attributes, but web API separate +/// question and its answer. So here it is split into GenericQuestion +/// and GenericAnswer #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct GenericQuestion { id: u32, @@ -180,6 +165,11 @@ pub struct GenericQuestion { /// Facade of agama_lib::questions::WithPassword /// For fields details see it. +/// Reason why it does not use directly WithPassword from lib +/// is that it is not composition as used here, but more like +/// child of generic question and contain reference to Base. +/// Here for web API we want to have in json that separation that would +/// allow to compose any possible future specialization of question #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct QuestionWithPassword { password: String, @@ -248,7 +238,7 @@ pub async fn questions_stream( ))] async fn list_questions( State(state): State>, -) -> Result>, QuestionsError> { +) -> Result>, Error> { Ok(Json(state.questions.questions().await?)) } @@ -265,7 +255,7 @@ async fn answer( State(state): State>, Path(question_id): Path, Json(answer): Json, -) -> Result<(), QuestionsError> { +) -> Result<(), Error> { state.questions.answer(question_id, answer).await?; Ok(()) } From 3646c0b23306662fbc199d244838b31e850a4097 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 22 Mar 2024 16:44:54 +0100 Subject: [PATCH 11/11] fix formatting --- rust/agama-server/src/questions/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 44d09b2ecb..9cb1ca2a26 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -22,7 +22,7 @@ use tokio_stream::{Stream, StreamExt}; use zbus::{ fdo::ObjectManagerProxy, names::{InterfaceName, OwnedInterfaceName}, - zvariant::{ObjectPath,OwnedObjectPath} + zvariant::{ObjectPath, OwnedObjectPath}, }; // TODO: move to lib