Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rust): add support for auto-installation "init" scripts #1788

Merged
merged 12 commits into from
Nov 28, 2024
Merged
51 changes: 13 additions & 38 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -977,10 +968,7 @@
"examples": [1024, 2048]
},
"sizeValue": {
"anyOf": [
{ "$ref": "#/$defs/sizeString" },
{ "$ref": "#/$defs/sizeInteger" }
]
"anyOf": [{ "$ref": "#/$defs/sizeString" }, { "$ref": "#/$defs/sizeInteger" }]
},
"sizeValueWithCurrent": {
"anyOf": [
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 33 additions & 9 deletions rust/agama-lib/src/scripts/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use super::ScriptError;
pub enum ScriptsGroup {
Pre,
Post,
Init,
}

#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -128,22 +126,29 @@ 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.
///
/// 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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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());
}
}
11 changes: 7 additions & 4 deletions rust/agama-lib/src/scripts/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScriptConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
imobachgs marked this conversation as resolved.
Show resolved Hide resolved
pub pre: Option<Vec<ScriptConfig>>,
/// User-defined post-installation scripts
#[serde(skip_serializing_if = "Vec::is_empty")]
pub post: Vec<ScriptConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post: Option<Vec<ScriptConfig>>,
/// User-defined init scripts
#[serde(skip_serializing_if = "Option::is_none")]
pub init: Option<Vec<ScriptConfig>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
59 changes: 42 additions & 17 deletions rust/agama-lib/src/scripts/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,72 @@
// 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<ScriptsConfig, ServiceError> {
let scripts = self.client.scripts().await?;
let scripts = self.scripts.scripts().await?;

let pre = Self::to_script_configs(&scripts, ScriptsGroup::Pre);
let post = Self::to_script_configs(&scripts, ScriptsGroup::Post);
let init = Self::to_script_configs(&scripts, ScriptsGroup::Init);

Ok(ScriptsConfig {
pre: Self::to_script_configs(&scripts, ScriptsGroup::Pre),
post: Self::to_script_configs(&scripts, ScriptsGroup::Post),
pre: if pre.is_empty() { None } else { Some(pre) },
post: if post.is_empty() { None } else { Some(post) },
init: if init.is_empty() { None } else { Some(init) },
imobachgs marked this conversation as resolved.
Show resolved Hide resolved
})
}

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(())
}
Expand Down
27 changes: 26 additions & 1 deletion rust/agama-lib/src/software/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,12 +77,14 @@ impl TryFrom<u8> 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<SoftwareClient<'a>, ServiceError> {
Ok(Self {
software_proxy: Software1Proxy::new(&connection).await?,
proposal_proxy: ProposalProxy::new(&connection).await?,
})
}

Expand Down Expand Up @@ -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(())
}
}
20 changes: 20 additions & 0 deletions rust/agama-lib/src/software/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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(())
}
}
Loading