From 6c6cba26411cc1c2b276d74c8360f948259e6816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 19 Oct 2023 15:28:56 +0100 Subject: [PATCH] [service] Export a separate product object --- service/lib/agama/dbus/clients/software.rb | 12 +- service/lib/agama/dbus/clients/with_issues.rb | 24 +- service/lib/agama/dbus/software.rb | 3 +- service/lib/agama/dbus/software/manager.rb | 201 +----------- service/lib/agama/dbus/software/product.rb | 285 ++++++++++++++++++ service/lib/agama/dbus/software_service.rb | 1 + service/lib/agama/software/manager.rb | 46 ++- 7 files changed, 346 insertions(+), 226 deletions(-) create mode 100644 service/lib/agama/dbus/software/product.rb diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb index 5d9321698a..8f94786773 100644 --- a/service/lib/agama/dbus/clients/software.rb +++ b/service/lib/agama/dbus/clients/software.rb @@ -42,6 +42,9 @@ def initialize @dbus_object = service["/org/opensuse/Agama/Software1"] @dbus_object.introspect + @dbus_product = service["/org/opensuse/Agama/Software1/Product"] + @dbus_product.introspect + @dbus_proposal = service["/org/opensuse/Agama/Software1/Proposal"] @dbus_proposal.introspect end @@ -55,7 +58,7 @@ def service_name # # @return [Array>] name and display name of each product def available_products - dbus_object["org.opensuse.Agama.Software1"]["AvailableProducts"].map do |l| + dbus_product["org.opensuse.Agama.Software1.Product"]["AvailableProducts"].map do |l| l[0..1] end end @@ -64,7 +67,7 @@ def available_products # # @return [String, nil] name of the product def selected_product - product = dbus_object["org.opensuse.Agama.Software1"]["SelectedProduct"] + product = dbus_product["org.opensuse.Agama.Software1.Product"]["SelectedProduct"] return nil if product.empty? product @@ -74,7 +77,7 @@ def selected_product # # @param name [String] def select_product(name) - dbus_object.SelectProduct(name) + dbus_product.SelectProduct(name) end # Starts the probing process @@ -180,6 +183,9 @@ def on_product_selected(&block) # @return [::DBus::Object] attr_reader :dbus_object + # @return [::DBus::Object] + attr_reader :dbus_product + # @return [::DBus::Object] attr_reader :dbus_proposal end diff --git a/service/lib/agama/dbus/clients/with_issues.rb b/service/lib/agama/dbus/clients/with_issues.rb index a4a1dc363e..cc7ba3ea4e 100644 --- a/service/lib/agama/dbus/clients/with_issues.rb +++ b/service/lib/agama/dbus/clients/with_issues.rb @@ -28,10 +28,25 @@ module WithIssues ISSUES_IFACE = "org.opensuse.Agama1.Issues" private_constant :ISSUES_IFACE - # Returns the issues + # Returns issues from all objects that implement the issues interface. # # @return [Array] def issues + objects_with_issues_interface.map { |o| issues_from(o) }.flatten + end + + # Determines whether there are errors + # + # @return [Boolean] + def errors? + issues.any?(&:error?) + end + + def objects_with_issues_interface + service.root.descendant_objects.select { |o| o.interfaces.include?(ISSUES_IFACE) } + end + + def issues_from(dbus_object) sources = [nil, Issue::Source::SYSTEM, Issue::Source::CONFIG] severities = [Issue::Severity::WARN, Issue::Severity::ERROR] @@ -42,13 +57,6 @@ def issues severity: severities[dbus_issue[3]]) end end - - # Determines whether there are errors - # - # @return [Boolean] - def errors? - issues.any?(&:error?) - end end end end diff --git a/service/lib/agama/dbus/software.rb b/service/lib/agama/dbus/software.rb index 670cc4517e..ace66f64fc 100644 --- a/service/lib/agama/dbus/software.rb +++ b/service/lib/agama/dbus/software.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022] SUSE LLC +# Copyright (c) [2022-2023] SUSE LLC # # All Rights Reserved. # @@ -28,4 +28,5 @@ module Software end require "agama/dbus/software/manager" +require "agama/dbus/software/product" require "agama/dbus/software/proposal" diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb index adf4cd0cb3..06df099ee3 100644 --- a/service/lib/agama/dbus/software/manager.rb +++ b/service/lib/agama/dbus/software/manager.rb @@ -20,7 +20,6 @@ # find current contact information at www.suse.com. require "dbus" -require "suse/connect" require "agama/dbus/base_object" require "agama/dbus/clients/locale" require "agama/dbus/clients/network" @@ -28,7 +27,6 @@ require "agama/dbus/interfaces/progress" require "agama/dbus/interfaces/service_status" require "agama/dbus/with_service_status" -require "agama/registration" module Agama module DBus @@ -56,7 +54,7 @@ def initialize(backend, logger) @selected_patterns = {} end - # List of issues, see {DBus::Interfaces::Issues} + # List of software related issues, see {DBus::Interfaces::Issues} # # @return [Array] def issues @@ -67,23 +65,6 @@ def issues private_constant :SOFTWARE_INTERFACE dbus_interface SOFTWARE_INTERFACE do - dbus_reader :available_products, "a(ssa{sv})" - - dbus_reader :selected_product, "s" - - dbus_method :SelectProduct, "in id:s, out result:(us)" do |id| - logger.info "Selecting product #{id}" - - code, description = select_product(id) - - if code == 0 - dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedProduct" => id }, []) - dbus_properties_changed(REGISTRATION_INTERFACE, { "Requirement" => requirement }, []) - end - - [[code, description]] - end - # value of result hash is category, description, icon, summary and order dbus_method :ListPatterns, "in Filtered:b, out Result:a{s(sssss)}" do |filtered| [ @@ -129,41 +110,6 @@ def issues dbus_method(:Finish) { finish } end - def available_products - backend.products.map do |product| - [product.id, product.display_name, { "description" => product.description }] - end - end - - # Returns the selected base product - # - # @return [String] Product ID or an empty string if no product is selected - def selected_product - backend.product&.id || "" - end - - # Selects a product. - # - # @param id [String] Product id. - # @return [Array(Integer, String)] Result code and a description. - # Possible result codes: - # 0: success - # 1: already selected - # 2: deregister first - # 3: unknown product - def select_product(id) - if backend.product&.id == id - [1, "Product is already selected"] - elsif backend.registration.reg_code - [2, "Current product must be deregistered first"] - else - backend.select_product(id) - [0, ""] - end - rescue ArgumentError - [3, "Unknown product"] - end - def probe busy_while { backend.probe } end @@ -182,103 +128,6 @@ def finish busy_while { backend.finish } end - REGISTRATION_INTERFACE = "org.opensuse.Agama1.Registration" - private_constant :REGISTRATION_INTERFACE - - dbus_interface REGISTRATION_INTERFACE do - dbus_reader(:reg_code, "s") - - dbus_reader(:email, "s") - - dbus_reader(:requirement, "u") - - dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args| - [register(args[0], email: args[1]["Email"])] - end - - dbus_method(:Deregister, "out result:(us)") { [deregister] } - end - - def reg_code - backend.registration.reg_code || "" - end - - def email - backend.registration.email || "" - end - - # Registration requirement. - # - # @return [Integer] Possible values: - # 0: not required - # 1: optional - # 2: mandatory - def requirement - case backend.registration.requirement - when Agama::Registration::Requirement::MANDATORY - 2 - when Agama::Registration::Requirement::OPTIONAL - 1 - else - 0 - end - end - - # Tries to register with the given registration code. - # - # @param reg_code [String] - # @param email [String, nil] - # - # @return [Array(Integer, String)] Result code and a description. - # Possible result codes: - # 0: success - # 1: missing product - # 2: already registered - # 3: network error - # 4: timeout error - # 5: api error - # 6: missing credentials - # 7: incorrect credentials - # 8: invalid certificate - # 9: internal error (e.g., parsing json data) - def register(reg_code, email: nil) - if !backend.product - [1, "Product not selected yet"] - elsif backend.registration.reg_code - [2, "Product already registered"] - else - connect_result(first_error_code: 3) do - backend.registration.register(reg_code, email: email) - end - end - end - - # Tries to deregister. - # - # @return [Array(Integer, String)] Result code and a description. - # Possible result codes: - # 0: success - # 1: missing product - # 2: not registered yet - # 3: network error - # 4: timeout error - # 5: api error - # 6: missing credentials - # 7: incorrect credentials - # 8: invalid certificate - # 9: internal error (e.g., parsing json data) - def deregister - if !backend.product - [1, "Product not selected yet"] - elsif !backend.registration.reg_code - [2, "Product not registered yet"] - else - connect_result(first_error_code: 3) do - backend.registration.deregister - end - end - end - private # @return [Agama::Software] @@ -300,8 +149,6 @@ def register_callbacks self.selected_patterns = compute_patterns end - backend.registration.on_change { registration_properties_changed } - backend.on_issues_change { issues_properties_changed } end @@ -315,52 +162,6 @@ def compute_patterns patterns end - - def registration_properties_changed - dbus_properties_changed(REGISTRATION_INTERFACE, - interfaces_and_properties[REGISTRATION_INTERFACE], []) - end - - # Result from calling to SUSE connect. - # - # @raise [Exception] if an unexpected error is found. - # - # @return [Array(Integer, String)] List including a result code and a description - # (e.g., [1, "Connection to registration server failed (network error)"]). - def connect_result(first_error_code: 1, &block) - block.call - [0, ""] - rescue SocketError => e - connect_result_from_error(e, first_error_code, "network error") - rescue Timeout::Error => e - connect_result_from_error(e, first_error_code + 1, "timeout") - rescue SUSE::Connect::ApiError => e - connect_result_from_error(e, first_error_code + 2) - rescue SUSE::Connect::MissingSccCredentialsFile => e - connect_result_from_error(e, first_error_code + 3, "missing credentials") - rescue SUSE::Connect::MalformedSccCredentialsFile => e - connect_result_from_error(e, first_error_code + 4, "incorrect credentials") - rescue OpenSSL::SSL::SSLError => e - connect_result_from_error(e, first_error_code + 5, "invalid certificate") - rescue JSON::ParserError => e - connect_result_from_error(e, first_error_code + 6) - end - - # Generates a result from a given error. - # - # @param error [Exception] - # @param error_code [Integer] - # @param details [String, nil] - # - # @return [Array(Integer, String)] List including an error code and a description. - def connect_result_from_error(error, error_code, details = nil) - logger.error("Error connecting to registration server: #{error}") - - description = "Connection to registration server failed" - description += " (#{details})" if details - - [error_code, description] - end end end end diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb new file mode 100644 index 0000000000..2029d9c2a7 --- /dev/null +++ b/service/lib/agama/dbus/software/product.rb @@ -0,0 +1,285 @@ +# frozen_string_literal: true + +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "dbus" +require "suse/connect" +require "agama/dbus/base_object" +require "agama/dbus/interfaces/issues" +require "agama/registration" + +module Agama + module DBus + module Software + # D-Bus object to manage product configuration + class Product < BaseObject + include Interfaces::Issues + + PATH = "/org/opensuse/Agama/Software1/Product" + private_constant :PATH + + # Constructor + # + # @param backend [Agama::Software] + # @param logger [Logger] + def initialize(backend, logger) + super(PATH, logger: logger) + @backend = backend + @logger = logger + register_callbacks + end + + # List of issues, see {DBus::Interfaces::Issues} + # + # @return [Array] + def issues + backend.product_issues + end + + def available_products + backend.products.map do |product| + [product.id, product.display_name, { "description" => product.description }] + end + end + + # Returns the selected base product + # + # @return [String] Product ID or an empty string if no product is selected + def selected_product + backend.product&.id || "" + end + + # Selects a product. + # + # @param id [String] Product id. + # @return [Array(Integer, String)] Result code and a description. + # Possible result codes: + # 0: success + # 1: already selected + # 2: deregister first + # 3: unknown product + def select_product(id) + if backend.product&.id == id + [1, "Product is already selected"] + elsif backend.registration.reg_code + [2, "Current product must be deregistered first"] + else + backend.select_product(id) + [0, ""] + end + rescue ArgumentError + [3, "Unknown product"] + end + + PRODUCT_INTERFACE = "org.opensuse.Agama.Software1.Product" + private_constant :PRODUCT_INTERFACE + + dbus_interface PRODUCT_INTERFACE do + dbus_reader :available_products, "a(ssa{sv})" + + dbus_reader :selected_product, "s" + + dbus_method :SelectProduct, "in id:s, out result:(us)" do |id| + logger.info "Selecting product #{id}" + + code, description = select_product(id) + + if code == 0 + dbus_properties_changed(PRODUCT_INTERFACE, { "SelectedProduct" => id }, []) + dbus_properties_changed(REGISTRATION_INTERFACE, { "Requirement" => requirement }, []) + # FIXME: Product issues might change after selecting a product. Nevertheless, + # #on_issues_change callbacks should be used for emitting issues signals, ensuring + # they are emitted every time the backend changes its issues. Currently, + # #on_issues_change cannot be used for product issues. Note that Software::Manager + # backend takes care of both software and product issues. And it already uses + # #on_issues_change callbacks for software related issues. + issues_properties_changed + end + + [[code, description]] + end + end + + def reg_code + backend.registration.reg_code || "" + end + + def email + backend.registration.email || "" + end + + # Registration requirement. + # + # @return [Integer] Possible values: + # 0: not required + # 1: optional + # 2: mandatory + def requirement + case backend.registration.requirement + when Agama::Registration::Requirement::MANDATORY + 2 + when Agama::Registration::Requirement::OPTIONAL + 1 + else + 0 + end + end + + # Tries to register with the given registration code. + # + # @param reg_code [String] + # @param email [String, nil] + # + # @return [Array(Integer, String)] Result code and a description. + # Possible result codes: + # 0: success + # 1: missing product + # 2: already registered + # 3: network error + # 4: timeout error + # 5: api error + # 6: missing credentials + # 7: incorrect credentials + # 8: invalid certificate + # 9: internal error (e.g., parsing json data) + def register(reg_code, email: nil) + if !backend.product + [1, "Product not selected yet"] + elsif backend.registration.reg_code + [2, "Product already registered"] + else + connect_result(first_error_code: 3) do + backend.registration.register(reg_code, email: email) + end + end + end + + # Tries to deregister. + # + # @return [Array(Integer, String)] Result code and a description. + # Possible result codes: + # 0: success + # 1: missing product + # 2: not registered yet + # 3: network error + # 4: timeout error + # 5: api error + # 6: missing credentials + # 7: incorrect credentials + # 8: invalid certificate + # 9: internal error (e.g., parsing json data) + def deregister + if !backend.product + [1, "Product not selected yet"] + elsif !backend.registration.reg_code + [2, "Product not registered yet"] + else + connect_result(first_error_code: 3) do + backend.registration.deregister + end + end + end + + REGISTRATION_INTERFACE = "org.opensuse.Agama1.Registration" + private_constant :REGISTRATION_INTERFACE + + dbus_interface REGISTRATION_INTERFACE do + dbus_reader(:reg_code, "s") + + dbus_reader(:email, "s") + + dbus_reader(:requirement, "u") + + dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args| + [register(args[0], email: args[1]["Email"])] + end + + dbus_method(:Deregister, "out result:(us)") { [deregister] } + end + + private + + # @return [Agama::Software] + attr_reader :backend + + # @return [Logger] + attr_reader :logger + + # Registers callback to be called + def register_callbacks + # FIXME: Product issues might change after changing the registration. Nevertheless, + # #on_issues_change callbacks should be used for emitting issues signals, ensuring they + # are emitted every time the backend changes its issues. Currently, #on_issues_change + # cannot be used for product issues. Note that Software::Manager backend takes care of + # both software and product issues. And it already uses #on_issues_change callbacks for + # software related issues. + backend.registration.on_change { issues_properties_changed } + backend.registration.on_change { registration_properties_changed } + end + + def registration_properties_changed + dbus_properties_changed(REGISTRATION_INTERFACE, + interfaces_and_properties[REGISTRATION_INTERFACE], []) + end + + # Result from calling to SUSE connect. + # + # @raise [Exception] if an unexpected error is found. + # + # @return [Array(Integer, String)] List including a result code and a description + # (e.g., [1, "Connection to registration server failed (network error)"]). + def connect_result(first_error_code: 1, &block) + block.call + [0, ""] + rescue SocketError => e + connect_result_from_error(e, first_error_code, "network error") + rescue Timeout::Error => e + connect_result_from_error(e, first_error_code + 1, "timeout") + rescue SUSE::Connect::ApiError => e + connect_result_from_error(e, first_error_code + 2) + rescue SUSE::Connect::MissingSccCredentialsFile => e + connect_result_from_error(e, first_error_code + 3, "missing credentials") + rescue SUSE::Connect::MalformedSccCredentialsFile => e + connect_result_from_error(e, first_error_code + 4, "incorrect credentials") + rescue OpenSSL::SSL::SSLError => e + connect_result_from_error(e, first_error_code + 5, "invalid certificate") + rescue JSON::ParserError => e + connect_result_from_error(e, first_error_code + 6) + end + + # Generates a result from a given error. + # + # @param error [Exception] + # @param error_code [Integer] + # @param details [String, nil] + # + # @return [Array(Integer, String)] List including an error code and a description. + def connect_result_from_error(error, error_code, details = nil) + logger.error("Error connecting to registration server: #{error}") + + description = "Connection to registration server failed" + description += " (#{details})" if details + + [error_code, description] + end + end + end + end +end diff --git a/service/lib/agama/dbus/software_service.rb b/service/lib/agama/dbus/software_service.rb index a2d7100ec9..1ce788a18e 100644 --- a/service/lib/agama/dbus/software_service.rb +++ b/service/lib/agama/dbus/software_service.rb @@ -83,6 +83,7 @@ def service def dbus_objects @dbus_objects ||= [ Agama::DBus::Software::Manager.new(@backend, logger), + Agama::DBus::Software::Product.new(@backend, logger), Agama::DBus::Software::Proposal.new(logger) ] end diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index ef1a1be3b6..e2d6167cea 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -43,7 +43,15 @@ module Agama module Software - # This class is responsible for software handling + # This class is responsible for software handling. + # + # FIXME: This class has too many responsibilities: + # * Address the software service workflow (probe, propose, install). + # * Manages repositories, packages, patterns, services. + # * Manages product selection. + # * Manages software and product related issues. + # + # It shoud be splitted in separate and smaller classes. class Manager include Helpers include WithIssues @@ -322,6 +330,17 @@ def remove_service(service) true end + # Issues associated to the product. + # + # These issues are not considered as software issues, see {#update_issues}. + # + # @return [Array] + def product_issues + issues = [] + issues << missing_product_issue unless product + issues << missing_registration_issue if missing_registration? + issues + end private @@ -391,16 +410,16 @@ def selected_patterns_changed @selected_patterns_change_callbacks.each(&:call) end - # Updates the list of issues. + # Updates the list of software issues. def update_issues self.issues = current_issues end - # List of current issues. + # List of current software issues. # # @return [Array] def current_issues - return [missing_product_issue] unless product + return [] unless product issues = repos_issues @@ -408,19 +427,9 @@ def current_issues # packages. Those issues does not make any sense if there are no repositories to install # from. issues += proposal.issues if repositories.enabled.any? - issues << missing_registration_issue if missing_registration? issues end - # Issue when a product is missing - # - # @return [Agama::Issue] - def missing_product_issue - Issue.new("Product not selected yet", - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR) - end - # Issues related to the software proposal. # # Repositories that could not be probed are reported as errors. @@ -434,6 +443,15 @@ def repos_issues end end + # Issue when a product is missing + # + # @return [Agama::Issue] + def missing_product_issue + Issue.new("Product not selected yet", + source: Issue::Source::CONFIG, + severity: Issue::Severity::ERROR) + end + # Issue when a product requires registration but it is not registered yet. # # @return [Agama::Issue]