agama/rust/agama-software/src/model.rs
Josef Reidinger cf019876e0
fix missing predefined repositories during first run (bsc#1268635)
The tricky part is that with having them in system, we cause loop where first compute of wanted state do not know yet about them...so for interactive with a bit clicking, it works. But for autoinstallation we need to have it correctly from the initial phase.
2026-06-24 14:34:42 +02:00

228 lines
7.4 KiB
Rust

// Copyright (c) [2024-2026] 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.
use agama_security as security;
use agama_utils::{
actor::Handler,
api::{
software::{Repository, SoftwareProposal, SystemInfo},
Issue,
},
products::ProductSpec,
progress, question,
};
use async_trait::async_trait;
use tokio::sync::{mpsc, oneshot};
use crate::{model::state::SoftwareState, service, zypp_server::SoftwareAction};
pub mod conflict;
pub mod packages;
pub mod registration;
pub mod software_selection;
pub mod state;
// Re-export from agama-utils for backwards compatibility
pub use agama_utils::{Resolvable, ResolvableType};
pub use registration::Registration;
/// Issues found when applying the software configuration.
#[derive(Debug, Default)]
pub struct WriteIssues {
pub product: Vec<Issue>,
pub software: Vec<Issue>,
}
impl WriteIssues {
pub fn is_empty(&self) -> bool {
self.product.is_empty() && self.software.is_empty()
}
}
/// Abstract the software-related configuration from the underlying system.
///
/// It offers an API to query and set different software and product elements of a
/// libzypp. This trait can be implemented to replace the real libzypp interaction during
/// tests.
#[async_trait]
pub trait ModelAdapter: Send + Sync + 'static {
/// Returns the software system information.
async fn system_info(&self) -> Result<SystemInfo, service::Error>;
async fn proposal(&self) -> Result<SoftwareProposal, service::Error>;
/// Refresh repositories information.
async fn refresh(&mut self) -> Result<(), service::Error>;
/// install rpms to target system
async fn install(&self) -> Result<bool, service::Error>;
/// Finalizes system like disabling local repositories
async fn finish(&self) -> Result<(), service::Error>;
fn set_product(&mut self, product_spec: ProductSpec);
/// Applies the configuration to the system.
///
/// It does not perform the installation, just update the repositories and
/// the software selection.
async fn write(
&mut self,
software: SoftwareState,
l10n: Handler<agama_l10n::Service>,
progress: Handler<progress::Service>,
) -> Result<WriteIssues, service::Error>;
fn predefined_repositories(&self) -> Vec<Repository>;
}
/// [ModelAdapter] implementation for libzypp systems.
pub struct Model {
zypp_sender: mpsc::UnboundedSender<SoftwareAction>,
// FIXME: what about having a SoftwareServiceState to keep business logic state?
selected_product: Option<ProductSpec>,
progress: Handler<progress::Service>,
question: Handler<question::Service>,
security: Handler<security::Service>,
/// Predefined repositories (from the off-line media and Driver Update Disks).
/// They cannot be altered through user configuration.
predefined_repositories: Vec<Repository>,
}
impl Model {
/// Initializes the struct with the information from the underlying system.
pub fn new(
zypp_sender: mpsc::UnboundedSender<SoftwareAction>,
predefined_repositories: Vec<Repository>,
progress: Handler<progress::Service>,
question: Handler<question::Service>,
security: Handler<security::Service>,
) -> Result<Self, service::Error> {
Ok(Self {
zypp_sender,
selected_product: None,
progress,
question,
security,
predefined_repositories,
})
}
}
#[async_trait]
impl ModelAdapter for Model {
fn set_product(&mut self, product_spec: ProductSpec) {
self.selected_product = Some(product_spec);
}
fn predefined_repositories(&self) -> Vec<Repository> {
self.predefined_repositories.clone()
}
async fn write(
&mut self,
software: SoftwareState,
l10n: Handler<agama_l10n::Service>,
progress: Handler<progress::Service>,
) -> Result<WriteIssues, service::Error> {
let (tx, rx) = oneshot::channel();
let l10n_proposal = l10n
.call(agama_l10n::message::GetProposal)
.await
.unwrap_or_default();
self.zypp_sender.send(SoftwareAction::Write {
state: software,
progress,
l10n: l10n_proposal,
security: self.security.clone(),
question: self.question.clone(),
tx,
})?;
Ok(rx.await??)
}
/// Returns the software system information.
async fn system_info(&self) -> Result<SystemInfo, service::Error> {
let Some(product_spec) = self.selected_product.clone() else {
return Err(service::Error::MissingProduct);
};
let (tx, rx) = oneshot::channel();
self.zypp_sender
.send(SoftwareAction::GetSystemInfo(product_spec, tx))?;
let mut system_info = rx.await??;
// Set "predefined" field as this struct holds the information to determine
// which repositories are "predefined".
let predefined_urls: Vec<_> = self
.predefined_repositories
.iter()
.map(|r| r.url.as_str())
.collect();
let mut missing_predefined_repos = self.predefined_repositories.clone();
for repo in system_info.repositories.iter_mut() {
repo.predefined = predefined_urls.contains(&repo.url.as_str());
if repo.predefined {
missing_predefined_repos.retain(|r| r.url != repo.url);
}
}
// Add all predefined repositories that are missing in libzypp.
// As a result, they will be added to libzypp when writing the new
// software state.
system_info.repositories.extend(missing_predefined_repos);
tracing::info!("System info: {:?}", system_info);
Ok(system_info)
}
async fn refresh(&mut self) -> Result<(), service::Error> {
unimplemented!()
}
async fn finish(&self) -> Result<(), service::Error> {
let (tx, rx) = oneshot::channel();
self.zypp_sender.send(SoftwareAction::Finish(tx))?;
Ok(rx.await??)
}
async fn install(&self) -> Result<bool, service::Error> {
let (tx, rx) = oneshot::channel();
self.zypp_sender.send(SoftwareAction::Install(
tx,
self.progress.clone(),
self.question.clone(),
))?;
Ok(rx.await??)
}
async fn proposal(&self) -> Result<SoftwareProposal, service::Error> {
let Some(product_spec) = self.selected_product.clone() else {
return Err(service::Error::MissingProduct);
};
let (tx, rx) = oneshot::channel();
self.zypp_sender
.send(SoftwareAction::GetProposal(product_spec, tx))?;
Ok(rx.await??)
}
}