src/lib/y2storage/mount_point.rb
# Copyright (c) [2018-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 "y2storage/storage_class_wrapper"
require "y2storage/device"
require "y2storage/encryption"
require "y2storage/encryption_type"
require "y2storage/filesystems/mount_by_type"
require "pathname"
module Y2Storage
# Class to represent a mount point
#
# This is a wrapper for Storage::MountPoint
class MountPoint < Device
wrap_class Storage::MountPoint
# @return [Pathname] Object that represents the root path
ROOT_PATH = Pathname.new("/").freeze
# @return [Pathname] Object that represents the ESP path
ESP_PATH = Pathname.new("/boot/efi").freeze
# @return [Pathname] Object that represents the swap path
SWAP_PATH = Pathname.new("swap").freeze
# @return [Symbol] Filesystem types which should use some value
# (other than 0) in the fs_passno field
TYPES_WITH_PASSNO = [:ext2, :ext3, :ext4, :jfs].freeze
private_constant :TYPES_WITH_PASSNO
# @!method self.all(devicegraph)
# @param devicegraph [Devicegraph]
#
# @return [Array<MountPoint>] all mount points in the devicegraph
storage_class_forward :all, as: "MountPoint"
# @!method self.find_by_path(devicegraph, path)
# @param devicegraph [Devicegraph]
# @param path [String] path of the mount point. See {#path}
#
# @return [Array<MountPoint>]
storage_class_forward :find_by_path, as: "MountPoint"
# @!method path
# @return [String]
storage_forward :path
storage_forward :storage_path=, to: :path=
private :storage_path=
storage_forward :storage_default_mount_options, to: :default_mount_options
private :storage_default_mount_options
# Sets the value for {#path} and ensures {#passno} has a value consistent
# with the new path
#
# @param path [String]
# @raise [Storage::InvalidMountPointPath] if trying to set an invalid path
def path=(path)
self.storage_path = path
to_storage_value.passno =
if passno_must_be_set?
root? ? 1 : 2
else
0
end
Y2Storage::Encryption.update_dm_names(devicegraph)
end
# @!method mount_by
# The way the "mount" command identifies the mountable
#
# This defines the form of the first field in the fstab file.
#
# The concrete meaning depends on the value. Note that some types
# address the filesystem while others address the underlying device.
#
# * DEVICE: For NFS, the server and path. For regular filesystems, the
# kernel device name or a link in /dev (but not in /dev/disk) of the
# block device that contains the filesystem.
# * UUID: The UUID of the filesystem.
# * LABEL: the label of the filesystem.
# * ID: one of the links in /dev/disk/by-id to the block device
# containing the filesystem.
# * PATH: one of the links in /dev/disk/by-path to the block device
# containing the filesystem.
#
# Not to be confused with {Encryption#mount_by}, which refers to the form
# of the crypttab file.
#
# @return [Filesystems::MountByType]
storage_forward :mount_by, as: "Filesystems::MountByType"
# @!method assign_mount_by
# Low level setter to enforce a value for {#mount_by} without performing
# any consistency fix, like updating {#manual_mount_by?} or syncing the
# Btrfs subvolumes
#
# @see #mount_by=
storage_forward :assign_mount_by, to: :mount_by=
# Setter for {#mount_by} which ensures a consistent value for
# {#manual_mount_by?} and for the corresponding attribute of the Btrfs
# subvolumes (if applicable)
#
# @param value [Filesystems::MountByType]
def mount_by=(value)
self.manual_mount_by = true
assign_mount_by(value)
return unless mountable.respond_to?(:copy_mount_by_to_subvolumes)
mountable.copy_mount_by_to_subvolumes
end
# @!method mount_options
# Options to use in /etc/fstab for a newly created mount point.
#
# @note This returns an array based on the underlying SWIG vector,
# modifying the returned object will have no effect in the MountPoint
# object. Use #mount_options= to actually change the value. See examples.
#
# @example This will not modify the options
# mount_point.mount_options << "ro"
# mount_point.mount_options # "ro" was not added
#
# @example This will work as expected
# mount_point.mount_options = mount_point.mount_options + ["ro"]
# mount_point.mount_options # "ro" was added
#
# @return [Array<String>]
storage_forward :mount_options
# Sets mount options
#
# @note Avoid overriding the subvolume option for btrfs subvolumes unless
# you are certain what you are doing.
#
# @param options [Array<String>]
def mount_options=(options)
to_storage_value.mount_options.clear
options&.each { |o| to_storage_value.mount_options << o }
mountable.adjust_crypt_options
mount_options
end
# Adjusts the mount options as needed to avoid problems during booting
#
# See jsc#SLE-20535, bsc#1176140, bsc#1165937 and jsc#SLE-7687
def adjust_mount_options
self.mount_options = mount_options + missing_mount_options - unwanted_mount_options
end
# @see Mountable#missing_mount_options
def missing_mount_options
mountable.missing_mount_options
end
# @see Mountable#unwanted_mount_options
def unwanted_mount_options
mountable.unwanted_mount_options
end
# @!method set_default_mount_by
# Set the mount-by method to the global default, see Storage::get_default_mount_by()
storage_forward :set_default_mount_by, to: :default_mount_by=
# @!method storage_possible_mount_bys
# Returns the possible mount-by methods for the mount point.
# LABEL is included even if the filesystem label is not set.
#
# @return [Array<Filesystems::MountByType>]
storage_forward :storage_possible_mount_bys, as: "Filesystems::MountByType",
to: :possible_mount_bys
private :storage_possible_mount_bys
# redefine possible_mount_bys to filter out unsupported values
# @see #storage_possible_mount_bys
def possible_mount_bys
storage_possible_mount_bys & Filesystems::MountByType.all
end
# @!attribute mount_type
# Filesystem type used to mount the device, as specified in fstab and/or
# in the mount command.
#
# @return [Filesystems::Type]
storage_forward :mount_type, as: "Filesystems::Type"
storage_forward :mount_type=
# @!method in_etc_fstab?
# Whether the mount point is present (probed devicegraph) or
# will be present (staging devicegraph) in /etc/fstab
#
# @return [Boolean]
storage_forward :in_etc_fstab?
# @!method in_etc_fstab=(value)
# Whether the mount point will be present in /etc/fstab
#
# @param value [Boolean]
storage_forward :in_etc_fstab=
# @!method active?
# Whether the mount point is mounted (probed devicegraph) or
# should be mounted (staging devicegraph)
#
# @return [Boolean]
storage_forward :active?
# @!method active=(value)
#
# Sets the {#active?} flag
#
# @param value [Boolean]
storage_forward :active=
# @!method immediate_deactivate
# Immediately deactivates (unmount) the mount point object. In
# contrast to {#active=} this function acts immediately and does
# not require calling to commit.
#
# @note The mount point object must exist in the probed devicegraph.
#
# @raise [Storage::Exception] when it cannot be unmounted.
storage_forward :immediate_deactivate
# @!method mountable
# Gets the mountable of the mount point (filesystem, BTRFS subvolume, etc)
#
# @return [Mountable]
storage_forward :mountable, as: "Mountable", check_with: :has_mountable
# @!method filesystem
# Gets the filesystem of the mount point
#
# @return [Filesystems::Base]
storage_forward :filesystem, as: "Filesystems::Base"
# @!method passno
# Value for the fs_passno field for fstab(5). The passno field is used by
# the fsck(8) program to determine the order in which filesystem checks
# are done at reboot time.
#
# @return [Integer]
storage_forward :passno
# @!method freq
# Value for the fs_freq field for fstab(5). The freq field is used by the
# dump(8) command to determine which filesystems need to be dumped. The
# field is likely obsolete.
#
# @return [Integer]
storage_forward :freq
# Whether the mount point is root
#
# @return [Boolean]
def root?
path == ROOT_PATH.to_s
end
# Whether the mount point is the ESP
#
# @return [Boolean]
def esp?
path == ESP_PATH.to_s
end
# @see Device#in_etc?
# @see #in_etc_fstab
def in_etc?
in_etc_fstab?
end
# Whether the given path is equivalent to {#path}
#
# This method is more robust than a simple string comparison, since it takes
# into account trailing slashes and similar potential problems.
#
# @param other_path [String, Pathname]
# @return [Boolean]
def path?(other_path)
Pathname.new(other_path).cleanpath == Pathname.new(path).cleanpath
end
# List of mount-by methods that make sense for the mount point
#
# Using a value that is not suitable would lead to libstorage-ng ignoring
# that value during the commit phase. In such case, DEVICE is used by the
# library as fallback.
#
# @param label [Boolean, nil] whether the associated filesystem has a label.
# If set to nil, that is checked in the devicegraph. If set to true, it
# will assume the filesystem has a label. If set to false, it will assume
# there is no label, no matter what the devicegraph says.
# @param encryption [Boolean, nil] whether the filesystem sits on top of an
# encrypted device. Regarding the possible values (nil, true and false) it
# behaves like the label argument.
# @param assume_uuid [Boolean] whether it can be safely assumed that the
# filesystem has a known UUID (as long as UUIDs are supported for that
# filesystem type). True by default because most filesystems will get an
# UUID assigned to them in the moment they are created in the real system,
# even if that UUID is still not known by the devicegraph. If set to false,
# mounting by UUID will only be considered suitable if the UUID is already
# known in the devicegraph.
#
# @return [Array<Filesystems::MountByType>]
def suitable_mount_bys(label: nil, encryption: nil, assume_uuid: true)
with_mount_point_for_suitable(encryption) do |mount_point|
fs = mount_point.filesystem
# For swaps encrypted with volatile keys, UUID and LABEL are not an option
# because their are re-created on every boot.
# PATH and ID are not an option either, because encryption devices don't
# have udev links.
return [Filesystems::MountByType::DEVICE] if fs.volatile?
# #possible_mount_bys already filters out ID and PATH for devices without
# a current udev id and/or path recognized by libstorage-ng
candidates = mount_point.possible_mount_bys
return candidates unless fs.is?(:blk_filesystem)
label = (fs.label.size > 0) if label.nil?
uuid = assume_uuid ? true : !fs.uuid.empty?
filter_mount_bys(candidates, label, uuid)
candidates
end
end
# If the current mount_by is suitable, it does nothing.
#
# Otherwise, it assigns the best option from all the suitable ones
#
# @see #suitable_mount_bys
def ensure_suitable_mount_by
suitable = suitable_mount_bys
return if suitable.include?(mount_by)
assign_mount_by(Filesystems::MountByType.best_for(filesystem, suitable))
end
# Whether {#mount_by} was explicitly set by the user
#
# @note This relies on the userdata mechanism, see {#userdata_value}.
#
# @return [Boolean]
def manual_mount_by?
!!userdata_value(:manual_mount_by)
end
# Enforces de value for {#manual_mount_by?}
#
# @note This relies on the userdata mechanism, see {#userdata_value}.
#
# @param value [Boolean]
def manual_mount_by=(value)
save_userdata(:manual_mount_by, value)
end
# Mount options that YaST would propose as the default ones for this mount
# point, having into account the mount path, the type of filesystem, the
# underlying device and similar criteria
#
# @note This extends the corresponding Storage::MountPoint#default_mount_options
# provided by libstorage-ng. This adds YaST-specific options on top of the
# ones provided by the method in the library (which so far only returns the
# 'subvol=' option when needed).
#
# @return [Array<String>]
def default_mount_options
storage_default_mount_options + mountable.extra_default_mount_options
end
# Set {#mount_options} to the default value
#
# This overrides the Storage::MountsPoint#set_default_mount_options method
# provided by libstorage-ng. The original one only sets the default values
# calculated by the library, while this relies on {#default_mount_options}.
def set_default_mount_options
self.mount_options = default_mount_options
end
# @see #mounted_by_init?
INITRD_MOUNT_OPTION = "x-initrd.mount".freeze
private_constant :INITRD_MOUNT_OPTION
# Whether YaST expects this mount point to be already initialized in the initramfs
#
# @return [Boolean]
def mounted_by_init?
# Intentionally avoiding String#casecmp to check the mount option, turns out
# X-what.ever has a different semantic than x-what.ever (see "man -s8 mount")
root? || mount_options.include?(INITRD_MOUNT_OPTION)
end
protected
# @see Device#is?
def types_for_is
super << :mount_point
end
# Whether a non-zero {#passno} makes sense for this mount point
#
# @return [Boolean]
def passno_must_be_set?
return false unless mountable&.is?(:filesystem)
filesystem.type.is?(*TYPES_WITH_PASSNO) || esp?
end
# Executes the given block on a mount point that has been adapted to honor
# the argument "encryption" of {#suitable_mount_bys}
#
# @param encryption [Boolean, nil] see {#suitable_mount_bys}
def with_mount_point_for_suitable(encryption, &block)
mount_point =
if tmp_mount_point_needed?(encryption)
# Instance the temporary devicegraph here to make sure the garbage
# collector doesn't kill it before calling the given block
tmp_graph = devicegraph.dup
mount_point_for_suitable(tmp_graph)
else
self
end
block.call(mount_point)
end
# @see #with_mount_point_for_suitable
#
# @param encryption [Boolean, nil]
# @return [Boolean]
def tmp_mount_point_needed?(encryption)
return false if encryption.nil? || encryption == filesystem.encrypted?
return false unless filesystem.is?(:blk_filesystem)
# Since it's hard to know what to do in this case...
return false if filesystem.multidevice?
true
end
# DeviceMapper name for the temporary encryption created to calculate the
# suitable mount by types
TMP_NAME = "dmtemp".freeze
private_constant :TMP_NAME
# Temporary mount point used for the calculation of {#suitable_mount_bys}
#
# @param graph [Devicegraph] temporary devicegraph to safely do any change
# @return [MountPoint]
def mount_point_for_suitable(graph)
mount_point = graph.find_device(sid)
blk_dev = mount_point.filesystem.blk_devices.first
if blk_dev.is?(:encryption)
blk_dev.blk_device.remove_encryption
else
# We don't know which encryption type will be used, but LUKS1 is the
# default and, in most cases, the only option
blk_dev.create_encryption(TMP_NAME, EncryptionType::LUKS1)
end
mount_point
end
# @see #suitable_mount_bys
#
# @param candidates [Array<Filesystems::MountByType>]
# @param label [Boolean]
# @param uuid [Boolean]
def filter_mount_bys(candidates, label, uuid)
candidates.delete(Filesystems::MountByType::LABEL) unless label
candidates.delete(Filesystems::MountByType::UUID) unless uuid
end
end
end