From c3ac9e85a9a40d4af1ef9a066b6c9423a7a3a6a9 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Wed, 19 Jul 2023 13:53:40 +0200 Subject: [PATCH] WIP: draft for getting the volume template for a path --- service/lib/agama/storage/btrfs_settings.rb | 9 +- service/lib/agama/storage/proposal.rb | 17 +- service/lib/agama/storage/volume.rb | 45 +---- service/lib/agama/storage/volume_outline.rb | 63 ++----- .../agama/storage/volume_templates_builder.rb | 154 ++++++++++++++++++ 5 files changed, 188 insertions(+), 100 deletions(-) create mode 100644 service/lib/agama/storage/volume_templates_builder.rb diff --git a/service/lib/agama/storage/btrfs_settings.rb b/service/lib/agama/storage/btrfs_settings.rb index 52298cc779..e254432257 100644 --- a/service/lib/agama/storage/btrfs_settings.rb +++ b/service/lib/agama/storage/btrfs_settings.rb @@ -31,7 +31,8 @@ class BtrfsSettings # @return [Boolean] attr_accessor :read_only - # @return [Array] + # @return [Array, nil] if nil, a historical fallback list may be applied depending + # on the mount path of the volume attr_accessor :subvolumes # @return [String] @@ -40,11 +41,7 @@ class BtrfsSettings def initialize @snapshots = false @read_only = false - @subvolumes = [] - @default_subvolume = "@" - end - - def load_features(values) + @default_subvolume = "" end end end diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index db115ec727..457eb5790f 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -97,12 +97,9 @@ def candidate_devices # Volume definitions to be used as templates in the interface and to fill the # information missing in the volumes. # - # @return [Array] - def volume_templates - return @volume_templates if @volume_templates - - config_volumes = config.data.fetch("storage", {}).fetch("volumes", []) - Volume.read(config_volumes) + # @return [] + def volume_template(path) + volume_templates_builder.for(path) end # Settings with the data used during the calculation of the storage proposal @@ -277,6 +274,14 @@ def force_cleanup(settings) # Setting #linux_delete_mode to :all is not enough to prevent VG reusing in all cases settings.lvm_vg_reuse = false end + + def volume_templates_builder + @volume_templates_builder ||= VolumeTemplatesBuilder.new(volume_templates_config) + end + + def volume_templates_config + config.data.fetch("storage", {}).fetch("volume_templates", []) + end end end end diff --git a/service/lib/agama/storage/volume.rb b/service/lib/agama/storage/volume.rb index 335d283e10..0bd6fc482d 100644 --- a/service/lib/agama/storage/volume.rb +++ b/service/lib/agama/storage/volume.rb @@ -87,9 +87,12 @@ class Volume alias_method :auto_size?, :auto_size # Constructor - def initialize(values) - apply_defaults - load_features(values) + def initialize(mount_path) + @mount_path = mount_path + @mount_options = [] + @format_options = [] + @btrfs = BtrfsSettings.new + @outline = VolumeOutline.new end # Whether the mount point of the volume matches the given one @@ -101,42 +104,6 @@ def mounted_at?(path) Pathname.new(mount_point).cleanpath == Pathname.new(path).cleanpath end - - def self.read(volumes_data) - volumes = volumes_data.map { |v| Volume.new(v) } - volumes.each { |v| v.outline.assign_size_relevant_volumes(v, volumes) } - volumes - end - - private - - def apply_defaults - @mount_options = [] - @format_options = [] - @btrfs = BtrfsSettings.new - @outline = VolumeOutline.new - end - - def load_features(values) - @mount_path = values.fetch("mount", {}).fetch("path") - # @mount_options = xxx - # @format_options = xxx - - type_str = values.fetch("filesystem", {}).fetch("type", "ext4") - @fs_type = Y2Storage::Filesystems::Type.find(type.downcase.to_sym) - - btrfs.load_features(values) - outline.load_features(values) - - # TODO: part of this logic should likely be moved elsewhere (auto_size setter?) - if outline.adaptative_sizes? - @auto_size = true - else - @auto_size = false - @min_size = outline.base_min_size - @max_size = outline.base_max_size - end - end end end end diff --git a/service/lib/agama/storage/volume_outline.rb b/service/lib/agama/storage/volume_outline.rb index bc693eb4e5..450b1da8fd 100644 --- a/service/lib/agama/storage/volume_outline.rb +++ b/service/lib/agama/storage/volume_outline.rb @@ -25,19 +25,19 @@ module Agama module Storage # Set of rules and features used to fully define and validate a given volume class VolumeOutline - # Whether the volume is optional + # Whether the volume is mandatory # - # If this is false, the list of volumes used by the storage proposal will always contain a + # If this is true, the list of volumes used by the storage proposal will always contain # this volume or an equivalent one (ie. one with the same mount_path). # # @return [Boolean] - attr_reader :optional - alias_method :optional?, :optional + attr_reader :required + alias_method :required?, :required # Possible filesystem types for the volume # # @return [Array] - attr_reader :fs_types + attr_reader :filesystems # Base value to calculate the min size for the volume (if #auto_size is set to true # for that final volume) or to use as default value (if #auto_size is false) @@ -61,11 +61,11 @@ class VolumeOutline attr_reader :adjust_by_ram alias_method :adjust_by_ram?, :adjust_by_ram - # @return [String] mount point of another volume - attr_accessor :fallback_for_min_size + # @return [Array] mount paths of other volumes + attr_accessor :min_size_fallback_for - # @return [String] mount point of another volume - attr_accessor :fallback_for_max_size + # @return [Array] mount paths of other volumes + attr_accessor :max_size_fallback_for # Whether snapshots option can be configured # @@ -84,39 +84,16 @@ class VolumeOutline attr_reader :snapshots_percentage def initialize - @optional = false - @fs_types = [] + @filesystems = [] @base_min_size = Y2Storage::DiskSize.zero @base_max_size = Y2Storage::DiskSize.Unlimited @size_relevant_volumes = [] - @adjust_by_ram = false - @fallback_for_max_size = "" - @fallback_for_min_size = "" - @snapshots_configurable = false + @max_size_fallback_for = [] + @min_size_fallback_for = [] end - def load_features(values) - size = values.fetch("size", {}) - min = size["min"] - max = size["max"] - @base_min_size = DiskSize.parse(min, legacy_units: true) if min - @base_max_size = DiskSize.parse(max, legacy_units: true) if max - - # @optional - # @fs_types - # @adjust_by_ram - # @fallback_for_max_size - # @fallback_for_min_size - # @snapshots_configurable - # @snapshots_size - # @snapshots_percentage - end - - # Sets the mount points that affects the sizes of the volume - def assign_size_relevant_volumes(volume, other_volumes) - # FIXME: this should be a responsibility of the Proposal (since it's calculated by - # Proposal::DevicesPlanner) - @size_relevant_volumes = other_volumes.select { |v| fallback?(volume, v) }.map(&:mount_path) + def size_relevant_volumes + (max_size_fallbacks_for + min_size_fallbacks_for).sort.uniq end # Whether it makes sense to have automatic size limits for the volume @@ -140,18 +117,6 @@ def snapshots_affect_sizes? snapshots_percentage && !snapshots_percentage.zero? end - - private - - # Whether the given volume outline has this volume as fallback for sizes - # - # @param volume [Volume] the volume of this outline - # @param other [Volume] - # @return [Boolean] - def fallback?(volume, other) - volume.mounted_at?(other.outline.fallback_for_min_size) || - volume.mounted_at?(other.outline.fallback_for_max_size) - end end end end diff --git a/service/lib/agama/storage/volume_templates_builder.rb b/service/lib/agama/storage/volume_templates_builder.rb new file mode 100644 index 0000000000..c0a2825e7b --- /dev/null +++ b/service/lib/agama/storage/volume_templates_builder.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "pathname" +require "yast" +require "y2storage" + +module Agama + module Storage + class VolumeTemplatesBuilder + def initialize(config_data) + @data = {} + + # TODO: maybe add an entry for "" if no default is provided? + config_data.each do |volume_data| + @data[key(volume_data)] = values(volume_data) + end + end + + def for(path) + data = @data[cleanpath(path)] || @data[""] + + Volume.new(path).tap do |vol| + vol.btrfs = data[:btrfs] + vol.outline = data[:outline] + vol.filesystem = data[:filesystem] + # vol.mount_options = xxx + # vol.format_options = xxx + + if data[:auto_size] && vol.outline.adaptative_sizes? + vol.auto_size = true + else + vol.auto_size = false + vol.min_size = data[:min_size] if data[:min_size] + vol.max_size = data[:max_size] if data[:max_size] + end + end + end + + private + + def key(data) + path = data["mount_path"] + return "" unless path + + cleanpath(path) + end + + def values(data) + Hash.new.tap do |values| + values[:btrfs] = btrfs(data) + values[:outline] = outline(data) + + # TODO: maybe ensure consistency of values[:filesystem] and values[:outline].filesystems ? + fs = data["filesystem"] + values[:filesystem] = Y2Storage::Filesystems::Type.find(fs.downcase.to_sym) if fs + values[:filesystem] ||= values[:outline].filesystems.first + values[:filesystem] ||= Y2Storage::Filesystems::Type::EXT4 + + # values[:mount_options] = xxx + # values[:format_options] = xxx + + size = outline_data.fetch("size", {}) + values[:auto_size] = size.fetch("auto", false) + values[:min_size] = parse_disksize(size["min"]) + values[:max_size] = parse_disksize(size["max"]) + end + end + + def btrfs(data) + btrfs_data = data.fetch("btrfs", {}) + BtrfsSettings.new.tap do |btrfs| + btrfs.snapshots = btrfs_data.fetch("snapshots", false) + btrfs.read_only = btrfs_data.fetch("read_only", false) + btrfs.default_subvolume = btrfs_data.fetch("default_subvolume", "") + btrfs.subvolumes = btrfs_data["subvolumes"] + if btrfs.subvolumes + btrfs.subvolumes.map! { |subvol_data| subvolume(subvol_data) } + end + end + end + + def subvolume(data) + return Y2Storage::SubvolSpecification.new(data) if data.kind_of?(String) + + Y2Storage::SubvolSpecification.new( + data["path"], copy_on_write: data["copy_on_write"], archs: data["archs"] + ) + end + + def outline(data) + outline_data = data.fetch("outline", {}) + VolumeOutline.new.tap do |outline| + outline.required = outline_data.fetch("required", false) + outline.filesystems = outline_data.fetch("filesystems", []) + outline.filesystems.map! { |fs| Y2Storage::Filesystems::Type.find(fs.downcase.to_sym) } + outline.snapshots_configurable = outline_data.fetch("snapshots_configurable", true) + + size = outline_data.fetch("auto_size", {}) + min = parse_disksize(size["min"]) + max = parse_disksize(size["max"]) + outline.base_min_size = min if min + outline.base_max_size = max if max + outline.adjust_by_ram = size.fetch("adjust_by_ram", false) + outline.min_size_fallback_for = Array(size["min_fallback_for"]) + outline.min_size_fallback_for.map! { |p| cleanpath(p) } + outline.max_size_fallback_for = Array(size["max_fallback_for"]) + outline.max_size_fallback_for.map! { |p| cleanpath(p) } + + + assign_snapshots_increment(outline, size["snapshots_increment"]) + end + end + + def assign_snapshots_increment(outline, increment) + return if increment.nil + + if increment =~ /(\d+)\s*%/ + outline.snapshots_percentage = $1.to_i + else + outline.snapshots_size = Y2Storage::DiskSize.parse(increment, legacy_units: true) + end + end + + def parse_disksize(value) + return nil unless value + + Y2Storage::DiskSize.parse_or(value, nil, legacy_units: true) + end + + def cleanpath(path) + Pathname.new(path).cleanpath + end + end + end +end