yast/yast-storage-ng

View on GitHub
src/lib/y2partitioner/actions/controllers/fstabs.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2018-2019] 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 "yast/i18n"
require "y2partitioner/device_graphs"
require "y2partitioner/filesystems"
require "y2partitioner/actions/controllers/base"
require "y2storage"

Yast.import "Arch"

module Y2Partitioner
  module Actions
    module Controllers
      # This class stores information about the fstab files read from all
      # the filesystem in the system. It also saves information about the
      # selected fstab to be used to import mount points.
      class Fstabs < Base
        include Yast::I18n

        # @return [Y2Storage::Fstab] fstab file selected to import mount points
        attr_reader :selected_fstab

        # @return [Boolean] whether the system volumes must be formatted
        #
        # @note A volume is considered as "system volume" when it is mounted in certain
        #   specific mount point like /, /usr, etc. See {#system_mount_points}.
        attr_accessor :format_system_volumes
        alias_method :format_system_volumes?, :format_system_volumes

        # Constructor
        def initialize
          super()
          textdomain "storage"
        end

        # All fstab files found in the system
        #
        # @return [Array<Y2Storage::Fstab>]
        def fstabs
          disk_analyzer.fstabs
        end

        # Sets the selected fstab
        #
        # Note that the system graph with crypttab names need to be reset after selecting a new fstab.
        #
        # @param [Y2Storage::Fstab] new_fstab
        def selected_fstab=(new_fstab)
          reset_system_graph_with_crypttab_names

          @selected_fstab = new_fstab
        end

        # Selects the previous fstab
        #
        # The current selected fstab does not change if it already is the first one.
        #
        # @return [Y2Storage::Fstab]
        def select_prev_fstab
          current_index = fstabs.index(selected_fstab)
          prev_index = [0, current_index - 1].max

          self.selected_fstab = fstabs.at(prev_index)
        end

        # Selects the next fstab
        #
        # The current selected fstab does not change if it already is the last one.
        #
        # @return [Y2Storage::Fstab]
        def select_next_fstab
          current_index = fstabs.index(selected_fstab)
          next_index = [fstabs.size - 1, current_index + 1].min

          self.selected_fstab = fstabs.at(next_index)
        end

        # Checks whether the selected fstab is the first one
        #
        # @return [Boolean]
        def selected_first_fstab?
          selected_fstab == fstabs.first
        end

        # Checks whether the selected fstab is the last one
        #
        # @return [Boolean]
        def selected_last_fstab?
          selected_fstab == fstabs.last
        end

        # Errors in the selected fstab
        #
        # @see #not_importable_entries_error
        #
        # @return [Array<String>]
        def selected_fstab_errors
          [not_importable_entries_error].compact
        end

        # Imports mount points from the selected fstab
        #
        # Before importing, the current devicegraph is reset to the system one.
        def import_mount_points
          reset_current_graph
          importable_entries.each { |e| import_mount_point(e) }
          Y2Storage::Filesystems::Btrfs.refresh_subvolumes_shadowing(current_graph)
        end

        private

        # System mount points are taken from old code, see
        # https://github.com/yast/yast-storage/blob/master_old/src/modules/FileSystems.rb#L438
        SYSTEM_MOUNT_POINTS = ["/", "/usr", "/var", "/opt", "/boot"].freeze
        private_constant :SYSTEM_MOUNT_POINTS

        # Error when some entries in the selected fstab cannot be imported
        #
        # An entry cannot be imported when the device is not found or it is used
        # by other device (e.g., used by LVM or MD RAID).
        #
        # @return [String, nil] nil if all entries can be imported
        def not_importable_entries_error
          entries = not_importable_entries
          return nil if entries.empty?

          mount_points = entries.map(&:mount_point).join("\n")

          # TRANSLATORS: %{mount_points} is replaced by a list of mount points, please
          # do not modify it.
          format(_("The following mount points cannot be imported:\n%{mount_points}"),
            mount_points: mount_points)
        end

        # Entries in the current selected fstab that can be imported
        #
        # @see #can_be_imported?
        #
        # @return[Array<Y2Storage::SimpleEtcFstabEntry>]
        def importable_entries
          selected_fstab.filesystem_entries.select { |e| can_be_imported?(e) }
        end

        # Entries in the current selected fstab that cannot be imported
        #
        # @see #can_be_imported?
        #
        # @return[Array<Y2Storage::SimpleEtcFstabEntry>]
        def not_importable_entries
          selected_fstab.filesystem_entries - importable_entries
        end

        # Whether a fstab entry can be imported
        #
        # An entry can be imported when the device is known and it is not used
        # by other device (e.g., used by LVM or MD RAID), or it is a known NFS.
        # Moreover, in case the device must be formatted, the entry also must
        # indicate a known filesystem type.
        #
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        # @return [Boolean]
        def can_be_imported?(entry)
          device = entry.device(system_graph_with_crypttab_names)

          return false unless device

          return true unless must_be_formatted?(device, entry.mount_point)

          usable_fs_type?(entry) && can_be_formatted?(device)
        end

        # Whether the device must be formatted in order to import the mount point
        #
        # A device must be formatted when it is not currently formatted or it is a device
        # that should be mounted over a system mount point (see #{system_mount_points}).
        # For this last case, the option to format system volumes should be selected
        # (see {#format_system_volumes?}).
        #
        # @param device [Y2Storage::BlkDevice, Y2Storage::Filesystems::Base]
        # @param mount_point [String] mount point of fstab entry
        #
        # @return [Boolean]
        def must_be_formatted?(device, mount_point)
          # In case the device is a filesystem (i.e., NFS), the device should not be formatted.
          return false if device.is?(:filesystem)

          !device.formatted? ||
            (system_mount_point?(mount_point) && format_system_volumes?)
        end

        # Whether the mount point is included in the list of system mount points
        #
        # @param mount_point [String] mount point of fstab entry
        # @return [Boolean]
        def system_mount_point?(mount_point)
          system_mount_points.include?(mount_point)
        end

        # Mount points considered as system mount points
        #
        # The list of system mount points are taken from old code, see
        # https://github.com/yast/yast-storage/blob/master_old/src/modules/FileSystems.rb#L438
        #
        # @return [Array<String>]
        def system_mount_points
          return @system_mount_points if @system_mount_points

          @system_mount_points = SYSTEM_MOUNT_POINTS.dup
          @system_mount_points << "/boot/zipl" if Yast::Arch.s390
          @system_mount_points
        end

        # Whether a fstab entry has an usable filesystem type
        #
        # A filesystem type is usable when it is known and supported.
        #
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        # @return [Boolean]
        def usable_fs_type?(entry)
          known_fs_type?(entry) && supported_fs_type?(entry)
        end

        # Whether a fstab entry has a known filesystem type
        #
        # In case the fstab entry contains "auto" or "none" in the third
        # field (fs_vfstype), the filesystem type cannot be determined.
        #
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        # @return [Boolean]
        def known_fs_type?(entry)
          !entry.fs_type.is?(:auto) && !entry.fs_type.is?(:unknown)
        end

        # Whether a fstab entry has a filesystem type supported by the Partitioner
        #
        # Only some filesystem types are supported by Partitioner, see {Filesystems.all}.
        #
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        # @return [Boolean]
        def supported_fs_type?(entry)
          Filesystems.supported?(entry.fs_type)
        end

        # Whether a device can be formatted
        #
        # A device can be formatted if it is already formatted or it is not used by
        # another device (e.g., LVM or MD RAID). Moreover, in case of a encryption
        # device, the device must be active.
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def can_be_formatted?(device)
          return false if device.is?(:encryption) && !device.active?

          unused?(device) || device.formatted?
        end

        # Whether the device has not been used yet
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def unused?(device)
          device.descendants.empty?
        end

        # Initializes current devicegraph with system
        def reset_current_graph
          DeviceGraphs.instance.current = system_graph.dup
        end

        # Resets the system graph containing crypttab names
        def reset_system_graph_with_crypttab_names
          @system_graph_with_crypttab_names = nil
        end

        # Imports the mount point of a fstab entry
        #
        # When the device needs to be formatted (see {#must_be_formatted?}), the filesystem type
        # indicated in the entry is used. In case the device is not a block device (e.g., NFS),
        # the device is not formatted and only the mount point, mount by method and mount options
        # are assigned.
        #
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        def import_mount_point(entry)
          device = entry.device(system_graph_with_crypttab_names)
          return unless device

          device = device_from_current_graph(device)

          if must_be_formatted?(device, entry.mount_point)
            filesystem = format_device(device, entry)
            create_mount_point(filesystem, entry)
            setup_blk_filesystem(filesystem)
          else
            filesystem = device.is?(:filesystem) ? device : device.filesystem
            create_mount_point(filesystem, entry)
          end
        end

        # Device version from the current devicegraph
        #
        # @param device [Y2Storage::Device]
        # @return [Y2Storage::Device]
        def device_from_current_graph(device)
          if missing_swap_encryption?(device)
            copy_swap_encryption(device)
          else
            current_graph.find_device(device.sid)
          end
        end

        # Copies a plain encryption for swap into the current devicegraph
        #
        # @param device [Y2Storage::Encryption]
        # @return [Y2Storage::Encryption]
        def copy_swap_encryption(device)
          blk_device = device_from_current_graph(device.blk_device)
          blk_device.remove_descendants

          device.copy_to(current_graph)
        end

        # Formats the device indicated in the fstab entry
        #
        # The filesystem label is preserved.
        #
        # @param device [Y2Storage::BlkDevice]
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        #
        # @return [Y2Storage::Filesystems::Base]
        def format_device(device, entry)
          label = filesystem_label(device)

          device.delete_filesystem

          filesystem = device.create_filesystem(entry.fs_type)
          filesystem.label = label if label

          filesystem
        end

        # Label of the current filesystem (if any)
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [String, nil] nil if the device is not formatted
        def filesystem_label(device)
          return nil unless device.formatted?

          device.filesystem.label
        end

        # Creates the #{Y2Storage::MountPoint} object based on the imported
        # fstab entry.
        #
        # @param filesystem [Y2Storage::Filesystems::Base]
        # @param entry [Y2Storage::SimpleEtcFstabEntry]
        def create_mount_point(filesystem, entry)
          filesystem.mount_path = entry.mount_point
          filesystem.mount_point.mount_by = entry.mount_by if entry.mount_by
          filesystem.mount_point.mount_options = entry.mount_options if entry.mount_options.any?
        end

        # Performs any additional final step needed for the new block filesystem (Btrfs stuff, so far)
        #
        # @param filesystem [Y2Storage::Filesystems::BlkFilesystem]
        def setup_blk_filesystem(filesystem)
          add_filesystem_devices(filesystem)

          if filesystem.can_configure_snapper?
            filesystem.configure_snapper = filesystem.default_configure_snapper?
          end

          filesystem.setup_default_btrfs_subvolumes if filesystem.supports_btrfs_subvolumes?
        end

        # Adds missing devices to the filesystem when the original filesystem was multidevice
        #
        # @param filesystem [Y2Storage::Filesystems::BlkFilesystem]
        def add_filesystem_devices(filesystem)
          original_filesystem = original_filesystem(filesystem)

          return unless add_devices?(filesystem)

          devices = original_filesystem.blk_devices.map { |d| current_graph.find_device(d.sid) }.compact

          devices.each { |d| add_filesystem_device(filesystem, d) }
        end

        # Whether more devices should be added to a multi-device filesystem
        #
        # @param filesystem [Y2Storage::Filesystems::BlkFilesystem]
        def add_devices?(filesystem)
          original_filesystem = original_filesystem(filesystem)

          return false unless original_filesystem&.multidevice?

          # FIXME: check the fstab entry UUID instead
          filesystem.type == original_filesystem&.type
        end

        # Adds a device to the filesystem
        #
        # @param filesystem [Y2Storage::Filesystems::BlkFilesystem]
        # @param device [Y2Storage::BlkDevice]
        def add_filesystem_device(filesystem, device)
          return unless filesystem.respond_to?(:add_device)
          return if filesystem.blk_devices.map(&:sid).include?(device.sid)

          filesystem.add_device(device)
        end

        # Original version of the filesystem in the system graph
        #
        # @param filesystem [Y2Storage::Filesystems::BlkFilesystem]
        # @return [Y2Storage::Filesystems::BlkFilesystem, nil] nil if the filesystem cannot be found in
        #   system graph.
        def original_filesystem(filesystem)
          original_device = system_device(filesystem.blk_devices.first)

          return nil unless original_device&.formatted?

          original_device.filesystem
        end

        # System graph with information about crypttab names indicated in the selected crypttab file
        #
        # When the device name in a fstab entry corresponds to a encryption device, the device could be
        # not found by its fstab name. In general, encryptions might be probed with a different name, so
        # before searching for the device, the encryption names from the crypttab file are saved into the
        # proper encryption device. Also note that a random encryption layer can be created when a
        # crypttab entry points to a swap device encrypted with random password. All these changes are
        # made in a temporary devicegraph, so the original system graph is never altered.
        #
        # @see #add_crypttab_names_to
        #
        # @return [Y2Storage::Devicegraph]
        def system_graph_with_crypttab_names
          @system_graph_with_crypttab_names ||= add_crypttab_names_to(system_graph)
        end

        # Duplicates the given devicegraph and saves the encryption names from the crypttab file
        #
        # @see #crypttab
        #
        # @param devicegraph [Y2Storage::Devicegraph]
        # @return [Y2Storage::Devicegraph]
        def add_crypttab_names_to(devicegraph)
          fixed_devicegraph = devicegraph.dup

          return fixed_devicegraph unless crypttab

          crypttab.save_encryption_names(fixed_devicegraph)
          fixed_devicegraph
        end

        # Selects the crypttab contained in the same filesystem than the selected fstab
        #
        # @return [Y2Storage::Crypttab, nil] nil if the filesystem does not contain a crypttab file.
        def crypttab
          disk_analyzer.crypttabs.find { |c| c.filesystem == selected_fstab.filesystem }
        end

        # Whether the given device represents a not probed encryption generated by a swap encryption
        # method
        #
        # Note that no headers are written into the device when using plain encryption (which is the
        # underlying technology used for randomly encrypted swaps). For this reason, plain encryption
        # devices are only probed for the root filesystem by parsing its crypttab file.
        #
        # A plain encryption device might be created when searching for a device from a fstab entry, see
        # {Y2Storage::Crypttab.save_encryption_names}.
        #
        # @param device [Y2Storage::Device]
        # @return [Boolean]
        def missing_swap_encryption?(device)
          missing_device?(device) && swap_encryption?(device)
        end

        # Whether the given device is missing in the probed devicegraph
        #
        # @param device [Y2Storage::Device]
        # @return [Boolean]
        def missing_device?(device)
          !device.exists_in_devicegraph?(system_graph)
        end

        # Whether the given device is an encryption generated by a swap encryption method
        #
        # @param device [Y2Storage::Encryption]
        # @return [Boolean]
        def swap_encryption?(device)
          device.is?(:encryption) && device.method.only_for_swap?
        end
      end
    end
  end
end