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
2 changes: 2 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/agama-software/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tokio-stream = "0.1.16"
tracing = "0.1.41"
url = "2.5.7"
utoipa = { version = "5.2.0", features = ["axum_extras", "uuid"] }
suseconnect-agama = { path = "../suseconnect-agama" }
zypp-agama = { path = "../zypp-agama" }

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-software/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod service;
pub use service::Service;

mod model;
pub use model::{state, Model, ModelAdapter, Resolvable, ResolvableType};
pub use model::{state, Model, ModelAdapter, Registration, Resolvable, ResolvableType};

mod callbacks;
pub mod message;
Expand Down
1 change: 1 addition & 0 deletions rust/agama-software/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod software_selection;
pub mod state;

pub use packages::{Resolvable, ResolvableType};
pub use registration::{Registration, RegistrationBuilder};

/// Abstract the software-related configuration from the underlying system.
///
Expand Down
277 changes: 240 additions & 37 deletions rust/agama-software/src/model/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,250 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.

use serde::{Deserialize, Serialize};

/// Software service configuration (product, patterns, etc.).
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RegistrationParams {
/// Registration key.
pub key: String,
/// Registration email.
pub email: String,
//! This module implements support for registering a system.
//!
//! It interacts with SUSEConnect-ng (using the [suseconnect_agama] crate) to register
//! the system and its add-ons and with libzypp (through [zypp_agama]) to add the
//! corresponding services to `libzypp`.

use agama_utils::{
api::software::{AddonInfo, RegistrationInfo},
arch::Arch,
};
use camino::Utf8PathBuf;
use suseconnect_agama::{self, ConnectParams, Credentials};
use url::Url;

#[derive(thiserror::Error, Debug)]
pub enum RegistrationError {
#[error(transparent)]
Registration(#[from] suseconnect_agama::Error),
#[error("Failed to add the service {0}: {1}")]
AddService(String, #[source] zypp_agama::ZyppError),
}

/// Addon registration
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AddonParams {
// Addon identifier
pub id: String,
// Addon version, if not specified the version is found from the available addons
pub version: Option<String>,
// Optional registration code, not required for free extensions
pub registration_code: Option<String>,
type RegistrationResult<T> = Result<T, RegistrationError>;

/// Represents a registered system.
///
/// It is used to activate products and add the corresponding services.
/// It is created from a [RegistrationBuilder].
#[derive(Debug)]
pub struct Registration {
root_dir: Utf8PathBuf,
product: String,
version: String,
// The connection parameters are kept because they are needed by the
// `to_registration_info` function.
connect_params: ConnectParams,
creds: Credentials,
services: Vec<suseconnect_agama::Service>,
}

/// Information about registration configuration (product, patterns, etc.).
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationInfo {
/// Registration status. True if base system is already registered.
pub registered: bool,
/// Registration key. Empty value mean key not used or not registered.
pub key: String,
/// Registration email. Empty value mean email not used or not registered.
pub email: String,
/// Registration URL. Empty value mean that de default value is used.
pub url: String,
impl Registration {
pub fn builder(root_dir: Utf8PathBuf, product: &str, version: &str) -> RegistrationBuilder {
RegistrationBuilder::new(root_dir, product, version)
}

// This activate_product should receive the code
pub fn activate_product(
&mut self,
zypp: &zypp_agama::Zypp,
name: &str,
version: &str,
code: Option<&str>,
) -> RegistrationResult<()> {
let product = Self::product_specification(name, version);
let mut params = self.connect_params.clone();
params.token = code.map(ToString::to_string);

tracing::debug!("Registering product {product:?}");
let service = suseconnect_agama::activate_product(
product,
params,
self.connect_params
.email
.as_ref()
.map(|e| e.as_str())
.unwrap_or(""),
)?;

if let Some(file) = Self::credentials_from_url(&service.url) {
let path = self
.root_dir
.join(format!("etc/zypp/credentials.d/{}", file));
tracing::debug!(
"Creating the credentials file for {} at {}",
&service.name,
&path
);
suseconnect_agama::create_credentials_file(
&self.creds.login,
&self.creds.password,
path.as_str(),
)?;
}

// Add the libzypp service
zypp.add_service(&service.name, &service.url)
.map_err(|e| RegistrationError::AddService(service.name.clone(), e))?;
self.services.push(service);
Ok(())
}

/// Returns the registration information.
///
/// It includes not only the basic data (like the registration code or the e-mail),
/// but the list of extensions.
pub fn to_registration_info(&self) -> RegistrationInfo {
let addons: Vec<AddonInfo> = match self.base_product() {
Ok(product) => product
.extensions
.into_iter()
.map(|e| AddonInfo {
id: e.identifier,
version: e.version,
label: e.friendly_name,
available: e.available,
free: e.free,
recommended: e.recommended,
description: e.description,
release: e.release_stage,
})
.collect(),
Err(error) => {
tracing::error!("Failed to get the product from the registration server: {error}");
vec![]
}
};

RegistrationInfo {
code: self.connect_params.token.clone(),
email: self.connect_params.email.clone(),
url: self.connect_params.url.clone(),
addons,
}
}

fn base_product(&self) -> RegistrationResult<suseconnect_agama::Product> {
let product = suseconnect_agama::show_product(
self.base_product_specification(),
self.connect_params.clone(),
)?;
Ok(product)
}

fn base_product_specification(&self) -> suseconnect_agama::ProductSpecification {
Self::product_specification(&self.product, &self.version)
}

fn product_specification(id: &str, version: &str) -> suseconnect_agama::ProductSpecification {
// We do not expect this to happen.
let arch = Arch::current().expect("Failed to determine the architecture");
suseconnect_agama::ProductSpecification {
identifier: id.to_string(),
arch: arch.to_string(),
version: version.to_string(),
}
}

fn credentials_from_url(url: &str) -> Option<String> {
let url = Url::parse(url)
.inspect_err(|e| tracing::warn!("Could not parse the service URL: {e}"))
.ok()?;
url.query_pairs()
.find(|(k, _v)| k == "credentials")
.map(|(_k, v)| v.to_string())
}
}

#[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,
/// A builder for a [Registration] object.
///
/// It is used to configure the build a registration object. It allows to configure
/// the registration parameters like the product and version, the registration code,
/// the e-mail, etc.
/// [Registration] object.
#[derive(Debug)]
pub struct RegistrationBuilder {
root_dir: Utf8PathBuf,
product: String,
version: String,
code: Option<String>,
email: Option<String>,
}

impl RegistrationBuilder {
/// Creates a new builder.
///
/// It receives the mandatory arguments for registering a system.
///
/// * `root_dir`: root directory where libzypp configuration lives.
/// * `product`: product name (e.g., "SLES").
/// * `version`: product version (e.g., "16.1").
pub fn new(root_dir: Utf8PathBuf, product: &str, version: &str) -> Self {
RegistrationBuilder {
root_dir,
product: product.to_string(),
version: version.to_string(),
code: None,
email: None,
}
}

/// Sets the registration code to use.
///
/// * `code`: registration code.
pub fn with_code(mut self, code: &str) -> Self {
self.code = Some(code.to_string());
self
}

/// Sets the e-mail associated to the registration.
pub fn with_email(mut self, email: &str) -> Self {
self.email = Some(email.to_string());
self
}

/// Registers the system and return a [Registration] object.
///
/// It announces the system, gets the credentials and registers the base product.
///
/// * `zypp`: zypp instance.
pub fn register(self, zypp: &zypp_agama::Zypp) -> RegistrationResult<Registration> {
let params = suseconnect_agama::ConnectParams {
token: self.code.clone(),
email: self.email.clone(),
language: "en-us".to_string().into(),
// unwrap: it is guaranteed to be a correct URL.
url: Some(Url::parse(suseconnect_agama::DEFAULT_SCC_URL).unwrap()),
..Default::default()
};
// https://github.com/agama-project/agama/blob/master/service/lib/agama/registration.rb#L294
let version = self.version.split(".").next().unwrap_or("1");
let target_distro = format!("{}-{}-{}", &self.product, version, std::env::consts::ARCH);
tracing::debug!("Announcing system {target_distro}");
let creds = suseconnect_agama::announce_system(params.clone(), &target_distro)?;

tracing::debug!(
"Creating the base credentials file at {}",
suseconnect_agama::GLOBAL_CREDENTIALS_FILE
);
suseconnect_agama::create_credentials_file(
&creds.login,
&creds.password,
suseconnect_agama::GLOBAL_CREDENTIALS_FILE,
)?;

let mut registration = Registration {
root_dir: self.root_dir,
connect_params: params,
product: self.product.clone(),
version: self.version.clone(),
creds,
services: vec![],
};

registration.activate_product(zypp, &self.product, &self.version, None)?;
Ok(registration)
}
}
Loading
Loading