src/lib/y2storage/subvol_specification.rb
# 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"
require "y2storage/shadower"
require "y2storage/equal_by_instance_variables"
Yast.import "Arch"
Yast.import "ProductFeatures"
module Y2Storage
# Helper class to represent a subvolume specification as defined
# in control.xml
#
class SubvolSpecification
include Yast::Logger
include EqualByInstanceVariables
attr_accessor :path, :copy_on_write, :archs, :referenced_limit
COW_SUBVOL_PATHS = [
"home",
"opt",
"srv",
"tmp",
"usr/local",
"var/cache",
"var/crash",
"var/lib/machines",
"var/lib/mailman",
"var/lib/named",
"var/log",
"var/opt",
"var/spool",
"var/tmp",
"boot/grub2/i386-pc",
"boot/grub2/x86_64-efi",
"boot/grub2/powerpc-ieee1275",
"boot/grub2/s390x-emu"
]
# No Copy On Write for SQL databases and libvirt virtual disks to
# minimize performance impact
NO_COW_SUBVOL_PATHS = [
"var/lib/libvirt/images",
"var/lib/mariadb",
"var/lib/mysql",
"var/lib/pgsql"
]
# Subvolumes, from the lists above, that contain architecture modifiers
SUBVOL_ARCHS = {
"boot/grub2/i386-pc" => ["i386", "x86_64"],
"boot/grub2/x86_64-efi" => ["x86_64"],
"boot/grub2/powerpc-ieee1275" => ["ppc", "!board_powernv"],
"boot/grub2/s390x-emu" => ["s390"]
}
def initialize(path, copy_on_write: true, archs: nil, referenced_limit: nil)
@path = path
@copy_on_write = copy_on_write
@archs = archs
@referenced_limit = referenced_limit
end
def to_s
text = "SubvolSpecification #{@path}"
text += " (NoCOW)" unless @copy_on_write
text += " (archs: #{@archs})" if arch_specific?
text += " (limit): #{@referenced_limit}" if @referenced_limit
text
end
def arch_specific?
!archs.nil?
end
# Comparison operator for sorting
#
def <=>(other)
path <=> other.path
end
# Check if this subvolume should be used for the current architecture.
# A subvolume is used if its archs contain the current arch.
# It is not used if its archs contain the current arch negated
# (e.g. "!ppc").
#
# @return [Boolean] true if this subvolume matches the current architecture
#
def current_arch?
matches_arch? { |arch| Yast::Arch.respond_to?(arch.to_sym) && Yast::Arch.send(arch.to_sym) }
end
# Check if this subvolume should be used for an architecture.
#
# If a block is given, the block is called as the matcher with the
# architecture to be tested as its argument.
#
# If no block is given (and only then), the 'target_arch' parameter is
# used to check against.
#
# @return [Boolean] true if this subvolume matches
#
def matches_arch?(target_arch = nil, &block)
return true unless arch_specific?
use_subvol = false
archs.each do |a|
arch = a.dup
negate = arch.start_with?("!")
arch[0] = "" if negate # remove leading "!"
match = block_given? ? block.call(arch) : arch == target_arch
if match && negate
log.info("Not using #{self} for explicitly excluded arch #{arch}")
return false
end
use_subvol ||= match
end
log.info("Using arch specific #{self}: #{use_subvol}")
use_subvol
end
# Checks whether this device is shadowed by any of the given mount points
# @see BtrfsSubvolume#shadowing?
#
# @param other_mount_points [Array<String>]
#
# @return [Boolean]
def shadowed?(fs_mount_point, other_mount_points)
return false if fs_mount_point.nil? || other_mount_points.nil?
mount_point = Y2Storage::Filesystems::Btrfs.btrfs_subvolume_mount_point(fs_mount_point, path)
other_mount_points.compact.any? { |m| Y2Storage::Shadower.shadowing?(m, mount_point) }
end
# Creates a new btrfs subvolume for the indicated filesystem
#
# @note The new subvolume is set as 'can be auto deleted'.
#
# @param filesystem [Filesystems::Btrfs]
# @return [BtrfsSubvolume,nil] New BtrfsSubvolume; nil if the subvolume could not
# be created.
# @see FileSystems::Btrfs#create_btrfs_subvolume
def create_btrfs_subvolume(filesystem)
subvolume_path = filesystem.btrfs_subvolume_path(path)
subvolume = filesystem.create_btrfs_subvolume(subvolume_path, !copy_on_write)
return if subvolume.nil?
subvolume.referenced_limit = referenced_limit if referenced_limit
subvolume.can_be_auto_deleted = true
subvolume
end
# Factory method: Create one SubvolSpecification from XML data.
#
# @param xml [Hash,String] can be a map (for fully specified subvolumes)
# or just a string (for subvolumes specified just as a path)
# @return [SubvolSpecification] or nil if error
def self.create_from_xml(xml)
xml ||= {}
xml = { "path" => xml } if xml.is_a?(String)
return nil unless xml.key?("path")
cow = xml.key?("copy_on_write") ? xml["copy_on_write"] : true
archs = nil
archs = xml["archs"].gsub(/\s+/, "").split(",") if xml.key?("archs")
referenced_limit = DiskSize.parse_or(xml["referenced_limit"]) if xml["referenced_limit"]
planned_subvol = SubvolSpecification.new(
xml["path"], copy_on_write: cow, archs: archs, referenced_limit: referenced_limit
)
log.info("Creating from XML: #{planned_subvol}")
planned_subvol
end
# Create a SubvolSpecification from a Btrfs subvolume
#
# @param subvolume [BtrfsSubvolume] Btrfs subvolume
# @return [SubvolSpecification]
def self.create_from_btrfs_subvolume(subvolume)
subvol = SubvolSpecification.new(
subvolume.path,
copy_on_write: !subvolume.nocow?,
referenced_limit: subvolume.referenced_limit
)
log.info "Creating from Btrfs subvolume: #{subvol}"
subvol
end
# Create a list of SubvolSpecification objects from the <subvolumes> part of
# control.xml or an AutoYaST profile. The map may be empty if there is a
# <subvolumes> section, but that section is empty.
#
# Returns nil if the section is nil or impossible to process.
#
# This function does not do much error handling or reporting; it is assumed
# that control.xml and/or the AutoYaST profile are validated against the
# corresponding schema.
#
# Note that the AutoYaST format is a superset of the control.xml one,
# accepting fully described subvolumes (like in control.xml) and also
# subvolumes specified as a simple path.
#
# @param subvolumes_xml [Array] list of XML <subvolume> entries
# @return [Array<SubvolSpecification>, nil]
def self.list_from_control_xml(subvolumes_xml)
return nil if subvolumes_xml.nil?
return nil unless subvolumes_xml.respond_to?(:map)
subvols = subvolumes_xml.each_with_object([]) do |xml, result|
# Remove nil subvols due to XML parse errors
next if xml.nil?
new_subvol = SubvolSpecification.create_from_xml(xml)
next if new_subvol.nil?
result << new_subvol
end
subvols.sort!
end
# Create a fallback list of subvol specifications. This is useful if
# nothing is specified in the control.xml file.
#
# @return [Array<SubvolSpecification>]
def self.fallback_list
subvols = COW_SUBVOL_PATHS.map { |path| SubvolSpecification.new(path) }
subvols.concat(
NO_COW_SUBVOL_PATHS.map { |path| SubvolSpecification.new(path, copy_on_write: false) }
)
subvols.each { |subvol| subvol.archs = SUBVOL_ARCHS[subvol.path] }
subvols.sort!
end
# Filters specs and returns only what makes sense for the current architecture
#
# @see #current_arch?
#
# @param specs [Array<SubvolSpecification>]
# @return [Array<SubvolSpecification>]
def self.for_current_arch(specs)
specs.select(&:current_arch?)
end
# Creates a list of SubvolSpecification objects from the <subvolumes> part of
# control.xml or an AutoYaST profile.
#
# @see .list_from_control_xml
#
# @return [Array<SubvolSpecification>, nil]
def self.from_control_file
xml = Yast::ProductFeatures.GetSection("partitioning")
list_from_control_xml(xml["subvolumes"])
end
end
end