diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 587b4e268b..ac277b3c22 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -51,27 +51,32 @@ def initialize(backend, logger: nil) textdomain "agama" super(PATH, logger: logger) @backend = backend + @system_json = generate_system_json + @config_json = generate_config_json + @config_model_json = generate_config_model_json + @proposal_json = generate_proposal_json + @issues_json = generate_issues_json register_progress_callbacks add_s390_interfaces if Yast::Arch.s390 end dbus_interface "org.opensuse.Agama.Storage1" do + dbus_reader_attr_accessor :system_json, "s", dbus_name: "System" + dbus_reader_attr_accessor :config_json, "s", dbus_name: "Config" + dbus_reader_attr_accessor :config_model_json, "s", dbus_name: "ConfigModel" + dbus_reader_attr_accessor :proposal_json, "s", dbus_name: "Proposal" + dbus_reader_attr_accessor :issues_json, "s", dbus_name: "Issues" dbus_method(:Activate) { activate } dbus_method(:Probe) { probe } dbus_method(:Install) { install } dbus_method(:Finish) { finish } dbus_method(:Umount) { umount } dbus_method(:SetLocale, "in locale:s") { |locale| backend.configure_locale(locale) } - dbus_method(:GetSystem, "out system:s") { recover_system } - dbus_method(:GetConfig, "out config:s") { recover_config } dbus_method(:SetConfig, "in product:s, in config:s") { |p, c| configure(p, c) } dbus_method( :GetConfigFromModel, "in model:s, out config:s" ) { |m| convert_config_model(m) } - dbus_method(:GetConfigModel, "out model:s") { recover_config_model } dbus_method(:SolveConfigModel, "in model:s, out result:s") { |m| solve_config_model(m) } - dbus_method(:GetProposal, "out proposal:s") { recover_proposal } - dbus_method(:GetIssues, "out issues:s") { recover_issues } dbus_signal(:SystemChanged, "system:s") dbus_signal(:ProposalChanged, "proposal:s") dbus_signal(:ProgressChanged, "progress:s") @@ -87,8 +92,7 @@ def activate backend.activate next_progress_step(PROBING_STEP) - backend.probe - emit_system_changed + perform_probe next_progress_step(CONFIGURING_STEP) configure_with_current @@ -104,8 +108,7 @@ def probe backend.activate unless backend.activated? next_progress_step(PROBING_STEP) - backend.probe - emit_system_changed + perform_probe next_progress_step(CONFIGURING_STEP) configure_with_current @@ -113,69 +116,7 @@ def probe finish_progress end - # Implementation for the API method #Install. - def install - start_progress(3, _("Preparing bootloader proposal")) - backend.bootloader.configure - - next_progress_step(_("Preparing the storage devices")) - backend.install - - next_progress_step(_("Writing bootloader sysconfig")) - backend.bootloader.install - - finish_progress - end - - # Implementation for the API method #Finish. - def finish - start_progress(1, _("Finishing installation")) - backend.finish - finish_progress - end - - def umount - start_progress(1, _("Unmounting devices")) - backend.umount - finish_progress - end - - # NOTE: memoization of the values? - # @return [String] - def recover_system - return nil.to_json unless backend.probed? - - json = { - devices: json_devices(:probed), - availableDrives: available_drives, - availableMdRaids: available_md_raids, - candidateDrives: candidate_drives, - candidateMdRaids: candidate_md_raids, - issues: system_issues, - productMountPoints: product_mount_points, - encryptionMethods: encryption_methods, - volumeTemplates: volume_templates - } - JSON.pretty_generate(json) - end - - # Gets and serializes the storage config used for calculating the current proposal. - # - # @return [String] - def recover_config - json = proposal.storage_json - JSON.pretty_generate(json) - end - - # Gets and serializes the storage config model. - # - # @return [String] - def recover_config_model - json = proposal.model_json - JSON.pretty_generate(json) - end - - # Applies the given serialized config according to the JSON schema. + # Configures storage. # # The JSON schema supports two different variants: # { "storage": ... } or { "legacyAutoyastStorage": ... }. @@ -192,30 +133,16 @@ def configure(serialized_product_config, serialized_config) return if backend.configured?(product_config_json, config_json) logger.info("Configuring storage") - - system_changed = false product_config = Agama::Config.new(product_config_json) - - if backend.product_config != product_config - system_changed = true - backend.update_product_config(product_config) - end + backend.update_product_config(product_config) if backend.product_config != product_config start_progress(3, ACTIVATING_STEP) - if !backend.activated? - backend.activate - # Potential change in system - issues - system_changed = true - end + backend.activate unless backend.activated? next_progress_step(PROBING_STEP) - if !backend.probed? - backend.probe - # Potential change in system - devices, issues, candidateX, availableX - system_changed = true - end + backend.probe unless backend.probed? - emit_system_changed if system_changed + update_system_json next_progress_step(CONFIGURING_STEP) calculate_proposal(config_json) @@ -223,12 +150,20 @@ def configure(serialized_product_config, serialized_config) finish_progress end - # Converts the given serialized config model according to the JSON schema. + # Converts the given serialized config model to a config. # # @param serialized_model [String] Serialized config model. # @return [String] Serialized config according to JSON schema. def convert_config_model(serialized_model) - config_json = config_from_model(serialized_model) + model_json = JSON.parse(serialized_model, symbolize_names: true) + + config = Agama::Storage::ConfigConversions::FromModel.new( + model_json, + product_config: product_config, + storage_system: proposal.storage_system + ).convert + + config_json = { storage: Agama::Storage::ConfigConversions::ToJSON.new(config).convert } JSON.pretty_generate(config_json) end @@ -242,24 +177,32 @@ def solve_config_model(serialized_model) JSON.pretty_generate(solved_model_json) end - # NOTE: memoization of the values? - # @return [String] - def recover_proposal - return nil.to_json unless backend.proposal.success? + # Implementation for the API method #Install. + def install + start_progress(3, _("Preparing bootloader proposal")) + backend.bootloader.configure - json = { - devices: json_devices(:staging), - actions: actions - } - JSON.pretty_generate(json) + next_progress_step(_("Preparing the storage devices")) + backend.install + + next_progress_step(_("Writing bootloader sysconfig")) + backend.bootloader.install + + finish_progress end - # Gets and serializes the list of issues. - # - # @return [String] - def recover_issues - json = backend.issues.map { |i| json_issue(i) } - JSON.pretty_generate(json) + # Implementation for the API method #Finish. + def finish + start_progress(1, _("Finishing installation")) + backend.finish + finish_progress + end + + # Implementation for the API method #Umount. + def umount + start_progress(1, _("Unmounting devices")) + backend.umount + finish_progress end dbus_interface "org.opensuse.Agama.Storage1.Bootloader" do @@ -312,6 +255,14 @@ def register_progress_callbacks on_progress_finish { self.ProgressFinished } end + # Probes storage and updates the associated info. + # + # @see #update_system_info + def perform_probe + backend.probe + update_system_json + end + # Configures storage using the current config. # # @note Skips if no proposal has been calculated yet. @@ -325,67 +276,157 @@ def configure_with_current calculate_bootloader end + # Calculates a proposal with the given config and updates the associated info. + # + # @see #configure and #configure_with_current + # + # @param config_json [Hash, nil] + def calculate_proposal(config_json = nil) + backend.configure(config_json) + backend.add_packages if backend.proposal.success? + + update_config_json + update_config_model_json + update_proposal_json + update_issues_json + end + # Performs the bootloader configuration applying the current config. def calculate_bootloader logger.info("Configuring bootloader") backend.bootloader.configure end - # @see #configure + # Updates the system info if needed. + def update_system_json + system_json = generate_system_json + return if self.system_json == system_json + + # This assignment emits a D-Bus PropertiesChanged. + self.system_json = system_json + self.SystemChanged(system_json) + end + + # Updates the config info if needed. + def update_config_json + config_json = generate_config_json + return if self.config_json == config_json + + # This assignment emits a D-Bus PropertiesChanged. + self.config_json = config_json + end + + # Updates the config model info if needed. + def update_config_model_json + config_model_json = generate_config_model_json + return if self.config_model_json == config_model_json + + # This assignment emits a D-Bus PropertiesChanged. + self.config_model_json = config_model_json + end + + # Updates the proposal info if needed. + def update_proposal_json + proposal_json = generate_proposal_json + return if self.proposal_json == proposal_json + + # This assignment emits a D-Bus PropertiesChanged. + self.proposal_json = proposal_json + self.ProposalChanged(proposal_json) + end + + # Updates the issues info if needed. + def update_issues_json + issues_json = generate_issues_json + return if self.issues_json == issues_json + + # This assignment emits a D-Bus PropertiesChanged. + self.issues_json = issues_json + end + + # Generates the serialized JSON of the system. # - # @param config_json [Hash, nil] see Agama::Storage::Manager#configure - def calculate_proposal(config_json = nil) - backend.configure(config_json) - backend.add_packages if backend.proposal.success? + # @return [String] + def generate_system_json + return null_json unless backend.probed? - self.ProposalChanged(recover_proposal) + json = { + devices: devices_hash(:probed), + availableDrives: available_drives, + availableMdRaids: available_md_raids, + candidateDrives: candidate_drives, + candidateMdRaids: candidate_md_raids, + issues: system_issues_hash, + productMountPoints: product_mount_points, + encryptionMethods: encryption_methods, + volumeTemplates: volume_templates + } + JSON.pretty_generate(json) end - # Generates a config JSON from a serialized config model. + # Generates the serialized JSON of the storage config used for calculating the current + # proposal. # - # @param serialized_model [String] Serialized config model. - # @return [Hash] Config according to JSON schema. - def config_from_model(serialized_model) - model_json = JSON.parse(serialized_model, symbolize_names: true) - config = Agama::Storage::ConfigConversions::FromModel.new( - model_json, - product_config: product_config, - storage_system: proposal.storage_system - ).convert + # @return [String] + def generate_config_json + json = proposal.storage_json + JSON.pretty_generate(json) + end - { storage: Agama::Storage::ConfigConversions::ToJSON.new(config).convert } + # Generates the serialized JSON of the storage config model. + # + # @return [String] + def generate_config_model_json + json = proposal.model_json + JSON.pretty_generate(json) end - # JSON representation of the given devicegraph from StorageManager + # Generates the serialized JSON of the proposal. # - # @param meth [Symbol] method used to get the devicegraph from StorageManager - # @return [Hash] - def json_devices(meth) - devicegraph = Y2Storage::StorageManager.instance.send(meth) - Agama::Storage::DevicegraphConversions::ToJSON.new(devicegraph).convert + # @return [String] + def generate_proposal_json + return null_json unless backend.proposal.success? + + json = { + devices: devices_hash(:staging), + actions: actions_hash + } + JSON.pretty_generate(json) end - # JSON representation of the given Agama issue + # Generates the serialized JSON of the list of issues. # - # @param issue [Array] + # @return [String] + def generate_issues_json + json = backend.issues.map { |i| issue_hash(i) } + JSON.pretty_generate(json) + end + + # Representation of the null JSON. + # + # @return [String] + def null_json + nil.to_json + end + + # Hash representation of the given devicegraph from StorageManager. + # + # @param meth [Symbol] method used to get the devicegraph from StorageManager # @return [Hash] - def json_issue(issue) - { - description: issue.description, - class: issue.kind&.to_s, - details: issue.details&.to_s - }.compact + def devices_hash(meth) + devicegraph = Y2Storage::StorageManager.instance.send(meth) + Agama::Storage::DevicegraphConversions::ToJSON.new(devicegraph).convert end - # List of sorted actions. + # List of hash representation of the actions. # - # @return [Hash] + # @return [Array] # * :device [Integer] # * :text [String] # * :subvol [Boolean] # * :delete [Boolean] # * :resize [Boolean] - def actions + def actions_hash backend.actions.map do |action| { device: action.device_sid, @@ -397,6 +438,27 @@ def actions end end + # List of hash representation of the problems found during system probing. + # + # @see #generate_system_json + # + # @return [Array] + def system_issues_hash + backend.system_issues.map { |i| issue_hash(i) } + end + + # Hash representation of the given issue. + # + # @param issue [Agama::Issue] + # @return [Hash] + def issue_hash(issue) + { + description: issue.description, + class: issue.kind&.to_s, + details: issue.details&.to_s + }.compact + end + # @see Storage::System#available_drives # @return [Array] def available_drives @@ -421,15 +483,6 @@ def candidate_md_raids proposal.storage_system.candidate_md_raids.map(&:sid) end - # Problems found during system probing - # - # @see #recover_system - # - # @return [Hash] - def system_issues - backend.system_issues.map { |i| json_issue(i) } - end - # Meaningful mount points for the current product. # # @return [Array] @@ -461,11 +514,6 @@ def volume_templates end end - # Emits the SystemChanged signal - def emit_system_changed - self.SystemChanged(recover_system) - end - def add_s390_interfaces require "agama/dbus/storage/interfaces/zfcp_manager" singleton_class.include Interfaces::ZFCPManager