Skip to content

Commit

Permalink
New Y2Storage proposal for Agama (#1552)
Browse files Browse the repository at this point in the history
In order to implement the unattended behavior for storage described at
[auto_storage.md](https://github.com/openSUSE/agama/blob/master/doc/auto_storage.md),
we decided we need a new kind of proposal since we want to achieve
things that would be impossible with `Y2Storage::GuidedProposal` or
`Y2Storage::AutoinstProposal`.

This introduces a new `Y2Storage::AgamaProposal` that combines
components from the mentioned existing proposals.

Replaces #1448
  • Loading branch information
ancorgs committed Aug 27, 2024
2 parents 005f61f + 4211435 commit 46f89e2
Show file tree
Hide file tree
Showing 46 changed files with 3,883 additions and 49 deletions.
196 changes: 196 additions & 0 deletions service/lib/agama/storage/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# frozen_string_literal: true

# Copyright (c) [2024] 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 "agama/storage/configs"

module Agama
module Storage
# Settings used to calculate an storage proposal.
class Config
# Boot settings.
#
# @return [Configs::Boot]
attr_accessor :boot

# @return [Array<Configs::Drive>]
attr_accessor :drives

# @return [Array]
attr_accessor :volume_groups

# @return [Array]
attr_accessor :md_raids

# @return [Array]
attr_accessor :btrfs_raids

# @return [Array]
attr_accessor :nfs_mounts

def initialize
@boot = Configs::Boot.new
@drives = []
@volume_groups = []
@md_raids = []
@btrfs_raids = []
@nfs_mounts = []
end

# Creates a config from JSON hash according to schema.
#
# @param config_json [Hash]
# @param product_config [Agama::Config]
#
# @return [Storage::Config]
def self.new_from_json(config_json, product_config:)
ConfigConversions::FromJSON.new(config_json, product_config: product_config).convert
end

# Name of the device that will presumably be used to boot the target system
#
# @return [String, nil] nil if there is no enough information to infer a possible boot disk
def boot_device
explicit_boot_device || implicit_boot_device
end

# Device used for booting the target system
#
# @return [String, nil] nil if no disk is explicitly chosen
def explicit_boot_device
return nil unless boot.configure?

boot.device
end

# Device that seems to be expected to be used for booting, according to the drive definitions
#
# @return [String, nil] nil if the information cannot be inferred from the list of drives
def implicit_boot_device
# NOTE: preliminary implementation with very simplistic checks
root_drive = drives.find do |drive|
drive.partitions.any? { |p| p.filesystem.root? }
end

root_drive&.found_device&.name
end

# Sets min and max sizes for all partitions and logical volumes with default size
#
# @param volume_builder [VolumeTemplatesBuilder] used to check the configuration of the
# product volume templates
def calculate_default_sizes(volume_builder)
default_size_devices.each do |dev|
dev.size.min = default_size(dev, :min, volume_builder)
dev.size.max = default_size(dev, :max, volume_builder)
end
end

private

# return [Array<Configs::Filesystem>]
def filesystems
(drives + partitions).map(&:filesystem).compact
end

# return [Array<Configs::Partition>]
def partitions
drives.flat_map(&:partitions)
end

# return [Array<Configs::Partitions>]
def default_size_devices
partitions.select { |p| p.size&.default? }
end

# Min or max size that should be used for the given partition or logical volume
#
# @param device [Configs::Partition] device configured to have a default size
# @param attr [Symbol] :min or :max
# @param builder [VolumeTemplatesBuilder] see {#calculate_default_sizes}
def default_size(device, attr, builder)
path = device.filesystem&.path || ""
vol = builder.for(path)
return fallback_size(attr) unless vol

# Theoretically, neither Volume#min_size or Volume#max_size can be nil
# At most they will be zero or unlimited, respectively
return vol.send(:"#{attr}_size") unless vol.auto_size?

outline = vol.outline
size = size_with_fallbacks(outline, attr, builder)
size = size_with_ram(size, outline)
size_with_snapshots(size, device, outline)
end

# TODO: these are the fallbacks used when constructing volumes, not sure if repeating them
# here is right
def fallback_size(attr)
return Y2Storage::DiskSize.zero if attr == :min

Y2Storage::DiskSize.unlimited
end

# @see #default_size
def size_with_fallbacks(outline, attr, builder)
fallback_paths = outline.send(:"#{attr}_size_fallback_for")
missing_paths = fallback_paths.reject { |p| proposed_path?(p) }

size = outline.send(:"base_#{attr}_size")
missing_paths.inject(size) { |total, p| total + builder.for(p).send(:"#{attr}_size") }
end

# @see #default_size
def size_with_ram(initial_size, outline)
return initial_size unless outline.adjust_by_ram?

[initial_size, ram_size].max
end

# @see #default_size
def size_with_snapshots(initial_size, device, outline)
return initial_size unless device.filesystem.btrfs_snapshots?
return initial_size unless outline.snapshots_affect_sizes?

if outline.snapshots_size && outline.snapshots_size > DiskSize.zero
initial_size + outline.snapshots_size
else
multiplicator = 1.0 + (outline.snapshots_percentage / 100.0)
initial_size * multiplicator
end
end

# Whether there is a separate filesystem configured for the given path
#
# @param path [String, Pathname]
# @return [Boolean]
def proposed_path?(path)
filesystems.any? { |fs| fs.path?(path) }
end

# Return the total amount of RAM as DiskSize
#
# @return [DiskSize] current RAM size
def ram_size
@ram_size ||= Y2Storage::DiskSize.new(Y2Storage::StorageManager.instance.arch.ram_size)
end
end
end
end
39 changes: 39 additions & 0 deletions service/lib/agama/storage/config_conversions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

# Copyright (c) [2024] 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 "agama/storage/config_conversions/block_device"
require "agama/storage/config_conversions/drive"
require "agama/storage/config_conversions/encrypt"
require "agama/storage/config_conversions/filesystem"
require "agama/storage/config_conversions/format"
require "agama/storage/config_conversions/from_json"
require "agama/storage/config_conversions/mount"
require "agama/storage/config_conversions/partition"
require "agama/storage/config_conversions/partitionable"
require "agama/storage/config_conversions/size"

module Agama
module Storage
# Conversions for the storage config.
module ConfigConversions
end
end
end
32 changes: 32 additions & 0 deletions service/lib/agama/storage/config_conversions/block_device.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

# Copyright (c) [2024] 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 "agama/storage/config_conversions/block_device/from_json"

module Agama
module Storage
module ConfigConversions
# Conversions for block device.
module BlockDevice
end
end
end
end
128 changes: 128 additions & 0 deletions service/lib/agama/storage/config_conversions/block_device/from_json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# frozen_string_literal: true

# Copyright (c) [2024] 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 "agama/storage/config_conversions/encryption/from_json"
require "agama/storage/config_conversions/filesystem/from_json"
require "agama/storage/config_conversions/filesystem_type/from_json"
require "agama/storage/configs/encryption"
require "agama/storage/configs/filesystem"
require "agama/storage/configs/filesystem_type"

module Agama
module Storage
module ConfigConversions
module BlockDevice
# Block device conversion from JSON hash according to schema.
class FromJSON
# @todo Replace settings and volume_builder params by a ProductDefinition.
#
# @param blk_device_json [Hash]
# @param settings [ProposalSettings]
# @param volume_builder [VolumeTemplatesBuilder]
def initialize(blk_device_json, settings:, volume_builder:)
@blk_device_json = blk_device_json
@settings = settings
@volume_builder = volume_builder
end

# Performs the conversion from Hash according to the JSON schema.
#
# @param config [#encrypt=, #format=, #mount=]
def convert(config)
config.encryption = convert_encrypt
config.filesystem = convert_filesystem
config
end

private

# @return [Hash]
attr_reader :blk_device_json

# @return [ProposalSettings]
attr_reader :settings

# @return [VolumeTemplatesBuilder]
attr_reader :volume_builder

# @return [Configs::Encrypt, nil]
def convert_encrypt
encrypt_json = blk_device_json[:encryption]
return unless encrypt_json

Encryption::FromJSON.new(encrypt_json, default: default_encrypt_config).convert
end

# @return [Configs::Filesystem, nil]
def convert_filesystem
filesystem_json = blk_device_json[:filesystem]
return if filesystem_json.nil?

default = default_filesystem_config(filesystem_json&.dig(:path) || "")

# @todo Check whether the given filesystem can be used for the mount point.
# @todo Check whether snapshots can be configured and restore to default if needed.

Filesystem::FromJSON.new(filesystem_json).convert(default)
end

# @todo Recover values from ProductDefinition instead of ProposalSettings.
#
# Default encryption config from the product definition.
#
# @return [Configs::Encryption]
def default_encrypt_config
Configs::Encryption.new.tap do |config|
config.password = settings.encryption.password
config.method = settings.encryption.method
config.pbkd_function = settings.encryption.pbkd_function
end
end

# Default format config from the product definition.
#
# @param mount_path [String]
# @return [Configs::Filesystem]
def default_filesystem_config(mount_path)
Configs::Filesystem.new.tap do |config|
config.type = default_fstype_config(mount_path)
end
end

# @todo Recover values from ProductDefinition instead of VolumeTemplatesBuilder.
#
# Default filesystem config from the product definition.
#
# @param mount_path [String]
# @return [Configs::FilesystemType]
def default_fstype_config(mount_path)
volume = volume_builder.for(mount_path)

Configs::FilesystemType.new.tap do |config|
config.fs_type = volume.fs_type
config.btrfs = volume.btrfs
end
end
end
end
end
end
end
Loading

0 comments on commit 46f89e2

Please sign in to comment.