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
4 changes: 4 additions & 0 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ jobs:
# send the code coverage for the Rust part to the coveralls.io
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2
# ignore errors in this step
continue-on-error: true
with:
base-path: ./rust
format: cobertura
Expand All @@ -122,6 +124,8 @@ jobs:
# only with the "parallel-finished: true" option)
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
# ignore errors in this step
continue-on-error: true
with:
parallel-finished: true
carryforward: "service,web"
8 changes: 6 additions & 2 deletions rust/agama-lib/src/product/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ impl<'a> ProductClient<'a> {
.into_iter()
.map(|(id, version, code)| AddonParams {
id,
version,
version: if version.is_empty() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could be useful in this case. However, it is not included yet.

None
} else {
Some(version)
},
registration_code: if code.is_empty() { None } else { Some(code) },
})
.collect();
Expand All @@ -158,7 +162,7 @@ impl<'a> ProductClient<'a> {
.registration_proxy
.register_addon(
&addon.id,
&addon.version,
&addon.version.clone().unwrap_or_default(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would an empty version work?

Copy link
Contributor Author

@lslezak lslezak Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the end the backend receives empty version because nil cannot be sent over DBus. So omitting the version is the same as using empty string.

And if SCC really uses an empty version then it will work too, just using a slightly more complicated path in the code.

&addon.registration_code.clone().unwrap_or_default(),
)
.await?)
Expand Down
7 changes: 6 additions & 1 deletion rust/agama-lib/src/product/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "camelCase")]
pub struct AddonSettings {
pub id: String,
pub version: String,
/// Optional version of the addon, if not specified the version is found
/// from the available addons
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
/// Free extensions do not require a registration code
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_code: Option<String>,
}

Expand Down
4 changes: 2 additions & 2 deletions rust/agama-lib/src/software/model/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub struct RegistrationParams {
pub struct AddonParams {
// Addon identifier
pub id: String,
// Addon version, the same addon might be available in multiple versions
pub version: 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>,
}
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 @@
-------------------------------------------------------------------
Tue Apr 1 12:44:57 UTC 2025 - Ladislav Slezák <[email protected]>

- Make the extension version attribute optional, search the version
automatically if it is missing (related to jsc#AGM-100)

-------------------------------------------------------------------
Fri Mar 28 21:45:05 UTC 2025 - Josef Reidinger <[email protected]>

Expand Down
9 changes: 8 additions & 1 deletion service/lib/agama/autoyast/product_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,16 @@ def from_suse_register(section)
# convert addons according to the new schema
def convert_addons(addons)
addons.map do |a|
addon = { "id" => a["name"], "version" => a["version"] }
addon = { "id" => a["name"] }

version = a["version"].to_s
# omit the version if it was 11.x, 12.x or 15.x, the version is now optional
version = "" if version.match(/^1[125]\.\d$/)
addon["version"] = version unless version.empty?

code = a["reg_code"].to_s
addon["registrationCode"] = code unless code.empty?

addon
end
end
Expand Down
13 changes: 11 additions & 2 deletions service/lib/agama/dbus/software/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require "agama/dbus/base_object"
require "agama/dbus/interfaces/issues"
require "agama/dbus/clients/locale"
require "agama/errors"
require "agama/registration"

module Agama
Expand Down Expand Up @@ -143,7 +144,8 @@ def registered_addons
backend.registration.registered_addons.map do |addon|
[
addon.name,
addon.version,
# return empty string if the version was not explicitly specified (was autodetected)
addon.required_version ? addon.version : "",
addon.reg_code
]
end
Expand Down Expand Up @@ -199,7 +201,8 @@ def register(reg_code, email: nil)
# have to explicitly call to #Probe after registering a product.
#
# @param name [String] name (id) of the addon, e.g. "sle-ha"
# @param version [String] version of the addon, e.g. "16.0"
# @param version [String] version of the addon, e.g. "16.0", if empty the version is found
# automatically in the list of available addons
# @param reg_code [String] registration code, if the code is not required for the addon use
# an empty string ("")
#
Expand All @@ -216,6 +219,8 @@ def register(reg_code, email: nil)
# 8: incorrect credentials
# 9: invalid certificate
# 10: internal error (e.g., parsing json data)
# 11: addon not found
# 12: addon found in multiple versions
def register_addon(name, version, reg_code)
if !backend.product
[1, "Product not selected yet"]
Expand Down Expand Up @@ -335,6 +340,10 @@ def connect_result(first_error_code: 1, &block)
connect_result_from_error(e, first_error_code + 5, "invalid certificate")
rescue JSON::ParserError => e
connect_result_from_error(e, first_error_code + 6)
rescue Errors::Registration::ExtensionNotFound => e
connect_result_from_error(e, first_error_code + 7)
rescue Errors::Registration::MultipleExtensionsFound => e
connect_result_from_error(e, first_error_code + 8)
end

# Generates a result from a given error.
Expand Down
17 changes: 17 additions & 0 deletions service/lib/agama/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,22 @@ module Agama
module Errors
# Invalid value given by the user
class InvalidValue < StandardError; end

# Registration specific errors
module Registration
# The requested extension was not found
class ExtensionNotFound < StandardError
def initialize(name)
super("#{name.inspect} is not available")
end
end

# The requested extension exists in multiple versions
class MultipleExtensionsFound < StandardError
def initialize(name, versions)
super("#{name.inspect} is available in multiple versions: #{versions.join(", ")}")
end
end
end
end
end
8 changes: 7 additions & 1 deletion service/lib/agama/registered_addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ class RegisteredAddon
# @return [String]
attr_reader :version

# The addon version was explicitly specified by the user or it was autodetected.
#
# @return [Boolean] true if explicitly specified by user, false when autodetected
attr_reader :required_version

# Code used for registering the addon.
#
# @return [String] empty string if the registration code is not required
attr_reader :reg_code

def initialize(name, version, reg_code = "")
def initialize(name, version, required_version, reg_code = "")
@name = name
@version = version
@required_version = required_version
@reg_code = reg_code
end
end
Expand Down
69 changes: 58 additions & 11 deletions service/lib/agama/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
require "y2packager/resolvable"

require "agama/cmdline_args"
require "agama/errors"
require "agama/registered_addon"

Yast.import "Arch"
Expand Down Expand Up @@ -90,13 +91,8 @@ def register(code, email: "")
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(@login, @password, GLOBAL_CREDENTIALS_PATH)

target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: product.id,
version: product.version || "1.0"
)
activate_params = {}
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
service = SUSE::Connect::YaST.activate_product(base_target_product, activate_params, email)
process_service(service)

@reg_code = code
Expand All @@ -105,25 +101,33 @@ def register(code, email: "")
end

def register_addon(name, version, code)
if @registered_addons.any? { |a| a.name == name && a.version == version }
@logger.info "Addon #{name}-#{version} already registered, skipping registration"
register_version = if version.empty?
# version is not specified, find it automatically
find_addon_version(name)
else
# use the explicitly required version
version
end

if @registered_addons.any? { |a| a.name == name && a.version == register_version }
@logger.info "Addon #{name}-#{register_version} already registered, skipping registration"
return
end

@logger.info "Registering addon #{name}-#{version}"
@logger.info "Registering addon #{name}-#{register_version}"
# do not log the code, but at least log if it is empty
@logger.info "Using empty registration code" if code.empty?

target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: name,
version: version
version: register_version
)
activate_params = { token: code }
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, @email)
process_service(service)

@registered_addons << RegisteredAddon.new(name, version, code)
@registered_addons << RegisteredAddon.new(name, register_version, !version.empty?, code)
# select the products to install
@software.addon_products(find_addon_products)

Expand All @@ -150,6 +154,7 @@ def deregister
# reset
@software.addon_products([])
@services = []
@available_addons = nil

reg_params = connect_params(token: reg_code, email: email)
SUSE::Connect::YaST.deactivate_system(reg_params)
Expand Down Expand Up @@ -314,5 +319,47 @@ def repository_data(repo)
data["SrcId"] = repo
data
end

# Get the available addons for the specified base product.
#
# @note The result is bound to the registration code used for the base product, the result
# might be different for different codes. E.g. the Alpha/Beta extensions might or might not
# be included in the list.
def available_addons
return @available_addons if @available_addons

@available_addons = SUSE::Connect::YaST.show_product(base_target_product,
connect_params).extensions
@logger.info "Available addons: #{available_addons.inspect}"
@available_addons
end

# Find the version for the specified addon, if none if multiple addons with the same name
# are found an exception is thrown.
#
# @return [String] the addon version, e.g. "16.0"
def find_addon_version(name)
raise Errors::Registration::ExtensionNotFound, name unless available_addons

requested_addons = available_addons.select { |a| a.identifier == name }
case requested_addons.size
when 0
raise Errors::Registration::ExtensionNotFound, name
when 1
requested_addons.first.version
else
raise Errors::Registration::MultipleExtensionsFound.new(name,
requested_addons.map(&:version))
end
end

# Construct the base product data for sending to the server
def base_target_product
OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: product.id,
version: product.version || "1.0"
)
end
end
end
6 changes: 6 additions & 0 deletions service/package/rubygem-agama-yast.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Tue Apr 1 12:44:57 UTC 2025 - Ladislav Slezák <[email protected]>

- Make the extension version attribute optional, search the version
automatically if it is missing (related to jsc#AGM-100)

-------------------------------------------------------------------
Fri Mar 28 21:45:56 UTC 2025 - Josef Reidinger <[email protected]>

Expand Down
61 changes: 61 additions & 0 deletions service/test/agama/registration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,27 @@

let(:code) { "867136984314" }

let(:ha_extension) do
OpenStruct.new(
id: 2937,
identifier: "sle-ha",
version: "16.0",
arch: "x86_64",
isbase: false,
friendly_name: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)",
ProductLine: "",
available: true,
free: false,
recommended: false,
description: "SUSE Linux High Availability Extension provides...",
former_identifier: "sle-ha",
product_type: "extension",
shortname: "SLEHA16",
name: "SUSE Linux Enterprise High Availability Extension",
release_stage: "beta"
)
end

it "registers addon" do
expect(SUSE::Connect::YaST).to receive(:activate_product).with(
addon, { token: code }, anything
Expand All @@ -255,6 +276,46 @@
subject.register_addon(addon.identifier, addon.version, code)
subject.register_addon(addon.identifier, addon.version, code)
end

context "the requested addon version is not specified" do
it "finds the version automatically" do
expect(SUSE::Connect::YaST).to receive(:activate_product).with(
addon, { token: code }, anything
)

expect(SUSE::Connect::YaST).to receive(:show_product).and_return(
OpenStruct.new(
extensions: [ha_extension]
)
)

subject.register_addon(addon.identifier, "", code)
end

it "raises exception when the requested addon is not found" do
expect(SUSE::Connect::YaST).to receive(:show_product).and_return(
OpenStruct.new(extensions: [])
)

expect do
subject.register_addon(addon.identifier, "", code)
end.to raise_error(Agama::Errors::Registration::ExtensionNotFound)
end

it "raises exception when multiple addon versions are found" do
ha1 = ha_extension
ha2 = ha1.dup
ha2.version = "42"

expect(SUSE::Connect::YaST).to receive(:show_product).and_return(
OpenStruct.new(extensions: [ha1, ha2])
)

expect do
subject.register_addon(addon.identifier, "", code)
end.to raise_error(Agama::Errors::Registration::MultipleExtensionsFound)
end
end
end
end

Expand Down
Loading