diff --git a/rust/agama-lib/share/examples/storage.json b/rust/agama-lib/share/examples/storage_drives.json similarity index 100% rename from rust/agama-lib/share/examples/storage.json rename to rust/agama-lib/share/examples/storage_drives.json diff --git a/rust/agama-lib/share/examples/storage-guided.json b/rust/agama-lib/share/examples/storage_guided.json similarity index 100% rename from rust/agama-lib/share/examples/storage-guided.json rename to rust/agama-lib/share/examples/storage_guided.json diff --git a/rust/agama-lib/share/examples/storage_lvm.json b/rust/agama-lib/share/examples/storage_lvm.json new file mode 100644 index 0000000000..8e0c64ccec --- /dev/null +++ b/rust/agama-lib/share/examples/storage_lvm.json @@ -0,0 +1,71 @@ +{ + "storage": { + "drives": [ + { + "partitions": [ + { + "alias": "pv1", + "id": "lvm", + "size": { "min": "10 GiB" } + } + ] + }, + { + "partitions": [ + { + "alias": "pv2", + "id": "lvm", + "size": { "min": "10 GiB" } + } + ] + } + ], + "volumeGroups": [ + { + "name": "system", + "physicalVolumes": ["pv1", "pv2"], + "extentSize": "8 MiB", + "logicalVolumes": [ + { + "name": "root", + "size": { + "min": "10 GiB" + }, + "encryption": { + "luks2": { + "password": "notsecret" + } + }, + "filesystem": { + "type": "btrfs", + "path": "/" + } + }, + { + "name": "home", + "size": "5 GiB", + "filesystem": { + "type": "xfs", + "path": "/home" + } + }, + { + "alias": "lvm_thin_pool", + "pool": true, + "name": "pool", + "size": { + "min": "5 GiB" + }, + "stripes": 10, + "stripeSize": "4 KiB" + }, + { + "name": "data", + "size": "100 GiB", + "usedPool": "lvm_thin_pool" + } + ] + } + ] + } +} diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index a223ff04ae..445b9b485d 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -471,6 +471,9 @@ "description": "The search is limited to drives scope.", "$ref": "#/$defs/search" }, + "alias": { + "$ref": "#/$defs/alias" + }, "encryption": { "$ref": "#/$defs/encryption" }, @@ -489,6 +492,9 @@ "description": "The search is limited to drives scope.", "$ref": "#/$defs/search" }, + "alias": { + "$ref": "#/$defs/alias" + }, "ptableType": { "title": "Partition table type", "description": "The partition table is created only if all the current partitions are deleted.", @@ -502,6 +508,140 @@ ] } }, + "volumeGroups": { + "title": "LVM volume groups", + "description": "Section describing the LVM volume groups.", + "type": "array", + "items": { + "title": "LVM volume group", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "title": "Volume group name", + "type": "string", + "examples": ["vg0"] + }, + "extentSize": { + "title": "Extent size", + "$ref": "#/$defs/sizeValue" + }, + "physicalVolumes": { + "title": "Physical volumes", + "description": "Devices to use as physical volumes.", + "type": "array", + "items": { + "title": "Device alias", + "type": "string" + } + }, + "logicalVolumes": { + "title": "Logical volumes", + "type": "array", + "items": { + "anyOf": [ + { + "title": "Logical volume", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "title": "Logical volume name", + "type": "string", + "examples": ["lv0"] + }, + "size": { + "title": "Logical volume size", + "$ref": "#/$defs/size" + }, + "stripes": { + "title": "Number of stripes", + "type": "integer", + "minimum": 1, + "maximum": 128 + }, + "stripeSize": { + "title": "Stripe size", + "$ref": "#/$defs/sizeValue" + }, + "encryption": { + "$ref": "#/$defs/encryption" + }, + "filesystem": { + "$ref": "#/$defs/filesystem" + } + } + }, + { + "title": "Thin pool logical volume", + "type": "object", + "additionalProperties": false, + "properties": { + "pool": { + "title": "LVM thin pool", + "const": true + }, + "alias": { + "$ref": "#/$defs/alias" + }, + "name": { + "title": "Logical volume name", + "type": "string", + "examples": ["lv0"] + }, + "size": { + "title": "Logical volume size", + "$ref": "#/$defs/size" + }, + "stripes": { + "title": "Number of stripes", + "type": "integer", + "minimum": 1, + "maximum": 128 + }, + "stripeSize": { + "title": "Stripe size", + "$ref": "#/$defs/sizeValue" + }, + "encryption": { + "$ref": "#/$defs/encryption" + } + } + }, + { + "title": "Thin logical volume", + "type": "object", + "additionalProperties": false, + "required": ["usedPool"], + "properties": { + "name": { + "title": "Thin logical volume name", + "type": "string", + "examples": ["lv0"] + }, + "size": { + "title": "Thin logical volume size", + "$ref": "#/$defs/size" + }, + "usedPool": { + "title": "Used LVM thin pool", + "description": "Alias of a LVM thin pool.", + "type": "string" + }, + "encryption": { + "$ref": "#/$defs/encryption" + }, + "filesystem": { + "$ref": "#/$defs/filesystem" + } + } + } + ] + } + } + } + } + }, "guided": { "title": "Guided proposal settings", "$comment": "This guided section will be extracted to a separate schema. Only storage and legacyAutoyastStorage will be offered as valid schemas for the storage config.", @@ -859,6 +999,11 @@ } ] }, + "alias": { + "title": "Alias", + "description": "Name used to reference a device.", + "type": "string" + }, "boot": { "title": "Boot options", "description": "Allows configuring boot partitions automatically.", @@ -1070,6 +1215,9 @@ "description": "The search is limited to the partitions of the selected device scope.", "$ref": "#/$defs/search" }, + "alias": { + "$ref": "#/$defs/alias" + }, "id": { "title": "Partition ID", "enum": ["linux", "swap", "lvm", "raid", "esp", "prep", "bios_boot"] diff --git a/service/lib/agama/storage/config.rb b/service/lib/agama/storage/config.rb index 27ad3a4f42..7384c447c8 100644 --- a/service/lib/agama/storage/config.rb +++ b/service/lib/agama/storage/config.rb @@ -33,7 +33,7 @@ class Config # @return [Array] attr_accessor :drives - # @return [Array] + # @return [Array] attr_accessor :volume_groups # @return [Array] @@ -107,7 +107,7 @@ def calculate_default_sizes(volume_builder) # return [Array] def filesystems - (drives + partitions).map(&:filesystem).compact + (drives + partitions + logical_volumes).map(&:filesystem).compact end # return [Array] @@ -115,9 +115,14 @@ def partitions drives.flat_map(&:partitions) end - # return [Array] + # return [Array] + def logical_volumes + volume_groups.flat_map(&:logical_volumes) + end + + # return [Array] def default_size_devices - partitions.select { |p| p.size&.default? } + (partitions + logical_volumes).select { |p| p.size&.default? } end # Min or max size that should be used for the given partition or logical volume diff --git a/service/lib/agama/storage/config_builder.rb b/service/lib/agama/storage/config_builder.rb new file mode 100644 index 0000000000..7a5738858b --- /dev/null +++ b/service/lib/agama/storage/config_builder.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/configs" +require "agama/storage/proposal_settings_reader" +require "agama/storage/volume_templates_builder" + +module Agama + module Storage + # Class for building configs. + class ConfigBuilder + # @todo Replace product_config param by a ProductDefinition. + # + # @param product_config [Agama::Config] + def initialize(product_config) + @product_config = product_config + end + + # Default encryption config from the product definition. + # + # @return [Configs::Encryption] + def default_encryption + Configs::Encryption.new.tap do |config| + config.password = settings.encryption.password + config.method = settings.encryption.method + config.pbkd_function = settings.encryption.pbkd_function + end + end + + # Default format config from the product definition. + # + # @param path [String, nil] + # @return [Configs::Filesystem] + def default_filesystem(path = nil) + Configs::Filesystem.new.tap do |config| + config.type = default_fstype(path) + end + end + + private + + # @return [Agama::Config] + attr_reader :product_config + + # Default filesystem type config from the product definition. + # + # @param path [String, nil] + # @return [Configs::FilesystemType] + def default_fstype(path = nil) + volume = volume_builder.for(path || "") + + Configs::FilesystemType.new.tap do |config| + config.fs_type = volume.fs_type + config.btrfs = volume.btrfs + end + end + + # @return [ProposalSettings] + def settings + @settings ||= ProposalSettingsReader.new(product_config).read + end + + # @return [VolumeTemplatesBuilder] + def volume_builder + @volume_builder ||= VolumeTemplatesBuilder.new_from_config(product_config) + end + end + end +end diff --git a/service/lib/agama/storage/config_checker.rb b/service/lib/agama/storage/config_checker.rb new file mode 100644 index 0000000000..e05d920f05 --- /dev/null +++ b/service/lib/agama/storage/config_checker.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/issue" +require "yast/i18n" +require "y2storage/mount_point" + +module Agama + module Storage + # Class for checking a storage config. + class ConfigChecker + include Yast::I18n + + # @param config [Storage::Config] + def initialize(config) + textdomain "agama" + @config = config + end + + # Issues detected in the config. + # + # @return [Array] + def issues + config.drives.flat_map { |d| drive_issues(d) } + + config.volume_groups.flat_map { |v| volume_group_issues(v) } + end + + private + + # @return [Storage::Config] + attr_reader :config + + # Issues from a drive config. + # + # @param config [Configs::Drive] + # @return [Array] + def drive_issues(config) + issues = encryption_issues(config) + partitions_issues = config.partitions.flat_map { |p| partition_issues(p) } + + issues + partitions_issues + end + + # Issues from a partition config. + # + # @param config [Configs::Partition] + # @return [Array] + def partition_issues(config) + encryption_issues(config) + end + + # Issues from a volume group config. + # + # @param config [Configs::VolumeGroup] + # @return [Array] + def volume_group_issues(config) + lvs_issues = config.logical_volumes.flat_map { |v| logical_volume_issues(v, config) } + pvs_issues = config.physical_volumes.map { |v| missing_physical_volume_issue(v) }.compact + + lvs_issues + pvs_issues + end + + # Issues from a logical volume config. + # + # @param lv_config [Configs::LogicalVolume] + # @param vg_config [Configs::VolumeGroup] + # + # @return [Array] + def logical_volume_issues(lv_config, vg_config) + [ + encryption_issues(lv_config), + missing_thin_pool_issue(lv_config, vg_config) + ].compact.flatten + end + + # @see #logical_volume_issues + # + # @param lv_config [Configs::LogicalVolume] + # @param vg_config [Configs::VolumeGroup] + # + # @return [Issue, nil] + def missing_thin_pool_issue(lv_config, vg_config) + return unless lv_config.thin_volume? + + pool = vg_config.logical_volumes + .select(&:pool?) + .find { |p| p.alias == lv_config.used_pool } + + return if pool + + error( + format( + # TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). + _("There is no LVM thin pool volume with alias %s"), + lv_config.used_pool + ) + ) + end + + # @see #logical_volume_issues + # + # @param pv_alias [String] + # @return [Issue, nil] + def missing_physical_volume_issue(pv_alias) + configs = config.drives + config.drives.flat_map(&:partitions) + return if configs.any? { |c| c.alias == pv_alias } + + error( + format( + # TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). + _("There is no LVM physical volume with alias %s"), + pv_alias + ) + ) + end + + # Issues related to encryption. + # + # @param config [Configs::Drive, Configs::Partition, Configs::LogicalVolume] + # @return [Array] + def encryption_issues(config) + return [] unless config.encryption + + [ + missing_encryption_password_issue(config), + available_encryption_method_issue(config), + wrong_encryption_method_issue(config) + ].compact + end + + # @see #encryption_issues + # + # @param config [Configs::Drive, Configs::Partition, Configs::LogicalVolume] + # @return [Issue, nil] + def missing_encryption_password_issue(config) + return unless config.encryption&.missing_password? + + error( + format( + # TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device + # (e.g., 'luks1', 'random_swap'). + _("No passphrase provided (required for using the method '%{crypt_method}')."), + crypt_method: config.encryption.method.id.to_s + ) + ) + end + + # @see #encryption_issues + # + # @param config [Configs::Drive, Configs::Partition, Configs::LogicalVolume] + # @return [Issue, nil] + def available_encryption_method_issue(config) + method = config.encryption&.method + return if !method || method.available? + + error( + format( + # TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device + # (e.g., 'luks1', 'random_swap'). + _("Encryption method '%{crypt_method}' is not available in this system."), + crypt_method: method.id.to_s + ) + ) + end + + # @see #encryption_issues + # + # @param config [Configs::Drive, Configs::Partition, Configs::LogicalVolume] + # @return [Issue, nil] + def wrong_encryption_method_issue(config) + method = config.encryption&.method + return unless method&.only_for_swap? + return if config.filesystem&.path == Y2Storage::MountPoint::SWAP_PATH.to_s + + error( + format( + # TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device + # (e.g., 'luks1', 'random_swap'). + _("'%{crypt_method}' is not a suitable method to encrypt the device."), + crypt_method: method.id.to_s + ) + ) + end + + # Creates an error issue. + # + # @param message [String] + # @return [Issue] + def error(message) + Agama::Issue.new( + message, + source: Agama::Issue::Source::CONFIG, + severity: Agama::Issue::Severity::ERROR + ) + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions.rb b/service/lib/agama/storage/config_conversions.rb index 0412053670..c1284c76bf 100644 --- a/service/lib/agama/storage/config_conversions.rb +++ b/service/lib/agama/storage/config_conversions.rb @@ -19,15 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/block_device" -require "agama/storage/config_conversions/drive" -require "agama/storage/config_conversions/encryption" -require "agama/storage/config_conversions/filesystem" require "agama/storage/config_conversions/from_json" -require "agama/storage/config_conversions/partition" -require "agama/storage/config_conversions/partitionable" -require "agama/storage/config_conversions/search" -require "agama/storage/config_conversions/size" module Agama module Storage diff --git a/service/lib/agama/storage/config_conversions/block_device/from_json.rb b/service/lib/agama/storage/config_conversions/block_device/from_json.rb deleted file mode 100644 index fdf1d30f16..0000000000 --- a/service/lib/agama/storage/config_conversions/block_device/from_json.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/encryption/from_json" -require "agama/storage/config_conversions/filesystem/from_json" -require "agama/storage/config_conversions/filesystem_type/from_json" -require "agama/storage/configs/encryption" -require "agama/storage/configs/filesystem" -require "agama/storage/configs/filesystem_type" - -module Agama - module Storage - module ConfigConversions - module BlockDevice - # Block device conversion from JSON hash according to schema. - class FromJSON - # @todo Replace settings and volume_builder params by a ProductDefinition. - # - # @param blk_device_json [Hash] - # @param settings [ProposalSettings] - # @param volume_builder [VolumeTemplatesBuilder] - def initialize(blk_device_json, settings:, volume_builder:) - @blk_device_json = blk_device_json - @settings = settings - @volume_builder = volume_builder - end - - # Performs the conversion from Hash according to the JSON schema. - # - # @param default [Configs::Drive, Configs::Partition] - # @return [Configs::Drive, Configs::Partition] - def convert(default) - default.dup.tap do |config| - config.encryption = convert_encrypt - config.filesystem = convert_filesystem - end - end - - private - - # @return [Hash] - attr_reader :blk_device_json - - # @return [ProposalSettings] - attr_reader :settings - - # @return [VolumeTemplatesBuilder] - attr_reader :volume_builder - - # @return [Configs::Encrypt, nil] - def convert_encrypt - encrypt_json = blk_device_json[:encryption] - return unless encrypt_json - - Encryption::FromJSON.new(encrypt_json, default: default_encrypt_config).convert - end - - # @return [Configs::Filesystem, nil] - def convert_filesystem - filesystem_json = blk_device_json[:filesystem] - return if filesystem_json.nil? - - default = default_filesystem_config(filesystem_json&.dig(:path) || "") - - # @todo Check whether the given filesystem can be used for the mount point. - # @todo Check whether snapshots can be configured and restore to default if needed. - - Filesystem::FromJSON.new(filesystem_json).convert(default) - end - - # @todo Recover values from ProductDefinition instead of ProposalSettings. - # - # Default encryption config from the product definition. - # - # @return [Configs::Encryption] - def default_encrypt_config - Configs::Encryption.new.tap do |config| - config.password = settings.encryption.password - config.method = settings.encryption.method - config.pbkd_function = settings.encryption.pbkd_function - end - end - - # Default format config from the product definition. - # - # @param mount_path [String] - # @return [Configs::Filesystem] - def default_filesystem_config(mount_path) - Configs::Filesystem.new.tap do |config| - config.type = default_fstype_config(mount_path) - end - end - - # @todo Recover values from ProductDefinition instead of VolumeTemplatesBuilder. - # - # Default filesystem config from the product definition. - # - # @param mount_path [String] - # @return [Configs::FilesystemType] - def default_fstype_config(mount_path) - volume = volume_builder.for(mount_path) - - Configs::FilesystemType.new.tap do |config| - config.fs_type = volume.fs_type - config.btrfs = volume.btrfs - end - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/drive/from_json.rb b/service/lib/agama/storage/config_conversions/drive/from_json.rb deleted file mode 100644 index 94dbfafbac..0000000000 --- a/service/lib/agama/storage/config_conversions/drive/from_json.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/block_device/from_json" -require "agama/storage/config_conversions/search/from_json" -require "agama/storage/config_conversions/partitionable/from_json" -require "agama/storage/configs/drive" - -module Agama - module Storage - module ConfigConversions - module Drive - # Drive conversion from JSON hash according to schema. - class FromJSON - # @todo Replace settings and volume_builder params by a ProductDefinition. - # - # @param drive_json [Hash] - # @param settings [ProposalSettings] - # @param volume_builder [VolumeTemplatesBuilder] - def initialize(drive_json, settings:, volume_builder:) - @drive_json = drive_json - @settings = settings - @volume_builder = volume_builder - end - - # Performs the conversion from Hash according to the JSON schema. - # - # @param default [Configs::Drive, nil] - # @return [Configs::Drive] - def convert(default = nil) - default_config = default.dup || Configs::Drive.new - - convert_drive(default_config).tap do |config| - search = convert_search(config.search) - config.search = search if search - end - end - - private - - # @return [Hash] - attr_reader :drive_json - - # @return [ProposalSettings] - attr_reader :settings - - # @return [VolumeTemplatesBuilder] - attr_reader :volume_builder - - # @param config [Configs::Drive] - # @return [Configs::Drive] - def convert_drive(config) - convert_block_device( - convert_partitionable(config) - ) - end - - # @param config [Configs::Drive] - def convert_block_device(config) - converter = BlockDevice::FromJSON.new(drive_json, - settings: settings, volume_builder: volume_builder) - - converter.convert(config) - end - - # @param config [Configs::Drive] - def convert_partitionable(config) - converter = Partitionable::FromJSON.new(drive_json, - settings: settings, volume_builder: volume_builder) - - converter.convert(config) - end - - # @param config [Configs::Search] - # @return [Configs::Search, nil] - def convert_search(config) - search_json = drive_json[:search] - return unless search_json - - converter = Search::FromJSON.new(search_json) - converter.convert(config) - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/encryption/from_json.rb b/service/lib/agama/storage/config_conversions/encryption/from_json.rb deleted file mode 100644 index c9e193d694..0000000000 --- a/service/lib/agama/storage/config_conversions/encryption/from_json.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/configs/encryption" -require "y2storage/encryption_method" -require "y2storage/pbkd_function" - -module Agama - module Storage - module ConfigConversions - module Encryption - # Encryption conversion from JSON hash according to schema. - class FromJSON - # @param encryption_json [Hash, String] - # @param default [Configs::Encrypt] - def initialize(encryption_json, default: nil) - @encryption_json = encryption_json - @default_config = default || Configs::Encryption.new - end - - # Performs the conversion from Hash according to the JSON schema. - # - # @return [Configs::Encryption] - def convert - default_config.dup.tap do |config| - convert_luks1(config) || - convert_luks2(config) || - convert_pervasive_luks2(config) || - convert_swap_encryption(config) - end - end - - private - - # @return [Hash, String] - attr_reader :encryption_json - - # @return [Configs::Encryption] - attr_reader :default_config - - # @param config [Configs::Encryption] - # @return [Configs::Encryption, nil] nil if JSON does not match LUKS1 schema. - def convert_luks1(config) - luks1_json = encryption_json.is_a?(Hash) && encryption_json[:luks1] - return unless luks1_json - - key_size = convert_key_size(luks1_json) - cipher = convert_cipher(luks1_json) - - config.method = Y2Storage::EncryptionMethod::LUKS1 - config.password = convert_password(luks1_json) - config.key_size = key_size if key_size - config.cipher = cipher if cipher - end - - # @param config [Configs::Encryption] - # @return [Configs::Encryption, nil] nil if JSON does not match LUKS2 schema. - def convert_luks2(config) - luks2_json = encryption_json.is_a?(Hash) && encryption_json[:luks2] - return unless luks2_json - - key_size = convert_key_size(luks2_json) - cipher = convert_cipher(luks2_json) - label = convert_label - pbkdf = convert_pbkd_function - - config.method = Y2Storage::EncryptionMethod::LUKS2 - config.password = convert_password(luks2_json) - config.key_size = key_size if key_size - config.cipher = cipher if cipher - config.label = label if label - config.pbkd_function = pbkdf if pbkdf - end - - # @param config [Configs::Encryption] - # @return [Configs::Encryption, nil] nil if JSON does not match pervasive LUKS2 schema. - def convert_pervasive_luks2(config) - pervasive_json = encryption_json.is_a?(Hash) && encryption_json[:pervasive_luks2] - return unless pervasive_json - - config.method = Y2Storage::EncryptionMethod::PERVASIVE_LUKS2 - config.password = convert_password(pervasive_json) - end - - # @param config [Configs::Encryption] - # @return [Configs::Encryption, nil] nil if JSON does not match a swap encryption schema. - def convert_swap_encryption(config) - return unless encryption_json.is_a?(String) - - # @todo Report issue if the schema admits an unknown method. - method = Y2Storage::EncryptionMethod.find(encryption_json.to_sym) - return unless method - - config.method = method - end - - # @param method_json [Hash] - # @return [String, nil] - def convert_password(method_json) - method_json[:password] - end - - # @param method_json [Hash] - # @return [Integer, nil] - def convert_key_size(method_json) - method_json[:keySize] - end - - # @param method_json [Hash] - # @return [String, nil] - def convert_cipher(method_json) - method_json[:cipher] - end - - # @return [String, nil] - def convert_label - encryption_json.dig(:luks2, :label) - end - - # @return [Y2Storage::PbkdFunction, nil] - def convert_pbkd_function - Y2Storage::PbkdFunction.find(encryption_json.dig(:luks2, :pbkdFunction)) - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/filesystem_type.rb b/service/lib/agama/storage/config_conversions/filesystem_type.rb deleted file mode 100644 index ace22105b4..0000000000 --- a/service/lib/agama/storage/config_conversions/filesystem_type.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/filesystem_type/from_json" - -module Agama - module Storage - module ConfigConversions - # Conversions for filesystem types - module FilesystemType - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/from_json.rb b/service/lib/agama/storage/config_conversions/from_json.rb index 8fe8b0abfa..14ba24df15 100644 --- a/service/lib/agama/storage/config_conversions/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json.rb @@ -19,36 +19,14 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config" -require "agama/storage/config_conversions/drive/from_json" -require "agama/storage/configs/boot" -require "agama/storage/proposal_settings_reader" +require "agama/storage/config_builder" +require "agama/storage/config_conversions/from_json_conversions/config" +require "agama/storage/volume_templates_builder" module Agama module Storage module ConfigConversions # Config conversion from JSON hash according to schema. - # - # TODO: The approach for generating a Config from JSON could be improved: - # * All the FromJSON classes receive only a JSON and an optional default config to start - # converting from it. - # * There should be a "config generator" class which knows the ProductDefinition and creates - # config objects calling to the proper FromJSON classes, passing the default config for - # each case (drive, partition, etc). - # - # For example: - # - # def generate_drive(drive_json) - # default = default_drive(drive_json.dig(:filesystem, :path)) - # drive = Drive::FromJson.new(drive_json).convert(default) - # drive.partitions = drive_json[:partitions].map do |partition_json| - # default = default_partition(partition_json.dig(:fileystem, :path)) - # Partition::FromJSON.new(partition_json).convert(default) - # end - # drive - # end - # - # This improvement could be done at the time of introducing the ProductDefinition class. class FromJSON # @todo Replace product_config param by a ProductDefinition. # @@ -61,17 +39,16 @@ def initialize(config_json, product_config:) # Performs the conversion from Hash according to the JSON schema. # + # @todo Raise error if config_json does not match the JSON schema. + # # @return [Storage::Config] def convert - # @todo Raise error if config_json does not match the JSON schema. - Storage::Config.new.tap do |config| - boot = convert_boot - drives = convert_drives + config = FromJSONConversions::Config + .new(config_json, config_builder: config_builder) + .convert - config.boot = boot if boot - config.drives = drives if drives - config.calculate_default_sizes(volume_builder) - end + config.calculate_default_sizes(volume_builder) + config end private @@ -82,34 +59,9 @@ def convert # @return [Agama::Config] attr_reader :product_config - # @return [Configs::Boot, nil] - def convert_boot - boot_json = config_json[:boot] - return unless boot_json - - Configs::Boot.new.tap do |config| - config.configure = boot_json[:configure] - config.device = boot_json[:device] - end - end - - # @return [Array, nil] - def convert_drives - drives_json = config_json[:drives] - return unless drives_json - - drives_json.map { |d| convert_drive(d) } - end - - # @return [Configs::Drive] - def convert_drive(drive_json) - Drive::FromJSON.new(drive_json, - settings: settings, volume_builder: volume_builder).convert - end - - # @return [ProposalSettings] - def settings - @settings ||= ProposalSettingsReader.new(product_config).read + # @return [ConfigBuilder] + def config_builder + @config_builder ||= ConfigBuilder.new(product_config) end # @return [VolumeTemplatesBuilder] diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions.rb b/service/lib/agama/storage/config_conversions/from_json_conversions.rb new file mode 100644 index 0000000000..e463c51003 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/boot" +require "agama/storage/config_conversions/from_json_conversions/btrfs" +require "agama/storage/config_conversions/from_json_conversions/config" +require "agama/storage/config_conversions/from_json_conversions/drive" +require "agama/storage/config_conversions/from_json_conversions/encryption" +require "agama/storage/config_conversions/from_json_conversions/filesystem" +require "agama/storage/config_conversions/from_json_conversions/filesystem_type" +require "agama/storage/config_conversions/from_json_conversions/logical_volume" +require "agama/storage/config_conversions/from_json_conversions/partition" +require "agama/storage/config_conversions/from_json_conversions/search" +require "agama/storage/config_conversions/from_json_conversions/size" +require "agama/storage/config_conversions/from_json_conversions/volume_group" + +module Agama + module Storage + module ConfigConversions + # Conversions from JSON. + module FromJSONConversions + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb new file mode 100644 index 0000000000..c295b92427 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Base class for conversions from JSON hash according to schema. + class Base + # @param config_builder [ConfigBuilder, nil] + def initialize(config_builder = nil) + @config_builder = config_builder + end + + # Performs the conversion from Hash according to the JSON schema. + # + # @param default [Object] A {Config} or any its configs from {Storage::Configs}. + # @return [Object] A {Config} or any its configs from {Storage::Configs}. + def convert(default) + default.dup.tap do |config| + conversions(config).each do |property, value| + next if value.nil? + + config.public_send("#{property}=", value) + end + end + end + + private + + # @return [ConfigBuilder, nil] + attr_reader :config_builder + + # Values to apply to the config. + # + # @param _default [Object] A {Config} or any its configs from {Storage::Configs}. + # @return [Hash] e.g., { name: "/dev/vda" }. + def conversions(_default) + {} + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb new file mode 100644 index 0000000000..259d12ec13 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/configs/boot" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Boot conversion from JSON hash according to schema. + class Boot < Base + # @param boot_json [Hash] + def initialize(boot_json) + super() + @boot_json = boot_json + end + + # @see Base#convert + # + # @param default [Configs::Boot, nil] + # @return [Configs::Boot] + def convert(default = nil) + super(default || Configs::Boot.new) + end + + private + + # @return [Hash] + attr_reader :boot_json + + # @see Base#conversions + # + # @param _default [Configs::Boot] + # @return [Hash] + def conversions(_default) + { + configure: boot_json[:configure], + device: boot_json[:device] + } + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb new file mode 100644 index 0000000000..0e95b0a24b --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/configs/btrfs" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Btrfs conversion from JSON hash according to schema. + class Btrfs < Base + # @param btrfs_json [Hash] + def initialize(btrfs_json) + super() + @btrfs_json = btrfs_json + end + + # @see Base#convert + # + # @param default [Configs::Btrfs, nil] + # @return [Configs::Btrfs] + def convert(default = nil) + super(default || Configs::Btrfs.new) + end + + private + + # @return [String] + attr_reader :btrfs_json + + # @see Base#conversions + # + # @param _default [Configs::Btrfs] + # @return [Hash] + def conversions(_default) + { + snapshots: btrfs_json[:snapshots] + } + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb new file mode 100644 index 0000000000..fbb4c6fd3a --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/boot" +require "agama/storage/config_conversions/from_json_conversions/drive" +require "agama/storage/config_conversions/from_json_conversions/volume_group" +require "agama/storage/config" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Config conversion from JSON hash according to schema. + class Config < Base + # @param config_json [Hash] + # @param config_builder [ConfigBuilder, nil] + def initialize(config_json, config_builder: nil) + super(config_builder) + @config_json = config_json + end + + # @see Base#convert + # + # @param default [Config, nil] + # @return [Config] + def convert(default = nil) + super(default || Storage::Config.new) + end + + private + + # @return [Hash] + attr_reader :config_json + + # @see Base#conversions + # + # @param default [Config] + # @return [Hash] + def conversions(default) + { + boot: convert_boot(default.boot), + drives: convert_drives, + volume_groups: convert_volume_groups + } + end + + # @param default [Configs::Boot, nil] + # @return [Configs::Boot, nil] + def convert_boot(default = nil) + boot_json = config_json[:boot] + return unless boot_json + + FromJSONConversions::Boot.new(boot_json).convert(default) + end + + # @return [Array, nil] + def convert_drives + drives_json = config_json[:drives] + return unless drives_json + + drives_json.map { |d| convert_drive(d) } + end + + # @param drive_json [Hash] + # @return [Configs::Drive] + def convert_drive(drive_json) + FromJSONConversions::Drive.new(drive_json, config_builder: config_builder).convert + end + + # @return [Array, nil] + def convert_volume_groups + volume_groups_json = config_json[:volumeGroups] + return unless volume_groups_json + + volume_groups_json.map { |v| convert_volume_group(v) } + end + + # @param volume_group_json [Hash] + # @return [Configs::VolumeGroup] + def convert_volume_group(volume_group_json) + FromJSONConversions::VolumeGroup + .new(volume_group_json, config_builder: config_builder) + .convert + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb new file mode 100644 index 0000000000..8146da6208 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/with_encryption" +require "agama/storage/config_conversions/from_json_conversions/with_filesystem" +require "agama/storage/config_conversions/from_json_conversions/with_partitions" +require "agama/storage/config_conversions/from_json_conversions/with_ptable_type" +require "agama/storage/config_conversions/from_json_conversions/with_search" +require "agama/storage/configs/drive" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Drive conversion from JSON hash according to schema. + class Drive < Base + include WithSearch + include WithEncryption + include WithFilesystem + include WithPtableType + include WithPartitions + + # @param drive_json [Hash] + # @param config_builder [ConfigBuilder, nil] + def initialize(drive_json, config_builder: nil) + super(config_builder) + @drive_json = drive_json + end + + # @see Base#convert + # + # @param default [Configs::Drive, nil] + # @return [Configs::Drive] + def convert(default = nil) + super(default || Configs::Drive.new) + end + + private + + # @return [Hash] + attr_reader :drive_json + + # @see Base#conversions + # + # @param default [Configs::Drive] + # @return [Hash] + def conversions(default) + { + search: convert_search(drive_json, default: default.search), + alias: drive_json[:alias], + encryption: convert_encryption(drive_json, default: default.encryption), + filesystem: convert_filesystem(drive_json, default: default.filesystem), + ptable_type: convert_ptable_type(drive_json), + partitions: convert_partitions(drive_json) + } + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb new file mode 100644 index 0000000000..c280f0ebf6 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/configs/encryption" +require "y2storage/encryption_method" +require "y2storage/pbkd_function" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Encryption conversion from JSON hash according to schema. + class Encryption < Base + # @param encryption_json [Hash, String] + # @param config_builder [ConfigBuilder, nil] + def initialize(encryption_json, config_builder: nil) + super(config_builder) + @encryption_json = encryption_json + end + + # @see Base#convert + # + # @param default [Configs::Encryption, nil] + # @return [Configs::Encryption] + def convert(default = nil) + super(default || self.default) + end + + private + + # @return [Hash, String] + attr_reader :encryption_json + + # @return [Configs::Encryption] + attr_reader :default_config + + # @see Base#conversions + # + # @param _default [Configs::Encryption] + # @return [Hash] + def conversions(_default) + return luks1_conversions if luks1? + return luks2_conversions if luks2? + return pervasive_luks2_conversions if pervasive_luks2? + + swap_encryption_conversions + end + + def luks1? + return false unless encryption_json.is_a?(Hash) + + !encryption_json[:luks1].nil? + end + + def luks2? + return false unless encryption_json.is_a?(Hash) + + !encryption_json[:luks2].nil? + end + + def pervasive_luks2? + return false unless encryption_json.is_a?(Hash) + + !encryption_json[:pervasive_luks2].nil? + end + + # @return [Hash] + def luks1_conversions + luks1_json = encryption_json[:luks1] + + { + method: Y2Storage::EncryptionMethod::LUKS1, + password: convert_password(luks1_json), + key_size: convert_key_size(luks1_json), + cipher: convert_cipher(luks1_json) + } + end + + # @return [Hash] + def luks2_conversions + luks2_json = encryption_json[:luks2] + + { + method: Y2Storage::EncryptionMethod::LUKS2, + password: convert_password(luks2_json), + key_size: convert_key_size(luks2_json), + cipher: convert_cipher(luks2_json), + label: convert_label, + pbkd_function: convert_pbkd_function + } + end + + # @return [Hash] + def pervasive_luks2_conversions + pervasive_json = encryption_json[:pervasive_luks2] + + { + method: Y2Storage::EncryptionMethod::PERVASIVE_LUKS2, + password: convert_password(pervasive_json) + } + end + + # @return [Hash] + def swap_encryption_conversions + return {} unless encryption_json.is_a?(String) + + # TODO: Report issue if the schema admits an unknown method. + method = Y2Storage::EncryptionMethod.find(encryption_json.to_sym) + return {} unless method + + { + method: method + } + end + + # @param json [Hash] + # @return [String, nil] + def convert_password(json) + json[:password] + end + + # @param json [Hash] + # @return [Integer, nil] + def convert_key_size(json) + json[:keySize] + end + + # @param json [Hash] + # @return [String, nil] + def convert_cipher(json) + json[:cipher] + end + + # @return [String, nil] + def convert_label + encryption_json.dig(:luks2, :label) + end + + # @return [Y2Storage::PbkdFunction, nil] + def convert_pbkd_function + Y2Storage::PbkdFunction.find(encryption_json.dig(:luks2, :pbkdFunction)) + end + + # Default encryption config. + # + # @return [Configs::Encryption] + def default + return Configs::Encryption.new unless config_builder + + config_builder.default_encryption + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/filesystem/from_json.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb similarity index 68% rename from service/lib/agama/storage/config_conversions/filesystem/from_json.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb index fdd61b7503..68b605c7f8 100644 --- a/service/lib/agama/storage/config_conversions/filesystem/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb @@ -19,50 +19,53 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/filesystem_type/from_json" +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/filesystem_type" require "agama/storage/configs/filesystem" require "y2storage/filesystems/mount_by_type" module Agama module Storage module ConfigConversions - module Filesystem + module FromJSONConversions # Filesystem conversion from JSON hash according to schema. - class FromJSON + class Filesystem < Base # @param filesystem_json [Hash] - def initialize(filesystem_json) + # @param config_builder [ConfigBuilder, nil] + def initialize(filesystem_json, config_builder: nil) + super(config_builder) @filesystem_json = filesystem_json end - # Performs the conversion from Hash according to the JSON schema. + # @see Base#convert # # @param default [Configs::Filesystem, nil] # @return [Configs::Filesystem] def convert(default = nil) - default_config = default.dup || Configs::Filesystem.new + super(default || self.default) + end + + private + + # @return [Hash] + attr_reader :filesystem_json - values = { + # @see Base#conversions + # + # @param default [Configs::Filesystem] + # @return [Hash] + def conversions(default) + { reuse: filesystem_json[:reuseIfPossible], label: filesystem_json[:label], path: filesystem_json[:path], mount_options: filesystem_json[:mountOptions], mkfs_options: filesystem_json[:mkfsOptions], mount_by: convert_mount_by, - type: convert_type(default_config.type) + type: convert_type(default.type) } - - default_config.tap do |config| - values.each do |property, value| - config.public_send("#{property}=", value) unless value.nil? - end - end end - private - - # @return [Hash] - attr_reader :filesystem_json - # @return [Y2Storage::Filesystems::MountByType, nil] def convert_mount_by value = filesystem_json[:mountBy] @@ -77,7 +80,16 @@ def convert_type(default = nil) filesystem_type_json = filesystem_json[:type] return unless filesystem_type_json - FilesystemType::FromJSON.new(filesystem_type_json).convert(default) + FromJSONConversions::FilesystemType.new(filesystem_type_json).convert(default) + end + + # Default filesystem config. + # + # @return [Configs::Filesystem] + def default + return Configs::Filesystem.new unless config_builder + + config_builder.default_filesystem(filesystem_json[:path]) end end end diff --git a/service/lib/agama/storage/config_conversions/filesystem_type/from_json.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb similarity index 69% rename from service/lib/agama/storage/config_conversions/filesystem_type/from_json.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb index 960838f84c..094e9976cb 100644 --- a/service/lib/agama/storage/config_conversions/filesystem_type/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb @@ -19,6 +19,8 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/btrfs" require "agama/storage/configs/btrfs" require "agama/storage/configs/filesystem_type" require "y2storage/filesystems/type" @@ -26,27 +28,21 @@ module Agama module Storage module ConfigConversions - module FilesystemType + module FromJSONConversions # Filesystem type conversion from JSON hash according to schema. - class FromJSON + class FilesystemType < Base # @param filesystem_type_json [Hash, String] def initialize(filesystem_type_json) + super() @filesystem_type_json = filesystem_type_json end - # Performs the conversion from Hash according to the JSON schema. + # @see Base#convert # # @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) - - config.fs_type = convert_type - config.btrfs = btrfs if btrfs - end + super(default || Configs::FilesystemType.new) end private @@ -54,7 +50,18 @@ def convert(default = nil) # @return [Hash, String] attr_reader :filesystem_type_json - # @return [Y2Storage::Filesystems::Type] + # @see Base#conversions + # + # @param default [Configs::FilesystemType] + # @return [Hash] + def conversions(default) + { + fs_type: convert_type, + btrfs: convert_btrfs(default.btrfs) + } + end + + # @return [Y2Storage::Filesystems::Type, nil] def convert_type value = filesystem_type_json.is_a?(String) ? filesystem_type_json : "btrfs" Y2Storage::Filesystems::Type.find(value.to_sym) @@ -63,16 +70,12 @@ def convert_type # @param default [Configs::Btrfs, nil] # @return [Configs::Btrfs, nil] def convert_btrfs(default = nil) - return if filesystem_type_json.nil? || filesystem_type_json.is_a?(String) + return if filesystem_type_json.is_a?(String) btrfs_json = filesystem_type_json[:btrfs] - default_config = default.dup || Configs::Btrfs.new - - default_config.tap do |config| - snapshots = btrfs_json[:snapshots] + return unless btrfs_json - config.snapshots = snapshots unless snapshots.nil? - end + FromJSONConversions::Btrfs.new(btrfs_json).convert(default) end end end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb new file mode 100644 index 0000000000..cd48f8de1f --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/with_encryption" +require "agama/storage/config_conversions/from_json_conversions/with_filesystem" +require "agama/storage/config_conversions/from_json_conversions/with_size" +require "agama/storage/configs/logical_volume" +require "y2storage/disk_size" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Logical volume conversion from JSON hash according to schema. + class LogicalVolume < Base + include WithEncryption + include WithFilesystem + include WithSize + + # @param logical_volume_json [Hash] + # @param config_builder [ConfigBuilder, nil] + def initialize(logical_volume_json, config_builder: nil) + super(config_builder) + @logical_volume_json = logical_volume_json + end + + # @see Base#convert + # + # @param default [Configs::LogicalVolume, nil] + # @return [Configs::LogicalVolume] + def convert(default = nil) + super(default || Configs::LogicalVolume.new) + end + + private + + # @return [Hash] + attr_reader :logical_volume_json + + # @see Base#conversions + # + # @param default [Configs::LogicalVolume] + # @return [Hash] + def conversions(default) + { + alias: logical_volume_json[:alias], + encryption: convert_encryption(logical_volume_json, default: default.encryption), + filesystem: convert_filesystem(logical_volume_json, default: default.filesystem), + size: convert_size(logical_volume_json, default: default.size), + name: logical_volume_json[:name], + stripes: logical_volume_json[:stripes], + stripe_size: convert_stripe_size, + pool: logical_volume_json[:pool], + used_pool: logical_volume_json[:usedPool] + } + end + + # @return [Y2Storage::DiskSize, nil] + def convert_stripe_size + value = logical_volume_json[:stripeSize] + return unless value + + Y2Storage::DiskSize.new(value) + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb new file mode 100644 index 0000000000..81a487057e --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/with_encryption" +require "agama/storage/config_conversions/from_json_conversions/with_filesystem" +require "agama/storage/config_conversions/from_json_conversions/with_search" +require "agama/storage/config_conversions/from_json_conversions/with_size" +require "agama/storage/configs/partition" +require "y2storage/partition_id" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Partition conversion from JSON hash according to schema. + class Partition < Base + include WithSearch + include WithEncryption + include WithFilesystem + include WithSize + + # @param partition_json [Hash] + def initialize(partition_json, config_builder: nil) + super(config_builder) + @partition_json = partition_json + end + + # @see Base#convert + # + # @param default [Configs::Partition, nil] + # @return [Configs::Partition] + def convert(default = nil) + super(default || Configs::Partition.new) + end + + private + + # @return [Hash] + attr_reader :partition_json + + # @see Base#conversions + # + # @param default [Configs::Partition] + # @return [Hash] + def conversions(default) + { + search: convert_search(partition_json, default: default.search), + alias: partition_json[:alias], + encryption: convert_encryption(partition_json, default: default.encryption), + filesystem: convert_filesystem(partition_json, default: default.filesystem), + size: convert_size(partition_json, default: default.size), + id: convert_id, + delete: partition_json[:delete], + delete_if_needed: partition_json[:deleteIfNeeded] + } + end + + # @return [Y2Storage::PartitionId, nil] + def convert_id + value = partition_json[:id] + return unless value + + Y2Storage::PartitionId.find(value) + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/search/from_json.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb similarity index 75% rename from service/lib/agama/storage/config_conversions/search/from_json.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/search.rb index a03be629d0..327568a36d 100644 --- a/service/lib/agama/storage/config_conversions/search/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb @@ -19,31 +19,27 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/configs/search" + module Agama module Storage module ConfigConversions - module Search + module FromJSONConversions # Search conversion from JSON hash according to schema. - class FromJSON + class Search < Base # @param search_json [Hash, String] def initialize(search_json) + super() @search_json = search_json end - # Performs the conversion from Hash according to the JSON schema. + # @see Base#convert # # @param default [Configs::Search, nil] # @return [Configs::Search] def convert(default = nil) - default_config = default.dup || Configs::Search.new - - default_config.tap do |config| - name = convert_name - not_found = convert_not_found - - config.name = name if name - config.if_not_found = not_found if not_found - end + super(default || Configs::Search.new) end private @@ -51,6 +47,17 @@ def convert(default = nil) # @return [Hash, String] attr_reader :search_json + # @see Base#conversions + # + # @param _default [Configs::Partition] + # @return [Hash] + def conversions(_default) + { + name: convert_name, + if_not_found: convert_not_found + } + end + # @return [String, nil] def convert_name return search_json if search_json.is_a?(String) diff --git a/service/lib/agama/storage/config_conversions/size/from_json.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb similarity index 77% rename from service/lib/agama/storage/config_conversions/size/from_json.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/size.rb index 02fe5a6102..19e4a1850f 100644 --- a/service/lib/agama/storage/config_conversions/size/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb @@ -19,32 +19,28 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/config_conversions/from_json_conversions/base" require "agama/storage/configs/size" require "y2storage/disk_size" module Agama module Storage module ConfigConversions - module Size + module FromJSONConversions # Size conversion from JSON hash according to schema. - class FromJSON + class Size < Base # @param size_json [Hash] def initialize(size_json) + super() @size_json = size_json end - # Performs the conversion from Hash according to the JSON schema. + # @see Base#convert # # @param default [Configs::Size, nil] # @return [Configs::Size] def convert(default = nil) - default_config = default.dup || Configs::Size.new - - default_config.tap do |config| - config.default = false - config.min = convert_size(:min) - config.max = convert_size(:max) || Y2Storage::DiskSize.unlimited - end + super(default || Configs::Size.new) end private @@ -52,6 +48,18 @@ def convert(default = nil) # @return [Hash] attr_reader :size_json + # @see Base#conversions + # + # @param _default [Configs::Size] + # @return [Hash] + def conversions(_default) + { + default: false, + min: convert_size(:min), + max: convert_size(:max) || Y2Storage::DiskSize.unlimited + } + end + # @return [Y2Storage::DiskSize, nil] def convert_size(field) value = case size_json diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb new file mode 100644 index 0000000000..b88e26212d --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/base" +require "agama/storage/config_conversions/from_json_conversions/logical_volume" +require "agama/storage/configs/drive" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Volume group conversion from JSON hash according to schema. + class VolumeGroup < Base + # @param volume_group_json [Hash] + # @param config_builder [ConfigBuilder, nil] + def initialize(volume_group_json, config_builder: nil) + super(config_builder) + @volume_group_json = volume_group_json + end + + # @see Base#convert + # + # @param default [Configs::VolumeGroup, nil] + # @return [Configs::VolumeGroup] + def convert(default = nil) + super(default || Configs::VolumeGroup.new) + end + + private + + # @return [Hash] + attr_reader :volume_group_json + + # @see Base#conversions + # + # @param _default [Configs::VolumeGroup] + # @return [Hash] + def conversions(_default) + { + name: volume_group_json[:name], + extent_size: convert_extent_size, + physical_volumes: volume_group_json[:physicalVolumes], + logical_volumes: convert_logical_volumes + } + end + + # @return [Y2Storage::DiskSize, nil] + def convert_extent_size + value = volume_group_json[:extentSize] + return unless value + + Y2Storage::DiskSize.new(value) + end + + # @return [Array, nil] + def convert_logical_volumes + logical_volumes_json = volume_group_json[:logicalVolumes] + return unless logical_volumes_json + + logical_volumes_json.map { |l| convert_logical_volume(l) } + end + + # @param logical_volume_json [Hash] + # @return [Configs::LogicalVolume] + def convert_logical_volume(logical_volume_json) + FromJSONConversions::LogicalVolume + .new(logical_volume_json, config_builder: config_builder) + .convert + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/with_encryption.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_encryption.rb new file mode 100644 index 0000000000..2dce8c39dc --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_encryption.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/encryption" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Mixin for encryption conversion. + module WithEncryption + # @param json [Hash] + # @param default [Configs::Encryption, nil] + # + # @return [Configs::Encryption, nil] + def convert_encryption(json, default: nil) + encryption_json = json[:encryption] + return unless encryption_json + + FromJSONConversions::Encryption + .new(encryption_json, config_builder: config_builder) + .convert(default) + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/with_filesystem.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_filesystem.rb new file mode 100644 index 0000000000..9f5e846781 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_filesystem.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/filesystem" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Mixin for filesystem conversion. + module WithFilesystem + # @param json [Hash] + # @param default [Configs::Filesystem, nil] + # + # @return [Configs::Filesystem, nil] + def convert_filesystem(json, default: nil) + filesystem_json = json[:filesystem] + return unless filesystem_json + + # @todo Check whether the given filesystem can be used for the mount point. + # @todo Check whether snapshots can be configured and restore to default if needed. + + FromJSONConversions::Filesystem + .new(filesystem_json, config_builder: config_builder) + .convert(default) + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/with_partitions.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_partitions.rb new file mode 100644 index 0000000000..432fb9ab61 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_partitions.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/partition" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Mixin for partitions conversion. + module WithPartitions + # @param json [Hash] + # @return [Array, nil] + def convert_partitions(json) + partitions_json = json[:partitions] + return unless partitions_json + + partitions_json.map { |p| convert_partition(p) } + end + + # @param partition_json [Hash] + # @return [Configs::Partition] + def convert_partition(partition_json) + FromJSONConversions::Partition + .new(partition_json, config_builder: config_builder) + .convert + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/drive.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_ptable_type.rb similarity index 65% rename from service/lib/agama/storage/config_conversions/drive.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/with_ptable_type.rb index f42dd41358..d6c8d6b6fa 100644 --- a/service/lib/agama/storage/config_conversions/drive.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_ptable_type.rb @@ -19,13 +19,24 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/drive/from_json" +require "y2storage/partition_tables/type" module Agama module Storage module ConfigConversions - # Conversions for drive. - module Drive + module FromJSONConversions + # Mixin for partition table type conversion. + module WithPtableType + # @param json [Hash] + # + # @return [Y2Storage::PartitionTables::Type, nil] + def convert_ptable_type(json) + value = json[:ptableType] + return unless value + + Y2Storage::PartitionTables::Type.find(value) + end + end end end end diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/with_search.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_search.rb new file mode 100644 index 0000000000..7a25dbc259 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_search.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/config_conversions/from_json_conversions/search" + +module Agama + module Storage + module ConfigConversions + module FromJSONConversions + # Mixin for search conversion. + module WithSearch + # @param json [Hash] + # @param default [Configs::Search, nil] + # + # @return [Configs::Search, nil] + def convert_search(json, default: nil) + search_json = json[:search] + return unless search_json + + converter = FromJSONConversions::Search.new(search_json) + converter.convert(default) + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/with_size.rb similarity index 62% rename from service/lib/agama/storage/config_conversions/filesystem.rb rename to service/lib/agama/storage/config_conversions/from_json_conversions/with_size.rb index 35828920a5..33e9baa815 100644 --- a/service/lib/agama/storage/config_conversions/filesystem.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/with_size.rb @@ -19,13 +19,25 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/filesystem/from_json" +require "agama/storage/config_conversions/from_json_conversions/size" module Agama module Storage module ConfigConversions - # Conversions for filesystem. - module Filesystem + module FromJSONConversions + # Mixin for size conversion. + module WithSize + # @param json [Hash] + # @param default [Configs::Size, nil] + # + # @return [Configs::Size, nil] + def convert_size(json, default: nil) + size_json = json[:size] + return unless size_json + + FromJSONConversions::Size.new(size_json).convert(default) + end + end end end end diff --git a/service/lib/agama/storage/config_conversions/partition.rb b/service/lib/agama/storage/config_conversions/partition.rb deleted file mode 100644 index 52b67d2f50..0000000000 --- a/service/lib/agama/storage/config_conversions/partition.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/partition/from_json" - -module Agama - module Storage - module ConfigConversions - # Conversions for partition. - module Partition - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/partition/from_json.rb b/service/lib/agama/storage/config_conversions/partition/from_json.rb deleted file mode 100644 index 7784029cd0..0000000000 --- a/service/lib/agama/storage/config_conversions/partition/from_json.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/block_device/from_json" -require "agama/storage/config_conversions/search/from_json" -require "agama/storage/config_conversions/size/from_json" -require "agama/storage/configs/partition" -require "y2storage/partition_id" - -module Agama - module Storage - module ConfigConversions - module Partition - # Partition conversion from JSON hash according to schema. - class FromJSON - # @todo Replace settings and volume_builder params by a ProductDefinition. - # - # @param partition_json [Hash] - # @param settings [ProposalSettings] - # @param volume_builder [VolumeTemplatesBuilder] - def initialize(partition_json, settings:, volume_builder:) - @partition_json = partition_json - @settings = settings - @volume_builder = volume_builder - end - - # Performs the conversion from Hash according to the JSON schema. - # - # @param default [Configs::Partition, nil] - # @return [Configs::Partition] - def convert(default = nil) - default_config = default.dup || Configs::Partition.new - - convert_block_device(default_config).tap do |config| - search = convert_search(config.search) - delete = partition_json[:delete] - delete_if_needed = partition_json[:deleteIfNeeded] - id = convert_id - size = convert_size(config.size) - - config.search = search if search - config.delete = delete unless delete.nil? - config.delete_if_needed = delete_if_needed unless delete_if_needed.nil? - config.id = id if id - config.size = size if size - end - end - - private - - # @return [Hash] - attr_reader :partition_json - - # @return [ProposalSettings] - attr_reader :settings - - # @return [VolumeTemplatesBuilder] - attr_reader :volume_builder - - # @param config [Configs::Partition] - # @return [Configs::Partition] - def convert_block_device(config) - converter = BlockDevice::FromJSON.new(partition_json, - settings: settings, volume_builder: volume_builder) - - converter.convert(config) - end - - # @param config [Configs::Search] - # @return [Configs::Search, nil] - def convert_search(config) - search_json = partition_json[:search] - return unless search_json - - converter = Search::FromJSON.new(search_json) - converter.convert(config) - end - - # @return [Y2Storage::PartitionId, nil] - def convert_id - value = partition_json[:id] - return unless value - - Y2Storage::PartitionId.find(value) - end - - # @param config [Configs::Size] - # @return [Configs::Size, nil] - def convert_size(config) - size_json = partition_json[:size] - return unless size_json - - Size::FromJSON.new(size_json).convert(config) - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/partitionable.rb b/service/lib/agama/storage/config_conversions/partitionable.rb deleted file mode 100644 index 95094cb0a4..0000000000 --- a/service/lib/agama/storage/config_conversions/partitionable.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/partitionable/from_json" - -module Agama - module Storage - module ConfigConversions - # Conversions for partitionable. - module Partitionable - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/partitionable/from_json.rb b/service/lib/agama/storage/config_conversions/partitionable/from_json.rb deleted file mode 100644 index 330ef37802..0000000000 --- a/service/lib/agama/storage/config_conversions/partitionable/from_json.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/partition/from_json" -require "y2storage/partition_tables/type" - -module Agama - module Storage - module ConfigConversions - module Partitionable - # Partitionable device conversion from JSON hash according to schema. - class FromJSON - # @todo Replace settings and volume_builder params by a ProductDefinition. - # - # @param partitionable_json [Hash] - # @param settings [ProposalSettings] - # @param volume_builder [VolumeTemplatesBuilder] - def initialize(partitionable_json, settings:, volume_builder:) - @partitionable_json = partitionable_json - @settings = settings - @volume_builder = volume_builder - end - - # Performs the conversion from Hash according to the JSON schema. - # - # @param default [Configs::Drive] - # @return [Configs::Drive] - def convert(default) - default.dup.tap do |config| - config.ptable_type = convert_ptable_type - config.partitions = convert_partitions - end - end - - private - - # @return [Hash] - attr_reader :partitionable_json - - # @return [ProposalSettings] - attr_reader :settings - - # @return [VolumeTemplatesBuilder] - attr_reader :volume_builder - - # @return [Y2Storage::PartitionTables::Type, nil] - def convert_ptable_type - value = partitionable_json[:ptableType] - return unless value - - Y2Storage::PartitionTables::Type.find(value) - end - - # @return [Array] - def convert_partitions - partitions_json = partitionable_json[:partitions] - return [] unless partitions_json - - partitions_json.map { |p| convert_partition(p) } - end - - # @param partition_json [Hash] - # @return [Configs::Partition] - def convert_partition(partition_json) - Partition::FromJSON.new(partition_json, - settings: settings, volume_builder: volume_builder).convert - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/search.rb b/service/lib/agama/storage/config_conversions/search.rb deleted file mode 100644 index 5a6c6d44f3..0000000000 --- a/service/lib/agama/storage/config_conversions/search.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/search/from_json" - -module Agama - module Storage - module ConfigConversions - # Conversions for search. - module Search - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/size.rb b/service/lib/agama/storage/config_conversions/size.rb deleted file mode 100644 index b85b9e734d..0000000000 --- a/service/lib/agama/storage/config_conversions/size.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/size/from_json" - -module Agama - module Storage - module ConfigConversions - # Conversions for size. - module Size - end - end - end -end diff --git a/service/lib/agama/storage/configs.rb b/service/lib/agama/storage/configs.rb index 7f276374c0..fc5f7e8486 100644 --- a/service/lib/agama/storage/configs.rb +++ b/service/lib/agama/storage/configs.rb @@ -33,6 +33,8 @@ module Configs require "agama/storage/configs/encryption" require "agama/storage/configs/filesystem" require "agama/storage/configs/filesystem_type" +require "agama/storage/configs/logical_volume" require "agama/storage/configs/partition" require "agama/storage/configs/search" require "agama/storage/configs/size" +require "agama/storage/configs/volume_group" diff --git a/service/lib/agama/storage/configs/drive.rb b/service/lib/agama/storage/configs/drive.rb index ff1b8260f5..ee6113f383 100644 --- a/service/lib/agama/storage/configs/drive.rb +++ b/service/lib/agama/storage/configs/drive.rb @@ -20,6 +20,8 @@ # find current contact information at www.suse.com. require "agama/storage/configs/search" +require "agama/storage/configs/with_alias" +require "agama/storage/configs/with_search" module Agama module Storage @@ -27,8 +29,8 @@ module Configs # Section of the configuration representing a device that is expected to exist in the target # system and that can be used as a regular disk. class Drive - # @return [Search] - attr_accessor :search + include WithAlias + include WithSearch # @return [Encryption, nil] attr_accessor :encryption @@ -49,15 +51,6 @@ def initialize @search = Search.new end - # Assigned device according to the search. - # - # @see Y2Storage::Proposal::AgamaSearcher - # - # @return [Y2Storage::Device, nil] - def found_device - search.device - end - # Whether the drive definition contains partition definitions # # @return [Boolean] diff --git a/service/lib/agama/storage/configs/encryption.rb b/service/lib/agama/storage/configs/encryption.rb index 5b28ed4ea6..d36a4215b2 100644 --- a/service/lib/agama/storage/configs/encryption.rb +++ b/service/lib/agama/storage/configs/encryption.rb @@ -28,7 +28,7 @@ module Configs class Encryption include Y2Storage::SecretAttributes - # @return [Y2Storage::EncryptionMethod::Base] + # @return [Y2Storage::EncryptionMethod::Base, nil] attr_accessor :method # @!attribute password @@ -53,9 +53,18 @@ class Encryption # Specific key size (in bits) if LUKS is going to be used # - # @return [Integer,nil] If nil, the default key size will be used. If an integer + # @return [Integer, nil] If nil, the default key size will be used. If an integer # value is used, it has to be a multiple of 8 attr_accessor :key_size + + # Whether the password is missing. + # + # @return [Boolean] + def missing_password? + return false unless method&.password_required? + + password.nil? || password.empty? + end end end end diff --git a/service/lib/agama/storage/configs/logical_volume.rb b/service/lib/agama/storage/configs/logical_volume.rb new file mode 100644 index 0000000000..1f1696a5a1 --- /dev/null +++ b/service/lib/agama/storage/configs/logical_volume.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/configs/size" +require "agama/storage/configs/with_alias" + +module Agama + module Storage + module Configs + # Section of the configuration representing a LVM logical volume. + class LogicalVolume + include WithAlias + + # @return [String, nil] + attr_accessor :name + + # @return [Size] + attr_accessor :size + + # @return [Integer, nil] + attr_accessor :stripes + + # @return [Y2Storage::DiskSize, nil] + attr_accessor :stripe_size + + # @return [Boolean] + attr_accessor :pool + alias_method :pool?, :pool + + # @return [String, nil] + attr_accessor :used_pool + + # @return [Encryption, nil] + attr_accessor :encryption + + # @return [Filesystem, nil] + attr_accessor :filesystem + + def initialize + @size = Size.new + @pool = false + end + + # Whether the config represents a thin logical volume. + # + # @return [Boolean] + def thin_volume? + !used_pool.nil? + end + end + end + end +end diff --git a/service/lib/agama/storage/configs/partition.rb b/service/lib/agama/storage/configs/partition.rb index 83d048f46b..7d8a853e47 100644 --- a/service/lib/agama/storage/configs/partition.rb +++ b/service/lib/agama/storage/configs/partition.rb @@ -20,14 +20,16 @@ # find current contact information at www.suse.com. require "agama/storage/configs/size" +require "agama/storage/configs/with_search" +require "agama/storage/configs/with_alias" module Agama module Storage module Configs # Section of the configuration representing a partition class Partition - # @return [Search, nil] - attr_accessor :search + include WithAlias + include WithSearch # @return [Boolean] attr_accessor :delete @@ -54,15 +56,6 @@ def initialize @delete = false @delete_if_needed = false end - - # Assigned device according to the search. - # - # @see Y2Storage::Proposal::AgamaSearcher - # - # @return [Y2Storage::Device, nil] - def found_device - search&.device - end end end end diff --git a/service/lib/agama/storage/configs/volume_group.rb b/service/lib/agama/storage/configs/volume_group.rb new file mode 100644 index 0000000000..0939c57102 --- /dev/null +++ b/service/lib/agama/storage/configs/volume_group.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +module Agama + module Storage + module Configs + # Section of the configuration representing a LVM volume group. + class VolumeGroup + # @return [String, nil] + attr_accessor :name + + # @return [Y2Storage::DiskSize, nil] + attr_accessor :extent_size + + # @return [Array] + attr_accessor :physical_volumes + + # @return [Array] + attr_accessor :logical_volumes + + def initialize + @physical_volumes = [] + @logical_volumes = [] + end + end + end + end +end diff --git a/service/lib/agama/storage/config_conversions/block_device.rb b/service/lib/agama/storage/configs/with_alias.rb similarity index 73% rename from service/lib/agama/storage/config_conversions/block_device.rb rename to service/lib/agama/storage/configs/with_alias.rb index 7fcbb01077..e61d9cec7a 100644 --- a/service/lib/agama/storage/config_conversions/block_device.rb +++ b/service/lib/agama/storage/configs/with_alias.rb @@ -19,13 +19,20 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/block_device/from_json" - module Agama module Storage - module ConfigConversions - # Conversions for block device. - module BlockDevice + module Configs + # Mixin for configs with alias. + module WithAlias + # @return [String, nil] + attr_accessor :alias + + # Whether the config has the given alias. + # + # @return [Boolean] + def alias?(value) + self.alias == value + end end end end diff --git a/service/lib/agama/storage/config_conversions/encryption.rb b/service/lib/agama/storage/configs/with_search.rb similarity index 69% rename from service/lib/agama/storage/config_conversions/encryption.rb rename to service/lib/agama/storage/configs/with_search.rb index 288818bb96..f8cca8e743 100644 --- a/service/lib/agama/storage/config_conversions/encryption.rb +++ b/service/lib/agama/storage/configs/with_search.rb @@ -19,13 +19,22 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/config_conversions/encryption/from_json" - module Agama module Storage - module ConfigConversions - # Conversions for encryption. - module Encryption + module Configs + # Mixin for configs with search. + module WithSearch + # @return [Search, nil] + attr_accessor :search + + # Assigned device according to the search. + # + # @see Y2Storage::Proposal::AgamaSearcher + # + # @return [Y2Storage::Device, nil] + def found_device + search&.device + end end end end diff --git a/service/lib/y2storage/agama_proposal.rb b/service/lib/y2storage/agama_proposal.rb index 15ff647fd7..f5b980ea03 100644 --- a/service/lib/y2storage/agama_proposal.rb +++ b/service/lib/y2storage/agama_proposal.rb @@ -76,24 +76,24 @@ class AgamaProposal < Proposal::Base include Proposal::PlannedDevicesHandler # @return [Agama::Storage::Config] - attr_reader :settings + attr_reader :config # @return [Array] List of found issues attr_reader :issues_list # Constructor # - # @param initial_settings [Agama::Storage::Config] Agama storage settings + # @param initial_config [Agama::Storage::Config] Agama storage config # @param devicegraph [Devicegraph] starting point. If nil, then probed devicegraph # will be used # @param disk_analyzer [DiskAnalyzer] by default, the method will create a new one # based on the initial devicegraph or will use the one from the StorageManager if # starting from probed (i.e. 'devicegraph' argument is also missing) # @param issues_list [Array] def drives_with_empty_partition_table(devicegraph) - devices = settings.drives.map { |d| device_for(d, devicegraph) }.compact + devices = config.drives.map { |d| device_for(d, devicegraph) }.compact devices.select { |d| d.partition_table && d.partitions.empty? } end @@ -244,7 +244,7 @@ def protect_sids # @param devicegraph [Devicegraph] the graph gets modified def create_devices(devicegraph) devices_creator = Proposal::AgamaDevicesCreator.new(devicegraph, issues_list) - names = settings.drives.map(&:found_device).compact.map(&:name) + names = config.drives.map(&:found_device).compact.map(&:name) protect_sids result = devices_creator.populated_devicegraph(planned_devices, names, space_maker) end diff --git a/service/lib/y2storage/proposal/agama_device_planner.rb b/service/lib/y2storage/proposal/agama_device_planner.rb index 95c3815b31..e0076fb47d 100644 --- a/service/lib/y2storage/proposal/agama_device_planner.rb +++ b/service/lib/y2storage/proposal/agama_device_planner.rb @@ -20,14 +20,11 @@ # find current contact information at www.suse.com. require "y2storage/planned" -require "agama/issue" module Y2Storage module Proposal # Base class used by Agama planners. class AgamaDevicePlanner - include Yast::I18n - # @!attribute [r] devicegraph # Devicegraph to be used as starting point. # @return [Devicegraph] @@ -43,16 +40,14 @@ class AgamaDevicePlanner # @param devicegraph [Devicegraph] see {#devicegraph} # @param issues_list [Array] see {#issues_list} def initialize(devicegraph, issues_list) - textdomain "agama" - @devicegraph = devicegraph @issues_list = issues_list end - # Planned devices according to the given settings. + # Planned devices according to the given config. # # @return [Array] Array of planned devices. - def planned_devices(_setting) + def planned_devices(_config) raise NotImplementedError end @@ -81,145 +76,100 @@ def reformat?(device, config) end # @param planned [Planned::Disk, Planned::Partition] - # @param settings [#encryption, #filesystem] - def configure_device(planned, settings) - configure_encryption(planned, settings.encryption) if settings.encryption - configure_filesystem(planned, settings.filesystem) if settings.filesystem + # @param config [#encryption, #filesystem] + def configure_block_device(planned, config) + configure_encryption(planned, config.encryption) if config.encryption + configure_filesystem(planned, config.filesystem) if config.filesystem end # @param planned [Planned::Disk, Planned::Partition] - # @param settings [Agama::Storage::Configs::Filesystem] - def configure_filesystem(planned, settings) - planned.mount_point = settings.path - planned.mount_by = settings.mount_by - planned.fstab_options = settings.mount_options - planned.mkfs_options = settings.mkfs_options.join(",") - planned.label = settings.label - configure_filesystem_type(planned, settings.type) if settings.type + # @param config [Agama::Storage::Configs::Filesystem] + def configure_filesystem(planned, config) + planned.mount_point = config.path + planned.mount_by = config.mount_by + planned.fstab_options = config.mount_options + planned.mkfs_options = config.mkfs_options.join(",") + planned.label = config.label + configure_filesystem_type(planned, config.type) if config.type end # @param planned [Planned::Disk, Planned::Partition] - # @param settings [Agama::Storage::Configs::FilesystemType] - def configure_filesystem_type(planned, settings) - planned.filesystem_type = settings.fs_type - configure_btrfs(planned, settings.btrfs) if settings.btrfs + # @param config [Agama::Storage::Configs::FilesystemType] + def configure_filesystem_type(planned, config) + planned.filesystem_type = config.fs_type + configure_btrfs(planned, config.btrfs) if config.btrfs end # @param planned [Planned::Disk, Planned::Partition] - # @param settings [Agama::Storage::Configs::Btrfs] - def configure_btrfs(planned, settings) + # @param config [Agama::Storage::Configs::Btrfs] + def configure_btrfs(planned, config) # TODO: we need to discuss what to do with transactional systems and the read_only # property. We are not sure whether those things should be configurable by the user. - # planned.read_only = settings.read_only? - planned.snapshots = settings.snapshots? - planned.default_subvolume = settings.default_subvolume - planned.subvolumes = settings.subvolumes + # planned.read_only = config.read_only? + planned.snapshots = config.snapshots? + planned.default_subvolume = config.default_subvolume + planned.subvolumes = config.subvolumes end # @param planned [Planned::Disk, Planned::Partition] - # @param settings [Agama::Storage::Configs::Encryption] - def configure_encryption(planned, settings) - planned.encryption_password = settings.password - planned.encryption_method = settings.method - planned.encryption_pbkdf = settings.pbkd_function - planned.encryption_label = settings.label - planned.encryption_cipher = settings.cipher - planned.encryption_key_size = settings.key_size - - check_encryption(planned) - end - - # @see #configure_encryption - def check_encryption(dev) - issues_list << issue_missing_enc_password(dev) if missing_enc_password?(dev) - issues_list << issue_available_enc_method(dev) unless dev.encryption_method.available? - issues_list << issue_wrong_enc_method(dev) unless supported_enc_method?(dev) - end - - # @see #check_encryption - def missing_enc_password?(planned) - return false unless planned.encryption_method&.password_required? - - planned.encryption_password.nil? || planned.encryption_password.empty? - end - - # @see #check_encryption - def supported_enc_method?(planned) - planned.supported_encryption_method?(planned.encryption_method) - end - - # @see #check_encryption - def issue_missing_enc_password(planned) - msg = format( - # TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device (like - # 'luks1' or 'random_swap'). - _("No passphrase provided (required for using the method '%{crypt_method}')."), - crypt_method: planned.encryption_method.id.to_s - ) - encryption_issue(msg) - end - - # @see #check_encryption - def issue_available_enc_method(planned) - msg = format( - # TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device (like - # 'luks1' or 'random_swap'). - _("Encryption method '%{crypt_method}' is not available in this system."), - crypt_method: planned.encryption_method.id.to_s - ) - encryption_issue(msg) - end - - # @see #check_encryption - def issue_wrong_enc_method(planned) - msg = format( - # TRANSLATORS: 'crypt_method' is the name of the method to encrypt the device (like - # 'luks1' or 'random_swap'). - _("'%{crypt_method}' is not a suitable method to encrypt the device."), - crypt_method: planned.encryption_method.id.to_s - ) - encryption_issue(msg) - end - - # @see #check_encryption - def encryption_issue(message) - Agama::Issue.new( - message, - source: Agama::Issue::Source::CONFIG, - severity: Agama::Issue::Severity::ERROR - ) + # @param config [Agama::Storage::Configs::Encryption] + def configure_encryption(planned, config) + planned.encryption_password = config.password + planned.encryption_method = config.method + planned.encryption_pbkdf = config.pbkd_function + planned.encryption_label = config.label + planned.encryption_cipher = config.cipher + planned.encryption_key_size = config.key_size end # @param planned [Planned::Partition] - # @param settings [Agama::Storage::Configs::Size] - def configure_size(planned, settings) - planned.min_size = settings.min - planned.max_size = settings.max + # @param config [Agama::Storage::Configs::Size] + def configure_size(planned, config) + planned.min_size = config.min + planned.max_size = config.max planned.weight = 100 end # @param planned [Planned::Disk] - # @param config [Agama::Storage::Configs::Drive] - def configure_partitions(planned, config) - partition_configs = config.partitions + # @param device_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + def configure_partitions(planned, device_config, config) + partition_configs = device_config.partitions .reject(&:delete?) .reject(&:delete_if_needed?) planned.partitions = partition_configs.map do |partition_config| - planned_partition(partition_config).tap { |p| p.disk = config.found_device.name } + planned_partition(partition_config, device_config, config) end end - # @param config [Agama::Storage::Configs::Partition] + # @param partition_config [Agama::Storage::Configs::Partition] + # @param device_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + # # @return [Planned::Partition] - def planned_partition(config) + def planned_partition(partition_config, device_config, config) Planned::Partition.new(nil, nil).tap do |planned| - planned.partition_id = config.id - configure_reuse(planned, config) - configure_device(planned, config) - configure_size(planned, config.size) + planned.disk = device_config.found_device.name + planned.partition_id = partition_config.id + configure_reuse(planned, partition_config) + configure_block_device(planned, partition_config) + configure_size(planned, partition_config.size) + configure_pv(planned, partition_config, config) end end + + # @param planned [Planned::Disk, Planned::Partition] + # @param device_config [Agama::Storage::Configs::Drive, Agama::Storage::Configs::Partition] + # @param config [Agama::Storage::Config] + def configure_pv(planned, device_config, config) + return unless planned.respond_to?(:lvm_volume_group_name) && device_config.alias + + vg = config.volume_groups.find { |v| v.physical_volumes.include?(device_config.alias) } + return unless vg + + planned.lvm_volume_group_name = vg.name + end end end end diff --git a/service/lib/y2storage/proposal/agama_devices_creator.rb b/service/lib/y2storage/proposal/agama_devices_creator.rb index 1c273535a1..9f4dc8abd0 100644 --- a/service/lib/y2storage/proposal/agama_devices_creator.rb +++ b/service/lib/y2storage/proposal/agama_devices_creator.rb @@ -19,8 +19,10 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "y2storage/proposal/agama_lvm_helper" require "y2storage/exceptions" +require "y2storage/proposal/agama_lvm_helper" +require "y2storage/proposal/lvm_creator" +require "y2storage/proposal/partition_creator" module Y2Storage module Proposal @@ -110,6 +112,13 @@ def reset # planned devices have been allocated def process_devices process_existing_partitionables + process_volume_groups + + # This may be unexpected if the storage configuration provided by the user includes + # carefully crafted mount options but may be needed in weird situations for more automated + # proposals. Let's re-evaluate over time. + devicegraph.mount_points.each(&:adjust_mount_options) + creator_result end @@ -136,17 +145,43 @@ def process_existing_partitionables # But I'm not so sure if growing is so fine (we may need to make some space first). # I don't think we have the growing case covered by SpaceMaker, the distribution # calculator, etc. - # - planned_devices.each do |planned| - next unless planned.reuse? + planned_devices + .select(&:reuse?) + .map { |d| d.reuse!(devicegraph) } + end - planned.reuse!(devicegraph) - end + # @see #process_devices + def process_volume_groups + # TODO: Reuse volume groups. + planned_devices.vgs.map { |v| create_volume_group(v) } + end - # This may be unexpected if the storage configuration provided by the user includes - # carefully crafted mount options but may be needed in weird situations for more automated - # proposals. Let's re-evaluate over time. - devicegraph.mount_points.each(&:adjust_mount_options) + # Creates a volume group for the the given planned device. + # + # @param planned [Planned::LvmVg] + def create_volume_group(planned) + pv_names = physical_volumes_for(planned.volume_group_name) + # TODO: Generate issue if there are no physical volumes. + return if pv_names.empty? + + creator = Proposal::LvmCreator.new(creator_result.devicegraph) + new_result = creator.create_volumes(planned, pv_names) + self.creator_result = creator_result.merge(new_result) + end + + # Physical volumes (new partitions and reused devices) for a new volume group. + # + # @param vg_name [String] + # @return [Array] + def physical_volumes_for(vg_name) + pv_condition = proc { |d| d.respond_to?(:pv_for?) && d.pv_for?(vg_name) } + + new_pvs = creator_result.created_names(&pv_condition) + reused_pvs = reused_planned_devices + .select(&pv_condition) + .map(&:reuse_name) + + new_pvs + reused_pvs end # @see #process_existing_partitionables @@ -163,6 +198,14 @@ def partitions_for_existing(planned_devices) planned_devices.partitions.reject(&:reuse?) end + # Planned devices configured to be reused. + # + # @return [Array] + def reused_planned_devices + planned_devices.disks.select(&:reuse?) + + planned_devices.disks.flat_map(&:partitions).select(&:reuse?) + end + # Formats and/or mounts the disk-like block devices # # XEN partitions (StrayBlkDevice) are intentionally left out for now diff --git a/service/lib/y2storage/proposal/agama_devices_planner.rb b/service/lib/y2storage/proposal/agama_devices_planner.rb index 4476f66cc1..3624f6f21b 100644 --- a/service/lib/y2storage/proposal/agama_devices_planner.rb +++ b/service/lib/y2storage/proposal/agama_devices_planner.rb @@ -19,8 +19,10 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/config_checker" +require "y2storage/planned/devices_collection" require "y2storage/proposal/agama_drive_planner" -require "y2storage/planned" +require "y2storage/proposal/agama_vg_planner" module Y2Storage module Proposal @@ -28,46 +30,57 @@ module Proposal class AgamaDevicesPlanner include Yast::Logger - # Settings used to calculate the planned devices. - # - # @return [Agama::Storage::Config] - attr_reader :settings - - # @param settings [Agama::Storage::Config] + # @param devicegraph [Devicegraph] # @param issues_list [Array] - def initialize(settings, issues_list) - @settings = settings + def initialize(devicegraph, issues_list) + @devicegraph = devicegraph @issues_list = issues_list end # List of devices that need to be created to satisfy the settings. Does not include # devices needed for booting. # - # For the time being, this implements only stuff coming from partitition elements within - # drive elements. + # For the time being, this only plans for drives, partitions, and new LVM volume groups. # - # @param devicegraph [Devicegraph] + # @param config [Agama::Storage::Config] # @return [Planned::DevicesCollection] - def initial_planned_devices(devicegraph) + def planned_devices(config) + checker = Agama::Storage::ConfigChecker.new(config) + issues_list.concat(checker.issues) + # In the future this will also include planned devices that are equivalent to # those typically generated by the Guided Proposal. For those, note that: # - For dedicated VGs it creates a Planned VG containing a Planned LV, but no PVs # - For LVM volumes it create a Planned LV but associated to no planned VG # - For partition volumes, it creates a planned partition, of course - - devs = settings.drives.flat_map { |d| planned_for_drive(d, devicegraph) }.compact - Planned::DevicesCollection.new(devs) + planned = planned_drives(config) + planned_vgs(config) + Planned::DevicesCollection.new(planned) end protected + # @return [Y2Storage::Devicegraph] + attr_reader :devicegraph + # @return [Array] List to register any found issue attr_reader :issues_list - # @see #initial_planned_devices - def planned_for_drive(drive, devicegraph) - planner = AgamaDrivePlanner.new(devicegraph, issues_list) - planner.planned_devices(drive) + # @param config [Agama::Storage::Config] + # @return [Array] + def planned_drives(config) + config.drives.flat_map do |drive| + planner = AgamaDrivePlanner.new(devicegraph, issues_list) + planner.planned_devices(drive, config) + end + end + + # @param config [Agama::Storage::Config] + # @return [Array] + def planned_vgs(config) + config.volume_groups.flat_map do |vg| + planner = AgamaVgPlanner.new(devicegraph, issues_list) + planner.planned_devices(vg) + end end end end diff --git a/service/lib/y2storage/proposal/agama_drive_planner.rb b/service/lib/y2storage/proposal/agama_drive_planner.rb index 2503cb8a92..a99e573fb0 100644 --- a/service/lib/y2storage/proposal/agama_drive_planner.rb +++ b/service/lib/y2storage/proposal/agama_drive_planner.rb @@ -19,16 +19,19 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "y2storage/planned/disk" require "y2storage/proposal/agama_device_planner" module Y2Storage module Proposal # Drive planner for Agama. class AgamaDrivePlanner < AgamaDevicePlanner - # @param settings [Agama::Storage::Configs::Drive] + # @param drive_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + # # @return [Array] - def planned_devices(settings) - [planned_drive(settings)] + def planned_devices(drive_config, config) + [planned_drive(drive_config, config)] end private @@ -36,29 +39,36 @@ def planned_devices(settings) # Support for StrayBlkDevice is intentionally left out. As far as we know, the plan # for SLE/Leap 16 is to drop XEN support # - # @param settings [Agama::Storage::Configs::Drive] + # @param drive_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + # # @return [Planned::Disk] - def planned_drive(settings) - return planned_full_drive(settings) unless settings.partitions? + def planned_drive(drive_config, config) + return planned_full_drive(drive_config, config) unless drive_config.partitions? - planned_partitioned_drive(settings) + planned_partitioned_drive(drive_config, config) end - # @param settings [Agama::Storage::Configs::Drive] + # @param drive_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + # # @return [Planned::Disk] - def planned_full_drive(settings) + def planned_full_drive(drive_config, config) Planned::Disk.new.tap do |planned| - configure_reuse(planned, settings) - configure_device(planned, settings) + configure_reuse(planned, drive_config) + configure_block_device(planned, drive_config) + configure_pv(planned, drive_config, config) end end - # @param settings [Agama::Storage::Configs::Drive] + # @param drive_config [Agama::Storage::Configs::Drive] + # @param config [Agama::Storage::Config] + # # @return [Planned::Disk] - def planned_partitioned_drive(settings) + def planned_partitioned_drive(drive_config, config) Planned::Disk.new.tap do |planned| - configure_reuse(planned, settings) - configure_partitions(planned, settings) + configure_reuse(planned, drive_config) + configure_partitions(planned, drive_config, config) end end end diff --git a/service/lib/y2storage/proposal/agama_vg_planner.rb b/service/lib/y2storage/proposal/agama_vg_planner.rb new file mode 100644 index 0000000000..e510abad4f --- /dev/null +++ b/service/lib/y2storage/proposal/agama_vg_planner.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "y2storage/lv_type" +require "y2storage/proposal/agama_device_planner" + +module Y2Storage + module Proposal + # Volume group planner for Agama. + class AgamaVgPlanner < AgamaDevicePlanner + # @param config [Agama::Storage::Configs::VolumeGroup] + # @return [Array] + def planned_devices(config) + [planned_vg(config)] + end + + private + + # @param config [Agama::Storage::Configs::VolumeGroup] + # @return [Planned::LvmVg] + def planned_vg(config) + # TODO: A volume group name is expected. Otherwise, the planned physical volumes cannot + # be associated to the planned volume group. Should the volume group name be + # automatically generated if missing? + # + # @see AgamaDevicePlanner#configure_pv + Y2Storage::Planned::LvmVg.new(volume_group_name: config.name).tap do |planned| + planned.extent_size = config.extent_size + planned.lvs = planned_lvs(config) + end + end + + # @param config [Agama::Storage::Configs::VolumeGroup] + # @return [Array] + def planned_lvs(config) + normal_lvs = planned_normal_lvs(config) + thin_pool_lvs = planned_thin_pool_lvs(config) + + normal_lvs + thin_pool_lvs + end + + # @param config [Agama::Storage::Configs::VolumeGroup] + # @return [Array] + def planned_normal_lvs(config) + configs = config.logical_volumes.reject(&:pool?).reject(&:thin_volume?) + configs.map { |c| planned_lv(c, LvType::NORMAL) } + end + + # @param config [Agama::Storage::Configs::VolumeGroup] + # @return [Array] + def planned_thin_pool_lvs(config) + pool_configs = config.logical_volumes.select(&:pool?) + pool_configs.map { |c| planned_thin_pool_lv(c, config) } + end + + # Plan a thin pool logical volume and its thin volumes. + # + # @param pool_config [Agama::Storage::Configs::LogicalVolume] + # @param config [Agama::Storage::Configs::VolumeGroup] + # + # @return [Planned::LvmLv] + def planned_thin_pool_lv(pool_config, config) + planned_thin_lvs = planned_thin_lvs(config, pool_config.alias) + + planned_lv(pool_config, LvType::THIN_POOL).tap do |planned| + planned_thin_lvs.each { |v| planned.add_thin_lv(v) } + end + end + + # @param config [Agama::Storage::Configs::VolumeGroup] + # @param pool_alias [String] + # + # @return [Array] + def planned_thin_lvs(config, pool_alias) + thin_configs = config.logical_volumes + .select(&:thin_volume?) + .select { |c| c.used_pool == pool_alias } + + thin_configs.map { |c| planned_lv(c, LvType::THIN) } + end + + # @param config [Agama::Storage::Configs::LogicalVolume] + # @param type [LvType] + # + # @return [Planned::LvmLv] + def planned_lv(config, type) + Planned::LvmLv.new(nil, nil).tap do |planned| + planned.logical_volume_name = config.name + planned.lv_type = type + planned.stripes = config.stripes + planned.stripe_size = config.stripe_size + configure_block_device(planned, config) + configure_size(planned, config.size) + end + end + end + end +end diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 193b83d92e..887efd7576 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Sep 16 14:55:42 UTC 2024 - José Iván López González + +- Storage: add support for creating LVM volume groups and logical + volumes (gh#openSUSE/agama#1581). + ------------------------------------------------------------------- Tue Sep 10 10:03:04 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/test/agama/storage/config_conversions/from_json_test.rb b/service/test/agama/storage/config_conversions/from_json_test.rb index 6ad9257b57..cfe4f508bc 100644 --- a/service/test/agama/storage/config_conversions/from_json_test.rb +++ b/service/test/agama/storage/config_conversions/from_json_test.rb @@ -109,8 +109,14 @@ boot: { configure: true, device: "/dev/sdb" }, drives: [ { + alias: "first-disk", ptableType: "gpt", - partitions: [{ filesystem: { path: "/" } }] + partitions: [ + { + alias: "root", + filesystem: { path: "/" } + } + ] } ] } @@ -133,9 +139,11 @@ expect(config.drives.size).to eq 1 drive = config.drives.first expect(drive).to be_a(Agama::Storage::Configs::Drive) + expect(drive.alias).to eq "first-disk" expect(drive.ptable_type).to eq Y2Storage::PartitionTables::Type::GPT expect(drive.partitions.size).to eq 1 partition = drive.partitions.first + expect(partition.alias).to eq "root" expect(partition.filesystem.path).to eq "/" end @@ -872,5 +880,129 @@ ) end end + + context "with some LVM volume groups" do + let(:config_json) do + { + volumeGroups: [ + { + name: "vg0", + extentSize: "2 MiB", + physicalVolumes: ["alias1", "alias2"], + logicalVolumes: [ + { + name: "root", + filesystem: { path: "/" }, + encryption: { + luks2: { password: "12345" } + } + }, + { + alias: "thin-pool", + name: "pool", + pool: true, + size: "100 GiB", + stripes: 10, + stripeSize: "4 KiB" + }, + { + name: "data", + size: "50 GiB", + usedPool: "thin-pool", + filesystem: { type: "xfs" } + } + ] + }, + { + name: "vg1" + } + ] + } + end + + it "generates the corresponding volume groups and logical volumes" do + config = subject.convert + + expect(config.volume_groups).to contain_exactly( + an_object_having_attributes( + name: "vg0", + extent_size: 2.MiB, + physical_volumes: ["alias1", "alias2"] + ), + an_object_having_attributes( + name: "vg1", + extent_size: be_nil, + physical_volumes: be_empty, + logical_volumes: be_empty + ) + ) + + logical_volumes = config.volume_groups + .find { |v| v.name == "vg0" } + .logical_volumes + + expect(logical_volumes).to include( + an_object_having_attributes( + alias: be_nil, + name: "root", + encryption: have_attributes( + password: "12345", + method: Y2Storage::EncryptionMethod::LUKS2, + pbkd_function: Y2Storage::PbkdFunction::ARGON2ID + ), + filesystem: have_attributes( + path: "/", + type: have_attributes( + fs_type: Y2Storage::Filesystems::Type::BTRFS + ) + ), + size: have_attributes( + default: true, + min: 40.GiB, + max: Y2Storage::DiskSize.unlimited + ), + stripes: be_nil, + stripe_size: be_nil, + pool: false, + used_pool: be_nil + ), + an_object_having_attributes( + alias: "thin-pool", + name: "pool", + encryption: be_nil, + filesystem: be_nil, + size: have_attributes( + default: false, + min: 100.GiB, + max: 100.GiB + ), + stripes: 10, + stripe_size: 4.KiB, + pool: true, + used_pool: be_nil + ), + an_object_having_attributes( + alias: be_nil, + name: "data", + encryption: be_nil, + filesystem: have_attributes( + path: be_nil, + type: have_attributes( + fs_type: Y2Storage::Filesystems::Type::XFS + ) + ), + size: have_attributes( + default: false, + min: 50.GiB, + max: 50.GiB + ), + stripes: be_nil, + stripe_size: be_nil, + pool: false, + used_pool: "thin-pool" + ) + ) + end + end end end diff --git a/service/test/y2storage/agama_proposal_test.rb b/service/test/y2storage/agama_proposal_test.rb index ccb133968b..6f0c209a1d 100644 --- a/service/test/y2storage/agama_proposal_test.rb +++ b/service/test/y2storage/agama_proposal_test.rb @@ -22,8 +22,12 @@ require_relative "../agama/storage/storage_helpers" require "agama/config" require "agama/storage/config" +require "agama/storage/config_conversions/from_json" +require "y2storage" require "y2storage/agama_proposal" +using Y2Storage::Refinements::SizeCasts + # @param config [Agama::Storage::Configs::Drive, Agama::Storage::Configs::Partition] # @param name [String, nil] e.g., "/dev/vda" # @param filesystem [String, nil] e.g., "xfs" @@ -72,7 +76,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) describe Y2Storage::AgamaProposal do include Agama::RSpec::StorageHelpers - let(:initial_settings) do + let(:initial_config) do Agama::Storage::Config.new.tap do |settings| settings.drives = drives end @@ -95,7 +99,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end part.size = Agama::Storage::Configs::Size.new.tap do |size| - size.min = Y2Storage::DiskSize.GiB(8.5) + size.min = 8.5.GiB size.max = Y2Storage::DiskSize.unlimited end end @@ -110,7 +114,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end part.size = Agama::Storage::Configs::Size.new.tap do |size| - size.min = Y2Storage::DiskSize.GiB(10) + size.min = 10.GiB size.max = Y2Storage::DiskSize.unlimited end end @@ -121,7 +125,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end subject(:proposal) do - described_class.new(initial_settings, issues_list: issues_list) + described_class.new(initial_config, issues_list: issues_list) end let(:scenario) { "empty-hd-50GiB.yaml" } @@ -135,7 +139,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) expect(partitions.size).to eq 2 expect(partitions.first.id).to eq Y2Storage::PartitionId::BIOS_BOOT root_part = partitions.last - expect(root_part.size).to be > Y2Storage::DiskSize.GiB(49) + expect(root_part.size).to be > 49.GiB root_fs = root_part.filesystem expect(root_fs.root?).to eq true expect(root_fs.type.is?(:btrfs)).to eq true @@ -144,7 +148,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "if no boot devices should be created" do before do - initial_settings.boot = Agama::Storage::Configs::Boot.new.tap { |b| b.configure = false } + initial_config.boot = Agama::Storage::Configs::Boot.new.tap { |b| b.configure = false } end it "proposes to create only the root device" do @@ -153,7 +157,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) expect(partitions.size).to eq 1 root_part = partitions.first expect(root_part.id).to eq Y2Storage::PartitionId::LINUX - expect(root_part.size).to be > Y2Storage::DiskSize.GiB(49) + expect(root_part.size).to be > 49.GiB root_fs = root_part.filesystem expect(root_fs.root?).to eq true expect(root_fs.type.is?(:btrfs)).to eq true @@ -304,7 +308,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end - context "if the encryption method is not available for this system" do + context "if the encryption method is not suitable" do let(:encryption_method) { Y2Storage::EncryptionMethod::RANDOM_SWAP } it "aborts the proposal process" do @@ -585,7 +589,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "if deleting the partition is not needed" do before do - root_partition.size.min = Y2Storage::DiskSize.GiB(15) + root_partition.size.min = 15.GiB end it "does not delete the partition" do @@ -601,7 +605,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "if the partition has to be deleted" do before do - root_partition.size.min = Y2Storage::DiskSize.GiB(20) + root_partition.size.min = 20.GiB end it "deletes the partition" do @@ -730,7 +734,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) drive_config.tap { |c| c.partitions = [partition] } end - let(:partition) { partition_config(filesystem: "ext3", size: Y2Storage::DiskSize.GiB(1)) } + let(:partition) { partition_config(filesystem: "ext3", size: 1.GiB) } context "if trying to reuse the file system" do before do @@ -760,5 +764,273 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end end + + context "when the config has LVM volume groups" do + let(:scenario) { "empty-hd-50GiB.yaml" } + + let(:initial_config) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json, product_config: product_config) + .convert + end + + let(:product_config) { Agama::Config.new } + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + alias: "system-pv", + size: "40 GiB" + }, + { + alias: "vg1-pv", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["system-pv"], + logicalVolumes: [ + { + name: "root", + size: "10 GiB", + filesystem: { + path: "/", + type: "btrfs" + }, + encryption: { + luks2: { password: "12345" } + } + }, + { + alias: "system-pool", + name: "pool", + pool: true, + size: "20 GiB", + stripes: 10, + stripeSize: "4 KiB" + }, + { + name: "data", + size: "50 GiB", + usedPool: "system-pool", + filesystem: { type: "xfs" } + } + ] + }, + { + name: "vg1", + physicalVolumes: ["vg1-pv"], + logicalVolumes: [ + { + name: "home", + filesystem: { + path: "/home", + type: "xfs" + } + } + ] + } + ] + } + end + + it "proposes the expected devices" do + devicegraph = proposal.propose + + expect(devicegraph.lvm_vgs).to contain_exactly( + an_object_having_attributes( + vg_name: "system", + extent_size: 2.MiB + ), + an_object_having_attributes( + vg_name: "vg1", + extent_size: 4.MiB + ) + ) + + system_vg = devicegraph.find_by_name("/dev/system") + system_pvs = system_vg.lvm_pvs.map(&:plain_blk_device) + system_lvs = system_vg.lvm_lvs + expect(system_pvs).to contain_exactly( + an_object_having_attributes(name: "/dev/sda2", size: 40.GiB) + ) + expect(system_lvs).to contain_exactly( + an_object_having_attributes( + lv_name: "root", + lv_type: Y2Storage::LvType::NORMAL, + size: 10.GiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::BTRFS, + mount_path: "/" + ), + encryption: an_object_having_attributes( + type: Y2Storage::EncryptionType::LUKS2, + password: "12345" + ) + ), + an_object_having_attributes( + lv_name: "pool", + lv_type: Y2Storage::LvType::THIN_POOL, + size: 20.GiB, + filesystem: be_nil, + encryption: be_nil, + stripes: 10, + stripe_size: 4.KiB, + lvm_lvs: contain_exactly( + an_object_having_attributes( + lv_name: "data", + lv_type: Y2Storage::LvType::THIN, + size: 50.GiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::XFS + ) + ) + ) + ) + ) + + vg1 = devicegraph.find_by_name("/dev/vg1") + vg1_pvs = vg1.lvm_pvs.map(&:plain_blk_device) + vg1_lvs = vg1.lvm_lvs + expect(vg1_pvs).to contain_exactly( + an_object_having_attributes(name: "/dev/sda3", size: 5.GiB) + ) + expect(vg1_lvs).to contain_exactly( + an_object_having_attributes( + lv_name: "home", + lv_type: Y2Storage::LvType::NORMAL, + size: 5.GiB - 4.MiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::XFS, + mount_path: "/home" + ) + ) + ) + end + end + + context "when a LVM physical volume is not found" do + let(:initial_config) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json, product_config: product_config) + .convert + end + + let(:product_config) { Agama::Config.new } + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + size: "40 GiB" + }, + { + alias: "pv1", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["pv1", "pv2"], + logicalVolumes: [ + { + name: "root", + filesystem: { + path: "/" + } + } + ] + } + ] + } + end + + it "aborts the proposal process" do + proposal.propose + expect(proposal.failed?).to eq true + end + + it "reports the corresponding error" do + proposal.propose + expect(proposal.issues_list).to include an_object_having_attributes( + description: /no LVM physical volume with alias pv2/, + severity: Agama::Issue::Severity::ERROR + ) + end + end + + context "when a LVM thin pool volume is not found" do + let(:initial_config) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json, product_config: product_config) + .convert + end + + let(:product_config) { Agama::Config.new } + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + size: "40 GiB" + }, + { + alias: "pv1", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["pv1"], + logicalVolumes: [ + { + pool: true + }, + { + name: "root", + filesystem: { + path: "/" + }, + usedPool: "pool" + } + ] + } + ] + } + end + + it "aborts the proposal process" do + proposal.propose + expect(proposal.failed?).to eq true + end + + it "reports the corresponding error" do + proposal.propose + expect(proposal.issues_list).to include an_object_having_attributes( + description: /no LVM thin pool volume with alias pool/, + severity: Agama::Issue::Severity::ERROR + ) + end + end end end