yast/yast-storage-ng

View on GitHub
src/lib/y2storage/filesystems/blk_filesystem.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017-2020] 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/i18n"
require "yast2/execute"
require "y2storage/storage_class_wrapper"
require "y2storage/filesystems/base"
require "y2storage/volume_specification"

module Y2Storage
  module Filesystems
    # A local filesystem.
    #
    # This is a wrapper for Storage::BlkFilesystem
    class BlkFilesystem < Base
      include Yast::I18n

      wrap_class Storage::BlkFilesystem, downcast_to: ["Filesystems::Btrfs"]

      # Binary of the command uuidgen. See #{init_uuid}.
      UUIDGEN = "/usr/bin/uuidgen".freeze
      private_constant :UUIDGEN

      # @!method self.all(devicegraph)
      #   @param devicegraph [Devicegraph]
      #   @return [Array<Filesystems::BlkFilesystem>] all the block filesystems
      #     in the given devicegraph
      storage_class_forward :all, as: "Filesystems::BlkFilesystem"

      # @!method supports_label?
      #   @return [Boolean] whether the filesystem supports having a label
      storage_forward :supports_label?, to: :supports_label

      # @!method max_labelsize
      #   @return [Integer] max size of the label
      storage_forward :max_labelsize

      # @!attribute label
      #   @return [String] filesystem label
      storage_forward :label
      storage_forward :label=

      # @!method supports_uuid?
      #   @return [Boolean] whether the filesystem supports UUID
      storage_forward :supports_uuid?, to: :supports_uuid

      # @!attribute uuid
      #   @return [String] filesystem UUID
      storage_forward :uuid
      storage_forward :uuid=

      # @!method supports_shrink?
      #   @return [Boolean] whether the filesystem supports shrinking
      storage_forward :supports_shrink?, to: :supports_shrink

      # @!method supports_mounted_shrink?
      #   @return [Boolean] whether the filesystem supports shrinking while being mounted
      storage_forward :supports_mounted_shrink?, to: :supports_mounted_shrink

      # @!method supports_grow?
      #   @return [Boolean] whether the filesystem supports growing
      storage_forward :supports_grow?, to: :supports_grow

      # @!method supports_mounted_grow?
      #   @return [Boolean] whether the filesystem supports growing while being mounted
      storage_forward :supports_mounted_grow?, to: :supports_mounted_grow

      # @!attribute mkfs_options
      #   Options to use when calling mkfs during devicegraph commit (if the
      #   filesystem needs to be created in the system).
      #
      #   @return [String]
      storage_forward :mkfs_options
      storage_forward :mkfs_options=

      # @!attribute tune_options
      #   @return [String]
      storage_forward :tune_options
      storage_forward :tune_options=

      # @!method detect_content_info
      #   @return [Storage::ContentInfo]
      storage_forward :detect_content_info

      # @!method blk_devices
      #   Formatted block devices. It returns the block devices directly hosting
      #   the filesystem. That is, for encrypted filesystems it returns the
      #   encryption devices.
      #
      #   In most cases, this collection will contain just one element, since
      #   most filesystems sit on top of just one block device.
      #   But that's not necessarily true for Btrfs or for filesystems in which
      #   the journal or the metadata is stored in a separate device.
      #   For the Btrfs case, see
      #   https://btrfs.wiki.kernel.org/index.php/Using_Btrfs_with_Multiple_Devices
      #   For the extX case, see the journal options at mkfs.ext4 documentation.
      #
      #   @return [Array<BlkDevice>]
      storage_forward :blk_devices, as: "BlkDevice"

      # Raw (non encrypted) version of the formatted devices. If the filesystem
      # is not encrypted, it returns the same collection that #blk_devices,
      # otherwise it returns the original devices instead of the encryption
      # ones.
      #
      # @return [Array<BlkDevice>]
      def plain_blk_devices
        blk_devices.map(&:plain_device)
      end

      # Block device base name
      #
      # When the filesystem is single-device, this method simply returns the base name of the block
      # device (e.g., "sda1"). And for multi-device ones, it returns the first base name between brackets
      # and followed by horizontal ellipsis (e.g., "(sda1...)").
      #
      # @return [String]
      def blk_device_basename
        basename = plain_blk_devices.map(&:basename).min

        return basename unless multidevice?

        textdomain "storage"

        # TRANSLATORS: block device basename for a multi-device filesystem, where %{basename} is replaced
        # by the basename of the first block device (e.g., "(sda1...)").
        format(_("(%{basename}\u2026)"), basename: basename)
      end

      # Display name to represent the filesystem
      #
      # Only multidevice filesystems have a display name
      #
      # @return [String, nil]
      def display_name
        return nil unless multidevice?

        # FIXME: wrapper classes should not provide strings to be presented in the UI. Use decorators.
        textdomain "storage"

        format(
          # TRANSLATORS: display name when is a multidevice, where %{fs_type} is replaced by the
          #   filesystem type (e.g., BtrFS), %{num_devices} is replaced by the number of devices
          #   (e.g., "2") and %{device_name} is replaced by a device representation (e.g., "(sda1...)").
          #
          #   Example: "BtrFS over 2 devices (sda1...)"
          _("%{fs_type} over %{num_devices} devices %{device_name}"),
          fs_type:     type.to_human_string,
          num_devices: blk_devices.size,
          device_name: blk_device_basename
        )
      end

      # Name used to identify the filesystem
      #
      # @return [String]
      def name
        # FIXME: wrapper classes should not provide strings to be presented in the UI. Use decorators.
        textdomain "storage"

        format(
          # TRANSLATORS: name used to identify a filesystem, where %{fs_type} is replaced by the
          #   filesystem type (e.g., BtrFS) and %{device_name} is replaced by a device representation
          #   (e.g., "sda1", "(sda1...)").
          #
          #   Examples: "BtrFS (sda1...)", "Ext4 sda2"
          _("%{fs_type} %{device_name}"),
          fs_type:     type.to_human_string,
          device_name: blk_device_basename
        )
      end

      # Whether it is a multi-device filesystem
      #
      # So far, filesystems detected as multi-device are
      #
      #   - Btrfs, when built over several devices
      #   - Ext3/4, if the journal is placed in an external device
      #
      # @return [Boolean]
      def multidevice?
        blk_devices.size > 1
      end

      # Returns the device used to hold the journal
      #
      # Mainly useful for Ext3/4 filesystems with an external journal
      #
      # @return [BlkDevice, nil] nil if there is not a device used for the journal
      def journal_device
        blk_devices.find(&:journal?)
      end

      # @return [Boolean]
      def in_network?
        blk_devices.any?(&:in_network?)
      end

      # @see BlkDevice#systemd_remote?
      #
      # @return [Boolean]
      def systemd_remote?
        blk_devices.any?(&:systemd_remote?)
      end

      # Option used in the fstab file for devices that require network
      NETWORK_OPTION = "_netdev".freeze
      private_constant :NETWORK_OPTION

      # @see Mountable#missing_mount_options
      #
      # Detecting missing or unwanted occurrences of _netdev is implemented in BlkFilesystem so far.
      #  - Fully network-based filesystems like NFS do not need it because systemd always detect
      #    those right, without the need of _netdev.
      #  - We don't specify extra options for Btrfs subvolumes because the current libstorage-ng
      #    implementation would ignore them (BtrfsSubvolume#mount_options is bypassed to only return
      #    subvol=$path).
      #
      # @return [Array<String>]
      def missing_mount_options
        # Adding _netdev is implemented in BlkFilesystem so far.
        if needs_network_mount_options? && included_network_mount_options.empty?
          (super + [NETWORK_OPTION]).uniq
        else
          super
        end
      end

      # @see Mountable#unwanted_mount_options
      #
      # See note about _netdev at {#missing_mount_options}.
      #
      # @return [Array<String>]
      def unwanted_mount_options
        if needs_network_mount_options?
          super
        else
          (super + included_network_mount_options).uniq
        end
      end

      # @see Base#stable_name?
      def stable_name?
        blk_devices.all?(&:stable_name?)
      end

      # @see Base#encrypted?
      def encrypted?
        plain_blk_devices.any?(&:encrypted?)
      end

      # @see Base#encrypted?
      def volatile?
        blk_dev = blk_devices.first
        return false unless blk_dev.is?(:encryption)

        enc_method = blk_dev.method
        return false unless enc_method

        enc_method.only_for_swap?
      end

      # Checks if this filesystem type supports any kind of resize at all,
      # either shrinking or growing.
      #
      # @return [Boolean]
      def supports_resize?
        supports_shrink? || supports_grow?
      end

      # @see Filesystems::Base#match_fstab_spec?
      def match_fstab_spec?(spec)
        if /^UUID=(['"]?)(.*)\1$/ =~ spec
          return !Regexp.last_match(2).empty? && uuid == Regexp.last_match(2)
        end

        if /^LABEL=(['"]?)(.*)\1$/ =~ spec
          return !Regexp.last_match(2).empty? && label == Regexp.last_match(2)
        end

        named_device = devicegraph.find_by_any_name(spec)
        blk_devices.include?(named_device)
      end

      # Whether it makes sense modify the attribute about snapper configuration
      #
      # @see Y2Storage::Filesystems::Btrfs.configure_snapper
      #
      # @return [Boolean]
      def can_configure_snapper?
        root? && respond_to?(:configure_snapper=)
      end

      # Volume specification that applies for this filesystem
      #
      # @see Y2Storage::VolumeSpecification.for
      #
      # @return [Y2Storage::VolumeSpecification, nil] nil if no specification
      #   matches the filesystem
      def volume_specification
        return nil unless mount_point

        Y2Storage::VolumeSpecification.for(mount_point.path)
      end

      # If the current UUID is blank, generates a valid one if possible
      #
      # Enforcing the presence of an UUID that is already known for each swap in the
      # devicegraph (before committing changes) is convenient for other installer
      # proposals, like the bootloader one (see bug#1177926, bug#1169874 and
      # jsc#SLE-17081).
      #
      # Due to libstorage-ng limitations in the commit phase, this only works for
      # filesystems of type swap.
      #
      # Apart from the mentioned limitation, it's not always possible to generate a
      # valid uuid in all systems. Thus, executing this method does not guarantee
      # a not blank value for {#uuid}.
      def init_uuid
        # In libstorage-ng, setting the uuid for a newly created filesystem only works in
        # the swap case (which is the only BlkFilesystem type which implements the set_uuid
        # action). It produces an error in any other case
        return unless type.is?(:swap)
        return unless uuid.empty?

        self.uuid = uuidgen
      end

      # Most convenient file path to reference the filesystem
      #
      # If possible, the path is chosen based on the {#mount_by} attribute of the filesystem.
      # If the filesystem is not mounted or the path for the specified mount_by cannot be
      # calculated from the information present in the devicegraph, an alternative name
      # based on {Filesystems::MountByType.best_for} (which already takes
      # {Configuration#default_mount_by} into account) is calculated.
      #
      # This method always return a valid full-path filename that can be inferred from the
      # information already available in the devicegraph
      #
      # @return [String]
      def preferred_name
        path_for_mount_by(preferred_mount_by)
      end

      # File path to reference the filesystem based on the current mount by option
      #
      # @see #mount_by
      #
      # @return [String, nil] nil if the name cannot be determined for the current mount by option
      def mount_by_name
        return nil unless mount_by

        path_for_mount_by(mount_by)
      end

      # Name (full path) that can be used to reference the filesystem for the given mount by option
      #
      # @return [String, nil] nil if the name cannot be determined for the given mount by option
      def path_for_mount_by(mount_by)
        if mount_by.is?(:label, :uuid)
          attr_value = public_send(mount_by.to_sym)
          mount_by.udev_name(attr_value)
        else
          blk_devices.first.path_for_mount_by(mount_by)
        end
      end

      protected

      # Whether the network-related mount options (e.g. _netdev) should be part
      # of the adjusted mount options
      #
      # @return [Boolean]
      def needs_network_mount_options?
        # Adding "_netdev" and similar options in fstab for /var, for / or for any mount point that
        # is hosted in the same disk than / should not be necessary and it confuses systemd.
        # See bsc#1165937, bsc#176140 and jsc#SLE-20535
        return false if disk_with_mount_point?(&:mounted_by_init?)
        return false if disk_with_mount_point? { |mp| mp.path == "/var" }

        systemd_remote?
      end

      # Network-related mount options (e.g. _netdev) in the current {#mount_options}
      #
      # @return [Array<String>]
      def included_network_mount_options
        mount_options.select { |opt| opt.casecmp(NETWORK_OPTION) == 0 }
      end

      # @see Device#is?
      def types_for_is
        super << :blk_filesystem
      end

      # Executes the uuidgen command and returns the generated UUID
      #
      # @return [String] empty string if there was any problem executing the command
      def uuidgen
        Yast::Execute.locally!(UUIDGEN, stdout: :capture).chomp
      rescue Cheetah::ExecutionFailed
        ""
      end

      # Most convenient mount_by option to reference the filesystem
      #
      # @see #preferred_name
      #
      # This method always returns an option that can be safely used by
      # {#path_for_mount_by} to construct a valid filename.
      #
      # @return [Filesystems::MountByType]
      def preferred_mount_by
        mount_bys = with_mount_point { |mp| mp.suitable_mount_bys(assume_uuid: false) }
        return mount_by if mount_bys.include?(mount_by)

        Filesystems::MountByType.best_for(self, mount_bys)
      end
    end
  end
end