Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
27f14b5
web: add route to create partition
joseivanlopez Jan 20, 2025
fb3125b
fix(storage): export sizes for volumes with auto size
joseivanlopez Jan 28, 2025
b293f64
web: add SelectToggle component
joseivanlopez Jan 28, 2025
69b9e66
web: add SelectTypeaheadCreatable component
joseivanlopez Jan 28, 2025
23c16ea
web: add query for adding a partition
joseivanlopez Jan 28, 2025
4609425
web: add PartitionPage component
joseivanlopez Jan 28, 2025
0fdd169
web: add status to SelectTypeaheadCreatable
joseivanlopez Jan 29, 2025
677437a
web: add regexp validation for mount point
joseivanlopez Jan 29, 2025
e44b07d
feat(storage): add reuse to model
joseivanlopez Jan 29, 2025
255a46d
web: allow reusing file system
joseivanlopez Jan 29, 2025
309c0c7
fix(storage): do not set the partition to delete if used
joseivanlopez Jan 29, 2025
d5e2203
web: improve query for getting volume templates
joseivanlopez Jan 31, 2025
6cc7c1f
web: use useEffect to update state
joseivanlopez Jan 31, 2025
9930da4
wip API for getting a solved model
joseivanlopez Jan 31, 2025
429bac2
web: add query to get a solved model
joseivanlopez Feb 3, 2025
c2f8a81
web: use sizes from solved model
joseivanlopez Feb 3, 2025
5a21f50
feat(web): rename SelectToggle and add unit tests
dgdavid Feb 3, 2025
4578706
fix(web): ensure consistent header rendering
dgdavid Feb 3, 2025
6521002
web: ignore value of invalid mount point
joseivanlopez Feb 3, 2025
806354d
Revert "web: ignore value of invalid mount point"
ancorgs Feb 3, 2025
6d81026
web: Filter out already used mount paths
ancorgs Jan 28, 2025
5caf088
web: filter out already used partitions
ancorgs Jan 28, 2025
1e5e3f2
web: Improve partition descriptions
ancorgs Jan 29, 2025
d2eb91b
web: Prevent repeating a mount point
ancorgs Jan 29, 2025
9c73223
web: Validate format of size limits
ancorgs Jan 30, 2025
7215dce
web: Disable fstype and size for invalid mount points
ancorgs Jan 31, 2025
841e074
web: Size parsing closer to its Y2Storage counterpart
ancorgs Jan 31, 2025
ec8e012
web: Validate relatioship between min and max sizes
ancorgs Jan 31, 2025
c1351b0
web: Fix several issues with javascript code format
ancorgs Feb 3, 2025
3ba81b4
web: move useVolume component to queries file
joseivanlopez Feb 3, 2025
7de2d92
service: add unit tests
joseivanlopez Feb 4, 2025
fde47eb
service: fix tests
joseivanlopez Feb 4, 2025
2e08534
rust: fix format
joseivanlopez Feb 4, 2025
d55a827
doc: document #SolveConfigModel DBus method
joseivanlopez Feb 4, 2025
cb28ed6
web: add useModel hook
joseivanlopez Feb 4, 2025
c213d68
Merge branch 'storage-config-ui' into storage-add-partition-select-wr…
dgdavid Feb 4, 2025
b4ea463
fix(web): adjust typing
dgdavid Feb 4, 2025
a412dca
fix(web): make ProposalResultSection work
dgdavid Feb 5, 2025
875c180
fix(web): add missing mock
dgdavid Feb 5, 2025
62f7e1a
web: Adjust validations appeareance
ancorgs Feb 4, 2025
deb65bd
web: Changes in the appeareance of filesystem selector
ancorgs Feb 4, 2025
80a05b5
web: New texts for the widgets to configure partition sizes
ancorgs Feb 4, 2025
3c4a21f
web: New labels for the target widget at the partition form
ancorgs Feb 5, 2025
07cd9e1
web: Fix typo
ancorgs Feb 5, 2025
293030f
Merge pull request #5 from ancorgs/storage-add-partition-wording
joseivanlopez Feb 5, 2025
288e214
fix(web): limit size of size inputs
dgdavid Feb 5, 2025
4b33f79
fix(web): do not use label for maxSizeValue
dgdavid Feb 5, 2025
624f53b
fix(web): restore visual behavior of product selection
dgdavid Feb 5, 2025
c0bada7
fix(web): change size width limitation
dgdavid Feb 5, 2025
71112ac
fix(web): bring back CSS fix for form labels
dgdavid Feb 6, 2025
c4b3a3a
feat(web): split Size field for better clarity
dgdavid Feb 6, 2025
b9cd95d
fix(web): move actions form to the end of the form
dgdavid Feb 6, 2025
c515e99
fix(web): simplify the partition form markup a bit
dgdavid Feb 6, 2025
0a5d074
fix(web): allow mocking useParams hook
dgdavid Feb 6, 2025
8924cae
fix(web): allow focusing fields on label click
dgdavid Feb 7, 2025
83be3b6
Merge branch 'storage-config-ui' into storage-add-partition
joseivanlopez Feb 7, 2025
4ceec94
web: allow adding partition for devices without partitions config
joseivanlopez Feb 7, 2025
f9f8f76
fix(web): add basic unit test for partition form
dgdavid Feb 7, 2025
bad5359
fix(web): please ESLint
dgdavid Feb 7, 2025
fe18c85
fix(web): make storage overview test work again
dgdavid Feb 10, 2025
6aa0f5c
fix(web): make testsuit work again
dgdavid Feb 10, 2025
a27a99d
web: First version of an improved Proposal error alert
ancorgs Feb 7, 2025
5c129c1
fix(web): add missing import
dgdavid Feb 7, 2025
f057dae
web: Fixes from code review
ancorgs Feb 8, 2025
6bc6a71
fix(web): another relocation of storage useConfigModel
dgdavid Feb 10, 2025
272fc96
Merge branch 'storage-config-ui' into storage-add-partition
dgdavid Feb 10, 2025
7bd6d62
fix(web): adjust alert spaces and font sizes
dgdavid Feb 10, 2025
bde0e1e
fix(web): use paragraphs in proposal failed info
dgdavid Feb 10, 2025
55226b1
fix(web): simplify internal core/Page header
dgdavid Feb 10, 2025
16ae835
fix(web): adjust boot selection form
dgdavid Feb 10, 2025
09a2e2c
fix(web): split content for auto size info
dgdavid Feb 10, 2025
71ff9cc
web: Fix detection of partitions to be modified
ancorgs Feb 11, 2025
0839522
web: do not add the same partition twice
joseivanlopez Feb 11, 2025
af76da0
web: Fix the previous fix about detecting used partitions
ancorgs Feb 11, 2025
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
4 changes: 4 additions & 0 deletions doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
<method name="GetConfigModel">
<arg name="serialized_model" direction="out" type="s"/>
</method>
<method name="SolveConfigModel">
<arg name="model" direction="in" type="s"/>
<arg name="solved_model" direction="out" type="s"/>
</method>
<method name="Install">
</method>
<method name="Finish">
Expand Down
47 changes: 47 additions & 0 deletions doc/dbus/org.opensuse.Agama.Storage1.doc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,53 @@
-->
<arg name="serialized_model" direction="out" type="s"/>
</method>
<!--
Solves the given storage config model.
-->
<method name="SolveConfigModel">
<!--
E.g.,
{
"drives": [
{
"name": "/dev/vda",
"partitions": [
{
"mountPath": "/"
}
]
}
]
}
-->
<arg name="model" direction="in" type="s"/>
<!--
E.g.,
{
"drives": [
{
"name": "/dev/vda",
"spacePolicy": "keep",
"partitions": [
{
"mountPath": "/",
"filesystem": {
"default": true,
"type": "xfs"
},
"size": {
"default": true,
"min": "10 GiB",
"max": "10 GiB"
}
}
]
}
]
}
-->
<arg name="solved_model" direction="out" type="s"/>
</method>
<method name="Install">
</method>
<method name="Finish">
Expand Down
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
31 changes: 31 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,36 @@ 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.
# @return [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
Loading