src/lib/y2storage/disk_analyzer.rb
# Copyright (c) [2015-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 "storage"
require "y2packager/repository"
require "y2storage/disk_size"
require "y2storage/blk_device"
require "y2storage/lvm_pv"
require "y2storage/partition_id"
Yast.import "Arch"
module Y2Storage
#
# Class to analyze the disk devices (the storage setup) of the existing system:
# Check the existing disk devices (Dasd or Disk) and their partitions what candidates
# there are to install on, typically eliminate the installation media from that list
# (unless there is no other disk), check if there already are any
# partitions that look like there was a Linux system previously installed
# on that machine, check if there is a Windows partition that could be
# resized.
#
# Some of those operations involve trying to mount the underlying filesystem.
class DiskAnalyzer
include Yast::Logger
# Constructor
#
# @param devicegraph [Devicegraph]
def initialize(devicegraph)
@devicegraph = devicegraph
end
# Whether there is a Windows system
#
# @param disks [Disk, String] disks to analyze. All disks by default.
def windows_system?(*disks)
return false unless windows_architecture?
filesystems_collection(*disks).any?(&:windows_system?)
end
# Partitions containing an installation of MS Windows
#
# This involves mounting any Windows-like partition to check if there are
# some typical directories (/windows/system32).
#
# @param disks [Disk, String] disks to analyze. All disks by default.
# @return [Array<Partition>]
def windows_partitions(*disks)
return [] unless windows_architecture?
windows_filesystems(*disks).flat_map(&:blk_devices)
end
# Partitions with a proper Linux partition Id
#
# @see PartitionId.linux_system_ids
#
# @param disks [Disk, String] disks to analyze. All disks by default.
# @return [Array<Partition>]
def linux_partitions(*disks)
disks_collection(*disks).flat_map(&:linux_system_partitions)
end
# Name of installed systems
#
# @param disks [Disk, String] disks to analyze. All disks by default.
# @return [Array<String>] release names
def installed_systems(*disks)
windows_systems(*disks) + linux_systems(*disks)
end
# Name of installed Windows systems
#
# @param disks [Disk, String] disks to analyze. All disks by default.
# @return [Array<String>] release names
def windows_systems(*disks)
windows_filesystems(*disks).map(&:system_name).compact
end
# Release name of installed Linux systems
#
# @param disks [Disk, String] disks to analyze. All disks by default.
# @return [Array<String>] release names
def linux_systems(*disks)
linux_suitable_filesystems(*disks).map(&:release_name).compact
end
# All fstabs found in the system
#
# Note that all Linux filesystems are considered here, including filesystems over LVM LVs, see
# {#all_linux_suitable_filesystems}.
#
# @return [Array<Fstab>]
def fstabs
@fstabs ||= all_linux_suitable_filesystems.map(&:fstab).compact
end
# All crypttabs found in the system
#
# Note that all Linux filesystems are considered here, including filesystems over LVM LVs, see
# {#all_linux_suitable_filesystems}.
#
# @return [Array<Crypttab>]
def crypttabs
@crypttabs ||= all_linux_suitable_filesystems.map(&:crypttab).compact
end
# Disks that are suitable for installing Linux.
#
# Finds devices (disk devices and software RAIDs) that are suitable for installing Linux
#
# From fate#326573 on, software RAIDs with partition table or without children are also
# considered as valid candidates. But since that's a controversial behavior that has
# caused collateral problems (e.g. bsc#1166258), those software RAIDs are only taken
# into account when booting in EFI mode.
#
# @return [Array<BlkDevice>] candidate
def candidate_disks
return @candidate_disks if @candidate_disks
@candidate_disks = candidate_software_raids + candidate_disk_devices
log.info("Found candidate disks: #{@candidate_disks}")
@candidate_disks
end
# Look up devicegraph element by device name.
#
# @return [Device]
def device_by_name(name)
# Using BlkDevice because it is necessary to search in both, Dasd and Disk.
BlkDevice.find_by_name(devicegraph, name)
end
private
# @return [Devicegraph]
attr_reader :devicegraph
# Whether the architecture of the system is supported by MS Windows
#
# @return [Boolean]
def windows_architecture?
# Should we include ARM here?
Yast::Arch.x86_64 || Yast::Arch.i386
end
# Obtains a list of disk devices, software RAIDs, and bcaches
#
# @see #default_disks_collection for default values when disks are not given
#
# @param disks [Array<BlkDevice, String>] blk device to analyze.
# @return [Array<BlkDevice>] a list of blk devices
def disks_collection(*disks)
return default_disks_collection if disks.empty?
disks.map! { |d| d.is_a?(String) ? BlkDevice.find_by_name(devicegraph, d) : d }
disks.compact
end
# The default disks collection to be analyzed
#
# @note software RAIDs and bcache also could be analyzed because it is possible to find a Linux
# system installed on them.
#
# @see #disks_collection
def default_disks_collection
devicegraph.disk_devices + devicegraph.software_raids + devicegraph.bcaches
end
# All partitions from the given disks
#
# @see #disks_collection
#
# @param disks [Array<BlkDevice, String>]
# @return [Array<Partition>]
def partitions_collection(*disks)
disks_collection(*disks).flat_map(&:partitions)
end
# All filesystems from the given disks
#
# @see #disks_collection
# @see #partitions_collection
#
# @param disks [Array<BlkDevice, String>]
# @return [Array<Filesystems::BlkFilesystem>]
def filesystems_collection(*disks)
blk_devices = disks_collection(*disks) + partitions_collection(*disks)
blk_devices.map(&:filesystem).compact
end
# All filesystems that contain a Windows system from the given disks
#
# @see #filesystems_collection
#
# @param disks [Array<BlkDevice, String>]
# @return [Array<Filesystems::BlkFilesystem>]
def windows_filesystems(*disks)
filesystems_collection(*disks).select(&:windows_system?)
end
# All filesystems that could contain Linux system from the given disks
#
# Note that filesystems over LVM LVs are not included.
#
# @see #filesystems_collection
#
# @param disks [Array<BlkDevice, String>]
# @return [Array<Filesystems::BlkFilesystem>]
def linux_suitable_filesystems(*disks)
filesystems_collection(*disks).select(&:root_suitable?)
end
# All filesystems that could contain a Linux system
#
# Note that {#linux_suitable_filesystems} does not take into account filesystems over a LVM LV.
#
# @return [Array<Filesystems::Base>]
def all_linux_suitable_filesystems
@all_linux_suitable_filesystems ||= devicegraph.filesystems.select(&:root_suitable?)
end
# Finds software RAIDs that are considered valid candidates for a Linux installation
#
# Apart from matching conditions of #candidate_disk?, a valid software RAID candidate must
# either, have a partition table or do not have children.
#
# See {#candidate_disks} for extra explanations (e.g. the relevance of EFI) and for
# Fate/Bugzilla references.
#
# @return [Array<Md>]
def candidate_software_raids
return [] unless arch.efiboot?
devicegraph.software_raids.select do |md|
(md.partition_table? || md.children.empty?) && candidate_disk?(md)
end
end
# Finds disk devices that are considered valid candidates
#
# Basically, all available disk devices except those that are part of a candidate software RAID.
#
# @return [Array<BlkDevice>]
def candidate_disk_devices
rejected_disk_devices = candidate_software_raids.map(&:ancestors).flatten
candidate_disk_devices = devicegraph.disk_devices.select { |d| candidate_disk?(d) }
candidate_disk_devices - rejected_disk_devices
end
# Checks whether a device can be used as candidate disk for installation
#
# A device is candidate for installation if no filesystem belonging to the device is mounted and the
# device does not contain a repository for installation.
#
# @param device [BlkDevice]
# @return [Boolean]
def candidate_disk?(device)
!contain_mounted_filesystem?(device) &&
!contain_installation_repository?(device)
end
# Checks whether a device contains a mounted filesystem
#
# @see #device_filesystems, #mounted_filesystem?
#
# @param device [BlkDevice]
# @return [Boolean]
def contain_mounted_filesystem?(device)
device_filesystems(device).any? { |f| mounted_filesystem?(f) }
end
# All filesystems inside a device
#
# The device could be directly formatted or the filesystem could belong to a partition inside the
# device. Moreover, when the device (on any of its partitions) is used as LVM PV, all filesystem
# inside the LVM VG are considered as belonging to the device.
#
# @param device [BlkDevice]
# @return [Array<BlkFilesystem>]
def device_filesystems(device)
device.descendants.select { |d| d.is?(:blk_filesystem) }
end
# Checks whether a filesystem is currently mounted
#
# @param filesystem [Filesystems::Base]
# @return [Boolean]
def mounted_filesystem?(filesystem)
filesystem.active_mount_point?
end
# Checks whether a device contains an installation repository
#
# A device contains an installation repository if the device or any of its descendant devices
# is included in the list of devices from the installation repository URI. For example, if the
# URI is "hd:/subdir?device=/dev/sda1", then "/dev/sda" contains an installation repository.
#
# @param device [BlkDevice]
# @return [Boolean]
def contain_installation_repository?(device)
repositories_names = repositories_devices.map(&:name)
all_devices_from_device(device).any? { |d| repositories_names.include?(d.name) }
end
# All blk devices defined from a device, including the given device
# (e.g., a disk and all its partitions)
#
# Note that when a device contains a partition being used as LVM PV, all LVM LVs are included.
#
# @param device [BlkDevice]
# @return [Array<BlkDevice>]
def all_devices_from_device(device)
devices = device.descendants.select { |d| d.is?(:blk_device) }
devices.prepend(device)
end
# Devices whose name is indicated in the URI of the installation repositories
#
# @return [Array<BlkDevice>]
def repositories_devices
return @repositories_devices if @repositories_devices
devices = repositories_device_names.map { |n| devicegraph.find_by_any_name(n) }.compact
@repositories_devices = devices
end
# Device names indicated in the URI of the installation repositories
#
# @see #local_repositories
#
# @return [Array<String>]
def repositories_device_names
return @repositories_device_names if @repositories_device_names
names = local_repositories.flat_map { |r| repository_device_names(r) }.uniq
@repositories_device_names = names
end
# TODO: This method should be moved to Y2Packager::Repository class
#
# Device names indicated in the URI of an installation repository
#
# For example:
# "hd:/subdir?device=/dev/sda1&filesystem=reiserfs" => ["/dev/sda1"]
# "dvd:/?devices=/dev/sda,/dev/sdb" => ["/dev/sda", "/dev/sdb"]
#
# @param repository [Y2Packager::Repository]
# @return [Array<String>]
def repository_device_names(repository)
match_data = repository.url.to_s.match(/.*devices?=([^&]*)/)
return [] unless match_data
match_data[1].split(",").map(&:strip)
end
# Local repositories used during installation
#
# @return [Array<Y2Packager::Repository>]
def local_repositories
Y2Packager::Repository.all.select(&:local?)
end
# Current architecture
#
# @return [Y2Storage::Arch]
def arch
@arch ||= StorageManager.instance.arch
end
end
end