Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions rust/agama-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@
use std::{io::Write, path::PathBuf, process::Command};

use agama_lib::{
context::InstallationContext, http::BaseHTTPClient, install_settings::InstallSettings,
monitor::MonitorClient, profile::ValidationOutcome, utils::FileFormat, Store as SettingsStore,
context::InstallationContext,
http::{BaseHTTPClient, BaseHTTPClientError},
install_settings::InstallSettings,
monitor::MonitorClient,
profile::ValidationOutcome,
utils::FileFormat,
Store as SettingsStore,
};
use anyhow::{anyhow, Context};
use clap::Subcommand;
Expand Down Expand Up @@ -270,7 +275,10 @@ async fn generate(
/// Note that this client does not act on this *url*, it passes it as a parameter
/// to our web backend.
/// Return well-formed Agama JSON on success.
async fn autoyast_client(client: &BaseHTTPClient, url: &Uri<String>) -> anyhow::Result<String> {
async fn autoyast_client(
client: &BaseHTTPClient,
url: &Uri<String>,
) -> Result<String, BaseHTTPClientError> {
// FIXME: how to escape it?
let api_url = format!("/profile/autoyast?url={}", url);
let output: Box<serde_json::value::RawValue> = client.post(&api_url, &()).await?;
Expand Down
18 changes: 17 additions & 1 deletion rust/agama-lib/src/http/base_http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,26 @@ pub enum BaseHTTPClientError {
InvalidURL(#[from] url::ParseError),
#[error(transparent)]
InvalidJSON(#[from] serde_json::Error),
#[error("Backend call failed with status {0} and text '{1}'")]
// #[error("Backend call failed with status {0} and text '{1}'")]
#[error("Backend responded with code {} and the following message:\n\n{}", .0, format_backend_error(.1))]
BackendError(u16, String),
}

fn format_backend_error(error: &String) -> String {
let message: Result<serde_json::Value, _> = serde_json::from_str(&error);

match message {
Ok(message) => {
if let Some(error) = message.get("error") {
error.to_string().replace("\\n", "\n")
} else {
format!("{:?}", error)
}
}
Err(_) => format!("{:?}", error),
}
}

/// Base that all HTTP clients should use.
///
/// It provides several features including automatic base URL switching,
Expand Down
34 changes: 24 additions & 10 deletions rust/agama-lib/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,49 @@ use std::{fs, io::Write, path::Path, process::Command};
use tempfile::{tempdir, TempDir};
use url::Url;

#[derive(thiserror::Error, Debug)]
pub enum AutoyastError {
#[error("I/O error: {0}")]
IO(#[from] std::io::Error),
#[error("Failed to run the agama-autoyast script: {0}")]
Execute(#[source] std::io::Error),
#[error("Failed to convert the AutoYaST profile: {0}")]
Evaluation(String),
#[error("Unsupported AutoYaST format at {0}")]
UnsupportedFormat(Url),
}

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

impl AutoyastProfileImporter {
pub async fn read(url: &Url) -> anyhow::Result<Self> {
pub async fn read(url: &Url) -> Result<Self, AutoyastError> {
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));
return Err(AutoyastError::UnsupportedFormat(url.clone()));
}

const TMP_DIR_PREFIX: &str = "autoyast";
const AUTOINST_JSON: &str = "autoinst.json";

let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?;
tokio::process::Command::new("agama-autoyast")
let result = tokio::process::Command::new("agama-autoyast")
.env("YAST_SKIP_PROFILE_FETCH_ERROR", "1")
.args([url.as_str(), &tmp_dir.path().to_string_lossy()])
.status()
.output()
.await
.context("Failed to run agama-autoyast")?;
.map_err(AutoyastError::Execute)?;

if !result.status.success() {
return Err(AutoyastError::Evaluation(
String::from_utf8_lossy(&result.stderr).to_string(),
));
}

let autoinst_json = tmp_dir.path().join(AUTOINST_JSON);
let content = fs::read_to_string(&autoinst_json).context(format!(
"agama-autoyast did not produce {:?}",
autoinst_json
))?;
let content = fs::read_to_string(&autoinst_json)?;
Ok(Self { content })
}
}
Expand Down
27 changes: 16 additions & 11 deletions rust/agama-server/src/profile/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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::profile::AutoyastError;
use anyhow::Context;

use agama_lib::utils::Transfer;
Expand Down Expand Up @@ -50,6 +51,19 @@ impl std::fmt::Display for ProfileServiceError {
}
}

impl From<AutoyastError> for ProfileServiceError {
fn from(e: AutoyastError) -> Self {
let http_status = match e {
AutoyastError::Execute(..) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::BAD_REQUEST,
};
Self {
source: e.into(),
http_status,
}
}
}

// Make a 400 response
// ```
// let r: Result<T, anyhow::Error> = foo();
Expand Down Expand Up @@ -221,15 +235,6 @@ async fn autoyast(
}

let url = Url::parse(query.url.as_ref().unwrap()).map_err(anyhow::Error::new)?;
let importer_res = AutoyastProfileImporter::read(&url).await;
match importer_res {
Ok(importer) => Ok(importer.content),
Err(error) => {
// anyhow can be only displayed, not so nice
if format!("{}", error).contains("Failed to run") {
return Err(make_internal(error));
}
Err(error.into())
}
}
let importer = AutoyastProfileImporter::read(&url).await?;
Ok(importer.content)
}
6 changes: 6 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Wed Mar 11 14:39:15 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Add error reporting when working with AutoYaST profiles (related
to bsc#1259434).

-------------------------------------------------------------------
Fri Jan 30 13:48:53 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
10 changes: 6 additions & 4 deletions service/bin/agama-autoyast
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ begin
warn "Did not convert the profile (canceled by the user)."
exit 2
end
rescue Agama::Commands::CouldNotFetchProfile
warn "Could not fetch the AutoYaST profile."
rescue Agama::Commands::CouldNotFetchProfile => e
warn "Could not fetch the AutoYaST profile:\n\n"
warn e.full_message
exit 3
rescue Agama::Commands::CouldNotWriteAgamaConfig
warn "Could not write the Agama configuration."
rescue Agama::Commands::CouldNotWriteAgamaConfig => e
warn "Could not write the Agama configuration:\n\n"
warn e.full_message
exit 4
end
12 changes: 10 additions & 2 deletions service/lib/agama/autoyast/root_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,19 @@ def read
root_user = config.users.find { |u| u.name == "root" }
return {} unless root_user

hsh = { "password" => root_user.password.value.to_s }
hsh["hashedPassword"] = true if root_user.password.value.encrypted?
hsh = {}
password = root_user.password

if password
hsh["password"] = password.value.to_s
hsh["hashedPassword"] = true if password.value.encrypted?
end

public_key = root_user.authorized_keys.first
hsh["sshPublicKey"] = public_key if public_key

return {} if hsh.empty?

{ "root" => hsh }
end

Expand Down
9 changes: 6 additions & 3 deletions service/lib/agama/autoyast/user_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ def read

hsh = {
"userName" => user.name,
"fullName" => user.gecos.first.to_s,
"password" => user.password.value.to_s
"fullName" => user.gecos.first.to_s
}

hsh["hashedPassword"] = true if user.password.value.encrypted?
password = user.password
if password
hsh["password"] = password.value.to_s
hsh["hashedPassword"] = true if password.value.encrypted?
end

{ "user" => hsh }
end
Expand Down
11 changes: 11 additions & 0 deletions service/package/rubygem-agama-yast.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
-------------------------------------------------------------------
Wed Mar 11 14:38:44 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Add errors to the output of agama-autoyast (related to bsc#1259434).

-------------------------------------------------------------------
Wed Mar 11 06:55:42 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Properly handle nil user passwords when importing users from AutoYaST
(related to bsc#1259485).

-------------------------------------------------------------------
Tue Mar 10 14:10:18 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
14 changes: 14 additions & 0 deletions service/test/agama/autoyast/root_reader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@
"password" => "123456", "sshPublicKey" => "ssh-key 1"
)
end

context "but does not contain password or SSH public key" do
let(:root) do
{
"username" => "root",
"user_password" => nil,
"encrypted" => nil
}
end

it "returns an empty hash" do
expect(subject.read).to be_empty
end
end
end
end
end
19 changes: 19 additions & 0 deletions service/test/agama/autoyast/user_reader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,24 @@
)
end
end

context "when the password is nil" do
let(:user) do
{
"username" => "suse",
"fullname" => "SUSE",
"user_password" => nil,
"encrypted" => nil
}
end

it "does not include password information" do
user = subject.read["user"]
expect(user).to eq(
"userName" => "suse",
"fullName" => "SUSE"
)
end
end
end
end
Loading