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: add AutoYaST error reporting #1476

Merged
merged 28 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bdc3bbc
initial version of report patching
jreidinger Jul 18, 2024
fb76036
add readme for agama-yast with hint how to deploy modified gem
jreidinger Jul 18, 2024
ddd18bf
fix file loading
jreidinger Jul 18, 2024
d2a7391
make rubocop happy
jreidinger Jul 18, 2024
747ee38
fix returning correct question and add tracing to see which kind of q…
jreidinger Jul 18, 2024
4a7ece4
clarify format of questions and answers
jreidinger Jul 18, 2024
c5494e2
fix superclass
jreidinger Jul 18, 2024
f1dc311
fixes from testing
jreidinger Jul 18, 2024
d92deef
fix getting answer
jreidinger Jul 19, 2024
4d8c0ce
use correct property names
jreidinger Jul 19, 2024
7e550a7
fix questions list to list only unanswered ones
jreidinger Jul 21, 2024
9deacdb
implement general Question with Dialog component
jreidinger Jul 21, 2024
6262156
fix path for deleting question
jreidinger Jul 22, 2024
78d997c
fix parsing of result
jreidinger Jul 22, 2024
7879a7c
Apply suggestions from code review
jreidinger Jul 22, 2024
e4c74dc
Merge remote-tracking branch 'origin/master' into ay_errors
jreidinger Jul 22, 2024
b7e1a9e
do not require id for newly created question
jreidinger Jul 22, 2024
85a7159
changes
jreidinger Jul 22, 2024
c44f3af
Apply suggestions from code review
jreidinger Jul 23, 2024
2e9d7e7
introduce internal error
jreidinger Jul 23, 2024
2d7fff4
remove duplicite popup now
jreidinger Jul 23, 2024
20ea50a
fix formatting
jreidinger Jul 23, 2024
097f14f
add test for question with password component
jreidinger Jul 23, 2024
33f7c75
relax condition of products test to not fail when we change product
jreidinger Jul 23, 2024
8bc9b1b
fix formatting
jreidinger Jul 23, 2024
ce03cfc
Merge remote-tracking branch 'origin/master' into ay_errors
jreidinger Jul 23, 2024
fc249f8
test(web): simplify QuestionWithPassword tests
imobachgs Jul 23, 2024
5cd74da
Apply suggestions from code review
jreidinger Jul 23, 2024
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
12 changes: 8 additions & 4 deletions rust/agama-cli/src/questions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use agama_lib::questions::http_client::HTTPClient;
use agama_lib::{connection, error::ServiceError};
use clap::{Args, Subcommand, ValueEnum};

// TODO: use for answers also JSON to be consistent
#[derive(Subcommand, Debug)]
pub enum QuestionsCommands {
/// Set the mode for answering questions.
Expand All @@ -19,9 +20,9 @@ pub enum QuestionsCommands {
/// Path to a file containing the answers in YAML format.
path: String,
},
/// prints list of questions that is waiting for answer in YAML format
/// prints list of questions that is waiting for answer in JSON format
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
List,
/// Ask question from stdin in YAML format and print answer when it is answered.
/// Ask question from stdin in JSON format and print answer when it is answered.
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
Ask,
}

Expand Down Expand Up @@ -71,11 +72,14 @@ async fn ask_question() -> Result<(), ServiceError> {
let question = serde_json::from_reader(std::io::stdin())?;

let created_question = client.create_question(&question).await?;
let answer = client.get_answer(created_question.generic.id).await?;
let Some(id) = created_question.generic.id else {
return Err(ServiceError::QuestionNotExist(0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does that '0' mean? It looks like the error is not exactly that the question does not exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest this should not happen as it is assigned in that create_question from created question path. I am not sure if we should have something like Programatic error or something that for cases which should not happen and user cannot do naything with it

};
let answer = client.get_answer(id).await?;
let answer_json = serde_json::to_string_pretty(&answer).map_err(Into::<anyhow::Error>::into)?;
println!("{}", answer_json);

client.delete_question(created_question.generic.id).await?;
client.delete_question(id).await?;
Ok(())
}

Expand Down
2 changes: 2 additions & 0 deletions rust/agama-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub enum ServiceError {
UnsuccessfulAction(String),
#[error("Unknown installation phase: {0}")]
UnknownInstallationPhase(u32),
#[error("Question with id {0} does not exist")]
QuestionNotExist(u32),
#[error("Backend call failed with status {0} and text '{1}'")]
BackendError(u16, String),
#[error("You are not logged in. Please use: agama auth login")]
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/questions/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl HTTPClient {
}

pub async fn delete_question(&self, question_id: u32) -> Result<(), ServiceError> {
let path = format!("/questions/{}/answer", question_id);
let path = format!("/questions/{}", question_id);
self.client.delete(path.as_str()).await
}
}
3 changes: 2 additions & 1 deletion rust/agama-lib/src/questions/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ pub struct Question {
#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GenericQuestion {
pub id: u32,
/// id is optional as newly created questions does not have it assigned
pub id: Option<u32>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we get rid of D-Bus in the future, we might consider using an uuid instead of dealing with an Option. But that's out of scope.

pub class: String,
pub text: String,
pub options: Vec<String>,
Expand Down
116 changes: 60 additions & 56 deletions rust/agama-server/src/questions/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use crate::{error::Error, web::Event};
use agama_lib::{
dbus::{extract_id_from_path, get_property},
error::ServiceError,
proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy},
questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword},
Expand All @@ -19,13 +20,12 @@ use axum::{
routing::{delete, get},
Json, Router,
};
use regex::Regex;
use std::{collections::HashMap, pin::Pin};
use tokio_stream::{Stream, StreamExt};
use zbus::{
fdo::ObjectManagerProxy,
names::{InterfaceName, OwnedInterfaceName},
zvariant::{ObjectPath, OwnedObjectPath},
zvariant::{ObjectPath, OwnedObjectPath, OwnedValue},
};

// TODO: move to lib or maybe not and just have in lib client for http API?
Expand All @@ -34,6 +34,8 @@ struct QuestionsClient<'a> {
connection: zbus::Connection,
objects_proxy: ObjectManagerProxy<'a>,
questions_proxy: Questions1Proxy<'a>,
generic_interface: OwnedInterfaceName,
with_password_interface: OwnedInterfaceName,
}

impl<'a> QuestionsClient<'a> {
Expand All @@ -48,6 +50,14 @@ impl<'a> QuestionsClient<'a> {
.destination("org.opensuse.Agama1")?
.build()
.await?,
generic_interface: InterfaceName::from_str_unchecked(
"org.opensuse.Agama1.Questions.Generic",
)
.into(),
with_password_interface: InterfaceName::from_str_unchecked(
"org.opensuse.Agama1.Questions.WithPassword",
)
.into(),
})
}

Expand All @@ -61,6 +71,7 @@ impl<'a> QuestionsClient<'a> {
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let path = if question.with_password.is_some() {
tracing::info!("creating question with password");
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
self.questions_proxy
.new_with_password(
&generic.class,
Expand All @@ -71,6 +82,7 @@ impl<'a> QuestionsClient<'a> {
)
.await?
} else {
tracing::info!("creating generic question");
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
self.questions_proxy
.new_question(
&generic.class,
Expand All @@ -82,14 +94,9 @@ impl<'a> QuestionsClient<'a> {
.await?
};
let mut res = question.clone();
// we are sure that regexp is correct, so use unwrap
let id_matcher = Regex::new(r"/(?<id>\d+)$").unwrap();
let Some(id_cap) = id_matcher.captures(path.as_str()) else {
let msg = format!("Failed to get ID for new question: {}", path.as_str()).to_string();
return Err(ServiceError::UnsuccessfulAction(msg));
}; // TODO: better error if path does not contain id
res.generic.id = id_cap["id"].parse::<u32>().unwrap();
Ok(question)
res.generic.id = Some(extract_id_from_path(&path)?);
tracing::info!("new question gets id {:?}", res.generic.id);
Ok(res)
}

pub async fn questions(&self) -> Result<Vec<Question>, ServiceError> {
Expand All @@ -99,54 +106,46 @@ impl<'a> QuestionsClient<'a> {
.await
.context("failed to get managed object with Object Manager")?;
let mut result: Vec<Question> = Vec::with_capacity(objects.len());
let password_interface = OwnedInterfaceName::from(
InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword")
.context("Failed to create interface name for question with password")?,
);
for (path, interfaces_hash) in objects.iter() {
if interfaces_hash.contains_key(&password_interface) {
result.push(self.build_question_with_password(path).await?)
} else {
result.push(self.build_generic_question(path).await?)

for (_path, interfaces_hash) in objects.iter() {
let generic_properties = interfaces_hash
.get(&self.generic_interface)
.context("Failed to create interface name for generic question")?;
// skip if question is already answered
let answer: String = get_property(generic_properties, "Answer")?;
if !answer.is_empty() {
continue;
}
let mut question = self.build_generic_question(generic_properties)?;

if interfaces_hash.contains_key(&self.with_password_interface) {
question.with_password = Some(QuestionWithPassword {});
}

result.push(question);
}
Ok(result)
}

async fn build_generic_question(
fn build_generic_question(
&self,
path: &OwnedObjectPath,
properties: &HashMap<String, OwnedValue>,
) -> Result<Question, ServiceError> {
let dbus_question = GenericQuestionProxy::builder(&self.connection)
.path(path)?
.cache_properties(zbus::CacheProperties::No)
.build()
.await?;
let result = Question {
generic: GenericQuestion {
id: dbus_question.id().await?,
class: dbus_question.class().await?,
text: dbus_question.text().await?,
options: dbus_question.options().await?,
default_option: dbus_question.default_option().await?,
data: dbus_question.data().await?,
id: Some(get_property(properties, "Id")?),
class: get_property(properties, "Class")?,
text: get_property(properties, "Text")?,
options: get_property(properties, "Options")?,
default_option: get_property(properties, "DefaultOption")?,
data: get_property(properties, "Data")?,
},
with_password: None,
};

Ok(result)
}

async fn build_question_with_password(
&self,
path: &OwnedObjectPath,
) -> Result<Question, ServiceError> {
let mut result = self.build_generic_question(path).await?;
result.with_password = Some(QuestionWithPassword {});

Ok(result)
}

pub async fn delete(&self, id: u32) -> Result<(), ServiceError> {
let question_path = ObjectPath::from(
ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id))
Expand All @@ -164,24 +163,29 @@ impl<'a> QuestionsClient<'a> {
ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id))
.context("Failed to create dbus path")?,
);
let objects = self.objects_proxy.get_managed_objects().await?;
let password_interface = OwnedInterfaceName::from(
InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword")
.context("Failed to create interface name for question with password")?,
);
let mut result = Answer::default();
let dbus_password_res = QuestionWithPasswordProxy::builder(&self.connection)
.path(&question_path)?
.cache_properties(zbus::CacheProperties::No)
.build()
.await;
if let Ok(dbus_password) = dbus_password_res {
let question = objects
.get(&question_path)
.ok_or(ServiceError::QuestionNotExist(id))?;

if let Some(password_iface) = question.get(&password_interface) {
result.with_password = Some(PasswordAnswer {
password: dbus_password.password().await?,
password: get_property(password_iface, "Password")?,
});
}

let dbus_generic = GenericQuestionProxy::builder(&self.connection)
.path(&question_path)?
.cache_properties(zbus::CacheProperties::No)
.build()
.await?;
let answer = dbus_generic.answer().await?;
let generic_interface = OwnedInterfaceName::from(
InterfaceName::from_static_str("org.opensuse.Agama1.Questions.Generic")
.context("Failed to create interface name for generic question")?,
);
let generic_iface = question
.get(&generic_interface)
.context("Question does not have generic interface")?;
let answer: String = get_property(generic_iface, "Answer")?;
if answer.is_empty() {
Ok(None)
} else {
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 @@
-------------------------------------------------------------------
Mon Jul 22 15:27:44 UTC 2024 - Josef Reidinger <[email protected]>

- Fix `agama questions list` to list only unaswered questions and
improve its performance
(gh#openSUSE/agama#1476)

-------------------------------------------------------------------
Wed Jul 17 11:15:33 UTC 2024 - Jorik Cronenberg <[email protected]>

Expand Down
21 changes: 21 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Agama YaST

According to [Agama's architecture](../doc/architecture.md) this project implements the following components:

* The *Agama YaST*, the layer build on top of YaST functionality.

## Testing Changes

The easiest way to test changes done to ruby code on agama liveCD is to build
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
gem with modified sources with `gem build agama-yast`. Then copy resulting file
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
to agama live ISO. There do this sequence of commands:
jreidinger marked this conversation as resolved.
Show resolved Hide resolved

```sh
# ensure that only modified sources are installed
gem uninstall agama-yast
# install modified sources including proper binary names
gem install --no-doc --no-format-executable <path to gem>
```

If change modifies also dbus parts, then restart related dbus services.
jreidinger marked this conversation as resolved.
Show resolved Hide resolved

Empty file modified service/lib/agama/autoyast/bond_reader.rb
100644 → 100755
Empty file.
Empty file modified service/lib/agama/autoyast/connections_reader.rb
100644 → 100755
Empty file.
2 changes: 2 additions & 0 deletions service/lib/agama/autoyast/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
require "fileutils"
require "pathname"

require "agama/autoyast/report_patching"

# :nodoc:
module Agama
module AutoYaST
Expand Down
Empty file modified service/lib/agama/autoyast/l10n_reader.rb
100644 → 100755
Empty file.
Empty file modified service/lib/agama/autoyast/network_reader.rb
100644 → 100755
Empty file.
Empty file modified service/lib/agama/autoyast/product_reader.rb
100644 → 100755
Empty file.
Loading
Loading