From 9a8d488a624dad3d2a24d6cb5088389d94fbdc47 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 16 Dec 2024 10:47:43 +0100 Subject: [PATCH] Add bootloader section to agama config (jsc#AGM-54) --- .../bus/org.opensuse.Agama.Storage1.bus.xml | 9 ++ rust/agama-lib/share/profile.schema.json | 10 ++ rust/agama-lib/src/bootloader.rs | 28 ++++++ rust/agama-lib/src/bootloader/client.rs | 57 ++++++++++++ rust/agama-lib/src/bootloader/http_client.rs | 43 +++++++++ rust/agama-lib/src/bootloader/model.rs | 30 ++++++ rust/agama-lib/src/bootloader/proxies.rs | 35 +++++++ rust/agama-lib/src/bootloader/store.rs | 49 ++++++++++ rust/agama-lib/src/install_settings.rs | 3 + rust/agama-lib/src/lib.rs | 1 + rust/agama-lib/src/store.rs | 7 ++ rust/agama-server/src/bootloader.rs | 21 +++++ rust/agama-server/src/bootloader/web.rs | 93 +++++++++++++++++++ rust/agama-server/src/lib.rs | 1 + rust/agama-server/src/web.rs | 2 + rust/agama-server/src/web/docs.rs | 2 + rust/agama-server/src/web/docs/bootloader.rs | 43 +++++++++ service/.rubocop.yml | 3 + service/lib/agama/dbus/storage/manager.rb | 34 +++++++ service/lib/agama/storage/bootloader.rb | 85 +++++++++++++++++ service/lib/agama/storage/manager.rb | 7 ++ service/run_tests_in_container.sh | 2 +- service/test/agama/storage/bootloader_test.rb | 51 ++++++++++ 23 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 rust/agama-lib/src/bootloader.rs create mode 100644 rust/agama-lib/src/bootloader/client.rs create mode 100644 rust/agama-lib/src/bootloader/http_client.rs create mode 100644 rust/agama-lib/src/bootloader/model.rs create mode 100644 rust/agama-lib/src/bootloader/proxies.rs create mode 100644 rust/agama-lib/src/bootloader/store.rs create mode 100644 rust/agama-server/src/bootloader.rs create mode 100644 rust/agama-server/src/bootloader/web.rs create mode 100644 rust/agama-server/src/web/docs/bootloader.rs create mode 100644 service/lib/agama/storage/bootloader.rb create mode 100644 service/test/agama/storage/bootloader_test.rb diff --git a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml index 4b4e839ef5..701b615834 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml @@ -51,6 +51,15 @@ + + + + + + + + + diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index d71f7576d0..e9643f43a0 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -39,6 +39,16 @@ } } }, + "bootloader": { + "title": "Bootloader settings", + "type": "object", + "properties": { + "stopOnBootMenu": { + "title": "Specify if bootloader should stop on menu during boot.", + "type": "boolean" + } + } + }, "software": { "title": "Software settings", "type": "object", diff --git a/rust/agama-lib/src/bootloader.rs b/rust/agama-lib/src/bootloader.rs new file mode 100644 index 0000000000..dc6b83528e --- /dev/null +++ b/rust/agama-lib/src/bootloader.rs @@ -0,0 +1,28 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! Implements support for handling the storage settings + +pub mod client; +// pub mod http_client; +pub mod http_client; +pub mod model; +pub mod proxies; +pub mod store; diff --git a/rust/agama-lib/src/bootloader/client.rs b/rust/agama-lib/src/bootloader/client.rs new file mode 100644 index 0000000000..10287522e4 --- /dev/null +++ b/rust/agama-lib/src/bootloader/client.rs @@ -0,0 +1,57 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! Implements a client to access Agama's D-Bus API related to Bootloader management. + +use zbus::Connection; + +use crate::{ + bootloader::{model::BootloaderSettings, proxies::BootloaderProxy}, + error::ServiceError, +}; + +/// Client to connect to Agama's D-Bus API for Bootloader management. +#[derive(Clone)] +pub struct BootloaderClient<'a> { + bootloader_proxy: BootloaderProxy<'a>, +} + +impl<'a> BootloaderClient<'a> { + pub async fn new(connection: Connection) -> Result, ServiceError> { + let bootloader_proxy = BootloaderProxy::new(&connection).await?; + + Ok(Self { bootloader_proxy }) + } + + pub async fn get_config(&self) -> Result { + let serialized_string = self.bootloader_proxy.get_config().await?; + let settings = serde_json::from_str(serialized_string.as_str())?; + Ok(settings) + } + + pub async fn set_config(&self, config: &BootloaderSettings) -> Result<(), ServiceError> { + // ignore return value as currently it does not fail and who knows what future brings + // but it should not be part of result and instead transformed to ServiceError + self.bootloader_proxy + .set_config(serde_json::to_string(config)?.as_str()) + .await?; + Ok(()) + } +} diff --git a/rust/agama-lib/src/bootloader/http_client.rs b/rust/agama-lib/src/bootloader/http_client.rs new file mode 100644 index 0000000000..9cde4444bc --- /dev/null +++ b/rust/agama-lib/src/bootloader/http_client.rs @@ -0,0 +1,43 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! Implements a client to access Agama's HTTP API related to Bootloader management. + +use crate::base_http_client::BaseHTTPClient; +use crate::bootloader::model::BootloaderSettings; +use crate::ServiceError; + +pub struct BootloaderHTTPClient { + client: BaseHTTPClient, +} + +impl BootloaderHTTPClient { + pub fn new(base: BaseHTTPClient) -> Self { + Self { client: base } + } + + pub async fn get_config(&self) -> Result { + self.client.get("/bootloader/config").await + } + + pub async fn set_config(&self, config: &BootloaderSettings) -> Result<(), ServiceError> { + self.client.put_void("/bootloader/config", config).await + } +} diff --git a/rust/agama-lib/src/bootloader/model.rs b/rust/agama-lib/src/bootloader/model.rs new file mode 100644 index 0000000000..ac50f3dd16 --- /dev/null +++ b/rust/agama-lib/src/bootloader/model.rs @@ -0,0 +1,30 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! Implements a data model for Bootloader configuration. + +use serde::{Deserialize, Serialize}; + +/// Represents a Bootloader +#[derive(Clone, Debug, Serialize, Deserialize, Default, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct BootloaderSettings { + pub stop_on_boot_menu: bool, +} diff --git a/rust/agama-lib/src/bootloader/proxies.rs b/rust/agama-lib/src/bootloader/proxies.rs new file mode 100644 index 0000000000..48e2543ed5 --- /dev/null +++ b/rust/agama-lib/src/bootloader/proxies.rs @@ -0,0 +1,35 @@ +//! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1.Bootloader` +//! +//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data. +//! Source: `org.opensuse.Agama.Storage1.bus.xml`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::ObjectManagerProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1", + interface = "org.opensuse.Agama.Storage1.Bootloader", + assume_defaults = true +)] +pub trait Bootloader { + /// GetConfig method + fn get_config(&self) -> zbus::Result; + + /// SetConfig method + fn set_config(&self, serialized_config: &str) -> zbus::Result; +} diff --git a/rust/agama-lib/src/bootloader/store.rs b/rust/agama-lib/src/bootloader/store.rs new file mode 100644 index 0000000000..0ff742c787 --- /dev/null +++ b/rust/agama-lib/src/bootloader/store.rs @@ -0,0 +1,49 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! Implements the store for the storage settings. + +use crate::base_http_client::BaseHTTPClient; +use crate::error::ServiceError; + +use super::http_client::BootloaderHTTPClient; +use super::model::BootloaderSettings; + +/// Loads and stores the storage settings from/to the HTTP service. +pub struct BootloaderStore { + bootloader_client: BootloaderHTTPClient, +} + +impl BootloaderStore { + pub fn new(client: BaseHTTPClient) -> Result { + Ok(Self { + bootloader_client: BootloaderHTTPClient::new(client), + }) + } + + pub async fn load(&self) -> Result { + self.bootloader_client.get_config().await + } + + pub async fn store(&self, settings: &BootloaderSettings) -> Result<(), ServiceError> { + self.bootloader_client.set_config(settings).await?; + Ok(()) + } +} diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index fe5113da0b..77ebdf9a59 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -21,6 +21,7 @@ //! Configuration settings handling //! //! This module implements the mechanisms to load and store the installation settings. +use crate::bootloader::model::BootloaderSettings; use crate::{ localization::LocalizationSettings, network::NetworkSettings, product::ProductSettings, scripts::ScriptsConfig, software::SoftwareSettings, users::UserSettings, @@ -39,6 +40,8 @@ use std::path::Path; #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallSettings { + #[serde(default)] + pub bootloader: Option, #[serde(default, flatten)] pub user: Option, #[serde(default)] diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index d7aab9c5c1..23c27abc0d 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -45,6 +45,7 @@ pub mod auth; pub mod base_http_client; +pub mod bootloader; pub mod error; pub mod install_settings; pub mod jobs; diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 14efddfea5..ff4779d1e0 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -22,6 +22,7 @@ // TODO: quickly explain difference between FooSettings and FooStore, with an example use crate::base_http_client::BaseHTTPClient; +use crate::bootloader::store::BootloaderStore; use crate::error::ServiceError; use crate::install_settings::InstallSettings; use crate::manager::{InstallationPhase, ManagerHTTPClient}; @@ -38,6 +39,7 @@ use crate::{ /// /// This struct uses the default connection built by [connection function](super::connection). pub struct Store { + bootloader: BootloaderStore, users: UsersStore, network: NetworkStore, product: ProductStore, @@ -52,6 +54,7 @@ pub struct Store { impl Store { pub async fn new(http_client: BaseHTTPClient) -> Result { Ok(Self { + bootloader: BootloaderStore::new(http_client.clone())?, localization: LocalizationStore::new(http_client.clone())?, users: UsersStore::new(http_client.clone())?, network: NetworkStore::new(http_client.clone()).await?, @@ -67,6 +70,7 @@ impl Store { /// Loads the installation settings from the HTTP interface. pub async fn load(&self) -> Result { let mut settings = InstallSettings { + bootloader: Some(self.bootloader.load().await?), network: Some(self.network.load().await?), software: Some(self.software.load().await?), user: Some(self.users.load().await?), @@ -121,6 +125,9 @@ impl Store { if settings.storage.is_some() || settings.storage_autoyast.is_some() { self.storage.store(&settings.into()).await? } + if let Some(bootloader) = &settings.bootloader { + self.bootloader.store(bootloader).await?; + } Ok(()) } diff --git a/rust/agama-server/src/bootloader.rs b/rust/agama-server/src/bootloader.rs new file mode 100644 index 0000000000..f0e99a215d --- /dev/null +++ b/rust/agama-server/src/bootloader.rs @@ -0,0 +1,21 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +pub mod web; diff --git a/rust/agama-server/src/bootloader/web.rs b/rust/agama-server/src/bootloader/web.rs new file mode 100644 index 0000000000..bcce7944f6 --- /dev/null +++ b/rust/agama-server/src/bootloader/web.rs @@ -0,0 +1,93 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +//! This module implements the web API for the storage service. +//! +//! The module offers one public function: +//! +//! * `storage_service` which returns the Axum service. +//! +//! stream is not needed, as we do not need to emit signals (for NOW). + +use agama_lib::{ + bootloader::{client::BootloaderClient, model::BootloaderSettings}, + error::ServiceError, +}; +use axum::{extract::State, routing::put, Json, Router}; + +use crate::error; + +#[derive(Clone)] +struct BootloaderState<'a> { + client: BootloaderClient<'a>, +} + +/// Sets up and returns the axum service for the storage module. +pub async fn bootloader_service(dbus: zbus::Connection) -> Result { + let client = BootloaderClient::new(dbus).await?; + let state = BootloaderState { client }; + let router = Router::new() + .route("/config", put(set_config).get(get_config)) + .with_state(state); + Ok(router) +} + +/// Returns the bootloader configuration. +/// +/// * `state` : service state. +#[utoipa::path( + get, + path = "/config", + context_path = "/api/bootloader", + operation_id = "get_bootloader_config", + responses( + (status = 200, description = "bootloader configuration", body = BootloaderSettings), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] +async fn get_config( + State(state): State>, +) -> Result, error::Error> { + // StorageSettings is just a wrapper over serde_json::value::RawValue + let settings = state.client.get_config().await?; + Ok(Json(settings)) +} + +/// Sets the bootloader configuration. +/// +/// * `state`: service state. +/// * `config`: bootloader configuration. +#[utoipa::path( + put, + path = "/config", + context_path = "/api/bootloader", + operation_id = "set_bootloader_config", + responses( + (status = 200, description = "Set the bootloader configuration"), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] +async fn set_config( + State(state): State>, + Json(settings): Json, +) -> Result, error::Error> { + state.client.set_config(&settings).await?; + Ok(Json(())) +} diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index c26eaa774f..8ad2e6cb35 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -18,6 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +pub mod bootloader; pub mod cert; pub mod dbus; pub mod error; diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 2a7ad06e75..8904565ff7 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -25,6 +25,7 @@ //! * Serve the code for the web user interface (not implemented yet). use crate::{ + bootloader::web::bootloader_service, error::Error, l10n::web::l10n_service, manager::web::{manager_service, manager_stream}, @@ -79,6 +80,7 @@ where .add_service("/manager", manager_service(dbus.clone()).await?) .add_service("/software", software_service(dbus.clone()).await?) .add_service("/storage", storage_service(dbus.clone()).await?) + .add_service("/bootloader", bootloader_service(dbus.clone()).await?) .add_service("/network", network_service(network_adapter, events).await?) .add_service("/questions", questions_service(dbus.clone()).await?) .add_service("/users", users_service(dbus.clone()).await?) diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 1592161228..848b0b327c 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -24,6 +24,8 @@ mod network; pub use network::NetworkApiDocBuilder; mod storage; pub use storage::StorageApiDocBuilder; +mod bootloader; +pub use bootloader::BootloaderApiDocBuilder; mod software; pub use software::SoftwareApiDocBuilder; mod l10n; diff --git a/rust/agama-server/src/web/docs/bootloader.rs b/rust/agama-server/src/web/docs/bootloader.rs new file mode 100644 index 0000000000..974efa9135 --- /dev/null +++ b/rust/agama-server/src/web/docs/bootloader.rs @@ -0,0 +1,43 @@ +// 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 the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// 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. + +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; +pub struct BootloaderApiDocBuilder; + +impl ApiDocBuilder for BootloaderApiDocBuilder { + fn title(&self) -> String { + "Bootloader HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() + .path_from::() + .path_from::() + .build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new() + .schema_from::() + .build() + } +} diff --git a/service/.rubocop.yml b/service/.rubocop.yml index f5bea90ce8..2039f37050 100644 --- a/service/.rubocop.yml +++ b/service/.rubocop.yml @@ -31,3 +31,6 @@ Metrics/AbcSize: Metrics/ParameterLists: Max: 6 + +Metrics/ClassLength: + Max: 300 diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 0514b159b5..665ece1828 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -144,6 +144,40 @@ def deprecated_system dbus_reader(:deprecated_system, "b") end + BOOTLOADER_INTERFACE = "org.opensuse.Agama.Storage1.Bootloader" + private_constant :BOOTLOADER_INTERFACE + + # Applies the given serialized config according to the JSON schema. + # + # + # @raise If the config is not valid. + # + # @param serialized_config [String] Serialized storage config. + # @return [Integer] 0 success; 1 error + def load_bootloader_config_from_json(serialized_config) + logger.info("Setting bootloader config from D-Bus: #{serialized_config}") + + backend.bootloader.config.load_json(serialized_config) + + 0 + end + + # Gets and serializes the storage config used to calculate the current proposal. + # + # @return [String] Serialized config according to the JSON schema. + def bootloader_config_as_json + backend.bootloader.config.to_json + end + + dbus_interface BOOTLOADER_INTERFACE do + dbus_method(:SetConfig, "in serialized_config:s, out result:u") do |serialized_config| + load_bootloader_config_from_json(serialized_config) + end + dbus_method(:GetConfig, "out serialized_config:s") do + bootloader_config_as_json + end + end + # @todo Move device related properties here, for example, the list of system and staging # devices, dirty, etc. STORAGE_DEVICES_INTERFACE = "org.opensuse.Agama.Storage1.Devices" diff --git a/service/lib/agama/storage/bootloader.rb b/service/lib/agama/storage/bootloader.rb new file mode 100644 index 0000000000..eb142fad6d --- /dev/null +++ b/service/lib/agama/storage/bootloader.rb @@ -0,0 +1,85 @@ +# 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 "yast" +require "json" +require "bootloader/bootloader_factory" + +module Agama + module Storage + # Represents bootloader specific functionality + class Bootloader + # Represents bootloader settings + class Config + # If bootloader should stop on boot menu + attr_accessor :stop_on_boot_menu + + def initialize + @stop_on_boot_menu = false # false means use proposal, which has timeout + end + + def to_json(*_args) + result = {} + + # our json use camel case + result[:stopOnBootMenu] = stop_on_boot_menu + result.to_json + end + + def load_json(serialized_config) + hsh = JSON.parse(serialized_config, symbolize_names: true) + self.stop_on_boot_menu = hsh[:stopOnBootMenu] if hsh.include?(:stopOnBootMenu) + end + end + + attr_reader :config + + def initialize(logger) + @config = Config.new + @logger = logger + end + + def write_config + bootloader = ::Bootloader::BootloaderFactory.current + case @config.stop_on_boot_menu + when true + # grub2 based bootloaders + if bootloader.respond_to?(:grub_default) + bootloader.grub_default.timeout = -1 + # systemd bootloader + elsif bootloader.respond_to?(:menu_timeout) + bootloader.menu_timeout = -1 + else + @logger.info "bootloader #{bootloader.name} does not support forcing user input" + end + when false + # TODO: basically as we have single argument we repropose here. If more attributes comes + # we will need to do always propose first and then modify what is in config set + bootloader.propose + when nil + # not set, so do nothing and keep it as it is + else + @logger.error "unexpected value for stop_on_boot_menu #{@config.stop_on_boot_menu}" + end + end + end + end +end diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index d7ea94436c..4e5fb937ed 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -24,6 +24,7 @@ require "y2storage/storage_manager" require "y2storage/clients/inst_prepdisk" require "agama/storage/actions_generator" +require "agama/storage/bootloader" require "agama/storage/proposal" require "agama/storage/proposal_settings" require "agama/storage/callbacks" @@ -52,6 +53,9 @@ class Manager # @return [Config] attr_reader :config + # @return [Bootloader] + attr_reader :bootloader + # Constructor # # @param config [Config] @@ -61,6 +65,7 @@ def initialize(config, logger) @config = config @logger = logger + @bootloader = Bootloader.new(logger) register_proposal_callbacks on_progress_change { logger.info progress.to_s } end @@ -126,6 +131,8 @@ def install progress.step(_("Preparing bootloader proposal")) do # first make bootloader proposal to be sure that required packages are installed proposal = ::Bootloader::ProposalClient.new.make_proposal({}) + # then also apply changes to that proposal + bootloader.write_config logger.debug "Bootloader proposal #{proposal.inspect}" end progress.step(_("Adding storage-related packages")) { add_packages } diff --git a/service/run_tests_in_container.sh b/service/run_tests_in_container.sh index 429f2aa6ce..c298048703 100644 --- a/service/run_tests_in_container.sh +++ b/service/run_tests_in_container.sh @@ -4,7 +4,7 @@ set -ex SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) podman create -ti --rm --entrypoint '["sh", "-c"]' --name agama_ruby_tests -v $SCRIPT_DIR/..:/checkout registry.opensuse.org/yast/head/containers_tumbleweed/yast-ruby sh podman start agama_ruby_tests -podman exec agama_ruby_tests zypper --non-interactive install yast2-iscsi-client ruby3.2-rubygem-eventmachine +podman exec agama_ruby_tests zypper --non-interactive install yast2-iscsi-client yast2-bootloader ruby3.3-rubygem-eventmachine if podman exec --workdir /checkout/service agama_ruby_tests rake test:unit; then if [ "$KEEP_RUNNING" != "1" ]; then podman stop agama_ruby_tests diff --git a/service/test/agama/storage/bootloader_test.rb b/service/test/agama/storage/bootloader_test.rb new file mode 100644 index 0000000000..40a5d9c315 --- /dev/null +++ b/service/test/agama/storage/bootloader_test.rb @@ -0,0 +1,51 @@ +# 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_relative "../../test_helper" + +require "agama/storage/bootloader" + +describe Agama::Storage::Bootloader::Config do + subject(:config) { described_class.new } + + describe "#to_json" do + it "serializes its content with keys as camelCase" do + config.stop_on_boot_menu = true + expect(config.to_json).to eq "{\"stopOnBootMenu\":true}" + end + + it "can serialize in a way that #load_json can restore it" do + config.stop_on_boot_menu = false + json = config.to_json + config.stop_on_boot_menu = true + config.load_json(json) + expect(config.stop_on_boot_menu).to eq false + end + end + + describe "#load_json" do + it "loads config from given json" do + content = "{\"stopOnBootMenu\":true}" + config.load_json(content) + expect(config.stop_on_boot_menu).to eq true + end + end +end