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(storage): allow selecting space policy #1791

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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 @@ -52,6 +52,10 @@
<method name="GetConfig">
<arg name="serialized_config" direction="out" type="s"/>
</method>
<method name="SetConfigModel">
<arg name="serialized_model" direction="in" type="s"/>
<arg name="result" direction="out" type="u"/>
</method>
<method name="GetConfigModel">
<arg name="serialized_model" direction="out" type="s"/>
</method>
Expand Down
25 changes: 25 additions & 0 deletions doc/dbus/org.opensuse.Agama.Storage1.doc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@
-->
<arg name="serialized_config" direction="out" type="s"/>
</method>
<!--
Sets the storage config model.
-->
<method name="SetConfigModel">
<!--
E.g.,
{
"drives": [
{
"name": "/dev/vda",
"partitions": [
{ "mountPath": "/" }
]
}
]
}
-->
<arg name="serialized_model" direction="in" type="s"/>
<!--
Whether the proposal was correctly calculated with the given config model:
0: success
1: failure
-->
<arg name="result" direction="out" type="u"/>
</method>
<!--
Gets the storage config model.
-->
Expand Down
267 changes: 267 additions & 0 deletions doc/storage_proposal_from_profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Calculating a proposal from a profile

The Agama proposal can be calculated either from a very detailed JSON profile or from a "sparse
profile". The information not provided by the profile is automatically inferred (solved) by Agama.
Several layers are involved in the process of obtaining the final storage config used by the Agama
proposal, as shown in the following diagram:

```
JSON profile ------------> JSON profile (solved) ------------> Storage::Config ------------> Storage::Config (solved)
| | |
(JSON solver) (config conversion) (config solver)
```

## JSON solver

The JSON profile provides the *generator* concept. A *generator* allows indicating what volumes to
create without explicitly defining them. The JSON solver (`Agama::Storage::JSONConfigSolver` class)
takes care of replacing the volume generator by the corresponding JSON volumes according to the
product.

For example, a JSON profile like this:

~~~json
{
"drives": [
{
"partitions": [ { "generate": "default" } ]
}
]
}
~~~

would be solved to something like:

~~~json
{
"drives": [
{
"partitions": [
{ "filesystem": { "path": "/" } },
{ "filesystem": { "path": "swap" } }
]
}
]
}
~~~

The volumes are solved with their very minimum information (i.e., a mount path). The resulting
solved JSON is used for getting the storage config object.

## Config conversion

The class `Agama::Storage::ConfigConversions::FromJSON` takes a solved JSON profile and generates a
`Agama::Storage::Config` object. The resulting config only contains the information provided by the
profile. For example, if the profile does not specify a file system type for a partition, then the
config would not have any information about the file system to use for such a partition.

If something is not provided by the profile (e.g., "boot", "size", "filesystem"), then the config
marks that values as default ones. For example:

```json
{
"drives": [
{
"partitions": [
{ "filesystem": { "path": "/" } }
]
}
]
}
```

generates a config with these default values:

```ruby
config.boot.device.default? #=> true

partition = config.drives.first.partitions.first
partition.size.default? #=> true
partition.filesystem.type.default? #=> true
```

The configs set as default and any other missing value have to be solved to a value provided by the
product definition.

## Config solver

The config solver (`Agama::Storage::ConfigSolver` class) assigns a value to all the unknown
properties of a config object. As result, the config object is totally complete and ready to be used
by the agama proposal.

### How sizes are solved

A volume size in the profile:

* Can be totally omitted.
* Can omit the max size.
* Can use "current" as value for min and/or max.

Let's see each case.

#### Omitting size completely

```json
"partitions": [
{ "filesystem": { "path": "/" } }
]
```

In this case, the config conversion would generate something like:

```ruby
partition.size.default? #=> true
partition.size.min #=> nil
partition.size.max #=> nil
```

If the size is default, then the config solver always assigns a value for `#min` and `#max`
according to the product definition and ignoring the current values assigned to `#min` and `#max`.
The solver takes into account the mount path, the fallback devices and swap config in order to set
the proper sizes.

If the size is default and the volume already exists, then the solver sets the current size of the
volume to both `#min` and `#max` sizes.

#### Omitting the max size

```json
"partitions": [
{
"size": { "min": "10 GiB" },
"filesystem": { "path": "/" }
}
]
```

The config conversion generates:

```ruby
partition.size.default? #=> false
partition.size.min #=> Y2Storage::DiskSize.GiB(10)
partition.size.max #=> Y2Storage::DiskSize.Unlimited
```

Note that `#max` is set to unlimited when it does not appear in the profile. In this case, nothing
has to be solved because both `#min` and `#max` have a value.

#### Using "current"

Both *min* and *max* sizes admit "current" as a valid size value in the JSON profile. The "current"
value stands for the current size of the volume. Using "current" is useful for growing or shrinking
a device.

The config conversion knows nothing about the current size of a volume, so it simply replaces
"current" values by `nil`.

For example, in this case:

```json
"partitions": [
{
"search": "/dev/vda1",
"size": { "min": "current" },
"filesystem": { "path": "/" }
}
]
```

the config conversion generates a size with `nil` for `#min`:

```ruby
partition.size.default? #=> false
partition.size.min #=> nil
partition.size.max #=> Y2Storage::DiskSize.Unlimited
joseivanlopez marked this conversation as resolved.
Show resolved Hide resolved
```

The config solver replaces the `nil` sizes by the device size. In the example before, let's say that
/dev/vda1 has 10 GiB, so the resulting config would be:

```ruby
partition.size.default? #=> false
partition.size.min #=> Y2Storage::DiskSize.GiB(10)
partition.size.max #=> Y2Storage::DiskSize.Unlimited
```

##### Use case: growing a device

```json
"partitions": [
{
"search": "/dev/vda1",
"size": { "min": "current" },
"filesystem": { "path": "/" }
}
]
```

```ruby
partition.size.default? #=> false
partition.size.min #=> Y2Storage::DiskSize.GiB(10)
partition.size.max #=> Y2Storage::DiskSize.Unlimited
```

##### Use case: shrinking a device

```json
"partitions": [
{
"search": "/dev/vda1",
"size": { "min": 0, "max": "current" },
"filesystem": { "path": "/" }
}
]
```

```ruby
partition.size.default? #=> false
partition.size.min #=> 0
partition.size.max #=> Y2Storage::DiskSize.GiB(10)
```

##### Use case: keeping a device size

Note that this is equivalent to omitting the size.

```json
"partitions": [
{
"search": "/dev/vda1",
"size": { "min": "current", "max": "current" },
"filesystem": { "path": "/" }
}
]
```

```ruby
partition.size.default? #=> false
partition.size.min #=> Y2Storage::DiskSize.GiB(10)
partition.size.max #=> Y2Storage::DiskSize.GiB(10)
```

##### Use case: fallback for not found devices

A profile can specify an "advanced search" to indicate that a volume has to be created if it is not
found in the system.

```json
"partitions": [
{
"search": {
"condition": { "name": "/dev/vda1" },
"ifNotFound": "create"
},
"size": { "min": "current" },
"filesystem": { "path": "/" }
}
]
```

If the device does not exist, then "current" cannot be replaced by any device size. In this case,
the config solver uses the default size defined by the product as fallback for "current".

```ruby
partition.size.default? #=> false
partition.size.min #=> Y2Storage::DiskSize.GiB(15)
partition.size.max #=> Y2Storage::DiskSize.Unlimited
```
8 changes: 8 additions & 0 deletions rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ impl<'a> StorageClient<'a> {
Ok(settings)
}

/// Set the storage config according to the JSON schema
pub async fn set_config_model(&self, model: Box<RawValue>) -> Result<u32, ServiceError> {
Ok(self
.storage_proxy
.set_config_model(serde_json::to_string(&model).unwrap().as_str())
.await?)
}

/// Get the storage config model according to the JSON schema
pub async fn get_config_model(&self) -> Result<Box<RawValue>, ServiceError> {
let serialized_config_model = self.storage_proxy.get_config_model().await?;
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 @@ -62,6 +62,9 @@ pub trait Storage1 {
/// Get the current storage config according to the JSON schema
fn get_config(&self) -> zbus::Result<String>;

/// Set the storage config model according to the JSON schema
fn set_config_model(&self, model: &str) -> zbus::Result<u32>;

/// Get the storage config model according to the JSON schema
fn get_config_model(&self) -> zbus::Result<String>;

Expand Down
Loading
Loading