diff --git a/rust/share/system.dasd.schema.json b/rust/share/system.dasd.schema.json new file mode 100644 index 0000000000..a769fc30a7 --- /dev/null +++ b/rust/share/system.dasd.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://github.com/openSUSE/agama/blob/master/rust/share/system.storage.schema.json", + "title": "System", + "description": "API description of the DASD system", + "type": "object", + "additionalProperties": false, + "required": ["devices"], + "properties": { + "devices": { + "description": "DASD devices", + "type": "array", + "items": { "$ref": "#/$defs/device" } + } + }, + "$defs": { + "device": { + "type": "object", + "additionalProperties": false, + "required": [ + "channel", + "deviceName", + "type", + "diag", + "accessType", + "partitionInfo", + "status", + "active", + "formatted" + ], + "properties": { + "channel": { "type": "string" }, + "deviceName": { "type": "string" }, + "type": { "type": "string" }, + "diag": { "type": "boolean" }, + "accessType": { "type": "string" }, + "partitionInfo": { "type": "string" }, + "status": { "type": "string" }, + "active": { "type": "boolean" }, + "formatted": { "type": "boolean" } + } + } + } +} diff --git a/service/lib/agama/dbus/storage/dasd.rb b/service/lib/agama/dbus/storage/dasd.rb index 9a9f4eade5..de418091bd 100644 --- a/service/lib/agama/dbus/storage/dasd.rb +++ b/service/lib/agama/dbus/storage/dasd.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2026] SUSE LLC # # All Rights Reserved. # @@ -19,151 +19,164 @@ # 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/with_progress" +require "dbus" +require "json" +require "yast" module Agama module DBus module Storage - # Class representing a DASD in the D-Bus tree - class Dasd < BaseObject - # YaST representation of the DASD - # - # @return [Y2S390::Dasd] - attr_reader :dasd + # D-Bus object to manage DASD. + class DASD < BaseObject + include Yast::I18n + include Agama::WithProgress - # Constructor - # - # @param dasd [Y2S390::Dasd] See {#dasd} - # @param path [DBus::ObjectPath] Path in which the object is exported + PATH = "/org/opensuse/Agama/Storage1/DASD" + private_constant :PATH + + # @param manager [Agama::Storage::DASD::Manager] # @param logger [Logger, nil] - def initialize(dasd, path, logger: nil) - super(path, logger: logger) - @dasd = dasd + def initialize(manager, logger: nil) + textdomain "agama" + super(PATH, logger: logger) + @manager = manager + register_callbacks end - # The device channel id - # - # @return [String] - def id - dasd.id + dbus_interface "org.opensuse.Agama.Storage1.DASD" do + dbus_method(:Probe) { probe } + dbus_method(:GetSystem, "out system:s") { recover_system } + dbus_method(:GetConfig, "out config:s") { recover_config } + dbus_method(:SetConfig, "in serialized_config:s") do |serialized_config| + configure(serialized_config) + end + dbus_signal(:SystemChanged, "system:s") + dbus_signal(:ProgressChanged, "progress:s") + dbus_signal(:ProgressFinished) + dbus_signal(:FormatChanged, "summary:s") + dbus_signal(:FormatFinished, "status:s") end - # Whether the device is enabled - # - # @return [Boolean] - def enabled - !dasd.offline? + # Implementation for the API method #Probe. + def probe + start_progress(1, _("Probing DASD devices")) + manager.probe + emit_system_changed + finish_progress end - # The associated device name + # Gets the serialized system information. # - # @return [String] empty if the device is not enabled - def device_name - dasd.device_name || "" + # @return [String] + def recover_system + manager.probe unless manager.probed? + JSON.pretty_generate(system_json) end - # Whether the device is formatted + # Gets the serialized config. # - # @return [Boolean] - def formatted - dasd.formatted? + # @return [String] + def recover_config + JSON.pretty_generate(manager.config_json) end - # Whether the DIAG access method is enabled + # Applies the given serialized DASD config. # - # YaST traditionally displays #use_diag, which is always false for disabled devices (see - # more info about the YaST behavior regarding DIAG at Agama::Storage::DASD::Manager). - # But displaying #diag_wanted is surely more useful. For enabled DASDs both values match - # and for disabled DASDs #diag_wanted is more informative. + # @todo Raise error if the config is not valid. # - # @return [Boolean] - def diag - dasd.diag_wanted - end + # @param serialized_config [String] Serialized DASD config according to the JSON schema. + def configure(serialized_config) + config_json = JSON.parse(serialized_config, symbolize_names: true) - # The DASD device type concatenating the cutype and devtype (i.e. 3990/E9 3390/0A) - # - # @return [String] empty if unknown - def device_type - dasd.device_type || "" + # Do not configure if there is no config + return unless config_json + + # Do not configure if there is nothing to change. + return if manager.configured?(config_json) + + perform_configuration(config_json) end - ECKD = "ECKD" - FBA = "FBA" + private - # Return the type from the device type + # @return [Agama::Storage::ISCSI::Manager] + attr_reader :manager + + # Performs the configuration process in a separate thread. # - # @see https://github.com/SUSE/s390-tools/blob/master/dasd_configure#L162 + # The configuration could take long time (e.g., formatting devices). It is important to not + # block the service in order to make possible to attend other requests. # - # @return [String] - def type_from(device_type) - cu_type, dev_type = device_type.split - return ECKD if cu_type.to_s.start_with?("3990", "2105", "2107", "1750", "9343") - return FBA if cu_type.to_s.start_with?("6310") - - if cu_type.start_with?("3880") - return ECKD if dev_type.start_with?("3390") - return FBA if dev_type.start_with?("3370") - end + # @raise if there is an unfinished configuration. + # + # @param config_json [Hash] + def perform_configuration(config_json) + raise "Previous configuration is not finished yet" if @configuration_thread&.alive? - device_type - end + logger.info("Configuring DASD") - # The DASD type (ECKD, FBA) - # - # @return [String] empty if unknown - def type - dasd.type || type_from(device_type) + @configuration_thread = Thread.new do + start_progress(1, _("Configuring DASD")) + manager.configure(config_json) + emit_system_changed + finish_progress + end end - # Access type ('rw', 'ro') - # - # @return [String] empty if unknown - def access_type - dasd.access_type || "" + # @return [Hash] + def system_json + { devices: devices_json } end - # Device status if known or 'unknown' if not - # - # @return [String] 'active', 'read_only', 'offline', 'no_format', or 'unknown' - def status - dasd.status.to_s + # @return [Hash] + def devices_json + manager.devices.map { |d| device_json(d) } end - # Description of the partitions - # - # @return [String] empty if the information is unknown - def partition_info - dasd.partition_info || "" + # @param dasd [Y2S390::Dasd] + # @return [Hash] + def device_json(dasd) + { + channel: dasd.id, + deviceName: dasd.device_name || "", + type: manager.device_type(dasd), + diag: dasd.use_diag, + accessType: dasd.access_type || "", + partitionInfo: dasd.partition_info || "", + status: dasd.status.to_s, + active: !dasd.offline?, + formatted: dasd.formatted? + } end - # Sets the associated DASD object - # - # @note A properties changed signal is always emitted. - # - # @param value [Y2S390::Dasd] - def dasd=(value) - @dasd = value + # Emits the SystemChanged signal + def emit_system_changed + self.SystemChanged(recover_system) + end - properties = interfaces_and_properties[DASD_DEVICE_INTERFACE] - dbus_properties_changed(DASD_DEVICE_INTERFACE, properties, []) + def register_callbacks + on_progress_change { self.ProgressChanged(progress.to_json) } + on_progress_finish { self.ProgressFinished } + manager.on_format_change do |format_statuses| + summary_json = format_summary_json(format_statuses) + serialized_summary = JSON.pretty_generate(summary_json) + self.FormatChanged(serialized_summary) + end + manager.on_format_finish { |process_status| self.FormatFinished(process_status.to_s) } end - # Interface name representing a DASD object - DASD_DEVICE_INTERFACE = "org.opensuse.Agama.Storage1.DASD.Device" - private_constant :DASD_DEVICE_INTERFACE - - dbus_interface DASD_DEVICE_INTERFACE do - dbus_reader(:id, "s") - dbus_reader(:enabled, "b") - dbus_reader(:device_name, "s") - dbus_reader(:formatted, "b") - dbus_reader(:diag, "b") - dbus_reader(:status, "s") - dbus_reader(:type, "s") - dbus_reader(:access_type, "s") - dbus_reader(:partition_info, "s") + # @return [Array] + def format_summary_json(format_statuses) + format_statuses.map do |format_status| + { + channel: format_status.dasd.id, + totalCylinders: format_status.cylinders, + FormattedCylinders: format_status.progress, + finished: format_status.done? + } + end end end end diff --git a/service/lib/agama/dbus/storage/dasds_format_job.rb b/service/lib/agama/dbus/storage/dasds_format_job.rb deleted file mode 100644 index a2c89737ec..0000000000 --- a/service/lib/agama/dbus/storage/dasds_format_job.rb +++ /dev/null @@ -1,165 +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 "dbus" -require "agama/dbus/base_object" - -module Agama - module DBus - module Storage - # Class representing the process of formatting a set of DASDs - class DasdsFormatJob < BaseObject - # Internal class to make easier to index the status information both by DASD id and by job - # path, although in fact both are fully stable during the whole execution of the installer. - # - # This class helps to refresh the relationship between the DASD and its path every time a - # status update is sent from the backend, although as already mentioned that wouldn't be - # needed with the current implementation of the DASDs tree, since it keeps that relationship - # stable. - class DasdFormatInfo - # @return [String] channel id of the DASD - attr_accessor :id - - # @return [String] path of the DASD in the D-Bus tree - attr_accessor :path - - # @return [Integer] total number of cylinders reported by the format operation - attr_accessor :cylinders - - # @return [Integer] number of cylinders already processed by the format operation - attr_accessor :progress - - # @return [Boolean] whether the disk is already fully formatted - attr_accessor :done - - # Constructor - # - # @param status [Y2S390::FormatStatus] - # @param dasds_tree [DasdsTree] - def initialize(status, dasds_tree) - @id = status.dasd.id - @cylinders = status.cylinders - @progress = status.progress - @done = status.done? - - dbus_dasd = dasds_tree.find { |d| d.id == @id } - if dbus_dasd - @path = dbus_dasd.path - else - logger.warning "DASD is not longer in the D-BUS tree: #{status.inspect}" - end - end - - # Progress representation as expected by the D-Bus API (property Summary and signal - # SummaryUpdated) - def to_dbus - [cylinders, progress, done] - end - end - - JOB_INTERFACE = "org.opensuse.Agama.Storage1.Job" - private_constant :JOB_INTERFACE - - dbus_interface JOB_INTERFACE do - dbus_reader(:running, "b") - dbus_reader(:exit_code, "u") - dbus_signal(:Finished, "exit_code:u") - end - - DASD_FORMAT_INTERFACE = "org.opensuse.Agama.Storage1.DASD.Format" - private_constant :DASD_FORMAT_INTERFACE - - dbus_interface DASD_FORMAT_INTERFACE do - dbus_reader(:summary, "a{s(uub)}") - end - - # @return [Boolean] - attr_reader :running - - # @return [Integer] zero if still running - attr_reader :exit_code - - # @return [Array] - attr_reader :dasds - - # Constructor - # - # @param initial [Array] initial status report from the format process - # @param dasds_tree [DasdsTree] see #dasds_tree - # @param path [DBus::ObjectPath] path in which the Job object is exported - # @param logger [Logger, nil] - def initialize(initial, dasds_tree, path, logger: nil) - super(path, logger: logger) - - @exit_code = 0 - @running = true - @dasds_tree = dasds_tree - @infos = {} - update_info(initial) - end - - # Current status, in the format described by the D-Bus API - def summary - result = {} - @infos.each_value { |i| result[i.id] = i.to_dbus if i.id } - result - end - - # Marks the job as finished - # - # @note A Finished and a PropertiesChanged signals are always emitted. - # - # @param exit_code [Integer] - def finish_format(exit_code) - @running = false - @exit_code = exit_code - Finished(exit_code) - dbus_properties_changed(JOB_INTERFACE, interfaces_and_properties[JOB_INTERFACE], []) - end - - # Updates the internal status information - # - # @note A SummaryUpdated signal is always emitted - # - # @param statuses [Array summary }, []) - end - - private - - # @return [DasdsTree] D-Bus representation of the DASDs - attr_reader :dasds_tree - - # @param statuses [Array] - def populate(dasds) - delete_missing(dasds) - update(dasds) - end - - # Updates the information of the given DASDs in the D-Bus tree - # - # @param dasds [Array] - def update(dasds) - dasds.each { |dasd| publish(dasd) } - end - - # Finds in the tree the DASDs objects corresponding to the given paths - # - # Paths not corresponding to any DASD are simply ignored. - # - # @param paths [Array] - # @return [Array] - def find_paths(paths) - dbus_dasds.find_all { |d| paths.include?(d.path) } - end - - # @return [Array] - def find(*args, &block) - dbus_dasds.find(*args, &block) - end - - private - - # @return [::DBus::ObjectServer] - attr_reader :service - - # @return [Logger] - attr_reader :logger - - # All exported DASDs - # - # @return [Array] - def dbus_dasds - root = service.get_node(ROOT_PATH, create: false) - return [] unless root - - root.descendant_objects - end - - # @see #populate - def delete_missing(dasds) - missing_dbus_dasds(dasds).each do |dbus_dasd| - service.unexport(dbus_dasd) - end - end - - # @see #delete_missing - def missing_dbus_dasds(dasds) - wanted_ids = dasds.map(&:id) - dbus_dasds.reject { |d| wanted_ids.include?(d.id) } - end - - # Exports or updates the information of a given DASD object - # - # @return [DBus::Storage::Dasd] - def publish(dasd) - dbus_dasd = dbus_dasds.find { |d| d.id == dasd.id } - - if dbus_dasd - dbus_dasd.dasd = dasd - else - dbus_dasd = DBus::Storage::Dasd.new(dasd, next_path, logger: logger) - service.export(dbus_dasd) - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces.rb b/service/lib/agama/dbus/storage/interfaces.rb index 1c7b917c16..2a04dc0f69 100644 --- a/service/lib/agama/dbus/storage/interfaces.rb +++ b/service/lib/agama/dbus/storage/interfaces.rb @@ -29,6 +29,4 @@ module Interfaces end end -require "agama/dbus/storage/interfaces/dasd_manager" -require "agama/dbus/storage/interfaces/device" require "agama/dbus/storage/interfaces/zfcp_manager" diff --git a/service/lib/agama/dbus/storage/interfaces/dasd_manager.rb b/service/lib/agama/dbus/storage/interfaces/dasd_manager.rb deleted file mode 100644 index 08c13cb14a..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/dasd_manager.rb +++ /dev/null @@ -1,210 +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/dbus/storage/dasds_tree" -require "agama/dbus/storage/jobs_tree" -require "agama/storage/dasd/manager" - -module Agama - module DBus - module Storage - module Interfaces - # Mixin to define the D-Bus interface to manage DASD devices - # - # @note This mixin is expected to be included by {Agama::DBus::Storage::Manager}. - module DasdManager - DASD_MANAGER_INTERFACE = "org.opensuse.Agama.Storage1.DASD.Manager" - private_constant :DASD_MANAGER_INTERFACE - - def self.included(base) - base.class_eval do - dbus_interface DASD_MANAGER_INTERFACE do - # Finds DASDs in the system and populates the D-Bus objects tree according - dbus_method(:Probe) do - busy_while { dasd_backend.probe } - end - - # Enables the given list of DASDs. - # See documentation at Agama::Storage::DASD::Manager to understand how the - # use_diag flag is affected by this operation. - dbus_method(:Enable, "in devices:ao, out result:u") do |devs| - busy_while { dasd_enable(devs) } - end - - # Disables the given list of DASDs - dbus_method(:Disable, "in devices:ao, out result:u") do |devs| - busy_while { dasd_disable(devs) } - end - - # Sets the use_diag attribute for the given DASDs to the given value. - # See documentation at Agama::Storage::DASD::Manager to understand what that - # really means (since this follows the same strange convention than YaST). - dbus_method(:SetDiag, "in devices:ao, in diag:b, out result:u") do |devs, diag| - busy_while { dasd_set_diag(devs, diag) } - end - - # Creates a format job for the DASDs in the given list - dbus_method(:Format, "in devices:ao, out result:u, out job:o") do |devs| - busy_while { dasd_format(devs) } - end - end - end - end - - # Enables the DASDs devices indentified by the given D-Bus paths - # - # See {#dasds_dbus_method} for information about the possible return codes. - # - # Succeeds (returns 0) if all the devices where successfully enabled. - # - # @param paths [Array] - # @return [Integer] - def dasd_enable(paths) - dasds_dbus_method(paths) { |dasds| dasd_backend.enable(dasds) } - end - - # Disables the DASDs devices indentified by the given D-Bus paths - # - # See {#dasds_dbus_method} for information about the possible return codes. - # - # Succeeds (returns 0) if all the devices where successfully disabled. - # - # @param paths [Array] - # @return [Integer] - def dasd_disable(paths) - dasds_dbus_method(paths) { |dasds| dasd_backend.disable(dasds) } - end - - # Sets use_diag to the given value for the DASDs devices indentified by the given - # D-Bus paths - # - # See {#dasds_dbus_method} for information about the possible return codes. - # - # Succeeds (returns 0) if the desired value of use_diag is properly set for all the - # given devices. - # - # @param paths [Array] - # @param diag [Boolean] value of the use_diag flag - # @return [Integer] - def dasd_set_diag(paths, diag) - dasds_dbus_method(paths) { |dasds| dasd_backend.set_diag(dasds, diag) } - end - - # Format the DASDs devices indentified by the given D-Bus paths - # - # See {#dasds_dbus_method} for information about the possible return codes. - # - # Succeeds (returns 0) if all the devices where successfully formatted. - # - # @param paths [Array] - # @return [Array(Integer,::DBus::ObjectPath)] - def dasd_format(paths) - dasds = find_dasds(paths) - return [1, "/"] if dasds.nil? - - # Theoreticaly, there is room for a race condition here if the callbacks 'progress' or - # 'finish' are called before the job is created below. But in practice it will not - # happen because dasd_backend#format sleeps before calling any of the callbacks and, of - # course, it only calls them if the formatting process effectively started. - # - # We can change the approach in the future and always create the job beforehand if we - # feel the risk is not acceptable. That would make the Format operation a bit less - # consistent with other methods in this interface. If the format process cannot be - # started it would still return 0 as result and would create a job in the tree with kind - # of meaningless progress information representing the failed execution. - job = nil - progress = proc { |statuses| job.update_format(statuses) } - finish = proc { |result| job.finish_format(result) } - initial_statuses = dasd_backend.format(dasds, on_progress: progress, on_finish: finish) - return [2, "/"] unless initial_statuses - - job = jobs_tree.add_dasds_format(initial_statuses, dasds_tree) - [0, job.path] - end - - # Finds the DASDs matching the given D-Bus paths and calls the given block on them - # - # It only tries to execute the given block if all the paths are found. Otherwise it - # returns 1 without trying to process any of the DASDs. - # - # If all the paths are on the D-Bus tree, it runs the block and returns 0 if it succeeds - # or 2 otherwise. - # - # @return [Integer] - def dasds_dbus_method(paths) - dasds = find_dasds(paths) - return 1 if dasds.nil? - - return 0 if yield(dasds) - - 2 - end - - # Finds the DASDs matching the given D-Bus paths - # - # Returns nil (and logs an error) unless all the paths are known. - # - # @return [Array, nil] - def find_dasds(paths) - dbus_dasds = dasds_tree.find_paths(paths) - if dbus_dasds.size != paths.size - missing = paths - dbus_dasds.map(&:path) - logger.error "Unknown path(s) #{missing} at requested list #{paths}" - return nil - end - - dbus_dasds.map(&:dasd) - end - - # Registers the callbacks necessary to ensure accuracy of the tree of DASD objects - def register_dasd_callbacks - dasd_backend.on_probe do |dasds| - dasds_tree.populate(dasds) - deprecate_system - end - - dasd_backend.on_refresh do |dasds| - dasds_tree.update(dasds) - deprecate_system - end - end - - # Tree with the devices representing the DASDs in the system - def dasds_tree - @dasds_tree ||= Storage::DasdsTree.new(@service, logger: logger) - end - - # Tree with the storage jobs - def jobs_tree - @jobs_tree ||= Storage::JobsTree.new(@service, logger: logger) - end - - # DASD manager - # - # @return [Storage::DASD::Manager] - def dasd_backend - @dasd_backend ||= Agama::Storage::DASD::Manager.new(logger: logger) - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/jobs_tree.rb b/service/lib/agama/dbus/storage/jobs_tree.rb deleted file mode 100644 index 0bd57c3b32..0000000000 --- a/service/lib/agama/dbus/storage/jobs_tree.rb +++ /dev/null @@ -1,69 +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/dbus/with_path_generator" -require "agama/dbus/storage/dasds_format_job" - -module Agama - module DBus - module Storage - # Class representing the storage jobs (D-Bus objects representing long-running processes) - # exported on D-Bus - class JobsTree - include WithPathGenerator - - ROOT_PATH = "/org/opensuse/Agama/Storage1/jobs" - path_generator ROOT_PATH - - # Constructor - # - # @param service [::DBus::ObjectServer] - # @param logger [Logger, nil] - def initialize(service, logger: nil) - @service = service - @logger = logger - end - - # Registers a new job to format a set of DASDs - # - # @param initial [Array] def dbus_objects - @dbus_objects ||= [manager_object, iscsi_object] + @dbus_objects ||= [manager_object, iscsi_object, dasd_object].compact end # @return [Agama::DBus::Storage::Manager] @@ -119,6 +120,18 @@ def iscsi_object @iscsi_object ||= Agama::DBus::Storage::ISCSI.new(manager.iscsi, logger: logger) end + # @return [Agama::DBus::Storage::DASD, nil] + def dasd_object + return unless Yast::Arch.s390 + + return @dasd_object unless @dasd_object.nil? + + require "agama/storage/dasd/manager" + require "agama/dbus/storage/dasd" + manager = Agama::Storage::DASD::Manager.new(logger: logger) + @dasd_object = Agama::DBus::Storage::DASD.new(manager, logger: logger) + end + # @return [Agama::Storage::Manager] def manager @manager ||= Agama::Storage::Manager.new(logger: logger) diff --git a/service/lib/agama/storage/dasd/config.rb b/service/lib/agama/storage/dasd/config.rb new file mode 100644 index 0000000000..98a21d3ce5 --- /dev/null +++ b/service/lib/agama/storage/dasd/config.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 Storage + module DASD + # DASD config. + class Config + # List of devices. + # + # @return [Array] + attr_accessor :devices + + def initialize + @devices = [] + end + + # Whether the config includes a device with the given channel. + # + # @param channel [String] DASD channel. + # @return [Boolean] + def include_device?(channel) + !find_device(channel).nil? + end + + # Searchs for a device with the given channel. + # + # @param channel [String] + # @return [Configs::Device, nil] + def find_device(channel) + devices.find { |d| d.channel == channel } + end + end + end + end +end diff --git a/service/lib/agama/storage/dasd/config_importer.rb b/service/lib/agama/storage/dasd/config_importer.rb new file mode 100644 index 0000000000..d3af0605e2 --- /dev/null +++ b/service/lib/agama/storage/dasd/config_importer.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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/json_importer" +require "agama/storage/dasd/config" +require "agama/storage/dasd/config_importers/device" + +module Agama + module Storage + module DASD + # Class for generating a DASD config object from a JSON. + class ConfigImporter < JSONImporter + # @return [Config] + def target + Config.new + end + + # @see Agama::JSONImporter#imports + def imports + { + devices: import_devices + } + end + + def import_devices + devices_json = json[:devices] + return unless devices_json + + devices_json.map { |d| ConfigImporters::Device.new(d).import } + end + end + end + end +end diff --git a/service/lib/agama/storage/dasd/config_importers.rb b/service/lib/agama/storage/dasd/config_importers.rb new file mode 100644 index 0000000000..e78176d13d --- /dev/null +++ b/service/lib/agama/storage/dasd/config_importers.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 Storage + module DASD + # Namespace for DASD config importers. + module ConfigImporters + end + end + end +end + +require "agama/storage/dasd/config_importers/device" diff --git a/service/lib/agama/storage/dasd/config_importers/device.rb b/service/lib/agama/storage/dasd/config_importers/device.rb new file mode 100644 index 0000000000..01336a9dac --- /dev/null +++ b/service/lib/agama/storage/dasd/config_importers/device.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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/json_importer" +require "agama/storage/dasd/configs/device" + +module Agama + module Storage + module DASD + module ConfigImporters + # Class for generating a DASD device config from a JSON. + class Device < JSONImporter + # @return [Configs::Device] + def target + Configs::Device.new + end + + # @see Agama::JSONImporter#imports + def imports + { + channel: json[:channel], + active: import_active, + format_action: import_format_action, + diag_action: import_diag_action + } + end + + # @return [Boolean] + def import_active + return unless json[:state] + + json[:state] == "active" + end + + # @return [:format, :format_if_needed, :not_format] + def import_format_action + return Configs::Device::FormatAction::FORMAT_IF_NEEDED if json[:format].nil? + + return Configs::Device::FormatAction::FORMAT if json[:format] + + Configs::Device::FormatAction::NONE + end + + # @return [:enable, :disable, :keep] + def import_diag_action + return Configs::Device::DiagAction::NONE if json[:diag].nil? + + json[:diag] ? Configs::Device::DiagAction::ENABLE : Configs::Device::DiagAction::DISABLE + end + end + end + end + end +end diff --git a/service/lib/agama/storage/dasd/configs.rb b/service/lib/agama/storage/dasd/configs.rb new file mode 100644 index 0000000000..62576ed62f --- /dev/null +++ b/service/lib/agama/storage/dasd/configs.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 Storage + module DASD + # Namespace for the DASD configs. + module Configs + end + end + end +end + +require "agama/storage/dasd/configs/device" diff --git a/service/lib/agama/storage/dasd/configs/device.rb b/service/lib/agama/storage/dasd/configs/device.rb new file mode 100644 index 0000000000..f99dc1a64d --- /dev/null +++ b/service/lib/agama/storage/dasd/configs/device.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 Storage + module DASD + module Configs + # DASD device config. + class Device + # Possible format actions. + module FormatAction + FORMAT = :format + FORMAT_IF_NEEDED = :format_if_needed + NONE = :none + + # @return [Array] + def self.all + [FORMAT, FORMAT_IF_NEEDED, NONE] + end + end + + # Possible DIAG actions. + module DiagAction + ENABLE = :enable + DISABLE = :disable + NONE = :none + + # @return [Array] + def self.all + [ENABLE, DISABLE, NONE] + end + end + + # @return [String, nil] + attr_accessor :channel + + # @return [Boolean] + attr_writer :active + + # @return [Symbol] See {FormatAction} + attr_reader :format_action + + # @return [Symbol] See {DiagAction} + attr_reader :diag_action + + # @param channel [String, nil] + def initialize(channel = nil) + @channel = channel + @active = true + @format_action = FormatAction::FORMAT_IF_NEEDED + @diag_action = DiagAction::NONE + end + + # @return [Boolean] + def active? + @active + end + + # @param action [Symbol] see {FormatAction}. + def format_action=(action) + return unless FormatAction.all.include?(action) + + @format_action = action + end + + # @param action [Symbol] See {DiagAction}. + def diag_action=(action) + return unless DiagAction.all.include?(action) + + @diag_action = action + end + end + end + end + end +end diff --git a/service/lib/agama/storage/dasd/format_operation.rb b/service/lib/agama/storage/dasd/format_operation.rb index 03d6a2eb85..932fd781de 100644 --- a/service/lib/agama/storage/dasd/format_operation.rb +++ b/service/lib/agama/storage/dasd/format_operation.rb @@ -46,19 +46,16 @@ def initialize(dasds, on_progress = [], on_finish = []) # DasdActions::Activate seems to imply that, but the code at DasdActions::Format contains a # comment stating otherwise. Let's do nothing for the time being. # - # @return [Array, nil] Initial status for all DASDs, nil if the format - # process couldn't be started. + # @return [Boolean] false if the format process couldn't be started. def run - return nil unless start? + return false unless start? process.initialize_summary - Thread.new do - # Just to be absolutely sure, sleep to ensure the #run method returns and its result is - # processed by the caller before we start calling callbacks - wait - monitor_process - end - process.summary.values + # Just to be absolutely sure, sleep to ensure the #run method returns and its result is + # processed by the caller before we start calling callbacks + wait + monitor_process + true end private diff --git a/service/lib/agama/storage/dasd/manager.rb b/service/lib/agama/storage/dasd/manager.rb index 8ed847f4ed..c74d5d92b7 100644 --- a/service/lib/agama/storage/dasd/manager.rb +++ b/service/lib/agama/storage/dasd/manager.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,7 @@ require "yast" require "y2s390" require "agama/storage/dasd/enable_operation" +require "agama/storage/dasd/config_importer" require "agama/storage/dasd/disable_operation" require "agama/storage/dasd/diag_operation" require "agama/storage/dasd/format_operation" @@ -48,126 +49,309 @@ class Manager # @return [Y2S390::DasdsCollection] attr_reader :devices - # Constructor + # Config according to the JSON schema. # + # @return [Hash, nil] nil if not configured yet. + attr_reader :config_json + # @param logger [Logger, nil] def initialize(logger: nil) @logger = logger || ::Logger.new($stdout) @devices = Y2S390::DasdsCollection.new([]) - - @on_probe_callbacks = [] - @on_refresh_callbacks = [] + # Keeps whether a configuration was already applied. In some cases it is necessary to + # consider that the config has not being applied yet, see {#probe}. + @configured = false + # Keeps the list of locked devices. A device is considered as locked if it is active at + # the time of probing. Those devices should not be deactivated when applying a config that + # does not include such devices. + @locked_devices = [] + # Keeps the list of formatted devices. A device is added to the list when it was formatted + # as effect of applying a config. Those devices should not be formatted anymore when + # applying a new config. + @formatted_devices = [] end - # Reads the list of DASDs from the system + # Whether probing has been already performed. # - # Probe callbacks are called at the end, see {#on_probe}. + # @return [Boolean] + def probed? + !!@probed + end + + # Reads the list of DASDs from the system. def probe - logger.info "Probing DASDs" + @probed = true + # Considering as not configured in order to make possible to reapply the same config after + # probing. This is useful when the config contains some missing dasd, and such a dasd + # appears after a reprobing. + @configured = false @devices = reader.list(force_probing: true) # Initialize the attribute just in case the reader doesn't do it (see bsc#1209162) @devices.each { |d| d.diag_wanted = d.use_diag } - logger.info("Probed DASDs #{@devices.inspect}") + assign_locked_devices(@devices) + end - @on_probe_callbacks.each do |callback| - callback.call(@devices) - end + # Applies the given DASD config. + # + # @param config_json [Hash{Symbol=>Object}] Config according to the JSON schema. + def configure(config_json) + probe unless probed? + + @configured = true + @config_json = config_json + config = ConfigImporter.new(config_json).import + + activate_devices(config) + deactivate_devices(config) + format_devices(config) + enable_diag(config) + disable_diag(config) + remove_locked_devices(config) end - # Registers a callback to be called when the DASDs are probed + # Whether the system is already configured for the given config. # - # @param block [Proc] - def on_probe(&block) - @on_probe_callbacks << block + # @param config_json [Hash] + # @return [Boolean] + def configured?(config_json) + @configured && self.config_json == config_json end - # Registers a callback to be called when some DASDS are modified + # The DASD type (ECKD, FBA) # + # @param dasd [Y2S390::Dasd] + # @return [String] empty if unknown + def device_type(dasd) + dasd.type || type_from(dasd) + end + + # @param block [Proc] + def on_format_change(&block) + @on_format_change_callbacks ||= [] + @on_format_change_callbacks << block + end + # @param block [Proc] - def on_refresh(&block) - @on_refresh_callbacks << block + def on_format_finish(&block) + @on_format_finish_callbacks ||= [] + @on_format_finish_callbacks << block end - # Enables the given list of DASDs + private + + ECKD = "ECKD" + FBA = "FBA" + private_constant :ECKD, :FBA + + # @return [Logger] + attr_reader :logger + + # Activates DASDs devices. # - # Refresh callbacks are called at the end, see {#on_refresh}. + # @param config [Config] + # @return [Array] Activated devices. + def activate_devices(config) + devices = config.devices + .select(&:active?) + .map { |d| find_device(d.channel) } + .compact + .reject(&:active?) + + return [] if devices.empty? + + logger.info("Activating DASDs: #{devices}") + EnableOperation.new(devices, logger).run + refresh_devices(devices) + end + + # Deactivates DASDs devices. # - # NOTE: see note about use_diag at this class description. + # @param config [Config] + # @return [Array] Deactivated devices. + def deactivate_devices(config) + # Explictly deactivated devices. + deactivated_devices = config.devices + .reject(&:active?) + .map { |d| find_device(d.channel) } + .compact + + # Devices that are not included in the config and are not locked. + missing_devices = devices + .reject { |d| device_locked?(d) } + .reject { |d| config.include_device?(d.id) } + + devices = deactivated_devices + .concat(missing_devices) + .uniq + .select(&:active?) + + return [] if devices.empty? + + logger.info("Deactivating DASDs: #{devices}") + DisableOperation.new(devices, logger).run + refresh_devices(devices) + end + + # Formats DASDs devices. # - # @param devices [Array] - # @return [Boolean] true if all the given devices were enabled - def enable(devices) - refresh_after(devices) { EnableOperation.new(devices, logger).run } + # @param config [Config] + # @return [Array] Formatted devices. + def format_devices(config) + devices = config.devices + .select(&:active?) + .select { |d| format_device?(d) } + .map { |d| find_device(d.channel) } + .compact + + return [] if devices.empty? + + logger.info("Formatting DASDs: #{devices}") + + progress = @on_format_change_callbacks || [] + finish = @on_format_finish_callbacks || [] + FormatOperation.new(devices, progress, finish).run + add_formatted_devices(devices) + refresh_devices(devices) + end + + # Enables DIAG option. + # + # @param config [Config] + # @return [Array] Configured devices. + def enable_diag(config) + devices = config.devices + .select(&:active?) + .select { |d| d.diag_action == Configs::Device::DiagAction::ENABLE } + .map { |d| find_device(d.channel) } + .compact + .reject(&:use_diag) + + return [] if devices.empty? + + logger.info("Enabling DIAG: #{devices}") + DiagOperation.new(devices, logger, true).run + refresh_devices(devices) end - # Disables the given list of DASDs + # Disables DIAG option. # - # Refresh callbacks are called at the end, see {#on_refresh}. + # @param config [Config] + # @return [Array] Configured devices. + def disable_diag(config) + devices = config.devices + .select(&:active?) + .select { |d| d.diag_action == Configs::Device::DiagAction::DISABLE } + .map { |d| find_device(d.channel) } + .compact + .select(&:use_diag) + + return [] if devices.empty? + + logger.info("Disabling DASDs: #{devices}") + DiagOperation.new(devices, logger, false).run + refresh_devices(devices) + end + + # Adds the given devices to the list of already formatted devices. # # @param devices [Array] - # @return [Boolean] true if all the given devices were disabled - def disable(devices) - refresh_after(devices) { DisableOperation.new(devices, logger).run } + def add_formatted_devices(devices) + @formatted_devices.concat(devices.map(&:id)).uniq! end - # Sets the value of the use_diag flag for the given list of DASDs + # Updates the attributes of the given DASDs with the current information read from the + # system. # - # Refresh callbacks are called at the end, see {#on_refresh}. + # @param devices [Array] Devices to update. + def refresh_devices(devices) + devices.each { |d| reader.update_info(d, extended: true) } + end + + # Sets the list of locked devices. # - # NOTE: see note about management of the use_diag flag at this class description. + # A device is considered as locked if it is active and not configured yet. # # @param devices [Array] - # @param diag [Boolean] value of the flag - # @return [Boolean] true if the flag is successfully set for all the given devices - def set_diag(devices, diag) - refresh_after(devices) { DiagOperation.new(devices, logger, diag).run } + def assign_locked_devices(devices) + config = ConfigImporter.new(config_json || {}).import + @locked_devices = devices + .reject(&:offline?) + .reject { |d| config.include_device?(d.id) } + .map(&:id) end - # Formats the given list of DASDs + # Removes the given devices from the list of locked devices. # - # @param devices [Array] - # @return [Boolean] true if all the given devices were successfully formatted - def format(devices, on_progress: nil, on_finish: nil) - progress = [] - finish = [proc { |_status| refresh(devices) }] - progress << on_progress if on_progress - finish << on_finish if on_finish - FormatOperation.new(devices, progress, finish).run + # @param config [Config] + def remove_locked_devices(config) + config.devices.each { |d| @locked_devices.delete(d.channel) } end - private + # Whether the given device is locked. + # + # @param device [Y2S390::Dasd] + # @return [Boolean] + def device_locked?(device) + @locked_devices.include?(device.id) + end - # @return [Logger] - attr_reader :logger + # Whether the given device was formatted. + # + # @param device [Y2S390::Dasd] + # @return [Boolean] + def device_formatted?(device) + @formatted_devices.include?(device.id) + end - # @return [Y2S390::DasdsReader] - def reader - @reader ||= Y2S390::DasdsReader.new + # Whether the device has to be formatted. + # + # @param device_config [Configs::Device] + # @return [Boolean] + def format_device?(device_config) + return false if device_config.format_action == Configs::Device::FormatAction::NONE + + device = find_device(device_config.channel) + return false unless device + + return true unless device.formatted? + + device_config.format_action == Configs::Device::FormatAction::FORMAT && + !device_formatted?(device) end - # Updates the attributes of the given DASDs with the current information read from the - # system + # Finds a DASD device with the given channel. # - # Refresh callbacks are called at the end, see {#on_refresh}. + # @param channel [String] + # @return [Y2S390::Dasd, nil] + def find_device(channel) + devices.find { |d| d.id == channel } + end + + # Returns the type from the device. # - # @param dasds [Array] devices to update - def refresh(dasds) - logger.info "Refreshing DASDs" - dasds.each { |dev| reader.update_info(dev, extended: true) } - logger.info "Refreshed DASDs #{dasds.inspect}" + # @see https://github.com/SUSE/s390-tools/blob/master/dasd_configure#L162 + # + # @param device [Y2S390::Dasd] + # @return [String] + def type_from(device) + # The DASD device type concatenating the cutype and devtype (i.e. 3990/E9 3390/0A) + device_type = device.device_type || "" + + cu_type, dev_type = device_type.split + return ECKD if cu_type.to_s.start_with?("3990", "2105", "2107", "1750", "9343") + return FBA if cu_type.to_s.start_with?("6310") - @on_refresh_callbacks.each do |callback| - callback.call(dasds) + if cu_type.start_with?("3880") + return ECKD if dev_type.start_with?("3390") + return FBA if dev_type.start_with?("3370") end + + device_type end - # Calls the given block and performs a refresh of the affected DASDs afterwards - # - # @note Returns the result of the block. - # @param devices [Array] - # @param block [Proc] - def refresh_after(devices, &block) - block.call.tap { refresh(devices) } + # @return [Y2S390::DasdsReader] + def reader + @reader ||= Y2S390::DasdsReader.new end end end diff --git a/service/lib/agama/storage/iscsi/manager.rb b/service/lib/agama/storage/iscsi/manager.rb index 20aa639309..3ea1bfc410 100644 --- a/service/lib/agama/storage/iscsi/manager.rb +++ b/service/lib/agama/storage/iscsi/manager.rb @@ -28,8 +28,6 @@ module Storage module ISCSI # Manager for iSCSI. class Manager - include Yast::I18n - PACKAGES = ["open-iscsi", "iscsiuio"].freeze # Config according to the JSON schema. @@ -129,6 +127,7 @@ def adapter # Sets the new config and keeps the previous one. # # @param config_json [Hash{Symbol=>Object}] Config according to the JSON schema. + # @return [Config] def assign_config(config_json) @previous_config = @config @config_json = config_json diff --git a/service/lib/agama/with_progress.rb b/service/lib/agama/with_progress.rb index dd130f9913..41868786af 100644 --- a/service/lib/agama/with_progress.rb +++ b/service/lib/agama/with_progress.rb @@ -51,11 +51,11 @@ def finish_progress end def progress_change - @on_progress_change_callbacks.each(&:call) + @on_progress_change_callbacks&.each(&:call) end def progress_finish - @on_progress_finish_callbacks.each(&:call) + @on_progress_finish_callbacks&.each(&:call) end # @param block [Proc] diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index dec4da714b..e26c372fa7 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Feb 6 15:25:36 UTC 2026 - José Iván López González + +- Provide new D-Bus API for DASD (gh#agama-project/agama#3110). + ------------------------------------------------------------------- Wed Feb 4 14:01:15 UTC 2026 - Ancor Gonzalez Sosa diff --git a/service/test/agama/dbus/storage/dasd_test.rb b/service/test/agama/dbus/storage/dasd_test.rb index ad202c0df7..df5df11957 100644 --- a/service/test/agama/dbus/storage/dasd_test.rb +++ b/service/test/agama/dbus/storage/dasd_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2026] SUSE LLC # # All Rights Reserved. # @@ -21,38 +21,215 @@ require_relative "../../../test_helper" require "agama/dbus/storage/dasd" +require "agama/storage/dasd/manager" +require "json" -describe Agama::DBus::Storage::Dasd do - subject { described_class.new(y2s390_dasd1, path, logger: logger) } +RSpec.describe Agama::DBus::Storage::DASD do + subject { described_class.new(manager) } - let(:y2s390_dasd1) { instance_double("Y2S390::Dasd") } - let(:y2s390_dasd2) do - instance_double( - "Y2S390::Dasd", id: "0.0.002", offline?: true, device_name: nil, formatted?: false, - diag_wanted: false, type: nil, access_type: nil, partition_info: "", status: :unknown, - device_type: "" - ) - end - let(:path) { "/org/opensuse/Agama/Storage1/dasds/1" } - let(:logger) { Logger.new($stdout, level: :warn) } + let(:manager) { instance_double(Agama::Storage::DASD::Manager) } before do - allow(subject).to receive(:dbus_properties_changed) + allow(manager).to receive(:on_format_change) + allow(manager).to receive(:on_format_finish) + end + + describe "#probe" do + before do + allow(subject).to receive(:recover_system).and_return("{}") + end + it "probes for devices" do + expect(subject).to receive(:ProgressChanged).ordered + expect(manager).to receive(:probe).ordered + expect(subject).to receive(:SystemChanged).with("{}").ordered + expect(subject).to receive(:ProgressFinished).ordered + subject.probe + end + end + + describe "#recover_system" do + before do + allow(manager).to receive(:devices).and_return([dasd]) + allow(manager).to receive(:device_type).with(dasd).and_return("ECKD") + end + + let(:dasd) do + double("Y2S390::Dasd", + id: "0.0.0100", + device_name: "dasda", + use_diag: false, + access_type: "diag", + partition_info: "1", + status: :active, + offline?: false, + formatted?: true) + end + + let(:system_json) do + { + devices: [ + { + channel: "0.0.0100", + deviceName: "dasda", + type: "ECKD", + diag: false, + accessType: "diag", + partitionInfo: "1", + status: "active", + active: true, + formatted: true + } + ] + } + end + + context "when already probed" do + before do + allow(manager).to receive(:probed?).and_return(true) + end + + it "returns system information as JSON" do + expected_value = JSON.pretty_generate(system_json) + expect(manager).to_not receive(:probe) + expect(subject.recover_system).to eq(expected_value) + end + end + + context "when not probed yet" do + before do + allow(manager).to receive(:probed?).and_return(false) + end + + it "probes first" do + expected_value = JSON.pretty_generate(system_json) + expect(manager).to receive(:probe) + expect(subject.recover_system).to eq(expected_value) + end + end + end + + describe "#recover_config" do + before do + allow(manager).to receive(:config_json).and_return(config_json) + end + + let(:config_json) do + { + devices: [ + { channel: "0.0.0100", active: true } + ] + } + end + + it "returns the config as JSON" do + expected_value = JSON.pretty_generate(config_json) + expect(subject.recover_config).to eq(expected_value) + end + + context "if there is not config yet" do + let(:config_json) { nil } + + it "returns 'null'" do + expect(subject.recover_config).to eq("null") + end + end + end + + describe "#configure" do + let(:config_json) do + { + devices: [ + { channel: "0.0.0100", active: true } + ] + } + end + + let(:serialized_config) { JSON.generate(config_json) } + + context "with no config" do + it "does nothing" do + expect(manager).not_to receive(:configure) + subject.configure("null") + end + end + + context "when config has not changed" do + before do + allow(manager).to receive(:configured?).with(config_json).and_return(true) + end + + it "does nothing" do + expect(manager).not_to receive(:configure) + subject.configure(serialized_config) + end + end + + context "when config has changed" do + before do + allow(manager).to receive(:configured?).with(config_json).and_return(false) + allow(subject).to receive(:recover_system).and_return("{}") + end + + it "performs configuration in a thread" do + expect(Thread).to receive(:new).and_yield + expect(subject).to receive(:SystemChanged) + expect(subject).to receive(:ProgressChanged) + expect(subject).to receive(:ProgressFinished) + expect(manager).to receive(:configure).with(config_json) + + subject.configure(serialized_config) + end + end + + context "when already configuring" do + let(:thread) { instance_double(Thread, alive?: true) } + + before do + allow(manager).to receive(:configured?).with(config_json).and_return(false) + subject.instance_variable_set(:@configuration_thread, thread) + end + + it "raises an error" do + expect { subject.configure(serialized_config) } + .to raise_error(RuntimeError, "Previous configuration is not finished yet") + end + end end - describe "#dasd=" do - it "sets the iSCSI node value" do - allow(subject).to receive(:dbus_properties_changed) + describe "format callbacks" do + let(:format_status) do + double( + "Y2S390::FormatStatus", + dasd: double("Y2S390::Dasd", id: "0.0.0100"), + cylinders: 100, + progress: 10, + done?: false + ) + end + + it "emits FormatChanged signal on manager format change" do + format_cb = nil + allow(manager).to receive(:on_format_change) { |&block| format_cb = block } + + summary_json = [ + { + channel: "0.0.0100", + totalCylinders: 100, + FormattedCylinders: 10, + finished: false + } + ] - expect(subject.dasd).to_not eq y2s390_dasd2 - subject.dasd = y2s390_dasd2 - expect(subject.dasd).to eq y2s390_dasd2 + expect(subject).to receive(:FormatChanged).with(JSON.pretty_generate(summary_json)) + format_cb.call([format_status]) end - it "emits properties changed signal" do - expect(subject).to receive(:dbus_properties_changed) + it "emits FormatFinished signal on manager format finish" do + finish_cb = nil + allow(manager).to receive(:on_format_finish) { |&block| finish_cb = block } - subject.dasd = y2s390_dasd2 + expect(subject).to receive(:FormatFinished).with("success") + finish_cb.call("success") end end end diff --git a/service/test/agama/dbus/storage/dasds_tree_test.rb b/service/test/agama/dbus/storage/dasds_tree_test.rb deleted file mode 100644 index d5b8753e93..0000000000 --- a/service/test/agama/dbus/storage/dasds_tree_test.rb +++ /dev/null @@ -1,245 +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/dbus/storage/dasds_tree" -require "agama/dbus/storage/dasd" -require "dbus" - -describe Agama::DBus::Storage::DasdsTree do - subject { described_class.new(service, logger: logger) } - - let(:service) { instance_double(::DBus::ObjectServer) } - let(:root_node) { instance_double(::DBus::Node) } - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - allow(service).to receive(:get_node).with(described_class::ROOT_PATH, anything) - .and_return(root_node) - - allow(root_node).to receive(:descendant_objects).and_return(dbus_nodes) - end - - describe "#find_paths" do - let(:dbus_nodes) { [dbus_dasd1, dbus_dasd2] } - - let(:dbus_dasd1) do - instance_double(::DBus::Object, path: "/org/opensuse/Agama/Storage1/dasds/1") - end - - let(:dbus_dasd2) do - instance_double(::DBus::Object, path: "/org/opensuse/Agama/Storage1/dasds/2") - end - - context "when all the given paths are already exported on D-Bus" do - let(:paths) do - ["/org/opensuse/Agama/Storage1/dasds/1"] - end - - it "returns the corresponding D-BUS DASDs" do - expect(subject.find_paths(paths)).to eq [dbus_dasd1] - end - end - - context "when some of the given paths are already exported on D-Bus" do - let(:paths) do - ["/org/opensuse/Agama/Storage1/dasds/1", "/org/opensuse/Agama/Storage1/dasds/3"] - end - - it "returns the subset of D-BUS DASDs that are exported" do - expect(subject.find_paths(paths)).to eq [dbus_dasd1] - end - end - - context "when none of the given paths are already exported on D-Bus" do - let(:paths) do - ["/org/opensuse/Agama/Storage1/dasds/3"] - end - - it "returns an empty array" do - expect(subject.find_paths(paths)).to eq [] - end - end - end - - describe "#find" do - let(:dbus_nodes) { [dbus_dasd1, dbus_dasd2, dbus_dasd3] } - - let(:dbus_dasd1) do - instance_double(Agama::DBus::Storage::Dasd, id: "0.0.001", formatted: false) - end - - let(:dbus_dasd2) do - instance_double(Agama::DBus::Storage::Dasd, id: "0.0.002", formatted: true) - end - - let(:dbus_dasd3) do - instance_double(Agama::DBus::Storage::Dasd, id: "0.0.003", formatted: true) - end - - it "returns the first DASD that matches the criteria" do - expect(subject.find(&:formatted)).to eq dbus_dasd2 - end - end - - describe "#populate" do - let(:dbus_nodes) { [dbus_dasd1, dbus_dasd2] } - - let(:dbus_dasd1) do - instance_double(Agama::DBus::Storage::Dasd, dasd: dasd1, id: dasd1.id) - end - - let(:dbus_dasd2) do - instance_double(Agama::DBus::Storage::Dasd, dasd: dasd2, id: dasd2.id) - end - - let(:dasd1) do - instance_double( - "Y2S390::Dasd", id: "0.0.001", offline?: false, device_name: "/dev/dasda", type: "ECKD", - formatted?: false, use_diag: false, access_type: "rw", partition_info: "/dev/dasda1" - ) - end - - let(:dasd2) do - instance_double( - "Y2S390::Dasd", id: "0.0.002", offline?: true, device_name: nil, type: nil, - formatted?: false, use_diag: false, access_type: nil, partition_info: "" - ) - end - - before do - allow(service).to receive(:export) - allow(service).to receive(:unexport) - end - - context "if a given DASD is not exported yet" do - let(:dasds) { [dasd3] } - - let(:dasd3) do - instance_double( - "Y2S390::Dasd", id: "0.0.003", offline?: true, device_name: nil, type: nil, - formatted?: false, use_diag: false, access_type: nil, partition_info: "" - ) - end - - it "exports a new D-Bus DASD object" do - expect(service).to receive(:export) do |dbus_dasd| - expect(dbus_dasd.path).to match(/#{described_class::ROOT_PATH}\/[0-9]+/) - end - - subject.populate(dasds) - end - end - - context "if a given DASD is already exported" do - # This DASD has the same id than dasd1, so it represents the same device - let(:dasds) { [dasd3] } - - let(:dasd3) do - instance_double( - "Y2S390::Dasd", id: "0.0.001", offline?: true, device_name: nil, type: nil, - formatted?: false, use_diag: false, access_type: nil, partition_info: "" - ) - end - - it "updates the D-Bus node" do - expect(dbus_dasd1).to receive(:dasd=).with(dasd3) - - subject.populate(dasds) - end - end - - context "if an exported D-Bus DASD is not included in the given list of devices" do - # dasd2 is part of the tree, but is not included in the list below - let(:dasds) { [dasd1] } - - before do - allow(dbus_dasd1).to receive(:dasd=) - end - - it "unexports the D-Bus DASD object" do - expect(service).to receive(:unexport).with(dbus_dasd2) - - subject.populate(dasds) - end - end - end - - describe "#update" do - let(:dbus_nodes) { [dbus_dasd1, dbus_dasd2] } - - let(:dbus_dasd1) do - instance_double(Agama::DBus::Storage::Dasd, dasd: dasd1, id: dasd1.id) - end - - let(:dbus_dasd2) do - instance_double(Agama::DBus::Storage::Dasd, dasd: dasd2, id: dasd2.id) - end - - let(:dasd1) do - instance_double( - "Y2S390::Dasd", id: "0.0.001", offline?: false, device_name: "/dev/dasda", type: "ECKD", - formatted?: false, use_diag: false, access_type: "rw", partition_info: "/dev/dasda1" - ) - end - - let(:dasd2) do - instance_double( - "Y2S390::Dasd", id: "0.0.002", offline?: true, device_name: nil, type: nil, - formatted?: false, use_diag: false, access_type: nil, partition_info: "" - ) - end - - context "if a given DASD is already exported" do - # This DASD has the same id than dasd1, so it represents the same device - let(:dasds) { [dasd3] } - - let(:dasd3) do - instance_double( - "Y2S390::Dasd", id: "0.0.001", offline?: true, device_name: nil, type: nil, - formatted?: false, use_diag: false, access_type: nil, partition_info: "" - ) - end - - it "updates the D-Bus node" do - expect(dbus_dasd1).to receive(:dasd=).with(dasd3) - - subject.update(dasds) - end - end - - context "if an exported D-Bus DASD is not included in the given list of devices" do - # dasd2 is part of the tree, but is not included in the list below - let(:dasds) { [dasd1] } - before { allow(dbus_dasd1).to receive(:dasd=) } - - it "does not unexport the ommitted DASDs" do - expect(service).to_not receive(:unexport) - subject.update(dasds) - end - - it "does not update the ommitted DASDs" do - expect(dbus_dasd2).to_not receive(:dasd=) - subject.update(dasds) - end - end - end -end diff --git a/service/test/agama/dbus/storage/jobs_tree_test.rb b/service/test/agama/dbus/storage/jobs_tree_test.rb deleted file mode 100644 index ab73f9aee2..0000000000 --- a/service/test/agama/dbus/storage/jobs_tree_test.rb +++ /dev/null @@ -1,72 +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/dbus/storage/jobs_tree" -require "agama/dbus/storage/dasds_tree" -require "dbus" - -describe Agama::DBus::Storage::JobsTree do - subject { described_class.new(service, logger: logger) } - - let(:service) { instance_double(::DBus::ObjectServer) } - let(:dasds_root_node) { instance_double(::DBus::Node) } - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - allow(service).to receive(:get_node) - .with(Agama::DBus::Storage::DasdsTree::ROOT_PATH, anything).and_return(dasds_root_node) - - allow(dasds_root_node).to receive(:descendant_objects).and_return(dasd_nodes) - end - - describe "#add_dasds_format" do - let(:dbus_dasd1) do - instance_double(Agama::DBus::Storage::Dasd, id: "0.0.001", path: "/path/dasd1") - end - let(:dbus_dasd2) do - instance_double(Agama::DBus::Storage::Dasd, id: "0.0.002", path: "/path/dasd2") - end - let(:dasd_nodes) { [dbus_dasd1, dbus_dasd2] } - let(:dasds_tree) { Agama::DBus::Storage::DasdsTree.new(service, logger: logger) } - - let(:dasd1) { double("Y2S390::Dasd", id: "0.0.001") } - let(:dasd2) { double("Y2S390::Dasd", id: "0.0.002") } - let(:initial_status) do - [ - double("FormatStatus", dasd: dasd1, cylinders: 1000, progress: 0, done?: false), - double("FormatStatus", dasd: dasd2, cylinders: 2000, progress: 0, done?: false) - ] - end - - it "exports a new D-Bus Job object" do - expect(service).to receive(:export) do |job| - expect(job.path).to match(/#{described_class::ROOT_PATH}\/[0-9]+/) - - expect(job.summary).to eq( - { "0.0.001" => [1000, 0, false], "0.0.002" => [2000, 0, false] } - ) - end - - subject.add_dasds_format(initial_status, dasds_tree) - end - end -end diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 295658d68a..6ec1c08f0b 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023-2025] SUSE LLC +# Copyright (c) [2023-2026] SUSE LLC # # All Rights Reserved. # @@ -28,8 +28,6 @@ require "agama/storage/proposal" require "agama/storage/proposal_settings" require "agama/storage/volume" -require "agama/storage/dasd/manager" -require "agama/dbus/storage/dasds_tree" require "y2storage" require "dbus" @@ -1304,173 +1302,10 @@ def parse(string) context "in an s390 system" do before do allow(Yast::Arch).to receive(:s390).and_return true - allow(Agama::Storage::DASD::Manager).to receive(:new).and_return(dasd_backend) - end - - let(:dasd_backend) do - instance_double(Agama::Storage::DASD::Manager, - on_probe: nil, - on_refresh: nil) - end - - it "includes interface for managing DASD devices" do - expect(subject.intfs.keys).to include("org.opensuse.Agama.Storage1.DASD.Manager") end it "includes interface for managing zFCP devices" do expect(subject.intfs.keys).to include("org.opensuse.Agama.Storage1.ZFCP.Manager") end - - describe "#dasd_enable" do - before do - allow(Agama::DBus::Storage::DasdsTree).to receive(:new).and_return(dasds_tree) - allow(dasds_tree).to receive(:find_paths).and_return [dbus_dasd1, dbus_dasd2] - end - - let(:dasds_tree) { instance_double(Agama::DBus::Storage::DasdsTree) } - - let(:dasd1) { instance_double("Y2S390::Dasd") } - let(:path1) { "/org/opensuse/Agama/Storage1/dasds/1" } - let(:dbus_dasd1) { Agama::DBus::Storage::Dasd.new(dasd1, path1) } - - let(:dasd2) { instance_double("Y2S390::Dasd") } - let(:path2) { "/org/opensuse/Agama/Storage1/dasds/2" } - let(:dbus_dasd2) { Agama::DBus::Storage::Dasd.new(dasd2, path2) } - - let(:path3) { "/org/opensuse/Agama/Storage1/dasds/3" } - - context "when some of the paths do not correspond to an exported DASD" do - let(:paths) { [path1, path2, path3] } - - it "does not try enable any DASD" do - expect(dasd_backend).to_not receive(:enable) - subject.dasd_enable(paths) - end - - it "returns 1" do - result = subject.dasd_enable(paths) - expect(result).to eq(1) - end - end - - context "when all the paths correspond to exported DASDs" do - let(:paths) { [path1, path2] } - - it "tries to enable all the DASDs" do - expect(dasd_backend).to receive(:enable).with([dasd1, dasd2]) - subject.dasd_enable(paths) - end - - context "and the action successes" do - before do - allow(dasd_backend).to receive(:enable).with([dasd1, dasd2]).and_return true - end - - it "returns 0" do - result = subject.dasd_enable(paths) - expect(result).to eq 0 - end - end - - context "and the action fails" do - before do - allow(dasd_backend).to receive(:enable).with([dasd1, dasd2]).and_return false - end - - it "returns 2" do - result = subject.dasd_enable(paths) - expect(result).to eq 2 - end - end - end - end - - describe "#dasd_format" do - before do - allow(Agama::DBus::Storage::DasdsTree).to receive(:new).and_return(dasds_tree) - allow(dasds_tree).to receive(:find_paths).and_return [dbus_dasd1, dbus_dasd2] - end - - let(:dasds_tree) { instance_double(Agama::DBus::Storage::DasdsTree) } - - let(:dasd1) { instance_double("Y2S390::Dasd") } - let(:path1) { "/org/opensuse/Agama/Storage1/dasds/1" } - let(:dbus_dasd1) { Agama::DBus::Storage::Dasd.new(dasd1, path1) } - - let(:dasd2) { instance_double("Y2S390::Dasd") } - let(:path2) { "/org/opensuse/Agama/Storage1/dasds/2" } - let(:dbus_dasd2) { Agama::DBus::Storage::Dasd.new(dasd2, path2) } - - let(:path3) { "/org/opensuse/Agama/Storage1/dasds/3" } - - context "when some of the paths do not correspond to an exported DASD" do - let(:paths) { [path1, path2, path3] } - - it "does not try to format" do - expect(dasd_backend).to_not receive(:format) - subject.dasd_format(paths) - end - - it "returns 1 as code and '/' as path" do - result = subject.dasd_format(paths) - expect(result).to eq [1, "/"] - end - end - - context "when all the paths correspond to exported DASDs" do - let(:paths) { [path1, path2] } - - it "tries to format all the DASDs" do - expect(dasd_backend).to receive(:format).with([dasd1, dasd2], any_args) - subject.dasd_format(paths) - end - - context "and the action successes" do - before do - allow(dasd_backend).to receive(:format).and_return initial_status - - allow(Agama::DBus::Storage::JobsTree).to receive(:new).and_return(jobs_tree) - allow(jobs_tree).to receive(:add_dasds_format).and_return format_job - end - - let(:initial_status) { [double("FormatStatus"), double("FormatStatus")] } - let(:jobs_tree) { instance_double(Agama::DBus::Storage::JobsTree) } - let(:format_job) do - instance_double(Agama::DBus::Storage::DasdsFormatJob, path: job_path) - end - let(:job_path) { "/some/path" } - - it "returns 0 and the path to the new Job object" do - result = subject.dasd_format(paths) - expect(result).to eq [0, job_path] - end - end - - context "and the action fails" do - before do - allow(dasd_backend).to receive(:format).and_return nil - end - - it "returns 2 as code and '/' as path" do - result = subject.dasd_format(paths) - expect(result).to eq [2, "/"] - end - end - end - end - end - - context "in a system that is not s390" do - before do - allow(Yast::Arch).to receive(:s390).and_return false - end - - it "does not respond to #dasd_enable" do - expect { subject.dasd_enable }.to raise_error NoMethodError - end - - it "does not respond to #dasd_format" do - expect { subject.dasd_format }.to raise_error NoMethodError - end end end diff --git a/service/test/agama/dbus/storage_service_test.rb b/service/test/agama/dbus/storage_service_test.rb index f2118015ac..479684c96e 100644 --- a/service/test/agama/dbus/storage_service_test.rb +++ b/service/test/agama/dbus/storage_service_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2025] SUSE LLC +# Copyright (c) [2025-2026] SUSE LLC # # All Rights Reserved. # @@ -20,8 +20,11 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" +require "agama/dbus/storage/dasd" require "agama/dbus/storage_service" +require "agama/storage/dasd/manager" require "agama/storage/iscsi/adapter" +require "yast" describe Agama::DBus::StorageService do subject(:service) { described_class.new(logger) } @@ -37,6 +40,16 @@ instance_double(Agama::DBus::Storage::Manager, path: "/org/opensuse/Agama/Storage1") end + let(:iscsi_obj) do + instance_double(Agama::DBus::Storage::ISCSI, path: "/org/opensuse/Agama/Storage1/ISCSI") + end + + let(:dasd_obj) do + instance_double(Agama::DBus::Storage::DASD, path: "/org/opensuse/Agama/Storage1/DASD") + end + + let(:dasd) { instance_double(Agama::Storage::DASD::Manager) } + before do allow(Agama::DBus::Bus).to receive(:current).and_return(bus) allow(bus).to receive(:request_service).with("org.opensuse.Agama.Storage1") @@ -46,6 +59,9 @@ .and_return(manager) allow(Agama::DBus::Storage::Manager).to receive(:new).with(manager, logger: logger) .and_return(manager_obj) + allow(Agama::DBus::Storage::ISCSI).to receive(:new).and_return(iscsi_obj) + allow(Agama::DBus::Storage::DASD).to receive(:new).and_return(dasd_obj) + allow(Agama::Storage::DASD::Manager).to receive(:new).and_return(dasd) allow_any_instance_of(Agama::Storage::ISCSI::Adapter).to receive(:activate) end @@ -66,6 +82,33 @@ expect(object_server).to receive(:export).with(manager_obj) service.export end + + it "exports the ISCSI manager" do + expect(object_server).to receive(:export).with(iscsi_obj) + service.export + end + + context "in a s390 system" do + before do + allow(Yast::Arch).to receive(:s390).and_return(true) + end + + it "exports the ISCSI manager" do + expect(object_server).to receive(:export).with(dasd_obj) + service.export + end + end + + context "with other arch" do + before do + allow(Yast::Arch).to receive(:s390).and_return(false) + end + + it "does not export the ISCSI manager" do + expect(object_server).to_not receive(:export).with(dasd_obj) + service.export + end + end end describe "#dispatch" do diff --git a/service/test/agama/storage/dasd/config_importer_test.rb b/service/test/agama/storage/dasd/config_importer_test.rb new file mode 100644 index 0000000000..505d5923ff --- /dev/null +++ b/service/test/agama/storage/dasd/config_importer_test.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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/storage/dasd/config" +require "agama/storage/dasd/config_importer" +require "agama/storage/dasd/config_importers/device" + +RSpec.describe Agama::Storage::DASD::ConfigImporter do + subject { described_class.new(config_json) } + + describe "#import" do + let(:config_json) { {} } + + it "generates a DASD config" do + config = subject.import + expect(config).to be_a(Agama::Storage::DASD::Config) + end + + context "with an empty JSON" do + let(:config_json) { {} } + + it "sets #devices to an empty array" do + config = subject.import + expect(config.devices).to eq([]) + end + end + + context "with a JSON specifying 'devices' as nil" do + let(:config_json) { { devices: nil } } + + it "sets #devices to an empty array" do + config = subject.import + expect(config.devices).to eq([]) + end + end + + context "with a JSON specifying 'devices'" do + let(:config_json) { { devices: devices_json } } + + context "with an empty list" do + let(:devices_json) { [] } + + it "sets #devices to an empty array" do + config = subject.import + expect(config.devices).to eq([]) + end + end + + context "with a list of devices" do + let(:devices_json) { [device1_json, device2_json] } + let(:device1_json) { { channel: "0.0.1234" } } + let(:device2_json) { { channel: "0.0.5678" } } + + it "sets #devices to the expected value" do + config = subject.import + expect(config.devices.size).to eq(2) + expect(config.devices).to all(be_a(Agama::Storage::DASD::Configs::Device)) + + device1, device2 = config.devices + expect(device1.channel).to eq("0.0.1234") + expect(device2.channel).to eq("0.0.5678") + end + + context "if a device does not specify 'state'" do + let(:device1_json) { { channel: "0.0.1234" } } + + it "sets #active? to the expected value" do + device = subject.import.devices.first + expect(device.active?).to eq(true) + end + end + + context "if a device specifies 'state = active'" do + let(:device1_json) { { channel: "0.0.1234", state: "active" } } + + it "sets #active? to the expected value" do + device = subject.import.devices.first + expect(device.active?).to eq(true) + end + end + + context "if a device specifies 'state: offline'" do + let(:device1_json) { { channel: "0.0.1234", state: "offline" } } + + it "sets #active? to the expected value" do + device = subject.import.devices.first + expect(device.active?).to eq(false) + end + end + + context "if a device does not specify 'format'" do + let(:device1_json) { { channel: "0.0.1234" } } + + it "sets #format_action to the expected value" do + device = subject.import.devices.first + expect(device.format_action) + .to eq(Agama::Storage::DASD::Configs::Device::FormatAction::FORMAT_IF_NEEDED) + end + end + + context "if a device specifies 'format: true'" do + let(:device1_json) { { channel: "0.0.1234", format: true } } + + it "sets #format_action to the expected value" do + device = subject.import.devices.first + expect(device.format_action) + .to eq(Agama::Storage::DASD::Configs::Device::FormatAction::FORMAT) + end + end + + context "if a device specifies 'format: false'" do + let(:device1_json) { { channel: "0.0.1234", format: false } } + + it "sets #format_action to the expected value" do + device = subject.import.devices.first + expect(device.format_action) + .to eq(Agama::Storage::DASD::Configs::Device::FormatAction::NONE) + end + end + + context "if a device does not specify 'diag'" do + let(:device1_json) { { channel: "0.0.1234" } } + + it "sets #diag_action to the expected value" do + device = subject.import.devices.first + expect(device.diag_action) + .to eq(Agama::Storage::DASD::Configs::Device::DiagAction::NONE) + end + end + + context "if a device specifies 'diag: true'" do + let(:device1_json) { { channel: "0.0.1234", diag: true } } + + it "sets #diag_action to the expected value" do + device = subject.import.devices.first + expect(device.diag_action) + .to eq(Agama::Storage::DASD::Configs::Device::DiagAction::ENABLE) + end + end + + context "if a device specifies 'diag: false'" do + let(:device1_json) { { channel: "0.0.1234", diag: false } } + + it "sets #diag_action to the expected value" do + device = subject.import.devices.first + expect(device.diag_action) + .to eq(Agama::Storage::DASD::Configs::Device::DiagAction::DISABLE) + end + end + end + end + end +end diff --git a/service/test/agama/storage/dasd/manager_test.rb b/service/test/agama/storage/dasd/manager_test.rb index 26e3a96654..ed041be517 100644 --- a/service/test/agama/storage/dasd/manager_test.rb +++ b/service/test/agama/storage/dasd/manager_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2026] SUSE LLC # # All Rights Reserved. # @@ -21,6 +21,13 @@ require_relative "../../../test_helper" require "agama/storage/dasd/manager" +require "agama/storage/dasd/config" +require "agama/storage/dasd/configs/device" +require "agama/storage/dasd/config_importer" +require "agama/storage/dasd/enable_operation" +require "agama/storage/dasd/disable_operation" +require "agama/storage/dasd/format_operation" +require "agama/storage/dasd/diag_operation" require "forwardable" # Define some very basic (almost empty) Y2S390 classes to support the tests, @@ -52,295 +59,596 @@ def all describe Agama::Storage::DASD::Manager do subject { described_class.new(logger: logger) } - let(:logger) { Logger.new($stdout, level: :warn) } - let(:reader) { double(Y2S390::DasdsReader) } - before { allow(Y2S390::DasdsReader).to receive(:new).and_return(reader) } + let(:logger) { instance_double(Logger, info: nil) } - let(:dasds) { Y2S390::DasdsCollection.new([dasd1, dasd2, dasd3]) } - let(:dasd1) { double("Y2S390::Dasd", id: "0.0.001", use_diag: true, diag_wanted: true) } - let(:dasd2) { double("Y2S390::Dasd", id: "0.0.002", use_diag: false, diag_wanted: false) } - let(:dasd3) { double("Y2S390::Dasd", id: "0.0.003", use_diag: false, diag_wanted: false) } + before do + allow(Y2S390::DasdsReader).to receive(:new).and_return(reader) + allow(reader).to receive(:list).with(force_probing: true).and_return(devices_collection) + end + + let(:reader) { double("Y2S390::DasdsReader") } + let(:devices_collection) { Y2S390::DasdsCollection.new(devices) } + let(:devices) { [] } describe "#probe" do - before do - allow(reader).to receive(:list).and_return dasds + let(:dasd1) do + double("Y2S390::Dasd", id: "0.0.0100", use_diag: true, status: "online", offline?: false) + end + let(:dasd2) do + double("Y2S390::Dasd", id: "0.0.0101", use_diag: false, status: "offline", offline?: true) + end + + let(:devices) { [dasd1, dasd2] } + + before do allow(dasd1).to receive(:diag_wanted=) allow(dasd2).to receive(:diag_wanted=) - allow(dasd3).to receive(:diag_wanted=) end - it "reads the list of DASDs from the system" do - expect(reader).to receive(:list).with(force_probing: true).and_return dasds + it "sets probed? to true" do subject.probe - expect(subject.devices).to eq dasds + expect(subject).to be_probed end - it "ensures initial consistency of #use_diag and #diag_wanted" do - expect(dasd1).to receive(:diag_wanted=).with(dasd1.use_diag) - expect(dasd2).to receive(:diag_wanted=).with(dasd2.use_diag) - expect(dasd3).to receive(:diag_wanted=).with(dasd3.use_diag) + it "reads devices from the system" do subject.probe + expect(subject.devices).to eq(devices_collection) end - let(:callback) { proc {} } + it "ensures initial consistency of #use_diag and #diag_wanted" do + expect(dasd1).to receive(:diag_wanted=).with(true) + expect(dasd2).to receive(:diag_wanted=).with(false) + subject.probe + end - it "runs the probe callbacks" do - subject.on_probe(&callback) - expect(callback).to receive(:call).with(dasds) + it "locks devices that are not offline" do subject.probe + expect(subject.send(:device_locked?, dasd1)).to be(true) + expect(subject.send(:device_locked?, dasd2)).to be(false) + end + + context "when a configuration is already present" do + let(:config_json) { { devices: [{ channel: "0.0.0100" }] } } + + before do + allow(subject).to receive(:config_json).and_return(config_json) + end + + context "if a device is part of the configuration" do + before do + allow(dasd1).to receive(:offline?).and_return(false) + allow(dasd2).to receive(:offline?).and_return(false) + end + + it "does not lock active devices that are part of the configuration" do + subject.probe + expect(subject.send(:device_locked?, dasd1)).to be(false) + expect(subject.send(:device_locked?, dasd2)).to be(true) + end + end end end - describe "#enable" do - let(:callback) { proc {} } - let(:cheetah_error) { Cheetah::ExecutionFailed.new([], "", nil, nil) } + describe "#configured?" do + let(:config_json) { { devices: [] } } - before do - allow(reader).to receive(:update_info) + it "returns false if not configured yet" do + expect(subject.configured?(config_json)).to be(false) end - it "calls dasd_configure for each given DASD with the 'online' argument set to 1" do - expect(Yast::Execute).to receive(:locally!) do |cmd| - expect(cmd.join(" ")).to match(/dasd_configure #{dasd1.id} 1/) + context "when configured" do + before do + subject.instance_variable_set(:@configured, true) + subject.instance_variable_set(:@config_json, config_json) end - expect(Yast::Execute).to receive(:locally!) do |cmd| - expect(cmd.join(" ")).to match(/dasd_configure #{dasd2.id} 1/) + + it "returns true for the same config" do + expect(subject.configured?(config_json)).to be(true) end - expect(Yast::Execute).to receive(:locally!) do |cmd| - expect(cmd.join(" ")).to match(/dasd_configure #{dasd3.id} 1/) + + it "returns false if the system was probed again" do + subject.probe + expect(subject.configured?(config_json)).to be(false) end - subject.enable(dasds) + it "returns false for a different config" do + expect(subject.configured?({})).to be(false) + end end + end - it "returns true if none of the dasd_configure invocations fails" do - expect(Yast::Execute).to receive(:locally!).exactly(dasds.size).times - expect(subject.enable(dasds)).to eq true + describe "#configure" do + let(:dasd1) do + double("Y2S390::Dasd", + id: "0.0.0001", + status: :offline, + formatted?: false, + use_diag: false, + active?: false, + offline?: true) end - it "returns false if any of the dasd_configure invocations fails" do - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd1.id), any_args) - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd2.id), any_args) - .and_raise cheetah_error - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd3.id), any_args) - expect(subject.enable(dasds)).to eq false + let(:dasd2) do + double("Y2S390::Dasd", + id: "0.0.0002", + status: :offline, + formatted?: false, + use_diag: false, + active?: false, + offline?: true) end - it "updates the information of the DASDs and runs the refresh callbacks" do - allow(Yast::Execute).to receive(:locally!) - subject.on_refresh(&callback) + let(:dasd3) do + double("Y2S390::Dasd", + id: "0.0.0003", + status: :offline, + formatted?: false, + use_diag: false, + active?: false, + offline?: true) - expect(reader).to receive(:update_info).with(dasd1, extended: true) - expect(reader).to receive(:update_info).with(dasd2, extended: true) - expect(reader).to receive(:update_info).with(dasd3, extended: true) - expect(callback).to receive(:call).with(dasds) + end - subject.enable(dasds) + let(:dasd4) do + double("Y2S390::Dasd", + id: "0.0.0004", + status: :offline, + formatted?: false, + use_diag: false, + active?: false, + offline?: true) end - end - describe "#disable" do - let(:callback) { proc {} } - let(:cheetah_error) { Cheetah::ExecutionFailed.new([], "", nil, nil) } + let(:devices) { [dasd1, dasd2, dasd3, dasd4] } - before { allow(reader).to receive(:update_info) } + let(:config_json) { {} } - it "calls dasd_configure for each given DASD with the 'online' argument set to 0" do - expect(Yast::Execute).to receive(:locally!).with(["dasd_configure", dasd1.id, "0"], any_args) - expect(Yast::Execute).to receive(:locally!).with(["dasd_configure", dasd2.id, "0"], any_args) - expect(Yast::Execute).to receive(:locally!).with(["dasd_configure", dasd3.id, "0"], any_args) + before do + devices.each { |d| allow(d).to receive(:diag_wanted=) } + allow(reader).to receive(:update_info) + allow(subject).to receive(:device_locked?).and_return(false) - subject.disable(dasds) + # Mock all operations + allow(Agama::Storage::DASD::EnableOperation).to receive(:new).and_return(enable_operation) + allow(Agama::Storage::DASD::DisableOperation).to receive(:new).and_return(disable_operation) + allow(Agama::Storage::DASD::FormatOperation).to receive(:new).and_return(format_operation) + allow(Agama::Storage::DASD::DiagOperation).to receive(:new).and_return(diag_operation) end - it "returns true if none of the dasd_configure invocations fails" do - expect(Yast::Execute).to receive(:locally!).exactly(dasds.size).times - expect(subject.disable(dasds)).to eq true + let(:enable_operation) { instance_double(Agama::Storage::DASD::EnableOperation, run: nil) } + let(:disable_operation) { instance_double(Agama::Storage::DASD::DisableOperation, run: nil) } + let(:format_operation) { instance_double(Agama::Storage::DASD::FormatOperation, run: nil) } + let(:diag_operation) { instance_double(Agama::Storage::DASD::DiagOperation, run: nil) } + + context "if not probed yet" do + before do + allow(subject).to receive(:probed?).and_return(false) + end + + it "calls probe" do + expect(subject).to receive(:probe) + subject.configure(config_json) + end end - it "returns false if any of the dasd_configure invocations fails" do - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd1.id), any_args) - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd2.id), any_args) - .and_raise cheetah_error - expect(Yast::Execute).to receive(:locally!).with(array_including(dasd3.id), any_args) - expect(subject.disable(dasds)).to eq false + context "if already probed" do + before do + allow(subject).to receive(:probed?).and_return(true) + end + + it "does not call probe" do + expect(subject).to_not receive(:probe) + subject.configure(config_json) + end end - it "updates the information of the DASDs and runs the refresh callbacks" do - allow(Yast::Execute).to receive(:locally!) - subject.on_refresh(&callback) + context "if the config activates devices" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active" + }, + { + channel: "0.0.0002", + state: "active" + }, + { + channel: "0.0.0003", + state: "active" + } + ] + } + end - expect(reader).to receive(:update_info).with(dasd1, extended: true) - expect(reader).to receive(:update_info).with(dasd2, extended: true) - expect(reader).to receive(:update_info).with(dasd3, extended: true) - expect(callback).to receive(:call).with(dasds) + before do + allow(dasd1).to receive(:active?).and_return(false) + allow(dasd2).to receive(:active?).and_return(false) + allow(dasd3).to receive(:active?).and_return(true) + allow(dasd4).to receive(:active?).and_return(false) + end - subject.disable(dasds) + it "activates the device if not active yet" do + expect(Agama::Storage::DASD::EnableOperation).to receive(:new) do |devices, _| + expect(devices).to contain_exactly(dasd1, dasd2) + enable_operation + end + subject.configure(config_json) + end end - end - describe "#format" do - # Mocks cannot traverse threads, so we need some real classes here to somehow emulate the - # behavior of Y2S30 - module Y2S390 - # See above - class FormatStatus - attr_accessor :progress, :cylinders, :dasd - - def initialize(dasd, cylinders) - @dasd = dasd - @cylinders = cylinders - @progress = 0 - end + context "if the config deactivates devices" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "offline" + }, + { + channel: "0.0.0002", + state: "active" + }, + { + channel: "0.0.0003", + state: "offline" + }, + { + channel: "0.0.0004", + state: "offline" + } + ] + } + end - def done? - @progress >= @cylinders + before do + allow(dasd1).to receive(:active?).and_return(true) + allow(dasd2).to receive(:active?).and_return(false) + allow(dasd3).to receive(:active?).and_return(true) + allow(dasd4).to receive(:active?).and_return(false) + end + + it "deactivates the device if active" do + expect(Agama::Storage::DASD::DisableOperation).to receive(:new) do |devices, _| + expect(devices).to contain_exactly(dasd1, dasd3) + disable_operation end + subject.configure(config_json) end + end - # See above - class FormatProcess - attr_reader :summary, :status - alias_method :updated, :summary + context "if the config does not include a device" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0002", + state: "active" + } + ] + } + end - def initialize(dasds, max: 3, status: 0) - @dasds = dasds - @summary = {} - @counter = 0 - @max = max - @status = status - end + before do + allow(dasd1).to receive(:active?).and_return(true) + allow(dasd2).to receive(:active?).and_return(false) + allow(dasd3).to receive(:active?).and_return(true) + allow(dasd4).to receive(:active?).and_return(false) + end - def running? - @counter < @max + it "deactivates the unlisted device if active" do + expect(Agama::Storage::DASD::DisableOperation).to receive(:new) do |devices, _| + expect(devices).to contain_exactly(dasd1, dasd3) + disable_operation end + subject.configure(config_json) + end - def initialize_summary - @dasds.each_with_index { |d, i| @summary[i] = FormatStatus.new(d, @max) } + context "and some unlisted device is locked" do + before do + allow(subject).to receive(:device_locked?).with(dasd1).and_return(true) end - def update_summary - @counter += 1 - @dasds.each.with_index do |_d, index| - @summary[index].progress = @counter + it "does not deactivate the locked devices" do + expect(Agama::Storage::DASD::DisableOperation).to receive(:new) do |devices, _| + expect(devices).to contain_exactly(dasd3) + disable_operation end + subject.configure(config_json) end end end - let(:fmt_process) { Y2S390::FormatProcess.new(dasds, max: steps - 1, status: exit_code) } - let(:progress_callback) { proc {} } - let(:finish_callback) { proc {} } - let(:steps) { 3 } - let(:exit_code) { 5 } - - before do - allow(reader).to receive(:update_info) - allow(Y2S390::FormatProcess).to receive(:new).and_return fmt_process - allow(fmt_process).to receive(:start) - end + context "if the config formats a device" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active", + format: true + }, + { + channel: "0.0.0002", + state: "active", + format: true + }, + { + channel: "0.0.0003", + state: "offline", + format: true + }, + { + channel: "0.0.0004", + state: "active", + format: false + } + ] + } + end - it "starts a FormatProcess with the given dasds" do - expect(Y2S390::FormatProcess).to receive(:new).with(dasds) - expect(fmt_process).to receive(:start) - subject.format(dasds) - end + before do + allow(dasd1).to receive(:formatted?).and_return(false) + allow(dasd2).to receive(:formatted?).and_return(true) + allow(dasd3).to receive(:formatted?).and_return(false) + allow(dasd4).to receive(:formatted?).and_return(true) + end - it "returns the initial status of the process as an array of FormatStatus objects" do - result = subject.format(dasds) - expect(result).to all(be_a(Y2S390::FormatStatus)) - expect(result.map(&:dasd)).to contain_exactly(*dasds.all) - expect(result.map(&:progress)).to eq [0, 0, 0] - end + it "formats the device if configured as active" do + expect(Agama::Storage::DASD::FormatOperation).to receive(:new) do |devices, _, _| + expect(devices).to contain_exactly(dasd1, dasd2) + format_operation + end + subject.configure(config_json) + end - it "executes the progress and finish callbacks" do - expect(progress_callback).to receive(:call).exactly(steps).times - expect(finish_callback).to receive(:call).once.with(exit_code) - subject.format(dasds, on_progress: progress_callback, on_finish: finish_callback) - sleep(5) # give background threads a chance to run - end + context "if the device was already formatted" do + before do + allow(subject).to receive(:device_formatted?).and_call_original + allow(subject).to receive(:device_formatted?).with(dasd1).and_return(true) + allow(dasd1).to receive(:formatted?).and_return(true) + end - it "updates the information of the DASDs when formatting finishes" do - expect(reader).to receive(:update_info).with(dasd1, extended: true) - expect(reader).to receive(:update_info).with(dasd2, extended: true) - expect(reader).to receive(:update_info).with(dasd3, extended: true) - subject.format(dasds) - sleep(5) # give background threads a chance to run + it "does not format the device again" do + expect(Agama::Storage::DASD::FormatOperation).to receive(:new) do |devices, _, _| + expect(devices).to contain_exactly(dasd2) + format_operation + end + subject.configure(config_json) + end + end end - context "when the process returns false to the first call to #running?" do - let(:steps) { 0 } + context "if the config omits format" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active" + }, + { + channel: "0.0.0002", + state: "active" + }, + { + channel: "0.0.0003", + state: "offline" + }, + { + channel: "0.0.0004", + state: "active" + } + ] + } + end - it "returns nil" do - expect(subject.format(dasds)).to be_nil + before do + allow(dasd1).to receive(:formatted?).and_return(true) + allow(dasd2).to receive(:formatted?).and_return(false) + allow(dasd3).to receive(:formatted?).and_return(false) + allow(dasd4).to receive(:formatted?).and_return(false) end - it "does not execute any of the callbacks" do - expect(progress_callback).to_not receive(:call) - expect(finish_callback).to_not receive(:call) - subject.format(dasds, on_progress: progress_callback, on_finish: finish_callback) - sleep(5) # give background threads a chance to run + it "formats the device if configured as active and not formatted yet" do + expect(Agama::Storage::DASD::FormatOperation).to receive(:new) do |devices, _, _| + expect(devices).to contain_exactly(dasd2, dasd4) + format_operation + end + subject.configure(config_json) end end - end - describe "#set_diag" do - let(:callback) { proc {} } + context "if the config enables DIAG" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active", + diag: true + }, + { + channel: "0.0.0002", + state: "active", + diag: true + }, + { + channel: "0.0.0003", + state: "offline", + diag: true + }, + { + channel: "0.0.0004", + state: "active" + } + ] + } + end - let(:dasd1) do - double("Y2S390::Dasd", id: "0.0.001", offline?: false, use_diag: true, diag_wanted: wanted) - end - let(:dasd2) do - double("Y2S390::Dasd", id: "0.0.002", offline?: false, use_diag: false, diag_wanted: wanted) - end - let(:dasd3) do - double("Y2S390::Dasd", id: "0.0.003", offline?: true, use_diag: false, diag_wanted: wanted) + before do + allow(dasd1).to receive(:use_diag).and_return(true) + allow(dasd2).to receive(:use_diag).and_return(false) + allow(dasd3).to receive(:use_diag).and_return(false) + allow(dasd4).to receive(:use_diag).and_return(true) + end + + # TODO + it "enables DIAG if configured as active and not enabled yet" do + expect(Agama::Storage::DASD::DiagOperation) + .to receive(:new).with([dasd2], logger, true).and_return(diag_operation) + subject.configure(config_json) + end end - let(:wanted) { true } + context "if the config disables DIAG" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active", + diag: false + }, + { + channel: "0.0.0002", + state: "active", + diag: false + }, + { + channel: "0.0.0003", + state: "offline", + diag: false + }, + { + channel: "0.0.0004", + state: "active" + } + ] + } + end - before do - allow(reader).to receive(:update_info) - allow(dasd1).to receive(:diag_wanted=) - allow(dasd2).to receive(:diag_wanted=) - allow(dasd3).to receive(:diag_wanted=) - allow(Yast::Execute).to receive(:locally!).with(array_including(dasd2.id), any_args) + before do + allow(dasd1).to receive(:use_diag).and_return(false) + allow(dasd2).to receive(:use_diag).and_return(true) + allow(dasd3).to receive(:use_diag).and_return(true) + allow(dasd4).to receive(:use_diag).and_return(false) + end + + it "disables DIAG if configured as active and not disabled yet" do + expect(Agama::Storage::DASD::DiagOperation) + .to receive(:new).with([dasd2], logger, false).and_return(diag_operation) + subject.configure(config_json) + end end - context "for DASDs that are initially enabled" do - it "disables the devices and enables it again if the value of use_diag has changed" do - expect(Yast::Execute).to receive(:locally!) - .with(["dasd_configure", dasd2.id, "0"], any_args).ordered - expect(Yast::Execute).to receive(:locally!) - .with(["dasd_configure", dasd2.id, "1", "1"], any_args).ordered + context "if a device was modified" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0001", + state: "active", + format: false + }, + { + channel: "0.0.0002", + state: "active", + format: false, + diag: true + }, + { + channel: "0.0.0003", + state: "active", + format: true + }, + { + channel: "0.0.0004", + state: "active", + format: false + } + ] + } + end - subject.set_diag(dasds, wanted) + before do + allow(dasd1).to receive(:active?).and_return(false) + allow(dasd2).to receive(:active?).and_return(true) + allow(dasd2).to receive(:use_diag).and_return(false) + allow(dasd3).to receive(:active?).and_return(true) + allow(dasd4).to receive(:active?).and_return(true) + allow(subject).to receive(:device_locked?).and_call_original end - it "does nothing if the value of use_diag is the same than the current one" do - expect(Yast::Execute).to_not receive(:locally!).with(array_including(dasd1.id), any_args) - subject.set_diag(dasds, wanted) + it "refreshes all modified devices" do + expect(reader).to receive(:update_info).with(dasd1, extended: true) + expect(reader).to receive(:update_info).with(dasd2, extended: true) + expect(reader).to receive(:update_info).with(dasd3, extended: true) + expect(reader).to_not receive(:update_info).with(dasd4, extended: true) + subject.configure(config_json) end end - context "for DASDs that are initially disabled" do - it "does not enable the device" do - expect(Yast::Execute).to_not receive(:locally!).with(array_including(dasd3.id), any_args) - subject.set_diag(dasds, wanted) + context "if a device is configured" do + let(:config_json) do + { + devices: [ + { + channel: "0.0.0002", + state: "active", + format: false + }, + { + channel: "0.0.0003", + state: "active", + format: false + } + ] + } + end + + before do + allow(dasd1).to receive(:offline?).and_return(true) + allow(dasd2).to receive(:offline?).and_return(false) + allow(dasd2).to receive(:active?).and_return(true) + allow(dasd3).to receive(:offline?).and_return(false) + allow(dasd4).to receive(:offline?).and_return(false) + allow(subject).to receive(:device_locked?).and_call_original + end + + it "removes configured devices from locked devices" do + subject.configure(config_json) + expect(subject.send(:device_locked?, dasd1)).to eq(false) + expect(subject.send(:device_locked?, dasd2)).to eq(false) + expect(subject.send(:device_locked?, dasd3)).to eq(false) + expect(subject.send(:device_locked?, dasd4)).to eq(true) end end + end - it "updates the information of the DASDs and runs the refresh callbacks" do - allow(Yast::Execute).to receive(:locally!) - subject.on_refresh(&callback) + describe "#device_type" do + let(:dasd) { double("Y2S390::Dasd", id: "0.0.0100") } - expect(reader).to receive(:update_info).with(dasd1, extended: true) - expect(reader).to receive(:update_info).with(dasd2, extended: true) - expect(reader).to receive(:update_info).with(dasd3, extended: true) - expect(callback).to receive(:call).with(dasds) + it "returns type if already known" do + allow(dasd).to receive(:type).and_return("FOO") + expect(subject.device_type(dasd)).to eq("FOO") + end - subject.set_diag(dasds, wanted) + { + "3990/E9 3390/0A" => "ECKD", + "2105/E20 3390/03" => "ECKD", + "6310/C01" => "FBA", + "3880/01 3370/02" => "FBA", + "3880/03 3390/02" => "ECKD", + "BEEF/01" => "BEEF/01" + }.each do |device_type, expected| + it "returns '#{expected}' for device_type '#{device_type}'" do + allow(dasd).to receive(:type).and_return(nil) + allow(dasd).to receive(:device_type).and_return(device_type) + expect(subject.device_type(dasd)).to eq(expected) + end end end end