diff --git a/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml deleted file mode 100644 index 945966dedd..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml deleted file mode 120000 index d0feb248d1..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml +++ /dev/null @@ -1 +0,0 @@ -org.opensuse.Agama.Software1.bus.xml \ No newline at end of file diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml deleted file mode 100644 index 360c277ef0..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml deleted file mode 100644 index b501f0d052..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml deleted file mode 100644 index 959cc521b8..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml deleted file mode 100644 index fcb0439338..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml deleted file mode 100644 index 060c3de4d2..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml deleted file mode 120000 index d0feb248d1..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml +++ /dev/null @@ -1 +0,0 @@ -org.opensuse.Agama.Software1.bus.xml \ No newline at end of file diff --git a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml deleted file mode 120000 index 9cfd11ce93..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml +++ /dev/null @@ -1 +0,0 @@ -org.opensuse.Agama.Software1.Product.bus.xml \ No newline at end of file diff --git a/doc/dbus/bus/seed.sh b/doc/dbus/bus/seed.sh index a2ef64f46c..d7a1fb44e7 100755 --- a/doc/dbus/bus/seed.sh +++ b/doc/dbus/bus/seed.sh @@ -27,13 +27,4 @@ look() { >$DD.$1.bus.xml } -look Manager1 -look Software1 -look Software1.Proposal look Storage1 - -abusctl introspect --xml-interface \ - ${DD}.Manager1 \ - ${SS}/Users1 | - cleanup \ - >${DD}.Users1.bus.xml diff --git a/doc/dbus/org.opensuse.Agama.Security.doc.xml b/doc/dbus/org.opensuse.Agama.Security.doc.xml deleted file mode 100644 index 6b183e682b..0000000000 --- a/doc/dbus/org.opensuse.Agama.Security.doc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml deleted file mode 100644 index 165a67094b..0000000000 --- a/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml deleted file mode 100644 index 215acb0e81..0000000000 --- a/doc/dbus/org.opensuse.Agama.Software1.doc.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama.Users1.doc.xml b/doc/dbus/org.opensuse.Agama.Users1.doc.xml deleted file mode 100644 index 50c3971583..0000000000 --- a/doc/dbus/org.opensuse.Agama.Users1.doc.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama1.Manager.doc.xml b/doc/dbus/org.opensuse.Agama1.Manager.doc.xml deleted file mode 100644 index 13dd26b9a5..0000000000 --- a/doc/dbus/org.opensuse.Agama1.Manager.doc.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml b/doc/dbus/org.opensuse.Agama1.Progress.doc.xml deleted file mode 100644 index 1a1a3ed0e0..0000000000 --- a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/org.opensuse.Agama1.Registration.doc.xml b/doc/dbus/org.opensuse.Agama1.Registration.doc.xml deleted file mode 100644 index 750c11ee7d..0000000000 --- a/doc/dbus/org.opensuse.Agama1.Registration.doc.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/dbus_api.md b/doc/dbus_api.md index 7352bf2d12..7e5ae4c7a0 100644 --- a/doc/dbus_api.md +++ b/doc/dbus_api.md @@ -44,15 +44,6 @@ We use these resources to get more familiar with D-Bus API designing. - network manager design https://people.freedesktop.org/~lkundrak/nm-docs/spec.html - anakonda D-Bus API ( spread in `*_interface.py` files https://github.com/rhinstaller/anaconda/tree/master/pyanaconda/modules -## Base Product - -Iface: o.o.Agama.Software1 - -See the new-style [reference][lang-ref] ([source][lang-src]). - -[lang-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama.Software1.html -[lang-src]: dbus/org.opensuse.Agama.Software1.doc.xml - ## `org.opensuse.Agama.Storage1` Service Service for managing storage devices. @@ -594,17 +585,3 @@ Summary readable a{s(uub)} ##### Signals * `PropertiesChanged`, as standard from `org.freedesktop.DBus.Properties`. - -## Users - -See the new-style [reference][usr-ref] ([source][usr-src]). - -[usr-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama.Users1.html -[usr-src]: dbus/org.opensuse.Agama.Users1.doc.xml - -## Manager - -See the new-style [reference][mgr-ref] ([source][mgr-src]). - -[mgr-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama1.Manager.html -[mgr-src]: dbus/org.opensuse.Agama1.Manager.doc.xml diff --git a/service/Gemfile.lock b/service/Gemfile.lock index 7182a681bb..87e0536f53 100755 --- a/service/Gemfile.lock +++ b/service/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - agama-yast (17.devel564.e889a82fd) + agama-yast (17.devel904.666a28170) cfa (~> 1.0.2) cfa_grub2 (~> 2.0.0) cheetah (~> 1.0.0) diff --git a/service/agama-yast.gemspec b/service/agama-yast.gemspec index 438f65ccda..86ccb6d74b 100644 --- a/service/agama-yast.gemspec +++ b/service/agama-yast.gemspec @@ -40,7 +40,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/agama-project/agama" spec.license = "GPL-2.0-only" spec.files = Dir["lib/**/*.rb", "bin/*", "share/*", "conf.d/*", "install.sh"] - spec.executables = ["agamactl", "agama-proxy-setup", "agama-autoyast"] + spec.executables = ["agamactl", "agama-autoyast"] spec.metadata = { "rubygems_mfa_required" => "true" } spec.required_ruby_version = ">= 2.5.0" diff --git a/service/agama-yast.spec.in b/service/agama-yast.spec.in index 335e1e736f..177147e2d2 100644 --- a/service/agama-yast.spec.in +++ b/service/agama-yast.spec.in @@ -80,7 +80,6 @@ sh "%{SOURCE2}" "%{SOURCE1}" %{_datadir}/dbus-1/agama-services/org.opensuse.Agama*.service %{_unitdir}/agama.service %{_unitdir}/agama-dbus-monitor.service -%{_unitdir}/agama-proxy-setup.service %dir %{_datadir}/agama %dir %{_datadir}/agama/conf.d %{_datadir}/agama/conf.d diff --git a/service/bin/agama-proxy-setup b/service/bin/agama-proxy-setup deleted file mode 100755 index 8d16341175..0000000000 --- a/service/bin/agama-proxy-setup +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "agama/proxy_setup" - -Agama::ProxySetup.instance.run diff --git a/service/bin/agamactl b/service/bin/agamactl index a497f9027a..348a392172 100755 --- a/service/bin/agamactl +++ b/service/bin/agamactl @@ -21,7 +21,6 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -# TEMPORARY overwrite of Y2DIR to use DBus for communication with dependent yast modules $LOAD_PATH.unshift File.expand_path("../lib", __dir__) # Set the PATH to a known value @@ -55,10 +54,6 @@ end # @param name [Symbol] Service name # @see ORDERED_SERVICES def start_service(name) - general_y2dir = File.expand_path("../lib/agama/dbus/y2dir", __dir__) - module_y2dir = File.expand_path("../lib/agama/dbus/y2dir/#{name}", __dir__) - ENV["Y2DIR"] = [ENV.fetch("Y2DIR", nil), module_y2dir, general_y2dir].compact.join(":") - logger = logger_for(name) service_runner = Agama::DBus::ServiceRunner.new(name, logger: logger) service_runner.run diff --git a/service/lib/agama/dbus.rb b/service/lib/agama/dbus.rb index 23bfc6ee71..6c5239d8af 100644 --- a/service/lib/agama/dbus.rb +++ b/service/lib/agama/dbus.rb @@ -25,7 +25,4 @@ module DBus end end -require "agama/dbus/manager" -require "agama/dbus/software" require "agama/dbus/storage" -require "agama/dbus/users" diff --git a/service/lib/agama/dbus/clients/manager.rb b/service/lib/agama/dbus/clients/manager.rb deleted file mode 100644 index 2ef82a0cd5..0000000000 --- a/service/lib/agama/dbus/clients/manager.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "agama/dbus/clients/base" -require "agama/dbus/clients/with_service_status" -require "agama/dbus/clients/with_progress" -require "agama/dbus/manager" -require "agama/installation_phase" - -module Agama - module DBus - module Clients - # D-Bus client for manager service - class Manager < Base - include WithServiceStatus - include WithProgress - - def initialize - super - - @dbus_object = service["/org/opensuse/Agama/Manager1"] - @dbus_object.introspect - end - - def service_name - @service_name ||= "org.opensuse.Agama.Manager1" - end - - def probe - dbus_object.Probe - end - - # Starts the installation - def commit - dbus_object.Commit - end - - def current_installation_phase - dbus_phase = dbus_object["org.opensuse.Agama.Manager1"]["CurrentInstallationPhase"] - - case dbus_phase - when DBus::Manager::STARTUP_PHASE - InstallationPhase::STARTUP - when DBus::Manager::CONFIG_PHASE - InstallationPhase::CONFIG - when DBus::Manager::INSTALL_PHASE - InstallationPhase::INSTALL - when DBus::Manager::FINISH_PHASE - InstallationPhase::FINISH - end - end - - private - - # @return [::DBus::Object] - attr_reader :dbus_object - end - end - end -end diff --git a/service/lib/agama/dbus/clients/network.rb b/service/lib/agama/dbus/clients/network.rb deleted file mode 100644 index 0b8ff686ca..0000000000 --- a/service/lib/agama/dbus/clients/network.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "agama/dbus/clients/base" - -module Agama - module DBus - module Clients - # D-Bus client for the network service - # - # This client is intended to be used to watch for changes in - # NetworkManager because Agama does not implement its own network - # service. The configuration is done directly in the UI or through - # `nmcli`. - class Network < Base - def initialize - super - - @dbus_object = service["/org/freedesktop/NetworkManager"] - @dbus_object.introspect - @nm_iface = @dbus_object["org.freedesktop.NetworkManager"] - end - - def service_name - @service_name ||= "org.freedesktop.NetworkManager" - end - - CONNECTED_NM_STATE = 70 - private_constant :CONNECTED_NM_STATE - - # Registers a callback to call when connectivity state changes - # - # The block receives a boolean argument which is true when the network - # connection is working or false otherwise. - # - # @param [Proc] block - def on_connection_changed(&block) - @nm_iface.on_signal("StateChanged") do |nm_state| - block.call(nm_state == CONNECTED_NM_STATE) - end - end - - private - - def bus - @bus ||= ::DBus::SystemBus.instance - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb deleted file mode 100644 index 4e89a1c92a..0000000000 --- a/service/lib/agama/dbus/clients/software.rb +++ /dev/null @@ -1,215 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2024] 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 "agama/dbus/clients/base" -require "agama/dbus/clients/with_issues" -require "agama/dbus/clients/with_locale" -require "agama/dbus/clients/with_progress" -require "agama/dbus/clients/with_service_status" - -module Agama - module DBus - module Clients - # D-Bus client for software configuration - class Software < Base - include WithLocale - include WithIssues - include WithProgress - include WithServiceStatus - - TYPES = [:package, :pattern].freeze - private_constant :TYPES - - # @note This client is singleton because ruby-dbus does not work properly with several - # instances of the same client. - def self.instance - @instance ||= new - end - - # @return [String] - def service_name - @service_name ||= "org.opensuse.Agama.Software1" - end - - # Available products for the installation - # - # @return [Array>] name and display name of each product - def available_products - dbus_product["org.opensuse.Agama.Software1.Product"]["AvailableProducts"].map do |l| - l[0..1] - end - end - - # Product selected to install - # - # @return [String, nil] name of the product - def selected_product - product = dbus_product["org.opensuse.Agama.Software1.Product"]["SelectedProduct"] - return nil if product.empty? - - product - end - - # Selects the product to install - # - # @param name [String] - def select_product(name) - dbus_product.SelectProduct(name) - end - - # Starts the probing process - # - # If a block is given, the method returns immediately and the probing is performed in an - # asynchronous way. - # - # @param done [Proc] Block to execute once the probing is done - def probe(&done) - dbus_object.Probe(&done) - end - - # Performs the packages installation - def install - dbus_object.Install - end - - # Makes the software proposal - def propose - dbus_object.Propose - end - - # Finishes the software installation - def finish - dbus_object.Finish - end - - # Determine whether the given tags are provided by the selected packages - # - # @param tags [Array] Tags to search for (package names, requires/provides, or file - # names) - # @return [Array] An array containing whether each tag is selected or not - def provisions_selected?(tags) - dbus_object.ProvisionsSelected(tags) - end - - # Determines whether a package is installed. - # - # @param name [String] Package name. - # @return [Boolean] - def package_installed?(name) - dbus_object.IsPackageInstalled(name) - end - - # Determines whether a package is available. - # - # @param name [String] Package name. - # @return [Boolean] - def package_available?(name) - dbus_object.IsPackageAvailable(name) - end - - # Add the given list of resolvables to the packages proposal - # - # @param unique_id [String] Unique identifier for the resolvables list - # @param type [Symbol] Resolvables type (:package or :pattern) - # @param resolvables [Array] Resolvables to add - # @param [Boolean] optional True for optional list, false (the default) for - # the required list - def add_resolvables(unique_id, type, resolvables, optional: false) - dbus_proposal.AddResolvables(unique_id, TYPES.index(type), resolvables, optional) - end - - # Returns a list of resolvables - # - # @param unique_id [String] Unique identifier for the resolvables list - # @param type [Symbol] Resolvables type (:package or :pattern) - # @param [Boolean] optional True for optional list, false (the default) for - # the required list - # @return [Array] Resolvables - def get_resolvables(unique_id, type, optional: false) - dbus_proposal.GetResolvables(unique_id, TYPES.index(type), optional).first - end - - # Replace a list of resolvables in the packages proposal - # - # @param unique_id [String] Unique identifier for the resolvables list - # @param type [Symbol] Resolvables type (:package or :pattern) - # @param resolvables [Array] List of resolvables - # @param [Boolean] optional True for optional list, false (the default) for - # the required list - def set_resolvables(unique_id, type, resolvables, optional: false) - dbus_proposal.SetResolvables(unique_id, TYPES.index(type), resolvables, optional) - end - - # Remove resolvables from a list - # - # @param unique_id [String] Unique identifier for the resolvables list - # @param type [Symbol] Resolvables type (:package or :pattern) - # @param resolvables [Array] Resolvables to remove - # @param [Boolean] optional True for optional list, false (the default) for - # the required list - def remove_resolvables(unique_id, type, resolvables, optional: false) - dbus_proposal.RemoveResolvables(unique_id, TYPES.index(type), resolvables, optional) - end - - # Registers a callback to run when the product changes - # - # @param block [Proc] Callback to run when a product is selected - def on_product_selected(&block) - on_properties_change(dbus_product) do |_, changes, _| - product = changes["SelectedProduct"] - block.call(product) unless product.nil? - end - end - - # Registers a callback to run when the software is probed. - # - # @param block [Proc] - def on_probe_finished(&block) - subscribe(dbus_object, "org.opensuse.Agama.Software1", "ProbeFinished", &block) - end - - private - - # @return [::DBus::Object] - attr_reader :dbus_object - - # @return [::DBus::Object] - attr_reader :dbus_product - - # @return [::DBus::Object] - attr_reader :dbus_proposal - - def initialize - super - - @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 - end - end - end -end diff --git a/service/lib/agama/dbus/manager.rb b/service/lib/agama/dbus/manager.rb deleted file mode 100644 index b319e04f21..0000000000 --- a/service/lib/agama/dbus/manager.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2021-2025] 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 "agama/autoyast/converter" -require "agama/dbus/base_object" -require "agama/dbus/interfaces/locale" -require "agama/dbus/interfaces/progress" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/with_progress" -require "agama/dbus/with_service_status" -require "agama/manager" - -module Agama - module DBus - # D-Bus object to manage the installation process - class Manager < BaseObject - include WithProgress - include WithServiceStatus - include Interfaces::Progress - include Interfaces::ServiceStatus - include Interfaces::Locale - - PATH = "/org/opensuse/Agama/Manager1" - private_constant :PATH - - # Constructor - # - # @param backend [Agama::Manager] - # @param logger [Logger] - def initialize(backend, logger) - super(PATH, logger: logger) - @backend = backend - register_callbacks - register_progress_callbacks - register_service_status_callbacks - end - - MANAGER_INTERFACE = "org.opensuse.Agama.Manager1" - private_constant :MANAGER_INTERFACE - - STARTUP_PHASE = 0 - CONFIG_PHASE = 1 - INSTALL_PHASE = 2 - FINISH_PHASE = 3 - - dbus_interface MANAGER_INTERFACE do - dbus_method(:Probe, "in data:a{sv}") { |_| config_phase } - dbus_method(:Reprobe, "in data:a{sv}") { |_| config_phase(reprobe: true) } - dbus_method(:Commit, "") { install_phase } - dbus_method(:CanInstall, "out result:b") { can_install? } - dbus_method(:CollectLogs, "out tarball_filesystem_path:s") { collect_logs } - dbus_method(:Finish, "in method:s, out result:b") { |m| finish_phase(m) } - dbus_reader :installation_phases, "aa{sv}" - dbus_reader :current_installation_phase, "u" - dbus_reader :iguana_backend, "b" - dbus_reader :busy_services, "as" - end - - # Runs the config phase - # - # @param reprobe [Boolean] Whether a reprobe should be done instead of a probe. - def config_phase(reprobe: false) - safe_run do - busy_while { backend.config_phase(reprobe: reprobe) } - end - end - - # Runs the install phase - def install_phase - raise ::DBus::Error, "Installation settings are invalid" unless backend.valid? - - safe_run do - busy_while { backend.install_phase } - end - end - - # Determines whether the installation can start - # - # @return [Boolean] - def can_install? - backend.valid? - end - - # Collects the YaST logs - def collect_logs - backend.collect_logs - end - - # Last action for the installer - # - # @param method [String] - # @return [Boolean] - def finish_phase(method) - backend.finish_installation(method) - end - - # Description of all possible installation phase values - # - # @return [Array] - def installation_phases - [ - { "id" => STARTUP_PHASE, "label" => "startup" }, - { "id" => CONFIG_PHASE, "label" => "config" }, - { "id" => INSTALL_PHASE, "label" => "install" }, - { "id" => FINISH_PHASE, "label" => "finish" } - ] - end - - # Current value of the installation phase - # - # @return [Integer] - def current_installation_phase - return STARTUP_PHASE if backend.installation_phase.startup? - return CONFIG_PHASE if backend.installation_phase.config? - return INSTALL_PHASE if backend.installation_phase.install? - return FINISH_PHASE if backend.installation_phase.finish? - end - - # States whether installation runs on iguana - def iguana_backend - backend.iguana? - end - - # Name of the services that are currently busy - # - # @return [Array] - def busy_services - backend.busy_services - end - - # Redefines #service_status to use the one from the backend - # - # @return [DBus::ServiceStatus] - def service_status - backend.service_status - end - - def locale=(locale) - safe_run do - busy_while { backend.locale = locale } - end - end - - private - - # @return [Agama::Manager] - attr_reader :backend - - # Executes the given block only if the service is idle - # - # @note The service still dispatches messages while waiting for a D-Bus answer. - # - # @param block [Proc] - def safe_run(&block) - raise busy_error if service_status.busy? - - block.call - end - - def busy_error - ::DBus.error("org.opensuse.Agama1.Error.Busy") - end - - # Registers callback to be called - def register_callbacks - backend.installation_phase.on_change do - dbus_properties_changed(MANAGER_INTERFACE, - { "CurrentInstallationPhase" => current_installation_phase }, []) - end - - backend.on_services_status_change do - dbus_properties_changed(MANAGER_INTERFACE, { "BusyServices" => busy_services }, []) - end - end - end - end -end diff --git a/service/lib/agama/dbus/manager_service.rb b/service/lib/agama/dbus/manager_service.rb deleted file mode 100644 index 5804d9623b..0000000000 --- a/service/lib/agama/dbus/manager_service.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "agama/manager" -require "agama/users" -require "agama/dbus/bus" -require "agama/dbus/manager" -require "agama/dbus/users" - -module Agama - module DBus - # D-Bus service (org.opensuse.Agama.Manager1) - # - # It connects to the system D-Bus and answers requests on objects below - # `/org/opensuse/Agama1`. - class ManagerService - # D-Bus service (org.opensuse.Agama.Manager1) - SERVICE_NAME = "org.opensuse.Agama.Manager1" - private_constant :SERVICE_NAME - - # Agama D-Bus - # - # @return [::DBus::BusConnection] - attr_reader :bus - - # Installation manager - # - # @return [Agama::Manager] - attr_reader :manager - - # Users manager - # - # @return [Agama::Users] - attr_reader :users - - # @param config [Config] Configuration - # @param logger [Logger] - def initialize(config, logger = nil) - @config = config - @manager = Agama::Manager.new(config, logger) - @users = Agama::Users.new(logger) - @logger = logger || Logger.new($stdout) - @bus = Bus.current - end - - # Initializes and exports the D-Bus API - # - # @note The service runs its startup phase - def start - export - manager.on_progress_change { dispatch } # make single thread more responsive - manager.startup_phase - end - - # Exports the installer object through the D-Bus service - def export - # manager service initialization - dbus_objects.each { |o| service.export(o) } - - paths = dbus_objects.map(&:path).join(", ") - logger.info "Exported #{paths} objects" - end - - # Call this from some main loop to dispatch the D-Bus messages - def dispatch - bus.dispatch_message_queue - end - - private - - # @return [Logger] - attr_reader :logger - - # @return [Config] - attr_reader :config - - # @return [::DBus::ObjectServer] - def service - @service ||= bus.request_service(SERVICE_NAME) - end - - # @return [Array<::DBus::Object>] - def dbus_objects - @dbus_objects ||= [ - manager_dbus, - users_dbus - ] - end - - def manager_dbus - @manager_dbus ||= Agama::DBus::Manager.new(manager, logger) - end - - def users_dbus - @users_dbus ||= Agama::DBus::Users.new(manager.users, logger) - end - end - end -end diff --git a/service/lib/agama/dbus/software.rb b/service/lib/agama/dbus/software.rb deleted file mode 100644 index ace66f64fc..0000000000 --- a/service/lib/agama/dbus/software.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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. - -module Agama - module DBus - # Name space for software D-Bus classes - module Software - end - end -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 deleted file mode 100644 index fde826934d..0000000000 --- a/service/lib/agama/dbus/software/manager.rb +++ /dev/null @@ -1,280 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "agama/dbus/base_object" -require "agama/dbus/clients/network" -require "agama/dbus/interfaces/issues" -require "agama/dbus/interfaces/locale" -require "agama/dbus/interfaces/progress" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/with_progress" -require "agama/dbus/with_service_status" - -module Agama - module DBus - module Software - # D-Bus object to manage software installation - class Manager < BaseObject - include WithProgress - include WithServiceStatus - include Interfaces::Progress - include Interfaces::ServiceStatus - include Interfaces::Issues - include Interfaces::Locale - - PATH = "/org/opensuse/Agama/Software1" - private_constant :PATH - - # Constructor - # - # @param backend [Agama::Software] - # @param logger [Logger] - def initialize(backend, logger) - super(PATH, logger: logger) - @backend = backend - register_callbacks - register_progress_callbacks - register_service_status_callbacks - @selected_patterns = {} - @conflicts = [] - end - - # List of software related issues, see {DBus::Interfaces::Issues} - # - # @return [Array] - def issues - backend.issues - end - - def only_required - case backend.proposal.only_required - when nil then 0 - when false then 1 - when true then 2 - else - @logger.warn( - "Unexpected value in only_required #{backend.proposal.only_required.inspect}" - ) - 0 - end - end - - def only_required=(flag) - value = case flag - when 0 then nil - when 1 then false - when 2 then true - else - @logger.warn "Unexpected value in only_required #{flag.inspect}" - end - backend.proposal.only_required = value - # propose again after changing solver flag - propose - end - - SOFTWARE_INTERFACE = "org.opensuse.Agama.Software1" - private_constant :SOFTWARE_INTERFACE - - dbus_interface SOFTWARE_INTERFACE do - # Flag for proposing required only dependencies - # Propose is called automatically whenever the value is assigned. - # value mapping 0 for not set, 1 for false and 2 for true - dbus_accessor :only_required, "u" - - # array of repository properties: pkg-bindings ID, alias, name, URL, product dir, enabled - # and loaded flag - dbus_method :ListRepositories, "out Result:a(issssbb)" do - [ - backend.repositories.repositories.map do |repo| - [ - repo.repo_id, - repo.repo_alias, - repo.name, - repo.raw_url.uri.to_s, - repo.product_dir, - repo.enabled?, - !!repo.loaded? - ] - end - ] - end - - # set user specified repositories properties - dbus_method :SetUserRepositories, "in repos:aa{sv}" do |repos| - @logger.info "Setting user repositories #{repos.inspect}" - backend.repositories.user_repositories = repos - end - - # set user specified repositories properties - dbus_method :ListUserRepositories, "out repos:aa{sv}" do - [backend.repositories.user_repositories] - 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| - [ - backend.patterns(filtered).each_with_object({}) do |pattern, result| - # make sure all attributes are already preloaded, adjust the "patterns" method - # in service/lib/agama/software/manager.rb when changing this list - value = [ - pattern.category, - pattern.description, - pattern.icon, - pattern.summary, - pattern.order - ] - result[pattern.name] = value - end - ] - end - - # selected patterns is hash with pattern name as id and 0 for user selected and - # 1 for auto selected. Can be extended in future e.g. for mandatory patterns - dbus_reader_attr_accessor :selected_patterns, "a{sy}" - - dbus_method(:AddPattern, "in id:s, out result:b") { |p| backend.add_pattern(p) } - dbus_method(:RemovePattern, "in id:s, out result:b") { |p| backend.remove_pattern(p) } - dbus_method(:SetUserPatterns, "in add:as, in remove:as, out wrong:as") do |add, remove| - [backend.assign_patterns(add, remove)] - end - - dbus_reader_attr_accessor :conflicts, "a(ussa(uss))" - - dbus_method :SolveConflicts, "in solutions:a(uu)" do |solutions| - ret = backend.proposal.solve_conflicts(solutions) - # update the user selected patterns, patterns might be unselected as - # part of the conflict resolution - backend.update_selected_patterns - ret - end - - dbus_method :ProvisionsSelected, "in Provisions:as, out Result:ab" do |provisions| - [provisions.map { |p| backend.provision_selected?(p) }] - end - - dbus_method :IsPackageInstalled, "in Name:s, out Result:b" do |name| - backend.package_installed?(name) - end - - dbus_method :IsPackageAvailable, "in name:s, out result:b" do |name| - backend.package_available?(name) - end - - dbus_method(:UsedDiskSpace, "out SpaceSize:s") { backend.used_disk_space } - - dbus_signal(:ProbeFinished) - - dbus_method(:Probe) { probe } - dbus_method(:Propose) { propose } - dbus_method(:Install) { install } - dbus_method(:Finish) { finish } - end - - def probe - busy_while { backend.probe } - self.ProbeFinished - end - - def propose - busy_while { backend.propose } - - nil # explicit nil as return value - end - - def install - busy_while { backend.install } - end - - def finish - busy_while { backend.finish } - end - - def locale=(locale) - busy_while do - backend.locale = locale - end - end - - def ssl_fingerprints - ssl_storage.fingerprints.map { |f| [f.sum, f.value] } - end - - def ssl_fingerprints=(new_fps) - fps = new_fps.map { |f| SSL::Fingerprint.new(f[0], f[1]) } - ssl_storage.fingerprints.replace(fps) - end - - SECURITY_INTERFACE = "org.opensuse.Agama.Security" - private_constant :SECURITY_INTERFACE - - dbus_interface SECURITY_INTERFACE do - # List of SSL fingerprints serialized into type and its value - dbus_accessor :ssl_fingerprints, "a(ss)" - end - - private - - def ssl_storage - SSL::Storage.instance - end - # @return [Agama::Software] - attr_reader :backend - - # Registers callback to be called - def register_callbacks - nm_client = Agama::DBus::Clients::Network.new - nm_client.on_connection_changed do |connected| - probe if connected - end - - backend.on_selected_patterns_change do - self.selected_patterns = compute_patterns - end - - backend.proposal.on_conflicts_change do |conflicts| - self.conflicts = conflicts.map do |conflict| - [ - conflict["id"], conflict["description"], conflict["details"] || "", - conflict["solutions"].map do |solution| - [solution["id"], solution["description"], solution["details"] || ""] - end - ] - end - end - - backend.on_issues_change { issues_properties_changed } - end - - USER_SELECTED_PATTERN = 0 - AUTO_SELECTED_PATTERN = 1 - def compute_patterns - patterns = {} - user_selected, auto_selected = backend.selected_patterns - user_selected.each { |p| patterns[p] = USER_SELECTED_PATTERN } - auto_selected.each { |p| patterns[p] = AUTO_SELECTED_PATTERN } - - patterns - end - end - end - end -end diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb deleted file mode 100644 index 9e373cab48..0000000000 --- a/service/lib/agama/dbus/software/product.rb +++ /dev/null @@ -1,411 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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/errors" -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 - - # @param backend [Agama::Software::Manager] - # @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| - data = { - "description" => product.localized_description, - "icon" => product.icon, - "registration" => product.registration - } - data["license"] = product.license if product.license - [ - product.id, - product.display_name, - data - ] - 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.registered - [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_method :AvailableProducts, "out result:a(ssa{sv})" do - [available_products] - end - - 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 }, []) - # 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 registered - !!backend.registration.registered - end - - def reg_code - backend.registration.reg_code || "" - end - - def email - backend.registration.email || "" - end - - def url - backend.registration.registration_url || "" - end - - def url=(url) - # dbus has problem with nils, so empty string is only for dbus nil - backend.registration.registration_url = url.empty? ? nil : url - end - - # list of already registered addons - # - # @return [Array>] each list contains three items: addon id, version and - # registration code - def registered_addons - backend.registration.registered_addons.map do |addon| - [ - addon.name, - # return empty string if the version was not explicitly specified (was autodetected) - addon.required_version ? addon.version : "", - addon.reg_code - ] - end - end - - # list of available addons - # - # @return [Array>] List of addons - def available_addons - addons = backend.registration.available_addons || [] - - addons.map do |a| - { - "id" => a.identifier, - "version" => a.version, - "label" => a.friendly_name, - "available" => a.available, # boolean - "free" => a.free, # boolean - "recommended" => a.recommended, # boolean - "description" => a.description, - "type" => a.product_type, # "extension" - "release" => a.release_stage # "beta" - } - end - end - - # Tries to register with the given registration code. - # - # @note Software is not automatically probed after registering the product. The reason is - # to avoid dealing with possible probing issues in the registration D-Bus API. Clients - # have to explicitly call to #Probe after registering a product. - # - # @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: registration not required - # 4: network error - # 5: timeout error - # 6: api error - # 7: missing credentials - # 8: incorrect credentials - # 9: invalid certificate - # 10: internal error (e.g., parsing json data) - # 13: Failed to add service from registration - def register(reg_code, email: nil) - if !backend.product - [1, "Product not selected yet"] - # report success and do nothing when already registered with the same code - elsif backend.registration.registered && backend.registration.reg_code == reg_code - [0, ""] - elsif backend.registration.registered - [2, "Product already registered"] - elsif !backend.product.registration - [3, "Product does not require registration"] - else - connect_result(first_error_code: 4) do - backend.registration.register(reg_code, email: email) - end - end - end - - # Tries to register the given addon. The base product must be already registered and if the - # addon requires some other addon it must be already registered as well. (The code does not - # check any dependencies.) - # - # @note Software is not automatically probed after registering the product. The reason is - # to avoid dealing with possible probing issues in the registration D-Bus API. Clients - # 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", 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 ("") - # - # @return [Array(Integer, String)] Result code and a description. - # Possible result codes: - # 0: success - # 1: a base product was not selected yet - # 2: the base product does not require registration - # 3: the base product was not registered yet - # 4: network error - # 5: timeout error - # 6: api error - # 7: missing credentials - # 8: incorrect credentials - # 9: invalid certificate - # 10: internal error (e.g., parsing json data) - # 11: addon not found - # 12: addon found in multiple versions - # 13: Failed to add service from registration - def register_addon(name, version, reg_code) - if !backend.product - [1, "Product not selected yet"] - elsif !backend.product.registration - [2, "Base product does not require registration"] - elsif !backend.registration.registered - [3, "Base product not registered yet"] - else - connect_result(first_error_code: 4) do - backend.registration.register_addon(name, version, reg_code) - end - end - end - - # Tries to deregister. - # - # @note Software is not automatically probed after deregistering the product. The reason is - # to avoid dealing with possible probing issues in the deregistration D-Bus API. Clients - # have to explicitly call to #Probe after deregistering a product. - # - # @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) - # 13: Failed to remove service from registration - def deregister - if !backend.product - [1, "Product not selected yet"] - elsif !backend.registration.registered - [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(:registered, "b") - - dbus_reader(:reg_code, "s") - - dbus_reader(:email, "s") - - dbus_accessor(:url, "s") - - dbus_reader(:registered_addons, "a(sss)") - - dbus_reader(:available_addons, "aa{sv}") - - 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(:RegisterAddon, "in name:s, in version:s, in reg_code:s, out result:(us)") do |*args| - [register_addon(*args)] - 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 } - backend.on_issues_change { issues_properties_changed } - end - - def registration_properties_changed - dbus_properties_changed(REGISTRATION_INTERFACE, - interfaces_and_properties[REGISTRATION_INTERFACE], []) - end - - def product_properties_changed - dbus_properties_changed(PRODUCT_INTERFACE, - interfaces_and_properties[PRODUCT_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) - 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) - rescue Agama::Software::ServiceError => e - connect_result_from_error(e, first_error_code + 9) - rescue StandardError => e - connect_result_from_error(e, first_error_code + 10) - 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: #{error}" - description += " (#{details})" if details - - [error_code, description] - end - end - end - end -end diff --git a/service/lib/agama/dbus/software/proposal.rb b/service/lib/agama/dbus/software/proposal.rb deleted file mode 100644 index cd0e3587d0..0000000000 --- a/service/lib/agama/dbus/software/proposal.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "yast" -require "dbus" -Yast.import "PackagesProposal" - -module Agama - module DBus - module Software - # Software proposal D-Bus representation - # - # This class allows to change software proposal settings through D-Bus. - # - # @see Yast::PackagesProposal - class Proposal < ::DBus::Object - PATH = "/org/opensuse/Agama/Software1/Proposal" - private_constant :PATH - - INTERFACE = "org.opensuse.Agama.Software1.Proposal" - private_constant :INTERFACE - - TYPES = [:package, :pattern].freeze - private_constant :TYPES - - # Constructor - # - # @param logger [Logger] - def initialize(logger) - @logger = logger - super(PATH) - end - - dbus_interface INTERFACE do - dbus_method :AddResolvables, - "in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt| - Yast::PackagesProposal.AddResolvables(id, TYPES[type], resolvables, optional: opt) - end - - dbus_method :GetResolvables, - "in Id:s, in Type:y, in Optional:b, out Resolvables:as" do |id, type, opt| - [Yast::PackagesProposal.GetResolvables(id, TYPES[type], optional: opt)] - end - - dbus_method :SetResolvables, - "in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt| - Yast::PackagesProposal.SetResolvables(id, TYPES[type], resolvables, optional: opt) - end - - dbus_method :RemoveResolvables, - "in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt| - Yast::PackagesProposal.RemoveResolvables(id, TYPES[type], resolvables, optional: opt) - end - end - - private - - # @return [Logger] - attr_reader :logger - end - end - end -end diff --git a/service/lib/agama/dbus/software_service.rb b/service/lib/agama/dbus/software_service.rb deleted file mode 100644 index 650de0a894..0000000000 --- a/service/lib/agama/dbus/software_service.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "agama/dbus/bus" -require "agama/dbus/software" -require "agama/software" - -require "yast" -Yast.import "Pkg" -Yast.import "Language" - -module Agama - module DBus - # D-Bus service (org.opensuse.Agama.Software1) - # - # It connects to the system D-Bus and answers requests on objects below - # `/org/opensuse/Agama/Software1`. - class SoftwareService - SERVICE_NAME = "org.opensuse.Agama.Software1" - private_constant :SERVICE_NAME - - # D-Bus connection - # - # @return [::DBus::BusConnection] - attr_reader :bus - - # @param config [Config] Configuration object - # @param logger [Logger] - def initialize(config, logger = nil) - @logger = logger || Logger.new($stdout) - @bus = Bus.current - @backend = Agama::Software::Manager.new(config, logger) - end - - # Starts software service. It does more then just #export method. - def start - # for some reason the the "export" method must be called before - # registering the language change callback to work properly - export - end - - # Exports the software object through the D-Bus service - def export - dbus_objects.each { |o| service.export(o) } - paths = dbus_objects.map(&:path).join(", ") - logger.info "Exported #{paths} objects" - end - - # Call this from some main loop to dispatch the D-Bus messages - def dispatch - bus.dispatch_message_queue - end - - private - - # @return [Logger] - attr_reader :logger, :backend - - # @return [::DBus::ObjectServer] - def service - @service ||= bus.request_service(SERVICE_NAME) - end - - # @return [Array<::DBus::Object>] - 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 - end - end -end diff --git a/service/lib/agama/dbus/users.rb b/service/lib/agama/dbus/users.rb deleted file mode 100644 index 27fafa3993..0000000000 --- a/service/lib/agama/dbus/users.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "agama/users" -require "agama/dbus/base_object" -require "agama/dbus/with_service_status" -require "agama/dbus/interfaces/issues" -require "agama/dbus/interfaces/service_status" - -module Agama - module DBus - # YaST D-Bus object (/org/opensuse/Agama/Users1) - class Users < BaseObject - include WithServiceStatus - include Interfaces::Issues - include Interfaces::ServiceStatus - - PATH = "/org/opensuse/Agama/Users1" - private_constant :PATH - - # Constructor - # - # @param backend [Agama::Users] - # @param logger [Logger] - def initialize(backend, logger) - super(PATH, logger: logger) - @backend = backend - register_service_status_callbacks - register_users_callbacks - end - - # List of issues, see {DBus::Interfaces::Issues} - # - # @return [Array] - def issues - backend.issues - end - - USERS_INTERFACE = "org.opensuse.Agama.Users1" - private_constant :USERS_INTERFACE - - FUSER_SIG = "in FullName:s, in UserName:s, in Password:s, in HashedPassword:b, " \ - "in data:a{sv}" - private_constant :FUSER_SIG - - dbus_interface USERS_INTERFACE do - dbus_reader :root_user, "(sbs)" - - dbus_reader :first_user, "(sssba{sv})" - - dbus_method :SetRootPassword, - "in Value:s, in Hashed:b, out result:u" do |value, hashed| - logger.info "Setting Root Password" - backend.assign_root_password(value, hashed) - - dbus_properties_changed(USERS_INTERFACE, { "RootUser" => root_user }, []) - 0 - end - - dbus_method :RemoveRootPassword, "out result:u" do - logger.info "Clearing the root password" - backend.remove_root_password - - dbus_properties_changed(USERS_INTERFACE, { "RootUser" => root_user }, - []) - 0 - end - - dbus_method :SetRootSSHKey, "in Value:s, out result:u" do |value| - logger.info "Setting Root ssh key" - backend.root_ssh_key = (value) - - dbus_properties_changed(USERS_INTERFACE, { "RootUser" => root_user }, []) - 0 - end - - # It returns an Struct with the first field with the result of the operation as a boolean - # and the second parameter as an array of issues found in case of failure - dbus_method :SetFirstUser, FUSER_SIG + ", out result:(bas)" do |full_name, user_name, - password, hashed_password, data| - logger.info "Setting first user #{full_name}" - user_issues = backend.assign_first_user(full_name, user_name, password, - hashed_password, data) - - if user_issues.empty? - dbus_properties_changed(USERS_INTERFACE, { "FirstUser" => first_user }, []) - else - logger.info "First user fatal issues detected: #{issues}" - end - - [[user_issues.empty?, user_issues]] - end - - dbus_method :RemoveFirstUser, "out result:u" do - logger.info "Removing the first user" - backend.remove_first_user - - dbus_properties_changed(USERS_INTERFACE, { "FirstUser" => first_user }, []) - 0 - end - - dbus_method :Write, "out result:u" do - logger.info "Writting users" - - backend.write - 0 - end - end - - def root_user - root = backend.root_user - - [ - root.password_content || "", - root.password&.value&.encrypted?, - root.authorized_keys.first || "" - ] - end - - def first_user - user = backend.first_user - - return ["", "", "", false, {}] unless user - - [ - user.full_name, - user.name, - user.password_content || "", - user.password&.value&.encrypted? || false, - {} - ] - end - - private - - # @return [Agama::Users] - attr_reader :backend - - def register_users_callbacks - backend.on_issues_change { issues_properties_changed } - end - end - end -end diff --git a/service/lib/agama/dbus/y2dir/README.md b/service/lib/agama/dbus/y2dir/README.md deleted file mode 100644 index 7febb8ce64..0000000000 --- a/service/lib/agama/dbus/y2dir/README.md +++ /dev/null @@ -1,28 +0,0 @@ -This directory contains some redefinitions of YaST modules in order to call to D-Bus methods instead -of executing the actual code of the module. - -# Why is this needed? - -Agama relies on YaST code and YaST usually works as a single process. By contrast, Agama works as a -set of different processes (software service, storage service, etc), and each service runs its own -YaST instance. - -Having different YaST instances implies that the information is scattered in different processes. -For example, only the YaST instance in the software service has the information about the software -configuration. This means that other YaST instances need to ask to the software YaST instance for -the information. - -# How to communicate among YaST instances - -A YaST instance can get information from other instance by doing a D-Bus call to the service running -such an instance. For example, the YaST instance in the storage service has to call to the D-Bus API -of the software service instead of directly calling to the software module code. To achieve that, -the storage service replaces the implementation of the YaST software module by its own -implementation which uses D-Bus calls. - -# How to replace a YaST module - -The code replacement of the YaST modules is done by means of the *Y2DIR* mechanism of YaST. When a -service is started (check *agamactl* script), the YaST modules redefined by the service (under -*lib/agama/dbus/y2dir/*) are added to the *Y2DIR* environment variable. YaST takes precedence of the -paths at *Y2DIR*, so these files will be loaded instead of the files originally delivered by YaST. diff --git a/service/lib/agama/dbus/y2dir/manager/modules/Package.rb b/service/lib/agama/dbus/y2dir/manager/modules/Package.rb deleted file mode 100644 index 4ec7600b70..0000000000 --- a/service/lib/agama/dbus/y2dir/manager/modules/Package.rb +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) [2022-2024] 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 "yast" -require "agama/http/clients/software" - -# :nodoc: -module Yast - # Replacement for the Yast::Package module. - class PackageClass < Module - def main - puts "Loading mocked module #{__FILE__}" - @client = Agama::HTTP::Clients::Software.new(::Logger.new($stdout)) - end - - # Determines whether a package is available. - # - # @param name [String] Package name. - # @return [Boolean] - def Available(name) - client.package_available?(name) - end - - # Determines whether a set of packages is available. - # - # @param names [Array] Names of the packages. - # @return [Boolean] - def AvailableAll(names) - names.all? { |name| client.package_available?(name) } - end - - # Determines whether a package is installed in the target system. - # - # @param name [String] Package name. - # @return [Boolean] - def Installed(name, target: nil) - client.package_installed?(name) - end - - private - - attr_reader :client - end - - Package = PackageClass.new - Package.main -end diff --git a/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb b/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb deleted file mode 100644 index 7d0ceeabce..0000000000 --- a/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) [2022] 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 "yast" -require "agama/http/clients/main" - -# :nodoc: -module Yast - # Replacement for the Yast::PackagesProposal module - class PackagesProposalClass < Module - def main - puts "Loading mocked module #{__FILE__}" - @client = Agama::HTTP::Clients::Main.new(::Logger.new($stdout)) - end - - # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L118 - def AddResolvables(unique_id, type, resolvables, optional: false) - orig_resolvables = client.get_resolvables(unique_id, type, optional: optional) - orig_resolvables += resolvables - orig_resolvables.uniq! - SetResolvables(unique_id, type, orig_resolvables, optional: optional) - true - end - - # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L145 - def SetResolvables(unique_id, type, resolvables, optional: false) - client.set_resolvables(unique_id, type, resolvables || []) - true - end - - # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L285 - def GetResolvables(unique_id, type, optional: false) - client.get_resolvables(unique_id, type, optional) - end - - # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L177 - def RemoveResolvables(unique_id, type, resolvables, optional: false) - orig_resolvables = client.get_resolvables(unique_id, type, optional: optional) - orig_resolvables -= resolvables - orig_resolvables.uniq! - SetResolvables(unique_id, type, orig_resolvables, optional: optional) - true - end - - private - - attr_reader :client - end - - PackagesProposal = PackagesProposalClass.new - PackagesProposal.main -end diff --git a/service/lib/agama/dbus/y2dir/modules/Autologin.rb b/service/lib/agama/dbus/y2dir/modules/Autologin.rb deleted file mode 100644 index ed97f71f14..0000000000 --- a/service/lib/agama/dbus/y2dir/modules/Autologin.rb +++ /dev/null @@ -1,214 +0,0 @@ -# ------------------------------------------------------------------------------ -# Copyright (c) 2006-2012 Novell, Inc. 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 Novell, Inc. -# -# To contact Novell about this file by physical or electronic mail, you may find -# current contact information at www.novell.com. -# ------------------------------------------------------------------------------ - -# File: modules/Autologin.ycp -# Package: yast2 -# Summary: Autologin read/write routines -# Author: Jiri Suchomel -# Flags: Stable -# -# $Id$ -require "yast" -require "agama/http/clients/software" - -module Yast - class AutologinClass < Module - include Yast::Logger - - # Display managers that support autologin. - # Notice that xdm does NOT support it! - # - # "autologin-support" is a pseudo-"provides" that maintainers of display - # manager packages can add to indicate that the package has that - # capability. - DISPLAY_MANAGERS = ["autologin-support", "kdm", "gdm", "sddm", "lightdm"].freeze - - def main - textdomain "pam" - - Yast.import "Popup" - - # User to log in automaticaly - @user = "" - - # Login without passwords? - @pw_less = false - - # Is autologin used? Usualy true when user is not empty, but for the first - # time (during installation), this can be true by default although user is "" - # (depends on the control file) - @used = false - - # Autologin settings modified? - @modified = false - - # Pkg stuff initialized? - @pkg_initialized = false - - # Software service client - @software_client = nil - end - - def available - @available = supported? if @available.nil? - @available - end - - # Read autologin settings - # @return used? - def Read - if SCR.Read(path(".target.size"), "/etc/sysconfig/displaymanager") == -1 - @available = false - @user = "" - @used = false - return false - end - - @available = supported? - @user = Convert.to_string( - SCR.Read(path(".sysconfig.displaymanager.DISPLAYMANAGER_AUTOLOGIN")) - ) - @pw_less = Convert.to_string( - SCR.Read( - path(".sysconfig.displaymanager.DISPLAYMANAGER_PASSWORD_LESS_LOGIN") - ) - ) == "yes" - - @user = "" if @user.nil? || @user == "" - - @used = @user != "" - @used - end - - # Write autologin settings - # @param _write_only [Boolean] when true, suseconfig script will not be run - # @return [Boolean] - def Write(_write_only) - return false if !available || !@modified - - Builtins.y2milestone( - "writing user %1 for autologin; pw_less is %2", - @user, - @pw_less - ) - - SCR.Write( - path(".sysconfig.displaymanager.DISPLAYMANAGER_AUTOLOGIN"), - @user - ) - SCR.Write( - path(".sysconfig.displaymanager.DISPLAYMANAGER_PASSWORD_LESS_LOGIN"), - @pw_less ? "yes" : "no" - ) - SCR.Write(path(".sysconfig.displaymanager"), nil) - - @modified = false - true - end - - # Disable autologin - def Disable - @user = "" - @pw_less = false - @used = false - @modified = true - - nil - end - - # Wrapper for setting the 'used' variable - def Use(use) - if @used != use - @used = use - @modified = true - end - - nil - end - - # Disable autologin and write it (used probably for automatic - # disabling without asking) - # @param [Boolean] write_only when true, suseconfig script will not be run - # @return written anything? - def DisableAndWrite(write_only) - Disable() - Write(write_only) - end - - # Ask if autologin should be disabled (and disable it in such case) - # @param [String] new The reason for disabling autologin (e.g. new set of users) - # @return Is autologin used? - def AskForDisabling(new) - # popup text (%1 is user name, %2 is additional info, - # like "Now LDAP was enabled") - question = Builtins.sformat( - _( - "The automatic login feature is enabled for user %1.\n" + - "%2\n" + - "Disable automatic login?" - ), - @user, - new - ) - - Disable() if @used && Popup.YesNo(question) - @used - end - - # Check if autologin is supported with the currently selected or installed - # packages. - # - # @return Boolean - def supported? - supported = software_client.provisions_selected?(DISPLAY_MANAGERS).any? - - if supported - log.info("Autologin is supported") - else - log.info("Autologin is not supported: No package provides any of #{DISPLAY_MANAGERS}") - end - - supported - end - - publish variable: :user, type: "string" - publish variable: :pw_less, type: "boolean" - publish variable: :used, type: "boolean" - publish variable: :modified, type: "boolean" - publish function: :Read, type: "boolean ()" - publish function: :Write, type: "boolean (boolean)" - publish function: :Disable, type: "void ()" - publish function: :Use, type: "void (boolean)" - publish function: :supported?, type: "boolean ()" - publish function: :DisableAndWrite, type: "boolean (boolean)" - publish function: :AskForDisabling, type: "boolean (string)" - - private - - # Software service client - # - # @return [Agama::DBus::Clients::Software] Software service client - def software_client - @software_client ||= Agama::HTTP::Clients::Software.new(::Logger.new($stdout)) - end - end - - Autologin = AutologinClass.new - Autologin.main -end diff --git a/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb b/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb deleted file mode 100644 index a1d5a21cc4..0000000000 --- a/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) [2022-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 "yast" - -# :nodoc: -module Yast - # Replacement for the Yast::Package module - # - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb - class InstFunctionsClass < Module - def main - puts "Loading mocked module #{__FILE__}" - end - - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L56 - def ignored_features - [] - end - - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L83 - def reset_ignored_features; end - - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L91 - def feature_ignored?(_feature_name) - false - end - - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L107 - def second_stage_required? - false - end - - # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L137 - def self_update_explicitly_enabled? - false - end - end - - InstFunctions = InstFunctionsClass.new - InstFunctions.main -end diff --git a/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb b/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb deleted file mode 100644 index db2f77543b..0000000000 --- a/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) [2022-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 "yast" -require "logger" -require "agama/software/callbacks" -require "agama/http/clients" - -# :nodoc: -module Yast - # Replacement for the Yast::PackageCallbacks module. - class PackageCallbacksClass < Module - def main - puts "Loading mocked module #{__FILE__}" - end - - # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L183 - def InitPackageCallbacks(logger = nil) - @logger = logger || ::Logger.new($stdout) - - Agama::Software::Callbacks::Digest.new( - questions_client, logger - ).setup - - Agama::Software::Callbacks::Media.new( - questions_client, logger - ).setup - - Agama::Software::Callbacks::Provide.new( - questions_client, logger - ).setup - - Agama::Software::Callbacks::Signature.new( - questions_client, logger - ).setup - - Agama::Software::Callbacks::Script.new( - questions_client, logger - ).setup - - Agama::Software::Callbacks::PkgGpgCheck.new( - questions_client, logger - ).setup - end - - # Returns the client to ask questions - # - # @return [Agama::DBus::Clients::Questions] - def questions_client - @questions_client ||= Agama::HTTP::Clients::Questions.new(logger) - end - - private - - # @return [Logger] - attr_reader :logger - end - - PackageCallbacks = PackageCallbacksClass.new - PackageCallbacks.main -end diff --git a/service/lib/agama/dbus/y2dir/software/modules/SpaceCalculation.rb b/service/lib/agama/dbus/y2dir/software/modules/SpaceCalculation.rb deleted file mode 100644 index 91055fed57..0000000000 --- a/service/lib/agama/dbus/y2dir/software/modules/SpaceCalculation.rb +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) [2022] 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 "yast" - -# :nodoc: -module Yast - # Replacement for the Yast::SpaceCalculation module. - class SpaceCalculationClass < Module - def main - puts "Loading mocked module #{__FILE__}" - end - - # @see https://github.com/yast/yast-packager/blob/master/src/modules/SpaceCalculation.rb#L711 - def GetPartitionInfo; end - - # @see https://github.com/yast/yast-packager/blob/master/src/modules/SpaceCalculation.rb#L860 - def CheckDiskSize; true; end - - # @see https://github.com/yast/yast-packager/blob/master/src/modules/SpaceCalculation.rb#L894 - def CheckDiskFreeSpace(*_args); []; end - - # @see https://github.com/yast/yast-packager/blob/master/src/modules/SpaceCalculation.rb#L60 - def GetFailedMounts - [] - end - end - - SpaceCalculation = SpaceCalculationClass.new - SpaceCalculation.main -end diff --git a/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb b/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb deleted file mode 120000 index 42c5d80092..0000000000 --- a/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb +++ /dev/null @@ -1 +0,0 @@ -../../modules/InstFunctions.rb \ No newline at end of file diff --git a/service/lib/agama/dbus/y2dir/storage/modules/Package.rb b/service/lib/agama/dbus/y2dir/storage/modules/Package.rb deleted file mode 120000 index c56312372d..0000000000 --- a/service/lib/agama/dbus/y2dir/storage/modules/Package.rb +++ /dev/null @@ -1 +0,0 @@ -../../manager/modules/Package.rb \ No newline at end of file diff --git a/service/lib/agama/dbus/y2dir/storage/modules/PackagesProposal.rb b/service/lib/agama/dbus/y2dir/storage/modules/PackagesProposal.rb deleted file mode 120000 index e496394bb7..0000000000 --- a/service/lib/agama/dbus/y2dir/storage/modules/PackagesProposal.rb +++ /dev/null @@ -1 +0,0 @@ -../../manager/modules/PackagesProposal.rb \ No newline at end of file diff --git a/service/lib/agama/errors.rb b/service/lib/agama/errors.rb deleted file mode 100644 index 8fb2d21959..0000000000 --- a/service/lib/agama/errors.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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. - -module Agama - # Module containing common errors - 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 diff --git a/service/lib/agama/http/clients.rb b/service/lib/agama/http/clients.rb index 5c7b765755..b3396525d8 100644 --- a/service/lib/agama/http/clients.rb +++ b/service/lib/agama/http/clients.rb @@ -28,9 +28,5 @@ module Clients end require "agama/http/clients/base" -require "agama/http/clients/files" require "agama/http/clients/main" -require "agama/http/clients/network" require "agama/http/clients/questions" -require "agama/http/clients/scripts" -require "agama/http/clients/software" diff --git a/service/lib/agama/http/clients/files.rb b/service/lib/agama/http/clients/files.rb deleted file mode 100644 index 55f2c08b82..0000000000 --- a/service/lib/agama/http/clients/files.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "agama/http/clients/base" - -module Agama - module HTTP - module Clients - # HTTP client to interact with the files API. - class Files < Base - # writes additional files - def write - post("files/write", nil) - end - end - end - end -end diff --git a/service/lib/agama/http/clients/main.rb b/service/lib/agama/http/clients/main.rb index 600bef871c..daa04cc0b1 100644 --- a/service/lib/agama/http/clients/main.rb +++ b/service/lib/agama/http/clients/main.rb @@ -37,7 +37,7 @@ def install # @param resolvables [Array] Resolvables names. def set_resolvables(unique_id, type, resolvables) data = resolvables.map do |name| - { "name" => name, "type" => type } + { "name" => name, "type" => type.to_s } end put("v2/private/resolvables/#{unique_id}", data) end diff --git a/service/lib/agama/http/clients/network.rb b/service/lib/agama/http/clients/network.rb deleted file mode 100644 index ad98ab1d0d..0000000000 --- a/service/lib/agama/http/clients/network.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "agama/http/clients/base" - -module Agama - module HTTP - module Clients - # HTTP client to interact with the network API. - class Network < Base - def connections - JSON.parse(get("network/connections")) - end - - def devices - JSON.parse(get("network/devices")) - end - - def persist_connections - post("network/connections/persist", { value: true }) - end - - def state - JSON.parse(get("network/state")) - end - end - end - end -end diff --git a/service/lib/agama/http/clients/scripts.rb b/service/lib/agama/http/clients/scripts.rb deleted file mode 100644 index 773b714db4..0000000000 --- a/service/lib/agama/http/clients/scripts.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] 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 "agama/http/clients/base" - -module Agama - module HTTP - module Clients - # HTTP client to interact with the scripts API. - class Scripts < Base - # Runs the scripts - def run(group) - post("scripts/run", group) - end - end - end - end -end diff --git a/service/lib/agama/http/clients/software.rb b/service/lib/agama/http/clients/software.rb deleted file mode 100644 index caa1ba7b18..0000000000 --- a/service/lib/agama/http/clients/software.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "agama/http/clients/base" - -module Agama - module HTTP - module Clients - # HTTP client to interact with the software API. - class Software < Base - def products - JSON.parse(get("software/products")) - end - - def proposal - JSON.parse(get("software/proposal")) - end - - def probe - post("software/probe", nil) - end - - def propose - # it is noop, probe already do proposal - # post("software/propose", nil) - end - - def install - http = Net::HTTP.new("localhost") - # FIXME: we need to improve it as it can e.g. wait for user interaction. - http.read_timeout = 3 * 60 * 60 # set timeout to three hours for rpm installation - response = http.post("/api/software/install", "", headers) - - return unless response.is_a?(Net::HTTPClientError) - - @logger.warn "server returned #{response.code} with body: #{response.body}" - end - - def finish - post("software/finish", nil) - end - - def locale=(value) - # TODO: implement it - post("software/locale", value) - end - - def config - JSON.parse(get("v2/config")) - end - - def selected_product - config.dig("product", "id") - end - - def errors? - # TODO: severity as integer is nasty for http API - JSON.parse(get("software/issues/software"))&.select { |i| i["severity"] == 1 }&.any? - end - - def get_resolvables(unique_id, type, optional) - JSON.parse(get("software/resolvables/#{unique_id}?type=#{type}&optional=#{optional}")) - end - - # (Yes, with a question mark. Bad naming.) - # @return [Array] Those names that are selected for installation - def provisions_selected?(provisions) - provisions.select do |prov| - package_installed?(prov) - end - end - - def package_available?(_name) - JSON.parse(get("software/available?tag=#{name}")) - end - - def package_installed?(name) - JSON.parse(get("software/selected?tag=#{name}")) - end - - def set_resolvables(unique_id, type, resolvables, optional) - data = { - "names" => resolvables, - "type" => type, - "optional" => optional - } - put("software/resolvables/#{unique_id}", data) - end - - def add_patterns(patterns) - config_patterns = config["patterns"] || {} - selected = config_patterns.select { |_k, v| v }.keys - modified = false - - patterns.each do |pattern| - unless selected.include?(pattern) - config_patterns[pattern] = true - modified = true - end - end - return unless modified - - put("software/config", { "patterns" => config_patterns }) - end - - def on_probe_finished(&block) - # TODO: it was agreed to change this storage observation to have the code - # in rust part and call via dbus ruby part - end - end - end - end -end diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb deleted file mode 100644 index 9f69447aa0..0000000000 --- a/service/lib/agama/manager.rb +++ /dev/null @@ -1,334 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "shellwords" - -require "yast" -require "agama/config" -require "agama/network" -require "agama/proxy_setup" -require "agama/with_locale" -require "agama/with_progress_manager" -require "agama/installation_phase" -require "agama/service_status_recorder" -require "agama/dbus/service_status" -require "agama/http/clients/software" -require "agama/dbus/clients/storage" -require "agama/helpers" -require "agama/http" -require "agama/ipmi" - -Yast.import "Stage" - -module Agama - # This class represents the top level installer manager. - # - # It is responsible for orchestrating the installation process. For module - # specific stuff it delegates it to the corresponding module class (e.g., - # {Agama::Network}, {Agama::Storage::Proposal}, etc.) or asks - # other services via HTTP (e.g., `/software`). - class Manager - include WithProgressManager - include WithLocale - include Helpers - include Yast::I18n - - # @return [Logger] - attr_reader :logger - - # @return [InstallationPhase] - attr_reader :installation_phase - - # @return [DBus::ServiceStatus] - attr_reader :service_status - - # Constructor - # - # @param config [Agama::Config] - # @param logger [Logger] - def initialize(config, logger) - textdomain "agama" - - @config = config - @logger = logger - @installation_phase = InstallationPhase.new - @service_status_recorder = ServiceStatusRecorder.new - @service_status = DBus::ServiceStatus.new.busy - @ipmi = Ipmi.new(logger) - - on_progress_change { logger.info progress.to_s } - end - - # Runs the startup phase - def startup_phase - service_status.busy - installation_phase.startup - # FIXME: hot-fix for decision taken at bsc#1224868 (RC1) - network.startup - config_phase if software.config.dig("product", "id") - - logger.info("Startup phase done") - service_status.idle - end - - # Runs the config phase - # - # @param reprobe [Boolean] Whether a reprobe should be done instead of a probe. - def config_phase(reprobe: false) - installation_phase.config - start_progress_with_descriptions(_("Analyze disks"), _("Configure software")) - progress.step { configure_storage(reprobe) } - progress.step { software.probe } - - logger.info("Config phase done") - rescue StandardError => e - logger.error "Startup error: #{e.inspect}. Backtrace: #{e.backtrace}" - # TODO: report errors - ensure - finish_progress - end - - # Runs the install phase - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def install_phase - @ipmi.started - - installation_phase.install - start_progress_with_descriptions( - _("Prepare disks"), - _("Install software"), - _("Configure the system") - ) - - Yast::Installation.destdir = "/mnt" - - progress.step do - storage.install - run_post_partitioning_scripts - proxy.propose - # propose software after /mnt is already separated, so it uses proper - # target - software.propose - end - - progress.step { software.install } - progress.step do - on_target do - users.write - network.install - http_client.install - software.finish - storage.finish - end - end - - @ipmi.finished - - logger.info("Install phase done") - rescue StandardError => e - @ipmi.failed - logger.error "Installation error: #{e.inspect}. Backtrace: #{e.backtrace}" - ensure - installation_phase.finish - finish_progress - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - - def locale=(locale) - change_process_locale(locale) - users.update_issues - start_progress_with_descriptions( - _("Load software translations"), - _("Load storage translations") - ) - progress.step { software.locale = locale } - progress.step { storage.locale = locale } - ensure - finish_progress - end - - # Software client - # - # @return [DBus::Clients::Software] - def software - @software ||= HTTP::Clients::Software.new(logger) - # TODO: watch for http websocket events regarding software status - # software.tap do |client| - # client.on_service_status_change do |status| - # service_status_recorder.save(client.service.name, status) - # end - # end - end - - # ProxySetup instance - # - # @return [ProxySetup] - def proxy - ProxySetup.instance - end - - # HTTP client. - # - # @return [HTTP::Clients::Base] - def http_client - @http_client ||= Agama::HTTP::Clients::Main.new(logger) - end - - # Users client - # - # @return [Agama::Users] - def users - @users ||= Users.new(logger) - end - - # Network manager - # - # @return [Network] - def network - @network ||= Network.new(logger) - end - - # Storage manager - # - # @return [DBus::Clients::Storage] - def storage - @storage ||= DBus::Clients::Storage.new - end - - # Name of busy services - # - # @see ServiceStatusRecorder - # - # @return [Array] - def busy_services - service_status_recorder.busy_services - end - - # Registers a callback to be called when the status of a service changes - # - # @see ServiceStatusRecorder - def on_services_status_change(&block) - service_status_recorder.on_service_status_change(&block) - end - - # Determines whether the configuration is valid and the system is ready for installation - # - # @return [Boolean] - def valid? - users.issues.empty? && !software.errors? - end - - # Collects the logs and stores them into an archive - # - # @param path [String] directory where to store logs - # - # @return [String] path to created archive - def collect_logs(path: nil) - opt = "-d #{path.shellescape}" unless path.nil? || path.empty? - - `agama logs store #{opt}`.strip - end - - # Whatever has to be done at the end of installation - # - # If a finish method is given it will call the related shutdown - # command. - # - # @param method [HALT, POWEROFF, STOP, REBOOT] - # @return [Boolean] - def finish_installation(method) - unless installation_phase.finish? - logger.error "The installer has not finished correctly. Please check logs" - return false - end - - if method == STOP - logger.info("Finished the installation (stop).") - return true - end - - cmd = finish_cmd(method) - logger.info("Finishing installation with '#{cmd}' (#{method})") - - !!system(cmd) - end - - # Says whether running on iguana or not - # - # @return [Boolean] true when running on iguana - def iguana? - Dir.exist?("/iguana") - end - - private - - # Possible finish methods - STOP = "stop" - REBOOT = "reboot" - HALT = "halt" - POWEROFF = "poweroff" - - # Default finish method to be called if not given or not find - DEFAULT_METHOD = "reboot" - # Finish shutdown option for each finish method - SHUTDOWN_OPT = { REBOOT => "-r", HALT => "-H", POWEROFF => "-P" }.freeze - - # Configures storage. - # - # Storage is configured as part of the config phase. The config phase is executed after - # selecting or registering a product. - # - # @param reprobe [Boolean] is used to keep the current storage config after registering a - # product, see https://github.com/agama-project/agama/pull/2532. - def configure_storage(reprobe) - # Note that probing storage is not needed after the product registration, but let's keep the - # current behavior. - return storage.probe if reprobe - - # Select the product - storage.product = software.selected_product - end - - # @param method [String, nil] - # @return [String] the cmd to be run for finishing the installation - def finish_cmd(method) - return "/usr/bin/agamactl -k" if iguana? - - opt = SHUTDOWN_OPT[method] - unless opt - log.info "Not recognized method, using the default one (reboot)." - opt = SHUTDOWN_OPT[DEFAULT_METHOD] - end - "/usr/sbin/shutdown #{opt} now" - end - - attr_reader :config - - # @return [ServiceStatusRecorder] - attr_reader :service_status_recorder - - # Runs post partitioning scripts - def run_post_partitioning_scripts - client = Agama::HTTP::Clients::Scripts.new(logger) - client.run("postPartitioning") - end - end -end diff --git a/service/lib/agama/network.rb b/service/lib/agama/network.rb deleted file mode 100644 index bd91caff30..0000000000 --- a/service/lib/agama/network.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "singleton" -require "yast" -require "yast2/systemd/service" -require "y2network/proposal_settings" -require "agama/proxy_setup" -require "agama/http" - -Yast.import "Installation" - -module Agama - # Backend class to handle network configuration - class Network - def initialize(logger) - @logger = logger - end - - def startup - persist_connections if do_proposal? - end - - # Writes the network configuration to the installed system - # - # * Copies the connections configuration for NetworkManager, as Agama is not - # performing further configuration of the network. - # * Enables the NetworkManager service. - def install - copy_files - enable_service - - ProxySetup.instance.install - end - - def link_resolv - return unless File.exist?(RESOLV) - - link = File.join(Yast::Installation.destdir, RESOLV) - target = File.join(RUN_NM_DIR, File.basename(RESOLV)) - - return if File.exist?(link) - - FileUtils.touch RESOLV_FLAG - FileUtils.ln_s target, link - end - - def unlink_resolv - return unless File.exist?(RESOLV_FLAG) - - link = File.join(Yast::Installation.destdir, RESOLV) - FileUtils.rm_f link - FileUtils.rm_f RESOLV_FLAG - end - - private - - # @return [Logger] - attr_reader :logger - - HOSTNAME = "/etc/hostname" - RESOLV = "/etc/resolv.conf" - NOT_COPY_NETWORK = "/run/agama/not_copy_network" - AGAMA_SYSTEMD_LINK = "/run/agama/systemd/network" - SYSTEMD_LINK = "/etc/systemd/network" - RESOLV_FLAG = "/run/agama/manage_resolv" - ETC_NM_DIR = "/etc/NetworkManager" - RUN_NM_DIR = "/run/NetworkManager" - private_constant :ETC_NM_DIR - - def enable_service - service = Yast2::Systemd::Service.find("NetworkManager") - if service.nil? - logger.error "NetworkManager service was not found" - return - end - - service.enable - end - - # Copies NetworkManager configuration files - def copy_files - copy(HOSTNAME) - - copy_directory( - AGAMA_SYSTEMD_LINK, - File.join(Yast::Installation.destdir, SYSTEMD_LINK) - ) - - return unless Dir.exist?(ETC_NM_DIR) - return if File.exist?(NOT_COPY_NETWORK) - - copy_directory( - File.join(ETC_NM_DIR, "system-connections"), - File.join(Yast::Installation.destdir, ETC_NM_DIR, "system-connections") - ) - end - - # Copies a directory - # - # This method checks whether the source directory exists. If preserves the target directory if - # it exists (otherwise, it creates the directory). - # - # @param source [String] source directory - # @param target [String] target directory - def copy_directory(source, target) - return unless Dir.exist?(source) - - FileUtils.mkdir_p(target) - FileUtils.cp(Dir.glob(File.join(source, "*")), target) - end - - # Copies a file - # - # This method checks whether the source file exists. It copies the file to the target system if - # it exists - # - # @param source [String] source file - # @param target [String,nil] target directory, only needed in case it is different to the - # original source path in the target system. - def copy(source, target = nil) - return unless File.exist?(source) - - path = target || File.join(Yast::Installation.destdir, source) - FileUtils.mkdir_p(File.dirname(path)) - FileUtils.copy_entry(source, path) - end - - def http_client - @http_client ||= Agama::HTTP::Clients::Network.new(logger) - end - - def persist_connections - http_client.persist_connections - end - - def copy_connections? - http_client.state["copyNetwork"] - end - - def do_proposal? - return false unless copy_connections? - return false if http_client.connections.any? { |c| c["persistent"] } - - !http_client.connections.empty? - end - end -end diff --git a/service/lib/agama/proxy_setup.rb b/service/lib/agama/proxy_setup.rb deleted file mode 100644 index 00ae9e3f88..0000000000 --- a/service/lib/agama/proxy_setup.rb +++ /dev/null @@ -1,153 +0,0 @@ -# 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 "yast" -require "uri" -require "fileutils" -require "agama/helpers" - -module Agama - # This class is responsible of parsing the proxy url from the kernel cmdline or configured - # through the dracut ask prompt configuration file (/etc/cmdline-menu.conf) during the boot - # proccess of the system writing the configuration to /etc/sysconfig/proxy - class ProxySetup - include Singleton - include Yast - include Logger - include Helpers - - CMDLINE_PATH = "/proc/cmdline" - CMDLINE_MENU_CONF = "/etc/cmdline-menu.conf" - PACKAGES = ["microos-tools"].freeze - CONFIG_PATH = "/etc/sysconfig/proxy" - PROPOSAL_ID = "network_proposal" - - # @return [URI::Generic] - attr_accessor :proxy - - alias_method :logger, :log - - # Constructor - def initialize - Yast.import "Proxy" - Yast.import "Installation" - Yast.import "PackagesProposal" - - Proxy.Read - end - - def run - read - write - end - - def propose - add_packages if Proxy.enabled - end - - def install - return unless Proxy.enabled - - on_local { copy_files } - enable_services - end - - private - - def read - self.proxy = proxy_from_cmdline || proxy_from_dracut - end - - def proxy_from_dracut - return unless File.exist?(CMDLINE_MENU_CONF) - - options = File.read(CMDLINE_MENU_CONF) - proxy_url_from(options) - end - - def proxy_url_from(options) - proxy_url = options.split.find { |o| o.start_with?(/proxy/i) } - return unless proxy_url - - URI(proxy_url.downcase.gsub("proxy=", "")) - end - - def proxy_from_cmdline - return unless File.exist?(CMDLINE_PATH) - - options = File.read(CMDLINE_PATH) - proxy_url_from(options) - end - - def proxy_import_settings - proto = proxy.scheme - # save user name and password separately - settings = { - "proxy_user" => proxy.user, - "proxy_password" => proxy.password, - "enabled" => true - } - proxy.user = nil - proxy.password = nil - - settings["#{proto}_proxy"] = proxy.to_s - # Use the proxy also for https and ftp - if proto == "http" - settings["https_proxy"] = proxy.to_s - settings["ftp_proxy"] = proxy.to_s - end - settings - end - - def write - return unless proxy - - settings = proxy_import_settings - Proxy.Import(settings) - - log.info "Writing proxy settings: #{proxy.scheme}_proxy = '#{proxy}'" - log.debug "Writing proxy settings: #{settings}" - - Proxy.Write - end - - def add_packages - log.info "Selecting these packages for installation: #{PACKAGES}" - Yast::PackagesProposal.SetResolvables(PROPOSAL_ID, :package, PACKAGES) - end - - def copy_files - log.info "Copying proxy configuration to the target system" - ::FileUtils.cp(CONFIG_PATH, File.join(Yast::Installation.destdir, CONFIG_PATH)) - end - - def enable_services - service = Yast2::Systemd::Service.find("setup-systemd-proxy-env") - if service.nil? - log.error "setup-systemd-proxy-env service was not found" - return - end - - Yast::Execute.on_target!("systemctl", "enable", "setup-systemd-proxy-env.service") - Yast::Execute.on_target!("systemctl", "enable", "setup-systemd-proxy-env.path") - end - end -end diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb deleted file mode 100644 index 4dd3e483de..0000000000 --- a/service/lib/agama/registration.rb +++ /dev/null @@ -1,541 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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 "fileutils" -require "yast" -require "ostruct" -require "suse/connect" -require "y2packager/new_repository_setup" -require "y2packager/resolvable" -require "yast2/execute" - -require "agama/cmdline_args" -require "agama/errors" -require "agama/registered_addon" -require "agama/ssl/certificate" -require "agama/ssl/errors" -require "agama/ssl/fingerprint" -require "agama/ssl/storage" - -Yast.import "Arch" -Yast.import "Pkg" - -module Agama - # Handles everything related to registration of system to SCC, RMT or similar. - class Registration - include Yast::I18n - - # NOTE: identical and keep in sync with Software::Manager::TARGET_DIR - TARGET_DIR = "/run/agama/zypp" - private_constant :TARGET_DIR - - # FIXME: it should use TARGET_DIR instead of "/", but connect failed to read it even - # if fs_root passed as client params. Check with SCC guys why. - GLOBAL_CREDENTIALS_PATH = File.join("/", - SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) - private_constant :GLOBAL_CREDENTIALS_PATH - - DEFAULT_REGISTRATION_URL = "https://scc.suse.com" - private_constant :DEFAULT_REGISTRATION_URL - - # Code used for registering the product. - # - # @return [boolean] true if base product is already registered - attr_reader :registered - - # Code used for registering the product. - # - # @return [String, nil] nil if the product is not registered yet. - attr_reader :reg_code - - # Email used for registering the product. - # - # @return [String, nil] - attr_reader :email - - # List of already registered addons - # - # @return [Array] - attr_reader :registered_addons - - # Overwritten default registration URL - # - # @return [String, nil] - attr_accessor :registration_url - - # @param software_manager [Agama::Software::Manager] - # @param logger [Logger] - def initialize(software_manager, logger) - @software = software_manager - @logger = logger - @services = [] - @credentials_files = [] - @registered_addons = [] - @registration_url = registration_url_from_cmdline - @registered = false - end - - # Registers the selected product. - # - # @raise [ - # SocketError|Timeout::Error|SUSE::Connect::ApiError| - # SUSE::Connect::MissingSccCredentialsFile|SUSE::Connect::MissingSccCredentialsFile| - # OpenSSL::SSL::SSLError|JSON::ParserError|Agama::Software::AddServiceError - # ] - # - # @param code [String] Registration code. - # @param email [String] Email for registering the product. - def register(code, email: "") - return if product.nil? || registered - - catch_registration_errors do - reg_params = connect_params(token: code, email: email) - - # hide the registration code in the logs - log_params = reg_params.dup - log_params[:token] = "" if log_params[:token] != "" - @logger.info "Registering system #{target_distro}: #{log_params.inspect}" - - @login, @password = SUSE::Connect::YaST.announce_system(reg_params, target_distro) - # write the global credentials - # TODO: check if we can do it in memory for libzypp - SUSE::Connect::YaST.create_credentials_file(@login, @password, GLOBAL_CREDENTIALS_PATH) - - activate_params = {} - @logger.info "Activating product #{base_target_product.inspect}" - service = SUSE::Connect::YaST.activate_product(base_target_product, activate_params, email) - process_service(service) - @registered = true - end - ensure - @reg_code = code - @email = email - run_on_change_callbacks - end - - def register_addon(name, version, code) - catch_registration_errors do - 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}-#{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: 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, register_version, !version.empty?, code) - # select the products to install - @software.addon_products(find_addon_products) - - run_on_change_callbacks - end - end - - # Deregisters the selected product. - # - # It uses the registration code and email passed to {#register}. - # - # @raise [ - # SocketError|Timeout::Error|SUSE::Connect::ApiError| - # SUSE::Connect::MissingSccCredentialsFile|SUSE::Connect::MissingSccCredentialsFile| - # OpenSSL::SSL::SSLError|JSON::ParserError - # ] - def deregister - return unless registered - - @services.each do |service| - Y2Packager::NewRepositorySetup.instance.services.delete(service.name) - @software.remove_service(service) - end - - # reset - @software.addon_products([]) - @services = [] - @available_addons = nil - - reg_params = connect_params(token: reg_code, email: email) - SUSE::Connect::YaST.deactivate_system(reg_params) - FileUtils.rm(GLOBAL_CREDENTIALS_PATH) # connect does not remove it itself - @credentials_files.each do |credentials_file| - FileUtils.rm(File.join(TARGET_DIR, credentials_path(credentials_file))) - end - @credentials_files = [] - - @registered = false - @reg_code = nil - @email = nil - @registered_addons = [] - - run_on_change_callbacks - end - - # Copies configuration and credentials files to the target system. - # - # The configuration file is copied only if a registration URL was given. - def finish - return unless registered - - files = [[ - GLOBAL_CREDENTIALS_PATH, File.join(Yast::Installation.destdir, GLOBAL_CREDENTIALS_PATH) - ]] - @credentials_files.each do |credentials_file| - files << [ - File.join(TARGET_DIR, credentials_path(credentials_file)), - File.join(Yast::Installation.destdir, credentials_path(credentials_file)) - ] - end - - if registration_url - SUSE::Connect::YaST.write_config("url" => registration_url) - files << [ - SUSE::Connect::Config::DEFAULT_CONFIG_FILE, - File.join(Yast::Installation.destdir, SUSE::Connect::Config::DEFAULT_CONFIG_FILE) - ] - end - - files.each do |src_dest| - FileUtils.cp(*src_dest) - end - - copy_certificates_to_target - 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 - - # Callbacks to be called when registration changes (e.g., a different product is selected). - def on_change(&block) - @on_change_callbacks ||= [] - @on_change_callbacks << block - end - - private - - # @return [Agama::Software::Manager] - attr_reader :software - - # Currently selected product. - # - # @return [Agama::Software::Product, nil] - def product - software.product - end - - def copy_certificates_to_target - cert_file = SSL::Certificate.default_certificate_path - return unless File.exist?(cert_file) # no certificate imported? - - # copy the imported certificate - @logger.info "Copying SSL certificate (#{cert_file}) to the target system..." - cert_target_file = File.join("/mnt", - SUSE::Connect::YaST::SERVER_CERT_FILE) - ::FileUtils.mkdir_p(File.dirname(cert_target_file)) - ::FileUtils.cp(cert_file, cert_target_file) - - # update the certificate links - cmd = SUSE::Connect::YaST::UPDATE_CERTIFICATES - @logger.info "Updating certificate links (#{cmd})..." - # beware that Yast::Execute.on_target! does not work here due - # to not changed SCR root when registration finish is executed. - # So do chroot explicitelly. - Yast::Execute.locally!(cmd, chroot: "/mnt") - end - - # Product name expected by SCC. - # - # @return [String] E.g., "ALP-Dolomite-1-x86_64". - def target_distro - v = product.version.to_s.split(".").first || "1" - "#{product.id}-#{v}-#{Yast::Arch.rpm_arch}" - end - - def run_on_change_callbacks - @on_change_callbacks&.map(&:call) - end - - # taken from https://github.com/yast/yast-registration/blob/master/src/lib/registration/url_helpers.rb#L109 - def credentials_from_url(url) - parsed_url = URI(url) - params = URI.decode_www_form(parsed_url.query).to_h - - params["credentials"] - rescue StandardError - # if something goes wrong try to continue like if there is no credentials param - nil - end - - def credentials_path(file) - File.join(SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR, file) - end - - # Returns the arguments to connect to the registration server - # - # @param params [Hash] additional parameters (e.g., email and token) - # @return [Hash] - def connect_params(params = {}) - default_params = {} - default_params[:language] = http_language if http_language - default_params[:url] = registration_url || DEFAULT_REGISTRATION_URL - default_params[:verify_callback] = verify_callback - default_params.merge(params) - end - - def http_language - lang = Yast::WFM.GetLanguage - return nil if ["POSIX", "C"].include?(lang) - - # remove the encoding suffix (e.g. ".UTF-8") - lang = lang.sub(/\..*$/, "") - - # replace Linux locale separator "_" by the HTTP separator "-", downcase the country name - # see https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language - lang.tr!("_", "-") - lang.downcase! - - lang - end - - # returns SSL verify callback - def verify_callback - lambda do |verify_ok, context| - - # we cannot raise an exception with details here (all exceptions in - # verify_callback are caught and ignored), we need to store the error - # details in a global instance - store_ssl_error(context) unless verify_ok - - verify_ok - rescue StandardError => e - @logger.error "Exception in SSL verify callback: #{e.class}: #{e.message} : #{e.backtrace}" - # the exception will be ignored, but reraise anyway... - raise e - - end - end - - def store_ssl_error(context) - @logger.error "SSL verification failed: #{context.error}: #{context.error_string}" - SSL::Errors.instance.ssl_error_code = context.error - SSL::Errors.instance.ssl_error_msg = context.error_string - SSL::Errors.instance.ssl_failed_cert = - context.current_cert ? SSL::Certificate.load(context.current_cert) : nil - end - - def catch_registration_errors(&block) - # import the SSL certificate just once to avoid an infinite loop - certificate_imported = false - begin - # reset the previous SSL errors - Agama::SSL::Errors.instance.reset - - block.call - - true - rescue OpenSSL::SSL::SSLError => e - @logger.error "OpenSSL error: #{e}" - should_retry = handle_ssl_error(e, certificate_imported) - puts "handle ssl error #{should_retry}" - if should_retry - certificate_imported = true - SSL::Errors.instance.ssl_failed_cert.import - retry - end - raise e - end - end - - # @return [Boolean] - def handle_ssl_error(_error, certificate_imported) - return false if certificate_imported - - cert = SSL::Errors.instance.ssl_failed_cert - return false unless cert - - # Import certificate if it matches predefined fingerprint. - return true if SSL::Storage.instance.fingerprints.any? { |f| cert.match_fingerprint?(f) } - - error_code = SSL::Errors.instance.ssl_error_code - return false unless SSL::ErrorCodes::IMPORT_ERROR_CODES.include?(error_code) - - question = certificate_question(cert) - questions_client = Agama::HTTP::Clients::Questions.new(@logger) - questions_client.ask(question) { |a| a.action == :trust } - end - - # @param certificate [Agama::SSL::Certificate] - # @return [Agama::Question] - def certificate_question(certificate) - question_data = { - "url" => registration_url || DEFAULT_REGISTRATION_URL, - "issuer_name" => certificate.issuer_name, - "issue_date" => certificate.issued_on, - "expiration_date" => certificate.expires_on, - "sha1_fingerprint" => certificate.fingerprint(SSL::Fingerprint::SHA1).value, - "sha256_fingerprint" => certificate.fingerprint(SSL::Fingerprint::SHA256).value - }.filter { |_, v| !v.nil? } - - question_text = _( - "Trying to import a self signed certificate. Do you want to trust it and register the " \ - "product?" - ) - - Agama::Question.new( - qclass: "registration.certificate", - text: question_text, - options: [:trust, :reject], - default_option: :reject, - data: question_data - ) - end - - # Returns the URL of the registration server - # - # At this point, it just checks the kernel's command-line. - # - # @return [String, nil] - def registration_url_from_cmdline - cmdline_args = CmdlineArgs.read - cmdline_args.data["register_url"] - end - - # process a newly added service, create the credentials file and add the service to libzypp - def process_service(service) - @services << service - credentials_file = credentials_from_url(service.url) - if credentials_file - @credentials_files << credentials_file - # addons use the same SCC credentials as the base product - SUSE::Connect::YaST.create_credentials_file(@login, @password, - File.join(TARGET_DIR, credentials_path(credentials_file))) - end - Y2Packager::NewRepositorySetup.instance.add_service(service.name) - @software.add_service(service) - end - - # Find all addon products - # - # @return [Array] names of the products - def find_addon_products - # find all repositories for registered addons (their services) - addon_repos = @services.reduce([]) do |acc, service| - # skip the first service, it belongs to the base product - next acc if service == @services.first - - acc.concat(service_repos(service)) - end - - # find all products from those repositories - products = Y2Packager::Resolvable.find(kind: :product) - products.select! do |product| - addon_repos.any? { |addon_repo| product.source == addon_repo["SrcId"] } - end - - products.map!(&:name) - @logger.info "Addon products to install: #{products}" - - products - end - - # Find all repositories belonging to a service. - # - # @param product_service [OpenStruct] repository service from suseconnect - # - # @return [Array] repository data as returned by the Pkg.SourceGeneralData - # call, additionally with the "SrcId" key - def service_repos(product_service) - @logger.info "product_service: #{product_service.inspect}" - repo_data = Yast::Pkg.SourceGetCurrent(false).map { |repo| repository_data(repo) } - - service_name = product_service.name - # select only repositories belonging to the product services - repos = repo_data.select { |repo| service_name == repo["service"] } - @logger.info "Service #{service_name.inspect} repositories: #{repos}" - - repos - end - - # Get repository data - # @param [Fixnum] repo repository ID - # @return [Hash] repository properties, including the repository ID ("SrcId" key) - def repository_data(repo) - data = Yast::Pkg.SourceGeneralData(repo) - data["SrcId"] = repo - data - 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 diff --git a/service/lib/agama/security.rb b/service/lib/agama/security.rb deleted file mode 100644 index 0f6d7af9e6..0000000000 --- a/service/lib/agama/security.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "yast" -require "y2security/lsm" -require "yast2/execute" -require "agama/config" -require "agama/http" - -Yast.import "Bootloader" - -# FIXME: monkey patching of security config to not read control.xml and -# instead use Agama::Config -# TODO: add ability to set product features in LSM::Base -module Y2Security - module LSM - # modified LSM Base class to use Agama config - class Base - def product_feature_settings - return @product_feature_settings unless @product_feature_settings.nil? - - value = ::Agama::Config.current.data["security"]["available_lsms"][id.to_s] - res = if value - { - selectable: true, - configurable: true, - patterns: (value["patterns"] || []).join(" "), - mode: value["policy"] - } - else - { - selectable: false, - configurable: false, - patterns: "", - mode: nil - } - end - @product_feature_settings = res - end - end - end -end - -module Agama - # Backend class between dbus service and yast code - class Security - # @return [Logger] - attr_reader :logger - - # Constructor - # - # @param logger [Logger] - # @param config [Agama::Config] - def initialize(logger, config) - @config = config - @logger = logger - end - - def write - # at first clear previous kernel params - selected = lsm_selected - selected&.reset_kernel_params - - candidate = select_software_lsm - return unless candidate - - lsm_config.select(candidate) - kernel_params = lsm_selected.kernel_params - # write manually here to bootloader as lsm_config.save do more than agama wants (bsc#1247046) - @logger.info("Modifying Bootlooader kernel params using #{kernel_params}") - Yast::Bootloader.modify_kernel_params(kernel_params) - end - - private - - attr_reader :config - - def select_software_lsm - candidates = [lsm_selected&.id&.to_s].compact | available_lsms.keys - - candidates.find { |c| proposal_patterns_include?(c) } - end - - def available_lsms - config.data.dig("security", "available_lsms") || {} - end - - def proposal_patterns_include?(lsm_id) - patterns = available_lsms.dig(lsm_id.to_s, "patterns") || [] - - (patterns - proposal_patterns).empty? - end - - def lsm_config - Y2Security::LSM::Config.instance - end - - def lsm_selected - lsm_config.selected - end - - def lsm_patterns(lsm_id) - config.data.dig("security", "available_lsms", lsm_id.to_s, "patterns") || [] - end - - def proposal_patterns - return @proposal_patterns if @proposal_patterns - - proposal = software_client.proposal || {} - - @proposal_patterns = - (proposal["patterns"] || {}).select { |_p, v| [0, 1].include? v }.keys - end - - # Returns the client to ask the software service - # - # @return [Agama::HTTP::Clients::Software] - def software_client - @software_client ||= Agama::HTTP::Clients::Software.new(logger) - end - end -end diff --git a/service/lib/agama/software.rb b/service/lib/agama/software.rb deleted file mode 100644 index cc38ef8194..0000000000 --- a/service/lib/agama/software.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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. - -module Agama - # Namespace for software backend - module Software - end -end - -require "agama/software/manager" diff --git a/service/lib/agama/software/callbacks.rb b/service/lib/agama/software/callbacks.rb deleted file mode 100644 index 659a021f47..0000000000 --- a/service/lib/agama/software/callbacks.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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. - -module Agama - module Software - # Namespace for software callbacks - module Callbacks - end - end -end - -require "agama/software/callbacks/digest" -require "agama/software/callbacks/media" -require "agama/software/callbacks/pkg_gpg_check" -require "agama/software/callbacks/progress" -require "agama/software/callbacks/provide" -require "agama/software/callbacks/script" -require "agama/software/callbacks/signature" diff --git a/service/lib/agama/software/callbacks/base.rb b/service/lib/agama/software/callbacks/base.rb deleted file mode 100644 index fa6a26199d..0000000000 --- a/service/lib/agama/software/callbacks/base.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "yast" - -module Agama - module Software - module Callbacks - # Base class for libzypp callbacks for sharing some common texts. - class Base - include Yast::I18n - - # Constructor - # - # @param questions_client [Agama::HTTP::Clients::Questions] - # @param logger [Logger] - def initialize(questions_client, logger) - textdomain "agama" - @questions_client = questions_client - @logger = logger || ::Logger.new($stdout) - end - - def setup - raise NotImplementedError - end - - # label for the "retry" action - def retry_label - # TRANSLATORS: button label, try downloading the failed package again - _("Try again") - end - - # label for the "continue" action - def continue_label - # TRANSLATORS: button label, ignore the failed download, skip package installation - _("Continue anyway") - end - - # label for the "abort" action - def abort_label - # TRANSLATORS: button label, abort the installation completely after an error - _("Abort installation") - end - - # label for the "skip" action - def skip_label - # TRANSLATORS: button label, skip the error - _("Skip") - end - - # label for the "yes" action - def yes_label - # TRANSLATORS: button label - _("Yes") - end - - # label for the "no" action - def no_label - # TRANSLATORS: button label - _("No") - end - - private - - # @return [Agama::HTTP::Clients::Questions] - attr_reader :questions_client - - # @return [Logger] - attr_reader :logger - end - end - end -end diff --git a/service/lib/agama/software/callbacks/digest.rb b/service/lib/agama/software/callbacks/digest.rb deleted file mode 100644 index d36f9157b0..0000000000 --- a/service/lib/agama/software/callbacks/digest.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "yast" -require "agama/question" -require "agama/software/callbacks/base" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # Callbacks related to digest handling - class Digest < Base - def setup - Yast::Pkg.CallbackAcceptFileWithoutChecksum( - Yast::FunRef.new( - method(:accept_file_without_checksum), "boolean (string)" - ) - ) - Yast::Pkg.CallbackAcceptUnknownDigest( - Yast::FunRef.new( - method(:accept_unknown_digest), "boolean (string, string)" - ) - ) - Yast::Pkg.CallbackAcceptWrongDigest( - Yast::FunRef.new( - method(:accept_wrong_digest), "boolean (string, string, string)" - ) - ) - end - - # Callback to accept a file without a checksum - # - # @param filename [String] File name - # @return [Boolean] - def accept_file_without_checksum(filename) - name = strip_download_prefix(filename) - message = format( - _( - "No checksum for the file %{file} was found in the repository. This means that " \ - "although the file is part of the signed repository, the list of checksums " \ - "does not mention this file. Use it anyway?" - ), file: name - ) - - question = Agama::Question.new( - qclass: "software.digest.no_digest", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: yes_label.to_sym - ) - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - # Callback to accept an unknown digest - # - # @param filename [String] File name - # @param digest [String] expected checksum - # @return [Boolean] - def accept_unknown_digest(filename, digest) - name = strip_download_prefix(filename) - message = format( - _( - "The checksum of the file %{file} is \"%{digest}\" but the expected checksum is " \ - "unknown. This means that the origin and integrity of the file cannot be verified. " \ - "Use it anyway?" - ), file: name, digest: digest - ) - - question = Agama::Question.new( - qclass: "software.digest.unknown_digest", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: yes_label.to_sym - ) - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - # Callback to accept wrong digest - # - # @param filename [String] File name - # @param expected_digest [String] expected checksum - # @param found_digest [String] found checksum - # @return [Boolean] - def accept_wrong_digest(filename, expected_digest, found_digest) - name = strip_download_prefix(filename) - message = format( - _( - "The expected checksum of file %{file} is \"%{found}\" but it was expected to be " \ - "\"%{expected}\". The file has changed by accident or by an attacker since the " \ - "creater signed it. Use it anyway?" - ), file: name, found: found_digest, expected: expected_digest - ) - - question = Agama::Question.new( - qclass: "software.digest.unknown_digest", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: yes_label.to_sym - ) - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - private - - # helper to strip download path. It uses internal knowledge that download - # prefix ends in TmpDir.* zypp location - # - # From https://github.com/yast/yast-yast2/blob/master/library/packages/src/modules/SignatureCheckDialogs.rb#L836 - def strip_download_prefix(path) - path.sub(/\A\/.*\/TmpDir\.[^\/]+\//, "") - end - end - end - end -end diff --git a/service/lib/agama/software/callbacks/media.rb b/service/lib/agama/software/callbacks/media.rb deleted file mode 100644 index f0cf972641..0000000000 --- a/service/lib/agama/software/callbacks/media.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2021-2025] 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 "logger" -require "yast" -require "agama/question" -require "agama/software/callbacks/base" -require "agama/software/repository" - -Yast.import "Pkg" -Yast.import "URL" - -module Agama - module Software - module Callbacks - # Callbacks related to media handling - class Media < Base - def initialize(questions_client, logger) - super - # retry counter - self.attempt = 0 - end - - # Register the callbacks - def setup - Yast::Pkg.CallbackMediaChange( - Yast::FunRef.new( - method(:media_change), - "string (string, string, string, string, integer, string, integer, string, " \ - "boolean, list , integer)" - ) - ) - Yast::Pkg.CallbackStartProvide( - Yast::FunRef.new(method(:start_provide), "void (string, integer, boolean)") - ) - end - - # @param name [String] name of the package to download - # @param size [Integer] download size - # @param _remote [Boolean] true if the package is downloaded from a remote repository, - # false for local packages - def start_provide(name, size, _remote) - self.attempt = 1 - logger.debug("Downloading #{name}, size: #{size}") - end - - # Media change callback - # - # @return [String] - # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 - # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength - def media_change(error_code, error, url, product, current, current_label, wanted, - wanted_label, double_sided, devices, current_device) - logger.debug( - format("MediaChange callback: error_code: %s, error: %s, url: %s, product: %s, " \ - "current: %s, current_label: %s, wanted: %s, wanted_label: %s, " \ - "double_sided: %s, devices: %s, current_device: %s", - error_code, - error, - Yast::URL.HidePassword(url), - product, - current, - current_label, - wanted, - wanted_label, - double_sided, - devices, - current_device) - ) - - # "IO" = IO error (scratched DVD or HW failure) - # "IO_SOFT" = network timeout - # in other cases automatic retry usually does not make much sense - if ["IO", "IO_SOFT"].include?(error_code) && attempt <= Repository::RETRY_COUNT - self.attempt += 1 - logger.debug("Retry in #{Repository::RETRY_DELAY} seconds, attempt #{attempt}...") - sleep(Repository::RETRY_DELAY) - - # retry - return "" - end - - question = Agama::Question.new( - qclass: "software.package_error.medium_error", - text: error, - options: [retry_label.to_sym, continue_label.to_sym], - data: { "url" => url } - ) - questions_client.ask(question) do |answer| - if answer.action == retry_label.to_sym - self.attempt += 1 - "" - else - "S" - end - end - end - # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength - - private - - attr_accessor :attempt - end - end - end -end diff --git a/service/lib/agama/software/callbacks/pkg_gpg_check.rb b/service/lib/agama/software/callbacks/pkg_gpg_check.rb deleted file mode 100644 index 8c0537fb92..0000000000 --- a/service/lib/agama/software/callbacks/pkg_gpg_check.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "logger" -require "yast" -require "agama/cmdline_args" -require "agama/question" -require "agama/software/manager" -require "agama/software/callbacks/base" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # Provide callbacks - class PkgGpgCheck < Base - # https://github.com/openSUSE/libzypp/blob/6b385649d18269fcba8d80ed356adb8100be920d/zypp/target/rpm/RpmDb.h#L378-L384 - CHK_OK = 0 # Signature is OK - CHK_NOTFOUND = 1 # Signature is unknown type - CHK_FAIL = 2 # Signature does not verify - CHK_NOTTRUSTED = 3 # Signature is OK, but key is not trusted - CHK_NOKEY = 4 # Public key is unavailable - CHK_ERROR = 5 # File does not exist or can't be opened - CHK_NOSIG = 6 # File has no gpg signature - - # Register the callbacks - def setup - Yast::Pkg.CallbackPkgGpgCheck( - Yast::FunRef.new(method(:pkg_gpg_check), "string(map)") - ) - end - - # Package GPG check callback - # - # @param data [Hash] callback data, see - # https://github.com/yast/yast-pkg-bindings/blob/853496f527543e6d51730fd7e3126ad94b13c303/src/Callbacks.cc#L739 - # @return [String] "I" for ignore, "R" for retry and "A" for abort, - # empty string ("") means no decision has been made - # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 - def pkg_gpg_check(data) - error_code = data["CheckPackageResult"] - package = data["Package"] - - if error_code == CHK_OK - logger.debug "GPG check succeeded for package #{package}" - return "" - end - - logger.warn "GPG check failed for package #{package}, error code: #{error_code}" - - # ignore the error when the package comes from the DUD repository and - # the DUD package GPG checks are disabled via a boot option - if data["RepoMediaUrl"] == Agama::Software::Manager.dud_repository_url && - ignore_dud_packages_gpg_errors? - - logger.info "Ignoring the GPG check failure for a DUD package" - return "I" - end - - # no decision made, the error will be reported by the DoneProvide callback again - "" - end - - private - - # Should be the DUD packages GPG signatures verified? The GPG errors can - # be ignored by using the "inst.dud_packages.gpg=0" boot option - # - # @return [Boolean] `true` if the GPG errors should be ignored, `false` otherwise - def ignore_dud_packages_gpg_errors? - return @ignore_dud_packages_gpg_errors unless @ignore_dud_packages_gpg_errors.nil? - - cmdline_args = CmdlineArgs.read - dud = cmdline_args.data["dud_packages"] - gpg = dud && dud["gpg"] - - @ignore_dud_packages_gpg_errors = [false, "0"].include?(gpg) - end - end - end - end -end diff --git a/service/lib/agama/software/callbacks/progress.rb b/service/lib/agama/software/callbacks/progress.rb deleted file mode 100644 index ce23409ca1..0000000000 --- a/service/lib/agama/software/callbacks/progress.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2021-2025] 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 "logger" -require "yast" -require "agama/question" -require "agama/http/clients" -require "agama/software/callbacks/base" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # This class represents the installer status - class Progress < Base - class << self - def setup(pkg_count, progress, logger) - new(pkg_count, progress, logger).setup - end - end - - def initialize(pkg_count, progress, logger) - super(questions_client, logger) - - textdomain "agama" - - @total = pkg_count - @installed = 0 - @progress = progress - @logger = logger || ::Logger.new($stdout) - end - - def setup - Yast::Pkg.CallbackStartPackage( - Yast::FunRef.new( - method(:start_package), "void (string, string, string, integer, boolean)" - ) - ) - - Yast::Pkg.CallbackDonePackage( - Yast::FunRef.new( - method(:done_package), "string (integer, string)" - ) - ) - end - - private - - # @return [Agama::Progress] - attr_reader :progress - - # @return [String,nil] - attr_accessor :current_package - - # @return [Logger] - attr_reader :logger - - # @return [Agama::HTTP::Clients::Questions] - def questions_client - @questions_client ||= Agama::HTTP::Clients::Questions.new(logger) - end - - def start_package(package, _file, _summary, _size, _other) - progress.step("Installing #{package}") - self.current_package = package - end - - def done_package(error_code, description) - return "" if error_code == 0 - - logger.error("Package #{current_package} failed: #{description}") - - question = Agama::Question.new( - qclass: "software.package_error.install_error", - text: description, - # FIXME: temporarily removed the "Abort" option until the final failed - # state is handled properly - options: [retry_label.to_sym, continue_label.to_sym], - data: { "package" => current_package } - ) - - questions_client.ask(question) do |answer| - case answer - when retry_label.to_sym - "R" - # FIXME: temporarily disabled - # when abort_label.to_sym - # "C" - when continue_label.to_sym - "I" - else - logger.error("Unexpected response #{question_client.answer.inspect}, " \ - "ignoring the package error") - "I" - end - end - end - - def msg - "Installing packages (#{@total - @installed} remains)" - end - end - end - end -end diff --git a/service/lib/agama/software/callbacks/provide.rb b/service/lib/agama/software/callbacks/provide.rb deleted file mode 100644 index 708c2d9745..0000000000 --- a/service/lib/agama/software/callbacks/provide.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "logger" -require "yast" -require "agama/question" -require "agama/software/callbacks/base" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # Provide callbacks - class Provide < Base - # From https://github.com/openSUSE/libzypp/blob/d90a93fc2a248e6592bd98114f82a0b88abadb72/zypp/ZYppCallbacks.h#L111 - NO_ERROR = 0 - NOT_FOUND = 1 - IO_ERROR = 2 - INVALID = 3 - - # Register the callbacks - def setup - Yast::Pkg.CallbackDoneProvide( - Yast::FunRef.new(method(:done_provide), "string (integer, string, string)") - ) - end - - # DoneProvide callback - # - # @return [String, nil] "I" for ignore, "R" for retry and "C" for abort (not implemented) - # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 - def done_provide(error, reason, name) - args = [error, reason, name] - logger.debug "DoneProvide callback: #{args.inspect}" - - error_code = case error - when NO_ERROR, NOT_FOUND - # "Not found" (error 1) is handled by the MediaChange callback. - nil - when IO_ERROR - "IO_ERROR" - when INVALID - "INVALID" - else - logger.warn "DoneProvide: unknown error: '#{error}'" - nil - end - - return nil if error_code.nil? - - question = Agama::Question.new( - qclass: "software.package_error.provide_error", - text: reason, - options: [retry_label.to_sym, continue_label.to_sym], - data: { "package" => name, "error_code" => error_code } - ) - - questions_client.ask(question) do |answer| - (answer.action == retry_label.to_sym) ? "R" : "I" - end - end - end - end - end -end diff --git a/service/lib/agama/software/callbacks/script.rb b/service/lib/agama/software/callbacks/script.rb deleted file mode 100644 index 74b66c8d39..0000000000 --- a/service/lib/agama/software/callbacks/script.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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 "yast" -require "agama/question" -require "agama/software/callbacks/base" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # Script callbacks - class Script < Base - include Yast::I18n - - # Register the callbacks - def setup - Yast::Pkg.CallbackScriptProblem( - Yast::FunRef.new(method(:script_problem), "string (string)") - ) - end - - # DoneProvide callback - # - # @param description [String] Problem description - # @return [String] "I" for ignore, "R" for retry and "C" for abort (not implemented) - # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 - def script_problem(description) - logger.debug "ScriptProblem callback: description: #{description}" - - message = _("There was a problem running a package script.") - question = Agama::Question.new( - qclass: "software.script_problem", - text: message, - options: [retry_label, continue_label], - data: { "details" => description } - ) - questions_client.ask(question) do |answer| - (answer.action == retry_label.to_sym) ? "R" : "I" - end - end - - private - - # @return [Agama::HTTP::Clients::Questions] - attr_reader :questions_client - - # @return [Logger] - attr_reader :logger - end - end - end -end diff --git a/service/lib/agama/software/callbacks/signature.rb b/service/lib/agama/software/callbacks/signature.rb deleted file mode 100644 index 5aaabef771..0000000000 --- a/service/lib/agama/software/callbacks/signature.rb +++ /dev/null @@ -1,208 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "yast" -require "agama/question" -require "agama/software/callbacks/base" -require "agama/software/repositories_manager" - -Yast.import "Pkg" - -module Agama - module Software - module Callbacks - # Callbacks related to signatures handling - class Signature < Base - # Register the callbacks - def setup - Yast::Pkg.CallbackAcceptUnsignedFile( - Yast::FunRef.new(method(:accept_unsigned_file), "boolean (string, integer)") - ) - Yast::Pkg.CallbackImportGpgKey( - Yast::FunRef.new(method(:import_gpg_key), "boolean (map , integer)") - ) - Yast::Pkg.CallbackAcceptUnknownGpgKey( - Yast::FunRef.new( - method(:accept_unknown_gpg_key), "boolean (string, string, integer)" - ) - ) - Yast::Pkg.CallbackAcceptVerificationFailed( - Yast::FunRef.new( - method(:accept_verification_failed), "boolean (string, map , integer)" - ) - ) - end - - # Callback to handle unsigned files - # - # @param filename [String] File name - # @param repo_id [Integer] Repository ID. It might be -1 if there is not an associated repo. - def accept_unsigned_file(filename, repo_id) - repo = Yast::Pkg.SourceGeneralData(repo_id) - if repo && Agama::Software::RepositoriesManager.instance.unsigned_allowed?(repo["alias"]) - return true - end - - message = if repo - format( - _("The file %{filename} from %{repo_url} is not digitally signed. The origin " \ - "and integrity of the file cannot be verified. Use it anyway?"), - filename: filename, repo_url: repo["url"] - ) - else - format( - _("The file %{filename} is not digitally signed. The origin " \ - "and integrity of the file cannot be verified. Use it anyway?"), - filename: filename - ) - end - - question = Agama::Question.new( - qclass: "software.unsigned_file", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: no_label.to_sym, - data: { "filename" => filename } - ) - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - # Callback to handle signature verification failures - # - # @param key [Hash] GPG key data (id, name, fingerprint, etc.) - # @param repo_id [Integer] Repository ID - def import_gpg_key(key, repo_id) - fingerprint = key["fingerprint"].scan(/.{4}/).join(" ") - repo = Yast::Pkg.SourceGeneralData(repo_id) - return true if repo && repo_manager.trust_gpg?(repo["alias"], fingerprint) - - message = format( - _("The key %{id} (%{name}) with fingerprint %{fingerprint} is unknown. " \ - "Do you want to trust this key?"), - id: key["id"], name: key["name"], fingerprint: fingerprint - ) - - question = Agama::Question.new( - qclass: "software.import_gpg", - text: message, - options: [trust_label.to_sym, skip_label.to_sym], - default_option: skip_label.to_sym, - data: { - "id" => key["id"], - "name" => key["name"], - "fingerprint" => fingerprint - } - ) - - questions_client.ask(question) do |answer| - answer.action == trust_label.to_sym - end - end - - # Callback to handle unknown GPG keys - # - # @param filename [String] Name of the file. - # @param key_id [String] Key ID. - # @param repo_id [String] Repository ID. - def accept_unknown_gpg_key(filename, key_id, repo_id) - repo = Yast::Pkg.SourceGeneralData(repo_id) - message = if repo - format( - _("The file %{filename} from %{repo_url} is digitally signed with " \ - "the following unknown GnuPG key: %{key_id}. Use it anyway?"), - filename: filename, repo_url: repo["url"], key_id: key_id - ) - else - format( - _("The file %{filename} is digitally signed with " \ - "the following unknown GnuPG key: %{key_id}. Use it anyway?"), - filename: filename, key_id: key_id - ) - end - - question = Agama::Question.new( - qclass: "software.unknown_gpg", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: no_label.to_sym, - data: { - "id" => key_id, - "filename" => filename - } - ) - - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - # Callback to handle file verification failures - # - # @param filename [String] File name - # @param key [Hash] GPG key data (id, name, fingerprint, etc.) - # @param repo_id [Integer] Repository ID - def accept_verification_failed(filename, key, repo_id) - repo = Yast::Pkg.SourceGeneralData(repo_id) - message = if repo - format( - _("The file %{filename} from %{repo_url} is digitally signed with the " \ - "following GnuPG key, but the integrity check failed: %{key_id} (%{key_name}). " \ - "Use it anyway?"), - filename: filename, repo_url: repo["url"], key_id: key["id"], key_name: key["name"] - ) - else - format( - _("The file %{filename} is digitally signed with the " \ - "following GnuPG key, but the integrity check failed: %{key_id} (%{key_name}). " \ - "Use it anyway?"), - filename: filename, key_id: key["id"], key_name: key["name"] - ) - end - - question = Agama::Question.new( - qclass: "software.unsigned_file", - text: message, - options: [yes_label.to_sym, no_label.to_sym], - default_option: no_label.to_sym, - data: { "filename" => filename } - ) - questions_client.ask(question) do |answer| - answer.action == yes_label.to_sym - end - end - - private - - # label for the "trust" action - def trust_label - # TRANSLATORS: button label, trust the GPG key or the signature - _("Trust") - end - - def repo_manager - Agama::Software::RepositoriesManager.instance - end - end - end - end -end diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb deleted file mode 100644 index f75903dfe6..0000000000 --- a/service/lib/agama/software/manager.rb +++ /dev/null @@ -1,916 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2021-2025] 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 "fileutils" -require "json" -require "shellwords" -require "yast" -require "packager/cfa/zypp_conf" -require "cfa/augeas_parser" -require "y2packager/product" -require "y2packager/resolvable" -require "agama/config" -require "agama/helpers" -require "agama/issue" -require "agama/registration" -require "agama/software/callbacks" -require "agama/software/product" -require "agama/software/product_builder" -require "agama/software/proposal" -require "agama/software/repositories_manager" -require "agama/with_locale" -require "agama/with_progress_manager" -require "agama/with_issues" - -Yast.import "Installation" -Yast.import "Language" -Yast.import "Package" -Yast.import "Packages" -Yast.import "PackageCallbacks" -Yast.import "Pkg" - -module Agama - module Software - class ServiceError < StandardError; end - - # 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 should be splitted in separate and smaller classes. - class Manager # rubocop:disable Metrics/ClassLength - include Helpers - include WithLocale - include WithIssues - include WithProgressManager - include Yast::I18n - - GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*" - private_constant :GPG_KEYS_GLOB - - # location of the custom DUD package repository, - # see the /usr/lib/dracut/modules.d/99agama-dud/agama-dud-apply.sh script - DUD_REPOSITORY_DIR = "/var/lib/agama/dud/repo" - private_constant :DUD_REPOSITORY_DIR - - # name for the custom DUD package repository, use some special name to - # minimize possible conflicts with user defined repositories, ugly name - # does not matter, it is deleted in the end anyway - DUD_REPOSITORY_NAME = "AgamaDriverUpdate" - private_constant :DUD_REPOSITORY_NAME - - # use a higher priority for the custom DUD package repository, - # the default priority is 99, the lower number the higher priority! - # the linuxrc default is 50, let's use the same value here as well - DUD_REPOSITORY_PRIORITY = 50 - private_constant :DUD_REPOSITORY_PRIORITY - - # Selected product. - # - # @return [Agama::Product, nil] - attr_reader :product - - DEFAULT_LANGUAGES = ["en_US"].freeze - private_constant :DEFAULT_LANGUAGES - - PROPOSAL_ID = "agama-user-software-selection" - private_constant :PROPOSAL_ID - - # create the libzypp lock and the zypp caches in a special directory to - # not be affected by the Live system package management - TARGET_DIR = "/run/agama/zypp" - private_constant :TARGET_DIR - - attr_accessor :languages - - # Available products for installation. - # - # @return [Array] - attr_reader :products - - # @return [Agama::RepositoriesManager] - attr_reader :repositories - - # @param config [Agama::Config] - # @param logger [Logger] - def initialize(config, logger) - textdomain "agama" - - @config = config - @logger = logger - @languages = DEFAULT_LANGUAGES - @products = build_products - @product = find_initial_product - @repositories = RepositoriesManager.instance - # patterns selected by user - @user_patterns = [] - @selected_patterns_change_callbacks = [] - on_progress_change { logger.info(progress.to_s) } - Yast::PackageCallbacks.InitPackageCallbacks(logger) - initialize_target - end - - def self.dud_repository_url - "dir:#{DUD_REPOSITORY_DIR}" - end - - # Selects a product with the given id. - # - # @raise {ArgumentError} If id is unknown. - # - # @param id [String] - # @return [Boolean] true on success. - def select_product(id) - return false if id == product&.id - - new_product = @products.find { |p| p.id == id } - - raise ArgumentError unless new_product - - proposal.set_resolvables( - PROPOSAL_ID, :pattern, new_product.preselected_patterns - ) - update_repositories(new_product) - - @product = new_product - - update_issues - true - end - - # select additional products to install - # @param addon_products [Array] list of product names - def addon_products(addon_products) - # The PackagesProposal module can handle only packages and patterns, - # so products need to be handled differently. - proposal.addon_products = addon_products - end - - def probe - # Should an error be raised? - return unless product - - logger.info "Probing software" - - common_steps = [ - _("Refreshing repositories metadata"), - _("Calculating the software proposal") - ] - if repositories.empty? - start_progress_with_descriptions( - _("Initializing sources"), *common_steps - ) - progress.step { add_base_repos } - else - start_progress_with_descriptions(*common_steps) - end - - progress.step { repositories.load } - progress.step { propose } - - update_issues - end - - def initialize_target - # create the zypp lock also in the target directory - ENV["ZYPP_LOCKFILE_ROOT"] = TARGET_DIR - # cleanup the previous content (after service restart or crash) - FileUtils.rm_rf(TARGET_DIR) - FileUtils.mkdir_p(TARGET_DIR) - Yast::Pkg.TargetInitialize(TARGET_DIR) - import_gpg_keys - end - - # Updates the software proposal - def propose - # Should an error be raised? - return unless product - - proposal.base_product = product.name - proposal.languages = languages - select_resolvables - result = proposal.calculate - update_issues - logger.info "Proposal result: #{result.inspect}" - selected_patterns_changed - result - end - - # Installs the packages to the target system - def install - # move the target from the Live ISO to the installed system (/mnt) - Yast::Pkg.TargetFinish - Yast::Pkg.TargetInitialize(Yast::Installation.destdir) - Yast::Pkg.TargetLoad - - steps = proposal.packages_count - start_progress_with_size(steps) - Callbacks::Progress.setup(steps, progress, logger) - - # TODO: error handling - commit_result = Yast::Pkg.Commit({}) - - if commit_result.nil? || commit_result.empty? - logger.error("Commit failed") - raise Yast::Pkg.LastError - end - - logger.info "Commit result #{commit_result}" - rescue Agama::NotFinishedProgress => e - logger.error "There is an unfinished progress: #{e.inspect}" - finish_progress - end - - # Writes the repositories information to the installed system - def finish - # disable local repositories (DVD, USB flash...) - disable_local_repos - remove_dud_repo - Yast::Pkg.SourceSaveAll - Yast::Pkg.TargetFinish - # copy the libzypp caches to the target - if Agama::Software::Repository.all.empty? - logger.info("No repository defined, not copying the libzypp caches") - else - copy_zypp_to_target - end - registration.finish - modify_zypp_conf - end - - # Determine whether the given tag is provided by the selected packages - # - # @param tag [String] Tag to search for (package names, requires/provides, or file - # names) - # @return [Boolean] true if it is provided; false otherwise - def provision_selected?(tag) - Yast::Pkg.IsSelected(tag) || Yast::Pkg.IsProvided(tag) - end - - # Enlist available patterns - # - # @param filtered [Boolean] If list of patterns should be filtered. - # Filtering criteria can change. - # @return [Array] - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def patterns(filtered) - # huge speed up, preload the used attributes to avoid querying libzypp again, - # see "ListPatterns" method in service/lib/agama/dbus/software/manager.rb - preload = [:category, :description, :icon, :summary, :order, :source, :user_visible] - patterns = Y2Packager::Resolvable.find({ kind: :pattern }, preload) - patterns = patterns.select(&:user_visible) if filtered - - # only display the configured patterns from the base product, from addons display everything - if product.user_patterns && filtered - base_repos = base_repositories - - user_patterns_names = (product.user_patterns || []).map(&:name) - patterns.select! do |p| - # the pattern is not from a base repository or is included in the display list - !base_repos.include?(p.source) || user_patterns_names.include?(p.name) - end - end - - patterns - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - - def add_pattern(id) - return false unless pattern_exist?(id) - - res = Yast::Pkg.ResolvableInstall(id, :pattern) - logger.info "Adding pattern #{res.inspect}" - Yast::PackagesProposal.AddResolvables(PROPOSAL_ID, :pattern, [id]) - proposal.solve_dependencies - selected_patterns_changed - - true - end - - def remove_pattern(id) - return false unless pattern_exist?(id) - - res = Yast::Pkg.ResolvableNeutral(id, :pattern, force = false) - logger.info "Removing pattern #{res.inspect}" - Yast::PackagesProposal.RemoveResolvables(PROPOSAL_ID, :pattern, [id]) - proposal.solve_dependencies - selected_patterns_changed - - true - end - - def assign_patterns(add, remove) - wrong_patterns = [add, remove].flatten.reject { |p| pattern_exist?(p) } - return wrong_patterns unless wrong_patterns.empty? - - user_patterns = Yast::PackagesProposal.GetResolvables(PROPOSAL_ID, :pattern) - user_patterns.each { |p| Yast::Pkg.ResolvableNeutral(p, :pattern, force = false) } - logger.info "Adding patterns: #{add.inspect}, removing patterns: #{remove.inspect}" - - Yast::PackagesProposal.SetResolvables(PROPOSAL_ID, :pattern, add) - add.each do |id| - res = Yast::Pkg.ResolvableInstall(id, :pattern) - logger.info "Adding pattern #{id}: #{res.inspect}" - end - - remove.each do |id| - res = Yast::Pkg.ResolvableNeutral(id, :pattern, force = false) - logger.info "Removing pattern #{id}: #{res.inspect}" - Yast::PackagesProposal.RemoveResolvables(PROPOSAL_ID, :pattern, [id]) - end - - proposal.solve_dependencies - - selected_patterns_changed - - [] - end - - # @return [Array,Array] returns pair of arrays where the first one - # is user selected pattern ids and in other is auto selected ones - def selected_patterns - user_patterns = Yast::PackagesProposal.GetResolvables(PROPOSAL_ID, :pattern) - - patterns = Y2Packager::Resolvable.find(kind: :pattern, status: :selected) - patterns.map!(&:name) - logger.info "Currently selected patterns: #{patterns.inspect}" - - patterns.partition { |p| user_patterns.include?(p) } - end - - def update_selected_patterns - user_patterns = Yast::PackagesProposal.GetResolvables(PROPOSAL_ID, :pattern) - patterns = Y2Packager::Resolvable.find(kind: :pattern, status: :selected).map!(&:name) - - unselect_patterns = user_patterns - patterns - unselect_patterns.each do |id| - logger.info "Unselecting pattern #{id}" - Yast::PackagesProposal.RemoveResolvables(PROPOSAL_ID, :pattern, [id]) - end - - selected_patterns_changed if !unselect_patterns.empty? - end - - def on_selected_patterns_change(&block) - @selected_patterns_change_callbacks << block - end - - # Determines whether a package is installed in the target system. - # - # @param name [String] Package name - # @return [Boolean] true if it is installed; false otherwise - def package_installed?(name) - on_target { Yast::Package.Installed(name, target: :system) } - end - - # Determines whether a package is available. - # - # @param name [String] Package name - # @return [Boolean] - def package_available?(name) - # Beware: apart from true and false, Available can return nil if things go wrong. - on_local { !!Yast::Package.Available(name) } - end - - # Counts how much disk space installation will use. - # @return [String] - # @note Reimplementation of Yast::Package.CountSizeToBeInstalled - # @todo move to Software::Proposal - def used_disk_space - return "" unless proposal.valid? - - # FormatSizeWithPrecision(bytes, precision, omit_zeroes) - Yast::String.FormatSizeWithPrecision(proposal.packages_size, 1, true) - end - - def registration - @registration ||= Registration.new(self, logger) - end - - # code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365 - # rubocop:disable Metrics/AbcSize - def add_service(service) - # save repositories before refreshing added services (otherwise - # pkg-bindings will treat them as removed by the service refresh and - # unload them) - if !Yast::Pkg.SourceSaveAll - # error message - @logger.error("Saving repository configuration failed.") - end - - @logger.info "Adding service #{service.name.inspect} (#{service.url})" - if !Yast::Pkg.ServiceAdd(service.name, service.url.to_s) - raise ServiceError, format(_("Adding service '%s' failed."), service.name) - end - - if !Yast::Pkg.ServiceSet(service.name, "autorefresh" => true) - # error message - raise ServiceError, format(_("Updating service '%s' failed."), service.name) - end - - # refresh works only for saved services - if !Yast::Pkg.ServiceSave(service.name) - # error message - raise ServiceError, format(_("Saving service '%s' failed."), service.name) - end - - # Force refreshing due timing issues (bnc#967828) - if !Yast::Pkg.ServiceForceRefresh(service.name) - # error message - raise ServiceError, format(_("Refreshing service '%s' failed."), service.name) - end - ensure - Yast::Pkg.SourceSaveAll - end - # rubocop:enable Metrics/AbcSize - - def remove_service(service) - if Yast::Pkg.ServiceDelete(service.name) && !Yast::Pkg.SourceSaveAll - raise ServiceError, format(_("Removing service '%s' failed."), service_name) - end - - 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 - - # Change the locale and activate new locale in the libzypp backend - # - # @param locale [String] the new locale - def locale=(locale) - change_process_locale(locale) - language, = locale.split(".") - - # set the locale in the Language module, when changing the repository - # (product) it calls Pkg.SetTextLocale(Language.language) internally - Yast::Language.Set(language) - - # set libzypp locale (for communication only, Pkg.SetPackageLocale - # call can be used for *installing* the language packages) - Yast::Pkg.SetTextLocale(language) - - # refresh all enabled repositories to download the missing translation files - Yast::Pkg.SourceGetCurrent(true).each do |src| - Yast::Pkg.SourceForceRefreshNow(src) - end - - # remember the currently selected packages and patterns by YaST - # (ignore the automatic selections done by the solver) - # - # NOTE: we will need to handle also the tabooed and soft-locked objects - # when we allow to set them via UI or CLI - selected = Y2Packager::Resolvable.find(status: :selected, transact_by: :appl_high) - - # save and reload all repositories to activate the new translations - Yast::Pkg.SourceSaveAll - Yast::Pkg.SourceFinishAll - Yast::Pkg.SourceRestore - Yast::Pkg.SourceLoad - - # restore back the selected objects - selected.each { |s| Yast::Pkg.ResolvableInstall(s.name, s.kind) } - end - - def proposal - @proposal ||= Proposal.new.tap do |proposal| - proposal.on_issues_change { update_issues } - end - end - - private - - # @return [Agama::Config] - attr_reader :config - - # @return [Logger] - attr_reader :logger - - # Generates a list of products according to the information of the config file. - # - # @return [Array] - def build_products - ProductBuilder.new(config).build - end - - # Determines the initially selected product. - # - # A product is automatically selected if it is the only product - # and it does not require acccepting a license. - def find_initial_product - product = @products.first - return product if @products.size == 1 && product.license.to_s.empty? - - nil - end - - def import_gpg_keys - gpg_keys = Dir.glob(GPG_KEYS_GLOB).map(&:to_s) - logger.info "Importing GPG keys: #{gpg_keys}" - gpg_keys.each do |path| - Yast::Pkg.ImportGPGKey(path, true) - end - end - - def add_base_repos - add_dud_repo - return if add_repos_by_label - return if add_repos_by_dir - - # local repositories not found, use the online repositories - product.repositories.each { |url| repositories.add(url) } - end - - def add_repos_by_dir - # path to the installation repository present on the Live medium (only on the Full medium) - dir_path = "/run/initramfs/live/install" - return false unless File.exist?(dir_path) - - logger.info "/install found on Live medium" - url = full_repo_url(dir_path, "/install") - return false unless url - - logger.info "Using Full media installation repository #{url}" - # disable autorefresh, the packages on DVD cannot be updated, for USB flash disks it can be - # manually enabled in the installed system if needed (updating the packages need some user - # interaction anyway) - repositories.add(url, repo_alias: product.name, name: product.display_name, - autorefresh: false) - - true - end - - def add_repos_by_label - # NOTE: support multiple labels/installation media? - label = product.labels.first - - if label - logger.info "Installation repository label: #{label.inspect}" - # we cannot use the simple /dev/disk/by-label/* device file as there - # might be multiple devices with the same label - device = installation_device(label) - if device - logger.info "Installation device: #{device}" - repositories.add("hd:/?device=" + device) - return true - end - end - - false - end - - # add a custom repository provided by DUD - def add_dud_repo - return unless File.directory?(DUD_REPOSITORY_DIR) && !Dir.empty?(DUD_REPOSITORY_DIR) - - logger.info "Adding DUD repository at #{DUD_REPOSITORY_DIR}" - # if there is no repository metadata present in the dir:/ repository then libzypp - # automatically uses the "plaindir" repository type - repositories.add(self.class.dud_repository_url, repo_alias: DUD_REPOSITORY_NAME, - name: DUD_REPOSITORY_NAME, priority: DUD_REPOSITORY_PRIORITY) - end - - # find all devices with the required disk label - # @return [Array] returns list of devices, e.g. `["/dev/sr1"]`, - # returns empty list if there is no device with the required label - def disks_with_label(label) - data = list_disks - disks = data.fetch("blockdevices", []).map do |device| - device["kname"] if device["label"] == label - end - disks.compact! - logger.info "Disks with the installation label: #{disks.inspect}" - disks - end - - # get list of disks, returns parsed data from the `lsblk` call - # @return [Hash] parsed data - def list_disks - # we need only the kernel device name and the label - output = `lsblk --paths --json --output kname,label` - JSON.parse(output) - rescue StandardError => e - logger.error "ERROR: Cannot read disk devices: #{e}" - {} - end - - # find the installation device with the required label - # @return [String,nil] Device name (`/dev/sr1`) or `nil` if not found - def installation_device(label) - disks = disks_with_label(label) - - # multiple installation media? - if disks.size > 1 - # prefer optical media (/dev/srX) to disk so the disk can be used as - # the installation target - optical = disks.find { |d| d.match(/\A\/dev\/sr[0-9]+\z/) } - optical || disks.first - else - # none or just one disk - disks.first - end - end - - # build URL for the Full installation repository - # @param path [String] Local path where the Full repository is mounted - # @param url_path [String] Path part of the resulting URL - # @return [String,nil] URL or `nil` if the Full repository device was not found - def full_repo_url(path, url_path) - # find the device which is mounted at the repository location - live_device = `findmnt -n -o SOURCE --target #{Shellwords.escape(path)}`.chomp - logger.info "Installation device: #{live_device}" - return nil unless live_device - - # distinguish between DVD and hard disks/USB flash - if live_device.match(/\A\/dev\/sr[0-9]+\z/) - "dvd:#{url_path}?devices=#{live_device}" - else - # try using a more stable by-id device name, important esp. for USB flash - by_id_devices = `find -L /dev/disk/by-id -samefile #{Shellwords.escape(live_device)}` - .chomp - # if there are more names just use the first one - by_id_device = by_id_devices.split("\n").first - device = (by_id_device && !by_id_device.empty?) ? by_id_device : live_device - "hd:#{url_path}?device=#{device}" - end - end - - # Adds resolvables for selected product - def select_resolvables - proposal.set_resolvables("agama", :pattern, product.mandatory_patterns) - proposal.set_resolvables("agama", :pattern, product.optional_patterns, optional: true) - proposal.set_resolvables("agama", :package, product.mandatory_packages) - proposal.set_resolvables("agama", :package, product.optional_packages, optional: true) - end - - def selected_patterns_changed - @selected_patterns_change_callbacks.each(&:call) - end - - # Updates the list of software issues. - def update_issues - self.issues = current_issues - end - - # List of current software issues. - # - # @return [Array] - def current_issues - return [] unless product - - issues = repos_issues - - # If none of the repositories could be probed, then do not report missing patterns and/or - # packages. Those issues does not make any sense if there are no repositories to install - # from. - issues += proposal.issues if repositories.enabled.any? - issues - end - - # Issues related to the software proposal. - # - # Repositories that could not be probed are reported as errors. - # - # @return [Array] - def repos_issues - repositories.disabled.map do |repo| - Issue.new(_("Could not read repository \"%s\"") % repo.name, - source: Issue::Source::SYSTEM, - severity: Issue::Severity::ERROR) - 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] - def missing_registration_issue - Issue.new(_("Product must be registered"), - kind: :missing_registration, - source: Issue::Source::SYSTEM, - severity: Issue::Severity::ERROR) - end - - # Whether the registration is missing. - # - # @return [Boolean] - def missing_registration? - return false unless product - - product.registration && missing_base_product? - end - - # Whether the base product is missing - # - # @return [Boolean] - def missing_base_product? - products = Y2Packager::Resolvable.find(kind: :product, name: product.name) - products.empty? - end - - def pattern_exist?(pattern_name) - !Y2Packager::Resolvable.find(kind: :pattern, name: pattern_name).empty? - end - - # this reimplements the Pkg.SourceCacheCopyTo call which works correctly - # only from the inst-sys (it copies the data from "/" where is actually - # the Live system package manager) - # @see https://github.com/yast/yast-pkg-bindings/blob/3d314480b70070299f90da4c6e87a5574e9c890c/src/Source_Installation.cc#L213-L267 - def copy_zypp_to_target - # copy the zypp "raw" cache - cache = File.join(TARGET_DIR, "/var/cache/zypp/raw") - if Dir.exist?(cache) - target_cache = File.join(Yast::Installation.destdir, "/var/cache/zypp") - FileUtils.mkdir_p(target_cache) - FileUtils.cp_r(cache, target_cache) - end - - # copy the "solv" cache but skip the "@System" directory because it - # contains empty installed packages (there were no installed packages - # before moving the target to "/mnt") - solv_cache = File.join(TARGET_DIR, "/var/cache/zypp/solv") - target_solv = File.join(Yast::Installation.destdir, "/var/cache/zypp/solv") - solvs = Dir.entries(solv_cache) - [".", "..", "@System"] - solvs.each do |s| - FileUtils.cp_r(File.join(solv_cache, s), target_solv) - end - - # copy the zypp credentials if present - credentials = File.join(TARGET_DIR, "/etc/zypp/credentials.d") - if Dir.exist?(credentials) - target_credentials = File.join(Yast::Installation.destdir, "/etc/zypp") - FileUtils.mkdir_p(target_credentials) - FileUtils.cp_r(credentials, target_credentials) - end - - # copy the global credentials if present - glob_credentials = File.join(TARGET_DIR, "/etc/zypp/credentials.cat") - return unless File.exist?(glob_credentials) - - target_dir = File.join(Yast::Installation.destdir, "/etc/zypp") - FileUtils.mkdir_p(target_dir) - FileUtils.copy(glob_credentials, target_dir) - end - - # private class to ensure that cfa reads installed system - # YaST target file does not work reliably as Agama does not have - # always switched SCR - class TargetFile - # Reads file content with respect of changed root in installation. - def self.read(path) - ::File.read(final_path(path)) - end - - # Writes file content with respect of changed root in installation. - def self.write(path, content) - ::File.write(final_path(path), content) - end - - def self.final_path(path) - ::File.join(Yast::Installation.destdir, path) - end - private_class_method :final_path - end - - def modify_zypp_conf - # use defaults unless user explicitelly sets flag - return if proposal.only_required.nil? - - # minimal system does not need to have libzypp, so in this case do not - # modify zypp.conf - if !File.exist?(File.join(Yast::Installation.destdir, "/etc/zypp/zypp.conf")) - logger.info "Target system does not have zypp.conf so skipping modification of it" - return - end - - zypp_conf = Yast::Packager::CFA::ZyppConf.new(file_handler: TargetFile) - zypp_conf.load - tree = zypp_conf.generic_get("main") - if !tree - tree = ::CFA::AugeasTree.new - zypp_conf.generic_get("main", tree) - end - zypp_conf.generic_set("solver.onlyRequires", (!!proposal.only_required).to_s, tree) - zypp_conf.save - end - - # Is any local repository (CD/DVD, disk) currently used? - # @return [Boolean] true if any local repository is used - def local_repo? - Agama::Software::Repository.all.any?(&:local?) - end - - # update the zypp repositories for the new product, either delete them - # or keep them untouched - # @param new_product [Agama::Software::Product] the new selected product - def update_repositories(new_product) - # reuse the repositories when they are the same as for the previously - # selected product and no local repository is currently used - # (local repositories are usually product specific) - # TODO: what about registered products? - # TODO: allow a partial match? i.e. keep the same repositories, delete - # additional repositories and add missing ones - if product&.repositories&.sort == new_product.repositories.sort && !local_repo? - # the same repositories, we just needed to reset the package selection - Yast::Pkg.PkgReset() - else - # delete all, the #probe call will add the new repos - repositories.delete_all - # deleting happens only in memory, to really delete the caches we need - # to write the repository setup to the disk - Yast::Pkg.SourceSaveAll - end - end - - # disable all local repositories, remove device name from the DVD Full repository - def disable_local_repos - local_repos = Agama::Software::Repository.all.select(&:local?) - local_repos.each(&:disable!) - - # remove the installation device from the URL, libzypp will probe all present devices, - # this allows inserting the DVD medium into a different drive later - full_dvd_repo = local_repos.find { |r| r.url.to_s.start_with?("dvd:/install?devices=") } - return unless full_dvd_repo - - new_url = "dvd:/install" - logger.info "Changing repository URL from #{full_dvd_repo.url} to #{new_url}" - full_dvd_repo.url = new_url - end - - def remove_dud_repo - dud_repo = Agama::Software::Repository.all.find { |r| r.name == DUD_REPOSITORY_NAME } - return unless dud_repo - - logger.info "Removing the temporary DUD repository" - dud_repo.delete! - end - - # Return all enabled repositories belonging to the base product. - # - # @return [Array] List of repository IDs, returns empty list if - # no repository is defined yet - def base_repositories - # process only the enabled repositories - only_enabled_repos = true - # the base product repo is the first added repository (the lowest number) - base_src_id = Yast::Pkg.SourceGetCurrent(only_enabled_repos).min - # a repository might not be defined yet - return [] unless base_src_id - - # if the base repository comes from a service consider all repositories from that service - # (SCC uses Pool + Updates, use both of them just in case a pattern is updated) - service = Yast::Pkg.SourceGeneralData(base_src_id)["service"] - - if service.empty? - [base_src_id] - else - logger.info "The base product is from a service" - Yast::Pkg.SourceGetCurrent(only_enabled_repos).select do |r| - Yast::Pkg.SourceGeneralData(r)["service"] == service - end - end - end - end - end -end diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb deleted file mode 100644 index d2402d6815..0000000000 --- a/service/lib/agama/software/product.rb +++ /dev/null @@ -1,178 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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 "agama/registration" - -module Agama - module Software - # Represents a user selectable product. - UserPattern = Struct.new(:name, :selected) - - # Represents a product that Agama can install. - class Product - # Product id. - # - # @return [String] - attr_reader :id - - # Name of the product to be display. - # - # @return [String, nil] - attr_accessor :display_name - - # Description of the product. - # - # @return [String, nil] - attr_accessor :description - - # Internal name of the product. This is relevant for registering the product. - # - # @return [String, nil] - attr_accessor :name - - # Version of the product. This is relevant for registering the product. - # - # @return [String, nil] E.g., "1.0". - attr_accessor :version - - # Product icon. Please use specify filename with svg suffix and ensure referenced - # file exists inside agama/web/src/assets/product. - # `default.svg` will be used unless specified otherwise. - # - # @return [String] E.g. "leap.svg" - attr_accessor :icon - - # List of repositories. - # - # @return [Array] Empty if the product requires registration. - attr_accessor :repositories - - # List of disk labels used for installation repository. - # - # @return [Array] Empty if the product does not support offline installation. - attr_accessor :labels - - # Mandatory packages. - # - # @return [Array] - attr_accessor :mandatory_packages - - # Optional packages. - # - # @return [Array] - attr_accessor :optional_packages - - # Mandatory patterns. - # - # @return [Array] - attr_accessor :mandatory_patterns - - # Optional patterns. - # - # These patterns are always installed if they are available. - # - # @return [Array] - attr_accessor :optional_patterns - - # Optional user selectable patterns - # - # @return [Array, nil] - attr_accessor :user_patterns - - # Whether the registration is enabled for the product. - # - # @return [boolean] - attr_accessor :registration - - # Product translations. - # - # @example - # product.translations #=> - # { - # "description" => { - # "cs" => "Czech translation", - # "es" => "Spanish translation" - # } - # - # @return [Hash>] - attr_accessor :translations - - # License ID - attr_accessor :license - - # @param id [string] Product id. - def initialize(id) - @id = id - @icon = "default.svg" - @repositories = [] - @labels = [] - @mandatory_packages = [] - @optional_packages = [] - @mandatory_patterns = [] - @optional_patterns = [] - # nil = display all visible patterns, [] = display no patterns - @user_patterns = nil - @registration = false - @license = nil - @translations = {} - end - - # Localized product description. - # - # If there is no translation for the current language, then the untranslated description is - # used. - # - # @return [String, nil] - def localized_description - translations = self.translations["description"] - lang = ENV["LANG"] - - # No translations or language not set, return untranslated value. - return description unless translations && lang - - # Remove the character encoding if present. - lang = lang.split(".").first - # Full matching (language + country) - return translations[lang] if translations[lang] - - # Remove the country part. - lang = lang.split("_").first - # Partial match (just the language). - return translations[lang] if translations[lang] - - # Fallback to original untranslated description. - description - end - - # Preselected patterns. - # - # These patterns are pre-selected if they are available, but - # the user can unselect them. - # - # @return [Array] - def preselected_patterns - return [] if user_patterns.nil? - - user_patterns.filter_map { |p| p.name if p.selected } - end - end - end -end diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb deleted file mode 100644 index 687df9056a..0000000000 --- a/service/lib/agama/software/product_builder.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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 "agama/cmdline_args" -require "agama/software/product" -require "logger" - -module Agama - module Software - # Builds products from the information of a config file. - class ProductBuilder - # @param config [Agama::Config] - def initialize(config, logger: Logger.new($stdout)) - @config = config - @logger = logger - end - - # Builds the products. - # - # @return [Array] - def build - cmdline_args = CmdlineArgs.read_from("/run/agama/cmdline.d/agama.conf") - @logger.info cmdline_args - config.products.map do |id, attrs| - data = product_data_from_config(id) - create_product(id, data, attrs, cmdline_args) - end - end - - private - - # @return [Agama::Config] - attr_reader :config - - # @return [Agama::Software::Product] - def create_product(id, data, attrs, cmdline_args) - product = initialize_product(id, data, attrs) - set_repositories(product, data, cmdline_args) - set_software(product, data) - set_translations(product, attrs) - product - end - - def initialize_product(id, data, attrs) - Agama::Software::Product.new(id).tap do |product| - product.display_name = attrs["name"] - product.description = attrs["description"] - product.name = data[:name] - product.version = data[:version] - product.icon = attrs["icon"] if attrs["icon"] - product.registration = !!attrs["registration"] - product.license = attrs["license"] if attrs["license"] - product.version = attrs["version"] if attrs["version"] - end - end - - def set_repositories(product, data, cmdline_args) - install_url = cmdline_args.data["install_url"] - if install_url - @logger.info "agama.install_url is set to #{install_url}" - product.repositories = install_url.split(",") - else - product.repositories = data[:repositories] - end - end - - def set_software(product, data) - product.labels = data[:labels] - product.mandatory_packages = data[:mandatory_packages] - product.optional_packages = data[:optional_packages] - product.mandatory_patterns = data[:mandatory_patterns] - product.optional_patterns = data[:optional_patterns] - product.user_patterns = build_user_patterns(data[:user_patterns]) - end - - def set_translations(product, attrs) - product.translations = attrs["translations"] || {} - end - - # Data from config, filtering by arch. - # - # @param id [String] - # @return [Hash] - def product_data_from_config(id) - { - name: config.products.dig(id, "software", "base_product"), - icon: config.products.dig(id, "software", "icon"), - labels: config.arch_elements_from( - id, "software", "installation_labels", property: :label - ), - repositories: config.arch_elements_from( - id, "software", "installation_repositories", property: :url - ), - mandatory_packages: config.arch_elements_from( - id, "software", "mandatory_packages", property: :package - ), - optional_packages: config.arch_elements_from( - id, "software", "optional_packages", property: :package - ), - mandatory_patterns: config.arch_elements_from( - id, "software", "mandatory_patterns", property: :pattern - ), - optional_patterns: config.arch_elements_from( - id, "software", "optional_patterns", property: :pattern - ), - user_patterns: config.arch_elements_from( - id, "software", "user_patterns", property: nil, default: nil - ) - } - end - - # Build the list of user patterns. - # - # @param [Array, nil] user_patterns - def build_user_patterns(user_patterns) - return nil if user_patterns.nil? - - user_patterns.map do |d| - if d.is_a?(Hash) - UserPattern.new(d["name"], d["selected"]) - else - UserPattern.new(d, false) - end - end - end - end - end -end diff --git a/service/lib/agama/software/proposal.rb b/service/lib/agama/software/proposal.rb deleted file mode 100644 index d63203f84f..0000000000 --- a/service/lib/agama/software/proposal.rb +++ /dev/null @@ -1,298 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "yast" -require "agama/issue" -require "agama/with_issues" - -Yast.import "Stage" -Yast.import "Installation" -Yast.import "Pkg" -Yast.import "PackagesProposal" -Yast.import "Packages" - -module Agama - module Software - # Backend class to calculate the software proposal - # - # This class represents a software proposal. Beware that it is a wrapper around `Yast::Pkg` and - # `Yast::PackagesProposal` and most of the state is kept in those modules. For that reason, a - # new instance of this class might has already some implicit state (e.g., the list of - # repositories to use, the list of packages/patterns to install, etc.). - # - # @todo implement a reset mechanism to clear repositories, seleced packages/patterns, etc. - # @note you might expect that it receives a RepositoriesManager instance. However, as the state - # is kept in the `Yast::Pkg` module, it is not needed at all. - # - # @example Calculate a proposal - # proposal = Proposal.new - # proposal.base_product = "openSUSE" - # proposal.add_resolvables("agama", :pattern, ["enhanced_base"]) - # proposal.languages = ["en_US", "de_DE"] - # proposal.calculate #=> true - # proposal.issues #=> [] - class Proposal - include WithIssues - include Yast::I18n - - # @return [String,nil] Base product - attr_accessor :base_product - - # @return [Array] Addon products - attr_accessor :addon_products - - # @return [Array] List of languages to install - attr_reader :languages - - # @return [Array>>] List of conflicts from the last solver run - attr_reader :conflicts - - # @return [boolean, nil] flag to indicate that solver should add only required packages - # and not recommended. Nil means not set - attr_accessor :only_required - - # Constructor - # - # @param logger [Logger] - def initialize(logger: nil) - textdomain "agama" - - @logger = logger || Logger.new($stdout) - @base_product = nil - @addon_products = [] - @conflicts = [] - @conflicts_change_callbacks = [] - @only_required = nil - end - - # Adds the given list of resolvables to the proposal - # - # It relies on the Yast::PackagesProposal module which keeps its own state. - # - # @param unique_id [String] Unique identifier for the resolvables list - # @param type [Symbol] Resolvables type (:package or :pattern) - # @param resolvables [Array] Resolvables to add - # @param optional [Boolean] Whether the resolvable is optional (or mandatory) - def set_resolvables(unique_id, type, resolvables, optional: false) - Yast::PackagesProposal.SetResolvables(unique_id, type, resolvables, optional: optional) - end - - # Calculates the proposal - # - # @return [Boolean] - def calculate - initialize_target - @proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true) - # select the base product after running the Packages.Proposal, the force_reset = true - # option would reset the selection and a random product would be selected by the solver - select_base_product - select_addon_products - solve_dependencies - - valid? - end - - # Runs the solver to satisfy the dependencies. - # - # Issues are updated once the solver finishes. - # - # @return [Boolean] whether the solver ran successfully - def solve_dependencies - res = Yast::Pkg.PkgSolve(unused = true) - logger.info "Solver run #{res.inspect}" - update_issues - update_conflicts - - return true if res - - logger.error "Solver failed: #{Yast::Pkg.LastError}" - logger.error "Details: #{Yast::Pkg.LastErrorDetails}" - logger.error "Solver errors: #{Yast::Pkg.PkgSolveErrors}" - false - end - - # @param [Array<(Integer, Integer)>] solutions is array of conflict id and solution id - def solve_conflicts(solutions) - pkg_solutions = solutions.map do |sol| - con_id, sol_id = sol - conflict = @conflicts[con_id] or raise "Unknown conflict id #{con_id.inspect}" - solution = conflict["solutions"][sol_id] or raise "unknown solution id #{sol_id.inspect}" - { - "description" => conflict["description"], - "details" => conflict["details"], - "solution_description" => solution["description"], - "solution_details" => solution["details"] - } - end - logger.info "Sending solver solutions #{pkg_solutions.inspect}" - - Yast::Pkg.PkgSetSolveSolutions(pkg_solutions) - - # and rerun solver to also update conflicts - solve_dependencies - end - - def on_conflicts_change(&block) - @conflicts_change_callbacks << block - end - - # Returns the count of packages to install - # - # @return [Integer] count of packages to install - def packages_count - Yast::Pkg.PkgMediaCount.reduce(0) { |sum, res| sum + res.reduce(0, :+) } - end - - # Returns the size of the packages to install - # - # @return [Integer] size of the installation in bytes - def packages_size - Yast::Pkg.PkgMediaSizes.reduce(0) do |res, media_size| - media_size.reduce(res, :+) - end - end - - # Determines whether the proposal is valid - # - # @return [Boolean] - def valid? - !(proposal.nil? || errors?) - end - - # Sets the languages to install - # - # @param [Array] value Languages in xx_XX format (e.g., "en_US"). - def languages=(value) - @languages = value.map { |l| l.split(".").first }.compact - end - - private - - # @return [Logger] - attr_reader :logger - - # Proposal result - # - # @return [Hash, nil] nil if not calculated yet. - attr_reader :proposal - - # Initializes the target, closing the previous one - def initialize_target - preferred, *additional = languages - Yast::Pkg.SetPackageLocale(preferred || "") - Yast::Pkg.SetAdditionalLocales(additional) - - Yast::Pkg.SetSolverFlags("ignoreAlreadyRecommended" => false, - "onlyRequires" => !!@only_required) - end - - # Selects the base product - # - # @see #base_product - def select_base_product - base_product = Y2Packager::Product.available_base_products.find do |product| - product.name == @base_product - end - - if base_product.nil? - logger.error "Could not select the base product '#{@base_product}'" - else - logger.info "Selecting the base product '#{base_product.name}'" - base_product&.select - end - end - - # select the addon products to install - def select_addon_products - addon_products.each { |a| Yast::Pkg.ResolvableInstall(a, :product) } - end - - # Updates the issues from the attempt to create a proposal. - # - # It collects issues from: - # - # * The proposal result. - # * The last solver execution. - # - # @return [Array] - def update_issues - msgs = [] - msgs.concat(warning_messages(proposal)) if proposal - - issues = msgs.map do |msg| - Issue.new(msg, - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR) - end - - solver_issues = solver_messages.map do |msg| - Issue.new(msg, - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR, - kind: :solver) - end - - self.issues = issues + solver_issues - end - - # Extracts the warning messages from the proposal result - # - # @param proposal_result [Hash] Proposal result; it might contain a "warning" key with warning - # messages. - def warning_messages(proposal_result) - return [] unless proposal_result["warning_level"] == :blocker - - proposal_result["warning"] - .split("
") - .grep_v(/Please manually select .*/) # FIXME: it depends on the language - end - - # Returns solver error messages from the last attempt - # - # @return [Array] Error messages - def solver_messages - solve_errors = Yast::Pkg.PkgSolveErrors - return [] if solve_errors.zero? - - res = [] - res << (_("Found %s dependency issues.") % solve_errors) if solve_errors > 0 - res - end - - def update_conflicts - pkg_conflicts = Yast::Pkg.PkgSolveProblems - @conflicts = [] - pkg_conflicts.each_with_index do |pkg_conflict, index| - conflict = pkg_conflict - conflict["id"] = index - conflict["solutions"].each_with_index do |solution, index2| - solution["id"] = index2 - end - @conflicts << conflict - end - - @conflicts_change_callbacks.each { |c| c.call(@conflicts) } - - @conflicts - end - end - end -end diff --git a/service/lib/agama/software/repositories_manager.rb b/service/lib/agama/software/repositories_manager.rb deleted file mode 100644 index 648b24dd80..0000000000 --- a/service/lib/agama/software/repositories_manager.rb +++ /dev/null @@ -1,166 +0,0 @@ -# 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 "agama/software/repository" -require "singleton" - -module Agama - module Software - # Class to manage repositories - # - # @see Repository - class RepositoriesManager - include Singleton - - # @return [Array] - attr_reader :repositories - - def reset - @repositories = [] - @user_repositories = [] - # remember how exactly user specify repos and return it identical - @plain_user_repositories = [] - @unsigned_repos = [] - @gpg_fingerprints = {} - end - - # Adds a new repository - # - # @param url [String] Repository URL - # @param name [String] Repository name, if not specified the URL is used - # @param repo_alias [String] Repository alias, must be unique, - # if not specified a random one is generated - # @param autorefresh [Boolean] Whether the repository should be autorefreshed - # @param priority [Integer] Repository priority, the lower number the higher (!) - # priority, the default libzypp priority is 99 - def add(url, name: nil, repo_alias: "", autorefresh: true, priority: 99) - repositories << Repository.create(name: name || url, url: url, repo_alias: repo_alias, - autorefresh: autorefresh, priority: priority) - end - - # returns user repositories as it was previously specified - def user_repositories - @plain_user_repositories - end - - # sets and loads user repositories - def user_repositories=(repos) - @plain_user_repositories = repos - clear_user_repositories - repos.each do |repo| - id = Yast::Pkg.RepositoryAdd( - "name" => repo["name"], - "base_urls" => [repo["url"].to_s], - "alias" => repo["alias"], - "prod_dir" => repo["product_dir"], - "enabled" => repo["enabled"], - "priority" => repo["priority"] - ) - # TODO: better error reporting - raise "failed to add repo" unless id - - zypp_repo = Repository.find(id) - - @user_repositories << zypp_repo - repositories << zypp_repo - - @unsigned_repos << repo["alias"] if repo["allow_unsigned"] - @gpg_fingerprints[repo["alias"]] = repo["gpg_fingerprints"] - &.map { |f| f.gsub(/\s/, "") } || [] - end - - # load new repos - self.load - end - - def unsigned_allowed?(repo_alias) - @unsigned_repos.include?(repo_alias) - end - - def trust_gpg?(repo_alias, fingerprint) - @gpg_fingerprints[repo_alias]&.include?(fingerprint.gsub(/\s/, "")) - end - - # Determines if there are registered repositories - # - # @return [Boolean] true if there are not repositories; false otherwise - def empty? - repositories.empty? - end - - # Returns the enabled repositories - # - # @return [Array] - def enabled - repositories.select(&:enabled?) - end - - # Returns the disabled repositories - # - # @return [Array] - def disabled - repositories.reject(&:enabled?) - end - - # Loads the repository metadata - # - # As a side effect, it disables those repositories that cannot be read. - # The intent is to prevent the proposal from trying to read them - # again. - def load - repositories.each do |repo| - if repo.probe - repo.enable! - # In some rare scenarios although the repository probe succeeded the refresh might fail - # with network timeout. In that case disable the repository to avoid implicitly - # refreshing it again in the Pkg.SourceLoad call which could time out again, effectively - # doubling the total timeout. - repo.disable! unless repo.refresh - else - repo.disable! - end - end - Yast::Pkg.SourceLoad - end - - # Deletes all the repositories - def delete_all - repositories.each(&:delete!) - repositories.clear - end - - private - - def clear_user_repositories - @repositories -= @user_repositories - @user_repositories.each(&:delete!) - @user_repositories.clear - - @unsigned_repos = [] - @gpg_fingerprints = {} - end - - def initialize - reset - end - end - end -end diff --git a/service/lib/agama/software/repository.rb b/service/lib/agama/software/repository.rb deleted file mode 100644 index 99a8d3232f..0000000000 --- a/service/lib/agama/software/repository.rb +++ /dev/null @@ -1,106 +0,0 @@ -# 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 "yast" -require "y2packager/repository" - -Yast.import "Pkg" - -module Agama - module Software - # This class represents a software repository - # - # It extends the `Y2Packager::Repository` with some methods in the context - # of Agama. - # - # @see RepositoriesManager - class Repository < Y2Packager::Repository - # delay before retrying (in seconds) - RETRY_DELAY = 5 - # number of automatic retries - RETRY_COUNT = 1 - - class << self - # Add a repository - # Override the Y2Packager::Repository.create which does not accept the alias option - # - # @param name [String] Name - # @param repo_alias [String] Unique alias, if missing some ugly name is generated - # @param url [URI::Generic, ZyppUrl] Repository URL - # @param product_dir [String] Product directory - # @param enabled [Boolean] Is the repository enabled? - # @param autorefresh [Boolean] Is auto-refresh enabled for this repository? - # @param priority [Integer] Repository priority, the lower number the higher (!) - # priority, the default libzypp priority is 99 - # @return [Y2Packager::Repository,nil] New repository or nil if creation failed - def create(name:, url:, repo_alias: "", product_dir: "", enabled: true, autorefresh: true, - priority: 99) - - repo_id = Yast::Pkg.RepositoryAdd( - "name" => name, "base_urls" => [url.to_s], "enabled" => enabled, - "autorefresh" => autorefresh, "prod_dir" => product_dir, "alias" => repo_alias, - "priority" => priority - ) - - repo_id ? find(repo_id) : nil - end - end - - # Probes a repository - # - # @return [Boolean] true if the repository can be read; false otherwise - def probe - attempt = 1 - type = nil - - loop do - # on a timeout error the result is nil, retry automatically in that case, - # note: callbacks are disabled during repo probing call - type = Yast::Pkg.RepositoryProbe(url.to_s, product_dir) - break if !type.nil? || attempt > RETRY_COUNT - - sleep(RETRY_DELAY) - attempt += 1 - end - - !!type && type != "NONE" - end - - def loaded? - @loaded - end - - def refresh - attempt = 1 - - loop do - @loaded = !!super - break if @loaded || attempt > RETRY_COUNT - - sleep(RETRY_DELAY) - attempt += 1 - end - - @loaded - end - end - end -end diff --git a/service/lib/agama/storage/finisher.rb b/service/lib/agama/storage/finisher.rb index 31ecf18bd1..0f08409763 100644 --- a/service/lib/agama/storage/finisher.rb +++ b/service/lib/agama/storage/finisher.rb @@ -28,8 +28,6 @@ require "y2storage/storage_manager" require "agama/with_progress_manager" require "agama/helpers" -require "agama/http" -require "agama/network" require "abstract_method" require "fileutils" @@ -46,11 +44,9 @@ class Finisher # Constructor # @param logger [Logger] # @param config [Config] - # @param security [Security] - def initialize(logger, config, security) + def initialize(logger, config) @logger = logger @config = config - @security = security end # Execute the final storage actions, reporting the progress @@ -75,20 +71,14 @@ def run # @return [Config] attr_reader :config - # @return [Security] - attr_reader :security - # All possible steps, that may or not need to be executed def possible_steps [ - SecurityStep.new(logger, security), CopyFilesStep.new(logger), StorageStep.new(logger), IscsiStep.new(logger), BootloaderStep.new(logger), SnapshotsStep.new(logger), - FilesStep.new(logger), - PostScripts.new(logger), CopyLogsStep.new(logger), UnmountStep.new(logger) ] @@ -174,23 +164,6 @@ def glob_files end end - # Step to write the security settings - class SecurityStep < Step - # Constructor - def initialize(logger, security) - super(logger) - @security = security - end - - def label - _("Writing Linux Security Modules configuration") - end - - def run - @security.write - end - end - # Step to write the bootloader configuration class BootloaderStep < Step def label @@ -282,64 +255,6 @@ def logs_dir end end - # Executes post-installation scripts - class PostScripts < Step - def label - _("Running user-defined scripts") - end - - def run - run_post_scripts - enable_init_scripts - end - - private - - # Run the post scripts - def run_post_scripts - network.link_resolv - client = Agama::HTTP::Clients::Scripts.new(logger) - client.run("post") - ensure - network.unlink_resolv - end - - def network - @network ||= Agama::Network.new(logger) - end - - # Enables the agama-scripts service to run init scripts - # - # The package agama-scripts is only installed when needed. - # So this method just tries to enable the service. - def enable_init_scripts - # systemctl will return 1 if the service does not exist. - Yast::Execute.on_target!( - "systemctl", "enable", "agama-scripts", - allowed_exitstatus: [0, 1] - ) - end - end - - # Executes post-installation scripts - class FilesStep < Step - def label - _("Deploying user-defined files") - end - - def run - deploy_files - end - - private - - def deploy_files - require "agama/http" - client = Agama::HTTP::Clients::Files.new(logger) - client.write - end - end - # Step to unmount the target file-systems class UnmountStep < Step def label diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index 92c8e427eb..92070590d9 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -21,7 +21,6 @@ require "agama/http/clients" require "agama/issue" -require "agama/security" require "agama/storage/actions_generator" require "agama/storage/bootloader" require "agama/storage/callbacks" @@ -38,8 +37,6 @@ require "y2storage/luks" require "y2storage/storage_manager" -Yast.import "PackagesProposal" - module Agama module Storage # Manager to handle storage configuration @@ -121,12 +118,12 @@ def add_packages return if packages.empty? logger.info "Selecting these packages for installation: #{packages}" - Yast::PackagesProposal.SetResolvables(PROPOSAL_ID, :package, packages) + http_client.set_resolvables(PROPOSAL_ID, :package, packages) end # Performs the final steps on the target file system(s). def finish - Finisher.new(logger, product_config, security).run + Finisher.new(logger, product_config).run end # Storage proposal manager @@ -164,13 +161,6 @@ def configure_locale(locale) update_issues end - # Security manager - # - # @return [Security] - def security - @security ||= Security.new(logger, product_config) - end - # Issues from the system # # @return [Array] @@ -237,6 +227,10 @@ def candidate_devices_issue def questions_client @questions_client ||= Agama::HTTP::Clients::Questions.new(logger) end + + def http_client + @http_client = Agama::HTTP::Clients::Main.new(::Logger.new($stdout)) + end end end end diff --git a/service/lib/agama/users.rb b/service/lib/agama/users.rb deleted file mode 100644 index c0a250288e..0000000000 --- a/service/lib/agama/users.rb +++ /dev/null @@ -1,221 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "yast" -require "y2users" -require "y2users/linux" # FIXME: linux is not in y2users file -require "yast2/execute" -require "y2firewall/firewalld" -require "agama/helpers" -require "agama/issue" -require "agama/with_issues" - -Yast.import "Service" - -module Agama - # Backend class using YaST code. - # - # {Agama::DBus::Users} wraps it with a D-Bus interface and - class Users - include Helpers - include WithIssues - include Yast::I18n - - def initialize(logger) - textdomain "agama" - @logger = logger - update_issues - end - - def root_ssh_key=(value) - root_user.authorized_keys = [value] # just one supported for now - update_issues - end - - def assign_root_password(value, hashed) - pwd = if hashed - Y2Users::Password.create_encrypted(value) - else - Y2Users::Password.create_plain(value) - end - - logger.info "Updating the user password" - root_user.password = value.empty? ? nil : pwd - update_issues - end - - # Root user - # - # @return [Y2Users::User] - def root_user - return @root_user if @root_user - - @root_user = config.users.root - return @root_user if @root_user - - @root_user = Y2Users::User.create_root - config.attach(@root_user) - @root_user - end - - # First created user - # - # @return [Y2Users::User, nil] - def first_user - config.users.reject(&:root?).first - end - - # Clears the root password - def remove_root_password - root_user.password = nil - update_issues - end - - # It adds the user with the given parameters to the login config only if there are no error - # issues detected like no user_name or no password given. - # - # @param full_name [String] - # @param user_name [String] - # @param password [String] - # @param hashed_password [Boolean] true = hashed password, false = plain text password - # @param _data [Hash] - # @return [Array] the list of fatal issues found - def assign_first_user(full_name, user_name, password, hashed_password, _data) - remove_first_user - - user = Y2Users::User.new(user_name) - user.gecos = [full_name] - user.password = if hashed_password - Y2Users::Password.create_encrypted(password) - else - Y2Users::Password.create_plain(password) - end - - fatal_issues = user.issues.map.select(&:error?) - return fatal_issues.map(&:message) unless fatal_issues.empty? - - config.attach(user) - add_user_to_group(user_name, "wheel") - update_issues - [] - end - - # Removes the first user - def remove_first_user - old_users = config.users.reject(&:root?) - config.detach(old_users) unless old_users.empty? - update_issues - end - - def write - without_run_mount do - on_target do - # if root ssh key is specified ensure that sshd running and firewall has port opened - enable_ssh if root_ssh_key? - - # disable root password if not set - assign_root_password("!", true) unless root_password? - - system_config = Y2Users::ConfigManager.instance.system(force_read: true) - target_config = system_config.copy - Y2Users::ConfigMerger.new(target_config, config).merge - - writer = Y2Users::Linux::Writer.new(target_config, system_config) - issues = writer.write - logger.error(issues.inspect) unless issues.empty? - end - end - end - - # Recalculates the list of issues - def update_issues - new_issues = [] - - unless root_password? || root_ssh_key? || first_user? - new_issues << Issue.new( - _("Defining a user, setting the root password or a SSH public key is required"), - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR - ) - end - - self.issues = new_issues - end - - private - - attr_reader :logger - - # Determines whether a first user is defined or not - # - # @return [Boolean] - def first_user? - config.users.reject(&:root?).any? - end - - def without_run_mount(&block) - Yast::Execute.locally!("/usr/bin/umount", "/mnt/run") - block.call - ensure - Yast::Execute.locally!("/usr/bin/mount", "-o", "bind", "/run", "/mnt/run") - end - - def config - return @config if @config - - @config = Y2Users::ConfigManager.instance.target - if !@config - @config = Y2Users::Config.new - Y2Users::ConfigManager.instance.target = @config - end - - @config - end - - # NOTE: the root user is created if it does not exist - def root_password? - !!root_user.password_content - end - - def root_ssh_key - root_user.authorized_keys.first || "" - end - - def root_ssh_key? - !root_ssh_key.empty? - end - - def enable_ssh - logger.info "root SSH public key is set, enabling sshd and opening the firewall" - Yast::Service.Enable("sshd") - firewalld = Y2Firewall::Firewalld.instance - # open port only if firewalld is installed, otherwise it will crash - firewalld.api.add_service(firewalld.default_zone, "ssh") if firewalld.installed? - end - - def add_user_to_group(user_name, group_name) - group = config.groups.by_name(group_name) - group ||= Y2Users::Group.new(group_name) - group.users_name << user_name - config.attach(group) unless group.attached? - end - end -end diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index f9dbc6f76f..fa6eb2f194 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -11,7 +11,6 @@ BuildRequires: dbus-1-common Requires: dbus-1-common Requires: dbus-1-daemon - Requires: suseconnect-ruby-bindings # YaST dependencies Requires: autoyast2-installation # ArchFilter @@ -24,7 +23,6 @@ Requires: yast2-network Requires: yast2-proxy Requires: yast2-storage-ng >= 5.0.31 - Requires: yast2-users %ifarch s390 s390x Requires: yast2-s390 >= 4.6.4 Requires: yast2-reipl diff --git a/service/share/agama-proxy-setup.service b/service/share/agama-proxy-setup.service deleted file mode 100644 index 9436a5fc4b..0000000000 --- a/service/share/agama-proxy-setup.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Configure wide proxy setup for agama and systemd services -Before=agama.service -Wants=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/bin/agama-proxy-setup - -[Install] -WantedBy=multi-user.target - diff --git a/service/share/dbus.conf b/service/share/dbus.conf index e7dcecf975..a7d72596c1 100644 --- a/service/share/dbus.conf +++ b/service/share/dbus.conf @@ -36,14 +36,10 @@ - - - - diff --git a/service/share/org.opensuse.Agama.Manager1.service b/service/share/org.opensuse.Agama.Manager1.service deleted file mode 100644 index 58fbc754ea..0000000000 --- a/service/share/org.opensuse.Agama.Manager1.service +++ /dev/null @@ -1,4 +0,0 @@ -[D-BUS Service] -Name=org.opensuse.Agama.Manager1 -Exec=/usr/bin/agamactl manager -User=root diff --git a/service/test/agama/dbus/clients/manager_test.rb b/service/test/agama/dbus/clients/manager_test.rb deleted file mode 100644 index 9b3ff7e488..0000000000 --- a/service/test/agama/dbus/clients/manager_test.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../../test_helper" -require_relative "with_service_status_examples" -require_relative "with_progress_examples" -require "dbus" -require "agama/dbus/clients/manager" -require "agama/dbus/manager" -require "agama/installation_phase" - -describe Agama::DBus::Clients::Manager do - before do - allow(Agama::DBus::Bus).to receive(:current).and_return(bus) - allow(bus).to receive(:service).with("org.opensuse.Agama.Manager1").and_return(service) - allow(service).to receive(:[]).with("/org/opensuse/Agama/Manager1") - .and_return(dbus_object) - allow(dbus_object).to receive(:introspect) - allow(dbus_object).to receive(:[]).with("org.opensuse.Agama.Manager1") - .and_return(manager_iface) - allow(dbus_object).to receive(:[]).with("org.freedesktop.DBus.Properties") - .and_return(properties_iface) - end - - let(:bus) { instance_double(Agama::DBus::Bus) } - let(:service) { instance_double(DBus::ProxyService) } - let(:dbus_object) { instance_double(DBus::ProxyObject) } - let(:manager_iface) { instance_double(DBus::ProxyObjectInterface) } - let(:properties_iface) { instance_double(DBus::ProxyObjectInterface, on_signal: nil) } - - subject { described_class.new } - - describe "#Probe" do - # Using partial double because methods are dynamically added to the proxy object - let(:dbus_object) { double(DBus::ProxyObject) } - - it "starts the config phase" do - expect(dbus_object).to receive(:Probe) - - subject.probe - end - end - - describe "#commit" do - # Using partial double because methods are dynamically added to the proxy object - let(:dbus_object) { double(DBus::ProxyObject) } - - it "starts the install phase" do - expect(dbus_object).to receive(:Commit) - - subject.commit - end - end - - describe "#current_installation_phase" do - before do - expect(manager_iface).to receive(:[]).with("CurrentInstallationPhase") - .and_return(current_phase) - end - - context "when the current phase is startup" do - let(:current_phase) { Agama::DBus::Manager::STARTUP_PHASE } - - it "returns the startup phase value" do - expect(subject.current_installation_phase).to eq(Agama::InstallationPhase::STARTUP) - end - end - - context "when the current phase is config" do - let(:current_phase) { Agama::DBus::Manager::CONFIG_PHASE } - - it "returns the config phase value" do - expect(subject.current_installation_phase).to eq(Agama::InstallationPhase::CONFIG) - end - end - - context "when the current phase is install" do - let(:current_phase) { Agama::DBus::Manager::INSTALL_PHASE } - - it "returns the install phase value" do - expect(subject.current_installation_phase).to eq(Agama::InstallationPhase::INSTALL) - end - end - - context "when the current phase is finish" do - let(:current_phase) { Agama::DBus::Manager::FINISH_PHASE } - - it "returns the install phase value" do - expect(subject.current_installation_phase).to eq(Agama::InstallationPhase::FINISH) - end - end - end - - include_examples "service status" - include_examples "progress" -end diff --git a/service/test/agama/dbus/clients/network_test.rb b/service/test/agama/dbus/clients/network_test.rb deleted file mode 100644 index 2de638560f..0000000000 --- a/service/test/agama/dbus/clients/network_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../../test_helper" -require "agama/dbus/clients/network" - -describe Agama::DBus::Clients::Network do - before do - allow(DBus::SystemBus).to receive(:instance).and_return(bus) - allow(bus).to receive(:service).with("org.freedesktop.NetworkManager").and_return(service) - allow(service).to receive(:[]).with("/org/freedesktop/NetworkManager") - .and_return(dbus_object) - allow(dbus_object).to receive(:introspect) - allow(dbus_object).to receive(:[]).with("org.freedesktop.NetworkManager") - .and_return(network_iface) - end - - let(:bus) { instance_double(DBus::SystemBus) } - let(:service) { instance_double(DBus::ProxyService) } - let(:dbus_object) { instance_double(DBus::ProxyObject) } - let(:network_iface) { instance_double(DBus::ProxyObjectInterface) } - - describe "#on_connection_changed" do - it "registers a callback for the StateChanged signal" do - expect(network_iface).to receive(:on_signal).with("StateChanged") - subject.on_connection_changed { "test" } - end - end -end diff --git a/service/test/agama/dbus/clients/software_test.rb b/service/test/agama/dbus/clients/software_test.rb deleted file mode 100644 index 09fc5dedce..0000000000 --- a/service/test/agama/dbus/clients/software_test.rb +++ /dev/null @@ -1,244 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2024] 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_relative "../../../test_helper" -require_relative "with_issues_examples" -require_relative "with_service_status_examples" -require_relative "with_progress_examples" -require "agama/dbus/clients/software" -require "agama/dbus/service_status" -require "agama/dbus/interfaces/service_status" -require "dbus" - -describe Agama::DBus::Clients::Software do - before do - allow(Agama::DBus::Bus).to receive(:current).and_return(bus) - allow(bus).to receive(:service).with("org.opensuse.Agama.Software1").and_return(service) - allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1") - .and_return(dbus_object) - allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1/Product") - .and_return(dbus_product) - allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1/Proposal") - .and_return(dbus_proposal) - allow(dbus_object).to receive(:[]).with("org.opensuse.Agama.Software1") - .and_return(software_iface) - allow(dbus_product).to receive(:[]).with("org.opensuse.Agama.Software1.Product") - .and_return(product_iface) - allow(dbus_product).to receive(:[]).with("org.freedesktop.DBus.Properties") - .and_return(properties_iface) - end - - let(:bus) { instance_double(Agama::DBus::Bus) } - let(:service) { instance_double(::DBus::ProxyService) } - let(:dbus_object) { instance_double(::DBus::ProxyObject, introspect: nil) } - let(:dbus_product) { instance_double(::DBus::ProxyObject, introspect: nil) } - let(:dbus_proposal) { instance_double(::DBus::ProxyObject, introspect: nil) } - let(:software_iface) { instance_double(::DBus::ProxyObjectInterface) } - let(:properties_iface) { instance_double(::DBus::ProxyObjectInterface) } - let(:product_iface) { instance_double(::DBus::ProxyObjectInterface) } - - subject { described_class.new } - - describe "#available_products" do - before do - allow(product_iface).to receive(:[]).with("AvailableProducts").and_return( - [ - ["Tumbleweed", "openSUSE Tumbleweed", {}], - ["Leap15.3", "openSUSE Leap 15.3", {}] - ] - ) - end - - it "returns the name and display name for all available products" do - expect(subject.available_products).to contain_exactly( - ["Tumbleweed", "openSUSE Tumbleweed"], - ["Leap15.3", "openSUSE Leap 15.3"] - ) - end - end - - describe "#selected_product" do - before do - allow(product_iface).to receive(:[]).with("SelectedProduct").and_return(product) - end - - context "when there is no selected product" do - let(:product) { "" } - - it "returns nil" do - expect(subject.selected_product).to be_nil - end - end - - context "when there is a selected product" do - let(:product) { "Tumbleweed" } - - it "returns the name of the selected product" do - expect(subject.selected_product).to eq("Tumbleweed") - end - end - end - - describe "#select_product" do - # Using partial double because methods are dynamically added to the proxy object - let(:dbus_product) { double(::DBus::ProxyObject, introspect: nil) } - - it "selects the given product" do - expect(dbus_product).to receive(:SelectProduct).with("Tumbleweed") - - subject.select_product("Tumbleweed") - end - end - - describe "#probe" do - let(:dbus_object) { double(::DBus::ProxyObject, introspect: nil, Probe: nil) } - - it "calls the D-Bus Probe method" do - expect(dbus_object).to receive(:Probe) - - subject.probe - end - - context "when a block is given" do - it "passes the block to the Probe method (async)" do - callback = proc {} - expect(dbus_object).to receive(:Probe) do |&block| - expect(block).to be(callback) - end - - subject.probe(&callback) - end - end - end - - describe "#provisions_selected" do - let(:dbus_object) { double(::DBus::ProxyObject, introspect: nil) } - - it "returns true/false for every tag given" do - expect(dbus_object).to receive(:ProvisionsSelected) - .with(["sddm", "gdm"]).and_return([true, false]) - expect(subject.provisions_selected?(["sddm", "gdm"])) - .to eq([true, false]) - end - end - - describe "#package_installed?" do - let(:dbus_object) do - double(::DBus::ProxyObject, introspect: nil, IsPackageInstalled: installed?) - end - - let(:package) { "NetworkManager" } - - context "when the package is installed" do - let(:installed?) { true } - - it "returns true" do - expect(subject.package_installed?(package)).to eq(true) - end - end - - context "when the package is installed" do - let(:installed?) { false } - - it "returns false" do - expect(subject.package_installed?(package)).to eq(false) - end - end - end - - describe "#package_available?" do - let(:dbus_object) do - double(::DBus::ProxyObject, introspect: nil, IsPackageAvailable: available) - end - - let(:package) { "NetworkManager" } - - context "when the package is available" do - let(:available) { true } - - it "returns true" do - expect(subject.package_available?(package)).to eq(true) - end - end - - context "when the package is available" do - let(:available) { false } - - it "returns false" do - expect(subject.package_available?(package)).to eq(false) - end - end - end - - describe "#on_product_selected" do - before do - allow(dbus_product).to receive(:path).and_return("/org/opensuse/Agama/Test") - allow(properties_iface).to receive(:on_signal) - end - - context "if there are no callbacks for changes in properties" do - it "subscribes to properties change signal" do - expect(properties_iface).to receive(:on_signal) - subject.on_product_selected { "test" } - end - end - - context "if there already are callbacks for changes in properties" do - before do - subject.on_product_selected { "test" } - end - - it "does not subscribe to properties change signal again" do - expect(properties_iface).to_not receive(:on_signal) - subject.on_product_selected { "test" } - end - end - end - - describe "#on_probe_finished" do - before do - allow(dbus_object).to receive(:path).and_return("/org/opensuse/Agama/Test") - allow(software_iface).to receive(:on_signal) - end - - context "if there are no callbacks for the signal" do - it "subscribes to the signal" do - expect(software_iface).to receive(:on_signal) - subject.on_probe_finished { "test" } - end - end - - context "if there already are callbacks for the signal" do - before do - subject.on_probe_finished { "test" } - end - - it "does not subscribe to the signal again" do - expect(software_iface).to_not receive(:on_signal) - subject.on_probe_finished { "test" } - end - end - end - - include_examples "issues" - include_examples "service status" - include_examples "progress" -end diff --git a/service/test/agama/dbus/manager_service_test.rb b/service/test/agama/dbus/manager_service_test.rb deleted file mode 100644 index 2155cfe098..0000000000 --- a/service/test/agama/dbus/manager_service_test.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../test_helper" -require "agama/dbus/manager_service" -require "agama/config" - -describe Agama::DBus::ManagerService do - subject(:service) { described_class.new(config, logger) } - - let(:config) { Agama::Config.new } - let(:logger) { Logger.new($stdout, level: :warn) } - let(:manager) { Agama::Manager.new(config, logger) } - - let(:object_server) { instance_double(DBus::ObjectServer, export: nil) } - let(:bus) { instance_double(Agama::DBus::Bus, request_name: nil) } - - let(:manager_obj) { instance_double(Agama::DBus::Manager, path: "/org/opensuse/Agama/Users1") } - let(:users_obj) { instance_double(Agama::DBus::Users, path: "/org/opensuse/Agama/Users1") } - - before do - allow(Agama::DBus::Bus).to receive(:current).and_return(bus) - allow(bus).to receive(:request_service).with("org.opensuse.Agama.Manager1") - .and_return(object_server) - allow(Agama::Manager).to receive(:new).with(config, logger).and_return(manager) - allow(Agama::DBus::Manager).to receive(:new).with(manager, logger).and_return(manager_obj) - allow(Agama::DBus::Users).to receive(:new).and_return(users_obj) - end - - describe "#start" do - it "runs the startup phase" do - expect(manager).to receive(:startup_phase) - subject.start - end - end - - describe "#export" do - it "exports the manager and the user objects" do - expect(object_server).to receive(:export).with(manager_obj) - expect(object_server).to receive(:export).with(users_obj) - service.export - end - end - - describe "#dispatch" do - it "dispatches the messages from the bus" do - expect(bus).to receive(:dispatch_message_queue) - service.dispatch - end - end -end diff --git a/service/test/agama/dbus/manager_test.rb b/service/test/agama/dbus/manager_test.rb deleted file mode 100644 index 51264bbf28..0000000000 --- a/service/test/agama/dbus/manager_test.rb +++ /dev/null @@ -1,275 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../test_helper" -require "agama/dbus/manager" -require "agama/dbus/service_status" -require "agama/installation_phase" -require "agama/service_status_recorder" - -describe Agama::DBus::Manager do - subject { described_class.new(backend, logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - let(:service_status) { Agama::DBus::ServiceStatus.new.idle } - - let(:backend) do - instance_double(Agama::Manager, - installation_phase: installation_phase, - software: software_client, - on_services_status_change: nil, - valid?: true, - service_status: service_status) - end - - let(:installation_phase) { Agama::InstallationPhase.new } - let(:software_client) do - instance_double(Agama::DBus::Clients::Software, on_product_selected: nil) - end - let(:service_status_recorder) { Agama::ServiceStatusRecorder.new } - - let(:idle) { Agama::DBus::ServiceStatus::IDLE } - let(:busy) { Agama::DBus::ServiceStatus::BUSY } - let(:progress_interface) { Agama::DBus::Interfaces::Progress::PROGRESS_INTERFACE } - let(:service_status_interface) do - Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE - end - - before do - allow_any_instance_of(described_class).to receive(:register_progress_callbacks) - allow_any_instance_of(described_class).to receive(:register_service_status_callbacks) - end - - it "defines Progress D-Bus interface" do - expect(subject.intfs.keys).to include(progress_interface) - end - - it "defines ServiceStatus D-Bus interface" do - expect(subject.intfs.keys).to include(service_status_interface) - end - - describe ".new" do - it "configures callbacks for changes in the installation phase" do - expect(subject).to receive(:dbus_properties_changed) do |iface, properties, _| - expect(iface).to match(/Agama\.Manager1/) - expect(properties["CurrentInstallationPhase"]).to eq(0) - end - - installation_phase.startup - end - - it "configures callbacks for changes in the status of other services" do - expect(backend).to receive(:on_services_status_change) - subject - end - - it "configures callbacks from Progress interface" do - expect_any_instance_of(described_class).to receive(:register_progress_callbacks) - subject - end - - it "configures callbacks from ServiceStatus interface" do - expect_any_instance_of(described_class).to receive(:register_service_status_callbacks) - subject - end - end - - describe "#config_phase" do - context "when the service is idle" do - before do - service_status.idle - end - - it "runs the config phase, setting the service as busy meanwhile" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:config_phase) - expect(subject.service_status).to receive(:idle) - - subject.config_phase - end - end - - context "when the service is busy" do - before do - service_status.busy - end - - it "raises a D-Bus error" do - expect { subject.config_phase }.to raise_error(DBus::Error) - end - end - end - - describe "#install_phase" do - context "when the service is idle" do - before do - service_status.idle - end - - it "runs the install phase, setting the service as busy meanwhile" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:install_phase) - expect(subject.service_status).to receive(:idle) - - subject.install_phase - end - end - - context "when services configuration is invalid" do - before do - allow(backend).to receive(:valid?).and_return(false) - end - - it "raises a DBus::Error" do - expect { subject.install_phase }.to raise_error(DBus::Error) - end - end - - context "when the service is busy" do - before do - service_status.busy - end - - it "raises a D-Bus error" do - expect { subject.install_phase }.to raise_error(DBus::Error) - end - end - end - - describe "#installation_phases" do - it "includes all possible values for the installation phase" do - labels = subject.installation_phases.map { |i| i["label"] } - expect(labels).to contain_exactly("startup", "config", "install", "finish") - end - - it "associates 'startup' with the id 0" do - startup = subject.installation_phases.find { |i| i["label"] == "startup" } - expect(startup["id"]).to eq(described_class::STARTUP_PHASE) - end - - it "associates 'config' with the id 1" do - config = subject.installation_phases.find { |i| i["label"] == "config" } - expect(config["id"]).to eq(described_class::CONFIG_PHASE) - end - - it "associates 'install' with the id 2" do - install = subject.installation_phases.find { |i| i["label"] == "install" } - expect(install["id"]).to eq(described_class::INSTALL_PHASE) - end - - it "associates 'finish' with the id 3" do - install = subject.installation_phases.find { |i| i["label"] == "finish" } - expect(install["id"]).to eq(described_class::FINISH_PHASE) - end - end - - describe "#current_installation_phase" do - context "when the current installation phase is startup" do - before do - installation_phase.startup - end - - it "returns the startup phase vale" do - expect(subject.current_installation_phase).to eq(described_class::STARTUP_PHASE) - end - end - - context "when the current installation phase is config" do - before do - installation_phase.config - end - - it "returns the config phase value" do - expect(subject.current_installation_phase).to eq(described_class::CONFIG_PHASE) - end - end - - context "when the current installation phase is install" do - before do - installation_phase.install - end - - it "returns the install phase value" do - expect(subject.current_installation_phase).to eq(described_class::INSTALL_PHASE) - end - end - end - - describe "#busy_services" do - before do - allow(backend).to receive(:busy_services).and_return(["org.opensuse.Agama.Software1"]) - end - - it "returns the names of the busy services" do - expect(subject.busy_services).to contain_exactly("org.opensuse.Agama.Software1") - end - end - - describe "#can_install?" do - before do - allow(backend).to receive(:valid?).and_return(valid?) - end - - context "when installation settings are valid" do - let(:valid?) { true } - - it "returns true" do - expect(subject.can_install?).to eq(true) - end - end - - context "when installation settings are valid" do - let(:valid?) { false } - - it "returns false" do - expect(subject.can_install?).to eq(false) - end - end - end - - describe "#finish_phase" do - let(:finished) { true } - - before do - allow(backend).to receive(:finish_installation).and_return(finished) - end - - it "finish the installation with the method given" do - method = "reboot" - expect(backend).to receive(:finish_installation).with(method) - subject.finish_phase("reboot") - end - - context "when finished" do - it "returns true" do - expect(subject.finish_phase("poweroff")).to eq(true) - end - end - - context "when not finished" do - let(:finished) { false } - - it "returns false" do - expect(subject.finish_phase("halt")).to eq(false) - end - end - end -end diff --git a/service/test/agama/dbus/service_runner_test.rb b/service/test/agama/dbus/service_runner_test.rb index 1f3632bf8b..5be98a4492 100644 --- a/service/test/agama/dbus/service_runner_test.rb +++ b/service/test/agama/dbus/service_runner_test.rb @@ -22,22 +22,22 @@ require_relative "../../test_helper" require "agama/config" require "agama/dbus/service_runner" -require "agama/dbus/manager_service" -require "agama/manager" +require "agama/dbus/storage_service" +require "agama/storage/manager" describe Agama::DBus::ServiceRunner do describe "#run" do - subject(:runner) { Agama::DBus::ServiceRunner.new(:manager) } + subject(:runner) { Agama::DBus::ServiceRunner.new(:storage) } let(:config) { Agama::Config.new } let(:logger) { Logger.new($stdout) } - let(:manager) { Agama::Manager.new(config, logger) } - let(:service) { instance_double(Agama::DBus::ManagerService) } + let(:storage) { instance_double(Agama::Storage::Manager) } + let(:service) { instance_double(Agama::DBus::StorageService, start: nil) } before do allow(Agama::Config).to receive(:current).and_return(config) - allow(Agama::Manager).to receive(:new).with(config).and_return(manager) - allow(Agama::DBus::ManagerService).to receive(:new).with(config, Logger) + allow(Agama::Storage::Manager).to receive(:new).with(config).and_return(storage) + allow(Agama::DBus::StorageService).to receive(:new).with(config, Logger) .and_return(service) end diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb deleted file mode 100644 index db093d4a48..0000000000 --- a/service/test/agama/dbus/software/manager_test.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2024] 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_relative "../../../test_helper" -require "agama/config" -require "agama/dbus/clients/network" -require "agama/dbus/interfaces/issues" -require "agama/dbus/interfaces/progress" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/software/manager" -require "agama/software" - -describe Agama::DBus::Software::Manager do - subject { described_class.new(backend, logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:backend) { Agama::Software::Manager.new(config, logger) } - - let(:target_dir) { Dir.mktmpdir } - - let(:config) { Agama::Config.new(config_data) } - - let(:config_data) do - path = File.join(FIXTURES_PATH, "root_dir/etc/agama.yaml") - YAML.safe_load(File.read(path)) - end - - let(:progress_interface) { Agama::DBus::Interfaces::Progress::PROGRESS_INTERFACE } - - let(:service_status_interface) do - Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE - end - - let(:issues_interface) { Agama::DBus::Interfaces::Issues::ISSUES_INTERFACE } - - before do - stub_const("Agama::Software::Manager::TARGET_DIR", target_dir) - allow(Yast::PackageCallbacks).to receive(:InitPackageCallbacks) - allow(Agama::DBus::Clients::Network).to receive(:new).and_return(network_client) - allow(backend).to receive(:probe) - allow(backend).to receive(:propose) - allow(backend).to receive(:install) - allow(backend).to receive(:finish) - allow(subject).to receive(:dbus_properties_changed) - end - - after do - FileUtils.rm_r(target_dir) - end - - let(:network_client) do - instance_double(Agama::DBus::Clients::Network, on_connection_changed: nil) - end - - it "defines Issues D-Bus interface" do - expect(subject.intfs.keys).to include(issues_interface) - end - - it "defines Progress D-Bus interface" do - expect(subject.intfs.keys).to include(progress_interface) - end - - it "defines ServiceStatus D-Bus interface" do - expect(subject.intfs.keys).to include(service_status_interface) - end - - it "emits signal when issues changes" do - expect(subject).to receive(:issues_properties_changed) - backend.issues = [] - end - - describe "#probe" do - it "runs the probing, setting the service as busy meanwhile, and emits a signal" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:probe) - expect(subject.service_status).to receive(:idle) - expect(subject).to receive(:ProbeFinished) - - subject.probe - end - end - - describe "#propose" do - it "calculates the proposal, setting the service as busy meanwhile" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:propose) - expect(subject.service_status).to receive(:idle) - - subject.propose - end - end - - describe "#install" do - it "installs the software, setting the service as busy meanwhile" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:install) - expect(subject.service_status).to receive(:idle) - - subject.install - end - end - - describe "#finish" do - it "finishes the software installation, setting the service as busy meanwhile" do - expect(subject.service_status).to receive(:busy) - expect(backend).to receive(:finish) - expect(subject.service_status).to receive(:idle) - - subject.finish - end - end - - describe "D-Bus IsPackageInstalled" do - it "returns whether the package is installed or not" do - expect(backend).to receive(:package_installed?).with("NetworkManager").and_return(true) - installed = subject.public_send( - "org.opensuse.Agama.Software1%%IsPackageInstalled", "NetworkManager" - ) - expect(installed).to eq(true) - end - end - - describe "D-Bus IsPackageAvailable" do - it "returns whether the package is available or not" do - expect(backend).to receive(:package_available?).with("NetworkManager").and_return(true) - available = subject.public_send( - "org.opensuse.Agama.Software1%%IsPackageAvailable", "NetworkManager" - ) - expect(available).to eq(true) - end - end -end diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb deleted file mode 100644 index 3516950e7a..0000000000 --- a/service/test/agama/dbus/software/product_test.rb +++ /dev/null @@ -1,440 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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_relative "../../../test_helper" -require "agama/dbus/software/product" -require "agama/config" -require "agama/registration" -require "agama/software/manager" -require "suse/connect" - -describe Agama::DBus::Software::Product do - subject { described_class.new(backend, logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:backend) { Agama::Software::Manager.new(config, logger) } - - let(:config) { Agama::Config.new } - - let(:target_dir) { Dir.mktmpdir } - - before do - stub_const("Agama::Software::Manager::TARGET_DIR", target_dir) - allow(Yast::PackageCallbacks).to receive(:InitPackageCallbacks) - allow(config).to receive(:products).and_return(products) - allow(subject).to receive(:dbus_properties_changed) - allow(Agama::ProductReader).to receive(:new).and_call_original - end - - after do - FileUtils.rm_r(target_dir) - end - - let(:products) do - { "Tumbleweed" => {}, "MicroOS" => {} } - end - - it "defines Product D-Bus interface" do - expect(subject.intfs.keys).to include("org.opensuse.Agama.Software1.Product") - end - - it "defines Registration D-Bus interface" do - expect(subject.intfs.keys).to include("org.opensuse.Agama1.Registration") - end - - it "defines Issues D-Bus interface" do - expect(subject.intfs.keys).to include("org.opensuse.Agama1.Issues") - end - - describe "select_product" do - context "if the product is correctly selected" do - it "returns result code 0 with empty description" do - expect(subject.select_product("Tumbleweed")).to contain_exactly(0, "") - end - end - - context "if the given product is already selected" do - before do - subject.select_product("Tumbleweed") - end - - it "returns result code 1 and description" do - expect(subject.select_product("Tumbleweed")).to contain_exactly(1, /already selected/) - end - end - - context "if the current product is registered" do - before do - subject.select_product("MicroOS") - allow(backend.registration).to receive(:registered).and_return(true) - end - - it "returns result code 2 and description" do - expect(subject.select_product("Tumbleweed")).to contain_exactly(2, /must be deregistered/) - end - end - - context "if the product is unknown" do - it "returns result code 3 and description" do - expect(subject.select_product("Unknown")).to contain_exactly(3, /unknown product/i) - end - end - end - - describe "#registered" do - before do - allow(backend.registration).to receive(:registered).and_return(registered) - end - - context "if there is no registered product yet" do - let(:registered) { false } - - it "returns false" do - expect(subject.registered).to eq(false) - end - end - - context "if there is a registered product" do - let(:registered) { true } - - it "returns true" do - expect(subject.registered).to eq(true) - end - end - end - - describe "#url" do - before do - allow(backend.registration).to receive(:registration_url).and_return(url) - end - - context "if there is no registration url yet" do - let(:url) { nil } - - it "returns an empty string" do - expect(subject.url).to eq("") - end - end - - context "if there is a registration url" do - let(:url) { "https://example.com" } - - it "returns the registration url" do - expect(subject.url).to eq("https://example.com") - end - end - end - - describe "#reg_code" do - before do - allow(backend.registration).to receive(:reg_code).and_return(reg_code) - end - - context "if there is no registration code yet" do - let(:reg_code) { nil } - - it "returns an empty string" do - expect(subject.reg_code).to eq("") - end - end - - context "if there is a registration code" do - let(:reg_code) { "123XX432" } - - it "returns the registration code" do - expect(subject.reg_code).to eq("123XX432") - end - end - end - - describe "#email" do - before do - allow(backend.registration).to receive(:email).and_return(email) - end - - context "if there is no registration email yet" do - let(:email) { nil } - - it "returns an empty string" do - expect(subject.email).to eq("") - end - end - - context "if there is a registration email" do - let(:email) { "test@suse.com" } - - it "returns the registration email" do - expect(subject.email).to eq("test@suse.com") - end - end - end - - describe "#register" do - before do - allow(backend.registration).to receive(:reg_code).and_return(nil) - end - - context "if there is no product selected yet" do - it "returns result code 1 and description" do - expect(subject.register("123XX432")).to contain_exactly(1, /product not selected/i) - end - end - - context "if there is a selected product" do - before do - backend.select_product("Tumbleweed") - - allow(backend.product).to receive(:repositories).and_return(repositories) - allow(backend.product).to receive(:registration).and_return(true) - end - - let(:repositories) { [] } - - context "if the product is already registered" do - it "returns result code 2 and description if the code is different" do - allow(backend.registration).to receive(:registered).and_return(true) - expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i) - end - - it "returns result code 0 and empty error if the code is the same" do - allow(backend.registration).to receive(:reg_code).and_return("123XX432") - allow(backend.registration).to receive(:registered).and_return(true) - expect(subject.register("123XX432")).to contain_exactly(0, "") - end - end - - context "if the product does not require registration" do - let(:repositories) { ["https://repo"] } - - before do - allow(backend.product).to receive(:registration).and_return(false) - end - - it "returns result code 3 and description" do - expect(subject.register("123XX432")).to contain_exactly(3, /not require registration/i) - end - end - - context "if there is a network error" do - before do - allow(backend.registration).to receive(:register).and_raise(SocketError) - end - - it "returns result code 4 and description" do - expect(subject.register("123XX432")).to contain_exactly(4, /network error/) - end - end - - context "if there is a timeout" do - before do - allow(backend.registration).to receive(:register).and_raise(Timeout::Error) - end - - it "returns result code 5 and description" do - expect(subject.register("123XX432")).to contain_exactly(5, /timeout/) - end - end - - context "if there is an API error" do - before do - allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "") - end - - it "returns result code 6 and description" do - expect(subject.register("123XX432")).to contain_exactly(6, /registration server failed/) - end - end - - context "if there is a missing credentials error" do - before do - allow(backend.registration) - .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile) - end - - it "returns result code 7 and description" do - expect(subject.register("123XX432")).to contain_exactly(7, /missing credentials/) - end - end - - context "if there is an incorrect credentials error" do - before do - allow(backend.registration) - .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile) - end - - it "returns result code 8 and description" do - expect(subject.register("123XX432")).to contain_exactly(8, /incorrect credentials/) - end - end - - context "if there is an invalid certificate error" do - before do - allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError) - end - - it "returns result code 9 and description" do - expect(subject.register("123XX432")).to contain_exactly(9, /invalid certificate/) - end - end - - context "if there is an internal error" do - before do - allow(backend.registration).to receive(:register).and_raise(JSON::ParserError) - end - - it "returns result code 10 and description" do - expect(subject.register("123XX432")).to contain_exactly(10, /registration server failed/) - end - end - - context "if there is any other error" do - before do - allow(backend.registration).to receive(:register).and_raise(RuntimeError) - end - - it "returns result code 14 and description" do - expect(subject.register("123XX432")).to contain_exactly(14, /registration server failed/) - end - end - - context "if the registration is correctly done" do - before do - allow(backend.registration).to receive(:register) - end - - it "returns result code 0 with empty description" do - expect(subject.register("123XX432")).to contain_exactly(0, "") - end - end - end - end - - describe "#deregister" do - before do - allow(backend.registration).to receive(:registered).and_return(true) - end - - context "if there is no product selected yet" do - it "returns result code 1 and description" do - expect(subject.deregister).to contain_exactly(1, /product not selected/i) - end - end - - context "if there is a selected product" do - before do - backend.select_product("Tumbleweed") - end - - context "if the product is not registered yet" do - before do - allow(backend.registration).to receive(:registered).and_return(false) - end - - it "returns result code 2 and description" do - expect(subject.deregister).to contain_exactly(2, /product not registered/i) - end - end - - context "if there is a network error" do - before do - allow(backend.registration).to receive(:deregister).and_raise(SocketError) - end - - it "returns result code 3 and description" do - expect(subject.deregister).to contain_exactly(3, /network error/) - end - end - - context "if there is a timeout" do - before do - allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error) - end - - it "returns result code 4 and description" do - expect(subject.deregister).to contain_exactly(4, /timeout/) - end - end - - context "if there is an API error" do - before do - allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "") - end - - it "returns result code 5 and description" do - expect(subject.deregister).to contain_exactly(5, /registration server failed/) - end - end - - context "if there is a missing credentials error" do - before do - allow(backend.registration) - .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile) - end - - it "returns result code 6 and description" do - expect(subject.deregister).to contain_exactly(6, /missing credentials/) - end - end - - context "if there is an incorrect credentials error" do - before do - allow(backend.registration) - .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile) - end - - it "returns result code 7 and description" do - expect(subject.deregister).to contain_exactly(7, /incorrect credentials/) - end - end - - context "if there is an invalid certificate error" do - before do - allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError) - end - - it "returns result code 8 and description" do - expect(subject.deregister).to contain_exactly(8, /invalid certificate/) - end - end - - context "if there is an internal error" do - before do - allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError) - end - - it "returns result code 9 and description" do - expect(subject.deregister).to contain_exactly(9, /registration server failed/) - end - end - - context "if the deregistration is correctly done" do - before do - allow(backend.registration).to receive(:deregister) - end - - it "returns result code 0 with empty description" do - expect(subject.deregister).to contain_exactly(0, "") - end - end - end - end -end diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 4e46f03755..030dc900f8 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -31,7 +31,6 @@ require "agama/storage/iscsi/manager" require "agama/storage/dasd/manager" require "agama/dbus/storage/dasds_tree" -require "agama/dbus/clients/software" require "y2storage" require "dbus" @@ -65,10 +64,6 @@ def parse(string) on_sessions_change: nil) end - let(:software) do - instance_double(Agama::DBus::Clients::Software, on_probe_finished: nil) - end - before do # Speed up tests by avoiding real check of TPM presence. allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) @@ -80,7 +75,6 @@ def parse(string) allow(backend).to receive(:on_issues_change) allow(backend).to receive(:actions).and_return([]) allow(backend).to receive(:iscsi).and_return(iscsi) - allow(backend).to receive(:software).and_return(software) allow(backend).to receive(:proposal).and_return(proposal) mock_storage(devicegraph: "empty-hd-50GiB.yaml") end diff --git a/service/test/agama/dbus/users_test.rb b/service/test/agama/dbus/users_test.rb deleted file mode 100644 index 4e60be3eae..0000000000 --- a/service/test/agama/dbus/users_test.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../test_helper" -require "agama/dbus/interfaces/issues" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/users" -require "agama/users" -require "y2users" - -describe Agama::DBus::Users do - subject { described_class.new(backend, logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:backend) { instance_double(Agama::Users, on_issues_change: nil) } - - let(:issues_interface) { Agama::DBus::Interfaces::Issues::ISSUES_INTERFACE } - - before do - allow_any_instance_of(described_class).to receive(:register_service_status_callbacks) - end - - it "defines ServiceStatus D-Bus interface" do - expect(subject.intfs.keys).to include( - Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE - ) - end - - it "defines Issues D-Bus interface" do - expect(subject.intfs.keys).to include(issues_interface) - end - - describe ".new" do - it "configures callbacks from ServiceStatus interface" do - expect_any_instance_of(described_class).to receive(:register_service_status_callbacks) - subject - end - - it "configures callbacks from Issues interface" do - expect(backend).to receive(:on_issues_change) - subject - end - end - - describe "first_user" do - before do - allow(backend).to receive(:first_user).and_return(user) - end - - context "if there is no user yet" do - let(:user) { nil } - - it "returns default data" do - expect(subject.first_user).to eq(["", "", "", false, {}]) - end - end - - context "if there is an user" do - let(:password) { Y2Users::Password.create_encrypted("12345") } - let(:user) do - instance_double(Y2Users::User, - full_name: "Test user", - name: "test", - password: password, - password_content: password.value.to_s) - end - - it "returns the first user data" do - expect(subject.first_user).to eq(["Test user", "test", password.value.to_s, true, {}]) - end - end - end -end diff --git a/service/test/agama/dbus/y2dir/modules/autologin_test.rb b/service/test/agama/dbus/y2dir/modules/autologin_test.rb deleted file mode 100644 index 3fbc34113c..0000000000 --- a/service/test/agama/dbus/y2dir/modules/autologin_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../../../../test_helper" -require_relative File.join( - SRC_PATH, "agama", "dbus", "y2dir", "modules", "Autologin.rb" -) - -describe Yast::Autologin do - subject { Yast::Autologin } - - before do - subject.main - allow(Agama::HTTP::Clients::Software).to receive(:new).and_return(client) - end - - let(:client) do - instance_double(Agama::HTTP::Clients::Software) - end - - describe "#supported?" do - before do - allow(client).to receive(:provisions_selected?).with(Array) - .and_return(provisions_selected?) - end - - context "when some display manager is selected" do - let(:provisions_selected?) { [true, false] } - - xit "returns true" do - expect(subject.supported?).to eq(true) - end - end - - context "when no display managers are selected" do - let(:provisions_selected?) { [false, false] } - - it "returns false" do - expect(subject.supported?).to eq(false) - end - end - end -end diff --git a/service/test/agama/http/clients/scripts_test.rb b/service/test/agama/http/clients/scripts_test.rb deleted file mode 100644 index b7a6517d6a..0000000000 --- a/service/test/agama/http/clients/scripts_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] 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_relative "../../../test_helper" -require "agama/http/clients/scripts" - -describe Agama::HTTP::Clients::Scripts do - subject(:scripts) { described_class.new(Logger.new($stdout)) } - let(:response) { instance_double(Net::HTTPResponse, body: "") } - - before do - allow(File).to receive(:read).with("/run/agama/token") - .and_return("123456") - end - - describe "#run" do - it "calls the end-point to run the scripts" do - url = URI("http://localhost/api/scripts/run") - expect(Net::HTTP).to receive(:post).with(url, "post".to_json, { - "Content-Type": "application/json", - Authorization: "Bearer 123456" - }).and_return(response) - scripts.run("post") - end - end -end diff --git a/service/test/agama/manager_test.rb b/service/test/agama/manager_test.rb deleted file mode 100644 index 539ac8e2b9..0000000000 --- a/service/test/agama/manager_test.rb +++ /dev/null @@ -1,287 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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_relative "../test_helper" -require_relative "with_progress_examples" -require "agama/manager" -require "agama/config" -require "agama/http" -require "agama/issue" -require "agama/question" -require "agama/dbus/service_status" -require "agama/users" - -describe Agama::Manager do - subject { described_class.new(config, logger) } - - let(:config_path) do - File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml") - end - let(:config) { Agama::Config.from_file(config_path) } - let(:logger) { Logger.new($stdout, level: :warn) } - let(:proxy) do - instance_double(Agama::ProxySetup, propose: nil, install: nil) - end - - let(:software) do - instance_double( - Agama::HTTP::Clients::Software, - probe: nil, install: nil, propose: nil, finish: nil, - config: { "product" => { "id" => product } }, errors?: false, - selected_product: product - ) - end - let(:users) do - instance_double( - Agama::Users, write: nil, issues: [] - ) - end - let(:http_client) { instance_double(Agama::HTTP::Clients::Main, install: nil) } - let(:network) { instance_double(Agama::Network, install: nil, startup: nil) } - let(:storage) do - instance_double( - Agama::DBus::Clients::Storage, probe: nil, install: nil, finish: nil, - :product= => nil - ) - end - let(:scripts) do - instance_double( - Agama::HTTP::Clients::Scripts, run: nil - ) - end - - let(:product) { nil } - - before do - allow(Agama::Network).to receive(:new).and_return(network) - allow(Agama::ProxySetup).to receive(:instance).and_return(proxy) - allow(Agama::HTTP::Clients::Main).to receive(:new).and_return(http_client) - allow(Agama::HTTP::Clients::Software).to receive(:new).and_return(software) - allow(Agama::DBus::Clients::Storage).to receive(:new).and_return(storage) - allow(Agama::Users).to receive(:new).and_return(users) - allow(Agama::HTTP::Clients::Scripts).to receive(:new) - .and_return(scripts) - end - - describe "#startup_phase" do - before do - allow(subject).to receive(:config_phase) - end - - it "sets the installation phase to startup" do - subject.startup_phase - expect(subject.installation_phase.startup?).to eq(true) - end - - it "does the network startup configuration" do - expect(network).to receive(:startup) - subject.startup_phase - end - - context "when there is no selected product" do - let(:product) { nil } - - it "does not run the config phase" do - expect(subject).to_not receive(:config_phase) - subject.startup_phase - end - end - - context "when there is a selected product" do - let(:product) { "Tumbleweed" } - - it "runs the config phase" do - expect(subject).to receive(:config_phase) - subject.startup_phase - end - end - end - - describe "#config_phase" do - let(:product) { "Geecko" } - - it "sets the installation phase to config" do - subject.config_phase - expect(subject.installation_phase.config?).to eq(true) - end - - it "sets the product for the storage module" do - expect(storage).to receive(:product=).with product - subject.config_phase - end - - it "calls #probe method for both software and storage modules if reprobe is requested" do - expect(storage).to receive(:probe) - expect(software).to receive(:probe) - subject.config_phase(reprobe: true) - end - - it "calls #probe method only for the software module if reprobe is not requested" do - expect(software).to receive(:probe) - expect(storage).to_not receive(:probe) - subject.config_phase(reprobe: false) - end - end - - describe "#install_phase" do - it "sets the installation phase to install and later to finish" do - expect(subject.installation_phase).to receive(:install) - subject.install_phase - expect(subject.installation_phase.finish?).to eq(true) - end - - it "calls #propose on proxy and software modules" do - expect(proxy).to receive(:propose) - expect(software).to receive(:propose) - subject.install_phase - end - - it "calls #install (or #write) method of each module" do - expect(network).to receive(:install) - expect(software).to receive(:install) - expect(software).to receive(:finish) - expect(http_client).to receive(:install) - expect(storage).to receive(:install) - expect(scripts).to receive(:run).with("postPartitioning") - expect(storage).to receive(:finish) - expect(users).to receive(:write) - subject.install_phase - end - end - - let(:idle) { Agama::DBus::ServiceStatus::IDLE } - let(:busy) { Agama::DBus::ServiceStatus::BUSY } - - describe "#busy_services" do - before do - allow(subject).to receive(:service_status_recorder).and_return(service_status_recorder) - - service_status_recorder.save("org.opensuse.Agama.Test1", busy) - service_status_recorder.save("org.opensuse.Agama.Test2", idle) - service_status_recorder.save("org.opensuse.Agama.Test3", busy) - end - - let(:service_status_recorder) { Agama::ServiceStatusRecorder.new } - - it "returns the name of the busy services" do - expect(subject.busy_services).to contain_exactly( - "org.opensuse.Agama.Test1", - "org.opensuse.Agama.Test3" - ) - end - end - - describe "#on_services_status_change" do - before do - allow(subject).to receive(:service_status_recorder).and_return(service_status_recorder) - end - - let(:service_status_recorder) { Agama::ServiceStatusRecorder.new } - - it "add a callback to be run when the status of a service changes" do - subject.on_services_status_change { logger.info("change status") } - - expect(logger).to receive(:info).with(/change status/) - service_status_recorder.save("org.opensuse.Agama.Test", busy) - end - end - - describe "#valid?" do - context "when there are not validation problems" do - it "returns true" do - expect(subject.valid?).to eq(true) - end - end - - context "when the users configuration is not valid" do - before do - allow(users).to receive(:issues).and_return([instance_double(Agama::Issue)]) - end - - it "returns false" do - expect(subject.valid?).to eq(false) - end - end - - context "when the software configuration is not valid" do - before do - allow(software).to receive(:errors?).and_return(true) - end - - it "returns false" do - expect(subject.valid?).to eq(false) - end - end - end - - describe "#finish_installation" do - let(:finished) { false } - let(:iguana) { true } - let(:method) { "reboot" } - - before do - allow(subject).to receive(:iguana?).and_return(iguana) - allow(subject.installation_phase).to receive(:finish?).and_return(finished) - allow(logger).to receive(:error) - end - - context "when it is not in finish the phase" do - it "logs the error and returns false" do - expect(logger).to receive(:error).with(/not finished/) - expect(subject.finish_installation(method)).to eq(false) - end - end - - context "when it is in the finish phase" do - let(:finished) { true } - - context "and it is executed using iguana" do - it "runs agamactl -k" do - expect(subject).to receive(:system).with(/agamactl -k/).and_return(true) - expect(subject.finish_installation(method)).to eq(true) - end - end - - context "and it is not executed using iguana" do - let(:iguana) { false } - - it "executes the command to the finish method given" do - expect(subject).to receive(:system).with(/shutdown -r now/).and_return(true) - expect(subject.finish_installation(method)).to eq(true) - end - end - end - end - - describe "#collect_logs" do - it "collects the logs and returns the path to the archive" do - # %x returns the command output including trailing \n - expect(subject).to receive(:`) - .with("agama logs store ") - .and_return("/tmp/y2log-hWBn95.tar.xz\n") - - path = subject.collect_logs - expect(path).to eq("/tmp/y2log-hWBn95.tar.xz") - end - end - - include_examples "progress" -end diff --git a/service/test/agama/network_test.rb b/service/test/agama/network_test.rb deleted file mode 100644 index 20d944cbbf..0000000000 --- a/service/test/agama/network_test.rb +++ /dev/null @@ -1,224 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../test_helper" -require "tmpdir" -require "agama/network" - -describe Agama::Network do - subject(:network) { described_class.new(logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - let(:targetdir) { File.join(rootdir, "mnt") } - let(:fixtures) { File.join(FIXTURES_PATH, "root_dir") } - let(:hostname_path) { File.join(fixtures, "etc", "hostname") } - let(:agama_dir) { File.join(rootdir, "run", "agama") } - let(:not_copy_network) { File.join(agama_dir, "not_copy_network") } - - before do - allow(Yast::Installation).to receive(:destdir).and_return(targetdir) - stub_const("Agama::Network::HOSTNAME", hostname_path) - stub_const("Agama::Network::RUN_NM_DIR", File.join(rootdir, "run", "NetworkManager")) - stub_const("Agama::Network::AGAMA_SYSTEMD_LINK", - File.join(rootdir, Agama::Network::AGAMA_SYSTEMD_LINK)) - stub_const("Agama::Network::NOT_COPY_NETWORK", not_copy_network) - FileUtils.mkdir_p(agama_dir) - end - - after do - FileUtils.remove_entry(rootdir) - end - - describe "#install" do - let(:rootdir) { Dir.mktmpdir } - - let(:etcdir) do - File.join(rootdir, "etc", "NetworkManager", "system-connections") - end - - let(:service) { instance_double(Yast2::Systemd::Service, enable: nil) } - - before do - allow(Yast2::Systemd::Service).to receive(:find).with("NetworkManager").and_return(service) - stub_const("Agama::Network::ETC_NM_DIR", etcdir) - end - - context "when there is some Agama systemd network link file" do - before do - FileUtils.cp_r(Dir["#{fixtures}/*"], rootdir) - end - - it "copies the files to /etc/systemd/network" do - network.install - expect(File).to exist( - File.join(targetdir, "etc", "systemd", "network", "10-agama-ifname-bootdev.link") - ) - end - end - - context "when NetworkManager configuration files are present" do - before do - FileUtils.mkdir_p(File.join(etcdir, "system-connections")) - FileUtils.touch(File.join(etcdir, "system-connections", "wired.nmconnection")) - end - - context "and the /run/agama/not_copy_network file does not exist" do - it "copies the configuration files" do - network.install - expect(File).to exist( - File.join(targetdir, etcdir, "system-connections", "wired.nmconnection") - ) - end - end - - context "and the /run/agama/not_copy_network file exists" do - around do |block| - FileUtils.mkdir_p(agama_dir) - FileUtils.touch(not_copy_network) - block.call - FileUtils.rm_f(not_copy_network) - end - - it "does not try to copy any file" do - expect(FileUtils).to_not receive(:cp_r) - network.install - end - end - end - - context "when NetworkManager configuration files are not available" do - it "does not try to copy any file" do - expect(FileUtils).to_not receive(:cp_r) - network.install - end - end - - context "when NetworkManager connections are not defined" do - before do - FileUtils.mkdir_p(etcdir) - end - - it "does not try to copy any file" do - network.install - expect(Dir).to_not exist( - File.join(targetdir, etcdir, "system-connections") - ) - end - end - - context "when an static hostname is present" do - let(:test_path) { File.join(fixtures, "etc", "hostname.test") } - - around do |block| - FileUtils.mv test_path, hostname_path - block.call - FileUtils.mv hostname_path, test_path - end - - it "copies it to the target system" do - network.install - expect(File.exist?(File.join(targetdir, Agama::Network::HOSTNAME))).to eql(true) - end - end - - it "enables the NetworkManager service" do - expect(service).to receive(:enable) - network.install - end - - context "when the NetworkManager service is not found" do - let(:service) { nil } - - it "logs an error" do - expect(logger).to receive(:error).with("NetworkManager service was not found") - network.install - end - end - end - - describe "#link_resolv" do - let(:rootdir) { Dir.mktmpdir } - - let(:resolv_fixture) { File.join(FIXTURES_PATH, "etc", "resolv.conf") } - let(:resolv_flag) { File.join(rootdir, "run", "agama", "manage_resolv") } - let(:resolv) { File.join(targetdir, "etc", "resolv.conf") } - - before do - stub_const("Agama::Network::RESOLV_FLAG", resolv_flag) - FileUtils.mkdir_p targetdir - FileUtils.cp_r(Dir["#{fixtures}/*"], rootdir) - FileUtils.cp_r(Dir["#{fixtures}/*"], targetdir) - end - - context "when the /etc/resolv.conf exists in the installation destdir" do - before do - FileUtils.mkdir_p File.join(targetdir, "etc") - FileUtils.touch File.join(targetdir, "etc", "resolv.conf") - end - - it "does nothing" do - expect(FileUtils).to_not receive(:ln_s) - network.link_resolv - end - end - - context "when there is no /etc/resolv.conf in the installation destdir" do - it "symlinks it to /run/NetworkManager/resolv.conf" do - network.link_resolv - expect(File.exist?(resolv)).to eql(true) - expect(File.symlink?(resolv)).to eql(true) - end - - it "creates a flag indicating that the resolv.conf is managed by Agama" do - network.link_resolv - expect(File.exist?(resolv_flag)).to eql(true) - end - end - end - - describe "#unlink_resolv" do - let(:rootdir) { Dir.mktmpdir } - - let(:fixtures) { File.join(FIXTURES_PATH, "root_dir") } - let(:resolv_fixture) { File.join(FIXTURES_PATH, "etc", "resolv.conf") } - let(:resolv_flag) { File.join(rootdir, "run", "agama", "manage_resolv") } - let(:resolv) { File.join(targetdir, "etc", "resolv.conf") } - - before do - stub_const("Agama::Network::RESOLV_FLAG", resolv_flag) - stub_const("Agama::Network::RUN_NM_DIR", File.join(rootdir, "run", "NetworkManager")) - FileUtils.mkdir_p targetdir - FileUtils.cp_r(Dir["#{fixtures}/*"], rootdir) - FileUtils.cp_r(Dir["#{fixtures}/*"], targetdir) - end - - context "when the /etc/resolv.conf was marked as managed by Agama" do - it "removes the /etc/resolv.con symlink from the installation destdir" do - network.link_resolv - expect(File.exist?(resolv_flag)).to eql(true) - expect(File.symlink?(resolv)).to eql(true) - network.unlink_resolv - expect(File.exist?(resolv)).to eql(false) - expect(File.exist?(resolv_flag)).to eql(false) - end - end - end -end diff --git a/service/test/agama/proxy_setup_test.rb b/service/test/agama/proxy_setup_test.rb deleted file mode 100644 index c4246921dc..0000000000 --- a/service/test/agama/proxy_setup_test.rb +++ /dev/null @@ -1,143 +0,0 @@ -# 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_relative "../test_helper" -require "agama/proxy_setup" - -describe Agama::ProxySetup do - subject(:proxy) { described_class.instance } - before do - proxy.proxy = nil - end - - before do - allow(Yast::Proxy).to receive(:Read) - allow(Yast::Proxy).to receive(:Write) - end - - describe "#run" do - let(:file_content) { "proxy=#{proxy_url}" } - let(:proxy_url) { "https://yast:1234@192.168.122.1:3128" } - - context "when some configuration is given through the kernel command line" do - before do - allow(proxy).to receive(:proxy_from_cmdline).and_return(URI(proxy_url)) - allow(proxy).to receive(:write) - end - - it "reads the given proxy configuraion" do - expect(proxy.proxy).to be_nil - proxy.run - expect(proxy.proxy).to be_a(URI) - end - - it "writes the proxy configuration to /etc/sysconfig/proxy" do - allow(proxy).to receive(:write).and_call_original - expect(Yast::Proxy).to receive(:Write) - proxy.run - config = Yast::Proxy.Export - expect(config).to include("https_proxy" => "https://192.168.122.1:3128", - "proxy_password" => "1234", - "proxy_user" => "yast", - "enabled" => true) - end - - context "when an http url is given" do - let(:proxy_url) { "http://192.168.122.1:3128" } - - it "sets also the https and ftp with the same url" do - allow(proxy).to receive(:write).and_call_original - proxy.run - config = Yast::Proxy.Export - expect(config).to include("http_proxy" => "http://192.168.122.1:3128", - "https_proxy" => "http://192.168.122.1:3128", - "ftp_proxy" => "http://192.168.122.1:3128", - "enabled" => true) - end - end - end - end - - describe "#propose" do - let(:config) do - { - "enabled" => false - } - end - - before do - Yast::Proxy.Import(config) - allow(Yast::Installation).to receive(:destdir).and_return("/mnt") - end - - context "when the use of proxy is enabled" do - let(:config) do - { - "enabled" => true, - "http_proxy" => "http://192.168.122.1:3128" - } - end - - it "adds microos-tools package to the set of resolvables" do - expect(Yast::PackagesProposal).to receive(:SetResolvables) do |_, _, packages| - expect(packages).to contain_exactly("microos-tools") - end - - proxy.propose - end - end - end - - describe "#install" do - let(:config) do - { - "enabled" => false - } - end - - before do - Yast::Proxy.Import(config) - allow(Yast::Installation).to receive(:destdir).and_return("/mnt") - end - - context "when the use of proxy is disabled" do - it "does not copy the configuration to the target system" do - expect(FileUtils).to_not receive(:cp) - proxy.install - end - end - - context "when the use of proxy is enabled" do - let(:config) do - { - "enabled" => true, - "http_proxy" => "http://192.168.122.1:3128" - } - end - - it "copies the configuration to the target system" do - expect(FileUtils).to receive(:cp).with(described_class::CONFIG_PATH, - File.join("/mnt", described_class::CONFIG_PATH)) - proxy.install - end - end - end -end diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb deleted file mode 100644 index 9f376e5554..0000000000 --- a/service/test/agama/registration_test.rb +++ /dev/null @@ -1,599 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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_relative "../test_helper" -require "agama/answer" -require "agama/config" -require "agama/registration" -require "agama/software/manager" -require "suse/connect" -require "yast" -require "y2packager/new_repository_setup" - -Yast.import("Arch") - -describe Agama::Registration do - subject { described_class.new(manager, logger) } - - let(:manager) { instance_double(Agama::Software::Manager) } - let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } } - - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - allow(Yast::Arch).to receive(:rpm_arch).and_return("x86_64") - - allow(manager).to receive(:product).and_return(product) - allow(manager).to receive(:add_service) - allow(manager).to receive(:remove_service) - allow(manager).to receive(:addon_products) - - allow(SUSE::Connect::YaST).to receive(:announce_system).and_return(["test-user", "12345"]) - allow(SUSE::Connect::YaST).to receive(:deactivate_system) - allow(SUSE::Connect::YaST).to receive(:create_credentials_file) - allow(SUSE::Connect::YaST).to receive(:activate_product).and_return(service) - allow(Y2Packager::NewRepositorySetup.instance).to receive(:add_service) - allow(Agama::CmdlineArgs).to receive(:read).and_return(cmdline_args) - end - - let(:service) { OpenStruct.new(name: "test-service", url: nil) } - let(:cmdline_args) { Agama::CmdlineArgs.new({}) } - - describe "#verify_callback" do - it "stores to SSL::Error ssl error details" do - error = double(error: 20, error_string: "Error", current_cert: nil) - - subject.send(:verify_callback).call(false, error) - expect(Agama::SSL::Errors.instance.ssl_error_code).to eq 20 - expect(Agama::SSL::Errors.instance.ssl_error_msg).to eq "Error" - end - end - - describe "#register" do - context "if there is no product selected yet" do - let(:product) { nil } - - it "does not try to register" do - expect(SUSE::Connect::YaST).to_not receive(:announce_system) - - subject.register("11112222", email: "test@test.com") - end - end - - context "if there is a selected product" do - let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } } - - context "and the product is already registered" do - before do - subject.register("11112222", email: "test@test.com") - end - - it "does not try to register" do - expect(SUSE::Connect::YaST).to_not receive(:announce_system) - - subject.register("11112222", email: "test@test.com") - end - end - - context "and the product is not registered yet" do - it "announces the system" do - expect(SUSE::Connect::YaST).to receive(:announce_system).with( - { - language: anything, - url: "https://scc.suse.com", - token: "11112222", - email: "test@test.com", - verify_callback: anything - }, - "test-5-x86_64" - ) - - subject.register("11112222", email: "test@test.com") - end - - it "sets the current language in the request" do - expect(SUSE::Connect::YaST).to receive(:announce_system).with( - { - language: "de-de", - url: anything, - token: "11112222", - email: "test@test.com", - verify_callback: anything - }, - "test-5-x86_64" - ) - - allow(Yast::WFM).to receive(:GetLanguage).and_return("de_DE") - - subject.register("11112222", email: "test@test.com") - end - - context "when a registration URL is set through the cmdline" do - let(:cmdline_args) do - Agama::CmdlineArgs.new("register_url" => "http://scc.example.net") - end - - it "registers using the given URL" do - expect(SUSE::Connect::YaST).to receive(:announce_system).with( - { token: "11112222", email: "test@test.com", url: "http://scc.example.net", - verify_callback: anything, language: anything }, - "test-5-x86_64" - ) - - subject.register("11112222", email: "test@test.com") - end - end - - it "creates credentials file" do - expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "/etc/zypp/credentials.d/SCCcredentials") - # TODO: when fixing suse-connect read of fsroot - # .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/SCCcredentials") - - subject.register("11112222", email: "test@test.com") - end - - it "activates the selected product" do - expect(SUSE::Connect::YaST).to receive(:activate_product).with( - an_object_having_attributes( - arch: "x86_64", identifier: "test", version: "5.0" - ), {}, "test@test.com" - ) - - subject.register("11112222", email: "test@test.com") - end - - it "adds the service to software manager" do - expect(Y2Packager::NewRepositorySetup.instance) - .to receive(:add_service).with("test-service") - - subject.register("11112222", email: "test@test.com") - end - - context "if the service requires a creadentials file" do - let(:service) { OpenStruct.new(name: "test-service", url: "https://credentials/file") } - - before do - allow(subject).to receive(:credentials_from_url) - .with("https://credentials/file") - .and_return("productA") - end - - it "creates the credentials file" do - expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/productA") - - subject.register("11112222", email: "test@test.com") - end - end - - context "if the service does not require a creadentials file" do - let(:service) { OpenStruct.new(name: "test-service", url: nil) } - - it "does not create the credentials file" do - expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - expect(SUSE::Connect::YaST).to_not receive(:create_credentials_file) - .with("test-user", "12345", anything) - - subject.register("11112222", email: "test@test.com") - end - end - - context "if the product was correctly registered" do - before do - subject.on_change(&callback) - end - - let(:callback) { proc {} } - - it "runs the callbacks" do - expect(callback).to receive(:call) - - subject.register("11112222", email: "test@test.com") - end - - it "sets the registration code" do - subject.register("11112222", email: "test@test.com") - - expect(subject.reg_code).to eq("11112222") - end - - it "sets the email" do - subject.register("11112222", email: "test@test.com") - - expect(subject.email).to eq("test@test.com") - end - end - - context "if the product was not correctly registered" do - before do - allow(SUSE::Connect::YaST).to receive(:activate_product).and_raise(Timeout::Error) - subject.on_change(&callback) - end - - let(:callback) { proc {} } - - it "raises an error" do - expect { subject.register("11112222", email: "test@test.com") } - .to raise_error(Timeout::Error) - end - - it "sets the registration code" do - expect { subject.register("11112222", email: "test@test.com") } - .to raise_error(Timeout::Error) - - expect(subject.reg_code).to eq("11112222") - end - - it "sets the email" do - expect { subject.register("11112222", email: "test@test.com") } - .to raise_error(Timeout::Error) - - expect(subject.email).to eq("test@test.com") - end - - it "runs the callbacks" do - expect(callback).to receive(:call) - - expect { subject.register("11112222", email: "test@test.com") } - .to raise_error(Timeout::Error) - end - end - - context "if the registration server has self-signed certificate" do - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - - let(:certificate) do - Agama::SSL::Certificate.load(File.read(File.join(FIXTURES_PATH, "test.pem"))) - end - before do - Agama::SSL::Errors.instance.ssl_error_code = Agama::SSL::ErrorCodes::SELF_SIGNED_CERT - Agama::SSL::Errors.instance.ssl_error_msg = "test error" - Agama::SSL::Errors.instance.ssl_failed_cert = certificate - # mock reset to avoid deleting of previous setup - allow(Agama::SSL::Errors.instance).to receive(:reset) - - @called = 0 - allow(SUSE::Connect::YaST).to receive(:activate_product) do - @called += 1 - raise OpenSSL::SSL::SSLError, "test" if @called == 1 - - service - end - end - - context "and certificate fingerprint is in storage" do - before do - Agama::SSL::Storage.instance.fingerprints - .replace([certificate.send(:sha256_fingerprint)]) - end - - it "tries to import certificate" do - expect(certificate).to receive(:import) - - subject.register("11112222", email: "test@test.com") - end - - after do - Agama::SSL::Storage.instance.fingerprints.clear - end - end - - it "opens question" do - expect(questions_client).to receive(:ask).and_yield(Agama::Answer.new(:Abort)) - expect(Agama::HTTP::Clients::Questions).to receive(:new) - .and_return(questions_client) - - expect { subject.register("11112222", email: "test@test.com") }.to( - raise_error(OpenSSL::SSL::SSLError) - ) - end - end - end - end - end - - describe "#register_addon" do - context "if there is no product selected yet" do - let(:addon) do - OpenStruct.new( - arch: Yast::Arch.rpm_arch, - identifier: "sle-ha", - version: "16.0" - ) - end - - 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 - ) - - subject.register_addon(addon.identifier, addon.version, code) - end - - it "registers addon only once" do - expect(SUSE::Connect::YaST).to receive(:activate_product).with( - addon, { token: code }, anything - ).once - - 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 - - describe "#deregister" do - before do - allow(FileUtils).to receive(:rm) - end - - context "if there is no product selected yet" do - let(:product) { nil } - - it "does not try to deregister" do - expect(SUSE::Connect::YaST).to_not receive(:deactivate_system) - - subject.deregister - end - end - - context "if there is a selected product" do - let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } } - - context "and the product is not registered yet" do - it "does not try to deregister" do - expect(SUSE::Connect::YaST).to_not receive(:deactivate_system) - - subject.deregister - end - end - - context "and the product is registered" do - before do - allow(subject).to receive(:credentials_from_url) - allow(subject).to receive(:credentials_from_url) - .with("https://credentials/file").and_return("credentials") - - subject.register("11112222", email: "test@test.com") - end - - it "deletes the service from the software config" do - expect(manager).to receive(:remove_service).with(service) - - subject.deregister - end - - it "deactivates the system" do - expect(SUSE::Connect::YaST).to receive(:deactivate_system).with( - { - url: anything, - token: "11112222", - email: "test@test.com", - verify_callback: anything, - language: anything - } - ) - - subject.deregister - end - - it "removes the credentials file" do - expect(FileUtils).to receive(:rm).with(/SCCcredentials/) - - subject.deregister - end - - context "if the service has a credentials files" do - let(:service) { OpenStruct.new(name: "test-service", url: "https://credentials/file") } - - it "removes the credentials file" do - expect(FileUtils).to receive(:rm) - expect(FileUtils).to receive(:rm).with(/\/credentials$/) - - subject.deregister - end - end - - context "if the product has no credentials file" do - let(:service) { OpenStruct.new(name: "test-service", url: nil) } - - it "does not try to remove the credentials file" do - expect(FileUtils).to_not receive(:rm).with(/\/credentials$/) - - subject.deregister - end - end - - context "if the product was correctly deregistered" do - before do - subject.on_change(&callback) - end - - let(:callback) { proc {} } - - it "runs the callbacks" do - expect(callback).to receive(:call) - - subject.deregister - end - - it "removes the registration code" do - subject.deregister - - expect(subject.reg_code).to be_nil - end - - it "removes the email" do - subject.deregister - - expect(subject.email).to be_nil - end - end - - context "if the product was not correctly deregistered" do - before do - allow(SUSE::Connect::YaST).to receive(:deactivate_system).and_raise(Timeout::Error) - subject.on_change(&callback) - end - - let(:callback) { proc {} } - - it "raises an error" do - expect { subject.deregister }.to raise_error(Timeout::Error) - end - - it "does not run the callbacks" do - expect(callback).to_not receive(:call) - - expect { subject.deregister }.to raise_error(Timeout::Error) - end - - it "does not remove the registration code" do - expect { subject.deregister }.to raise_error(Timeout::Error) - - expect(subject.reg_code).to eq("11112222") - end - - it "does not remove the email" do - expect { subject.deregister }.to raise_error(Timeout::Error) - - expect(subject.email).to eq("test@test.com") - end - end - end - end - end - - describe "#finish" do - context "system is not registered" do - before do - subject.instance_variable_set(:@registered, false) - end - - it "do nothing" do - expect(::FileUtils).to_not receive(:cp) - - subject.finish - end - end - - context "system is registered" do - before do - subject.instance_variable_set(:@registered, true) - subject.instance_variable_set(:@reg_code, "test") - subject.instance_variable_set(:@credentials_files, ["test"]) - Yast::Installation.destdir = "/mnt" - allow(::FileUtils).to receive(:cp) - end - - it "copies global credentials file" do - expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/SCCcredentials", - "/mnt/etc/zypp/credentials.d/SCCcredentials") - - subject.finish - end - - it "copies product credentials file" do - expect(::FileUtils).to receive(:cp).with("/run/agama/zypp/etc/zypp/credentials.d/test", - "/mnt/etc/zypp/credentials.d/test") - - subject.finish - end - - context "and a registration URL was given" do - before do - allow(subject).to receive(:registration_url).and_return("http://reg-server.lan") - end - - it "generates and copies the SUSEConnect configuration" do - expect(::FileUtils).to receive(:cp).with("/etc/SUSEConnect", "/mnt/etc/SUSEConnect") - expect(SUSE::Connect::YaST).to receive(:write_config).with("url" => "http://reg-server.lan") - - subject.finish - end - end - end - end -end diff --git a/service/test/agama/security_test.rb b/service/test/agama/security_test.rb deleted file mode 100644 index 72fd9e6c58..0000000000 --- a/service/test/agama/security_test.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../test_helper" -require "agama/security" - -describe Agama::Security do - subject(:security) { described_class.new(logger, config) } - - let(:logger) { Logger.new($stdout) } - - let(:config_path) do - File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml") - end - - let(:config) do - Agama::Config.new(YAML.safe_load(File.read(config_path))) - end - - let(:selected) { nil } - - let(:lsm_config) do - instance_double(Y2Security::LSM::Config, select: nil, selected: selected) - end - - let(:apparmor) do - instance_double(Y2Security::LSM::AppArmor, id: :apparmor) - end - - let(:selinux) do - instance_double(Y2Security::LSM::Selinux, id: :selinux) - end - - let(:proposal) do - { - "size" => "0 B", - "patterns" => { - "documentation" => 1, - "enhanced_base" => 1, - "sw_management" => 1, - "yast2_basis" => 1, - "apparmor" => 0, - "minimal_base" => 1, - "base" => 1, - "x86_64_v3" => 1 - } - } - end - - let(:software_client) do - instance_double(Agama::HTTP::Clients::Software, proposal: proposal, add_patterns: nil) - end - - before do - allow(Y2Security::LSM::Config).to receive(:instance).and_return(lsm_config) - allow(security).to receive(:software_client).and_return(software_client) - allow(Yast::Bootloader).to receive(:modify_kernel_params) - allow(lsm_config.selected).to receive(:reset_kernel_params) - allow(lsm_config.selected).to receive(:kernel_params) - end - - describe "#write" do - let(:selected) { apparmor } - - context "when the software proposal patterns includes the LSM patterns" do - it "saves kernel parameters for the LSM configuration" do - expect(Yast::Bootloader).to receive(:modify_kernel_params) - security.write - end - end - - context "when the software proposal patterns does not include the LSM patterns" do - let(:selected) { apparmor } - - let(:proposal) do - { - "size" => "0 B", - "patterns" => { - "documentation" => 1, - "enhanced_base" => 1, - "sw_management" => 1, - "yast2_basis" => 1, - "minimal_base" => 1, - "base" => 1, - "x86_64_v3" => 1 - } - } - end - - it "fallback to the first LSM which patterns are included by the software proposal" do - expect(lsm_config).to receive(:select).with("none") - expect(Yast::Bootloader).to receive(:modify_kernel_params) - security.write - end - - it "resets bootloader params for previous selection" do - expect(lsm_config.selected).to receive(:reset_kernel_params) - security.write - end - end - end -end diff --git a/service/test/agama/software/callbacks/digest_test.rb b/service/test/agama/software/callbacks/digest_test.rb deleted file mode 100644 index bd46bcb86c..0000000000 --- a/service/test/agama/software/callbacks/digest_test.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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_relative "../../../test_helper" -require "agama/software/callbacks/digest" -require "agama/http/clients" -require "agama/answer" -require "agama/question" - -describe Agama::Software::Callbacks::Digest do - subject { described_class.new(questions_client, logger) } - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:question) { instance_double(Agama::Question, answer: answer) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - allow(questions_client).to receive(:ask).and_yield(answer) - end - - describe "#accept_file_without_checksum" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "registers a question informing of the error" do - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to match("No checksum for the file repomd.xml") - end - subject.accept_file_without_checksum("repomd.xml") - end - - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "returns true" do - expect(subject.accept_file_without_checksum("repomd.xml")).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(subject.no_label) } - - it "returns false" do - expect(subject.accept_file_without_checksum("repomd.xml")).to eq(false) - end - end - end - - describe "#accept_unknown_digest" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "registers a question informing of the error" do - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to include("The checksum of the file repomd.xml is \"123456\"") - end - subject.accept_unknown_digest("repomd.xml", "123456") - end - - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "returns true" do - expect(subject.accept_unknown_digest("repomd.xml", "123456")).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(subject.no_label) } - - it "returns false" do - expect(subject.accept_unknown_digest("repomd.xml", "123456")).to eq(false) - end - end - end - - describe "#accept_wrong_digest" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "registers a question informing of the error" do - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to match( - /The expected checksum of file repomd.xml is "654321".*expected.*"123456"/ - ) - end - subject.accept_wrong_digest("repomd.xml", "123456", "654321") - end - - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(subject.yes_label) } - - it "returns true" do - expect(subject.accept_wrong_digest("repomd.xml", "123456", "654321")).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(subject.no_label) } - - it "returns false" do - expect(subject.accept_wrong_digest("repomd.xml", "123456", "654321")).to eq(false) - end - end - end -end diff --git a/service/test/agama/software/callbacks/media_test.rb b/service/test/agama/software/callbacks/media_test.rb deleted file mode 100644 index b77056f806..0000000000 --- a/service/test/agama/software/callbacks/media_test.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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_relative "../../../test_helper" -require "agama/software/callbacks/media" -require "agama/http/clients" -require "agama/question" -require "agama/answer" - -describe Agama::Software::Callbacks::Media do - subject { described_class.new(questions_client, logger) } - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:question) { instance_double(Agama::Question, answer: answer) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - describe "#media_changed" do - before do - allow(questions_client).to receive(:ask).and_yield(answer) - - # mock sleep() to speed up test - allow(subject).to receive(:sleep) - end - - context "when the user answers :Retry" do - let(:answer) { Agama::Answer.new(subject.retry_label) } - - it "returns ''" do - ret = subject.media_change( - "NOT_FOUND", "Package not found", "", "", 0, "", 0, "", true, [], 0 - ) - expect(ret).to eq("") - end - end - - context "when the user answers :Skip" do - let(:answer) { Agama::Answer.new(subject.continue_label.to_sym) } - - it "returns 'S'" do - ret = subject.media_change( - "NOT_FOUND", "Package not found", "", "", 0, "", 0, "", true, [], 0 - ) - expect(ret).to eq("S") - end - end - - context "when a timeout error occurs" do - # actually not used, just required by the global "before" - let(:answer) { nil } - - it "returns '' without asking" do - expect(questions_client).to_not receive(:ask) - ret = subject.media_change( - "IO_SOFT", "Timeout", "", "", 0, "", 0, "", true, [], 0 - ) - expect(ret).to eq("") - end - end - end -end diff --git a/service/test/agama/software/callbacks/pkg_gpg_check_test.rb b/service/test/agama/software/callbacks/pkg_gpg_check_test.rb deleted file mode 100644 index 63ef576868..0000000000 --- a/service/test/agama/software/callbacks/pkg_gpg_check_test.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] 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_relative "../../../test_helper" - -require "agama/software/manager" -require "agama/software/callbacks/pkg_gpg_check" - -describe Agama::Software::Callbacks::PkgGpgCheck do - subject { described_class.new(questions_client, logger) } - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:logger) { Logger.new($stdout, level: :error) } - - describe "#pkg_gpg_check" do - # libzypp GPG check result - let(:check_result) do - { - "CheckPackageResult" => result_code, - "Package" => "foo", - "RepoMediaUrl" => repo_url - } - end - let(:repo_url) { "http://example.com" } - let(:boot_params) { {} } - - before do - # set the boot parameters - allow(Agama::CmdlineArgs).to receive(:read).and_return(Agama::CmdlineArgs.new(boot_params)) - end - - context "when GPG check succeeds" do - let(:result_code) { Agama::Software::Callbacks::PkgGpgCheck::CHK_OK } - - it "requests no action" do - expect(subject.pkg_gpg_check(check_result)).to eq("") - end - end - - context "when the used GPG key is unknown" do - let(:result_code) { Agama::Software::Callbacks::PkgGpgCheck::CHK_NOKEY } - - context "when no boot parameter is used" do - context "the package comes from a regular repository" do - it "requests no action" do - expect(subject.pkg_gpg_check(check_result)).to eq("") - end - end - - context "the package comes from the DUD repository" do - let(:repo_url) { Agama::Software::Manager.dud_repository_url } - - it "requests no action" do - expect(subject.pkg_gpg_check(check_result)).to eq("") - end - end - end - - context "when 'inst.dud_packages.gpg=0' boot parameter is used" do - let(:boot_params) do - { - # emulate using the inst.dud_packages.gpg=0 boot option - "dud_packages" => { - "gpg" => "0" - } - } - end - - context "the package comes from a regular repository" do - # errors for regular packages are not ignored - it "requests no action" do - expect(subject.pkg_gpg_check(check_result)).to eq("") - end - end - - context "the package comes from the DUD repository" do - let(:repo_url) { Agama::Software::Manager.dud_repository_url } - - # only errors for the DUD packages are ignored - it "requests to ignore the GPG signature problem" do - expect(subject.pkg_gpg_check(check_result)).to eq("I") - end - end - end - end - end -end diff --git a/service/test/agama/software/callbacks/provide_test.rb b/service/test/agama/software/callbacks/provide_test.rb deleted file mode 100644 index a4070b2940..0000000000 --- a/service/test/agama/software/callbacks/provide_test.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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_relative "../../../test_helper" -require "agama/software/callbacks/provide" -require "agama/http/clients/questions" -require "agama/question" -require "agama/answer" - -describe Agama::Software::Callbacks::Provide do - subject { described_class.new(questions_client, logger) } - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:question) { instance_double(Agama::Question, answer: answer) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:answer) { Agama::Answer.new(subject.retry_label) } - - describe "#done_provide" do - before do - allow(questions_client).to receive(:ask).and_yield(answer) - end - - let(:question_client) { instance_double(Agama::DBus::Clients::Question) } - - context "when the file is not found" do - it "does not register a question" do - expect(questions_client).to_not receive(:ask) - subject.done_provide(1, "Some dummy reason", "dummy-package") - end - end - - context "when the there is an I/O error" do - it "registers a question informing of the error" do - reason = "could not be downloaded" - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to include(reason) - end - subject.done_provide(2, reason, "dummy-package") - end - end - - context "when the there is an I/O error" do - it "registers a question informing of the error" do - reason = "integrity check has failed" - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to include(reason) - end - subject.done_provide(3, "integrity check has failed", "dummy-package") - end - end - - context "when the user answers :Retry" do - it "returns 'R'" do - ret = subject.done_provide( - 2, "Some dummy reason", "dummy-package" - ) - expect(ret).to eq("R") - end - end - - context "when the user answers :Skip" do - let(:answer) { Agama::Answer.new(subject.continue_label) } - - it "returns 'I'" do - ret = subject.done_provide( - 2, "Some dummy reason", "dummy-package" - ) - expect(ret).to eq("I") - end - end - end -end diff --git a/service/test/agama/software/callbacks/script_test.rb b/service/test/agama/software/callbacks/script_test.rb deleted file mode 100644 index 34251131e9..0000000000 --- a/service/test/agama/software/callbacks/script_test.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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_relative "../../../test_helper" -require "agama/software/callbacks/script" -require "agama/http/clients/questions" -require "agama/question" -require "agama/answer" - -describe Agama::Software::Callbacks::Script do - subject { described_class.new(questions_client, logger) } - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:question) { instance_double(Agama::Question, answer: answer) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:answer) { Agama::Answer.new(subject.retry_label) } - - let(:description) { "Some description" } - - describe "#script_problem" do - before do - allow(questions_client).to receive(:ask).and_yield(answer) - end - - it "registers a question with the details" do - expect(questions_client).to receive(:ask) do |q| - expect(q.text).to include("running a package script") - expect(q.data).to include( - "details" => description - ) - end - subject.script_problem(description) - end - - context "when the user asks to retry" do - let(:answer) { Agama::Answer.new(subject.retry_label) } - - it "returns 'R'" do - ret = subject.script_problem(description) - expect(ret).to eq("R") - end - end - - context "when the user asks to continue" do - let(:answer) { Agama::Answer.new(subject.continue_label) } - - it "returns 'I'" do - ret = subject.script_problem(description) - expect(ret).to eq("I") - end - end - end -end diff --git a/service/test/agama/software/callbacks/signature_test.rb b/service/test/agama/software/callbacks/signature_test.rb deleted file mode 100644 index e940cd5b7f..0000000000 --- a/service/test/agama/software/callbacks/signature_test.rb +++ /dev/null @@ -1,210 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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_relative "../../../test_helper" -require "agama/software/callbacks/signature" -require "agama/http/clients" -require "agama/question" -require "agama/answer" - -describe Agama::Software::Callbacks::Signature do - before do - allow(questions_client).to receive(:ask).and_yield(answer) - end - - let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:question) { instance_double(Agama::Question, answer: answer) } - - let(:answer) { nil } - - let(:logger) { Logger.new($stdout, level: :warn) } - - subject { described_class.new(questions_client, logger) } - - describe "#accept_unsigned_file" do - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(:Yes) } - - it "returns true" do - expect(subject.accept_unsigned_file("repomd.xml", -1)).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(:No) } - - it "returns false" do - expect(subject.accept_unsigned_file("repomd.xml", -1)).to eq(false) - end - end - - context "when the repo information is available" do - before do - allow(Yast::Pkg).to receive(:SourceGeneralData).with(1) - .and_return("name" => "OSS", "url" => "http://localhost/repo") - end - - it "includes the name and the URL in the question" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include("repomd.xml from http://localhost/repo") - end - - expect(subject.accept_unsigned_file("repomd.xml", 1)) - end - end - - context "when the repo information is not available" do - before do - allow(Yast::Pkg).to receive(:SourceGeneralData).with(1).and_return(nil) - end - - it "includes a generic message containing the filename" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include("repomd.xml") - end - - expect(subject.accept_unsigned_file("repomd.xml", 1)) - end - end - end - - describe "import_gpg_key" do - let(:answer) { Agama::Answer.new("Trust") } - - let(:key) do - { - "id" => "0123456789ABCDEF", - "fingerprint" => "2E2EA448C9DDD7A91BC28441AEE969E90F05DB9D", - "name" => "YaST:Head:Agama" - } - end - - context "when the user answers :Trust" do - let(:answer) { Agama::Answer.new(:Trust) } - - it "returns true" do - expect(subject.import_gpg_key(key, 1)).to eq(true) - end - end - - context "when the user answers :Skip" do - let(:answer) { Agama::Answer.new(:Skip) } - - it "returns false" do - expect(subject.import_gpg_key(key, 1)).to eq(false) - end - end - - it "includes a message" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include(key["id"]) - expect(question.text).to include(key["name"]) - expect(question.text).to include("2E2E A448 C9DD") - end - subject.import_gpg_key(key, 1) - end - end - - describe "#accept_unknown_gpg_key" do - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(:Yes) } - - it "returns true" do - expect(subject.accept_unknown_gpg_key("repomd.xml", "KEYID", 1)).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(:No) } - - it "returns false" do - expect(subject.accept_unknown_gpg_key("repomd.xml", "KEYID", 1)).to eq(false) - end - end - - context "when the repo information is available" do - before do - allow(Yast::Pkg).to receive(:SourceGeneralData).with(1) - .and_return("name" => "OSS", "url" => "http://localhost/repo") - end - - it "includes the name and the URL in the question" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include("repomd.xml from http://localhost/repo") - end - - expect(subject.accept_unknown_gpg_key("repomd.xml", "KEYID", 1)) - end - end - - context "when the repo information is not available" do - before do - allow(Yast::Pkg).to receive(:SourceGeneralData).with(1).and_return(nil) - end - - it "includes a generic message containing the filename" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include("repomd.xml") - end - - expect(subject.accept_unknown_gpg_key("repomd.xml", "KEYID", 1)) - end - end - end - - describe "accept_verification_failed" do - let(:answer) { Agama::Answer.new(:Trust) } - - let(:key) do - { - "id" => "0123456789ABCDEF", - "fingerprint" => "2E2EA448C9DDD7A91BC28441AEE969E90F05DB9D", - "name" => "YaST:Head:Agama" - } - end - - let(:filename) { "repomd.xml" } - - context "when the user answers :Yes" do - let(:answer) { Agama::Answer.new(:Yes) } - - it "returns true" do - expect(subject.accept_verification_failed(filename, key, 1)).to eq(true) - end - end - - context "when the user answers :No" do - let(:answer) { Agama::Answer.new(:No) } - - it "returns false" do - expect(subject.accept_verification_failed(filename, key, 1)).to eq(false) - end - end - - it "includes a message" do - expect(questions_client).to receive(:ask) do |question| - expect(question.text).to include(key["id"]) - expect(question.text).to include(key["name"]) - end - subject.accept_verification_failed(filename, key, 1) - end - end -end diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb deleted file mode 100644 index 871f8883c4..0000000000 --- a/service/test/agama/software/manager_test.rb +++ /dev/null @@ -1,664 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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_relative "../../test_helper" -require_relative "../with_issues_examples" -require_relative "../with_progress_examples" -require_relative File.join(SRC_PATH, "agama/dbus/y2dir/software/modules/PackageCallbacks.rb") -require "agama/config" -require "agama/issue" -require "agama/registration" -require "agama/software/manager" -require "agama/software/product" -require "agama/software/proposal" -require "agama/http/clients" - -describe Agama::Software::Manager do - subject { described_class.new(config, logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - let(:base_url) { "" } - let(:destdir) { "/mnt" } - let(:gpg_keys) { [] } - - let(:repositories) do - instance_double( - Agama::Software::RepositoriesManager, - add: nil, - load: nil, - delete_all: nil, - empty?: true, - enabled: enabled_repos, - disabled: disabled_repos - ) - end - let(:products) { [] } - - let(:proposal) do - instance_double( - Agama::Software::Proposal, - :base_product= => nil, - calculate: nil, - :languages= => nil, - set_resolvables: nil, - packages_count: "500 MB", - issues: proposal_issues, - on_issues_change: nil, - only_required: nil - ) - end - - let(:enabled_repos) { [] } - let(:disabled_repos) { [] } - let(:proposal_issues) { [] } - - let(:config_path) do - File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml") - end - - let(:config) do - Agama::Config.new(YAML.safe_load(File.read(config_path))) - end - - let(:questions_client) do - instance_double(Agama::HTTP::Clients::Questions) - end - - let(:target_dir) { Dir.mktmpdir } - - before do - stub_const("Agama::Software::Manager::TARGET_DIR", target_dir) - allow(Yast::Pkg).to receive(:TargetInitialize) - allow(Yast::Pkg).to receive(:TargetFinish) - allow(Yast::Pkg).to receive(:TargetLoad) - allow(Yast::Pkg).to receive(:SourceSaveAll).and_return(true) - allow(Yast::Pkg).to receive(:SourceDelete) - allow(Yast::Pkg).to receive(:ImportGPGKey) - allow(Yast::Pkg).to receive(:ServiceAdd).and_return(true) - allow(Yast::Pkg).to receive(:ServiceSet).and_return(true) - allow(Yast::Pkg).to receive(:ServiceSave).and_return(true) - allow(Yast::Pkg).to receive(:ServiceForceRefresh).and_return(true) - # allow glob to work for other calls - allow(Dir).to receive(:glob).and_call_original - allow(Dir).to receive(:glob).with(/keys/).and_return(gpg_keys) - allow(Yast::Packages).to receive(:Proposal).and_return({}) - allow(Yast::InstURL).to receive(:installInf2Url).with("") - .and_return(base_url) - allow(Yast::Pkg).to receive(:SourceCreate) - allow(Yast::Installation).to receive(:destdir).and_return(destdir) - allow(Agama::HTTP::Clients::Questions).to receive(:new).and_return(questions_client) - allow(Agama::Software::RepositoriesManager).to receive(:instance).and_return(repositories) - allow(Agama::Software::Proposal).to receive(:new).and_return(proposal) - allow(Agama::ProductReader).to receive(:new).and_call_original - allow(FileUtils).to receive(:mkdir_p) - allow(FileUtils).to receive(:rm_rf) - allow(FileUtils).to receive(:cp_r) - allow(File).to receive(:exist?).and_call_original - end - - after do - FileUtils.rm_r(target_dir) - end - - describe "#new" do - before do - allow_any_instance_of(Agama::Software::ProductBuilder) - .to receive(:build).and_return(products) - end - - context "if there are several products" do - let(:products) do - [Agama::Software::Product.new("test1"), Agama::Software::Product.new("test2")] - end - - it "does not select a product by default" do - manager = described_class.new(config, logger) - - expect(manager.product).to be_nil - end - end - - context "if there is only a product" do - let(:products) { [product] } - - let(:product) { Agama::Software::Product.new("test1") } - - it "selects the product" do - manager = described_class.new(config, logger) - - expect(manager.product.id).to eq("test1") - end - end - - context "when GPG keys are available at /" do - let(:gpg_keys) { ["/usr/lib/gnupg/keys/gpg-key.asc"] } - - it "imports the GPG keys" do - expect(Yast::Pkg).to receive(:ImportGPGKey).with(gpg_keys.first, true) - described_class.new(config, logger) - end - end - - it "initializes the package system" do - expect(Yast::Pkg).to receive(:TargetInitialize) - described_class.new(config, logger) - end - end - - shared_examples "software issues" do |tested_method| - before do - allow(subject.registration).to receive(:reg_code).and_return(reg_code) - end - - let(:reg_code) { "123XX432" } - let(:proposal_issues) { [Agama::Issue.new("Proposal issue")] } - - context "if there are disabled repositories" do - let(:disabled_repos) do - [ - instance_double(Agama::Software::Repository, name: "Repo #1"), - instance_double(Agama::Software::Repository, name: "Repo #2") - ] - end - - it "adds an issue for each disabled repository" do - subject.public_send(tested_method) - - expect(subject.issues).to include( - an_object_having_attributes( - description: /Could not read repository "Repo #1"/i - ), - an_object_having_attributes( - description: /Could not read repository "Repo #2"/i - ) - ) - end - end - - context "if there is any enabled repository" do - let(:enabled_repos) { [instance_double(Agama::Software::Repository, name: "Repo #1")] } - - it "adds the proposal issues" do - subject.public_send(tested_method) - - expect(subject.issues).to include(an_object_having_attributes( - description: /proposal issue/i - )) - end - end - - context "if there is no enabled repository" do - let(:enabled_repos) { [] } - - it "does not add the proposal issues" do - subject.public_send(tested_method) - - expect(subject.issues).to_not include(an_object_having_attributes( - description: /proposal issue/i - )) - end - end - end - - describe "#probe" do - before do - subject.select_product("Tumbleweed") - allow(subject).to receive(:list_disks).and_return({}) - end - - it "creates a packages proposal" do - expect(proposal).to receive(:calculate) - subject.probe - end - - it "registers the repository from config" do - expect(repositories).to receive(:add).with(/tumbleweed/) - expect(repositories).to receive(:load) - subject.probe - end - - it "uses the offline medium if available" do - device = "/dev/sr1" - expect(subject).to receive(:list_disks).and_return({ - "blockdevices" => [ - { - "kname" => device, - "label" => "openSUSE-Tumbleweed-DVD-x86_64" - } - ] - }) - - expect(repositories).to receive(:add).with("hd:/?device=" + device) - subject.probe - end - - include_examples "software issues", "probe" - end - - describe "#products" do - it "returns the list of known products" do - products = subject.products - expect(products).to all(be_a(Agama::Software::Product)) - expect(products).to_not be_empty - end - end - - describe "#patterns" do - it "returns only the specified patterns" do - allow(Yast::Pkg).to receive(:SourceGetCurrent).and_return([0]) - allow(Yast::Pkg).to receive(:SourceGeneralData).and_return({ "service" => "" }) - expect(Y2Packager::Resolvable).to receive(:find).and_return( - [ - double( - arch: "x86_64", - category: "Base Technologies", - description: "YaST tools for installing your system.", - icon: "./yast", - kind: :pattern, - name: "yast2_install_wf", - order: "1240", - source: 0, - summary: "YaST Installation Packages", - user_visible: false, - version: "20220411-1.4" - ), - double( - arch: "x86_64", - category: "Base Technologies", - description: "YaST tools for basic system administration.", - icon: "./yast", - kind: :pattern, - name: "yast2_basis", - order: "1220", - source: 0, - summary: "YaST Base Utilities", - user_visible: true, - version: "20220411-1.4" - ), - double( - arch: "noarch", - category: "Graphical Environments", - description: - "Packages providing the Plasma desktop environment and " \ - "applications from KDE.", - icon: "./pattern-kde", - kind: :pattern, - name: "kde", - order: "1110", - source: 0, - summary: "KDE Applications and Plasma 5 Desktop", - user_visible: true, - version: "20230801-1.1" - ) - ] - ) - - kde = Agama::Software::UserPattern.new("kde", false) - allow(subject.product).to receive(:user_patterns).and_return([kde]) - patterns = subject.patterns(true) - - expect(patterns).to contain_exactly( - an_object_having_attributes(name: "kde") - ) - end - end - - describe "#propose" do - before do - subject.select_product("Tumbleweed") - end - - it "creates a new proposal for the selected product" do - expect(proposal).to receive(:languages=).with(["en_US"]) - expect(proposal).to receive(:base_product=).with("openSUSE") - expect(proposal).to receive(:calculate) - subject.propose - end - - include_examples "software issues", "propose" - - it "adds the patterns and packages to install depending on the system architecture" do - expect(proposal).to receive(:set_resolvables) - .with("agama", :pattern, ["enhanced_base"]) - expect(proposal).to receive(:set_resolvables) - .with("agama", :pattern, [], { optional: true }) - expect(proposal).to receive(:set_resolvables) - .with("agama", :package, [ - "NetworkManager", "kernel-default", - "openSUSE-repos-Tumbleweed", "sudo-policy-wheel-auth-self" - ]) - expect(proposal).to receive(:set_resolvables) - .with("agama", :package, [], { optional: true }) - subject.propose - end - end - - describe "#install" do - let(:commit_result) { [250, [], [], [], []] } - - before do - allow(Yast::Pkg).to receive(:Commit).and_return(commit_result) - end - - it "installs the packages" do - expect(Yast::Pkg).to receive(:Commit).with({}) - .and_return(commit_result) - subject.install - end - - it "sets up the package callbacks" do - expect(Agama::Software::Callbacks::Progress).to receive(:setup) - subject.install - end - - context "when packages installation fails" do - let(:commit_result) { nil } - - it "raises an exception" do - expect { subject.install }.to raise_error(RuntimeError) - end - end - - it "moves the packaging target to /mnt" do - expect(Yast::Pkg).to receive(:TargetFinish) - expect(Yast::Pkg).to receive(:TargetInitialize).with(destdir) - expect(Yast::Pkg).to receive(:TargetLoad) - subject.install - end - end - - describe "#finish" do - it "releases the packaging system" do - allow(subject).to receive(:copy_zypp_to_target) - expect(Yast::Pkg).to receive(:SourceSaveAll) - expect(Yast::Pkg).to receive(:TargetFinish) - - subject.finish - end - - it "copies the libzypp cache and credentials to the target system" do - allow(Agama::Software::Repository).to receive(:all).and_return( - [ - Agama::Software::Repository.new( - repo_id: 42, repo_alias: "alias", name: "name", - url: "http://example.com", enabled: true, autorefresh: false - ) - ] - ) - - allow(Dir).to receive(:exist?).and_call_original - allow(Dir).to receive(:entries).and_call_original - - # copying the raw cache - expect(Dir).to receive(:exist?).with( - File.join(target_dir, "/var/cache/zypp/raw") - ).and_return(true) - expect(FileUtils).to receive(:mkdir_p).with( - File.join(Yast::Installation.destdir, "/var/cache/zypp") - ) - expect(FileUtils).to receive(:cp_r).with( - File.join(target_dir, "/var/cache/zypp/raw"), - File.join(Yast::Installation.destdir, "/var/cache/zypp") - ) - - # copy the solv cache - repo_alias = "https-download.opensuse.org-94cc89aa" - expect(Dir).to receive(:entries) - .with(File.join(target_dir, "/var/cache/zypp/solv")) - .and_return([".", "..", "@System", repo_alias]) - expect(FileUtils).to receive(:cp_r).with( - File.join(target_dir, "/var/cache/zypp/solv/", repo_alias), - File.join(Yast::Installation.destdir, "/var/cache/zypp/solv") - ) - # ensure the @System cache is not copied - expect(FileUtils).to_not receive(:cp_r).with( - File.join(target_dir, "/var/cache/zypp/solv/@System"), - File.join(Yast::Installation.destdir, "/var/cache/zypp/solv") - ) - - # copying the credentials.d directory - expect(Dir).to receive(:exist?) - .with(File.join(target_dir, "/etc/zypp/credentials.d")) - .and_return(true) - expect(FileUtils).to receive(:mkdir_p) - .with(File.join(Yast::Installation.destdir, "/etc/zypp")) - expect(FileUtils).to receive(:cp_r).with( - File.join(target_dir, "/etc/zypp/credentials.d"), - File.join(Yast::Installation.destdir, "/etc/zypp") - ) - - # copying the global credentials file - expect(File).to receive(:exist?) - .with(File.join(target_dir, "/etc/zypp/credentials.cat")) - .and_return(true) - expect(FileUtils).to receive(:copy).with( - File.join(target_dir, "/etc/zypp/credentials.cat"), - File.join(Yast::Installation.destdir, "/etc/zypp") - ) - - subject.finish - end - - context "only a local repository is used" do - let(:repo_id) { 42 } - before do - allow(Agama::Software::Repository).to receive(:all).and_return( - [ - Agama::Software::Repository.new( - repo_id: repo_id, repo_alias: "alias", name: "name", - url: "dvd:/install?devices=/dev/sr0", enabled: true, autorefresh: false - ) - ] - ) - end - - it "disables the local repository" do - allow(subject).to receive(:copy_zypp_to_target) - expect(Yast::Pkg).to receive(:SourceSetEnabled).with(repo_id, false) - - subject.finish - end - - it "copies the libzypp cache" do - expect(subject).to receive(:copy_zypp_to_target) - - subject.finish - end - end - end - - describe "#package_installed?" do - before do - allow(Yast::Package).to receive(:Installed).with(package, target: :system) - .and_return(installed?) - end - - let(:package) { "NetworkManager" } - - context "when the package is installed" do - let(:installed?) { true } - - it "returns true" do - expect(subject.package_installed?(package)).to eq(true) - end - end - - context "when the package is not installed" do - let(:installed?) { false } - - it "returns false" do - expect(subject.package_installed?(package)).to eq(false) - end - end - end - - describe "#package_available?" do - before do - allow(Yast::Package).to receive(:Available).with(package).and_return(available) - end - - let(:package) { "NetworkManager" } - - context "when the package is available" do - let(:available) { true } - - it "returns true" do - expect(subject.package_available?(package)).to eq(true) - end - end - - context "when the package is not available" do - let(:available) { false } - - it "returns false" do - expect(subject.package_available?(package)).to eq(false) - end - end - - context "when there is an error checking its availability" do - let(:available) { nil } - - it "returns false" do - expect(subject.package_available?(package)).to eq(false) - end - end - end - - describe "#add_service" do - it "does not raise exception if everything goes well" do - service = double(name: "test", url: "http://test.com") - expect { subject.add_service(service) }.to_not raise_error - end - - it "raises ServiceError when failed to add service" do - expect(Yast::Pkg).to receive(:ServiceForceRefresh).and_return(false) - service = double(name: "test", url: "http://test.com") - expect { subject.add_service(service) }.to raise_error(Agama::Software::ServiceError) - end - end - - describe "#product_issues" do - before do - allow_any_instance_of(Agama::Software::ProductBuilder) - .to receive(:build).and_return([product1, product2]) - end - - let(:product1) do - Agama::Software::Product.new("test1").tap { |p| p.repositories = [] } - end - - let(:product2) do - Agama::Software::Product.new("test2").tap { |p| p.repositories = ["http://test"] } - end - - context "if no product is selected yet" do - it "contains a missing product issue" do - expect(subject.product_issues).to contain_exactly( - an_object_having_attributes( - description: /product not selected/i - ) - ) - end - end - - context "if a product is already selected" do - before do - subject.select_product(product_id) - end - - let(:product_id) { "test1" } - - it "does not include a missing product issue" do - expect(subject.product_issues).to_not include( - an_object_having_attributes( - description: /product not selected/i - ) - ) - end - - context "and the product does not require registration" do - let(:product_id) { "test2" } - - it "does not contain issues" do - expect(subject.product_issues).to be_empty - end - end - - context "and the product requires registration" do - let(:product_id) { "test1" } - let(:product) do - Agama::Software::Product.new("test1").tap do |p| - p.registration = true - p.name = "test1" - end - end - - before do - allow(subject).to receive(:product).and_return(product) - allow(Y2Packager::Resolvable).to receive(:find) - .with(kind: :product, name: product.name) - .and_return(resolvables) - end - - context "and the base product is not available" do - let(:resolvables) { [] } - - it "contains a missing registration issue" do - expect(subject.product_issues).to contain_exactly( - an_object_having_attributes( - kind: :missing_registration - ) - ) - end - - context "and the base product is available" do - let(:resolvables) { [instance_double("Product")] } - - it "does not contain issues" do - expect(subject.product_issues).to be_empty - end - end - end - end - end - end - - describe "#update_selected_patterns" do - it "unselects user patterns unselected by conflict resolution" do - # user selected patterns - expect(Yast::PackagesProposal).to receive(:GetResolvables).with(anything, - :pattern).and_return(["pattern1", "pattern2"]) - # patterns selected in libzypp - expect(Y2Packager::Resolvable).to receive(:find).with(kind: :pattern, - status: :selected).and_return([double(name: "pattern1")]) - # list of patterns is changed - expect(subject).to receive(:selected_patterns_changed) - # the "pattern2" is unselected - expect(Yast::PackagesProposal).to receive(:RemoveResolvables).with(anything, :pattern, - ["pattern2"]) - - subject.update_selected_patterns - end - end - - include_examples "issues" - include_examples "progress" -end diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb deleted file mode 100644 index 560aef47a7..0000000000 --- a/service/test/agama/software/product_builder_test.rb +++ /dev/null @@ -1,308 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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_relative "../../test_helper" -require "yast" -require "agama/config" -require "agama/product_reader" -require "agama/software/product" -require "agama/software/product_builder" - -Yast.import "Arch" - -describe Agama::Software::ProductBuilder do - before do - allow(Agama::ProductReader).to receive(:new).and_return(reader) - end - - let(:reader) { instance_double(Agama::ProductReader, load_products: products) } - - let(:products) do - [ - { - "id" => "Test1", - "name" => "Product Test 1", - "description" => "This is a test product named Test 1", - "version" => "1.0", - "registration" => true, - "license" => "suse", - "translations" => { - "description" => { - "cs" => "Czech", - "es" => "Spanish" - } - }, - "software" => { - "installation_repositories" => [ - { - "url" => "https://repos/test1/x86_64/product/", - "archs" => "x86_64" - }, - { - "url" => "https://repos/test1/aarch64/product/", - "archs" => "aarch64" - } - ], - "mandatory_packages" => [ - { - "package" => "package1-1" - }, - "package1-2", - { - "package" => "package1-3", - "archs" => "aarch64,x86_64" - }, - { - "package" => "package1-4", - "archs" => "ppc64" - } - ], - "optional_packages" => ["package1-5"], - "mandatory_patterns" => ["pattern1-1", "pattern1-2"], - "optional_patterns" => [ - { - "pattern" => "pattern1-3", - "archs" => "x86_64" - }, - { - "pattern" => "pattern1-4", - "archs" => "aarch64" - } - ], - "base_product" => "Test1" - } - }, - { - "id" => "Test2", - "name" => "Product Test 2", - "description" => "This is a test product named Test 2", - "archs" => "x86_64,aarch64", - "version" => "2.0", - "registration" => true, - "software" => { - "mandatory_patterns" => ["pattern2-1"], - "base_product" => "Test2" - } - }, - { - "id" => "Test3", - "name" => "Product Test 3", - "description" => "This is a test product named Test 3", - "archs" => "ppc64,aarch64", - "software" => { - "installation_repositories" => ["https://repos/test3/product/"], - "optional_patterns" => [ - { - "pattern" => "pattern3-1", - "archs" => "aarch64" - } - ], - "base_product" => "Test3" - } - } - ] - end - - subject { described_class.new(config) } - - let(:config) { Agama::Config.new } - - describe "#build" do - context "for x86_64" do - before do - allow(Yast::Arch).to receive("x86_64").and_return(true) - allow(Yast::Arch).to receive("aarch64").and_return(false) - allow(Yast::Arch).to receive("ppc64").and_return(false) - allow(Yast::Arch).to receive("s390").and_return(false) - end - - it "generates products according to the current architecture" do - products = subject.build - - expect(products).to all(be_a(Agama::Software::Product)) - - expect(products).to contain_exactly( - an_object_having_attributes( - id: "Test1", - display_name: "Product Test 1", - description: "This is a test product named Test 1", - name: "Test1", - version: "1.0", - registration: true, - license: "suse", - repositories: ["https://repos/test1/x86_64/product/"], - mandatory_patterns: ["pattern1-1", "pattern1-2"], - optional_patterns: ["pattern1-3"], - mandatory_packages: ["package1-1", "package1-2", "package1-3"], - optional_packages: ["package1-5"], - translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } } - ), - an_object_having_attributes( - id: "Test2", - display_name: "Product Test 2", - description: "This is a test product named Test 2", - name: "Test2", - version: "2.0", - repositories: [], - mandatory_patterns: ["pattern2-1"], - optional_patterns: [], - mandatory_packages: [], - optional_packages: [], - translations: {} - ) - ) - end - end - - context "for aarch64" do - before do - allow(Yast::Arch).to receive("x86_64").and_return(false) - allow(Yast::Arch).to receive("aarch64").and_return(true) - allow(Yast::Arch).to receive("ppc64").and_return(false) - allow(Yast::Arch).to receive("s390").and_return(false) - end - - it "generates products according to the current architecture" do - products = subject.build - - expect(products).to all(be_a(Agama::Software::Product)) - - expect(products).to contain_exactly( - an_object_having_attributes( - id: "Test1", - display_name: "Product Test 1", - description: "This is a test product named Test 1", - name: "Test1", - version: "1.0", - repositories: ["https://repos/test1/aarch64/product/"], - mandatory_patterns: ["pattern1-1", "pattern1-2"], - optional_patterns: ["pattern1-4"], - mandatory_packages: ["package1-1", "package1-2", "package1-3"], - optional_packages: ["package1-5"], - translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } } - ), - an_object_having_attributes( - id: "Test2", - display_name: "Product Test 2", - description: "This is a test product named Test 2", - name: "Test2", - version: "2.0", - registration: true, - repositories: [], - mandatory_patterns: ["pattern2-1"], - optional_patterns: [], - mandatory_packages: [], - optional_packages: [], - translations: {} - ), - an_object_having_attributes( - id: "Test3", - display_name: "Product Test 3", - description: "This is a test product named Test 3", - name: "Test3", - version: nil, - repositories: ["https://repos/test3/product/"], - mandatory_patterns: [], - optional_patterns: ["pattern3-1"], - mandatory_packages: [], - optional_packages: [], - translations: {} - ) - ) - end - end - - context "for ppc64" do - before do - allow(Yast::Arch).to receive("x86_64").and_return(false) - allow(Yast::Arch).to receive("aarch64").and_return(false) - allow(Yast::Arch).to receive("ppc64").and_return(true) - allow(Yast::Arch).to receive("s390").and_return(false) - end - - it "generates products according to the current architecture" do - products = subject.build - - expect(products).to all(be_a(Agama::Software::Product)) - - expect(products).to contain_exactly( - an_object_having_attributes( - id: "Test1", - display_name: "Product Test 1", - description: "This is a test product named Test 1", - name: "Test1", - version: "1.0", - repositories: [], - mandatory_patterns: ["pattern1-1", "pattern1-2"], - optional_patterns: [], - mandatory_packages: ["package1-1", "package1-2", "package1-4"], - optional_packages: ["package1-5"], - translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } } - ), - an_object_having_attributes( - id: "Test3", - display_name: "Product Test 3", - description: "This is a test product named Test 3", - name: "Test3", - version: nil, - repositories: ["https://repos/test3/product/"], - mandatory_patterns: [], - optional_patterns: [], - mandatory_packages: [], - optional_packages: [], - translations: {} - ) - ) - end - end - - context "for s390" do - before do - allow(Yast::Arch).to receive("x86_64").and_return(false) - allow(Yast::Arch).to receive("aarch64").and_return(false) - allow(Yast::Arch).to receive("ppc64").and_return(false) - allow(Yast::Arch).to receive("s390").and_return(true) - end - - it "generates products according to the current architecture" do - products = subject.build - - expect(products).to all(be_a(Agama::Software::Product)) - - expect(products).to contain_exactly( - an_object_having_attributes( - id: "Test1", - display_name: "Product Test 1", - description: "This is a test product named Test 1", - name: "Test1", - version: "1.0", - repositories: [], - mandatory_patterns: ["pattern1-1", "pattern1-2"], - optional_patterns: [], - mandatory_packages: ["package1-1", "package1-2"], - optional_packages: ["package1-5"], - translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } } - ) - ) - end - end - end -end diff --git a/service/test/agama/software/product_test.rb b/service/test/agama/software/product_test.rb deleted file mode 100644 index b02e480b23..0000000000 --- a/service/test/agama/software/product_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -# 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_relative "../../test_helper" -require "agama/software/product" - -describe Agama::Software::Product do - subject do - described_class.new("Test").tap do |product| - product.user_patterns = [ - Agama::Software::UserPattern.new("kde", false), - Agama::Software::UserPattern.new("selinux", true) - ] - end - end - - describe "#localized_description" do - before do - subject.description = "Original description" - subject.translations = { - "description" => { - "cs" => "Czech translation", - "es" => "Spanish translation" - } - } - end - - it "returns untranslated description when the language is not set" do - allow(ENV).to receive(:[]).with("LANG").and_return(nil) - - expect(subject.localized_description).to eq("Original description") - end - - it "returns Czech translation if locale is \"cs_CZ.UTF-8\"" do - allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8") - - expect(subject.localized_description).to eq("Czech translation") - end - - it "returns Czech translation if locale is \"cs\"" do - allow(ENV).to receive(:[]).with("LANG").and_return("cs") - - expect(subject.localized_description).to eq("Czech translation") - end - - it "return untranslated description when translation is not available" do - allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8") - subject.translations = {} - - expect(subject.localized_description).to eq("Original description") - end - end - - describe "#preselected_patterns" do - it "returns the user preselected patterns" do - expect(subject.preselected_patterns).to eq(["selinux"]) - end - end -end diff --git a/service/test/agama/software/proposal_test.rb b/service/test/agama/software/proposal_test.rb deleted file mode 100644 index 0aba698009..0000000000 --- a/service/test/agama/software/proposal_test.rb +++ /dev/null @@ -1,200 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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_relative "../../test_helper" -require "agama/software/proposal" -require "agama/config" - -describe Agama::Software::Proposal do - subject(:proposal) { described_class.new(logger: logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - let(:destdir) { "/mnt" } - let(:result) { {} } - let(:last_error) { "" } - let(:solve_errors) { 0 } - - before do - allow(Yast::Pkg).to receive(:SourceSaveAll) - allow(Yast::Packages).to receive(:Proposal).and_return(result) - allow(Yast::Pkg).to receive(:TargetFinish) - allow(Yast::Pkg).to receive(:TargetInitialize) - allow(Yast::Pkg).to receive(:TargetLoad) - allow(Yast::Installation).to receive(:destdir).and_return(destdir) - allow(Yast::Pkg).to receive(:LastError).and_return(last_error) - allow(Yast::Pkg).to receive(:PkgSolveErrors).and_return(solve_errors) - allow(Yast::Pkg).to receive(:SetSolverFlags) - end - - describe "#calculate" do - it "makes a proposal" do - expect(Yast::Packages).to receive(:Proposal).and_return(result) - expect(Yast::Pkg).to receive(:PkgSolve) - subject.calculate - end - - it "selects the language packages" do - expect(Yast::Pkg).to receive(:SetPackageLocale).with("cs_CZ") - expect(Yast::Pkg).to receive(:SetAdditionalLocales).with(["de_DE"]) - subject.languages = ["cs_CZ", "de_DE"] - subject.calculate - end - - it "returns true" do - expect(subject.calculate).to eq(true) - end - - context "when a proposal is not possible or contain errors" do - let(:solve_errors) { 1 } - - it "returns false" do - expect(subject.calculate).to eq(false) - end - end - - context "when no errors were reported" do - it "does not register any issue" do - subject.calculate - expect(subject.issues).to be_empty - end - end - - context "when a blocking warning is reported" do - let(:result) do - { "warning_level" => :blocker, "warning" => "Could not install..." } - end - - it "registers the corresponding issue" do - subject.calculate - expect(subject.issues).to contain_exactly( - an_object_having_attributes({ description: "Could not install..." }) - ) - end - end - - context "when solver errors are reported" do - let(:solve_errors) { 5 } - - it "registers them as issues" do - subject.calculate - expect(subject.issues).to contain_exactly( - an_object_having_attributes(description: "Found 5 dependency issues.") - ) - end - end - end - - describe "#solve_dependencies" do - it "calls the solver" do - expect(Yast::Pkg).to receive(:PkgSolve) - subject.solve_dependencies - end - - context "if the solver successes" do - before do - allow(Yast::Pkg).to receive(:PkgSolve).and_return(true) - end - - it "returns true" do - expect(subject.solve_dependencies).to eq(true) - end - end - - context "if the solver fails" do - before do - allow(Yast::Pkg).to receive(:PkgSolve).and_return(false) - end - - let(:solve_errors) { 2 } - - it "returns false" do - expect(subject.solve_dependencies).to eq(false) - end - - it "registers solver issue" do - subject.solve_dependencies - expect(subject.issues).to contain_exactly( - an_object_having_attributes(description: "Found 2 dependency issues.") - ) - end - end - end - - describe "#set_resolvables" do - it "adds the list of packages/patterns to the proposal" do - expect(Yast::PackagesProposal).to receive(:SetResolvables) - .with("agama", :pattern, "alp_base", optional: false) - subject.set_resolvables("agama", :pattern, "alp_base", optional: false) - end - end - - describe "#packages_count" do - before do - allow(Yast::Pkg).to receive(:PkgMediaCount).and_return([[75], [50], [25], [0]]) - end - - it "returns the amount of packages to install" do - expect(subject.packages_count).to eq(150) - end - end - - describe "#packages_size" do - before do - allow(Yast::Pkg).to receive(:PkgMediaSizes) - .and_return([[900000000], [0], [500000000]]) - end - - it "returns the size of packages to install" do - expect(subject.packages_size).to eq(1400000000) - end - end - - describe "#valid?" do - context "when the proposal was calculated and there were no errors" do - it "returns true" do - subject.calculate - expect(subject.valid?).to eq(true) - end - end - - context "when the proposal is not calculated yet" do - it "returns false" do - expect(subject.valid?).to eq(false) - end - end - - context "when there are errors" do - let(:solve_errors) { 1 } - - it "returns false" do - subject.calculate - expect(subject.valid?).to eq(false) - end - end - end - - describe "#languages" do - it "sets the languages to install removing the encoding" do - subject.languages = ["es_ES.UTF-8", "en_US"] - expect(subject.languages).to eq(["es_ES", "en_US"]) - end - end -end diff --git a/service/test/agama/software/repositories_manager_test.rb b/service/test/agama/software/repositories_manager_test.rb deleted file mode 100644 index e3dc87a923..0000000000 --- a/service/test/agama/software/repositories_manager_test.rb +++ /dev/null @@ -1,229 +0,0 @@ -# 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_relative "../../test_helper" -require "agama/software/repositories_manager" - -describe Agama::Software::RepositoriesManager do - subject do - res = described_class.instance - res.reset - res - end - - # probe and refresh succeed - let(:repo) do - instance_double( - Agama::Software::Repository, enable!: nil, probe: true, enabled?: true, refresh: true - ) - end - - let(:disabled_repo) do - instance_double(Agama::Software::Repository, enable!: nil, enabled?: false) - end - - let(:user_repositories) do - [{ - "url" => "http://testing.com", - "alias" => "test", - "enabled" => true, - "priority" => 50, - "allow_unsigned" => true, - "gpg_fingerprints" => ["0123"] - }] - end - - describe "#add" do - it "registers the repository in the packaging system" do - url = "https://example.net" - expect(Agama::Software::Repository).to receive(:create) - .with(autorefresh: true, name: url, repo_alias: "", url: url, priority: 99) - .and_return(repo) - subject.add(url) - expect(subject.repositories).to include(repo) - end - end - - describe "#load" do - # probe and refresh fail - let(:repo1) do - instance_double( - Agama::Software::Repository, enable!: nil, disable!: nil, probe: false, refresh: false - ) - end - - # corner case, probe succeeds but refresh fails - let(:repo2) do - instance_double( - Agama::Software::Repository, enable!: nil, disable!: nil, probe: true, refresh: false - ) - end - - before do - subject.repositories << repo - subject.repositories << repo1 - subject.repositories << repo2 - allow(Yast::Pkg).to receive(:SourceLoad) - end - - it "enables the repositories that can be read" do - expect(repo).to receive(:enable!) - expect(repo).to_not receive(:disable!) - subject.load - end - - it "disables the repositories that cannot be probed" do - expect(repo1).to receive(:disable!) - subject.load - end - - it "disables the repositories that cannot be refreshed" do - expect(repo2).to receive(:disable!) - subject.load - end - - it "loads the repositories" do - expect(Yast::Pkg).to receive(:SourceLoad) - subject.load - end - end - - describe "#delete_all" do - before do - subject.repositories << repo - subject.repositories << disabled_repo - end - - it "deletes all the repositories" do - expect(repo).to receive(:delete!) - expect(disabled_repo).to receive(:delete!) - subject.delete_all - end - end - - describe "#enabled" do - before do - subject.repositories << repo - subject.repositories << disabled_repo - end - - it "returns the enabled repositories" do - expect(subject.enabled).to eq([repo]) - end - end - - describe "#disabled" do - before do - subject.repositories << repo - subject.repositories << disabled_repo - end - - it "returns the enabled repositories" do - expect(subject.disabled).to eq([disabled_repo]) - end - end - - describe "unsigned_allowed?" do - it "returns true if user repo can be unsigned" do - allow(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - allow(Agama::Software::Repository).to receive(:find).and_return(double) - allow(subject).to receive(:load) - subject.user_repositories = user_repositories - - expect(subject.unsigned_allowed?("test")).to eq true - end - end - - describe "trust_gpg?" do - it "returns true if gpg key fingerprint matches user defined one" do - allow(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - allow(Agama::Software::Repository).to receive(:find).and_return(double) - allow(subject).to receive(:load) - subject.user_repositories = user_repositories - - expect(subject.trust_gpg?("test", "0123")).to eq true - end - - it "ignores any whitespaces in fingerprint" do - allow(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - allow(Agama::Software::Repository).to receive(:find).and_return(double) - allow(subject).to receive(:load) - subject.user_repositories = user_repositories - - expect(subject.trust_gpg?("test", "01 23")).to eq true - end - end - - describe "#user_repositories" do - before do - allow(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - allow(Agama::Software::Repository).to receive(:find).and_return(double) - allow(subject).to receive(:load) - end - - it "returns list of repositories as defined by user" do - subject.user_repositories = user_repositories - expect(subject.user_repositories).to eq user_repositories - end - end - - describe "#user_repositories=" do - before do - allow(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - allow(Agama::Software::Repository).to receive(:find).and_return(double) - allow(subject).to receive(:load) - end - - it "sets list of user repositories" do - subject.user_repositories = user_repositories - expect(subject.user_repositories).to eq user_repositories - end - - it "add repositories to repository pool" do - expect(Yast::Pkg).to receive(:RepositoryAdd).and_return(1) - repo = double - allow(Agama::Software::Repository).to receive(:find).with(1).and_return(repo) - - subject.user_repositories = user_repositories - expect(subject.repositories).to include repo - end - - it "loads repositories" do - expect(subject).to receive(:load) - - subject.user_repositories = user_repositories - end - - it "removes previous user repositories" do - old_repo = double - allow(Agama::Software::Repository).to receive(:find).and_return(old_repo) - subject.user_repositories = user_repositories - - expect(old_repo).to receive(:delete!) - new_repo = double - allow(Agama::Software::Repository).to receive(:find).and_return(new_repo) - - subject.user_repositories = user_repositories - expect(subject.repositories).to_not include old_repo - expect(subject.repositories).to include new_repo - end - end -end diff --git a/service/test/agama/software/repository_test.rb b/service/test/agama/software/repository_test.rb deleted file mode 100644 index e2c105cb26..0000000000 --- a/service/test/agama/software/repository_test.rb +++ /dev/null @@ -1,102 +0,0 @@ -# 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_relative "../../test_helper" -require "agama/software/repository" - -describe Agama::Software::Repository do - subject do - described_class.new( - repo_id: 1, repo_alias: "tumbleweed", name: "openSUSE Tumbleweed", - url: "https://example.net/oss", enabled: true, autorefresh: true, - product_dir: "/" - ) - end - - describe "#probe" do - before do - allow(Yast::Pkg).to receive(:RepositoryProbe).with(/example.net/, "/") - .and_return(repo_type) - - # do not call real sleep to make the test faster - allow_any_instance_of(Agama::Software::Repository).to receive(:sleep) - end - - context "if the repository can be read" do - let(:repo_type) { "YUM" } - - it "returns true" do - expect(subject.probe).to eq(true) - end - end - - context "if the repository type cannot be inferred" do - let(:repo_type) { "NONE" } - - it "returns false" do - expect(subject.probe).to eq(false) - end - end - - context "if the repository cannot be red" do - let(:repo_type) { nil } - - it "returns false" do - expect(subject.probe).to eq(false) - end - - it "retries probing automatically" do - expect(Yast::Pkg).to receive(:RepositoryProbe).at_least(2).times.and_return(nil) - subject.probe - end - end - end - - describe "#refresh" do - before do - allow(Yast::Pkg).to receive(:SourceRefreshNow).and_return(refresh_result) - - # do not call real sleep to make the test faster - allow(subject).to receive(:sleep) - end - - context "if the repository can be refreshed" do - let(:refresh_result) { true } - - it "returns true" do - expect(subject.refresh).to eq(true) - end - end - - context "if the repository cannot be refreshed" do - let(:refresh_result) { nil } - - it "returns false" do - expect(subject.refresh).to eq(false) - end - - it "retries refresh automatically" do - expect(Yast::Pkg).to receive(:SourceRefreshNow).at_least(2).times - subject.refresh - end - end - end -end diff --git a/service/test/agama/storage/finisher_test.rb b/service/test/agama/storage/finisher_test.rb index 4c2e3ed3a5..572abb52b7 100644 --- a/service/test/agama/storage/finisher_test.rb +++ b/service/test/agama/storage/finisher_test.rb @@ -24,13 +24,12 @@ require_relative "../with_progress_examples" require "agama/helpers" require "agama/config" -require "agama/security" require "agama/storage/finisher" describe Agama::Storage::Finisher do include Agama::RSpec::StorageHelpers - subject(:storage) { described_class.new(logger, config, security) } + subject(:storage) { described_class.new(logger, config) } let(:logger) { Logger.new($stdout, level: :warn) } let(:config_path) do @@ -39,7 +38,6 @@ let(:destdir) { File.join(FIXTURES_PATH, "target_dir") } let(:config) { Agama::Config.from_file(config_path) } - let(:security) { instance_double(Agama::Security, write: nil) } let(:copy_files) { Agama::Storage::Finisher::CopyFilesStep.new(logger) } let(:progress) { instance_double(Agama::OldProgress, step: nil) } diff --git a/service/test/agama/storage/manager_test.rb b/service/test/agama/storage/manager_test.rb index c497800c71..80a39ae596 100644 --- a/service/test/agama/storage/manager_test.rb +++ b/service/test/agama/storage/manager_test.rb @@ -50,23 +50,17 @@ File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml") end let(:config) { Agama::Config.from_file(config_path) } - let(:files_client) { instance_double(Agama::HTTP::Clients::Files, write: nil) } - let(:scripts_client) { instance_double(Agama::HTTP::Clients::Scripts, run: nil) } - let(:scripts_dir) { File.join(tmp_dir, "run", "agama", "scripts") } let(:tmp_dir) { Dir.mktmpdir } + let(:http_client) { instance_double(Agama::HTTP::Clients::Main) } before do mock_storage(devicegraph: scenario) allow(Agama::Storage::Proposal).to receive(:new).and_return(proposal) allow(Agama::HTTP::Clients::Questions).to receive(:new).and_return(questions_client) - allow(Agama::HTTP::Clients::Software).to receive(:new).and_return(software) + allow(Agama::HTTP::Clients::Main).to receive(:new).and_return(http_client) allow(Bootloader::FinishClient).to receive(:new).and_return(bootloader_finish) - allow(Agama::Security).to receive(:new).and_return(security) # mock writting config as proposal call can do storage probing, which fails in CI allow_any_instance_of(Agama::Storage::Bootloader).to receive(:write_config) - allow(Agama::HTTP::Clients::Files).to receive(:new).and_return(files_client) - allow(Agama::HTTP::Clients::Scripts).to receive(:new).and_return(scripts_client) - allow(Agama::Network).to receive(:new).and_return(network) allow(Yast::Installation).to receive(:destdir).and_return(File.join(tmp_dir, "mnt")) stub_const("Agama::Storage::Finisher::CopyLogsStep::SCRIPTS_DIR", File.join(tmp_dir, "run", "agama", "scripts")) @@ -79,12 +73,7 @@ let(:y2storage_manager) { Y2Storage::StorageManager.instance } let(:proposal) { Agama::Storage::Proposal.new(config, logger: logger) } let(:questions_client) { instance_double(Agama::HTTP::Clients::Questions) } - let(:software) do - instance_double(Agama::HTTP::Clients::Software, config: { "product" => "ALP" }) - end - let(:network) { instance_double(Agama::Network, link_resolv: nil, unlink_resolv: nil) } let(:bootloader_finish) { instance_double(Bootloader::FinishClient, write: nil) } - let(:security) { instance_double(Agama::Security, write: nil) } let(:scenario) { "empty-hd-50GiB.yaml" } describe "#activate" do @@ -277,7 +266,7 @@ describe "#add_packages" do before do allow(y2storage_manager).to receive(:staging).and_return(proposed_devicegraph) - allow(Yast::PackagesProposal).to receive(:SetResolvables) + allow(Agama::HTTP::Clients::Main).to receive(:new).and_return(http_client) end let(:proposed_devicegraph) do @@ -293,9 +282,8 @@ end it "adds storage software to install" do - expect(Yast::PackagesProposal).to receive(:SetResolvables) do |_, _, packages| - expect(packages).to contain_exactly("btrfsprogs", "snapper") - end + expect(http_client).to receive(:set_resolvables) + .with("storage_proposal", :package, match(include("btrfsprogs", "snapper"))) storage.add_packages end @@ -307,9 +295,8 @@ end it "adds the iSCSI software to install" do - expect(Yast::PackagesProposal).to receive(:SetResolvables) do |_, _, packages| - expect(packages).to include("open-iscsi", "iscsiuio") - end + expect(http_client).to receive(:set_resolvables) + .with("storage_proposal", :package, match(include("iscsiuio"))) storage.add_packages end @@ -326,9 +313,8 @@ end it "adds the iSCSI software to install" do - expect(Yast::PackagesProposal).to receive(:SetResolvables) do |_, _, packages| - expect(packages).to include("open-iscsi", "iscsiuio") - end + expect(http_client).to receive(:set_resolvables) + .with("storage_proposal", :package, match(include("open-iscsi", "iscsiuio"))) storage.add_packages end @@ -358,38 +344,17 @@ it "copy needed files, installs the bootloader, sets up the snapshots, " \ "copy logs, symlink resolv.conf, runs the post-installation scripts, " \ "unlink resolv.conf, and umounts the file systems" do - expect(security).to receive(:write) expect(copy_files).to receive(:run) expect(bootloader_finish).to receive(:write) expect(Yast::WFM).to receive(:CallFunction).with("storage_finish", ["Write"]) expect(Yast::WFM).to receive(:CallFunction).with("iscsi-client_finish", ["Write"]) expect(Yast2::FsSnapshot).to receive(:configure_snapper) - expect(network).to receive(:link_resolv) - expect(scripts_client).to receive(:run).with("post") - expect(files_client).to receive(:write) - expect(network).to receive(:unlink_resolv) - expect(Yast::Execute).to receive(:on_target!) - .with("systemctl", "enable", "agama-scripts", allowed_exitstatus: [0, 1]) expect(Yast::WFM).to receive(:CallFunction).with("umount_finish", ["Write"]) expect(Yast::Execute).to receive(:locally).with( "agama", "logs", "store", "--destination", /\/var\/log\/agama-installation\/logs/ ) storage.finish end - - context "when scripts artifacts exist" do - before do - FileUtils.mkdir_p(scripts_dir) - FileUtils.touch(File.join(scripts_dir, "test.sh")) - allow(Yast::Execute).to receive("locally").with("agama", "logs", "store", any_args) - end - - it "copies the artifacts to the installed system" do - storage.finish - expect(File).to exist(File.join(tmp_dir, "mnt", "var", "log", "agama-installation", - "scripts")) - end - end end describe "#actions" do diff --git a/service/test/agama/users_test.rb b/service/test/agama/users_test.rb deleted file mode 100644 index bd189eda4f..0000000000 --- a/service/test/agama/users_test.rb +++ /dev/null @@ -1,247 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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_relative "../test_helper" -require "agama/users" - -describe Agama::Users do - subject(:storage) { described_class.new(logger) } - - let(:logger) { Logger.new($stdout) } - - let(:users_config) { Y2Users::Config.new } - - before do - allow(Y2Users::ConfigManager.instance).to receive(:target) - .and_return(users_config) - end - - describe "#assign_root_password" do - let(:root_user) { instance_double(Y2Users::User) } - - describe "#root_user" do - it "returns the root user" do - root = subject.root_user - expect(root.name).to eq("root") - end - end - - context "when the password is hashed" do - it "sets the password as hashed" do - subject.assign_root_password("hashed", true) - root_user = users_config.users.root - expect(root_user.password).to eq(Y2Users::Password.create_encrypted("hashed")) - end - end - - context "when the password is not hashed" do - it "sets the password in clear text" do - subject.assign_root_password("12345", false) - root_user = users_config.users.root - expect(root_user.password).to eq(Y2Users::Password.create_plain("12345")) - end - end - end - - describe "#remove_root_password" do - it "removes the password" do - subject.assign_root_password("12345", false) - root = subject.root_user - expect(root.password).to be_kind_of(Y2Users::Password) - subject.remove_root_password - expect(root.password).to be_nil - end - end - - describe "#assign_first_user" do - context "when the options given do not present any issue" do - it "adds the user to the user's configuration" do - subject.assign_first_user("Jane Doe", "jane", "12345", false, {}) - user = users_config.users.by_name("jane") - expect(user.full_name).to eq("Jane Doe") - expect(user.password).to eq(Y2Users::Password.create_plain("12345")) - expect(user.groups.map(&:name)).to eq(["wheel"]) - end - - context "when a first user exists" do - before do - subject.assign_first_user("Jane Doe", "jane", "12345", false, {}) - end - - it "replaces the user with the new one" do - subject.assign_first_user("John Doe", "john", "12345", false, {}) - - user = users_config.users.by_name("jane") - expect(user).to be_nil - - user = users_config.users.by_name("john") - expect(user.full_name).to eq("John Doe") - end - end - - it "returns an empty array of issues" do - issues = subject.assign_first_user("Jane Doe", "jane", "12345", false, {}) - expect(issues).to be_empty - end - end - - context "when the given arguments presents some critical error" do - it "does not add the user to the config" do - subject.assign_first_user("Jonh Doe", "john", "", false, {}) - user = users_config.users.by_name("john") - expect(user).to be_nil - subject.assign_first_user("Ldap user", "ldap", "12345", false, {}) - user = users_config.users.by_name("ldap") - expect(user).to be_nil - end - - it "returns an array with all the issues" do - issues = subject.assign_first_user("Root user", "root", "12345", false, {}) - expect(issues.size).to eql(1) - end - end - end - - describe "#remove_first_user" do - before do - subject.assign_first_user("Jane Doe", "jane", "12345", false, {}) - end - - it "removes the already defined first user" do - expect { subject.remove_first_user } - .to change { users_config.users.by_name("jane") } - .from(Y2Users::User).to(nil) - end - end - - describe "#write" do - let(:writer) { instance_double(Y2Users::Linux::Writer, write: issues) } - let(:issues) { [] } - - let(:system_config) do - user = Y2Users::User.create_system("messagebus") - config = Y2Users::Config.new - config.attach(user) - end - - before do - allow(Y2Users::ConfigManager.instance).to receive(:system) - .with(force_read: true).and_return(system_config) - allow(Y2Users::Linux::Writer).to receive(:new).and_return(writer) - allow(Yast::Execute).to receive(:locally!) - end - - it "writes system and installer defined users" do - subject.assign_first_user("Jane Doe", "jane", "12345", false, {}) - - expect(Y2Users::Linux::Writer).to receive(:new) do |target_config, _old_config| - user_names = target_config.users.map(&:name) - expect(user_names).to include("messagebus", "jane") - writer - end - - expect(writer).to receive(:write).and_return([]) - subject.write - end - - context "when a SSH public key for the root user is given" do - let(:firewalld) do - Y2Firewall::Firewalld.instance - end - - before do - subject.root_ssh_key = "ssh-rsa ..." - allow(firewalld).to receive(:installed?).and_return(true) - end - - it "enables the sshd service" do - expect(Yast::Service).to receive(:Enable).with("sshd") - expect(firewalld.api).to receive(:add_service).with(firewalld.default_zone, "ssh") - subject.write - end - end - - context "when no SSH public key is given" do - before do - subject.assign_root_password("", false) - end - - it "disables the root password" do - expect(subject).to receive(:assign_root_password).with("!", true) - subject.write - end - end - - context "if some issue occurs" do - let(:issues) { [double("issue")] } - - it "logs the issue" do - expect(logger).to receive(:error).with(/issue/) - subject.write - end - end - - it "writes without /run bind mounted" do - expect(Yast::Execute).to receive(:locally!).with(/umount/, anything) - - subject.write - end - - describe "#issues" do - context "when a root password is set" do - before do - subject.assign_root_password("123456", true) - end - - it "returns an empty list" do - expect(subject.issues).to be_empty - end - end - - context "when a first user is defined" do - before do - subject.assign_first_user("Jane Doe", "jdoe", "123456", false, {}) - end - - it "returns an empty list" do - expect(subject.issues).to be_empty - end - end - - context "when a root SSH key is set" do - before do - subject.root_ssh_key = "ssh-rsa ..." - end - - it "returns an empty list" do - expect(subject.issues).to be_empty - end - end - - context "when neither a first user is defined nor the root password/SSH key is set" do - it "returns the problem" do - error = subject.issues.first - expect(error.description).to match(/Defining a user/) - end - end - end - end -end