diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 972575c8c5..7361b48427 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -28,6 +28,14 @@ "items": { "$ref": "#/$defs/script" } + }, + "init": { + "title": "Init scripts", + "description": "User-defined scripts to run booting the installed system", + "type": "array", + "items": { + "$ref": "#/$defs/script" + } } } }, @@ -298,30 +306,13 @@ "items": { "title": "List of EAP methods used", "type": "string", - "enum": [ - "leap", - "md5", - "tls", - "peap", - "ttls", - "pwd", - "fast" - ] + "enum": ["leap", "md5", "tls", "peap", "ttls", "pwd", "fast"] } }, "phase2Auth": { "title": "Phase 2 inner auth method", "type": "string", - "enum": [ - "pap", - "chap", - "mschap", - "mschapv2", - "gtc", - "otp", - "md5", - "tls" - ] + "enum": ["pap", "chap", "mschap", "mschapv2", "gtc", "otp", "md5", "tls"] }, "identity": { "title": "Identity string, often for example the user's login name", @@ -977,10 +968,7 @@ "examples": [1024, 2048] }, "sizeValue": { - "anyOf": [ - { "$ref": "#/$defs/sizeString" }, - { "$ref": "#/$defs/sizeInteger" } - ] + "anyOf": [{ "$ref": "#/$defs/sizeString" }, { "$ref": "#/$defs/sizeInteger" }] }, "sizeValueWithCurrent": { "anyOf": [ @@ -1007,12 +995,7 @@ }, "minItems": 1, "maxItems": 2, - "examples": [ - [1024, "current"], - ["1 GiB", "5 GiB"], - [1024, "2 GiB"], - ["2 GiB"] - ] + "examples": [[1024, "current"], ["1 GiB", "5 GiB"], [1024, "2 GiB"], ["2 GiB"]] }, { "title": "Size range", @@ -1370,15 +1353,7 @@ }, "id": { "title": "Partition ID", - "enum": [ - "linux", - "swap", - "lvm", - "raid", - "esp", - "prep", - "bios_boot" - ] + "enum": ["linux", "swap", "lvm", "raid", "esp", "prep", "bios_boot"] }, "size": { "title": "Partition size", diff --git a/rust/agama-lib/src/scripts/model.rs b/rust/agama-lib/src/scripts/model.rs index f32769b78a..4c6df019f1 100644 --- a/rust/agama-lib/src/scripts/model.rs +++ b/rust/agama-lib/src/scripts/model.rs @@ -40,6 +40,7 @@ use super::ScriptError; pub enum ScriptsGroup { Pre, Post, + Init, } #[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] @@ -68,10 +69,7 @@ impl Script { /// * `workdir`: where to write assets (script, logs and exit code). pub async fn run(&self, workdir: &Path) -> Result<(), ScriptError> { let dir = workdir.join(self.group.to_string()); - let path = dir.join(&self.name); - self.write(&path).await?; - let output = process::Command::new(&path).output()?; let stdout_log = dir.join(format!("{}.log", &self.name)); @@ -128,13 +126,22 @@ impl ScriptsRepository { /// Adds a new script to the repository. /// /// * `script`: script to add. - pub fn add(&mut self, script: Script) { + pub async fn add(&mut self, script: Script) -> Result<(), ScriptError> { + let workdir = self.workdir.join(script.group.to_string()); + std::fs::create_dir_all(&workdir)?; + let path = workdir.join(&script.name); + script.write(&path).await?; self.scripts.push(script); + Ok(()) } /// Removes all the scripts from the repository. - pub fn clear(&mut self) { + pub fn clear(&mut self) -> Result<(), ScriptError> { self.scripts.clear(); + if self.workdir.exists() { + std::fs::remove_dir_all(&self.workdir)?; + } + Ok(()) } /// Runs the scripts in the given group. @@ -142,8 +149,6 @@ impl ScriptsRepository { /// They run in the order they were added to the repository. If does not return an error /// if running a script fails, although it logs the problem. pub async fn run(&self, group: ScriptsGroup) -> Result<(), ScriptError> { - let workdir = self.workdir.join(group.to_string()); - std::fs::create_dir_all(&workdir)?; let scripts: Vec<_> = self.scripts.iter().filter(|s| s.group == group).collect(); for script in scripts { if let Err(error) = script.run(&self.workdir).await { @@ -188,7 +193,7 @@ mod test { }, group: ScriptsGroup::Pre, }; - repo.add(script); + repo.add(script).await.unwrap(); let script = repo.scripts.first().unwrap(); assert_eq!("test".to_string(), script.name); @@ -205,7 +210,7 @@ mod test { source: ScriptSource::Text { body }, group: ScriptsGroup::Pre, }; - repo.add(script); + repo.add(script).await.unwrap(); repo.run(ScriptsGroup::Pre).await.unwrap(); repo.scripts.first().unwrap(); @@ -220,4 +225,23 @@ mod test { let body = String::from_utf8(body).unwrap(); assert_eq!("error\n", body); } + + #[test] + async fn test_clear_scripts() { + let tmp_dir = TempDir::with_prefix("scripts-").expect("a temporary directory"); + let mut repo = ScriptsRepository::new(&tmp_dir); + let body = "#!/bin/bash\necho hello\necho error >&2".to_string(); + + let script = Script { + name: "test".to_string(), + source: ScriptSource::Text { body }, + group: ScriptsGroup::Pre, + }; + repo.add(script).await.unwrap(); + + let script_path = tmp_dir.path().join("pre").join("test"); + assert!(script_path.exists()); + _ = repo.clear(); + assert!(!script_path.exists()); + } } diff --git a/rust/agama-lib/src/scripts/settings.rs b/rust/agama-lib/src/scripts/settings.rs index ddc5f69d8e..d4d52ea1ba 100644 --- a/rust/agama-lib/src/scripts/settings.rs +++ b/rust/agama-lib/src/scripts/settings.rs @@ -26,11 +26,14 @@ use super::{Script, ScriptSource}; #[serde(rename_all = "camelCase")] pub struct ScriptsConfig { /// User-defined pre-installation scripts - #[serde(skip_serializing_if = "Vec::is_empty")] - pub pre: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub pre: Option>, /// User-defined post-installation scripts - #[serde(skip_serializing_if = "Vec::is_empty")] - pub post: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub post: Option>, + /// User-defined init scripts + #[serde(skip_serializing_if = "Option::is_none")] + pub init: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/rust/agama-lib/src/scripts/store.rs b/rust/agama-lib/src/scripts/store.rs index 82f165cbe5..ae503fcc1c 100644 --- a/rust/agama-lib/src/scripts/store.rs +++ b/rust/agama-lib/src/scripts/store.rs @@ -18,47 +18,68 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; +use crate::{ + base_http_client::BaseHTTPClient, + error::ServiceError, + software::{model::ResolvableType, SoftwareHTTPClient}, +}; use super::{client::ScriptsClient, settings::ScriptsConfig, Script, ScriptConfig, ScriptsGroup}; pub struct ScriptsStore { - client: ScriptsClient, + scripts: ScriptsClient, + software: SoftwareHTTPClient, } impl ScriptsStore { pub fn new(client: BaseHTTPClient) -> Self { Self { - client: ScriptsClient::new(client), + scripts: ScriptsClient::new(client.clone()), + software: SoftwareHTTPClient::new(client), } } pub async fn load(&self) -> Result { - let scripts = self.client.scripts().await?; + let scripts = self.scripts.scripts().await?; Ok(ScriptsConfig { pre: Self::to_script_configs(&scripts, ScriptsGroup::Pre), post: Self::to_script_configs(&scripts, ScriptsGroup::Post), + init: Self::to_script_configs(&scripts, ScriptsGroup::Init), }) } pub async fn store(&self, settings: &ScriptsConfig) -> Result<(), ServiceError> { - self.client.delete_scripts().await?; + self.scripts.delete_scripts().await?; - for pre in &settings.pre { - self.client - .add_script(&Self::to_script(pre, ScriptsGroup::Pre)) - .await?; + if let Some(scripts) = &settings.pre { + for pre in scripts { + self.scripts + .add_script(&Self::to_script(pre, ScriptsGroup::Pre)) + .await?; + } } - for post in &settings.post { - self.client - .add_script(&Self::to_script(post, ScriptsGroup::Post)) - .await?; + if let Some(scripts) = &settings.post { + for post in scripts { + self.scripts + .add_script(&Self::to_script(post, ScriptsGroup::Post)) + .await?; + } } - // TODO: find a better play to run the scripts (before probing). - self.client.run_scripts(ScriptsGroup::Pre).await?; + let mut packages = vec![]; + if let Some(scripts) = &settings.init { + for init in scripts { + self.scripts + .add_script(&Self::to_script(init, ScriptsGroup::Init)) + .await?; + } + packages.push("agama-scripts"); + } + self.software + .set_resolvables("agama-scripts", ResolvableType::Package, &packages, true) + .await?; Ok(()) } @@ -71,11 +92,17 @@ impl ScriptsStore { } } - fn to_script_configs(scripts: &[Script], group: ScriptsGroup) -> Vec { - scripts + fn to_script_configs(scripts: &[Script], group: ScriptsGroup) -> Option> { + let configs: Vec<_> = scripts .iter() .filter(|s| s.group == group) .map(|s| s.into()) - .collect() + .collect(); + + if configs.is_empty() { + return None; + } + + Some(configs) } } diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index fde5d31c20..5a901d29fb 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -18,7 +18,10 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use super::proxies::Software1Proxy; +use super::{ + model::ResolvableType, + proxies::{ProposalProxy, Software1Proxy}, +}; use crate::error::ServiceError; use serde::Serialize; use serde_repr::Serialize_repr; @@ -74,12 +77,14 @@ impl TryFrom for SelectedBy { #[derive(Clone)] pub struct SoftwareClient<'a> { software_proxy: Software1Proxy<'a>, + proposal_proxy: ProposalProxy<'a>, } impl<'a> SoftwareClient<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { software_proxy: Software1Proxy::new(&connection).await?, + proposal_proxy: ProposalProxy::new(&connection).await?, }) } @@ -172,4 +177,24 @@ impl<'a> SoftwareClient<'a> { pub async fn probe(&self) -> Result<(), ServiceError> { Ok(self.software_proxy.probe().await?) } + + /// Updates the resolvables list. + /// + /// * `id`: resolvable list ID. + /// * `r#type`: type of the resolvables. + /// * `resolvables`: resolvables to add. + /// * `optional`: whether the resolvables are optional. + pub async fn set_resolvables( + &self, + id: &str, + r#type: ResolvableType, + resolvables: &[&str], + optional: bool, + ) -> Result<(), ServiceError> { + let names: Vec<_> = resolvables.iter().map(|r| r.as_ref()).collect(); + self.proposal_proxy + .set_resolvables(id, r#type as u8, &names, optional) + .await?; + Ok(()) + } } diff --git a/rust/agama-lib/src/software/http_client.rs b/rust/agama-lib/src/software/http_client.rs index 4a770e6afb..21877e9b84 100644 --- a/rust/agama-lib/src/software/http_client.rs +++ b/rust/agama-lib/src/software/http_client.rs @@ -22,6 +22,8 @@ use crate::software::model::SoftwareConfig; use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; use std::collections::HashMap; +use super::model::{ResolvableParams, ResolvableType}; + pub struct SoftwareHTTPClient { client: BaseHTTPClient, } @@ -74,4 +76,22 @@ impl SoftwareHTTPClient { }; self.set_config(&config).await } + + /// Sets a resolvable list + pub async fn set_resolvables( + &self, + name: &str, + r#type: ResolvableType, + names: &[&str], + optional: bool, + ) -> Result<(), ServiceError> { + let path = format!("/software/resolvables/{}", name); + let options = ResolvableParams { + names: names.iter().map(|n| n.to_string()).collect(), + r#type, + optional, + }; + self.client.put_void(&path, &options).await?; + Ok(()) + } } diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 1a5ba5a65b..31f81fbcaf 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -80,3 +80,23 @@ impl TryFrom for RegistrationRequirement { } } } + +/// Software resolvable type (package or pattern). +#[derive(Deserialize, Serialize, strum::Display, utoipa::ToSchema)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] +pub enum ResolvableType { + Package = 0, + Pattern = 1, +} + +/// Resolvable list specification. +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct ResolvableParams { + /// List of resolvables. + pub names: Vec, + /// Resolvable type. + pub r#type: ResolvableType, + /// Whether the resolvables are optional or not. + pub optional: bool, +} diff --git a/rust/agama-lib/src/software/proxies/proposal.rs b/rust/agama-lib/src/software/proxies/proposal.rs index f699b6f6a4..bc88a686c0 100644 --- a/rust/agama-lib/src/software/proxies/proposal.rs +++ b/rust/agama-lib/src/software/proxies/proposal.rs @@ -42,6 +42,7 @@ use zbus::proxy; #[proxy( default_service = "org.opensuse.Agama.Software1", + default_path = "/org/opensuse/Agama/Software1/Proposal", interface = "org.opensuse.Agama.Software1.Proposal", assume_defaults = true )] diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index ef2c24dbea..14efddfea5 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -94,10 +94,10 @@ impl Store { pub async fn store(&self, settings: &InstallSettings) -> Result<(), ServiceError> { if let Some(scripts) = &settings.scripts { self.scripts.store(scripts).await?; - } - if settings.scripts.as_ref().is_some_and(|s| !s.pre.is_empty()) { - self.run_pre_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 { diff --git a/rust/agama-server/src/scripts/web.rs b/rust/agama-server/src/scripts/web.rs index 9fa8f70462..6957c20094 100644 --- a/rust/agama-server/src/scripts/web.rs +++ b/rust/agama-server/src/scripts/web.rs @@ -81,7 +81,7 @@ async fn add_script( Json(script): Json