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

fix(rust): better profile URL handling #1831

Merged
merged 11 commits into from
Dec 16, 2024
1 change: 1 addition & 0 deletions .github/workflows/ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
run: zypper --non-interactive install
clang-devel
dbus-1-daemon
golang-github-google-jsonnet
jq
libopenssl-3-devel
openssl-3
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mod questions;
use crate::error::CliError;
use agama_lib::base_http_client::BaseHTTPClient;
use agama_lib::{
error::ServiceError, manager::ManagerClient, progress::ProgressMonitor, transfer::Transfer,
error::ServiceError, manager::ManagerClient, progress::ProgressMonitor, utils::Transfer,
};
use auth::run as run_auth_cmd;
use commands::Commands;
Expand Down
89 changes: 52 additions & 37 deletions rust/agama-cli/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ use crate::show_progress;
use agama_lib::{
base_http_client::BaseHTTPClient,
install_settings::InstallSettings,
profile::{AutoyastProfile, ProfileEvaluator, ProfileValidator, ValidationResult},
transfer::Transfer,
profile::{AutoyastProfileImporter, ProfileEvaluator, ProfileValidator, ValidationResult},
utils::FileFormat,
utils::Transfer,
Store as SettingsStore,
};
use anyhow::Context;
use clap::Subcommand;
use console::style;
use std::os::unix::process::CommandExt;
use std::os::unix::{fs::PermissionsExt, process::CommandExt};
use std::{
fs::File,
io::stdout,
Expand Down Expand Up @@ -114,48 +115,62 @@ async fn import(url_string: String, dir: Option<PathBuf>) -> anyhow::Result<()>
tokio::spawn(async move {
show_progress().await.unwrap();
});

let url = Url::parse(&url_string)?;
let tmpdir = TempDir::new()?; // TODO: create it only if dir is not passed
let work_dir = dir.unwrap_or_else(|| tmpdir.into_path());
let profile_path = work_dir.join("profile.json");

// Specific AutoYaST handling
let path = url.path();
let output_file = if path.ends_with(".sh") {
"profile.sh"
} else if path.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())?;
if path.ends_with(".xml") || path.ends_with(".erb") || path.ends_with('/') {
// autoyast specific download and convert to json
AutoyastProfile::new(&url)?.read_into(output_fd)?;
// AutoYaST specific download and convert to JSON
AutoyastProfileImporter::read(&url)?.write_file(&profile_path)?;
} else {
// just download profile
Transfer::get(&url_string, output_fd)?;
pre_process_profile(&url_string, &profile_path)?;
}

// 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);
}
validate(&profile_path)?;
store_settings(&profile_path).await?;

// 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");
}
Ok(())
}

validate(&output_path)?;
store_settings(&output_path).await?;
// Preprocess the profile.
//
// The profile can be a JSON or a Jsonnet file or a script.
//
// * If it is a JSON file, no preprocessing is needed.
// * If it is a Jsonnet file, it is converted to JSON.
// * If it is a script, it is executed.
fn pre_process_profile<P: AsRef<Path>>(url_string: &str, path: P) -> anyhow::Result<()> {
let work_dir = path.as_ref().parent().unwrap();
let tmp_profile_path = work_dir.join("profile.temp");
let tmp_file = File::create(&tmp_profile_path)?;
Transfer::get(url_string, tmp_file)?;

match FileFormat::from_file(&tmp_profile_path)? {
FileFormat::Jsonnet => {
let file = File::create(path)?;
let evaluator = ProfileEvaluator {};
evaluator
.evaluate(&tmp_profile_path, file)
.context("Could not evaluate the profile".to_string())?;
}
FileFormat::Script => {
let mut perms = std::fs::metadata(&tmp_profile_path)?.permissions();
perms.set_mode(0o750);
std::fs::set_permissions(&tmp_profile_path, perms)?;
let err = Command::new(&tmp_profile_path).exec();
eprintln!("Exec failed: {}", err);
}
FileFormat::Json => {
std::fs::rename(&tmp_profile_path, path.as_ref())?;
}
_ => {
return Err(anyhow::Error::msg("Unsupported file format"));
}
}
Ok(())
}

Expand All @@ -168,8 +183,8 @@ async fn store_settings<P: AsRef<Path>>(path: P) -> anyhow::Result<()> {

fn autoyast(url_string: String) -> anyhow::Result<()> {
let url = Url::parse(&url_string)?;
let reader = AutoyastProfile::new(&url)?;
reader.read_into(std::io::stdout())?;
let importer = AutoyastProfileImporter::read(&url)?;
importer.write(std::io::stdout())?;
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::io;
use thiserror::Error;
use zbus::{self, zvariant};

use crate::transfer::TransferError;
use crate::utils::TransferError;

#[derive(Error, Debug)]
pub enum ServiceError {
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub use store::Store;
pub mod openapi;
pub mod questions;
pub mod scripts;
pub mod transfer;
pub mod utils;

use crate::error::ServiceError;
use reqwest::{header, Client};
Expand Down
50 changes: 28 additions & 22 deletions rust/agama-lib/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,49 @@ use anyhow::Context;
use jsonschema::JSONSchema;
use log::info;
use serde_json;
use std::{fs, io::Write, path::Path, process::Command};
use std::{
fs::{self, File},
io::Write,
path::Path,
process::Command,
};
use tempfile::{tempdir, TempDir};
use url::Url;

/// Downloads and converts autoyast profile.
pub struct AutoyastProfile {
url: Url,
pub struct AutoyastProfileImporter {
content: String,
}

impl AutoyastProfile {
pub fn new(url: &Url) -> anyhow::Result<Self> {
Ok(Self { url: url.clone() })
}

pub fn read_into(&self, mut out_fd: impl Write) -> anyhow::Result<()> {
let path = self.url.path();
if path.ends_with(".xml") || path.ends_with(".erb") || path.ends_with('/') {
let content = self.read_from_autoyast()?;
out_fd.write_all(content.as_bytes())?;
Ok(())
} else {
let msg = format!("Unsupported AutoYaST format at {}", self.url);
Err(anyhow::Error::msg(msg))
impl AutoyastProfileImporter {
pub fn read(url: &Url) -> anyhow::Result<Self> {
let path = url.path();
if !path.ends_with(".xml") && !path.ends_with(".erb") && !path.ends_with('/') {
let msg = format!("Unsupported AutoYaST format at {}", url);
return Err(anyhow::Error::msg(msg));
}
}

fn read_from_autoyast(&self) -> anyhow::Result<String> {
const TMP_DIR_PREFIX: &str = "autoyast";
const AUTOINST_JSON: &str = "autoinst.json";

let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?;
Command::new("agama-autoyast")
.args([self.url.as_str(), &tmp_dir.path().to_string_lossy()])
.args([url.as_str(), &tmp_dir.path().to_string_lossy()])
.status()?;

let autoinst_json = tmp_dir.path().join(AUTOINST_JSON);
Ok(fs::read_to_string(autoinst_json)?)
let content = fs::read_to_string(autoinst_json)?;
Ok(Self { content })
}

pub fn write(&self, mut file: impl Write) -> anyhow::Result<()> {
file.write_all(self.content.as_bytes())?;
Ok(())
}

pub fn write_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
let mut file = File::create(path)?;
self.write(&mut file)
}
}

Expand Down Expand Up @@ -170,7 +176,7 @@ impl ProfileEvaluator {
.args(["-json"])
.output()
.context("Failed to run lshw")?;
let helpers = fs::read_to_string("agama.libsonnet")
let helpers = fs::read_to_string("share/agama.libsonnet")
.or_else(|_| fs::read_to_string("/usr/share/agama-cli/agama.libsonnet"))
.context("Failed to read agama.libsonnet")?;
let mut file = fs::File::create(path)?;
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/scripts/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use std::io;
use thiserror::Error;

use crate::transfer::TransferError;
use crate::utils::TransferError;

#[derive(Error, Debug)]
pub enum ScriptError {
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/scripts/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use std::{

use serde::{Deserialize, Serialize};

use crate::transfer::Transfer;
use crate::utils::Transfer;

use super::ScriptError;

Expand Down
27 changes: 27 additions & 0 deletions rust/agama-lib/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.

//! Utility module for Agama.

mod file_format;
mod transfer;

pub use file_format::*;
pub use transfer::*;
Loading
Loading