Skip to content

Commit

Permalink
feat: implement product registration support (#1809)
Browse files Browse the repository at this point in the history
## Solution

This pull request implements support for product registration. It
includes changes at several levels:

- Adapt the Ruby part to handle the registration using `/run/agama/zypp`
as target directory.
- Add the information about whether a product should be registered or
not in the product (and not through a separate D-Bus attribute).
Propagate those changes to the HTTP API too.
- Implement a registration page to the web user-interface.

## Testing

You can use the web UI which sends the proper request to the backend or,
if you prefer, you can play around with cURL:

```
TOKEN=$(curl -k --silent $AGAMA_URL/api/auth -d '{"password": "linux"}' \
	-H "Content-Type: application/json" | jq .token | tr -d '"')

echo "Content-Type: application/json" >headers.txt
echo -n "Authorization: Bearer " >>headers.txt
echo $TOKEN >>headers.txt

curl -k -H @headers.txt -X PUT \
	$AGAMA_URL/api/software/config \
	-d "{\"product\": \"SLES\"}"

curl -k -H @headers.txt -X POST \
	$AGAMA_URL/api/software/registration \
	-d "{\"key\": \"YOUR-REGISTRATION-CODE\", \"email\": \"[email protected]\"}"
```

Set `$AGAMA_URL` to the Agama HTTP API (e.g., `https://192.168.122.10`).

## Screenshots


![reg2](https://github.com/user-attachments/assets/c311a69a-38af-49fd-a0eb-4734bba3bb91)

![reg1](https://github.com/user-attachments/assets/41ee1f07-70b8-4a9d-bdb8-ce78b5119bc6)
  • Loading branch information
jreidinger authored Jan 9, 2025
2 parents 496dae3 + 5014eff commit d4771bd
Show file tree
Hide file tree
Showing 59 changed files with 1,156 additions and 232 deletions.
7 changes: 6 additions & 1 deletion products.d/agama-products.changes
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
-------------------------------------------------------------------
Wed Jan 8 14:07:21 UTC 2025 - Imobach Gonzalez Sosa <[email protected]>

- Add support for products registration (jsc#PED-11192,
gh#agama-project/agama#1809).

-------------------------------------------------------------------
Tue Jan 7 12:57:13 UTC 2025 - Lubos Kocman <[email protected]>

- Drop yast from Leap 16.0 software selection
code-o-o#leap/features#173


-------------------------------------------------------------------
Mon Jan 6 14:41:28 UTC 2025 - Angela Briel <[email protected]>

Expand Down
17 changes: 4 additions & 13 deletions products.d/sles_160.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
id: SLES_16.0
id: SLES
name: SUSE Linux Enterprise Server 16.0 Alpha
registration: "mandatory"
version: "16-0"
# ------------------------------------------------------------------------------
# WARNING: When changing the product description delete the translations located
# at the at translations/description key below to avoid using obsolete
Expand Down Expand Up @@ -50,18 +52,7 @@ translations:
altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde,
bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır.
software:
installation_repositories:
# Use plain HTTP repositories, HTTPS does not work without importing the SSL
# certificate. It is safe as the repository is GPG checked and you neeed VPN
# to reach the internal server anyway.
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-x86_64/
archs: x86_64
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-aarch64/
archs: aarch64
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-ppc64le/
archs: ppc
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-s390x/
archs: s390
installation_repositories: []

mandatory_patterns:
- base_traditional
Expand Down
18 changes: 5 additions & 13 deletions products.d/sles_sap_160.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
id: SLES_SAP_16.0
id: SLES-SAP
name: SUSE Linux Enterprise Server for SAP Applications 16.0 Beta
archs: x86_64,aarch64,ppc
registration: "mandatory"
version: "16-0"
# ------------------------------------------------------------------------------
# WARNING: When changing the product description delete the translations located
# at the at translations/description key below to avoid using obsolete
Expand All @@ -14,18 +17,7 @@ icon: SUSE.svg
translations:
description:
software:
installation_repositories:
# Use plain HTTP repositories, HTTPS does not work without importing the SSL
# certificate. It is safe as the repository is GPG checked and you neeed VPN
# to reach the internal server anyway.
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-x86_64/
archs: x86_64
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-aarch64/
archs: aarch64
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-ppc64le/
archs: ppc
- url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-s390x/
archs: s390
installation_repositories: []

mandatory_patterns:
- base_traditional
Expand Down
17 changes: 10 additions & 7 deletions rust/agama-lib/src/product/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
// find current contact information at www.suse.com.

use std::collections::HashMap;
use std::str::FromStr;

use crate::dbus::get_property;
use crate::error::ServiceError;
use crate::software::model::RegistrationRequirement;
use crate::software::proxies::SoftwareProductProxy;
Expand All @@ -39,6 +41,8 @@ pub struct Product {
pub description: String,
/// Product icon (e.g., "default.svg")
pub icon: String,
/// Registration requirement
pub registration: RegistrationRequirement,
}

/// D-Bus client for the software service
Expand Down Expand Up @@ -72,11 +76,17 @@ impl<'a> ProductClient<'a> {
Some(value) => value.try_into().unwrap(),
None => "default.svg",
};

let registration = get_property::<String>(&data, "registration")
.map(|r| RegistrationRequirement::from_str(&r).unwrap_or_default())
.unwrap_or_default();

Product {
id,
name,
description: description.to_string(),
icon: icon.to_string(),
registration,
}
})
.collect();
Expand Down Expand Up @@ -114,13 +124,6 @@ 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 Down
21 changes: 19 additions & 2 deletions rust/agama-lib/src/product/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.

use crate::software::model::RegistrationError;
use crate::software::model::RegistrationInfo;
use crate::software::model::RegistrationParams;
use crate::software::model::SoftwareConfig;
Expand Down Expand Up @@ -64,13 +65,29 @@ impl ProductHTTPClient {
}

/// register product
pub async fn register(&self, key: &str, email: &str) -> Result<(u32, String), ServiceError> {
pub async fn register(&self, key: &str, email: &str) -> Result<(), ServiceError> {
// note RegistrationParams != RegistrationInfo, fun!
let params = RegistrationParams {
key: key.to_owned(),
email: email.to_owned(),
};
let result = self
.client
.post_void("/software/registration", &params)
.await;

self.client.post("/software/registration", &params).await
let Err(error) = result else {
return Ok(());
};

let message = match error {
ServiceError::BackendError(_, details) => {
let details: RegistrationError = serde_json::from_str(&details).unwrap();
format!("{} (error code: {})", details.message, details.id)
}
_ => format!("Could not register the product: #{error:?}"),
};

Err(ServiceError::FailedRegistration(message))
}
}
13 changes: 2 additions & 11 deletions rust/agama-lib/src/product/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,8 @@ impl ProductStore {
}
}
if let Some(reg_code) = &settings.registration_code {
let (result, message);
if let Some(email) = &settings.registration_email {
(result, message) = self.product_client.register(reg_code, email).await?;
} else {
(result, message) = self.product_client.register(reg_code, "").await?;
}
// FIXME: name the magic numbers. 3 is Registration not required
// FIXME: well don't register when not required (no regcode in profile)
if result != 0 && result != 3 {
return Err(ServiceError::FailedRegistration(message));
}
let email = settings.registration_email.as_deref().unwrap_or("");
self.product_client.register(reg_code, email).await?;
probe = true;
}

Expand Down
42 changes: 20 additions & 22 deletions rust/agama-lib/src/software/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,38 +47,36 @@ pub struct RegistrationInfo {
pub key: String,
/// Registration email. Empty value mean email not used or not registered.
pub email: String,
/// if registration is required, optional or not needed for current product.
/// Change only if selected product is changed.
pub requirement: RegistrationRequirement,
}

#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[derive(
Clone,
Default,
Debug,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
utoipa::ToSchema,
)]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub enum RegistrationRequirement {
/// Product does not require registration
NotRequired = 0,
#[default]
No = 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(()),
}
}
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RegistrationError {
/// ID of error. See dbus API for possible values
pub id: u32,
/// human readable error string intended to be displayed to user
pub message: String,
}

/// Software resolvable type (package or pattern).
Expand Down
49 changes: 9 additions & 40 deletions rust/agama-server/src/software/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ use agama_lib::{
error::ServiceError,
product::{proxies::RegistrationProxy, Product, ProductClient},
software::{
model::{RegistrationInfo, RegistrationParams, ResolvableParams, SoftwareConfig},
model::{
RegistrationError, RegistrationInfo, RegistrationParams, ResolvableParams,
SoftwareConfig,
},
proxies::{Software1Proxy, SoftwareProductProxy},
Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy,
},
Expand All @@ -49,7 +52,7 @@ use axum::{
routing::{get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use serde::Serialize;
use std::collections::HashMap;
use tokio_stream::{Stream, StreamExt};

Expand All @@ -74,10 +77,6 @@ pub async fn software_streams(dbus: zbus::Connection) -> Result<EventStreams, Er
"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?),
Expand Down Expand Up @@ -131,27 +130,6 @@ 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> {
Expand Down Expand Up @@ -269,19 +247,10 @@ async fn get_registration(
let result = RegistrationInfo {
key: state.product.registration_code().await?,
email: state.product.email().await?,
requirement: state.product.registration_requirement().await?,
};
Ok(Json(result))
}

#[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.
Expand All @@ -291,7 +260,7 @@ pub struct FailureDetails {
context_path = "/api/software",
responses(
(status = 204, description = "registration successfull"),
(status = 422, description = "Registration failed. Details are in body", body = FailureDetails),
(status = 422, description = "Registration failed. Details are in body", body = RegistrationError),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
Expand All @@ -300,10 +269,10 @@ async fn register(
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 {
let details = RegistrationError { id, message };
Ok((
StatusCode::UNPROCESSABLE_ENTITY,
Json(details).into_response(),
Expand All @@ -320,13 +289,13 @@ async fn register(
context_path = "/api/software",
responses(
(status = 200, description = "deregistration successfull"),
(status = 422, description = "De-registration failed. Details are in body", body = FailureDetails),
(status = 422, description = "De-registration failed. Details are in body", body = RegistrationError),
(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 };
let details = RegistrationError { id, message };
if id == 0 {
Ok((StatusCode::NO_CONTENT, ().into_response()))
} else {
Expand Down
6 changes: 6 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Wed Jan 8 14:05:34 UTC 2025 - Imobach Gonzalez Sosa <[email protected]>

- Add support for products registration (jsc#PED-11192,
gh#agama-project/agama#1809).

-------------------------------------------------------------------
Fri Dec 20 12:17:26 UTC 2024 - Josef Reidinger <[email protected]>

Expand Down
7 changes: 4 additions & 3 deletions service/lib/agama/dbus/software/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ def available_products
product.id,
product.display_name,
{
"description" => product.localized_description,
"icon" => product.icon
"description" => product.localized_description,
"icon" => product.icon,
"registration" => product.registration
}
]
end
Expand Down Expand Up @@ -177,7 +178,7 @@ def register(reg_code, email: nil)
[1, "Product not selected yet"]
elsif backend.registration.reg_code
[2, "Product already registered"]
elsif backend.registration.requirement == Agama::Registration::Requirement::NOT_REQUIRED
elsif backend.registration.requirement == Agama::Registration::Requirement::NO
[3, "Product does not require registration"]
else
connect_result(first_error_code: 4) do
Expand Down
Loading

0 comments on commit d4771bd

Please sign in to comment.