yast/yast-storage-ng

View on GitHub
src/lib/y2storage/volume_specification.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017-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 "yast"
require "y2storage/partitioning_features"
require "y2storage/subvol_specification"
require "y2storage/equal_by_instance_variables"

Yast.import "Kernel"

module Y2Storage
  # Helper class to represent a volume specification as defined in control.xml
  class VolumeSpecification
    include PartitioningFeatures
    include EqualByInstanceVariables

    # @return [PartitionId] when the volume needs to be a partition with a specific id
    attr_accessor :partition_id

    # @return [String] directory where the volume will be mounted in the system
    attr_accessor :mount_point

    # @return [String] mount options, separated by comma
    attr_accessor :mount_options

    # @return [Boolean] whether this volume should be created or skipped
    attr_accessor :proposed

    # @return [Boolean] whether the user can change the proposed setting in the UI
    attr_accessor :proposed_configurable

    # @return [Filesystems::Type] default file system type to format the volume
    attr_reader :fs_type

    # @return [List<Filesystems::Type>] acceptable filesystem types
    attr_reader :fs_types

    # @return [DiskSize] initial size to use in the first proposal attempt
    attr_accessor :desired_size

    # @return [DiskSize] initial size to use in the second proposal attempt
    attr_accessor :min_size

    # @return [DiskSize] maximum size to assign to the volume
    attr_accessor :max_size

    # @return [DiskSize] technical size limit; a volume larger than this is not usable
    attr_accessor :max_size_limit

    # @return [DiskSize] when LVM is used, this option can be used to override
    #   the value at max_size
    attr_accessor :max_size_lvm

    # @return [Numeric] value used to distribute the extra space (after assigning
    #   the initial ones) among the volumes
    attr_accessor :weight

    # @return [Boolean] whether the initial and max sizes of each attempt should be
    #   adjusted based in the RAM size
    attr_accessor :adjust_by_ram

    # @return [Boolean] whether the user can change the adjust_by_ram setting in the UI
    attr_accessor :adjust_by_ram_configurable

    # @return [String] mount point of another volume
    attr_accessor :fallback_for_min_size

    # @return [String] mount point of another volume
    attr_accessor :fallback_for_desired_size

    # @return [String] mount point of another volume
    attr_accessor :fallback_for_max_size

    # @return [String] mount point of another volume
    attr_accessor :fallback_for_max_size_lvm

    # @return [String] mount point of another volume
    attr_accessor :fallback_for_weight

    # @return [Boolean] whether snapshots should be activated
    attr_accessor :snapshots

    # @return [Boolean] whether the user can change the snapshots setting in the UI
    attr_accessor :snapshots_configurable

    # @note snaphots_size and snapshots_percentage are exclusive in the control file.
    # @return [DiskSize] the initial and maximum sizes for the volume will be
    #   increased according if snapshots are being used.
    attr_accessor :snapshots_size

    # @note snaphots_size and snapshots_percentage are exclusive in the control file.
    # @return [Integer] the initial and maximum sizes for the volume will be
    #   increased according if snapshots are being used. It represents a percentage
    #   of the original sizes.
    attr_accessor :snapshots_percentage

    # @return [Array<SubvolSpecification>] list of specifications (usually read
    #   from the control file) that will be used to plan the Btrfs subvolumes
    attr_accessor :subvolumes

    # @return [String] default btrfs subvolume path
    attr_accessor :btrfs_default_subvolume

    # @return [Boolean] whether the volume should be mounted as read-only
    attr_accessor :btrfs_read_only

    # @return [Numeric] order to disable volumes if needed to make the initial proposal
    attr_accessor :disable_order

    # Name of a separate LVM volume group that will be created to host only this volume,
    # if the option separate_vgs is active in the settings
    #
    # Only one PV will be created to back the volume group, unlike the default
    # "system" volume group that may be defined on top of several physical
    # volumes if needed.
    #
    # In the future we may consider to break both aspects in different settings.
    # #vg_name to specify the volume group name (with "system" as default) and
    # #isolated_vg to enforce just one PV for a particular volume group.
    #
    # If that ever happens, separate_vg_name=foo would become some kind of alias
    # for vg_name=foo + isolated_vg=true.
    #
    # @return [String]
    attr_accessor :separate_vg_name

    # Optional device name of the disk (DiskDevice to be precise) in which the volume
    # must be located.
    #
    # @return [String, nil]
    attr_accessor :device

    # Name of an existing device that will be used to allocate the filesystem described by
    # this volume.
    #
    # If this field is used, no new device will be created. As a consequence, many of the other
    # attributes (eg. those about sizes and weights) could be ignored.
    #
    # @return [String]
    attr_accessor :reuse_name

    # Whether a reused device should be formatted.
    #
    # If set to false, the existing filesystem should be kept.
    #
    # Only relevant if #reuse_name points to an existing device
    #
    # @return [Boolean]
    attr_accessor :reformat

    # Whether to ignore the fact that this volume is the fallback for the sizes of other volumes
    # (ie. is referenced at any #fallback_for_min_size, #fallback_for_desired_size,
    # #fallback_for_max_size or #fallback_for_max_size_lvm).
    #
    # @return [Boolean] true to indicate the absence of other volumes will not affect the size
    #   calculation of this one
    attr_accessor :ignore_fallback_sizes

    # Whether to ignore any possible effect on the size derived from (de)activating snapshots.
    #
    # @return [Boolean] true if #snapshots_size and #snapshots_percentage should be ignored
    attr_accessor :ignore_snapshots_sizes

    # Whether to ignore any possible effect on the size derived from RAM size
    #
    # @return [Boolean] true if #adjust_by_ram should be ignored
    attr_accessor :ignore_adjust_by_ram

    alias_method :proposed?, :proposed
    alias_method :proposed_configurable?, :proposed_configurable
    alias_method :adjust_by_ram?, :adjust_by_ram
    alias_method :adjust_by_ram_configurable?, :adjust_by_ram_configurable
    alias_method :ignore_adjust_by_ram?, :ignore_adjust_by_ram
    alias_method :snapshots?, :snapshots
    alias_method :snapshots_configurable?, :snapshots_configurable
    alias_method :ignore_snapshots_sizes?, :ignore_snapshots_sizes
    alias_method :ignore_fallback_sizes?, :ignore_fallback_sizes
    alias_method :btrfs_read_only?, :btrfs_read_only
    alias_method :reformat?, :reformat

    class << self
      # Returns the volume specification for the given mount point
      #
      # This method keeps a cache of already calculated volume specifications.
      # Call {.clear_cache} method in order to clear it. Beware that the cache
      # does not take into account that different proposal settings are being
      # used.
      #
      # @param mount_point       [String] Volume's mount point
      # @param proposal_settings [ProposalSettings] Proposal settings
      # @return [VolumeSpecification,nil] Volume specification or nil if not found
      def for(mount_point, proposal_settings: nil)
        clear_cache unless @cache
        @cache[mount_point] ||= VolumeSpecificationBuilder.new(proposal_settings).for(mount_point)
      end

      # Clear volume specifications cache
      def clear_cache
        @cache = {}
      end
    end

    # Constructor
    #
    # @param volume_features [Hash] features for a volume
    def initialize(volume_features)
      apply_defaults
      load_features(volume_features)
      adjust_features
    end

    # @see #fs_type
    #
    # @param type [Filesystems::Type, String]
    def fs_type=(type)
      @fs_type = validated_fs_type(type)
    end

    # @param types [Array<String>, String] an array of filesystem types or a
    #   list of comma-separated ones
    def fs_types=(types)
      types = types.strip.split(/\s*,\s*/) if types.is_a?(String)
      @fs_types = types.map { |t| validated_fs_type(t) }
    end

    # Whether the user can configure some aspect of the volume
    #
    # Returns false if there is no chance for the volume to be proposed or if
    # none of its attributes can be configured by the user.
    #
    # @return [Boolean]
    def configurable?
      return false if !proposed && !proposed_configurable?

      proposed_configurable? ||
        adjust_by_ram_configurable? ||
        snapshots_configurable? ||
        fs_type_configurable?
    end

    # Checks whether #fs_type can be configured by the user
    #
    # @return [Boolean]
    def fs_type_configurable?
      fs_types.size > 1
    end

    # Whether this volume is expected to reuse an existing device
    #
    # @return [Boolean]
    def reuse?
      !(reuse_name.nil? || reuse_name.empty?)
    end

    # Whether the resulting device will be mounted as root
    #
    # @return [Boolean]
    def root?
      mount_point && mount_point == "/"
    end

    # Whether the resulting device will be mounted as swap
    #
    # @return [Boolean]
    def swap?
      mount_point && mount_point == "swap"
    end

    # Whether this volume defines a {#separate_vg_name}
    #
    # @return [Boolean]
    def separate_vg?
      !!separate_vg_name
    end

    # Min size taking into account snapshots requirements
    #
    # @note If there are no special size requirements for snapshots, the
    #   min size is returned.
    #
    # @return [Y2Storage::DiskSize]
    def min_size_with_snapshots
      if snapshots_size > DiskSize.zero
        min_size + snapshots_size
      elsif snapshots_percentage > 0
        multiplicator = 1.0 + (snapshots_percentage / 100.0)
        min_size * multiplicator
      else
        min_size
      end
    end

    # Whether snapper configuration should be activated by default when applying
    # this specification to a given block device
    #
    # @param device [Y2Storage::BlkDevice]
    # @return [Boolean]
    def snapper_for_device?(device)
      if snapshots
        if snapshots_configurable # maybe check also disable_order
          device.size >= min_size_with_snapshots
        else
          true
        end
      else
        false
      end
    end

    # Whether it makes sense to enlarge the volume to suspend
    #
    # This only makes sense when the volume is for swap and the architecture supports to resume from
    # swap.
    #
    # @return [Boolean]
    def enlarge_for_resume_supported?
      swap? && resume_supported?
    end

    private

    FEATURES = {
      mount_point:                :string,
      mount_options:              :string,
      proposed:                   :boolean,
      proposed_configurable:      :boolean,
      fs_types:                   :list,
      fs_type:                    :string,
      adjust_by_ram:              :boolean,
      adjust_by_ram_configurable: :boolean,
      fallback_for_min_size:      :string,
      fallback_for_desired_size:  :string,
      fallback_for_max_size:      :string,
      fallback_for_max_size_lvm:  :string,
      fallback_for_weight:        :string,
      snapshots:                  :boolean,
      snapshots_configurable:     :boolean,
      btrfs_default_subvolume:    :string,
      btrfs_read_only:            :boolean,
      desired_size:               :size,
      min_size:                   :size,
      max_size:                   :size,
      max_size_limit:             :size,
      max_size_lvm:               :size,
      snapshots_size:             :size,
      snapshots_percentage:       :integer,
      weight:                     :integer,
      disable_order:              :integer,
      separate_vg_name:           :string,
      subvolumes:                 :subvolumes
    }.freeze

    private_constant :FEATURES

    def apply_defaults
      @proposed                   = true
      @proposed_configurable      = false
      @desired_size               = DiskSize.zero
      @min_size                   = DiskSize.zero
      @max_size                   = DiskSize.unlimited
      @max_size_limit             = DiskSize.unlimited
      @max_size_lvm               = DiskSize.zero
      @weight                     = 0
      @adjust_by_ram              = false
      @adjust_by_ram_configurable = false
      @snapshots                  = false
      @snapshots_configurable     = false
      @snapshots_size             = DiskSize.zero
      @snapshots_percentage       = 0
      @fs_types                   = []
      @ignore_fallback_sizes      = false
      @ignore_snapshots_sizes     = false
      @ignore_adjust_by_ram       = false
      @reformat                   = true
    end

    # For some features (i.e., fs_types and subvolumes) fallback values could be applied
    # @param volume_features [Hash] features for a volume
    def load_features(volume_features)
      FEATURES.each do |feature, type|
        type = nil if [:string, :boolean, :list].include?(type)
        loader = type.nil? ? "load_feature" : "load_#{type}_feature"
        send(loader, feature, source: volume_features)
      end

      apply_fallbacks
    end

    # Adjusts some features that need to be forced to certain value
    #
    # For example, {#adjust_by_ram} should be set to `false` by default for the swap partition when
    # the architecture does not support to resume from swap (i.e., for s390).
    def adjust_features
      self.adjust_by_ram = false if swap? && !resume_supported?
    end

    def validated_fs_type(type)
      raise(ArgumentError, "Filesystem cannot be nil") unless type
      return type if type.is_a?(Filesystems::Type)

      Filesystems::Type.find(type.downcase.to_sym)
    end

    def apply_fallbacks
      apply_subvolumes_fallback
      apply_fs_types_fallback
    end

    # If subvolumes is missing, a hard-coded list is used for root. If the section is
    # there but empty, no subvolumes are created.
    def apply_subvolumes_fallback
      return unless subvolumes.nil?

      @subvolumes = root? ? SubvolSpecification.fallback_list : []
    end

    # If fs_types is empty, a hard-coded list is used for root and home.
    #
    # @note It always includes fs_type in the list.
    def apply_fs_types_fallback
      if fs_types.empty?
        @fs_types = Filesystems::Type.root_filesystems if mount_point == "/"
        @fs_types = Filesystems::Type.home_filesystems if mount_point == "/home"
      end

      include_fs_type
    end

    # Adds fs_type to the list of possible filesystems
    def include_fs_type
      @fs_types.unshift(fs_type) if fs_type && !fs_types.include?(fs_type)
    end

    # Whether hibernation feature is considered as supported
    #
    # Resuming from swap can be considered as unsupported because of different reasons. For example,
    # because such feature is actually not supported for the current architecture (e.g., s390, Power) or
    # because it does not make much sense to offer such feature (e.g., virtual machines). Moreover, some
    # products can be explicitly configured (with a control file option) to offer hibernation or not.
    #
    # @return [Boolean]
    def resume_supported?
      Yast::Kernel.propose_hibernation?
    end
  end
end