Skip to content

Commit

Permalink
Adapt product registration to the new architecture (#1146)
Browse files Browse the repository at this point in the history
Another change in set that switch web UI from using dbus to HTTP API. As
part of this change the rust code is added to provide that HTTP API and
also adapted web UI. It also fixes all relevant js tests.

Things discussed but postponed (due to size of change and time
constraints ) in this change was:

1. move registration requirement from registration to product as it is
highly coupled with it
2. provide all changed data in websocket signal, so client does not need
to do another http call to get correct data

Matching trello card: https://trello.com/c/2bTZOnpB
  • Loading branch information
jreidinger committed Apr 19, 2024
2 parents 12587e1 + fcb2222 commit b43af77
Show file tree
Hide file tree
Showing 17 changed files with 445 additions and 264 deletions.
4 changes: 2 additions & 2 deletions rust/agama-lib/src/product.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Implements support for handling the product settings

mod client;
mod proxies;
pub mod proxies;
mod settings;
mod store;

pub use client::{Product, ProductClient};
pub use client::{Product, ProductClient, RegistrationRequirement};
pub use settings::ProductSettings;
pub use store::ProductStore;
43 changes: 42 additions & 1 deletion rust/agama-lib/src/product/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use crate::error::ServiceError;
use crate::software::proxies::SoftwareProductProxy;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use zbus::Connection;

use super::proxies::RegistrationProxy;
Expand All @@ -18,6 +18,35 @@ pub struct Product {
pub description: String,
}

#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub enum RegistrationRequirement {
/// Product does not require registration
NotRequired = 0,
/// Product has optional registration
Optional = 1,
/// It is mandatory to register the product
Mandatory = 2,
}

impl TryFrom<u32> for RegistrationRequirement {
type Error = ();

fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == RegistrationRequirement::NotRequired as u32 => {
Ok(RegistrationRequirement::NotRequired)
}
x if x == RegistrationRequirement::Optional as u32 => {
Ok(RegistrationRequirement::Optional)
}
x if x == RegistrationRequirement::Mandatory as u32 => {
Ok(RegistrationRequirement::Mandatory)
}
_ => Err(()),
}
}
}

/// D-Bus client for the software service
#[derive(Clone)]
pub struct ProductClient<'a> {
Expand Down Expand Up @@ -86,6 +115,13 @@ impl<'a> ProductClient<'a> {
Ok(self.registration_proxy.email().await?)
}

pub async fn registration_requirement(&self) -> Result<RegistrationRequirement, ServiceError> {
let requirement = self.registration_proxy.requirement().await?;
// unknown number can happen only if we do programmer mistake
let result: RegistrationRequirement = requirement.try_into().unwrap();
Ok(result)
}

/// register product
pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> {
let mut options: HashMap<&str, zbus::zvariant::Value> = HashMap::new();
Expand All @@ -94,4 +130,9 @@ impl<'a> ProductClient<'a> {
}
Ok(self.registration_proxy.register(code, options).await?)
}

/// de-register product
pub async fn deregister(&self) -> Result<(u32, String), ServiceError> {
Ok(self.registration_proxy.deregister().await?)
}
}
2 changes: 1 addition & 1 deletion rust/agama-server/src/software.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pub mod web;
pub use web::{software_service, software_stream};
pub use web::{software_service, software_streams};
193 changes: 182 additions & 11 deletions rust/agama-server/src/software/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,27 @@
use crate::{
error::Error,
web::{
common::{issues_router, progress_router, service_status_router},
common::{issues_router, progress_router, service_status_router, EventStreams},
Event,
},
};
use agama_lib::{
error::ServiceError,
product::{Product, ProductClient},
product::{proxies::RegistrationProxy, Product, ProductClient, RegistrationRequirement},
software::{
proxies::{Software1Proxy, SoftwareProductProxy},
Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy,
},
};
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin};
use std::collections::HashMap;
use tokio_stream::{Stream, StreamExt};

#[derive(Clone)]
Expand All @@ -49,14 +51,31 @@ pub struct SoftwareConfig {
/// It emits the Event::ProductChanged and Event::PatternsChanged events.
///
/// * `connection`: D-Bus connection to listen for events.
pub async fn software_stream(
dbus: zbus::Connection,
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Error> {
let stream = StreamExt::merge(
product_changed_stream(dbus.clone()).await?,
patterns_changed_stream(dbus.clone()).await?,
);
Ok(Box::pin(stream))
pub async fn software_streams(dbus: zbus::Connection) -> Result<EventStreams, Error> {
let result: EventStreams = vec![
(
"patterns_changed",
Box::pin(patterns_changed_stream(dbus.clone()).await?),
),
(
"product_changed",
Box::pin(product_changed_stream(dbus.clone()).await?),
),
(
"registration_requirement_changed",
Box::pin(registration_requirement_changed_stream(dbus.clone()).await?),
),
(
"registration_code_changed",
Box::pin(registration_code_changed_stream(dbus.clone()).await?),
),
(
"registration_email_changed",
Box::pin(registration_email_changed_stream(dbus.clone()).await?),
),
];

Ok(result)
}

async fn product_changed_stream(
Expand Down Expand Up @@ -99,6 +118,62 @@ async fn patterns_changed_stream(
Ok(stream)
}

async fn registration_requirement_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event>, Error> {
// TODO: move registration requirement to product in dbus and so just one event will be needed.
let proxy = RegistrationProxy::new(&dbus).await?;
let stream = proxy
.receive_requirement_changed()
.await
.then(|change| async move {
if let Ok(id) = change.get().await {
// unwrap is safe as possible numbers is send by our controlled dbus
return Some(Event::RegistrationRequirementChanged {
requirement: id.try_into().unwrap(),
});
}
None
})
.filter_map(|e| e);
Ok(stream)
}

async fn registration_email_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event>, Error> {
let proxy = RegistrationProxy::new(&dbus).await?;
let stream = proxy
.receive_email_changed()
.await
.then(|change| async move {
if let Ok(_id) = change.get().await {
// TODO: add to stream also proxy and return whole cached registration info
return Some(Event::RegistrationChanged);
}
None
})
.filter_map(|e| e);
Ok(stream)
}

async fn registration_code_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event>, Error> {
let proxy = RegistrationProxy::new(&dbus).await?;
let stream = proxy
.receive_reg_code_changed()
.await
.then(|change| async move {
if let Ok(_id) = change.get().await {
return Some(Event::RegistrationChanged);
}
None
})
.filter_map(|e| e);
Ok(stream)
}

// Returns a hash replacing the selection "reason" from D-Bus with a SelectedBy variant.
fn reason_to_selected_by(
patterns: HashMap<String, u8>,
Expand Down Expand Up @@ -130,6 +205,10 @@ pub async fn software_service(dbus: zbus::Connection) -> Result<Router, ServiceE
let router = Router::new()
.route("/patterns", get(patterns))
.route("/products", get(products))
.route(
"/registration",
get(get_registration).post(register).delete(deregister),
)
.route("/proposal", get(proposal))
.route("/config", put(set_config).get(get_config))
.route("/probe", post(probe))
Expand All @@ -153,6 +232,98 @@ async fn products(State(state): State<SoftwareState<'_>>) -> Result<Json<Vec<Pro
Ok(Json(products))
}

/// Information about registration configuration (product, patterns, etc.).
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationInfo {
/// Registration key. Empty value mean key not used or not registered.
key: String,
/// Registration email. Empty value mean email not used or not registered.
email: String,
/// if registration is required, optional or not needed for current product.
/// Change only if selected product is changed.
requirement: RegistrationRequirement,
}

/// returns registration info
///
/// * `state`: service state.
#[utoipa::path(get, path = "/software/registration", responses(
(status = 200, description = "registration configuration", body = RegistrationInfo),
(status = 400, description = "The D-Bus service could not perform the action")
))]
async fn get_registration(
State(state): State<SoftwareState<'_>>,
) -> Result<Json<RegistrationInfo>, Error> {
let result = RegistrationInfo {
key: state.product.registration_code().await?,
email: state.product.email().await?,
requirement: state.product.registration_requirement().await?,
};
Ok(Json(result))
}

/// Software service configuration (product, patterns, etc.).
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RegistrationParams {
/// Registration key.
key: String,
/// Registration email.
email: String,
}

#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct FailureDetails {
/// ID of error. See dbus API for possible values
id: u32,
/// human readable error string intended to be displayed to user
message: String,
}
/// Register product
///
/// * `state`: service state.
#[utoipa::path(post, path = "/software/registration", responses(
(status = 204, description = "registration successfull"),
(status = 422, description = "Registration failed. Details are in body", body=FailureDetails),
(status = 400, description = "The D-Bus service could not perform the action")
))]
async fn register(
State(state): State<SoftwareState<'_>>,
Json(config): Json<RegistrationParams>,
) -> Result<impl IntoResponse, Error> {
let (id, message) = state.product.register(&config.key, &config.email).await?;
let details = FailureDetails { id, message };
if id == 0 {
Ok((StatusCode::NO_CONTENT, ().into_response()))
} else {
Ok((
StatusCode::UNPROCESSABLE_ENTITY,
Json(details).into_response(),
))
}
}

/// Deregister product
///
/// * `state`: service state.
#[utoipa::path(delete, path = "/software/registration", responses(
(status = 200, description = "deregistration successfull"),
(status = 422, description = "De-registration failed. Details are in body", body=FailureDetails),
(status = 400, description = "The D-Bus service could not perform the action")
))]
async fn deregister(State(state): State<SoftwareState<'_>>) -> Result<impl IntoResponse, Error> {
let (id, message) = state.product.deregister().await?;
let details = FailureDetails { id, message };
if id == 0 {
Ok((StatusCode::NO_CONTENT, ().into_response()))
} else {
Ok((
StatusCode::UNPROCESSABLE_ENTITY,
Json(details).into_response(),
))
}
}

/// Returns the list of software patterns.
///
/// * `state`: service state.
Expand Down
9 changes: 3 additions & 6 deletions rust/agama-server/src/users/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use crate::{
error::Error,
web::{
common::{service_status_router, validation_router},
common::{service_status_router, validation_router, EventStreams},
Event,
},
};
Expand All @@ -17,7 +17,6 @@ use agama_lib::{
};
use axum::{extract::State, routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use tokio_stream::{Stream, StreamExt};

#[derive(Clone)]
Expand All @@ -30,16 +29,14 @@ struct UsersState<'a> {
/// It emits the Event::RootPasswordChange, Event::RootSSHKeyChanged and Event::FirstUserChanged events.
///
/// * `connection`: D-Bus connection to listen for events.
pub async fn users_streams(
dbus: zbus::Connection,
) -> Result<Vec<(&'static str, Pin<Box<dyn Stream<Item = Event> + Send>>)>, Error> {
pub async fn users_streams(dbus: zbus::Connection) -> Result<EventStreams, Error> {
const FIRST_USER_ID: &str = "first_user";
const ROOT_PASSWORD_ID: &str = "root_password";
const ROOT_SSHKEY_ID: &str = "root_sshkey";
// here we have three streams, but only two events. Reason is
// that we have three streams from dbus about property change
// and unify two root user properties into single event to http API
let result: Vec<(&str, Pin<Box<dyn Stream<Item = Event> + Send>>)> = vec![
let result: EventStreams = vec![
(
FIRST_USER_ID,
Box::pin(first_user_changed_stream(dbus.clone()).await?),
Expand Down
6 changes: 4 additions & 2 deletions rust/agama-server/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
manager::web::{manager_service, manager_stream},
network::{web::network_service, NetworkManagerAdapter},
questions::web::{questions_service, questions_stream},
software::web::{software_service, software_stream},
software::web::{software_service, software_streams},
users::web::{users_service, users_streams},
web::common::{issues_stream, progress_stream, service_status_stream},
};
Expand Down Expand Up @@ -110,7 +110,9 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res
for (id, user_stream) in users_streams(dbus.clone()).await? {
stream.insert(id, user_stream);
}
stream.insert("software", software_stream(dbus.clone()).await?);
for (id, software_stream) in software_streams(dbus.clone()).await? {
stream.insert(id, software_stream);
}
stream.insert(
"software-status",
service_status_stream(
Expand Down
2 changes: 2 additions & 0 deletions rust/agama-server/src/web/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use crate::error::Error;

use super::Event;

pub type EventStreams = Vec<(&'static str, Pin<Box<dyn Stream<Item = Event> + Send>>)>;

/// Builds a router to the `org.opensuse.Agama1.ServiceStatus` interface of the
/// given D-Bus object.
///
Expand Down

0 comments on commit b43af77

Please sign in to comment.