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.
228 lines
7.4 KiB
Rust
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??)
|
|
}
|
|
}
|