src/lib/y2storage/proposal/autoinst_devices_creator.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 "y2storage/proposal/partitions_distribution_calculator"
require "y2storage/proposal/partition_creator"
require "y2storage/proposal/autoinst_partition_size"
require "y2storage/proposal/autoinst_partitioner"
require "y2storage/proposal/autoinst_md_creator"
require "y2storage/proposal/autoinst_bcache_creator"
require "y2storage/proposal/lvm_creator"
require "y2storage/proposal/btrfs_creator"
require "y2storage/proposal/nfs_creator"
require "y2storage/proposal/tmpfs_creator"
require "y2storage/proposal/autoinst_creator_result"
require "y2storage/exceptions"
module Y2Storage
module Proposal
# Class to create and reuse devices during the AutoYaST proposal, based
# on the information contained in the profile.
#
# ## Comparison with the guided proposal
#
# This class receives a devicegraph in which the previous devices have
# already been deleted or resized according to the AutoYaST profile. This
# is different from the guided setup equivalent step, in which the minimal
# amount of existing devices are deleted/resized on demand while trying to
# allocate the planned devices.
#
# ## Reducing planned devices when there is not enough space
#
# Another key difference with the guided proposal is that, when there is
# not enough space (for partitions or logical volumes), it will do a second
# attempt reducing all planned devices proportionally. In order to do so,
# it will remove the min_size limit (setting it to just 1 byte) and,
# additionally, it will set a proportional weight for every partition (see
# {AutoinstPartitionSize#flexible_partitions}).
#
# Although this approach may not produce the optimal results, it is less
# intrusive and easier to maintain than other alternatives. Bear in mind
# that AutoYaST does not expect complex scenarios (like multiple disks with
# several gaps), so the result should be good enough.
#
# If we were aiming for the optimal devices distribution, we should look at
# {Y2Storage::Planned::PartitionsDistribution#assigned_space} and follow
# the same approach (reducing min_size and setting a proportional weight)
# when it is not possible to place the devices in the given free space. But
# we would also need to do further changes, like skipping some checks when
# running in this flexible mode.
#
# FIXME: class too long, refactoring needed.
class AutoinstDevicesCreator # rubocop:disable Metrics/ClassLength
include Yast::Logger
include AutoinstPartitionSize
# @return [AutoinstIssues::List] List of found AutoYaST issues
attr_reader :issues_list
# Constructor
#
# @param original_graph [Devicegraph] Devicegraph to be used as starting point
# @param issues_list [AutoinstIssues::List] List of AutoYaST issues to register the problems
# found during devices creation
def initialize(original_graph, issues_list)
@original_graph = original_graph
@issues_list = issues_list
end
# Devicegraph including all the specified planned devices
#
# @param planned_devices [Planned::DevicesCollection] Devices to create/reuse
# @param disk_names [Array<String>] Disks to consider
#
# @return [AutoinstCreatorResult] Result with new devicegraph in which all the
# planned devices have been allocated
def populated_devicegraph(planned_devices, disk_names)
# Process planned partitions
log.info "planned devices = #{planned_devices.to_a.inspect}"
log.info "disk names = #{disk_names.inspect}"
reset
@planned_devices = planned_devices
@disk_names = disk_names
process_devices
end
protected
# @return [Devicegraph] Original devicegraph
attr_reader :original_graph
# @return [Planned::DevicesCollection] Devices to create/reuse
attr_reader :planned_devices
# @return [Array<String>] Disks to consider
attr_reader :disk_names
# @return [Array<Planned::Device>] Devices to create
attr_reader :devices_to_create
# @return [Array<Planned::Device>] Devices to reuse
attr_reader :devices_to_reuse
# @return [Proposal::CreatorResult] Current result containing the devices that have been created
attr_reader :creator_result
# @return [Devicegraph] Current devicegraph
attr_reader :devicegraph
private
# Sets the current creator result
#
# The current devicegraph is properly updated.
#
# @param result [Proposal::CreatorResult]
def creator_result=(result)
@creator_result = result
@devicegraph = result.devicegraph
end
# Adds devices to the list of devices to create
#
# @param planned_devices [Array<Planned::Device>]
def add_devices_to_create(planned_devices)
@devices_to_create.concat(planned_devices)
end
# Adds devices to the list of devices to reuse
#
# @param planned_devices [Array<Planned::Device>]
def add_devices_to_reuse(planned_devices)
@devices_to_reuse.concat(planned_devices)
end
# Resets values before create devices
#
# @see #populated_devicegraph
def reset
@devices_to_create = []
@devices_to_reuse = []
@creator_result = nil
@devicegraph = original_graph.duplicate
end
# Reuses and creates planned devices
#
# @return [AutoinstCreatorResult] Result with new devicegraph in which all the
# planned devices have been allocated
def process_devices
process_partitions
# Process planned disk like devices (Xen virtual partitions and full disks)
process_disk_like_devs
process_mds
process_vgs
process_bcaches
process_btrfs_filesystems
process_nfs_filesystems
process_tmpfs_filesystems
Y2Storage::Proposal::AutoinstCreatorResult.new(creator_result, devices_to_create)
end
# Process planned partitions
def process_partitions
planned_partitions = planned_devices.disk_partitions
planned_partitions = sized_partitions(planned_partitions, devicegraph: original_graph)
parts_to_reuse, parts_to_create = planned_partitions.partition(&:reuse?)
AutoinstPartitioner.new(devicegraph).reuse_partitions(parts_to_reuse)
add_devices_to_create(parts_to_create)
add_devices_to_reuse(parts_to_reuse)
self.creator_result = create_partitions(parts_to_create, devicegraph)
end
# Formats and/or mounts the disk like block devices (Xen virtual partitions and full disks)
#
# Add planned disk like devices to reuse list so they can be considered for lvm and raids
# later on.
def process_disk_like_devs
planned_devs = planned_devices.select do |dev|
dev.is_a?(Planned::StrayBlkDevice) || dev.is_a?(Planned::Disk)
end
planned_devs.each { |d| d.reuse!(devicegraph) }
add_devices_to_reuse(planned_devs)
end
# Process planned Mds
def process_mds
mds_to_reuse, mds_to_create = planned_devices.mds.partition(&:reuse?)
devs_to_reuse_in_md = reusable_by_md(devices_to_reuse)
reuse_partitionables(mds_to_reuse)
add_devices_to_create(mds_to_create)
add_devices_to_reuse(mds_to_reuse.flat_map(&:partitions))
self.creator_result = create_mds(planned_devices.mds, devs_to_reuse_in_md)
end
# Process planned bcaches
def process_bcaches
bcaches_to_reuse, bcaches_to_create = planned_devices.bcaches.partition(&:reuse?)
reuse_partitionables(bcaches_to_reuse)
add_devices_to_create(bcaches_to_create)
add_devices_to_reuse(bcaches_to_reuse.flat_map(&:partitions))
self.creator_result = create_bcaches(planned_devices.bcaches, devices_to_reuse)
end
# Process planned Vgs
def process_vgs
planned_vgs = planned_devices.vgs
vgs_to_reuse = planned_vgs.select(&:reuse?)
reuse_vgs(vgs_to_reuse)
add_devices_to_create(planned_vgs)
add_devices_to_reuse(vgs_to_reuse.flat_map(&:lvs))
self.creator_result = set_up_lvm(planned_vgs, devices_to_reuse)
end
# Process planned Btrfs filesystems
def process_btrfs_filesystems
filesystems_to_reuse = planned_devices.btrfs_filesystems.select(&:reuse?)
filesystems_to_create = planned_devices.btrfs_filesystems.reject(&:reuse?)
reuse_btrfs_filesystems(filesystems_to_reuse)
add_devices_to_create(filesystems_to_create)
reusable_devices = reusable_by_btrfs(devices_to_reuse)
self.creator_result = create_btrfs_filesystems(filesystems_to_create, reusable_devices)
end
# Process planned NFS filesystems
def process_nfs_filesystems
add_devices_to_create(planned_devices.nfs_filesystems)
self.creator_result = create_nfs_filesystems(planned_devices.nfs_filesystems)
end
# Process planned Tmpfs filesystems
def process_tmpfs_filesystems
add_devices_to_create(planned_devices.tmpfs_filesystems)
self.creator_result = create_tmpfs_filesystems(planned_devices.tmpfs_filesystems)
end
# Reuses a partitionable device
#
# @param reused_devices [Array<Planned::Device>] MD RAIDs or bcache to reuse
def reuse_partitionables(reused_devices)
reused_devices.each_with_object(creator_result) do |dev, result|
partitioner = AutoinstPartitioner.new(result.devicegraph)
result.merge!(partitioner.reuse_device_partitions(dev))
end
end
# Reuses volume groups
#
# @param reused_vgs [Array<Planned::LvmVg>] Volume groups to reuse
def reuse_vgs(reused_vgs)
reused_vgs.each_with_object(creator_result) do |vg, result|
lvm_creator = Proposal::LvmCreator.new(result.devicegraph)
result.merge!(lvm_creator.reuse_volumes(vg))
end
end
# Reuses Btrfs filesystems
#
# @param filesystems_to_reuse [Array<Planned::Btrfs>]
def reuse_btrfs_filesystems(filesystems_to_reuse)
filesystems_to_reuse.each_with_object(creator_result) do |fs, result|
btrfs_creator = Proposal::BtrfsCreator.new(result.devicegraph)
result.merge!(btrfs_creator.reuse_filesystem(fs))
end
end
# Creates planned partitions
#
# @param new_partitions [Array<Planned::Partition>] Devices to create
# @return [PartitionCreatorResult]
def create_partitions(new_partitions, devicegraph)
disks = devicegraph.disk_devices.select { |d| disk_names.include?(d.name) }
partitioner = AutoinstPartitioner.new(devicegraph)
partitioner.create_partitions(new_partitions, disks)
end
# Creates MD RAID devices
#
# @param mds [Array<Planned::Md>] List of planned MD arrays to create
# @param devs_to_reuse [Array<Planned::Device>] List of devices to reuse as MD device
#
# @return [Proposal::CreatorResult] Result containing the specified MD RAIDs
def create_mds(mds, devs_to_reuse)
mds.reduce(creator_result) do |result, md|
# Normally, the profile will use the same naming convention
# (/dev/md0 vs /dev/md/0) to define the RAID itself (in its corresponding
# <drive> section) and to reference that RAID from its components
# (using <raid_name>). So populating the 'devices' list below could be
# as simple as matching Planned::Devices#raid_name with Planned::Md.name
#
# BUT if the old format is used to specify the RAID ("/dev/md" as name
# and a <partition_nr> to indicate the number), the name for the planned MD
# is auto-generated (with the /dev/md/0 format so far), so we must use
# Planned::Md#name? to ensure robust comparison no matter which format
# is used in #raid_name
devices = result.created_names { |d| d.respond_to?(:raid_name) && md.name?(d.raid_name) }
devices += devs_to_reuse.select { |d| md.name?(d.raid_name) }.map(&:reuse_name)
if devices.empty?
issues_list.add(Y2Storage::AutoinstIssues::NoComponents, md)
next result
end
result.merge(create_md(result.devicegraph, md, devices))
end
end
# Creates bcaches
#
# @param bcaches [Array<Planned::Bcache>] List of planned Bcache devices to create
# @param devs_to_reuse [Array<Planned::Device>] List of devices to reuse as backing or caching
# device
#
# @return [Proposal::CreatorResult] Result containing the specified Bcache devices
def create_bcaches(bcaches, devs_to_reuse)
bcaches.reduce(creator_result) do |result, bcache|
backing_devname = find_bcache_member(bcache.name, :backing, creator_result, devs_to_reuse)
caching_devname = find_bcache_member(bcache.name, :caching, creator_result, devs_to_reuse)
if backing_devname.nil?
issues_list.add(Y2Storage::AutoinstIssues::NoComponents, bcache)
next result
end
new_result = create_bcache(result.devicegraph, bcache, backing_devname, caching_devname)
result.merge(new_result)
end
end
# Creates volume groups
#
# @param vgs [Array<Planned::LvmVg>] List of planned volume groups to add
# @param devs_to_reuse [Array<Planned::Device>] List of devices to reuse as Physical Volumes
#
# @return [Proposal::CreatorResult] Result containing the specified volume groups
def set_up_lvm(vgs, devs_to_reuse)
# log separately to be more readable
log.info "set_up_lvm: vgs=#{vgs.inspect}"
log.info "set_up_lvm: previous_result=#{creator_result.inspect}"
log.info "set_up_lvm: devs_to_reuse=#{devs_to_reuse.inspect}"
vgs.reduce(creator_result) do |result, vg|
pvs = creator_result.created_names { |d| d.pv_for?(vg.volume_group_name) }
devs = devs_to_reuse.select { |d| d.respond_to?(:pv_for?) && d.pv_for?(vg.volume_group_name) }
pvs += devs.map(&:reuse_name)
if pvs.empty?
issues_list.add(Y2Storage::AutoinstIssues::NoComponents, vg)
next result
end
result.merge(create_logical_volumes(result.devicegraph, vg, pvs))
end
end
# Creates Btrfs filesystems
#
# @param filesystems_to_create [Array<Planned::Btrfs>]
# @param reusable_devices [Array<Planned::Device>] devices that can be reused for the new Btrfs
#
# @return [Proposal::CreatorResult] Result containing the specified Btrfs filesystems
def create_btrfs_filesystems(filesystems_to_create, reusable_devices)
filesystems_to_create.reduce(creator_result) do |result, planned_filesystem|
devices = created_devices_for_btrfs(planned_filesystem, result)
devices += reused_devices_for_btrfs(planned_filesystem, reusable_devices)
if devices.empty?
issues_list.add(Y2Storage::AutoinstIssues::NoComponents, planned_filesystem)
next result
end
result.merge(create_btrfs(result.devicegraph, planned_filesystem, devices))
end
end
# Name of devices that have been created for a specific planned Btrfs
#
# @param planned_filesystem [Planned::Btrfs]
# @param creator_result [Proposal::CreatorResult]
#
# @return [Array<String>] devices names
def created_devices_for_btrfs(planned_filesystem, creator_result)
creator_result.created_names do |device|
device.respond_to?(:btrfs_name) && device.btrfs_name == planned_filesystem.name
end
end
# Name of devices that can be reused for a specific planned Btrfs
#
# @param planned_filesystem [Planned::Btrfs]
# @param reusable_devices [Array<Planned::Device>]
#
# @return [Array<String>] devices names
def reused_devices_for_btrfs(planned_filesystem, reusable_devices)
devices = reusable_devices.select { |d| d.btrfs_name == planned_filesystem.name }
devices.map(&:reuse_name)
end
# Creates NFS filesystems
#
# @param nfs_filesystems [Array<Planned::Nfs>] List of planned NFS filesystems
# @return [Proposal::CreatorResult] Result containing the specified NFS filesystems
def create_nfs_filesystems(nfs_filesystems)
nfs_filesystems.reduce(creator_result) do |result, planned_nfs|
new_result = create_nfs_filesystem(result.devicegraph, planned_nfs)
result.merge(new_result)
end
end
# Creates Tmpfs filesystems
#
# @param tmpfs_filesystems [Array<Planned::Tmpfs>] List of planned Tmpfs filesystems
# @return [Proposal::CreatorResult] Result containing the specified Tmpfs filesystems
def create_tmpfs_filesystems(tmpfs_filesystems)
tmpfs_filesystems.reduce(creator_result) do |result, planned_tmpfs|
new_result = create_tmpfs_filesystem(result.devicegraph, planned_tmpfs)
result.merge(new_result)
end
end
# Creates a MD RAID in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param md [Planned::Md] Planned MD RAID
# @param devices [Array<Planned::Device>] List of devices to include in the RAID
# @return [Proposal::CreatorResult] Result containing the specified RAID
#
# @raise NoDiskSpaceError
def create_md(devicegraph, md, devices)
md_creator = Proposal::AutoinstMdCreator.new(devicegraph)
md_creator.create_md(md, devices)
end
# Creates a bcache in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param bcache [Planned::Bcache] Planned bcache
# @param backing_devname [String, nil] Backing device name
# @param caching_devname [String, nil] Caching device name
# @return [Proposal::CreatorResult] Result containing the specified bcache
def create_bcache(devicegraph, bcache, backing_devname, caching_devname)
if backing_devname.nil?
raise Y2Storage::DeviceNotFoundError, "No backing device for Bcache #{bcache.name}"
end
bcache_creator = Proposal::AutoinstBcacheCreator.new(devicegraph)
bcache_creator.create_bcache(bcache, backing_devname, caching_devname)
end
# Creates a volume group in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param vg [Planned::LvmVg] Volume group
# @param pvs [Array<String>] List of device names of the physical volumes
# @return [Proposal::CreatorResult] Result containing the specified volume group
def create_logical_volumes(devicegraph, vg, pvs)
lvm_creator = Proposal::LvmCreator.new(devicegraph)
lvm_creator.create_volumes(vg, pvs)
rescue RuntimeError => e
log.error e.message
lvm_creator = Proposal::LvmCreator.new(devicegraph)
new_vg = vg.clone
new_vg.lvs = flexible_partitions(vg.lvs)
lvm_creator.create_volumes(new_vg, pvs)
end
# Creates a multi-device Btrfs filesystem in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param planned_filesystem [Planned::Btrfs]
# @param devices [Array<Planned::Device>] devices to include in the Btrfs
#
# @return [Proposal::CreatorResult] Result containing the specified multi-device Btrfs
def create_btrfs(devicegraph, planned_filesystem, devices)
btrfs_creator = Proposal::BtrfsCreator.new(devicegraph)
btrfs_creator.create_filesystem(planned_filesystem, devices)
end
# Creates a NFS filesystem in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param planned_nfs [Planned::Nfs]
#
# @return [Proposal::CreatorResult] Result containing the specified NFS
def create_nfs_filesystem(devicegraph, planned_nfs)
nfs_creator = Proposal::NfsCreator.new(devicegraph)
nfs_creator.create_nfs(planned_nfs)
end
# Creates a Tmpfs filesystem in the given devicegraph
#
# @param devicegraph [Devicegraph] Starting devicegraph
# @param planned_tmpfs [Planned::Tmpfs]
#
# @return [Proposal::CreatorResult] Result containing the specified Tmpfs
def create_tmpfs_filesystem(devicegraph, planned_tmpfs)
tmpfs_creator = Proposal::TmpfsCreator.new(devicegraph)
tmpfs_creator.create_tmpfs(planned_tmpfs)
end
# Finds the Bcache member in the previous result and in the list of devices to use
#
# @return [String, nil] nil if no device is found
def find_bcache_member(bcache_name, role, result, devs_to_reuse)
names = result.created_names { |d| bcache_member_for?(d, bcache_name, role) }
return names.first unless names.empty?
device = devs_to_reuse.find { |d| bcache_member_for?(d, bcache_name, role) }
device&.reuse_name
end
# Determines whether a device plays a given role in a bcache
#
# @param device [Planned::Device] Device to consider
# @param bcache_name [String] bcache name
# @param role [:caching, :backing] Role that the device plays in the bcache device
# @return [Boolean]
def bcache_member_for?(device, bcache_name, role)
query_method = "bcache_#{role}_for?"
device.respond_to?(query_method) && device.send(query_method, bcache_name)
end
# Return devices which can be reused by an MD RAID
#
# @param planned_devices [Planned::DevicesCollection] collection of planned devices
# @return [Array<Planned::Device>]
def reusable_by_md(planned_devices)
planned_devices.select { |d| d.respond_to?(:raid_name) }
end
# Return devices which can be reused by a bcache
#
# @param planned_devices [Planned::DevicesCollection] collection of planned devices
# @return [Array<Planned::Device>]
def reusable_by_bcache(planned_devices)
planned_devices.select { |d| d.respond_to?(:bcache_backing_for) }
end
# Return devices which can be reused by a Btrfs filesystem
#
# @param planned_devices [Planned::DevicesCollection] collection of planned devices
# @return [Array<Planned::Device>]
def reusable_by_btrfs(planned_devices)
planned_devices.select { |d| d.respond_to?(:btrfs_name) }
end
end
end
end