src/lib/y2storage/storage_manager.rb
# Copyright (c) [2015-2022] 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 "storage"
require "y2storage/arch"
require "y2storage/fake_device_factory"
require "y2storage/devicegraph"
require "y2storage/probed_devicegraph_checker"
require "y2storage/devicegraph_sanitizer"
require "y2storage/disk_analyzer"
require "y2storage/dump_manager"
require "y2storage/callbacks"
require "y2storage/hwinfo_reader"
require "y2storage/configuration"
require "y2storage/storage_env"
require "yast2/fs_snapshot"
require "y2issues/list"
Yast.import "Mode"
Yast.import "Stage"
module Y2Storage
# Singleton class to provide access to the libstorage Storage object and
# to store related state information.
#
# FIXME: This class contains some responsibilities (and code) that could
# be extracted to a new place, mainly all stuff related to testing
# (e.g., {#probe_from_yaml}).
#
class StorageManager # rubocop:disable Metrics/ClassLength
include Yast::Logger
extend Forwardable
# Libstorage object
#
# Calls to several methods (e.g., #environment and #rootprefix) are forwarded to this object.
#
# @return [Storage::Storage]
attr_reader :storage
# Revision of the staging devicegraph.
#
# Zero means no modification (still not probed). Incremented every
# time the staging devicegraph is re-assigned.
# @see #copy_to_staging
# @see #staging_changed
#
# @return [Integer]
attr_reader :staging_revision
# Proposal that was used to calculate the current staging devicegraph.
#
# Nil if the devicegraph was set manually and not by accepting a proposal.
#
# @return [GuidedProposal, nil]
attr_reader :proposal
def_delegators :@storage, :environment, :rootprefix, :prepend_rootprefix, :rootprefix=
# @!method rootprefix
# @return [String] root prefix used by libstorage
# @!method rootprefix=(path)
# Sets the root prefix used by libstorage in subsequent operations
# @param path [String]
# @!method prepend_rootprefix(path)
# Prepends the current libstorage root prefix to a path, if necessary
# @param path [String] original path (without prefix)
# @return [String]
# @param storage_environment [::Storage::Environment]
def initialize(storage_environment)
@storage = Storage::Storage.new(storage_environment)
configuration.apply_defaults
@probed = false
@activate_issues = Y2Issues::List.new
@probe_issues = Y2Issues::List.new
reset_probed
reset_staging
reset_staging_revision
end
# Current architecture
#
# @return [Y2Storage::Arch]
def arch
@arch ||= Arch.new(@storage.arch)
end
# Whether probing has been done
# @return [Boolean]
def probed?
@probed
end
# Increments #staging_revision
#
# To be called explicitly if the staging devicegraph is modified without
# using #staging= or #proposal=
def increase_staging_revision
@staging_revision += 1
end
# Activate devices like multipath, MD and DM RAID, LVM and LUKS. It is not
# required to have probed the system to call this function. On the other
# hand, after calling this function the system should be probed.
#
# With the default callbacks, every question about activating a given
# technology is forwarded to the user using pop up dialogs. In addition,
# errors reported by libstorage-ng are stored in the {#activate_issues} list.
#
# @param callbacks [Callbacks::Activate, nil]
# @return [Boolean] whether activation was successful
def activate(callbacks = nil)
activate_callbacks = callbacks || Callbacks::Activate.new
@storage.activate(activate_callbacks)
@activate_issues = activate_callbacks.issues
true
rescue Storage::Exception
false
end
# Deactivate devices like multipath, MD and DM RAID, LVM and LUKS. It is
# not required to have probed the system to call this function. On the
# other hand after calling this function the system should be probed.
#
# @return [Storage::DeactivateStatus] status of subsystems, see
# libstorage-ng documentation for details.
def deactivate
@storage.deactivate
end
# Probes all storage devices
#
# If this method returns false, #staging and #probed could be in bad state
# (or not be there at all) so they should not be trusted in the subsequent
# code.
#
# @see #probe!
#
# @param callbacks [Callbacks::UserProbe, nil]
# @return [Boolean] whether probing was successful, false if libstorage-ng
# found a problem and the corresponding callback returned false (i.e. it
# was decided to abort due to the error)
def probe(callbacks = nil)
probe!(callbacks)
true
rescue Storage::Exception, Yast::AbortException => e
log.error("ERROR: #{e.message}")
false
end
# Probes all storage devices
#
# Invalidates the probed and staging devicegraph. Real probing is
# only performed when the instance is not for testing.
#
# With the default probe callbacks, the errors reported by libstorage-ng are stored in the
# {#probe_issues} list.
#
# @raise [Storage::Exception, Yast::AbortException] when probe fails
#
# @param callbacks [Callbacks::Probe, nil]
def probe!(callbacks = nil)
probe_callbacks = Callbacks::Probe.new(user_callbacks: callbacks)
begin
@storage.probe(probe_callbacks)
rescue Storage::Aborted
retry if probe_callbacks.again?
raise
end
@probe_issues = probe_callbacks.issues
probe_performed
manage_probing_issues(callbacks)
DumpManager.dump(@probed_graph)
nil
end
# Probed devicegraph, after sanitizing it (see {#manage_probing_issues})
#
# @note This devicegraph is not exactly the same than the initial
# raw probed returned by libstorage-ng. The raw probed can contain
# some errors (e.g., incomplete LVM VGs). This probed devicegraph
# is the result of sanitizing the initial raw probed.
#
# @raise [Storage::Exception, Yast::AbortException] when probe fails
#
# @return [Devicegraph]
def probed
probe! unless probed?
@probed_graph
end
# Probed devicegraph returned by libstorage-ng (without sanitizing)
#
# @see #probed
#
# @return [Devicegraph]
def raw_probed
@raw_probed ||= begin
probe unless probed?
Devicegraph.new(storage.probed)
end
end
# Staging devicegraph
#
# @note The initial staging is not exactly the same than the initial staging
# returned by libstorage-ng. This staging is initialized from the sanitized
# probed devicegraph (see {#manage_probing_issues}).
#
# @raise [Storage::Exception, Yast::AbortException] when probe fails
#
# @return [Devicegraph]
def staging
@staging ||= begin
probe! unless probed?
Devicegraph.new(storage.staging)
end
end
# Copies the manually-calculated (no proposal) devicegraph to staging.
#
# If the devicegraph was calculated by means of a proposal, use #proposal=
# instead.
# @see #proposal=
#
# @param [Devicegraph] devicegraph to copy
def staging=(devicegraph)
copy_to_staging(devicegraph)
end
# System devicegraph
#
# It is used to perform actions beforme the commit phase (e.g., immediate unmount).
#
# @return [Y2Storage::Devicegraph]
def system
@system ||= Devicegraph.new(storage.system)
end
# Stores the proposal, modifying the staging devicegraph and all the related
# information.
#
# If the proposal failed, it resets the staging devicegraph to the values of the probed one.
#
# @param proposal [GuidedProposal]
def proposal=(proposal)
if proposal.failed?
copy_to_staging(probed)
else
copy_to_staging(proposal.devices)
end
@proposal = proposal
end
# Disk analyzer used to analyze the probed devicegraph
#
# @return [DiskAnalyzer]
def probed_disk_analyzer
@probed_disk_analyzer ||= DiskAnalyzer.new(probed)
end
# Checks whether the staging devicegraph has been previously set, either
# manually or through a proposal.
#
# @return [Boolean] false if the staging devicegraph is just the result of
# probing (so a direct copy of #probed), true otherwise.
def staging_changed?
staging_revision != staging_revision_after_probing
end
# Checks whether the staging devicegraph has been committed to the system.
#
# @see #commit
#
# If this is false, the probed devicegraph (see {#probed}) should perfectly
# match the real current system... as long as the system has not been
# modified externally to YaST, which is impossible to control.
#
# @return [Boolean]
def committed?
@committed
end
# Performs in the system all the necessary operations to make it match the staging devicegraph.
#
# Beware: this method can cause data loss
#
# The user is asked whether to continue on each error reported by libstorage-ng.
#
# @param force_rw [Boolean] if mount points should be forced to have read/write permissions.
# @param callbacks [Storage::CommitCallbacks]
#
# @return [Boolean] whether commit was successful, false if libstorage-ng found a problem and it was
# decided to abort.
def commit(force_rw: false, callbacks: nil)
# Tell FsSnapshot whether Snapper should be configured later
Yast2::FsSnapshot.configure_on_install = configure_snapper?
callbacks ||= Callbacks::Commit.new
staging.pre_commit
storage.calculate_actiongraph
commit_options = ::Storage::CommitOptions.new(force_rw)
# Save committed devicegraph into logs
log.info("Committed devicegraph\n#{staging.to_xml}")
DumpManager.dump(staging, "committed")
# Log libstorage-ng checks
staging.check
storage.commit(commit_options, callbacks)
staging.post_commit
@committed = true
rescue Storage::Exception
false
end
# Probes from a yml file instead of doing real probing
def probe_from_yaml(yaml_file = nil)
fake_graph = Devicegraph.new(storage.create_devicegraph("fake"))
Y2Storage::FakeDeviceFactory.load_yaml_file(fake_graph, yaml_file) if yaml_file
fake_graph.to_storage_value.copy(storage.probed)
fake_graph.to_storage_value.copy(storage.staging)
fake_graph.to_storage_value.copy(storage.system)
probe_performed
manage_probing_issues
ensure
storage.remove_devicegraph("fake")
end
# Probes from a xml file instead of doing real probing
def probe_from_xml(xml_file)
storage.probed.load(xml_file)
storage.probed.copy(storage.staging)
storage.probed.copy(storage.system)
probe_performed
manage_probing_issues
end
# Access mode in which the storage system was initialized (read-only or read-write)
#
# @see StorageManager.setup
#
# @return [Symbol] :ro, :rw
def mode
environment.read_only? ? :ro : :rw
end
# Whether there is any device in the system that may be used to install a
# system.
#
# This method does not check sizes or any other property of the devices.
# It performs a very simple check and returns true if there is any device
# of one of the acceptable types (basically disks or DASDs).
#
# It will never trigger a hardware probing. The method works even if
# such probing has not been performed yet.
#
# @return [Boolean]
def devices_for_installation?
if probed?
!probed.disk_devices.empty?
else
begin
Storage.light_probe
rescue Storage::Exception
false
end
end
end
# Configuration of Y2Storage
#
# @return [Configuration]
def configuration
@configuration ||= Configuration.new(@storage)
end
private
# Value of #staging_revision right after executing the latest libstorage
# probing.
#
# Used to check if the system has been re-probed
#
# @return [Integer]
attr_reader :staging_revision_after_probing
# Issues detected while activating devices
#
# @return [Y2Issues::List<Issue>]
attr_reader :activate_issues
# Issues detected while probing the system
#
# @return [Y2Issues::List<Issue>]
attr_reader :probe_issues
# Sets the devicegraph as the staging one, updating all the associated
# information like #staging_revision
#
# @param [Devicegraph] devicegraph to copy
def copy_to_staging(devicegraph)
devicegraph.safe_copy(staging)
staging_changed
end
# Invalidates previous probed devicegraph and its related data
def reset_probed
# Invalidate probed and its two derivative devicegraphs
@raw_probed = @probed_graph = @system = nil
@probed_disk_analyzer = nil
@committed = false
Y2Storage::HWInfoReader.instance.reset
end
alias_method :probed_changed, :reset_probed
# Invalidates previous staging devicegraph and its related data
def reset_staging
@staging = nil
@proposal = nil
end
# Sets all necessary data after changing the staging devicegraph. To be executed
# always after a staging assignment
def staging_changed
reset_staging
increase_staging_revision
end
# Sets all necessary data after probing. To be executed always after probing.
def probe_performed
@probed = true
probed_changed
staging_changed
# Save probed devicegraph into logs
log.info("Probed devicegraph\n#{raw_probed.to_xml}")
@staging_revision_after_probing = staging_revision
# Probing issues will contain issues detected on activate and probe callbacks, and also issues
# detected after checking the probed devicegraph.
issues = activate_issues.concat(probe_issues)
issues.concat(ProbedDevicegraphChecker.new(raw_probed).issues)
raw_probed.probing_issues = issues
end
# Resets the #staging_revision
def reset_staging_revision
@staging_revision = 0
@staging_revision_after_probing = 0
end
# Manages issues detected during the probing phase
#
# The raw probed devicegraph is sanitized in order to fix the issues (e.g., when there are incomplete
# LVM VGs).
#
# The raw probed devicegraph remains untouched, and the new sanitized one is internally saved and
# copied into the staging devicegraph.
#
# @param callbacks [Callbacks::UserProbe,nil]
# @raise [Yast::AbortException] if the user decides to not continue. In that case, the probed
# and staging devicegraphs also remain untouched, but they are useless for
# proposal/partitioner.
def manage_probing_issues(callbacks = nil)
probing_issues = raw_probed.probing_issues
continue = true
if !StorageEnv.instance.ignore_probe_errors? && probing_issues.any?
callbacks ||= Callbacks::YastProbe.new
continue = callbacks.report_issues(raw_probed.probing_issues)
end
raise Yast::AbortException, "Devicegraph contains errors. User has aborted." unless continue
sanitizer = DevicegraphSanitizer.new(raw_probed)
@probed_graph = sanitizer.sanitized_devicegraph
@probed_graph.safe_copy(staging)
# Save sanitized devicegraph into logs
log.info("Sanitized probed devicegraph\n#{probed.to_xml}")
end
# Whether the final steps to configure Snapper should be performed by YaST
# at the end of the installation process.
#
# @return [Boolean]
def configure_snapper?
if !Yast::Mode.installation || !Yast::Stage.initial
log.info "Not a fresh installation. Don't configure Snapper."
return false
end
root = staging.filesystems.find(&:root?)
if !root
log.info "No root filesystem in staging. Don't configure Snapper."
return false
end
if !root.respond_to?(:configure_snapper)
log.info "The root filesystem can't configure snapper."
return false
end
log.info "Configure Snapper? #{root.configure_snapper}"
root.configure_snapper
end
# Class methods
class << self
# Initializes storage with a specific access mode (read-only or read-write)
#
# With the default callbacks, the user is asked whether to retry or abort
# when lock cannot be acquired.
#
# @raise [AccessModeError] if the requested mode is incompatible with the
# already created instance (i.e., current instance is ro but rw is requested).
#
# @param mode [Symbol, nil] :ro, :rw. If nil, a default mode is used, see
# {.default_storage_mode}.
# @param callbacks [Callbacks::Initialize, nil]
#
# @return [Boolean] true if the storage instance was correctly created for
# the given mode; false otherwise.
def setup(mode: nil, callbacks: nil)
# In case of mode is not given, it is necessary to initialize it with the
# default mode due to {.instance} without mode returns the current storage
# instance. In some cases, the current instance might not be valid for the
# default mode (e.g., current is read-only but {.setup} is called without
# mode during installation).
mode ||= default_storage_mode
Y2Storage::StorageManager.instance(mode: mode, callbacks: callbacks)
true
rescue Yast::AbortException
false
end
# Returns the singleton instance.
#
# In the first call, it will create a libstorage instance (using common
# defaults) if there isn't one yet.
#
# With the default callbacks, the user is asked whether to retry or abort
# when lock cannot be acquired.
#
# @see .create_instance if you need special parameters for creating the
# libstorage instance.
# @see .create_test_instance if you just need to create an instance that
# ensures not real hardware probing, even calling to #probe.
#
# @raise [AccessModeError] if the requested mode is incompatible with the
# already created instance (i.e., current instance is ro but rw is requested).
#
# @raise [Yast::AbortException] if the storage lock cannot be acquired and
# the user decides to abort.
#
# @param mode [Symbol, nil] :ro, :rw. If nil, a default mode is used, see
# {.default_storage_mode}.
# @param callbacks [Callbacks::Initialize, nil]
#
# @return [StorageManager]
def instance(mode: nil, callbacks: nil)
return @instance if @instance && mode.nil?
mode ||= default_storage_mode
if @instance
return @instance if valid_instance?(mode)
raise AccessModeError,
"Unexpected storage mode: current is #{@instance.mode}, requested is #{mode}"
else
read_only = mode == :ro
create_instance(Storage::Environment.new(read_only), callbacks)
end
end
# Creates the singleton instance with a customized libstorage object.
#
# Create your own Storage::Environment for custom purposes like mocking
# the hardware probing etc.
#
# If no Storage::Environment is provided, it uses a default one that
# allows hardware probing.
# With the default callbacks, the user is asked whether to retry or abort
# when lock cannot be acquired.
#
# @raise [Yast::AbortException] if lock cannot be acquired and the user
# decides to abort.
# Several process can access in read-only mode at the same time, but only
# one process can access in read-write mode. If a process is accessing in
# read-write mode, no other process can create a new instance.
#
# @param environment [Storage::Environment, nil]
# @param callbacks [Callbacks::Initialize, nil]
#
# @return [StorageManager] singleton instance
def create_instance(environment = nil, callbacks = nil)
environment ||= Storage::Environment.new(true)
create_logger
log.info "Creating Storage object"
@instance = new(environment)
rescue Storage::LockException => e
raise Yast::AbortException unless retry_create_instance?(e, callbacks)
retry
end
# Creates the singleton instance for testing.
# This instance avoids to perform real probing or commit.
#
# @return [StorageManager] singleton instance
def create_test_instance
create_instance(test_environment)
end
# Make sure only .instance can be used to create objects
private :new, :allocate
private
def test_environment
read_only = true
Storage::Environment.new(read_only, Storage::ProbeMode_NONE, Storage::TargetMode_DIRECT)
end
def create_logger
# Store the reference in a class instance variable to prevent the
# garbage collector from cleaning too much
@logger = StorageLogger.new
::Storage.logger = @logger
end
# Default access mode (read-only or read-write)
#
# @note During installation, access mode should be read-write.
#
# @return [Symbol] :ro, :rw
def default_storage_mode
Yast::Mode.installation ? :rw : :ro
end
# Checks whether the current instance can be used for the requested access mode
#
# A read-write instance can be always used independendly of the requested mode.
# In case of a read-only instance, it is only valid if requested mode is read-only.
#
# @note The instance is always considered as valid when it is created for
# testing purposes.
#
# @param requested_mode [Symbol] :ro, :rw
# @return [Boolean]
def valid_instance?(requested_mode)
return false unless @instance
return true if test_instance?
@instance.mode == :rw || requested_mode == :ro
end
# Whether the current instance is for testing
#
# @return [Boolean]
def test_instance?
@instance.environment.probe_mode == Storage::ProbeMode_NONE
end
# Whether the user decides to retry the creation of the instance
#
# It is used when intitial creation could not be done due to lock errors.
#
# @see create_instance
#
# @param error [Storage::LockException]
# @param callbacks [Callbacks::Initialize]
#
# @return [Boolean] true if the user decides to retry.
def retry_create_instance?(error, callbacks)
callbacks ||= Callbacks::Initialize.new(error)
callbacks.retry?
end
end
# Logger class for libstorage. This is needed to make libstorage log to the
# y2log.
class StorageLogger < ::Storage::Logger
# rubocop:disable Metrics/ParameterLists
def write(level, component, filename, line, function, content)
# libstorage pretent that it use same logging as y2_logger but y2_logger support also
# parameter expansion via printf, so we need double escaping to prevent this expansion
# (bsc#1091062)
content = content.gsub(/%/, "%%")
Yast.y2_logger(level, component, filename, line, function, content)
end
# rubocop:enable Metrics/ParameterLists
end
end
end