diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 9f8a94290c..78e721350b 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -209,7 +209,7 @@ jobs: # use the "stable" tool chain (installed by default) instead of the "nightly" default in tarpaulin RUSTC_BOOTSTRAP: 1 RUSTUP_TOOLCHAIN: stable - RUST_BACKTRACE: 1 + RUST_BACKTRACE: full RUSTFLAGS: --cfg ci # send the code coverage for the Rust part to the coveralls.io diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1d3276db22..98bcb7c3e9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -13,6 +13,7 @@ name = "agama-autoinstall" version = "0.1.0" dependencies = [ "agama-lib", + "agama-transfer", "agama-utils", "anyhow", "tempfile", @@ -26,13 +27,14 @@ name = "agama-cli" version = "1.0.0" dependencies = [ "agama-lib", + "agama-transfer", "agama-utils", "anyhow", "async-trait", "chrono", "clap", "console", - "fluent-uri", + "fluent-uri 0.3.2", "home", "indicatif", "inquire", @@ -45,6 +47,21 @@ dependencies = [ "url", ] +[[package]] +name = "agama-files" +version = "0.1.0" +dependencies = [ + "agama-software", + "agama-utils", + "async-trait", + "serde_json", + "tempfile", + "test-context", + "thiserror 2.0.17", + "tokio", + "tokio-test", +] + [[package]] name = "agama-l10n" version = "0.1.0" @@ -71,13 +88,14 @@ dependencies = [ "agama-l10n", "agama-locale-data", "agama-network", + "agama-transfer", "agama-utils", "anyhow", "async-trait", "chrono", "curl", "env_logger", - "fluent-uri", + "fluent-uri 0.3.2", "fs_extra", "futures-util", "home", @@ -123,6 +141,7 @@ dependencies = [ name = "agama-manager" version = "0.1.0" dependencies = [ + "agama-files", "agama-l10n", "agama-network", "agama-software", @@ -174,6 +193,7 @@ dependencies = [ "agama-manager", "agama-network", "agama-software", + "agama-transfer", "agama-utils", "anyhow", "async-trait", @@ -262,13 +282,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "agama-transfer" +version = "0.1.0" +dependencies = [ + "curl", + "regex", + "thiserror 2.0.17", + "url", +] + [[package]] name = "agama-utils" version = "0.1.0" dependencies = [ "agama-locale-data", + "agama-transfer", "async-trait", "cidr", + "fluent-uri 0.4.1", "fs-err", "gettext-rs", "macaddr", @@ -278,6 +310,7 @@ dependencies = [ "serde_with", "serde_yaml", "strum", + "tempfile", "test-context", "thiserror 2.0.17", "tokio", @@ -913,9 +946,9 @@ dependencies = [ [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "brotli" @@ -1305,24 +1338,24 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.47" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" +checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2 0.5.9", - "windows-sys 0.52.0", + "socket2 0.6.0", + "windows-sys 0.59.0", ] [[package]] name = "curl-sys" -version = "0.4.80+curl-8.12.1" +version = "0.4.84+curl-8.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734" +checksum = "abc4294dc41b882eaff37973c2ec3ae203d0091341ee68fbadd1d06e0c18a73b" dependencies = [ "cc", "libc", @@ -1330,7 +1363,7 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1627,6 +1660,17 @@ dependencies = [ "serde", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3587,7 +3631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" dependencies = [ "ahash", - "fluent-uri", + "fluent-uri 0.3.2", "once_cell", "parking_lot", "percent-encoding", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 72f3c67f58..ad6ef8727d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,6 +2,7 @@ members = [ "agama-autoinstall", "agama-cli", + "agama-files", "agama-l10n", "agama-lib", "agama-locale-data", @@ -10,6 +11,7 @@ members = [ "agama-server", "agama-software", "agama-storage", + "agama-transfer", "agama-utils", "xtask", "zypp-agama", diff --git a/rust/agama-autoinstall/Cargo.toml b/rust/agama-autoinstall/Cargo.toml index 9eda50e3b7..ae668af56a 100644 --- a/rust/agama-autoinstall/Cargo.toml +++ b/rust/agama-autoinstall/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] agama-lib = { path = "../agama-lib" } agama-utils = { path = "../agama-utils" } +agama-transfer = { path = "../agama-transfer" } anyhow = { version = "1.0.98" } tempfile = "3.20.0" thiserror = "2.0.12" diff --git a/rust/agama-autoinstall/src/scripts.rs b/rust/agama-autoinstall/src/scripts.rs index 6058c61c6b..9c6e1bddd9 100644 --- a/rust/agama-autoinstall/src/scripts.rs +++ b/rust/agama-autoinstall/src/scripts.rs @@ -26,7 +26,9 @@ use std::{ process::Output, }; -use agama_lib::{http::BaseHTTPClient, utils::Transfer}; +use agama_lib::http::BaseHTTPClient; +use agama_transfer::Transfer; +use agama_utils::command::run_with_retry; use anyhow::anyhow; use url::Url; @@ -60,6 +62,8 @@ impl ScriptsRunner { /// It downloads the script from the given URL to the runner directory. /// It saves the stdout, stderr and exit code to separate files. /// + /// It will retry if it cannot run the script. + /// /// * url: script URL, supporting agama-specific schemes. pub async fn run(&mut self, url: &str) -> anyhow::Result<()> { create_dir_all(&self.path)?; @@ -69,9 +73,9 @@ impl ScriptsRunner { let path = self.path.join(&file_name); self.save_script(url, &path).await?; - let output = std::process::Command::new(&path).output()?; + let command = tokio::process::Command::new(&path); + let output = run_with_retry(command).await?; self.save_logs(&path, output)?; - Ok(()) } @@ -98,11 +102,12 @@ impl ScriptsRunner { async fn save_script(&self, url: &str, path: &PathBuf) -> anyhow::Result<()> { let mut file = Self::create_file(&path, 0o700)?; while let Err(error) = Transfer::get(url, &mut file, self.insecure) { - eprintln!("Could not load configuration from {url}: {error}"); + eprintln!("Could not load the script from {url}: {error}"); if !self.should_retry(&url, &error.to_string()).await? { return Err(anyhow!(error)); } } + file.sync_all()?; Ok(()) } diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index da6110de27..4b1d87b9cc 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" clap = { version = "4.5.19", features = ["derive", "wrap_help"] } agama-lib = { path = "../agama-lib" } agama-utils = { path = "../agama-utils" } +agama-transfer = { path = "../agama-transfer" } serde_json = "1.0.128" indicatif = "0.17.8" thiserror = "2.0.12" diff --git a/rust/agama-cli/src/cli_input.rs b/rust/agama-cli/src/cli_input.rs index 61fa559d6b..fbe570e6a9 100644 --- a/rust/agama-cli/src/cli_input.rs +++ b/rust/agama-cli/src/cli_input.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_lib::utils::Transfer; +use agama_transfer::Transfer; use anyhow::Context; use std::{ collections::HashMap, diff --git a/rust/agama-cli/src/lib.rs b/rust/agama-cli/src/lib.rs index e473c397bf..10a81fdac4 100644 --- a/rust/agama-cli/src/lib.rs +++ b/rust/agama-cli/src/lib.rs @@ -22,6 +22,7 @@ use agama_lib::auth::AuthToken; use agama_lib::context::InstallationContext; use agama_lib::manager::{FinishMethod, ManagerHTTPClient}; use agama_lib::monitor::{Monitor, MonitorClient}; +use agama_transfer::Transfer; use anyhow::Context; use auth_tokens_file::AuthTokensFile; use clap::{Args, Parser}; @@ -40,8 +41,8 @@ mod progress; mod questions; use crate::error::CliError; +use agama_lib::error::ServiceError; use agama_lib::http::{BaseHTTPClient, WebSocketClient}; -use agama_lib::{error::ServiceError, utils::Transfer}; use auth::run as run_auth_cmd; use commands::Commands; use config::run as run_config_cmd; diff --git a/rust/agama-files/Cargo.toml b/rust/agama-files/Cargo.toml new file mode 100644 index 0000000000..34dd7f83d3 --- /dev/null +++ b/rust/agama-files/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "agama-files" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +agama-software = { version = "0.1.0", path = "../agama-software" } +agama-utils = { path = "../agama-utils" } +async-trait = "0.1.89" +tempfile = "3.23.0" +thiserror = "2.0.17" +tokio = { version = "1.48.0", features = ["sync"] } + +[dev-dependencies] +tokio-test = "0.4.4" +test-context = "0.4.1" +serde_json = "1.0.145" diff --git a/rust/agama-files/src/lib.rs b/rust/agama-files/src/lib.rs new file mode 100644 index 0000000000..5ed26f8c6d --- /dev/null +++ b/rust/agama-files/src/lib.rs @@ -0,0 +1,139 @@ +// Copyright (c) [2025] 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 crate implements the support for handling files and scripts in Agama. + +pub mod service; +pub use service::{Service, Starter}; + +pub mod message; + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use agama_software::test_utils::start_service as start_software_service; + use agama_utils::{ + actor::Handler, + api::{ + files::{scripts::ScriptsGroup, Config}, + Event, + }, + issue, progress, question, + }; + use tempfile::TempDir; + use test_context::{test_context, AsyncTestContext}; + use tokio::sync::broadcast; + + use crate::{message, service::Error, Service}; + + struct Context { + handler: Handler, + tmp_dir: TempDir, + } + + impl AsyncTestContext for Context { + async fn setup() -> Context { + // Set the PATH + let old_path = std::env::var("PATH").unwrap(); + let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../share/bin"); + std::env::set_var("PATH", format!("{}:{}", &bin_dir.display(), &old_path)); + + // Set up the chroot + let tmp_dir = TempDir::with_prefix("test").unwrap(); + std::fs::create_dir_all(tmp_dir.path().join("usr/bin")).unwrap(); + std::fs::copy("/usr/bin/install", tmp_dir.path().join("usr/bin/install")).unwrap(); + + // Set up the service + let (events_tx, _events_rx) = broadcast::channel::(16); + let issues = issue::Service::starter(events_tx.clone()).start(); + let progress = progress::Service::starter(events_tx.clone()).start(); + let questions = question::start(events_tx.clone()).await.unwrap(); + let software = + start_software_service(events_tx.clone(), issues, progress.clone(), questions) + .await; + let handler = Service::starter(events_tx.clone(), progress, software) + .with_scripts_workdir(tmp_dir.path()) + .with_install_dir(tmp_dir.path()) + .start() + .await + .unwrap(); + Context { handler, tmp_dir } + } + } + + #[test_context(Context)] + #[tokio::test] + async fn test_add_and_run_scripts(ctx: &mut Context) -> Result<(), Error> { + let test_file_1 = ctx.tmp_dir.path().join("file-1.txt"); + let test_file_2 = ctx.tmp_dir.path().join("file-2.txt"); + + let pre_script_json = format!( + "{{ \"name\": \"pre.sh\", \"content\": \"#!/usr/bin/bash\\nset -x\\ntouch {}\" }}", + test_file_1.to_str().unwrap() + ); + + let init_script_json = format!( + "{{ \"name\": \"init.sh\", \"content\": \"#!/usr/bin/bash\\ntouch {}\" }}", + test_file_2.to_str().unwrap() + ); + + let config = format!( + "{{ \"scripts\": {{ \"pre\": [{}], \"init\": [{}] }} }}", + pre_script_json, init_script_json + ); + + let config: Config = serde_json::from_str(&config).unwrap(); + ctx.handler + .call(message::SetConfig::with(config)) + .await + .unwrap(); + + ctx.handler + .call(message::RunScripts::new(ScriptsGroup::Pre)) + .await + .unwrap(); + + // Check that only the pre-script ran + assert!(std::fs::exists(&test_file_1).unwrap()); + assert!(!std::fs::exists(&test_file_2).unwrap()); + Ok(()) + } + + #[test_context(Context)] + #[tokio::test] + async fn test_add_and_write_files(ctx: &mut Context) -> Result<(), Error> { + let config = + r#"{ "files": [{ "destination": "/etc/README.md", "content": "Some text" }] }"#; + + let config: Config = serde_json::from_str(&config).unwrap(); + ctx.handler + .call(message::SetConfig::with(config)) + .await + .unwrap(); + + ctx.handler.call(message::WriteFiles).await.unwrap(); + + // Check that the file exists + let expected_path = ctx.tmp_dir.path().join("etc/README.md"); + assert!(std::fs::exists(&expected_path).unwrap()); + Ok(()) + } +} diff --git a/rust/agama-server/src/files.rs b/rust/agama-files/src/message.rs similarity index 52% rename from rust/agama-server/src/files.rs rename to rust/agama-files/src/message.rs index 1202c7853c..54d24ad160 100644 --- a/rust/agama-server/src/files.rs +++ b/rust/agama-files/src/message.rs @@ -18,4 +18,50 @@ // 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; +use agama_utils::{ + actor::Message, + api::files::{scripts::ScriptsGroup, Config}, +}; + +#[derive(Clone)] +pub struct SetConfig { + pub config: Option, +} + +impl Message for SetConfig { + type Reply = (); +} + +impl SetConfig { + pub fn new(config: Option) -> Self { + Self { config } + } + + pub fn with(config: Config) -> Self { + Self { + config: Some(config), + } + } +} + +#[derive(Clone)] +pub struct RunScripts { + pub group: ScriptsGroup, +} + +impl RunScripts { + pub fn new(group: ScriptsGroup) -> Self { + RunScripts { group } + } +} + +impl Message for RunScripts { + type Reply = (); +} + +#[derive(Clone)] +pub struct WriteFiles; + +impl Message for WriteFiles { + type Reply = (); +} diff --git a/rust/agama-files/src/service.rs b/rust/agama-files/src/service.rs new file mode 100644 index 0000000000..3be665e2b1 --- /dev/null +++ b/rust/agama-files/src/service.rs @@ -0,0 +1,201 @@ +// Copyright (c) [2025] 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 std::path::{Path, PathBuf}; + +use agama_software::{self as software, Resolvable, ResolvableType}; +use agama_utils::{ + actor::{self, Actor, Handler, MessageHandler}, + api::{ + event, + files::{ + scripts::{self, ScriptsRepository}, + user_file, ScriptsConfig, UserFile, + }, + }, + progress, +}; +use async_trait::async_trait; + +use crate::message; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Files(#[from] user_file::Error), + #[error(transparent)] + Scripts(#[from] scripts::Error), + #[error(transparent)] + Software(#[from] software::service::Error), + #[error(transparent)] + Actor(#[from] actor::Error), +} + +const DEFAULT_SCRIPTS_DIR: &str = "/run/agama/scripts"; +const DEFAULT_INSTALL_DIR: &str = "/mnt"; + +/// Builds and spawns the files service. +/// +/// This structs allows to build a files service. +pub struct Starter { + progress: Handler, + events: event::Sender, + software: Handler, + scripts_workdir: PathBuf, + install_dir: PathBuf, +} + +impl Starter { + /// Creates a new starter. + /// + /// * `events`: channel to emit the [localization-specific events](crate::Event). + pub fn new( + events: event::Sender, + progress: Handler, + software: Handler, + ) -> Self { + Self { + events, + progress, + software, + scripts_workdir: PathBuf::from(DEFAULT_SCRIPTS_DIR), + install_dir: PathBuf::from(DEFAULT_INSTALL_DIR), + } + } + + /// Starts the service and returns the handler to communicate with it. + pub async fn start(self) -> Result, Error> { + let scripts = ScriptsRepository::new(self.scripts_workdir); + let service = Service { + progress: self.progress, + events: self.events, + software: self.software, + scripts, + files: vec![], + install_dir: self.install_dir, + }; + let handler = actor::spawn(service); + Ok(handler) + } + + pub fn with_scripts_workdir>(mut self, workdir: P) -> Self { + self.scripts_workdir = PathBuf::from(workdir.as_ref()); + self + } + + pub fn with_install_dir>(mut self, install_dir: P) -> Self { + self.install_dir = PathBuf::from(install_dir.as_ref()); + self + } +} + +pub struct Service { + progress: Handler, + events: event::Sender, + software: Handler, + scripts: ScriptsRepository, + files: Vec, + install_dir: PathBuf, +} + +impl Service { + pub fn starter( + events: event::Sender, + progress: Handler, + software: Handler, + ) -> Starter { + Starter::new(events, progress, software) + } + + pub async fn add_scripts(&mut self, config: ScriptsConfig) -> Result<(), Error> { + if let Some(scripts) = config.pre { + for pre in scripts { + self.scripts.add(pre.into())?; + } + } + + if let Some(scripts) = config.post_partitioning { + for post in scripts { + self.scripts.add(post.into())?; + } + } + + if let Some(scripts) = config.post { + for post in scripts { + self.scripts.add(post.into())?; + } + } + + let mut packages = vec![]; + if let Some(scripts) = config.init { + for init in scripts { + self.scripts.add(init.into())?; + } + packages.push(Resolvable::new("agama-scripts", ResolvableType::Package)); + } + _ = self + .software + .call(agama_software::message::SetResolvables::new( + "agama-scripts".to_string(), + packages, + )) + .await?; + Ok(()) + } +} + +impl Actor for Service { + type Error = Error; +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { + self.scripts.clear()?; + + let config = message.config.unwrap_or_default(); + + if let Some(scripts) = config.scripts { + self.add_scripts(scripts.clone()).await?; + } + + self.files = config.files; + + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::RunScripts) -> Result<(), Error> { + self.scripts.run(message.group).await; + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::WriteFiles) -> Result<(), Error> { + for file in &self.files { + file.write(&self.install_dir).await?; + } + Ok(()) + } +} diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 5dcd9f5d00..8de268bf4a 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -11,6 +11,7 @@ agama-utils = { path = "../agama-utils" } agama-network = { path = "../agama-network" } agama-locale-data = { path = "../agama-locale-data" } agama-l10n = { path = "../agama-l10n" } +agama-transfer = { path = "../agama-transfer" } async-trait = "0.1.83" futures-util = "0.3.30" jsonschema = { version = "0.30.0", default-features = false, features = [ diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index bca62c042e..c54566ce6a 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -23,7 +23,7 @@ use std::io; use thiserror::Error; use zbus::{self, zvariant}; -use crate::utils::TransferError; +use agama_transfer::Error as TransferError; #[derive(Error, Debug)] pub enum ServiceError { diff --git a/rust/agama-lib/src/files.rs b/rust/agama-lib/src/files.rs index c51b9e9400..8f962ab049 100644 --- a/rust/agama-lib/src/files.rs +++ b/rust/agama-lib/src/files.rs @@ -20,8 +20,4 @@ //! Implements support for handling the file deployment -pub mod client; pub mod error; -pub mod model; -pub mod settings; -pub mod store; diff --git a/rust/agama-lib/src/files/client.rs b/rust/agama-lib/src/files/client.rs deleted file mode 100644 index 33716fbd17..0000000000 --- a/rust/agama-lib/src/files/client.rs +++ /dev/null @@ -1,55 +0,0 @@ -// 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 super::model::UserFile; -use crate::http::{BaseHTTPClient, BaseHTTPClientError}; - -#[derive(Debug, thiserror::Error)] -pub enum FilesHTTPClientError { - #[error(transparent)] - HTTP(#[from] BaseHTTPClientError), -} - -pub struct FilesClient { - client: BaseHTTPClient, -} - -impl FilesClient { - pub fn new(base: BaseHTTPClient) -> Self { - Self { client: base } - } - - /// returns list of files that will be manually deployed - pub async fn get_files(&self) -> Result, FilesHTTPClientError> { - Ok(self.client.get("/files").await?) - } - - /// Sets the list of files that will be manually deployed - pub async fn set_files(&self, config: &Vec) -> Result<(), FilesHTTPClientError> { - Ok(self.client.put_void("/files", config).await?) - } - - /// writes the files to target - pub async fn write_files(&self) -> Result<(), FilesHTTPClientError> { - Ok(self.client.post_void("/files/write", &()).await?) - } -} diff --git a/rust/agama-lib/src/files/store.rs b/rust/agama-lib/src/files/store.rs deleted file mode 100644 index ffefa1611e..0000000000 --- a/rust/agama-lib/src/files/store.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) [2025] 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 files settings. - -use super::{ - client::{FilesClient, FilesHTTPClientError}, - model::UserFile, -}; -use crate::http::BaseHTTPClient; - -#[derive(Debug, thiserror::Error)] -pub enum FilesStoreError { - #[error("Error processing files settings: {0}")] - FilesHTTPClient(#[from] FilesHTTPClientError), -} - -type FilesStoreResult = Result; - -/// Loads and stores the files settings from/to the HTTP service. -pub struct FilesStore { - files_client: FilesClient, -} - -impl FilesStore { - pub fn new(client: BaseHTTPClient) -> Self { - Self { - files_client: FilesClient::new(client), - } - } - - /// loads the list of user files from http API - pub async fn load(&self) -> FilesStoreResult>> { - let res = self.files_client.get_files().await?; - if res.is_empty() { - Ok(None) - } else { - Ok(Some(res)) - } - } - - /// stores the list of user files via http API - pub async fn store(&self, files: &Vec) -> FilesStoreResult<()> { - Ok(self.files_client.set_files(files).await?) - } -} diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index 676b40a490..e0f7a3add8 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -23,16 +23,10 @@ //! This module implements the mechanisms to load and store the installation settings. use crate::bootloader::model::BootloaderSettings; use crate::context::InstallationContext; -use crate::file_source::{FileSourceError, WithFileSource}; -use crate::files::model::UserFile; use crate::hostname::model::HostnameSettings; use crate::security::settings::SecuritySettings; use crate::storage::settings::zfcp::ZFCPConfig; -use crate::{ - network::NetworkSettings, scripts::ScriptsConfig, storage::settings::dasd::DASDConfig, - users::UserSettings, -}; -use fluent_uri::Uri; +use crate::{network::NetworkSettings, storage::settings::dasd::DASDConfig, users::UserSettings}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::default::Default; @@ -44,8 +38,6 @@ pub enum InstallSettingsError { InputOuputError(#[from] std::io::Error), #[error("Could not parse the settings: {0}")] ParseError(#[from] serde_json::Error), - #[error(transparent)] - FileSourceError(#[from] FileSourceError), } /// Installation settings @@ -60,8 +52,6 @@ pub struct InstallSettings { #[serde(skip_serializing_if = "Option::is_none")] pub dasd: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub files: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub hostname: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Object)] @@ -80,8 +70,6 @@ pub struct InstallSettings { #[serde(skip_serializing_if = "Option::is_none")] pub network: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub scripts: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub zfcp: Option, } @@ -102,28 +90,9 @@ impl InstallSettings { /// - `context`: Store context. pub fn from_json( json: &str, - context: &InstallationContext, + _context: &InstallationContext, ) -> Result { - let mut settings: InstallSettings = serde_json::from_str(json)?; - settings.resolve_urls(&context.source).unwrap(); + let settings: InstallSettings = serde_json::from_str(json)?; Ok(settings) } - - /// Resolves URLs in the settings. - /// - // Ideally, the context could be ready when deserializing the settings so - // the URLs can be resolved. One possible solution would be to use - // [DeserializeSeed](https://docs.rs/serde/1.0.219/serde/de/trait.DeserializeSeed.html). - fn resolve_urls(&mut self, source_uri: &Uri) -> Result<(), InstallSettingsError> { - if let Some(ref mut scripts) = self.scripts { - scripts.resolve_urls(source_uri)?; - } - - if let Some(ref mut files) = self.files { - for file in files.iter_mut() { - file.resolve_url(source_uri)?; - } - } - Ok(()) - } } diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index b859d77ba1..8684f2c6f7 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -47,8 +47,6 @@ pub mod auth; pub mod bootloader; pub mod context; pub mod error; -pub mod file_source; -pub mod files; pub mod hostname; pub mod http; pub mod install_settings; @@ -62,7 +60,6 @@ pub mod profile; pub mod progress; pub mod proxies; pub mod questions; -pub mod scripts; pub mod security; pub mod storage; mod store; diff --git a/rust/agama-lib/src/scripts.rs b/rust/agama-lib/src/scripts.rs deleted file mode 100644 index 153ee98c2e..0000000000 --- a/rust/agama-lib/src/scripts.rs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 user-defined scripts. - -mod client; -mod error; -mod model; -mod settings; -mod store; - -pub use client::{ScriptsClient, ScriptsClientError}; -pub use error::ScriptError; -pub use model::*; -pub use settings::*; -pub use store::{ScriptsStore, ScriptsStoreError}; diff --git a/rust/agama-lib/src/scripts/client.rs b/rust/agama-lib/src/scripts/client.rs deleted file mode 100644 index fa4afdd271..0000000000 --- a/rust/agama-lib/src/scripts/client.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 crate::http::{BaseHTTPClient, BaseHTTPClientError}; - -use super::{Script, ScriptsGroup}; - -#[derive(Debug, thiserror::Error)] -pub enum ScriptsClientError { - #[error(transparent)] - HTTP(#[from] BaseHTTPClientError), -} - -/// HTTP client to interact with scripts. -pub struct ScriptsClient { - client: BaseHTTPClient, -} - -impl ScriptsClient { - pub fn new(base: BaseHTTPClient) -> Self { - Self { client: base } - } - - /// Adds a script to the given group. - /// - /// * `script`: script's definition. - pub async fn add_script(&self, script: Script) -> Result<(), ScriptsClientError> { - Ok(self.client.post_void("/scripts", &script).await?) - } - - /// Runs user-defined scripts of the given group. - /// - /// * `group`: group of the scripts to run - pub async fn run_scripts(&self, group: ScriptsGroup) -> Result<(), ScriptsClientError> { - Ok(self.client.post_void("/scripts/run", &group).await?) - } - - /// Returns the user-defined scripts. - pub async fn scripts(&self) -> Result, ScriptsClientError> { - Ok(self.client.get("/scripts").await?) - } - - /// Remove all the user-defined scripts. - pub async fn delete_scripts(&self) -> Result<(), ScriptsClientError> { - Ok(self.client.delete_void("/scripts").await?) - } -} diff --git a/rust/agama-lib/src/scripts/store.rs b/rust/agama-lib/src/scripts/store.rs deleted file mode 100644 index 767ce7ca99..0000000000 --- a/rust/agama-lib/src/scripts/store.rs +++ /dev/null @@ -1,111 +0,0 @@ -// 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 crate::{file_source::FileSourceError, http::BaseHTTPClient}; - -use super::{ - client::{ScriptsClient, ScriptsClientError}, - settings::ScriptsConfig, - Script, ScriptError, -}; - -#[derive(Debug, thiserror::Error)] -pub enum ScriptsStoreError { - #[error("Error processing script settings: {0}")] - Script(#[from] ScriptsClientError), - #[error(transparent)] - FileSourceError(#[from] FileSourceError), -} - -type ScriptStoreResult = Result; - -pub struct ScriptsStore { - scripts: ScriptsClient, -} - -impl ScriptsStore { - pub fn new(client: BaseHTTPClient) -> Self { - Self { - scripts: ScriptsClient::new(client.clone()), - } - } - - pub async fn load(&self) -> ScriptStoreResult { - let scripts = self.scripts.scripts().await?; - - Ok(ScriptsConfig { - pre: Self::scripts_by_type(&scripts), - post_partitioning: Self::scripts_by_type(&scripts), - post: Self::scripts_by_type(&scripts), - init: Self::scripts_by_type(&scripts), - }) - } - - pub async fn store(&self, settings: &ScriptsConfig) -> ScriptStoreResult<()> { - self.scripts.delete_scripts().await?; - - if let Some(scripts) = &settings.pre { - for pre in scripts { - self.scripts.add_script(pre.clone().into()).await?; - } - } - - if let Some(scripts) = &settings.post_partitioning { - for post in scripts { - self.scripts.add_script(post.clone().into()).await?; - } - } - - if let Some(scripts) = &settings.post { - for post in scripts { - self.scripts.add_script(post.clone().into()).await?; - } - } - - let mut packages = vec![]; - if let Some(scripts) = &settings.init { - for init in scripts { - self.scripts.add_script(init.clone().into()).await?; - } - packages.push("agama-scripts"); - } - // TODO: use the new API. - // self.software - // .set_resolvables("agama-scripts", ResolvableType::Package, &packages, true) - // .await?; - - Ok(()) - } - - fn scripts_by_type(scripts: &[Script]) -> Option> - where - T: TryFrom + Clone, - { - let scripts: Vec = scripts - .iter() - .cloned() - .filter_map(|s| s.try_into().ok()) - .collect(); - if scripts.is_empty() { - return None; - } - Some(scripts) - } -} diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index f858ec633b..aea12366ec 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -23,13 +23,11 @@ use crate::{ bootloader::store::{BootloaderStore, BootloaderStoreError}, - files::store::{FilesStore, FilesStoreError}, hostname::store::{HostnameStore, HostnameStoreError}, http::BaseHTTPClient, install_settings::InstallSettings, manager::{http_client::ManagerHTTPClientError, InstallationPhase, ManagerHTTPClient}, network::{NetworkStore, NetworkStoreError}, - scripts::{ScriptsClient, ScriptsClientError, ScriptsGroup, ScriptsStore, ScriptsStoreError}, security::store::{SecurityStore, SecurityStoreError}, storage::{ http_client::{ @@ -52,8 +50,6 @@ pub enum StoreError { #[error(transparent)] DASD(#[from] DASDStoreError), #[error(transparent)] - Files(#[from] FilesStoreError), - #[error(transparent)] Hostname(#[from] HostnameStoreError), #[error(transparent)] Users(#[from] UsersStoreError), @@ -66,11 +62,6 @@ pub enum StoreError { #[error(transparent)] ISCSI(#[from] ISCSIHTTPClientError), #[error(transparent)] - Scripts(#[from] ScriptsStoreError), - // FIXME: it uses the client instead of the store. - #[error(transparent)] - ScriptsClient(#[from] ScriptsClientError), - #[error(transparent)] Manager(#[from] ManagerHTTPClientError), #[error(transparent)] ZFCP(#[from] ZFCPStoreError), @@ -87,13 +78,11 @@ pub enum StoreError { pub struct Store { bootloader: BootloaderStore, dasd: DASDStore, - files: FilesStore, hostname: HostnameStore, users: UsersStore, network: NetworkStore, security: SecurityStore, storage: StorageStore, - scripts: ScriptsStore, iscsi_client: ISCSIHTTPClient, manager_client: ManagerHTTPClient, http_client: BaseHTTPClient, @@ -105,13 +94,11 @@ impl Store { Ok(Self { bootloader: BootloaderStore::new(http_client.clone()), dasd: DASDStore::new(http_client.clone()), - files: FilesStore::new(http_client.clone()), hostname: HostnameStore::new(http_client.clone()), users: UsersStore::new(http_client.clone()), network: NetworkStore::new(http_client.clone()), security: SecurityStore::new(http_client.clone()), storage: StorageStore::new(http_client.clone()), - scripts: ScriptsStore::new(http_client.clone()), manager_client: ManagerHTTPClient::new(http_client.clone()), iscsi_client: ISCSIHTTPClient::new(http_client.clone()), zfcp: ZFCPStore::new(http_client.clone()), @@ -124,12 +111,10 @@ impl Store { let mut settings = InstallSettings { bootloader: self.bootloader.load().await?, dasd: self.dasd.load().await?, - files: self.files.load().await?, hostname: Some(self.hostname.load().await?), network: Some(self.network.load().await?), security: self.security.load().await?.to_option(), user: Some(self.users.load().await?), - scripts: self.scripts.load().await?.to_option(), zfcp: self.zfcp.load().await?, ..Default::default() }; @@ -145,20 +130,11 @@ impl Store { /// Stores the given installation settings in the Agama service /// - /// As part of the process it runs pre-scripts and forces a probe if the installation phase is - /// "config". It causes the storage proposal to be reset. This behavior should be revisited in + /// It causes the storage proposal to be reset. This behavior should be revisited in /// the future but it might be the storage service the responsible for dealing with this. /// /// * `settings`: installation settings. pub async fn store(&self, settings: &InstallSettings) -> Result<(), StoreError> { - if let Some(scripts) = &settings.scripts { - self.scripts.store(scripts).await?; - - if scripts.pre.as_ref().is_some_and(|s| !s.is_empty()) { - self.run_pre_scripts().await?; - } - } - if let Some(network) = &settings.network { self.network.store(network).await?; } @@ -202,9 +178,6 @@ impl Store { if let Some(hostname) = &settings.hostname { self.hostname.store(hostname).await?; } - if let Some(files) = &settings.files { - self.files.store(files).await?; - } Ok(()) } @@ -217,16 +190,4 @@ impl Store { } Ok(()) } - - /// Runs the pre-installation scripts and forces a probe if the installation phase is "config". - async fn run_pre_scripts(&self) -> Result<(), StoreError> { - let scripts_client = ScriptsClient::new(self.http_client.clone()); - scripts_client.run_scripts(ScriptsGroup::Pre).await?; - - let status = self.manager_client.status().await; - if status.is_ok_and(|s| s.phase == InstallationPhase::Config) { - self.manager_client.probe().await?; - } - Ok(()) - } } diff --git a/rust/agama-lib/src/utils.rs b/rust/agama-lib/src/utils.rs index 6a98caca59..8f545f4583 100644 --- a/rust/agama-lib/src/utils.rs +++ b/rust/agama-lib/src/utils.rs @@ -21,8 +21,6 @@ //! Utility module for Agama. mod file_format; -mod transfer; pub mod url; pub use file_format::*; -pub use transfer::*; diff --git a/rust/agama-manager/Cargo.toml b/rust/agama-manager/Cargo.toml index 9a021cd6e0..4eb3a9b3d2 100644 --- a/rust/agama-manager/Cargo.toml +++ b/rust/agama-manager/Cargo.toml @@ -5,11 +5,12 @@ rust-version.workspace = true edition.workspace = true [dependencies] -agama-utils = { path = "../agama-utils" } +agama-files = { path = "../agama-files" } agama-l10n = { path = "../agama-l10n" } agama-network = { path = "../agama-network" } agama-software = { path = "../agama-software" } agama-storage = { path = "../agama-storage" } +agama-utils = { path = "../agama-utils" } thiserror = "2.0.12" tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } async-trait = "0.1.83" diff --git a/rust/agama-manager/src/lib.rs b/rust/agama-manager/src/lib.rs index 346c0bf868..c6483e9109 100644 --- a/rust/agama-manager/src/lib.rs +++ b/rust/agama-manager/src/lib.rs @@ -23,6 +23,7 @@ pub use service::Service; pub mod message; +pub use agama_files as files; pub use agama_l10n as l10n; pub use agama_network as network; pub use agama_software as software; diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index fd763a86e4..bd272137ec 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -18,11 +18,12 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::{l10n, message, network, software, storage}; +use crate::{files, l10n, message, network, software, storage}; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{ self, event, + files::scripts::ScriptsGroup, manager::{self, LicenseContent}, status::State, Action, Config, Event, Issue, IssueMap, Proposal, Scope, Status, SystemInfo, @@ -55,6 +56,8 @@ pub enum Error { #[error(transparent)] Storage(#[from] storage::service::Error), #[error(transparent)] + Files(#[from] files::service::Error), + #[error(transparent)] Issues(#[from] issue::service::Error), #[error(transparent)] Questions(#[from] question::service::Error), @@ -80,6 +83,7 @@ pub struct Starter { network: Option, software: Option>, storage: Option>, + files: Option>, issues: Option>, progress: Option>, } @@ -98,6 +102,7 @@ impl Starter { network: None, software: None, storage: None, + files: None, issues: None, progress: None, } @@ -118,6 +123,11 @@ impl Starter { self } + pub fn with_files(mut self, files: Handler) -> Self { + self.files = Some(files); + self + } + pub fn with_l10n(mut self, l10n: Handler) -> Self { self.l10n = Some(l10n); self @@ -157,7 +167,7 @@ impl Starter { let software = match self.software { Some(software) => software, None => { - software::Service::builder( + software::Service::starter( self.events.clone(), issues.clone(), progress.clone(), @@ -182,6 +192,15 @@ impl Starter { } }; + let files = match self.files { + Some(files) => files, + None => { + files::Service::starter(self.events.clone(), progress.clone(), software.clone()) + .start() + .await? + } + }; + let network = match self.network { Some(network) => network, None => network::start().await?, @@ -196,6 +215,7 @@ impl Starter { network, software, storage, + files, products: products::Registry::default(), licenses: licenses::Registry::default(), // FIXME: state is already used for service state. @@ -215,6 +235,7 @@ pub struct Service { software: Handler, network: NetworkSystemClient, storage: Handler, + files: Handler, issues: Handler, progress: Handler, questions: Handler, @@ -267,6 +288,14 @@ impl Service { return Err(Error::MissingProduct); }; + self.files + .call(files::message::SetConfig::new(config.files.clone())) + .await?; + + self.files + .call(files::message::RunScripts::new(ScriptsGroup::Pre)) + .await?; + self.questions .call(question::message::SetConfig::new(config.questions.clone())) .await?; @@ -305,6 +334,16 @@ impl Service { return Err(Error::MissingProduct); }; + if let Some(files) = &config.files { + self.files + .call(files::message::SetConfig::with(files.clone())) + .await?; + + self.files + .call(files::message::RunScripts::new(ScriptsGroup::Pre)) + .await?; + } + if let Some(l10n) = &config.l10n { self.l10n .call(l10n::message::SetConfig::with(l10n.clone())) @@ -476,6 +515,7 @@ impl MessageHandler for Service { network: Some(network), software: Some(software), storage, + files: None, }) } } diff --git a/rust/agama-manager/src/test_utils.rs b/rust/agama-manager/src/test_utils.rs index 8080eb3818..7aa10771cf 100644 --- a/rust/agama-manager/src/test_utils.rs +++ b/rust/agama-manager/src/test_utils.rs @@ -22,6 +22,7 @@ use agama_l10n::test_utils::start_service as start_l10n_service; use agama_network::test_utils::start_service as start_network_service; +use agama_software::test_utils::start_service as start_software_service; use agama_storage::test_utils::start_service as start_storage_service; use agama_utils::{actor::Handler, api::event, issue, progress, question}; @@ -33,9 +34,12 @@ pub async fn start_service(events: event::Sender, dbus: zbus::Connection) -> Han let questions = question::start(events.clone()).await.unwrap(); let progress = progress::Service::starter(events.clone()).start(); - Service::starter(questions, events.clone(), dbus.clone()) + Service::starter(questions.clone(), events.clone(), dbus.clone()) .with_l10n(start_l10n_service(events.clone(), issues.clone()).await) - .with_storage(start_storage_service(events, issues, progress, dbus).await) + .with_storage( + start_storage_service(events.clone(), issues.clone(), progress.clone(), dbus).await, + ) + .with_software(start_software_service(events, issues, progress, questions).await) .with_network(start_network_service().await) .start() .await diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 1a11bc69b1..a7bb9db837 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -15,6 +15,7 @@ agama-locale-data = { path = "../agama-locale-data" } agama-manager = { path = "../agama-manager" } agama-network = { path = "../agama-network" } agama-software = { path = "../agama-software" } +agama-transfer = { path = "../agama-transfer" } zbus = { version = "5", default-features = false, features = ["tokio"] } uuid = { version = "1.10.0", features = ["v4"] } thiserror = "2.0.12" diff --git a/rust/agama-server/src/files/web.rs b/rust/agama-server/src/files/web.rs deleted file mode 100644 index 5b56efcb32..0000000000 --- a/rust/agama-server/src/files/web.rs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) [2025] 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 files deployment. -//! -//! The module offers one public function: -//! -//! * `files_service` which returns the Axum service. -//! -//! stream is not needed, as we do not need to emit signals (for NOW). - -use std::sync::Arc; - -use agama_lib::{ - error::ServiceError, - files::{error::FileError, model::UserFile, settings::FilesConfig}, -}; -use axum::{ - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, - routing::{post, put}, - Json, Router, -}; -use serde_json::json; -use tokio::sync::RwLock; - -use thiserror::Error; - -#[derive(Error, Debug)] -#[error("Files error: {0}")] -struct FilesServiceError(#[from] FileError); - -impl IntoResponse for FilesServiceError { - fn into_response(self) -> Response { - // TODO: is there better way to hook any error response to be logged with body? - tracing::warn!("Server return error {}", self); - let body = json!({ - "error": self.to_string() - }); - (StatusCode::BAD_REQUEST, Json(body)).into_response() - } -} - -#[derive(Clone, Default, Debug)] -struct FilesState { - files: Arc>, -} - -/// Sets up and returns the axum service for the files module. -pub async fn files_service() -> Result { - let state = FilesState::default(); - let router = Router::new() - .route("/", put(set_config).get(get_config)) - .route("/write", post(write_config)) - .with_state(state); - Ok(router) -} - -/// Returns the bootloader configuration. -/// -/// * `state` : service state. -#[utoipa::path( - get, - path = "/", - context_path = "/api/files", - responses( - (status = 200, description = "files configuration"), - (status = 400, description = "The D-Bus service could not perform the action") - ) -)] -async fn get_config( - State(state): State, -) -> Result>, FilesServiceError> { - // StorageSettings is just a wrapper over serde_json::value::RawValue - let settings = state.files.read().await; - Ok(Json(settings.files.to_vec())) -} - -/// Sets the files configuration. -/// -/// * `state`: service state. -/// * `config`: files configuration. -#[utoipa::path( - put, - path = "/", - context_path = "/api/files", - responses( - (status = 200, description = "Set the files configuration"), - (status = 400, description = "The D-Bus service could not perform the action") - ) -)] -async fn set_config( - State(state): State, - Json(settings): Json>, -) -> Result, FilesServiceError> { - let mut files = state.files.write().await; - files.files = settings; - Ok(Json(())) -} - -/// Writes the files. -/// -/// * `state`: service state. -#[utoipa::path( - put, - path = "/write", - context_path = "/api/files", - responses( - (status = 200, description = "Writes the files"), - (status = 400, description = "The D-Bus service could not perform the action") - ) -)] -async fn write_config(State(state): State) -> Result, FilesServiceError> { - let files = state.files.read().await; - for file in files.files.iter() { - file.write().await?; - } - Ok(Json(())) -} diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 43f220d53b..b6a2a2c18a 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -22,12 +22,10 @@ pub mod bootloader; pub mod cert; pub mod dbus; pub mod error; -pub mod files; pub mod hostname; pub mod logs; pub mod manager; pub mod profile; -pub mod scripts; pub mod security; pub mod storage; pub mod users; diff --git a/rust/agama-server/src/profile/web.rs b/rust/agama-server/src/profile/web.rs index 4535d8d57d..109233f31f 100644 --- a/rust/agama-server/src/profile/web.rs +++ b/rust/agama-server/src/profile/web.rs @@ -18,9 +18,9 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use agama_transfer::Transfer; use anyhow::Context; -use agama_lib::utils::Transfer; use agama_lib::{ error::ServiceError, profile::{AutoyastProfileImporter, ProfileEvaluator, ProfileValidator, ValidationOutcome}, diff --git a/rust/agama-server/src/scripts.rs b/rust/agama-server/src/scripts.rs deleted file mode 100644 index f0e99a215d..0000000000 --- a/rust/agama-server/src/scripts.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 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/scripts/web.rs b/rust/agama-server/src/scripts/web.rs deleted file mode 100644 index ad45b39c8e..0000000000 --- a/rust/agama-server/src/scripts/web.rs +++ /dev/null @@ -1,140 +0,0 @@ -// 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 std::sync::Arc; - -use agama_lib::{ - error::ServiceError, - scripts::{Script, ScriptError, ScriptsGroup, ScriptsRepository}, -}; -use axum::{ - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, - routing::{get, post}, - Json, Router, -}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use thiserror::Error; -use tokio::sync::RwLock; - -#[derive(Clone, Default)] -struct ScriptsState { - scripts: Arc>, -} - -#[derive(Error, Debug)] -#[error("Script error: {0}")] -struct ScriptServiceError(#[from] ScriptError); - -impl IntoResponse for ScriptServiceError { - fn into_response(self) -> Response { - tracing::warn!("Server return error {}", self); - let body = json!({ - "error": self.to_string() - }); - (StatusCode::BAD_REQUEST, Json(body)).into_response() - } -} - -/// Sets up and returns the axum service for the auto-installation scripts. -pub async fn scripts_service() -> Result { - let state = ScriptsState::default(); - let router = Router::new() - .route( - "/", - get(list_scripts).post(add_script).delete(remove_scripts), - ) - .route("/run", post(run_scripts)) - .with_state(state); - Ok(router) -} - -#[utoipa::path( - post, - path = "/", - context_path = "/api/scripts", - request_body(content = [Script], description = "Script definition"), - responses( - (status = 200, description = "The script was added.") - ) -)] -async fn add_script( - state: State, - Json(script): Json