Skip to content

Commit

Permalink
feat(storage): API support for storage settings (#1471)
Browse files Browse the repository at this point in the history
A JSON schema for the storage config is defined by
#1455. This PR adds support to the
D-Bus API to allow setting a JSON config following that new JSON schema:

~~~
org.opensuse.Agama.Storage1
  #SetConfig s
  #GetConfig s
~~~

## `#SetConfig`

For now, the `#SetConfig` D-Bus method admits 3 different JSON formats
for the given JSON config:

* Guided config
~~~
{ "storage": { "guided": ... } }
~~~

* New storage config
~~~
{ "storage": { ... } }
~~~

* Legacy AutoYaST config

~~~
{ "legacyAutoyastStorage": [ ... ] }
~~~

The plan is to make `#SetConfig` and `#GetConfig` only to work with a
*storage config* or a *legacy AutoYaST config*. The *guided config* is
still accepted by `#SetConfig` because the new *storage config* does not
offer all the options offered by *guided config* yet.
 
In the future, the *guided config* will be neither accepted nor reported
by `#SetConfig` and `#GetConfig` respectively. Nevertheless, a D-Bus
method accepting the *guided config* will still be offered as a
simplification of the *storage settings* to make easier to work with the
use cases supported by the web UI.

## `#GetConfig`

Currently, `#GetConfig` reports different config formats depending on
how the proposal was calculated:

* For the initial proposal: reports the *guided config*.
* If `#SetConfig` was called with *guided config*: reports *guided
config* (extended with mandatory volumes, etc).
* If `#SetConfig` was called with *storage config*: reports exactly the
given *storage config*.
* If `#SetConfig` was called with *legacy AutoYaST config*: reports
exactly the given *legacy AutoYaST config*.
* If `Storage1.Proposal#Calculate` was called: reports the *guided
config* (extended with mandatory volumes, etc).

In the future, `#GetConfig` will only report either the *storage config*
or the *legacy AutoYaST config*.
  • Loading branch information
joseivanlopez authored Aug 27, 2024
2 parents 10093da + 3e1c2ee commit d1a0e5a
Show file tree
Hide file tree
Showing 19 changed files with 952 additions and 356 deletions.
177 changes: 40 additions & 137 deletions service/lib/agama/dbus/storage/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module Agama
module DBus
module Storage
# D-Bus object to manage storage installation
class Manager < BaseObject # rubocop:disable Metrics/ClassLength
class Manager < BaseObject
include WithISCSIAuth
include WithServiceStatus
include ::DBus::ObjectManager
Expand Down Expand Up @@ -91,30 +91,31 @@ def probe
busy_while { backend.probe }
end

# Calculates a proposal (guided or AutoYaST) from a given storage config.
# Applies the given serialized config according to the JSON schema.
#
# @raise If config is not valid.
# The JSON schema supports two different variants:
# { "storage": ... } or { "legacyAutoyastStorage": ... }.
#
# @param serialized_config [String] Serialized storage config. It can be storage or legacy
# AutoYaST settings: { "storage": ... } vs { "legacyAutoyastStorage": ... }.
def apply_storage_config(serialized_config)
config_json = JSON.parse(serialized_config, symbolize_names: true)
# @note The guided settings ({ "storage": { "guided": ... } }) are supported too, but it
# will be removed from the JSON schema.
#
# @raise If the config is not valid.
#
# @param serialized_config [String] Serialized storage config.
# @return [Integer] 0 success; 1 error
def apply_config(serialized_config)
logger.info("Setting storage config from D-Bus: #{serialized_config}")

if (settings_json = config_json.dig(:storage, :guided))
calculate_guided_proposal(settings_json)
elsif (settings_json = config_json[:legacyAutoyastStorage])
calculate_autoyast_proposal(settings_json)
else
raise "Invalid config: #{serialized_config}"
end
config_json = JSON.parse(serialized_config, symbolize_names: true)
proposal.calculate_from_json(config_json)
proposal.success? ? 0 : 1
end

# Serialized storage config. It can contain storage or legacy AutoYaST settings:
# { "storage": ... } vs { "legacyAutoyastStorage": ... }
# Gets and serializes the storage config used to calculate the current proposal.
#
# @return [String]
def serialized_storage_config
JSON.pretty_generate(storage_config)
# @return [String] Serialized config according to the JSON schema.
def recover_config
JSON.pretty_generate(proposal.config_json)
end

def install
Expand All @@ -135,9 +136,9 @@ def deprecated_system
dbus_interface STORAGE_INTERFACE do
dbus_method(:Probe) { probe }
dbus_method(:SetConfig, "in serialized_config:s, out result:u") do |serialized_config|
busy_while { apply_storage_config(serialized_config) }
busy_while { apply_config(serialized_config) }
end
dbus_method(:GetConfig, "out config:s") { serialized_storage_config }
dbus_method(:GetConfig, "out serialized_config:s") { recover_config }
dbus_method(:Install) { install }
dbus_method(:Finish) { finish }
dbus_reader(:deprecated_system, "b")
Expand Down Expand Up @@ -179,10 +180,23 @@ def update_actions
dbus_reader_attr_accessor :actions, "aa{sv}"
end

# @todo Rename as "org.opensuse.Agama.Storage1.Proposal".
PROPOSAL_CALCULATOR_INTERFACE = "org.opensuse.Agama.Storage1.Proposal.Calculator"
private_constant :PROPOSAL_CALCULATOR_INTERFACE

# Calculates a guided proposal.
#
# @param settings_dbus [Hash]
# @return [Integer] 0 success; 1 error
def calculate_guided_proposal(settings_dbus)
logger.info("Calculating guided storage proposal from D-Bus: #{settings_dbus}")

settings = ProposalSettingsConversion.from_dbus(settings_dbus,
config: config, logger: logger)

proposal.calculate_guided(settings)
proposal.success? ? 0 : 1
end

# List of disks available for installation
#
# Each device is represented by an array containing the name of the device and the label to
Expand Down Expand Up @@ -220,58 +234,6 @@ def default_volume(mount_path)
VolumeConversion.to_dbus(volume)
end

module ProposalStrategy
GUIDED = "guided"
AUTOYAST = "autoyast"
end

# Calculates a guided proposal.
# @deprecated
#
# @param dbus_settings [Hash]
# @return [Integer] 0 success; 1 error
def calculate_proposal(dbus_settings)
settings = ProposalSettingsConversion.from_dbus(dbus_settings,
config: config, logger: logger)

logger.info(
"Calculating guided storage proposal from D-Bus.\n " \
"D-Bus settings: #{dbus_settings}\n" \
"Agama settings: #{settings.inspect}"
)

success = proposal.calculate_guided(settings)
success ? 0 : 1
end

# Whether a proposal was calculated.
#
# @return [Boolean]
def proposal_calculated?
proposal.calculated?
end

# Proposal result, including information about success, strategy and settings.
#
# @return [Hash] Empty if there is no proposal yet.
def proposal_result
return {} unless proposal.calculated?

if proposal.strategy?(ProposalStrategy::GUIDED)
{
"success" => proposal.success?,
"strategy" => ProposalStrategy::GUIDED,
"settings" => proposal.settings.to_json_settings.to_json
}
else
{
"success" => proposal.success?,
"strategy" => ProposalStrategy::AUTOYAST,
"settings" => proposal.settings.to_json
}
end
end

dbus_interface PROPOSAL_CALCULATOR_INTERFACE do
dbus_reader :available_devices, "ao"

Expand All @@ -284,16 +246,12 @@ def proposal_result
[default_volume(mount_path)]
end

# @todo Rename as CalculateGuided
# @todo Receive guided json settings.
#
# result: 0 success; 1 error
dbus_method(:Calculate, "in settings:a{sv}, out result:u") do |settings|
busy_while { calculate_proposal(settings) }
dbus_method(:Calculate, "in settings_dbus:a{sv}, out result:u") do |settings_dbus|
busy_while { calculate_guided_proposal(settings_dbus) }
end

dbus_reader :proposal_calculated?, "b", dbus_name: "Calculated"

dbus_reader :proposal_result, "a{sv}", dbus_name: "Result"
end

ISCSI_INITIATOR_INTERFACE = "org.opensuse.Agama.Storage1.ISCSI.Initiator"
Expand Down Expand Up @@ -396,61 +354,6 @@ def proposal
backend.proposal
end

# Calculates a guided proposal.
#
# @param settings_json [Hash] JSON settings according to schema.
# @return [Integer] 0 success; 1 error
def calculate_guided_proposal(settings_json)
proposal_settings = Agama::Storage::ProposalSettings
.new_from_json(settings_json, config: config)

logger.info(
"Calculating guided storage proposal from D-Bus.\n" \
"Input settings: #{settings_json}\n" \
"Agama settings: #{proposal_settings.inspect}"
)

success = proposal.calculate_guided(proposal_settings)
success ? 0 : 1
end

# Calculates an AutoYaST proposal.
#
# @param settings_json [Hash] AutoYaST settings.
# @return [Integer] 0 success; 1 error
def calculate_autoyast_proposal(settings_json)
# Ensures keys are strings.
autoyast_settings = JSON.parse(settings_json.to_json)

logger.info(
"Calculating AutoYaST storage proposal from D-Bus.\n" \
"Input settings: #{settings_json}\n" \
"AutoYaST settings: #{autoyast_settings}"
)

success = proposal.calculate_autoyast(autoyast_settings)
success ? 0 : 1
end

# Storage config from the current proposal, if any.
#
# @return [Hash] Storage config according to JSON schema.
def storage_config
if proposal.strategy?(ProposalStrategy::GUIDED)
{
storage: {
guided: proposal.settings.to_json_settings
}
}
elsif proposal.strategy?(ProposalStrategy::AUTOYAST)
{
autoyastLegacyStorage: proposal.settings
}
else
{}
end
end

def register_storage_callbacks
backend.on_issues_change { issues_properties_changed }
backend.on_deprecated_system_change { storage_properties_changed }
Expand Down Expand Up @@ -510,7 +413,7 @@ def export_proposal
@dbus_proposal = nil
end

return unless proposal.strategy?(ProposalStrategy::GUIDED)
return unless proposal.guided?

@dbus_proposal = DBus::Storage::Proposal.new(proposal, logger)
@service.export(@dbus_proposal)
Expand Down
4 changes: 2 additions & 2 deletions service/lib/agama/dbus/storage/proposal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ def initialize(backend, logger)
#
# @return [Hash]
def settings
return {} unless backend.settings
return {} unless backend.guided?

ProposalSettingsConversion.to_dbus(backend.settings)
ProposalSettingsConversion.to_dbus(backend.guided_settings)
end

# List of sorted actions in D-Bus format.
Expand Down
2 changes: 1 addition & 1 deletion service/lib/agama/storage/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def explicit_boot_device
def implicit_boot_device
# NOTE: preliminary implementation with very simplistic checks
root_drive = drives.find do |drive|
drive.partitions.any? { |p| p.filesystem.root? }
drive.partitions.any? { |p| p.filesystem&.root? }
end

root_drive&.found_device&.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

require "agama/storage/config_conversions/filesystem_type/from_json"
require "agama/storage/configs/filesystem"
require "y2storage/filesystems/mount_by_type"

module Agama
module Storage
Expand All @@ -41,12 +42,15 @@ def convert(default = nil)
default_config = default.dup || Configs::Filesystem.new

default_config.tap do |config|
mount_by = convert_mount_by
type = convert_type(config.type)
label = filesystem_json[:label]
mkfs_options = filesystem_json[:mkfsOptions]

config.path = filesystem_json[:path]
config.mount_options = filesystem_json[:mountOptions] || []
config.type = convert_type(config.type)
config.mount_by = mount_by if mount_by
config.type = type if type
config.label = label if label
config.mkfs_options = mkfs_options if mkfs_options
end
Expand All @@ -57,10 +61,21 @@ def convert(default = nil)
# @return [Hash]
attr_reader :filesystem_json

# @param default [Configs::Filesystem, nil]
# @return [Configs::FilesystemType]
# @return [Y2Storage::Filesystems::MountByType, nil]
def convert_mount_by
value = filesystem_json[:mountBy]
return unless value

Y2Storage::Filesystems::MountByType.find(value.to_sym)
end

# @param default [Configs::FilesystemType, nil]
# @return [Configs::FilesystemType, nil]
def convert_type(default = nil)
FilesystemType::FromJSON.new(filesystem_json[:type]).convert(default)
filesystem_type_json = filesystem_json[:type]
return unless filesystem_type_json

FilesystemType::FromJSON.new(filesystem_type_json).convert(default)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,45 @@ module Agama
module Storage
module ConfigConversions
module FilesystemType
# Filesystem conversion from JSON hash according to schema.
# Filesystem type conversion from JSON hash according to schema.
class FromJSON
# @param filesystem_json [Hash, String, nil]
def initialize(filesystem_json)
@filesystem_json = filesystem_json
# @param filesystem_type_json [Hash, String]
def initialize(filesystem_type_json)
@filesystem_type_json = filesystem_type_json
end

# Performs the conversion from Hash according to the JSON schema.
#
# @param default [Configs::Filesystem, nil]
# @return [Configs::Filesystem]
# @param default [Configs::FilesystemType, nil]
# @return [Configs::FilesystemType]
def convert(default = nil)
default_config = default.dup || Configs::FilesystemType.new

default_config.tap do |config|
btrfs = convert_btrfs(config.btrfs)
type = convert_type

config.fs_type = type if type
config.fs_type = convert_type
config.btrfs = btrfs if btrfs
end
end

private

# @return [Hash]
attr_reader :filesystem_json
# @return [Hash, String]
attr_reader :filesystem_type_json

# @return [Y2Storage::Filesystems::Type]
def convert_type
return if filesystem_json.nil?

value = filesystem_json.is_a?(String) ? filesystem_json : "btrfs"
value = filesystem_type_json.is_a?(String) ? filesystem_type_json : "btrfs"
Y2Storage::Filesystems::Type.find(value.to_sym)
end

# @param default [Configs::Btrfs]
# @param default [Configs::Btrfs, nil]
# @return [Configs::Btrfs, nil]
def convert_btrfs(default = nil)
return if filesystem_json.nil? || filesystem_json.is_a?(String)
return if filesystem_type_json.nil? || filesystem_type_json.is_a?(String)

btrfs_json = filesystem_json[:btrfs]
btrfs_json = filesystem_type_json[:btrfs]
default_config = default.dup || Configs::Btrfs.new

default_config.tap do |config|
Expand Down
2 changes: 1 addition & 1 deletion service/lib/agama/storage/configs/filesystem_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module Agama
module Storage
module Configs
class FilesystemType
# @return [Y2Storage::Filesystems::Type]
# @return [Y2Storage::Filesystems::Type, nil]
attr_accessor :fs_type

# @return [Configs::Btrfs, nil]
Expand Down
Loading

0 comments on commit d1a0e5a

Please sign in to comment.