yast/yast-storage-ng

View on GitHub
src/lib/y2storage/filesystems/base.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 "tempfile"

require "y2storage/storage_class_wrapper"
require "y2storage/mountable"
require "y2storage/filesystems/type"
require "y2storage/filesystem_reader"
require "y2storage/fstab"
require "y2storage/crypttab"

module Y2Storage
  module Filesystems
    # Abstract class to represent a filesystem, either a local (BlkFilesystem) or
    # a network one, like NFS.
    #
    # This is a wrapper for Storage::Filesystem
    class Base < Mountable
      wrap_class Storage::Filesystem,
        downcast_to: ["Filesystems::BlkFilesystem", "Filesystems::Nfs", "Filesystems::Tmpfs"]

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

      # @!method type
      #   @return [Filesystems::Type]
      storage_forward :type, as: "Filesystems::Type"

      # @!method detect_space_info
      #   Information about the free space on a device.
      #
      #   The filesystem has to exists on the disk (i.e., in the probed
      #   devicegraph), this will mount it and then call the "df" command.
      #   Since both operations are expensive, caching this value is advised if
      #   it is needed repeatedly.
      #
      #   @raise [Storage::Exception] if the filesystem couldn't be mounted
      #     (e.g. it does not exist in the system or mount command failed)
      #
      #   @return [SpaceInfo]
      storage_forward :detect_space_info, as: "SpaceInfo"

      # Smart detection of free space
      #
      # It tries to use detect_space_info and caches it. But if it fails, it tries
      # to compute it from resize_info. If it fails again or filesystem is
      # not a block filesystem, then it returns zero size.
      #
      # @return [DiskSize]
      def free_space
        return @free_space if @free_space

        begin
          @free_space = detect_space_info.free
        rescue Storage::Exception
          # ok, we do not know it, so we try to detect ourself
          @free_space = compute_free_space
        end
      end

      # @return [Boolean]
      def in_network?
        false
      end

      # Checks whether the filesystem has the capability of hosting Btrfs subvolumes
      #
      # @return [Boolean] it only should be true for Btrfs.
      def supports_btrfs_subvolumes?
        false
      end

      # Whether the filesystem supports having a label
      #
      # @return [Boolean]
      def supports_label?
        false
      end

      # @see Mountable#extra_default_mount_options
      #
      # @return [Array<String>]
      def extra_default_mount_options
        if mount_point
          (super + type.default_fstab_options(mount_path)).uniq
        else
          super
        end
      end

      # Whether the kernel name used to reference the filesystem (that is, the
      # one used when mounting via {Filesystems::MountByType::Device) is stable
      # and remains equal across system reboots
      #
      # @return [Boolean]
      def stable_name?
        true
      end

      # Whether the filesystem sits directly on top of an encrypted device
      #
      # @return [Boolean]
      def encrypted?
        false
      end

      # Whether the content of the filesystem is lost after each system reboot
      #
      # @return [Boolean] true for swap devices encrypted with volatile keys
      def volatile?
        false
      end

      # Whether the current filesystem matches with a given fstab spec
      #
      # Most formats supported in the first column of /etc/fstab are recognized.
      # E.g. the string can be a kernel name, an udev name, an NFS specification
      # or a string starting with "UUID=" or "LABEL=".
      #
      # This method doesn't match by PARTUUID or PARTLABEL.
      #
      # Take into account that libstorage-ng discards during probing all the
      # udev names not considered reliable or stable enough. This method only
      # checks by the udev names recognized by libstorage-ng (not discarded).
      #
      # @param spec [String] content of the first column of an /etc/fstab entry
      # @return [Boolean]
      def match_fstab_spec?(spec)
        log.warn "Method of the base abstract class used to check #{spec}"
        false
      end

      # Whether the filesystem is suitable for a root filesystem
      #
      # @see Filesystems::Type#root_ok?
      #
      # @return [Boolean]
      def root_suitable?
        type.root_ok?
      end

      # Whether the filesystem is suitable for a Windows system
      #
      # @see Filesystems::Type#windows_ok?
      # @see BlkDevice#windows_suitable?
      #
      # @return [Boolean]
      def windows_suitable?
        type.windows_ok? && blk_devices.first.windows_suitable?
      end

      # Whether the filesystem contains a Windows system
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Boolean]
      def windows_system?
        return false unless windows_suitable?

        # For BitLocker assume it is Windows without looking further
        # at the file system (it cannot be mounted anyway).
        return true if type.to_sym == :bitlocker

        !!fs_attribute(:windows)
      end

      # Whether the filesystem contains a Linux system
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Boolean]
      def linux_system?
        !release_name.nil?
      end

      # Name of the system allocated by the filesystem
      #
      # For a Windows system it simply returns "Windows" as system name. For Linux it tries to read the
      # release name.
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [String, nil] nil if the system cannot be detected
      def system_name
        windows_system? ? "Windows" : release_name
      end

      # Release name of Linux system allocated by the filesystem
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [String, nil] nil if the system cannot be detected
      def release_name
        fs_attribute(:release_name)
      end

      # Whether the filesystem contains the Raspberry Pi boot code in the root path
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Boolean]
      def rpi_boot?
        !!fs_attribute(:rpi_boot)
      end

      # Whether the filesystem contains the directory layout of an ESP partition
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Boolean]
      def efi?
        !!fs_attribute(:efi)
      end

      # Retrieves the fstab from the filesystem
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Y2Storage::Fstab, nil] nil if the fstab file is not found
      def fstab
        @fstab ||= etc_file("fstab")
      end

      # Retrieves the crypttab from the filesystem
      #
      # Note that the filesystem might be mounted when requested for first time.
      #
      # @return [Y2Storage::Crypttab, nil] nil if the crypttab file is not found
      def crypttab
        @crypttab ||= etc_file("crypttab")
      end

      protected

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

      # Value to return as fallback when the free space cannot be computed
      FREE_SPACE_FALLBACK = DiskSize.zero

      def compute_free_space
        # e.g. nfs where blk_devices cannot be queried
        return FREE_SPACE_FALLBACK unless respond_to?(:blk_devices)

        size = blk_devices.map(&:size).reduce(:+)
        used = resize_info.min_size
        size - used
      rescue Storage::Exception
        # it is questionable if this is correct behavior when resize_info failed,
        # but there is high chance we can't use it with libstorage, so better act like zero device.
        FREE_SPACE_FALLBACK
      end

      # Filesystem attribute obtained after mounting the filesystem
      #
      # Note that the filesystem is only mounted the first time that an attribute is requested.
      #
      # @param attr [Symbol] :windows, :release_name, :rpi_boot, :efi, :fstab, :crypttab
      # @return [Object] attribute value
      def fs_attribute(attr)
        read_fs_attributes unless userdata_value(:fs_attributes_already_read)

        userdata_value(attr)
      end

      # Reads and saves attributes from a probed filesystem
      #
      # It requires to mount the filesystem.
      #
      # @see Y2Storage::FilesystemReader
      def read_fs_attributes
        save_userdata(:fs_attributes_already_read, true)

        return unless exists_in_probed?

        reader = FilesystemReader.new(self)

        save_userdata(:windows, reader.windows?)
        save_userdata(:release_name, reader.release_name)
        save_userdata(:rpi_boot, reader.rpi_boot?)
        save_userdata(:efi, reader.efi?)
        save_userdata(:fstab, reader.fstab)
        save_userdata(:crypttab, reader.crypttab)
      end

      # Generates an etc file object
      #
      # Note that a temporary file is created with the content of the etc file because libstorage-ng API
      # requires a file path, see {Y2Storage::Fstab} and {Y2Storage::Crypttab}.
      #
      # @param file_name [String] "fstab" or "crypttab"
      # @return [Y2Storage::Fstab, Y2Storage::Crypttab, nil] nil if the file is not found
      def etc_file(file_name)
        file_content = fs_attribute(file_name.to_sym)

        return nil if file_content.nil?

        file_object = nil

        Tempfile.open("yast-storage-ng") do |file|
          file.write(file_content)
          file.rewind

          case file_name
          when "fstab"
            file_object = Fstab.new(file.path, self)
          when "crypttab"
            file_object = Crypttab.new(file.path, self)
          end
        end

        file_object
      end
    end
  end
end