src/lib/y2partitioner/actions/controllers/add_partition.rb
# Copyright (c) [2017-2021] 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"
require "y2partitioner/actions/controllers/base"
module Y2Partitioner
module Actions
module Controllers
# This class stores information about a future partition so that information
# can be shared across the different dialogs of the process. It also takes
# care of updating the devicegraph when needed.
class AddPartition < Base
include Yast::I18n
# @return [Y2Storage::PartitionType]
attr_accessor :type
# @return [:max_size,:custom_size,:custom_region]
attr_accessor :size_choice
# for {#size_choice} == :custom_size
# @return [Y2Storage::DiskSize]
attr_accessor :custom_size
# for any {#size_choice} value this ends up with a valid value
# @return [Y2Storage::Region]
attr_accessor :region
# New partition created by the controller.
#
# Nil if #create_partition has not beeing called or if the partition was
# removed with #delete_partition.
#
# @return [Y2Storage::Partition, nil]
attr_reader :partition
# Name of the device being partitioned
# @return [String]
attr_reader :device_name
def initialize(device_name)
super()
textdomain "storage"
@device_name = device_name
end
# Device being partitioned
# @return [Y2Storage::BlkDevice]
def device
Y2Storage::BlkDevice.find_by_name(current_graph, device_name)
end
# Available slots to create the partition in which the start is aligned
# according to AlignType::OPTIMAL (the end is not aligned)
#
# @see unused_slots
#
# @return [Array<Y2Storage::PartitionTables::PartitionSlot>]
def unused_optimal_slots
# Caching seems to be important for the current dialogs to work
@unused_optimal_slots ||=
if device.can_have_ptable?
device.ensure_partition_table.unused_partition_slots
else
[]
end
end
# All available slots to create the partition, honoring just the
# required alignment
#
# @see optimal_unused_slots
#
# @return [Array<Y2Storage::PartitionTables::PartitionSlot>]
def unused_slots
# Caching seems to be important for the current dialogs to work
@unused_slots ||=
if device.can_have_ptable?
device.ensure_partition_table.unused_partition_slots(
Y2Storage::AlignPolicy::ALIGN_START_KEEP_END, Y2Storage::AlignType::REQUIRED
)
else
[]
end
end
# Grain to use in order to keep the optimal alignment
#
# @return [Y2Storage::DiskSize]
def optimal_grain
device.ensure_partition_table.align_grain
end
# Grain to use in order to keep the required alignment
#
# @return [Y2Storage::DiskSize]
def required_grain
device.ensure_partition_table.align_grain(Y2Storage::AlignType::REQUIRED)
end
# Creates the partition in the device according to the controller
# attributes (#type, #region, etc.)
def create_partition
ptable = device.ensure_partition_table
slot = slot_for(region)
aligned = align(region, slot, ptable)
@partition = ptable.create_partition(slot.name, aligned, type)
end
# Removes the previously created partition from the device
def delete_partition
return if @partition.nil?
ptable = device.ensure_partition_table
ptable.delete_partition(@partition)
@partition = nil
end
# Removes the filesystem when the device is directly formatted
#
# Cached values must be reset (e.g., available s)
def delete_filesystem
device.delete_filesystem
reset
end
# Whether the device is in use
#
# @note A device is in use when it is used as physical volume or
# belongs to a MD RAID.
#
# @return [Boolean]
def device_used?
device.partition_table.nil? && device.descendants.any? { |d| d.is?(:lvm_pv, :md) }
end
# Whether the device is formatted
#
# @return [Boolean]
def device_formatted?
device.formatted?
end
# Whether the device is currently mounted in the system
#
# @return [Boolean]
def device_mounted?
return false unless committed_filesystem
committed_filesystem.active_mount_point?
end
# Committed version of the current filesystem
#
# @return [Y2Storage::BlkFilesystem, nil] nil if the device is not currently formatted or the
# the current filesystem does not exist on disk yet.
def committed_filesystem
return nil unless device.formatted?
system_devicegraph.find_device(device.filesystem.sid)
end
# Whether is possible to create any new partition in the device
#
# @return [Boolean]
def new_partition_possible?
unused_optimal_slots.any?(&:available?)
end
# Available partition types for new partitions according to
# the available unused slots
#
# @return [Array<Y2Storage::PartitionType>]
def available_partition_types
Y2Storage::PartitionType.all.select do |partition_type|
unused_optimal_slots.any? { |s| s.possible?(partition_type) }
end
end
# Title to display in the dialogs during the process
# @return [String]
def wizard_title
# TRANSLATORS: dialog title. %s is a device name like /dev/sda
_("Add Partition on %s") % device_name
end
# Error to display to the user if the blocks selected to define a
# custom region are not valid.
#
# The string, if any, is already internationalized.
#
# @return [String, nil] nil if the blocks are valid (no error)
def error_for_custom_region(start_block, end_block)
parent = unused_slots.map(&:region).find { |r| r.cover?(start_block) }
if !parent
# starting block must be in a region,
# TRANSLATORS: text for an error popup
_("The block entered as start is not available.")
elsif end_block < start_block
# TRANSLATORS: text for an error popup
_("The end block cannot be before the start.")
elsif !parent.cover?(end_block)
# ending block must be in the same region than the start
# TRANSLATORS: text for an error popup
_("The region entered collides with the existing partitions.")
elsif too_small_custom_region?(start_block, end_block)
# It's so small that we already know that we can't align both
# start and end
# TRANSLATORS: text for an error popup
_("The region entered is too small, not supported by this device.")
elsif !alignable_custom_region?(start_block, end_block)
# Almost pathological case, but still can happen if the user tries
# to break stuff
# TRANSLATORS: text for an error popup
_("Invalid region entered, increase the size or align the start.")
end
end
protected
# Resets cached values
#
# It is required when the device usage has changed (i.e., initially the
# device was directly formatted).
def reset
@unused_slots = nil
@unused_optimal_slots = nil
end
# Partition slot containing the given region
#
# @param region [Y2Storage::Region] region for the new partition
# @return [Y2Storage::PartitionTables::PartitionSlot, nil] nil if the
# region is not contained in any of the relevant slots
def slot_for(region)
slots = align_only_to_required? ? unused_slots : unused_optimal_slots
slots.find { |slot| region.inside?(slot.region) }
end
# Aligns the region that will be used to create a new partition,
# according to the following partitioner logic:
#
# * If the user specified a size, region.start is already granted to
# be aligned according to OPTIMAL and this method:
# * Leaves region.end untouched if it's equal to the end of the
# slot, because that means the user wants to use the whole space
# until the next "barrier" (the end of the device or the start of an
# existing partition) with no gap.
# * Aligns region.end to OPTIMAL if it's smaller then the end of the
# slot, because that means the user wants to leave some space and
# we want that remaining space to start in an optimal block.
# * If the user specified a custom region, region.start and region.end
# are aligned according only to REQUIRED, not taking the optimal
# performance into consideration. In most cases that implies no
# changes but in some devices (like DASD) that small alignment makes
# the creation possible.
#
# @param region [Y2Storage::Region] a region describing the intended
# location of the new partition
# @param slot [Y2Storage::PartitionTables::PartitionSlot] the slot
# to be used as base for the partition creation
# @param ptable [Y2Storage::PartitionTables::Base]
# @return [Y2Storage::Region] aligned according to the description above
def align(region, slot, ptable)
if align_only_to_required?
ptable.align(
region, Y2Storage::AlignPolicy::ALIGN_START_AND_END, Y2Storage::AlignType::REQUIRED
)
else
# Let's aim for optimal alignment or for usage of the whole space
ptable.align_end(region, max_end: slot.region.end)
end
rescue Storage::AlignError
# If something goes wrong during alignment, just try to create the
# smallest (thus, safer) possible partition with a valid start.
# Anyway, the validations in the dialog should prevent any possible
# alignment error, so this should never be reached.
grain = ptable.align_grain(Y2Storage::AlignType::REQUIRED)
blk_size = region.block_size.to_i
Y2Storage::Region.create(slot.region.start, grain.to_i / blk_size, blk_size)
end
# Whether the resulting partition should be aligned only to mandatory
# requirements (Y2Storage::AlignType::REQUIRED) ignoring any performance
# consideration.
#
# @return [Boolean] false if the partition should be aligned for optimal
# performance (i.e. honoring Y2Storage::AlignType::OPTIMAL)
def align_only_to_required?
# If the user defined a custom region, let's respect it as much as
# possible, so only the REQUIRED alignment is enforced.
size_choice == :custom_region
end
# @return [Y2Storage::DiskSize]
def block_size
unused_slots.first.region.block_size
end
# Whether the given custom region can be aligned to hardware
# requirements without disappearing in the process.
#
# @return [Boolean]
def alignable_custom_region?(start_blk, end_blk)
region = Y2Storage::Region.create(start_blk, end_blk - start_blk + 1, block_size)
device.ensure_partition_table.align(
region, Y2Storage::AlignPolicy::ALIGN_START_AND_END, Y2Storage::AlignType::REQUIRED
)
true
rescue Storage::AlignError
false
end
# Whether the given custom region is too small to be aligned to hardware
# requirements.
#
# @return [Boolean]
def too_small_custom_region?(start_blk, end_blk)
size = block_size * (end_blk - start_blk + 1)
size < required_grain
end
# Devicegraph that represents the current version of the devices in the system
#
# @note This is not the same than {Base#system_graph}. To check whether a
# filesystem is currently mounted, it must be checked in the real system
# devicegraph. When a mount point is "immediate deactivated", the
# mount point is set as inactive only in the system devicegraph.
#
# @return [Y2Storage::Devicegraph]
def system_devicegraph
Y2Storage::StorageManager.instance.system
end
end
end
end
end