Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): UI for adding a partition #1935

Draft
wants to merge 16 commits into
base: storage-config-ui
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/agama-lib/share/storage.model.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"additionalProperties": false,
"required": ["default"],
"properties": {
"reuse": { "type": "boolean" },
"default": { "type": "boolean" },
"type": { "$ref": "#/$defs/filesystemType" },
"snapshots": { "type": "boolean" }
Expand Down
9 changes: 8 additions & 1 deletion rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ impl<'a> StorageClient<'a> {
Ok(settings)
}

/// Set the storage config according to the JSON schema
/// Set the storage config model according to the JSON schema
pub async fn set_config_model(&self, model: Box<RawValue>) -> Result<u32, ServiceError> {
Ok(self
.storage_proxy
Expand All @@ -174,6 +174,13 @@ impl<'a> StorageClient<'a> {
Ok(config_model)
}

/// Solves the storage config model
pub async fn solve_config_model(&self, model: &str) -> Result<Box<RawValue>, ServiceError> {
let serialized_solved_model = self.storage_proxy.solve_config_model(model).await?;
let solved_model = serde_json::from_str(serialized_solved_model.as_str()).unwrap();
Ok(solved_model)
}

pub async fn calculate(&self, settings: ProposalSettingsPatch) -> Result<u32, ServiceError> {
let map: HashMap<&str, zbus::zvariant::Value> = settings.into();
let options: HashMap<&str, &zbus::zvariant::Value> =
Expand Down
3 changes: 3 additions & 0 deletions rust/agama-lib/src/storage/proxies/storage1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ pub trait Storage1 {
/// Get the storage config model according to the JSON schema
fn get_config_model(&self) -> zbus::Result<String>;

/// Solve a storage config model
fn solve_config_model(&self, model: &str) -> zbus::Result<String>;

/// DeprecatedSystem property
#[zbus(property)]
fn deprecated_system(&self) -> zbus::Result<bool>;
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-server/src/software/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio_stream::{Stream, StreamExt};

use super::license::{License, LicenseContent};
use super::license::License;

#[derive(Clone)]
struct SoftwareState<'a> {
Expand Down
32 changes: 32 additions & 0 deletions rust/agama-server/src/storage/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result<Router, ServiceEr
let router = Router::new()
.route("/config", put(set_config).get(get_config))
.route("/config_model", put(set_config_model).get(get_config_model))
.route("/config_model/solve", get(solve_config_model))
.route("/probe", post(probe))
.route("/reprobe", post(reprobe))
.route("/devices/dirty", get(devices_dirty))
Expand Down Expand Up @@ -235,6 +236,37 @@ async fn set_config_model(
Ok(Json(()))
}

/// Solves a storage config model.
#[utoipa::path(
get,
path = "/config_model/solve",
context_path = "/api/storage",
params(SolveModelQuery),
operation_id = "solve_storage_config_model",
responses(
(status = 200, description = "Solve the storage config model", body = String),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
async fn solve_config_model(
State(state): State<StorageState<'_>>,
query: Query<SolveModelQuery>,
) -> Result<Json<Box<RawValue>>, Error> {
let solved_model = state
.client
.solve_config_model(query.model.as_str())
.await
.map_err(Error::Service)?;
Ok(Json(solved_model))
}

#[derive(Deserialize, utoipa::IntoParams)]
struct SolveModelQuery {
/// Serialized config model.
model: String,
}


/// Probes the storage devices.
#[utoipa::path(
post,
Expand Down
15 changes: 15 additions & 0 deletions service/lib/agama/dbus/storage/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ def recover_model
JSON.pretty_generate(json)
end

# Solves the given serialized config model.
#
# @param serialized_model [String] Serialized storage config model.
# @return [String] Serialized solved model.
def solve_model(serialized_model)
logger.info("Solving storage config model from D-Bus: #{serialized_model}")

model_json = JSON.parse(serialized_model, symbolize_names: true)
solved_model_json = proposal.solve_model(model_json)
JSON.pretty_generate(solved_model_json)
end

def install
busy_while { backend.install }
end
Expand All @@ -173,6 +185,9 @@ def deprecated_system
busy_while { apply_config_model(serialized_model) }
end
dbus_method(:GetConfigModel, "out serialized_model:s") { recover_model }
dbus_method(:SolveConfigModel, "in sparse_model:s, out solved_model:s") do |sparse_model|
solve_model(sparse_model)
end
dbus_method(:Install) { install }
dbus_method(:Finish) { finish }
dbus_reader(:deprecated_system, "b")
Expand Down
15 changes: 12 additions & 3 deletions service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def convert
"Target" => volume.location.target.to_s,
"TargetDevice" => volume.location.device.to_s,
"FsType" => volume.fs_type&.to_human_string || "",
"MinSize" => volume.min_size&.to_i,
"MinSize" => min_size_conversion,
"AutoSize" => volume.auto_size?,
"Snapshots" => volume.btrfs.snapshots?,
"Transactional" => volume.btrfs.read_only?,
Expand All @@ -67,11 +67,20 @@ def convert
# @return [Agama::Storage::Volume]
attr_reader :volume

# @return [Integer]
def min_size_conversion
min_size = volume.min_size
min_size = volume.outline.base_min_size if volume.auto_size?
min_size.to_i
end

# @param target [Hash]
def max_size_conversion(target)
return if volume.max_size.nil? || volume.max_size.unlimited?
max_size = volume.max_size
max_size = volume.outline.base_max_size if volume.auto_size?
return if max_size.unlimited?

target["MaxSize"] = volume.max_size.to_i
target["MaxSize"] = max_size.to_i
end

# Converts volume outline to D-Bus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ def default_config
# @return [Hash]
def conversions
{
path: model_json[:mountPath],
type: convert_type
reuse: model_json.dig(:filesystem, :reuse),
path: model_json[:mountPath],
type: convert_type
}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def conversions
filesystem: convert_filesystem,
size: convert_size,
id: convert_id,
delete: partition_model[:delete],
delete_if_needed: partition_model[:deleteIfNeeded]
delete: convert_delete,
delete_if_needed: convert_delete_if_needed
}
end

Expand All @@ -67,6 +67,24 @@ def convert_id

Y2Storage::PartitionId.find(value)
end

# TODO: do not delete if the partition is used by other device (VG, RAID, etc).
# @return [Boolean]
def convert_delete
# Do not mark to delete if the partition is used.
return false if partition_model[:mountPath]

partition_model[:delete]
end

# TODO: do not delete if the partition is used by other device (VG, RAID, etc).
# @return [Boolean]
def convert_delete_if_needed
# Do not mark to delete if the partition is used.
return false if partition_model[:mountPath]

partition_model[:deleteIfNeeded]
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def initialize(config)
# @see Base#conversions
def conversions
{
reuse: config.reuse?,
default: convert_default,
type: convert_type,
snapshots: convert_snapshots
Expand Down
6 changes: 4 additions & 2 deletions service/lib/agama/storage/config_solvers/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ def filter_by_disk_analyzer(devices)
# Finds the partitions matching the given search config, if any
#
# @param search_config [Agama::Storage::Configs::Search]
# @return [Y2Storage::Device, nil]
# @param device [#partitions]
#
# @return [Array<Y2Storage::Partition>]
def find_partitions(search_config, device)
candidates = candidate_devices(search_config, default: device.partitions)
candidates.select! { |d| d.is?(:partition) }
Expand Down Expand Up @@ -175,7 +177,7 @@ def find_device(search_config)
#
# @param devices [Array<Y2Storage::Device>]
# @param search [Config::Search]
# @return [Y2Storage::Device, nil]
# @return [Array<Y2Storage::Device>]
def next_unassigned_devices(devices, search)
devices
.reject { |d| sids.include?(d.sid) }
Expand Down
19 changes: 19 additions & 0 deletions service/lib/agama/storage/proposal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require "agama/issue"
require "agama/storage/actions_generator"
require "agama/storage/config_conversions"
require "agama/storage/config_solver"
require "agama/storage/proposal_settings"
require "agama/storage/proposal_strategies"
require "json"
Expand Down Expand Up @@ -100,6 +101,24 @@ def model_json
ConfigConversions::ToModel.new(config).convert
end

# Solves a given model.
#
# @param model_json [Hash] Config model according to the JSON schema.
# @param [Hash, nil] Solved config model or nil if the model cannot be solved yet.
def solve_model(model_json)
return unless storage_manager.probed?

config = ConfigConversions::FromModel
.new(model_json, product_config: product_config)
.convert

ConfigSolver
.new(product_config, storage_manager.probed, disk_analyzer: disk_analyzer)
.solve(config)

ConfigConversions::ToModel.new(config).convert
end

# Calculates a new proposal using the given JSON.
#
# @raise If the JSON is not valid.
Expand Down
1 change: 1 addition & 0 deletions service/test/agama/dbus/storage/manager_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ def serialize(value)
{
mountPath: "/",
filesystem: {
reuse: false,
default: true,
type: "ext4"
},
Expand Down
52 changes: 43 additions & 9 deletions service/test/agama/dbus/storage/volume_conversion/to_dbus_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@
require "y2storage/disk_size"

describe Agama::DBus::Storage::VolumeConversion::ToDBus do
let(:default_volume) { Agama::Storage::Volume.new("/test") }
let(:volume1) { Agama::Storage::Volume.new("/test1") }

let(:custom_volume) do
let(:volume2) do
Agama::Storage::Volume.new("/test2").tap do |volume|
volume.min_size = nil
volume.max_size = nil
volume.auto_size = true
volume.outline.base_min_size = Y2Storage::DiskSize.new(1024)
volume.outline.base_max_size = Y2Storage::DiskSize.new(4096)
end
end

let(:volume3) do
volume_outline = Agama::Storage::VolumeOutline.new.tap do |outline|
outline.required = true
outline.filesystems = [Y2Storage::Filesystems::Type::EXT3, Y2Storage::Filesystems::Type::EXT4]
Expand All @@ -39,9 +49,11 @@
outline.snapshots_size = Y2Storage::DiskSize.new(1000)
outline.snapshots_percentage = 10
outline.adjust_by_ram = true
outline.base_min_size = Y2Storage::DiskSize.new(2048)
outline.base_max_size = Y2Storage::DiskSize.new(4096)
end

Agama::Storage::Volume.new("/test").tap do |volume|
Agama::Storage::Volume.new("/test3").tap do |volume|
volume.outline = volume_outline
volume.fs_type = Y2Storage::Filesystems::Type::EXT4
volume.btrfs.snapshots = true
Expand All @@ -57,8 +69,8 @@

describe "#convert" do
it "converts the volume to a D-Bus hash" do
expect(described_class.new(default_volume).convert).to eq(
"MountPath" => "/test",
expect(described_class.new(volume1).convert).to eq(
"MountPath" => "/test1",
"MountOptions" => [],
"TargetDevice" => "",
"Target" => "default",
Expand All @@ -78,14 +90,36 @@
}
)

expect(described_class.new(custom_volume).convert).to eq(
"MountPath" => "/test",
expect(described_class.new(volume2).convert).to eq(
"MountPath" => "/test2",
"MountOptions" => [],
"TargetDevice" => "",
"Target" => "default",
"FsType" => "",
"MinSize" => 1024,
"MaxSize" => 4096,
"AutoSize" => true,
"Snapshots" => false,
"Transactional" => false,
"Outline" => {
"Required" => false,
"FsTypes" => [],
"SupportAutoSize" => false,
"AdjustByRam" => false,
"SnapshotsConfigurable" => false,
"SnapshotsAffectSizes" => false,
"SizeRelevantVolumes" => []
}
)

expect(described_class.new(volume3).convert).to eq(
"MountPath" => "/test3",
"MountOptions" => ["rw", "default"],
"TargetDevice" => "/dev/sda",
"Target" => "new_partition",
"FsType" => "Ext4",
"MinSize" => 1024,
"MaxSize" => 2048,
"MinSize" => 2048,
"MaxSize" => 4096,
"AutoSize" => true,
"Snapshots" => true,
"Transactional" => true,
Expand Down
Loading
Loading