Skip to content

Commit

Permalink
feat(CLI): Agama profile import command (#1270)
Browse files Browse the repository at this point in the history
## Problem

There are many steps in autoinstallation processing that is splitted
over multiple command line arguments. It is problematic if user use
shell script based autoinstallation and need to all of those steps.

## Solution

Introduce new "agama profile import" command that do all
autoinstallation processing. Only install part is not done. This allows
user to use hooks or tuning before real installation begins.
  • Loading branch information
jreidinger authored May 29, 2024
2 parents a45601d + b66daa6 commit 903101b
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 78 deletions.
18 changes: 1 addition & 17 deletions autoinstallation/bin/agama-auto
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,8 @@ fi

echo "Using the profile at $url"

tmpdir=$(mktemp --directory --suffix "-agama")
echo "working on $tmpdir"

case "$url" in
*.jsonnet )
/usr/bin/agama profile download "$url" > "${tmpdir}/profile.jsonnet"
/usr/bin/agama profile evaluate "${tmpdir}/profile.jsonnet" > "${tmpdir}/profile.json"
/usr/bin/agama profile validate "${tmpdir}/profile.json" || echo "Validation failed"
/usr/bin/agama config load "${tmpdir}/profile.json"
/usr/bin/agama install;;
*.sh )
/usr/bin/agama profile download "$url" > "${tmpdir}/profile.sh"
exec $SHELL "/${tmpdir}/profile.sh";;
* )
/usr/bin/agama profile download "$url" > "${tmpdir}/profile.json"
/usr/bin/agama profile validate "${tmpdir}/profile.json" || echo "Validation failed"
/usr/bin/agama config load "${tmpdir}/profile.json"
/usr/bin/agama profile import "$url"
/usr/bin/agama install;;
esac

rm -r "$tmpdir"
2 changes: 1 addition & 1 deletion rust/agama-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async fn run_command(cli: Cli) -> anyhow::Result<()> {
wait_for_services(&manager).await?;
probe().await
}
Commands::Profile(subcommand) => Ok(run_profile_cmd(subcommand)?),
Commands::Profile(subcommand) => Ok(run_profile_cmd(subcommand).await?),
Commands::Install => {
let manager = build_manager().await?;
install(&manager, 3).await
Expand Down
80 changes: 74 additions & 6 deletions rust/agama-cli/src/profile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use agama_lib::profile::{ProfileEvaluator, ProfileReader, ProfileValidator, ValidationResult};
use anyhow::Context;
use clap::Subcommand;
use std::path::Path;
use std::os::unix::process::CommandExt;
use std::{
fs::File,
io::{stdout, Write},
path::{Path, PathBuf},
process::Command,
};
use tempfile::TempDir;

#[derive(Subcommand, Debug)]
pub enum ProfileCommands {
Expand All @@ -13,12 +20,21 @@ pub enum ProfileCommands {

/// Evaluate a profile, injecting the hardware information from D-Bus
Evaluate { path: String },

/// Process autoinstallation profile and loads it into agama
///
/// This is top level command that do all autoinstallation processing beside starting
/// installation. Unless there is a need to inject additional commands between processing
/// use this command instead of set of underlying commands.
/// Optional dir argument is location where profile is processed. Useful for debugging
/// if something goes wrong.
Import { url: String, dir: Option<PathBuf> },
}

fn download(url: &str) -> anyhow::Result<()> {
fn download(url: &str, mut out_fd: impl Write) -> anyhow::Result<()> {
let reader = ProfileReader::new(url)?;
let contents = reader.read()?;
print!("{}", contents);
out_fd.write_all(contents.as_bytes())?;
Ok(())
}

Expand All @@ -45,15 +61,67 @@ fn validate(path: String) -> anyhow::Result<()> {
fn evaluate(path: String) -> anyhow::Result<()> {
let evaluator = ProfileEvaluator {};
evaluator
.evaluate(Path::new(&path))
.evaluate(Path::new(&path), stdout())
.context("Could not evaluate the profile".to_string())?;
Ok(())
}

pub fn run(subcommand: ProfileCommands) -> anyhow::Result<()> {
async fn import(url: String, dir: Option<PathBuf>) -> anyhow::Result<()> {
let tmpdir = TempDir::new()?; // TODO: create it only if dir is not passed
let output_file = if url.ends_with(".sh") {
"profile.sh"
} else if url.ends_with(".jsonnet") {
"profile.jsonnet"
} else {
"profile.json"
};
let output_dir = dir.unwrap_or_else(|| tmpdir.into_path());
let mut output_path = output_dir.join(output_file);
let output_fd = File::create(output_path.clone())?;
//download profile
download(&url, output_fd)?;
// exec shell scripts
if output_file.ends_with(".sh") {
let err = Command::new("bash")
.args([output_path.to_str().context("Wrong path to shell script")?])
.exec();
eprintln!("Exec failed: {}", err);
}

// evaluate jsonnet profiles
if output_file.ends_with(".jsonnet") {
let fd = File::create(output_dir.join("profile.json"))?;
let evaluator = ProfileEvaluator {};
evaluator
.evaluate(&output_path, fd)
.context("Could not evaluate the profile".to_string())?;
output_path = output_dir.join("profile.json");
}

let output_path_string = output_path
.to_str()
.context("Failed to get output path")?
.to_string();
// Validate json profile
// TODO: optional skip of validation
validate(output_path_string.clone())?;
// load resulting json config
crate::config::run(
crate::config::ConfigCommands::Load {
path: output_path_string,
},
crate::printers::Format::Json,
)
.await?;

Ok(())
}

pub async fn run(subcommand: ProfileCommands) -> anyhow::Result<()> {
match subcommand {
ProfileCommands::Download { url } => download(&url),
ProfileCommands::Download { url } => download(&url, std::io::stdout()),
ProfileCommands::Validate { path } => validate(path),
ProfileCommands::Evaluate { path } => evaluate(path),
ProfileCommands::Import { url, dir } => import(url, dir).await,
}
}
18 changes: 10 additions & 8 deletions rust/agama-lib/share/examples/profile.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
// For the schema, see
// https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/profile.schema.json

// The "hw.libsonnet" file contains hardware information of the storage devices
// from the "lshw" tool. Agama generates this file at runtime by running (with
// root privileges):
// The "hw.libsonnet" file contains hardware information from the "lshw" tool.
// Agama generates this file at runtime by running (with root privileges):
//
// lshw -json -class disk
// lshw -json
//
// However, it is expected to change in the near future to include information
// from other subsystems (e.g., network).
// There are included also helpers to search this hardware tree. To see helpers check
// "/usr/share/agama-cli/agama.libsonnet"
local agama = import 'hw.libsonnet';

// Find the biggest disk which is suitable for installing the system.
Expand All @@ -19,9 +18,12 @@ local findBiggestDisk(disks) =
local sorted = std.sort(sizedDisks, function(x) -x.size);
sorted[0].logicalname;

// Find how much physical memory system has.
local memory = agama.findByID(agama.lshw, 'memory').size;

{
product: {
id: 'Tumbleweed'
id: if memory < 8000000000 then 'MicroOS' else 'Tumbleweed',
},
software: {
patterns: [
Expand All @@ -43,7 +45,7 @@ local findBiggestDisk(disks) =
keyboard: 'us',
},
storage: {
bootDevice: findBiggestDisk(agama.disks),
bootDevice: findBiggestDisk(agama.selectByClass(agama.lshw, 'disk')),
},
network: {
connections: [
Expand Down
43 changes: 0 additions & 43 deletions rust/agama-lib/share/examples/profile_Dolomite.json

This file was deleted.

6 changes: 3 additions & 3 deletions rust/agama-lib/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use curl::easy::Easy;
use jsonschema::JSONSchema;
use log::info;
use serde_json;
use std::{fs, io, io::Write, path::Path, process::Command};
use std::{fs, io::Write, path::Path, process::Command};
use tempfile::{tempdir, TempDir};
use url::Url;

Expand Down Expand Up @@ -132,7 +132,7 @@ impl ProfileValidator {
pub struct ProfileEvaluator {}

impl ProfileEvaluator {
pub fn evaluate(&self, profile_path: &Path) -> anyhow::Result<()> {
pub fn evaluate(&self, profile_path: &Path, mut out_fd: impl Write) -> anyhow::Result<()> {
let dir = tempdir()?;

let working_path = dir.path().join("profile.jsonnet");
Expand All @@ -152,7 +152,7 @@ impl ProfileEvaluator {
String::from_utf8(result.stderr).context("Invalid UTF-8 sequence from jsonnet")?;
return Err(ProfileError::EvaluationError(message).into());
}
io::stdout().write_all(&result.stdout)?;
out_fd.write_all(&result.stdout)?;
Ok(())
}

Expand Down
7 changes: 7 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
-------------------------------------------------------------------
Wed May 29 12:15:37 UTC 2024 - Josef Reidinger <[email protected]>

- CLI: Add new command "agama profile import" that does the whole
autoinstallation processing and loads the configuration
(gh#openSUSE/agama#1270).

-------------------------------------------------------------------
Wed May 29 11:16:11 UTC 2024 - Imobach Gonzalez Sosa <[email protected]>

Expand Down

0 comments on commit 903101b

Please sign in to comment.