Skip to content

Commit

Permalink
feat(storage): add boot config solver
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Dec 11, 2024
1 parent 019c776 commit 455437f
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 67 deletions.
75 changes: 8 additions & 67 deletions service/lib/agama/storage/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

require "agama/copyable"
require "agama/storage/configs/boot"
require "agama/storage/config_conversions/from_json"

module Agama
module Storage
Expand Down Expand Up @@ -58,11 +57,16 @@ def initialize
@nfs_mounts = []
end

# Name of the device that will presumably be used to boot the target system
# Name of the device that will be used to boot the target system, if any.
#
# @return [String, nil] nil if there is no enough information to infer a possible boot disk
# @note The config has to be solved.
#
# @return [String, nil]
def boot_device
explicit_boot_device || implicit_boot_device
return unless boot.configure? && boot.device.device_alias

boot_drive = drives.find { |d| d.alias?(boot.device.device_alias) }
boot_drive&.found_device&.name
end

# return [Array<Configs::Partition>]
Expand All @@ -74,69 +78,6 @@ def partitions
def logical_volumes
volume_groups.flat_map(&:logical_volumes)
end

private

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

boot_drive = drives.find { |d| d.alias?(boot.device.device_alias) }
boot_drive&.found_device&.name
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 config
def implicit_boot_device
implicit_drive_boot_device || implicit_lvm_boot_device
end

# @see #implicit_boot_device
#
# @return [String, nil] nil if the information cannot be inferred from the list of drives
def implicit_drive_boot_device
root_drive = drives.find do |drive|
drive.partitions.any? { |p| p.filesystem&.root? }
end

root_drive&.found_device&.name
end

# @see #implicit_boot_device
#
# @return [String, nil] nil if the information cannot be inferred from the list of LVM VGs
def implicit_lvm_boot_device
root_vg = root_volume_group
return nil unless root_vg

root_drives = drives.select { |d| drive_for_vg?(d, root_vg) }
names = root_drives.map { |d| d.found_device&.name }.compact
# Return the first name in alphabetical order
names.min
end

# @see #implicit_lvm_boot_device
#
# @return [Configs::VolumeGroup, nil]
def root_volume_group
volume_groups.find do |vg|
vg.logical_volumes.any? { |lv| lv.filesystem&.root? }
end
end

# @see #implicit_lvm_boot_device
#
# @return [Boolean]
def drive_for_vg?(drive, volume_group)
return true if volume_group.physical_volumes_devices.any? { |d| drive.alias?(d) }

volume_group.physical_volumes.any? do |pv|
drive.partitions.any? { |p| p.alias?(pv) }
end
end
end
end
end
1 change: 1 addition & 0 deletions service/lib/agama/storage/config_solver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def initialize(product_config, devicegraph, disk_analyzer: nil)
#
# @param config [Config]
def solve(config)
ConfigSolvers::Boot.new(product_config).solve(config)
ConfigSolvers::Encryption.new(product_config).solve(config)
ConfigSolvers::Filesystem.new(product_config).solve(config)
ConfigSolvers::Search.new(product_config, devicegraph, disk_analyzer).solve(config)
Expand Down
1 change: 1 addition & 0 deletions service/lib/agama/storage/config_solvers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# 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_solvers/boot"
require "agama/storage/config_solvers/encryption"
require "agama/storage/config_solvers/filesystem"
require "agama/storage/config_solvers/search"
Expand Down
132 changes: 132 additions & 0 deletions service/lib/agama/storage/config_solvers/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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_solvers/base"

module Agama
module Storage
module ConfigSolvers
# Solver for the boot config.
class Boot < Base
# Solves the boot config within a given config.
#
# @note The config object is modified.
#
# @param config [Config]
def solve(config)
@config = config
solve_device_alias
end

private

# Finds a device for booting and sets its alias, if needed.
def solve_device_alias
return unless config.boot.configure? && config.boot.device.device_alias.nil?

device_config = boot_device_config
return unless device_config

device_config.alias ||= generate_alias
config.boot.device.device_alias = device_config.alias
end

# Config of the device to be used for booting.
#
# @return [Configs::Drive, nil] nil if the boot device cannot be inferred from the config.
def boot_device_config
root_drive_config || root_lvm_device_config
end

# Config of the drive containing the root partition, if any.
#
# @return [Configs::Drive, nil]
def root_drive_config
config.drives.find { |d| root_drive_config?(d) }
end

# Config of the first drive used to allocate the root volume group config, if any.
#
# @return [Configs::Drive, nil]
def root_lvm_device_config
volume_group_config = root_volume_group_config
return unless volume_group_config

config.drives
.select { |d| candidate_for_physical_volumes?(d, volume_group_config) }
.first
end

# Config of the volume group containing the root logical volume, if any.
#
# @return [Configs::VolumeGroup, nil]
def root_volume_group_config
config.volume_groups.find { |v| root_volume_group_config?(v) }
end

# Whether the given drive config contains a root partition config.
#
# @param config [Configs::Drive]
# @return [Boolean]
def root_drive_config?(config)
config.partitions.any? { |p| root_config?(p) }
end

# Whether the given volume group config contains a root logical volume config.
#
# @param config [Configs::VolumeGroup]
# @return [Boolean]
def root_volume_group_config?(config)
config.logical_volumes.any? { |l| root_config?(l) }
end

# Whether the given config if for the root filesystem.
#
# @param config [#filesystem]
# @return [Boolean]
def root_config?(config)
config.filesystem&.root?
end

# Whether the given drive config can be used to allocate physcial volumes.
#
# @param drive [Configs::Drive]
# @param volume_group [Configs::VolumeGroup]
#
# @return [Boolean]
def candidate_for_physical_volumes?(drive, volume_group)
return true if volume_group.physical_volumes_devices.any? { |d| drive.alias?(d) }

volume_group.physical_volumes.any? do |pv|
drive.partitions.any? { |p| p.alias?(pv) }
end
end

# Generates a random alias.
#
# @return [String]
def generate_alias
"device#{rand(10**9)}"
end
end
end
end
end
2 changes: 2 additions & 0 deletions service/test/agama/dbus/storage/manager_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ def serialize(value)
storage: {
drives: [
{
alias: "root",
partitions: [
{
filesystem: { path: "/" }
Expand All @@ -739,6 +740,7 @@ def serialize(value)
drives: [
{
name: "/dev/sda",
alias: "root",
spacePolicy: "keep",
partitions: [
{
Expand Down
118 changes: 118 additions & 0 deletions service/test/agama/storage/config_solver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,124 @@
describe "#solve" do
let(:scenario) { "empty-hd-50GiB.yaml" }

context "if a config does not specify the boot device" do
context "and root is over a partition" do
let(:config_json) do
{
boot: { configure: true },
drives: [
{
alias: device_alias,
partitions: [
{ filesystem: { path: "/" } }
]
}
]
}
end

let(:device_alias) { "root" }

it "sets the alias of the root drive as boot device alias" do
subject.solve(config)
boot = config.boot
expect(boot.device.device_alias).to eq("root")
end

context "and the root drive has no alias" do
let(:device_alias) { nil }

it "sets an alias to the root drive" do
subject.solve(config)
drive = config.drives.first
expect(drive.alias).to match(/device\d*/)
end

it "sets the alias of root drive as boot device alias" do
subject.solve(config)
boot = config.boot
drive = config.drives.first
expect(boot.device.device_alias).to eq(drive.alias)
end
end
end

context "and root is over a logical volume" do
let(:scenario) { "disks.yaml" }

let(:config_json) do
{
boot: { configure: true },
drives: [
{
search: "/dev/vda",
alias: device_alias,
partitions: [
{ search: "/dev/vda2", alias: "pv" }
]
},
{ search: "/dev/vdb", alias: "disk2" }
],
volumeGroups: [
{
physicalVolumes: ["disk2", "pv"],
logicalVolumes: [
{ filesystem: { path: "/" } }
]
}
]
}
end

let(:device_alias) { "disk1" }

it "sets the alias of first partitioned pv drive as boot device alias" do
subject.solve(config)
boot = config.boot
expect(boot.device.device_alias).to eq("disk1")
end

context "and the drive has no alias" do
let(:device_alias) { nil }

it "sets an alias to the drive" do
subject.solve(config)
drive = config.drives.find { |d| d.search.name == "/dev/vda" }
expect(drive.alias).to match(/device\d*/)
end

it "sets the alias of the drive as boot device alias" do
subject.solve(config)
boot = config.boot
drive = config.drives.find { |d| d.search.name == "/dev/vda" }
expect(boot.device.device_alias).to eq(drive.alias)
end
end
end

context "and boot is set to not be configured" do
let(:config_json) do
{
boot: { configure: false },
drives: [
{
alias: "disk1",
partitions: [
{ filesystem: { path: "/" } }
]
}
]
}
end

it "does not set a boot device alias" do
subject.solve(config)
boot = config.boot
expect(boot.device.device_alias).to be_nil
end
end
end

context "if a config does not specify all the encryption properties" do
let(:config_json) do
{
Expand Down
Loading

0 comments on commit 455437f

Please sign in to comment.