src/lib/y2storage/autoinst_profile/partition_section.rb
# Copyright (c) [2017-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 "installation/autoinst_profile/section_with_attributes"
require "y2storage/subvol_specification"
module Y2Storage
module AutoinstProfile
# Thin object oriented layer on top of a <partition> section of the
# AutoYaST profile.
#
# More information can be found in the 'Partitioning' section ('Partition
# Configuration' subsection) of the AutoYaST documentation:
# https://www.suse.com/documentation/sles-12/singlehtml/book_autoyast/book_autoyast.html#ay.partition_configuration
# Check that document for details about the semantic of every attribute.
class PartitionSection < ::Installation::AutoinstProfile::SectionWithAttributes
ATTRIBUTES = [
{ name: :create },
{ name: :filesystem },
{ name: :format },
{ name: :label },
{ name: :uuid },
{ name: :lv_name },
{ name: :lvm_group },
{ name: :mount },
{ name: :mountby },
{ name: :partition_id },
{ name: :partition_nr },
{ name: :partition_type },
{ name: :subvolumes },
{ name: :size },
{ name: :crypt_fs },
{ name: :loop_fs },
{ name: :crypt_method },
{ name: :crypt_key },
{ name: :crypt_pbkdf },
{ name: :crypt_label },
{ name: :crypt_cipher },
{ name: :crypt_key_size },
{ name: :raid_name },
{ name: :raid_options },
{ name: :mkfs_options },
{ name: :fstab_options, xml_name: :fstopt },
{ name: :subvolumes_prefix },
{ name: :create_subvolumes },
{ name: :resize },
{ name: :pool },
{ name: :used_pool },
{ name: :stripes },
{ name: :stripe_size, xml_name: :stripesize },
{ name: :bcache_backing_for },
{ name: :bcache_caching_for },
{ name: :device },
{ name: :btrfs_name },
{ name: :quotas }
].freeze
private_constant :ATTRIBUTES
def self.attributes
ATTRIBUTES
end
define_attr_accessors
# @!attribute create
# @return [Boolean] whether the partition must be created or exists
# @!attribute crypt_fs
# @return [Boolean] whether the partition must be encrypted.
# @deprecated Use #crypt_method instead.
# @!attribute crypt_method
# @return [Symbol,nil] encryption method (:luks1, :pervasive_luks2,
# :protected_swap, :random_swap or :secure_swap). See {Y2Storage::EncryptionMethod}.
# @!attribute crypt_key
# @return [String] encryption key
# @!attribute crypt_pbkdf
# @return [Symbol,nil] password-based derivation function for LUKS2 (:pbkdf2, :argon2i,
# :argon2id). See {Y2Storage::PbkdFunction}.
# @!attribute crypt_label
# @return [String,nil] LUKS label if LUKS2 is going to be used
# @!attribute crypt_cipher
# @return [String,nil] specific cipher if LUKS is going to be used
#
# @!attribute crypt_key_size
# Specific key size (in bits) if LUKS is going to be used
#
# @return [Integer,nil] If nil, the default key size will be used. If an integer
# value is used, it has to be a multiple of 8.
# @!attribute filesystem
# @return [Symbol] file system type to use in the partition, it also
# influences other fields
# @see #type_for_filesystem
# @see #id_for_partition
# @!attribute partition_id
# @return [Integer] partition id. See #id_for_partition
# @!attribute format
# @return [Boolean] whether the partition should be formatted
# @!attribute label
# @return [String] label of the filesystem
# @!attribute uuid
# @return [String] UUID of the partition, only useful for reusing
# existing filesystems
# @!attribute lv_name
# @return [String] name of the LVM logical volume
# @!attribute mount
# @return [String] mount point for the partition
# @!attribute mountby
# @return [Symbol] :device, :label, :uuid, :path or :id
# @see #type_for_mountby
# @!attribute partition_nr
# @return [Integer] the partition number of this partition
# @!attribute partition_type
# @return [String, nil] the partition type of this partition (only can be "primary")
# @!attribute subvolumes
# @return [Array<SubvolSpecification>,nil] list of subvolumes or nil if not
# supported (from storage) or not specified (from hashes)
# @!attribute size
# @return [String] size of the partition in the flexible AutoYaST format
# @!attribute loop_fs
# @return [Boolean] undocumented attribute
# @!attribute raid_name
# @return [String] RAID name in which this partition will be included
# @!attribute raid_options
# @return [RaidOptionsSection] RAID options
# @see RaidOptionsSection
# @!attribute mkfs_options
# @return [String] mkfs options
#
# @!attribute fstab_options
# @return [Array<String>] Options to be used in the fstab for the filesystem
# @!attribute subvolumes_prefix
# @return [String] Name of the default Btrfs subvolume
# @!attribute device
# @return [String, nil] undocumented attribute, but used to indicate a NFS
# share when installing over NFS (with the old profile format)
# @!attribute btrfs_name
# @return [String] Btrfs in which this partition will be included
# @!attribute quotas
# @return [Boolean] Whether support for quotas is enabled or not
def init_from_hashes(hash)
super
if hash["raid_options"]
@raid_options = RaidOptionsSection.new_from_hashes(hash["raid_options"], self)
end
@subvolumes_prefix = hash["subvolumes_prefix"]
@create_subvolumes = hash.fetch("create_subvolumes", true)
@subvolumes = subvolumes_from_hashes(hash["subvolumes"]) if hash["subvolumes"]
@bcache_caching_for = hash.fetch("bcache_caching_for", [])
@fstab_options = hash["fstopt"].split(",").map(&:strip) if hash["fstopt"]
end
# Clones a device into an AutoYaST profile section by creating an instance
# of this class from the information of a device
#
# @see PartitioningSection.new_from_storage for more details
#
# @param device [Device] a device that can be cloned into a <partition> section,
# like a partition, an LVM logical volume, an MD RAID, a NFS filesystem or a
# Btrfs multi-device.
# @return [PartitionSection]
def self.new_from_storage(device, parent = nil)
exporter = PartitionExporter.new(device)
exporter.section(parent)
end
# Filesystem type to be used for the real partition object, based on the
# #filesystem value.
#
# @return [Filesystems::Type, nil] nil if #filesystem is not set or it's
# impossible to infer the type
def type_for_filesystem
return nil unless filesystem
Filesystems::Type.find(filesystem)
rescue NameError
nil
end
# Name schema type to be used for the real partition object, based on the
# #filesystem value
#
# @return [Filesystems::MountByType, nil] nil if #filesystem is not set
# or it's impossible to infer the type
def type_for_mountby
return nil unless mountby
Filesystems::MountByType.find(mountby)
rescue NameError
nil
end
# Partition id to be used for the real partition object.
#
# This implements the AutoYaST documented logic. If #partition_id is
# filled, the corresponding id is used. Otherwise SWAP or LINUX will be
# used, depending on the value of #filesystem.
#
# @return [PartitionId]
def id_for_partition
return PartitionId.new_from_legacy(partition_id) if partition_id
return PartitionId::SWAP if type_for_filesystem&.is?(:swap)
PartitionId::LINUX
end
# Device name to be used for the real MD device
#
# This implements the AutoYaST documented logic, if 'raid_name' is
# provided as one of the corresponding 'raid_options', that name should be
# used. Otherwise the name will be inferred from 'partition_nr'.
#
# @return [String] MD RAID device name
def name_for_md
name = raid_options&.raid_name
return name unless name.nil? || name.empty?
"/dev/md/#{partition_nr}"
end
# Name to reference a multi-device Btrfs (used when exporting).
#
# @param filesystem [Filesystems::BlkFilesystem, nil]
# @return [String, nil]
def name_for_btrfs(filesystem)
return nil unless filesystem&.multidevice? && filesystem&.is?(:btrfs)
"btrfs_#{filesystem.sid}"
end
def to_hashes
hash = super
hash["fstopt"] = fstab_options.join(",") if fstab_options && !fstab_options.empty?
if subvolumes
hash["create_subvolumes"] = !subvolumes.empty?
hash["subvolumes"] = subvolumes_to_hashes
hash["subvolumes_prefix"] = subvolumes_prefix
end
hash
end
# Return section name
#
# @return [String] "partitions"
def collection_name
"partitions"
end
protected
# Returns an array of hashes representing subvolumes
#
# AutoYaST only uses a subset of subvolumes properties: 'path', 'copy_on_write'
# and 'referenced_limit'.
#
# @return [Array<Hash>] Array of hash-based representations of subvolumes
def subvolumes_to_hashes
subvolumes.map do |subvol|
subvol_path = subvol.path.sub(/\A#{@subvolumes_prefix}\//, "")
hash = { "path" => subvol_path, "copy_on_write" => subvol.copy_on_write }
if subvol.referenced_limit && !subvol.referenced_limit.unlimited?
hash["referenced_limit"] = subvol.referenced_limit.to_s
end
hash
end
end
# Return a list of subvolumes from an array of hashes
#
# This method builds a list of SubvolSpecification objects from an array
# of subvolumes in hash form (according to AutoYaST specification).
#
# Additionally, it filters out "@" subvolumes entries which were
# generated by older AutoYaST versions. See bnc#1061253.
#
# @param hashes [Array<Hash>] List of subvolumes in hash form
# @return [Array<SubvolSpecification>] List of subvolumes
def subvolumes_from_hashes(hashes)
subvolumes = SubvolSpecification.list_from_control_xml(hashes)
subvolumes.reject { |s| s.path == "@" }
end
# Auxiliary class to encapsulate the conversion from storage objects to their
# representation as {PartitionSection}
class PartitionExporter
# Literal historically used at AutoinstPartPlan
CRYPT_KEY_VALUE = "ENTER KEY HERE"
private_constant :CRYPT_KEY_VALUE
# Partitions with these IDs are historically marked with format=false
# NOTE: "Dell Utility" was included here, but there is no such ID in the
# new libstorage.
NO_FORMAT_IDS = [PartitionId::PREP, PartitionId::DOS16]
private_constant :NO_FORMAT_IDS
# Partitions with these IDs are historically marked with create=false
# NOTE: "Dell Utility" was the only entry here. See above.
NO_CREATE_IDS = []
private_constant :NO_CREATE_IDS
# Encryption method to use when the method of an encryption device cannot be determined
DEFAULT_ENCRYPTION_METHOD = Y2Storage::EncryptionMethod.find(:luks1)
private_constant :DEFAULT_ENCRYPTION_METHOD
# @return [Device] a device that can be cloned into a <partition> section,
# like a partition, an LVM logical volume, an MD RAID or a NFS filesystem.
attr_reader :device
# Constructor
#
# @param device [Device] see {#device}
def initialize(device)
@device = device
end
# Method used by {PartitionSection.new_from_storage} to populate the attributes when
# cloning a partition device.
#
# As usual, it keeps the behavior of the old clone functionality, check
# the implementation of this class for details.
#
# @return [PartitionSection]
def section(parent)
result = PartitionSection.new(parent)
result.create = true
result.resize = false
init_fields_by_type(result)
# Exporting these values only makes sense when the device is a block device. Note that some
# exported devices (e.g., multi-device Btrfs and NFS filesystems) are not block devices.
return result unless device.is?(:blk_device)
init_encryption_fields(result)
init_filesystem_fields(result) unless device.filesystem&.multidevice?
# NOTE: The old AutoYaST exporter does not report the real size here.
# It intentionally reports one cylinder less. Cylinders is an obsolete
# unit (that equals to 8225280 bytes in my experiments).
# According to the comments there, that was done due to bnc#415005 and
# bnc#262535.
result.size = device.size.to_i.to_s if result.create && !fixed_size?
result
end
protected
# @param section [PartitionSection] section object to modify based on the device
def init_fields_by_type(section)
if device.is?(:lvm_lv)
init_lv_fields(section)
elsif device.is?(:disk_device, :software_raid, :stray_blk_device, :bcache)
init_disk_device_fields(section)
elsif device.is?(:nfs)
init_nfs_fields(section)
elsif device.is?(:tmpfs)
init_tmpfs_fields(section)
elsif device.is?(:blk_filesystem)
init_blk_filesystem_fields(section, device)
else
init_partition_fields(section)
end
end
# @param section [PartitionSection] section object to modify based on the device
def init_partition_fields(section)
section.create = !NO_CREATE_IDS.include?(device.id)
section.partition_nr = device.number
section.partition_type = "primary" if primary_partition?
section.partition_id = partition_id
section.lvm_group = lvm_group_name
section.raid_name = device.md.name if device.md
section.btrfs_name = section.name_for_btrfs(device.filesystem)
init_bcache_fields(section)
end
# @param section [PartitionSection] section object to modify based on the device
def init_disk_device_fields(section)
section.create = false
section.lvm_group = lvm_group_name
section.raid_name = device.md.name if device.respond_to?(:md) && device.md
section.btrfs_name = section.name_for_btrfs(device.filesystem)
init_bcache_fields(section)
end
# @param section [PartitionSection] section object to modify based on the device
def init_lv_fields(section)
section.lv_name = device.basename
section.stripes = device.stripes
section.stripe_size = device.stripe_size.to_i / DiskSize.KiB(1).to_i
section.pool = device.lv_type == LvType::THIN_POOL
parent = device.parents.first
section.used_pool = parent.lv_name if device.lv_type == LvType::THIN && parent.is?(:lvm_lv)
section.btrfs_name = section.name_for_btrfs(device.filesystem)
end
# @param section [PartitionSection] section object to modify based on the device
def init_encryption_fields(section)
return unless device.encrypted?
method = device.encryption.method || DEFAULT_ENCRYPTION_METHOD
section.loop_fs = true
section.crypt_method = method.id
section.crypt_key = CRYPT_KEY_VALUE if method.password_required?
init_luks_fields(section)
end
# @param section [PartitionSection] section object to modify based on the device
def init_luks_fields(section)
enc = device.encryption
section.crypt_pbkdf = enc.pbkdf&.to_sym if enc.supports_pbkdf?
section.crypt_label = enc.label if enc.supports_label? && !enc.label.empty?
section.crypt_cipher = enc.cipher if enc.supports_cipher? && !enc.cipher.empty?
section.crypt_key_size = enc.key_size * 8 if enc.supports_key_size? && !enc.key_size.zero?
end
# @param section [PartitionSection] section object to modify based on the device
def init_filesystem_fields(section)
section.format = false
fs = device.filesystem
return unless fs
section.format = true if device.respond_to?(:id) && !NO_FORMAT_IDS.include?(device.id)
init_blk_filesystem_fields(section, fs)
end
# @param section [PartitionSection] section object to modify based on the device
# @param filesystem [Filesystems::BlkFilesystem]
def init_blk_filesystem_fields(section, filesystem)
section.filesystem = filesystem.type.to_sym
section.label = filesystem.label unless filesystem.label.empty?
section.mkfs_options = filesystem.mkfs_options unless filesystem.mkfs_options.empty?
section.quotas = filesystem.quota? if filesystem.respond_to?(:quota?)
init_subvolumes(section, filesystem)
init_mount_options(section, filesystem)
end
# @param section [PartitionSection] section object to modify based on the device
# @param filesystem [Filesystems::BlkFilesystem]
def init_mount_options(section, filesystem)
return if filesystem.mount_point.nil?
section.mount = filesystem.mount_point.path
section.mountby = filesystem.mount_point.mount_by.to_sym
mount_options = filesystem.mount_point.mount_options
section.fstab_options = mount_options unless mount_options.empty?
end
# @param section [PartitionSection] section object to modify based on the device
# @param filesystem [Filesystems::BlkFilesystem] Filesystem to add subvolumes if required
def init_subvolumes(section, filesystem)
return unless filesystem.supports_btrfs_subvolumes?
section.subvolumes_prefix = filesystem.subvolumes_prefix
valid_subvolumes = filesystem.btrfs_subvolumes.reject do |subvol|
subvol.path.empty? || subvol.path == section.subvolumes_prefix ||
subvol.path.start_with?(filesystem.snapshots_root)
end
section.subvolumes = valid_subvolumes.map do |subvol|
SubvolSpecification.create_from_btrfs_subvolume(subvol)
end
end
# @param section [PartitionSection] section object to modify based on the device
def init_bcache_fields(section)
if device.bcache
section.bcache_backing_for = device.bcache.name
elsif device.in_bcache_cset
section.bcache_caching_for = device.in_bcache_cset.bcaches.map(&:name)
end
end
# @param section [PartitionSection] section object to modify based on the device
def init_nfs_fields(section)
section.create = false
init_mount_options(section, device)
end
# @param section [PartitionSection] section object to modify based on the device
def init_tmpfs_fields(section)
section.create = nil
section.resize = nil
init_mount_options(section, device)
section.mountby = nil
end
# Uses legacy ids for backwards compatibility. For example, BIOS Boot
# partitions in the old libstorage were represented by the internal
# code 259 and, thus, systems cloned with the old exporter
# (AutoinstPartPlan) use 259 instead of the current 257.
def partition_id
id = enforce_bios_boot? ? PartitionId::BIOS_BOOT : device.id
id.to_i_legacy
end
# Whether the given existing partition should be reported as GRUB (GPT
# Bios Boot) in the cloned profile.
#
# @note To ensure backward compatibility, this method implements the
# logic present in the old AutoYaST exporter that used to live in
# AutoinstPartPlan#ReadHelper.
# https://github.com/yast/yast-autoinstallation/blob/47c24fb98e074f5b6432f3a4f8b9421362ee29cc/src/modules/AutoinstPartPlan.rb#L345
# Thus, this returns true for any partition with a Windows-related ID
# that is configured to be mounted in /boot*
# See commit 54e236cd428636b3bf8f92d2ac2914e5b1d67a90 of
# yast-autoinstallation.
#
# @return [Boolean]
def enforce_bios_boot?
return false if device.filesystem_mountpoint.nil?
device.id.is?(:windows_system) && device.filesystem_mountpoint.include?("/boot")
end
# Returns the volume group associated to a given device
#
# @return [String,nil] Volume group; nil if it is not used as a physical volume or does
# not belong to any volume group.
def lvm_group_name
return nil if device.lvm_pv.nil? || device.lvm_pv.lvm_vg.nil?
device.lvm_pv.lvm_vg.basename
end
# Determines whether the device has a fixed size (disk, RAID, etc.)
#
# It is used to find out whether the size specification should be included
# in the profile.
#
# @return [Boolean]
def fixed_size?
device.is?(:disk_device, :software_raid)
end
# Determines whether the partition is primary or not
#
# Always false when the partition table does not allow extended partitions
#
# @return [Boolean] true when is a primary partition; false otherwise
def primary_partition?
return false unless device.partition_table.extended_possible?
device.type.is?(:primary)
end
end
end
end
end