Skip to content

Commit

Permalink
storage: resolve JSON keywords
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Sep 26, 2024
1 parent a73acce commit bd9ab6f
Show file tree
Hide file tree
Showing 4 changed files with 722 additions and 0 deletions.
10 changes: 10 additions & 0 deletions service/lib/agama/storage/config_conversions/from_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require "agama/config"
require "agama/storage/config_builder"
require "agama/storage/config_conversions/from_json_conversions/config"
require "agama/storage/config_json_solver"

module Agama
module Storage
Expand All @@ -41,6 +42,15 @@ def initialize(config_json, product_config: nil)
# @return [Storage::Config]
def convert
# TODO: Raise error if config_json does not match the JSON schema.
# Implementation idea: ConfigJSONChecker class which reports issues if:
# * The JSON does not match the schema.
# * The JSON contains both "default" and "mandatory" for partitions or logical volumes.
# * The JSON contains "default" or "mandatory" more than once.
# * The JSON contains invalid aliases (now checked by ConfigChecker).
ConfigJSONSolver
.new(product_config)
.solve(config_json)

FromJSONConversions::Config
.new(config_json, config_builder: config_builder)
.convert
Expand Down
263 changes: 263 additions & 0 deletions service/lib/agama/storage/config_json_solver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# 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/config"
require "agama/storage/volume_templates_builder"

module Agama
module Storage
# Class for solving a storage JSON config.
#
# A storage JSON config can contain keywords like "default" or "mandatory" for automatically
# generating partitions or logical volumes according to the product definition. That keywords
# are solved by replacing them by the corresponding configs. The solver takes into account other
# paths already present in the rest of the config.
#
# @example
# config_json = {
# drives: [
# "default",
# {
# filesystem: { path: "swap" }
# }
# ]
# }
#
# ConfigJSONSolver.new(product_config).solve(config_json)
# config_json # => {
# # drives: [
# # {
# # filesystem: { path: "/" }
# # },
# # {
# # filesystem: { path: "/home" }
# # },
# # {
# # filesystem: { path: "swap" }
# # }
# # ]
# # }
class ConfigJSONSolver
# @param product_config [Agama::Config]
def initialize(product_config = nil)
@product_config = product_config || Agama::Config.new
end

# Solves all the keywords within a given config.
#
# @note The config_json object is modified.
#
# @param config_json [Hash]
def solve(config_json)
@config_json = config_json

solve_keywords
end

private

# @return [Hash]
attr_reader :config_json

# @return [Agama::Config]
attr_reader :product_config

def solve_keywords
drives_with_keyword.each { |c| solve_partitions_keyword(c) }
volume_groups_with_keyword.each { |c| solve_logical_volumes_keyword(c) }
end

# @param config [Hash]
def solve_partitions_keyword(config)
partitions = config[:partitions]
return unless partitions

solve_keyword(partitions)
end

# @param config [Hash]
def solve_logical_volumes_keyword(config)
logical_volumes = config[:logicalVolumes]
return unless logical_volumes

solve_keyword(logical_volumes)
end

# @param configs [Array<Hash>]
def solve_keyword(configs)
if with_default_keyword?(configs)
solve_default_keyword(configs)
elsif with_mandatory_keyword?(configs)
solve_mandatory_keyword(configs)
end
end

# @param configs [Array<Hash>]
def solve_default_keyword(configs)
configs.delete("default")
configs.delete("mandatory")
configs.concat(missing_default_configs)
end

# @param configs [Array<Hash>]
def solve_mandatory_keyword(configs)
configs.delete("mandatory")
configs.concat(missing_mandatory_configs)
end

# @return [Array<Hash>]
def missing_default_configs
missing_default_paths.map { |p| volume_config(p) }
end

# @return [Array<String>]
def missing_default_paths
default_paths - current_paths
end

# @return [Array<String>]
def default_paths
product_config.data.dig("storage", "volumes") || []
end

# @return [Array<String>]
def current_paths
configs_with_filesystem
.select { |c| c.is_a?(Hash) }
.map { |c| c.dig(:filesystem, :path) }
.compact
end

# @return [Array<Hash>]
def missing_mandatory_configs
missing_mandatory_paths.map { |p| volume_config(p) }
end

# @return [Array<String>]
def missing_mandatory_paths
mandatory_paths - current_paths
end

# @return [Array<String>]
def mandatory_paths
default_paths.select { |p| mandatory_path?(p) }
end

# @param path [String]
# @return [Volume]
def mandatory_path?(path)
volume_builder.for(path).outline.required?
end

# @param path [String]
# @return [Hash]
def volume_config(path)
{ filesystem: { path: path } }
end

# @return [Array<Hash>]
def drives_with_keyword
drive_configs.select { |c| with_partitions_keyword?(c) }
end

# @return [Array<Hash>]
def volume_groups_with_keyword
volume_group_configs.select { |c| with_logical_volumes_keyword?(c) }
end

# @param config [Hash]
# @return [Boolean]
def with_partitions_keyword?(config)
partitions = config[:partitions]
return false unless partitions

with_keyword?(partitions)
end

# @param config [Hash]
# @return [Boolean]
def with_logical_volumes_keyword?(config)
logical_volumes = config[:logicalVolumes]
return false unless logical_volumes

with_keyword?(logical_volumes)
end

# @param configs [Array<Hash>]
# @return [Boolean]
def with_keyword?(configs)
with_default_keyword?(configs) || with_mandatory_keyword?(configs)
end

# @param configs [Array<Hash>]
# @return [Boolean]
def with_default_keyword?(configs)
configs.include?("default")
end

# @param configs [Array<Hash>]
# @return [Boolean]
def with_mandatory_keyword?(configs)
configs.include?("mandatory")
end

# @return [Array<Hash>]
def configs_with_filesystem
drive_configs + partition_configs + logical_volume_configs
end

# @return [Array<Hash>]
def drive_configs
config_json[:drives] || []
end

# @return [Array<Hash>]
def volume_group_configs
config_json[:volumeGroups] || []
end

# @return [Array<Hash>]
def partition_configs
drive_configs = config_json[:drives]
return [] unless drive_configs

drive_configs
.flat_map { |c| c[:partitions] }
.compact
end

# @return [Array<Hash>]
def logical_volume_configs
volume_group_configs = config_json[:volumeGroups]
return [] unless volume_group_configs

volume_group_configs
.flat_map { |c| c[:logicalVolumes] }
.compact
end

# @return [VolumeTemplatesBuilder]
def volume_builder
@volume_builder ||= VolumeTemplatesBuilder.new_from_config(product_config)
end
end
end
end
Loading

0 comments on commit bd9ab6f

Please sign in to comment.