src/lib/y2storage/dump_manager.rb
# Copyright (c) [2018] 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 "singleton"
require "fileutils"
require "yast"
require "y2storage/devicegraph"
require "y2storage/actions_presenter"
require "y2storage/storage_manager"
Yast.import "Mode"
module Y2Storage
# Helper class to manage XML and YAML devicegraph dumps.
# Most of this is handling log rotation.
# This is a singleton class; use DumpManager.instance for all methods.
class DumpManager
include Singleton
include Yast::Logger
# The number of old dump directories to keep.
# This is in addition to the current one.
KEEP_OLD_DUMP_DIRS = 3
def initialize
@initialized = false
end
# Kill (recursively remove) all dump directories.
def kill_all_dump_dirs
Dir.glob(base_dir + "/storage*").each { |dir| FileUtils.remove_dir(dir) }
end
# Reset the dump directories: Rotate any old ones, create a new one and
# start numbering devicegraph dump files from zero.
def reset
rotate_dump_dirs
ensure_dump_dir
@prefix = "00"
@initialized = true
end
# Dump a devicegraph or the actions from an ActionsPresenter to file in
# human-readable format.
#
# If 'file_base_name' is specified, this is used. If it is nil, for
# devicegraphs an appropriate name based on its role (probed, staging) is
# generated, for actions, it is "actions".
#
# In any case, use a numbered prefix to make the sequence of files clear,
# and use the appropriate suffix (filename extension) depending on type.
#
# This will typically result in something like this:
#
# 01-probed.xml
# 01-probed.yml
# 02-staging.xml
# 02-staging.yml
# 03-staging.xml
# 03-staging.yml
# 04-actions.txt
# 05-committed.xml
# 05-committed.yml
#
# The directory to use is chosen according to the YaST mode (installation
# vs. installed system), and the directories are cleared (installation) or
# rotated (installed system) for each program invocation.
#
# @param dump_obj [Y2Storage::Devicegraph, Y2Storage::ActionsPresenter]
# @param file_base_name [String, nil] File base name to use.
#
# @return [String] file base name with numeric prefix actually used
# ("01-probed", "02-staging", ...)
#
# @raise [ArgumentError] for unknown types to dump
#
def dump(dump_obj, file_base_name = nil)
return nil if dump_obj.nil?
if dump_obj.is_a?(Y2Storage::Devicegraph)
dump_devicegraph(dump_obj, file_base_name)
elsif dump_obj.is_a?(Y2Storage::ActionsPresenter)
dump_actions(dump_obj, file_base_name)
elsif mocked_object?(dump_obj)
log.warn("Not dumping #{dump_obj.class}")
else
raise ArgumentError, "Unsupported type to dump: #{dump_obj.class}"
end
end
# Class method for dumping (for convenience).
# @see Y2Storage::DumpManager#dump
def self.dump(dump_obj, file_base_name = nil)
instance.dump(dump_obj, file_base_name)
end
# Dump a devicegraph to both XML and YAML.
#
# Use the specified name as the file base name or, if not specified,
# generate a name based on the role of the devicegraph (probed,
# staging). In any case, use a numbered prefix to make the sequence of
# files clear, and use the appropriate suffix (filename extension)
# depending on type.
#
# @param devicegraph [Y2Storage::Devicegraph] devicegraph to dump
# @param file_base_name [String, nil] File base name to use.
#
# @return [String] file base name with numeric prefix actually used
# ("01-probed", "02-staging", ...)
#
def dump_devicegraph(devicegraph, file_base_name = nil)
file_base_name ||= devicegraph_dump_name(devicegraph)
dump_internal(devicegraph, file_base_name) do |file_base_path|
devicegraph.save(file_base_path + ".xml")
YamlWriter.write(devicegraph, file_base_path + ".yml", record_passwords: false)
end
end
# Dump actions from an ActionsPresenter. This works very much like dumping
# the devicegraph.
#
# @param actions_presenter [ActionsPresenter]
# @param file_base_name [String, nil] File base name to use.
#
# @return [String] file base name with numeric prefix actually used
#
def dump_actions(actions_presenter, file_base_name = nil)
file_base_name ||= "actions"
dump_internal(actions_presenter, file_base_name) do |file_path|
actions_presenter.save(file_path + ".txt")
end
end
# Get a suitable name for dumping for well-known devicegraphs.
#
# @return [String]
def devicegraph_dump_name(devicegraph)
return nil if devicegraph.nil?
return "probed" if devicegraph.equal?(StorageManager.instance.probed)
return "staging" if devicegraph.equal?(StorageManager.instance.staging)
"devicegraph"
end
# Return true if this is some installation mode: installation, update,
# AutoYaST.
#
# @return [Boolean]
def installation?
Yast::Mode.installation || Yast::Mode.update
end
# Return a suitable name for the devicegraph dump directory
# depending on the YaST mode (installation / installed system).
#
# @return [String] directory name with full path
def dump_dir
dir = installation? ? "storage-inst" : "storage"
base_dir + "/" + dir
end
# Return the base directory to put the dump directories in.
#
# @return [String] directory name with full path
def base_dir
if running_as_root?
Yast::Directory.logdir
else
Dir.home + "/.y2storage"
end
end
# Rotate the dump directories, depending on current YaST mode:
#
# During installation (or update or AutoYaST), clear and remove any old
# /var/log/YaST2/storage-inst directory.
#
# In the installed system, keep a number of old dump directories, remove
# any older ones in /var/log/YaST2, and rename the ones to keep:
#
# rm -rf storage-03
# mv storage-02 storage-03
# mv storage-01 storage-02
# mv storage storage-01
#
# This will NOT create any new dump directory.
def rotate_dump_dirs
# Intentionally not calling ensure_initialized here:
# that would rotate the dump dirs twice.
return unless File.exist?(base_dir)
if installation?
kill_old_dump_dirs([File.basename(dump_dir)])
else
dump_dirs = old_dump_dirs.sort
keep_dirs = dump_dirs.shift(KEEP_OLD_DUMP_DIRS)
kill_old_dump_dirs(dump_dirs)
keep_dirs.reverse.each { |dir| rename_old_dump_dir(dir) }
end
end
private
# Lazy initialisation and create initial dump dir
def ensure_initialized
lazy_init
ensure_dump_dir
end
# Lazy initialisation.
def lazy_init
return if @initialized
@initialized = true
log.info("Devicegraph dump directory: #{dump_dir}")
reset
end
# Return the next numeric prefix for the numbered devicegraph files.
# Each call to this increments the number.
#
# @return [String]
def next_prefix
ensure_initialized
@prefix = @prefix.next
@prefix + "-"
end
# Common part for all dump methods.
# Call this with a code block that does the actual dumping:
#
# dump_internal(obj, "base") { |path| obj.save(path + ".xyz") }
#
# @param dump_obj [Object] object to dump
# @param file_base_name [String]
# @param block [Block] code block that does the actual dumping
#
# @return [String] file base name with numeric prefix actually used
#
def dump_internal(dump_obj, file_base_name, &block)
return nil if dump_obj.nil?
return nil unless block_given?
ensure_initialized
short_name = next_prefix + file_base_name
file_base_path = dump_dir + "/" + short_name
dump_class = dump_obj.class.to_s.gsub("Y2Storage::", "")
log.info("Dumping #{dump_class} to #{short_name}")
block.call(file_base_path)
short_name # "01-probed", "02-staging", ...
end
# Return the old devicegraph dump directories for the installed system
# currently found in base_dir: ["storage", "storage-01", "storage-02", ...]
#
# @return [Array<String>] directory names without path
def old_dump_dirs
Dir.entries(base_dir).select do |entry|
entry.start_with?("storage") && entry != "storage-inst"
end
end
# Make sure the current dump directory (and possibly all its parents) is
# created.
def ensure_dump_dir
FileUtils.mkdir_p(dump_dir)
end
def clear_dump_dir
remove_dir(dump_dir) if File.exist?(dump_dir)
ensure_dump_dir
end
# Rename an old dump directory according to this schema:
#
# mv storage-02 storage-03
# mv storage-01 storage-02
# mv storage storage-01
#
# @param old_name [String] old directory name (without path)
def rename_old_dump_dir(old_name)
new_name =
if old_name =~ /[0-9]+$/
old_name.next
else
old_name + "-01"
end
log.info("Rotating devicegraph dump dir #{old_name} to #{new_name}")
File.rename(base_dir + "/" + old_name, base_dir + "/" + new_name)
end
# Kill (recursively remove) old dump directories.
#
# @param dump_dirs [Array<String>] directory names (without path) to remove
def kill_old_dump_dirs(dump_dirs)
dump_dirs.each do |dir|
next unless File.exist?(base_dir + "/" + dir)
log.info("Removing old devicegraph dump dir #{dir}")
FileUtils.remove_dir(base_dir + "/" + dir)
end
end
# Check if this process is running with root privileges
#
# @return [Boolean]
def running_as_root?
Process.euid == 0
end
# Check if an object is some kind of rspec mocked object
# (double or instance_double)
#
# @return [Boolean]
def mocked_object?(obj)
return false unless defined?(RSpec::Mocks::Double)
return true if obj.is_a?(RSpec::Mocks::Double)
return false unless defined?(RSpec::Mocks::InstanceVerifyingDouble)
obj.is_a?(RSpec::Mocks::InstanceVerifyingDouble)
end
end
end