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

fix(web): storage reprobe #1911

Merged
Merged
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
2 changes: 2 additions & 0 deletions doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
<interface name="org.opensuse.Agama.Storage1">
<method name="Probe">
</method>
<method name="Reprobe">
</method>
<method name="SetConfig">
<arg name="serialized_config" direction="in" type="s"/>
<arg name="result" direction="out" type="u"/>
Expand Down
5 changes: 5 additions & 0 deletions doc/dbus/org.opensuse.Agama.Storage1.doc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<interface name="org.opensuse.Agama.Storage1">
<method name="Probe">
</method>
<!--
Probes the system and recalculates the proposal using the current config.
-->
<method name="Reprobe">
</method>
<!--
Sets the storage config.
-->
Expand Down
7 changes: 6 additions & 1 deletion rust/agama-lib/src/storage/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) [2024] SUSE LLC
// Copyright (c) [2024-2025] SUSE LLC
//
// All Rights Reserved.
//
Expand Down Expand Up @@ -139,6 +139,11 @@ impl<'a> StorageClient<'a> {
Ok(self.storage_proxy.probe().await?)
}

/// Runs the reprobing process
pub async fn reprobe(&self) -> Result<(), ServiceError> {
Ok(self.storage_proxy.reprobe().await?)
}

/// Set the storage config according to the JSON schema
pub async fn set_config(&self, settings: StorageSettings) -> Result<u32, ServiceError> {
Ok(self
Expand Down
5 changes: 4 additions & 1 deletion rust/agama-lib/src/storage/proxies/storage1.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) [2024] SUSE LLC
// Copyright (c) [2024-2025] SUSE LLC
//
// All Rights Reserved.
//
Expand Down Expand Up @@ -56,6 +56,9 @@ pub trait Storage1 {
/// Probe method
fn probe(&self) -> zbus::Result<()>;

/// Reprobe method
fn reprobe(&self) -> zbus::Result<()>;

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

Expand Down
20 changes: 18 additions & 2 deletions rust/agama-server/src/storage/web.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) [2024] SUSE LLC
// Copyright (c) [2024-2025] SUSE LLC
//
// All Rights Reserved.
//
Expand Down Expand Up @@ -116,6 +116,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result<Router, ServiceEr
.route("/config", put(set_config).get(get_config))
.route("/config_model", put(set_config_model).get(get_config_model))
.route("/probe", post(probe))
.route("/reprobe", post(reprobe))
.route("/devices/dirty", get(devices_dirty))
.route("/devices/system", get(system_devices))
.route("/devices/result", get(staging_devices))
Expand Down Expand Up @@ -240,7 +241,7 @@ async fn set_config_model(
path = "/probe",
context_path = "/api/storage",
responses(
(status = 200, description = "Devices were probed and an initial proposal were performed"),
(status = 200, description = "Devices were probed and an initial proposal was performed"),
(status = 400, description = "The D-Bus service could not perform the action")
),
operation_id = "storage_probe"
Expand All @@ -249,6 +250,21 @@ async fn probe(State(state): State<StorageState<'_>>) -> Result<Json<()>, Error>
Ok(Json(state.client.probe().await?))
}

/// Reprobes the storage devices.
#[utoipa::path(
post,
path = "/reprobe",
context_path = "/api/storage",
responses(
(status = 200, description = "Devices were probed and the proposal was recalculated"),
(status = 400, description = "The D-Bus service could not perform the action")
),
operation_id = "storage_reprobe"
)]
async fn reprobe(State(state): State<StorageState<'_>>) -> Result<Json<()>, Error> {
Ok(Json(state.client.reprobe().await?))
}

/// Gets whether the system is in a deprecated status.
///
/// The system is usually set as deprecated as effect of managing some kind of devices, for example,
Expand Down
9 changes: 6 additions & 3 deletions service/lib/agama/dbus/storage/manager.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Copyright (c) [2022-2024] SUSE LLC
# Copyright (c) [2022-2025] SUSE LLC
#
# All Rights Reserved.
#
Expand Down Expand Up @@ -87,13 +87,15 @@ def issues
STORAGE_INTERFACE = "org.opensuse.Agama.Storage1"
private_constant :STORAGE_INTERFACE

def probe
# @param keep_config [Boolean] Whether to use the current storage config for calculating
# the proposal.
def probe(keep_config: false)
busy_while do
# Clean trees in advance to avoid having old objects exported in D-Bus.
system_devices_tree.clean
staging_devices_tree.clean

backend.probe
backend.probe(keep_config: keep_config)
end
end

Expand Down Expand Up @@ -162,6 +164,7 @@ def deprecated_system

dbus_interface STORAGE_INTERFACE do
dbus_method(:Probe) { probe }
dbus_method(:Reprobe) { probe(keep_config: true) }
dbus_method(:SetConfig, "in serialized_config:s, out result:u") do |serialized_config|
busy_while { apply_config(serialized_config) }
end
Expand Down
26 changes: 17 additions & 9 deletions service/lib/agama/storage/manager.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Copyright (c) [2022-2024] SUSE LLC
# Copyright (c) [2022-2025] SUSE LLC
#
# All Rights Reserved.
#
Expand Down Expand Up @@ -113,14 +113,21 @@ def on_probe(&block)
end

# Probes storage devices and performs an initial proposal
def probe
#
# @param keep_config [Boolean] Whether to use the current storage config for calculating the
# proposal.
def probe(keep_config: false)
start_progress_with_size(4)
product_config.pick_product(software.selected_product)
check_multipath
progress.step(_("Activating storage devices")) { activate_devices }
progress.step(_("Probing storage devices")) { probe_devices }
progress.step(_("Calculating the storage proposal")) { calculate_proposal }
progress.step(_("Calculating the storage proposal")) do
calculate_proposal(keep_config: keep_config)
end
progress.step(_("Selecting Linux Security Modules")) { security.probe }
# The system is not deprecated anymore
self.deprecated_system = false
update_issues
@on_probe_callbacks&.each(&:call)
end
Expand Down Expand Up @@ -216,14 +223,15 @@ def probe_devices

iscsi.probe
Y2Storage::StorageManager.instance.probe(callbacks)

# The system is not deprecated anymore
self.deprecated_system = false
end

# Calculates the proposal using the storage config from the product.
def calculate_proposal
config_json = ConfigJSONReader.new(product_config).read
# Calculates the proposal.
#
# @param keep_config [Boolean] Whether to use the current storage config for calculating the
# proposal. If false, then the default config from the product is used.
def calculate_proposal(keep_config: false)
config_json = proposal.storage_json if keep_config
config_json ||= ConfigJSONReader.new(product_config).read
proposal.calculate_from_json(config_json)
end

Expand Down
46 changes: 44 additions & 2 deletions service/test/agama/storage/manager_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Copyright (c) [2022-2024] SUSE LLC
# Copyright (c) [2022-2025] SUSE LLC
#
# All Rights Reserved.
#
Expand Down Expand Up @@ -167,6 +167,10 @@
allow(proposal).to receive(:issues).and_return(proposal_issues)
allow(proposal).to receive(:available_devices).and_return(devices)
allow(proposal).to receive(:calculate_from_json)
allow(proposal).to receive(:storage_json).and_return(current_config)

allow_any_instance_of(Agama::Storage::ConfigJSONReader)
.to receive(:read).and_return(default_config)

allow(config).to receive(:pick_product)
allow(iscsi).to receive(:activate)
Expand All @@ -181,6 +185,26 @@

let(:proposal) { Agama::Storage::Proposal.new(config, logger: logger) }

let(:default_config) do
{
storage: {
drives: [
search: "/dev/vda1"
]
}
}
end

let(:current_config) do
{
storage: {
drives: [
search: "/dev/vda2"
]
}
}
end

let(:iscsi) { Agama::Storage::ISCSI::Manager.new }

let(:devices) { [disk1, disk2] }
Expand All @@ -194,7 +218,7 @@

let(:callback) { proc {} }

it "probes the storage devices and calculates a proposal with the default settings" do
it "probes the storage devices and calculates a proposal" do
expect(config).to receive(:pick_product).with("ALP")
expect(iscsi).to receive(:activate)
expect(y2storage_manager).to receive(:activate) do |callbacks|
Expand Down Expand Up @@ -237,6 +261,24 @@
storage.probe
end

context "if :keep_config is false" do
let(:keep_config) { false }

it "calculates a proposal using the default product config" do
expect(proposal).to receive(:calculate_from_json).with(default_config)
storage.probe(keep_config: keep_config)
end
end

context "if :keep_config is true" do
let(:keep_config) { true }

it "calculates a proposal using the current config" do
expect(proposal).to receive(:calculate_from_json).with(current_config)
storage.probe(keep_config: keep_config)
end
end

context "if there are available devices" do
let(:devices) { [disk1] }

Expand Down
39 changes: 21 additions & 18 deletions web/src/api/storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2024] SUSE LLC
* Copyright (c) [2024-2025] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -22,16 +22,17 @@

import { get, post, put } from "~/api/http";
import { Job } from "~/types/job";
import { calculate, fetchSettings } from "~/api/storage/proposal";
import { config, configModel } from "~/api/storage/types";
import { Action, config, configModel, ProductParams, Volume } from "~/api/storage/types";

/**
* Starts the storage probing process.
*/

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const probe = (): Promise<any> => post("/api/storage/probe");

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reprobe = (): Promise<any> => post("/api/storage/reprobe");

const fetchConfig = (): Promise<config.Config | undefined> =>
get("/api/storage/config").then((config) => config.storage);

Expand All @@ -42,6 +43,17 @@ const setConfig = (config: config.Config) => put("/api/storage/config", { storag

const setConfigModel = (model: configModel.Config) => put("/api/storage/config_model", model);

const fetchUsableDevices = (): Promise<number[]> => get(`/api/storage/proposal/usable_devices`);

const fetchProductParams = (): Promise<ProductParams> => get("/api/storage/product/params");

const fetchDefaultVolume = (mountPath: string): Promise<Volume | undefined> => {
const path = encodeURIComponent(mountPath);
return get(`/api/storage/product/volume_for?mount_path=${path}`);
};

const fetchActions = (): Promise<Action[]> => get("/api/storage/devices/actions");

/**
* Returns the list of jobs
*/
Expand All @@ -53,26 +65,17 @@ const fetchStorageJobs = (): Promise<Job[]> => get("/api/storage/jobs");
const findStorageJob = (id: string): Promise<Job | undefined> =>
fetchStorageJobs().then((jobs: Job[]) => jobs.find((value) => value.id === id));

/**
* Refreshes the storage layer.
*
* It does the probing again and recalculates the proposal with the same
* settings. Internally, it is composed of three different API calls
* (retrieve the settings, probe the system, and calculate the proposal).
*/
const refresh = async (): Promise<void> => {
const settings = await fetchSettings();
await probe().catch(console.log);
await calculate(settings).catch(console.log);
};

export {
probe,
reprobe,
fetchConfig,
fetchConfigModel,
setConfig,
setConfigModel,
fetchUsableDevices,
fetchProductParams,
fetchDefaultVolume,
fetchActions,
fetchStorageJobs,
findStorageJob,
refresh,
};
36 changes: 4 additions & 32 deletions web/src/api/storage/proposal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2024] SUSE LLC
* Copyright (c) [2024-2025] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -20,38 +20,10 @@
* find current contact information at www.suse.com.
*/

import { get, put } from "../http";
import {
Action,
ProductParams,
ProposalSettings,
ProposalSettingsPatch,
Volume,
} from "~/api/storage/types";

const fetchUsableDevices = (): Promise<number[]> => get(`/api/storage/proposal/usable_devices`);

const fetchProductParams = (): Promise<ProductParams> => get("/api/storage/product/params");

const fetchDefaultVolume = (mountPath: string): Promise<Volume | undefined> => {
const path = encodeURIComponent(mountPath);
return get(`/api/storage/product/volume_for?mount_path=${path}`);
};

// NOTE: the settings might not exist.
const fetchSettings = (): Promise<ProposalSettings> =>
get("/api/storage/proposal/settings").catch(() => null);

const fetchActions = (): Promise<Action[]> => get("/api/storage/devices/actions");
import { put } from "../http";
import { ProposalSettingsPatch } from "~/api/storage/types";

const calculate = (settings: ProposalSettingsPatch) =>
put("/api/storage/proposal/settings", settings);

export {
fetchUsableDevices,
fetchProductParams,
fetchDefaultVolume,
fetchSettings,
fetchActions,
calculate,
};
export { calculate };
Loading
Loading