From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 1C3D61FF2CF for ; Tue, 9 Jul 2024 08:18:49 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0CDB318795; Tue, 9 Jul 2024 08:19:11 +0200 (CEST) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Tue, 9 Jul 2024 08:18:31 +0200 Message-Id: <20240709061832.52121-5-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240709061832.52121-1-w.bumiller@proxmox.com> References: <20240709061832.52121-1-w.bumiller@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.087 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [PATCH proxmox 5/6] apt: add cache feature X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Backup Server development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" From: Dietmar Maurer Save/read package state from a file, and add the api functions to manipulate that state. Signed-off-by: Dietmar Maurer --- proxmox-apt/Cargo.toml | 17 ++ proxmox-apt/debian/control | 23 +++ proxmox-apt/src/api.rs | 143 ++++++++++++++ proxmox-apt/src/cache.rs | 301 ++++++++++++++++++++++++++++++ proxmox-apt/src/cache_api.rs | 208 +++++++++++++++++++++ proxmox-apt/src/lib.rs | 9 + proxmox-apt/tests/repositories.rs | 8 +- 7 files changed, 706 insertions(+), 3 deletions(-) create mode 100644 proxmox-apt/src/api.rs create mode 100644 proxmox-apt/src/cache.rs create mode 100644 proxmox-apt/src/cache_api.rs diff --git a/proxmox-apt/Cargo.toml b/proxmox-apt/Cargo.toml index bbd4ff89..923be446 100644 --- a/proxmox-apt/Cargo.toml +++ b/proxmox-apt/Cargo.toml @@ -22,3 +22,20 @@ rfc822-like = "0.2.1" proxmox-apt-api-types.workspace = true proxmox-config-digest = { workspace = true, features = ["openssl"] } +proxmox-sys.workspace = true + +apt-pkg-native = { version = "0.3.2", optional = true } +regex = { workspace = true, optional = true } +nix = { workspace = true, optional = true } +log = { workspace = true, optional = true } +proxmox-schema = { workspace = true, optional = true } + +[features] +default = [] +cache = [ + "dep:apt-pkg-native", + "dep:regex", + "dep:nix", + "dep:log", + "dep:proxmox-schema", +] diff --git a/proxmox-apt/debian/control b/proxmox-apt/debian/control index 347631e6..7e0b79b1 100644 --- a/proxmox-apt/debian/control +++ b/proxmox-apt/debian/control @@ -13,6 +13,7 @@ Build-Depends: debhelper (>= 12), librust-proxmox-apt-api-types-1+default-dev , librust-proxmox-config-digest-0.1+default-dev , librust-proxmox-config-digest-0.1+openssl-dev , + librust-proxmox-sys-0.5+default-dev (>= 0.5.7-~~) , librust-rfc822-like-0.2+default-dev (>= 0.2.1-~~) , librust-serde-1+default-dev , librust-serde-1+derive-dev , @@ -37,10 +38,13 @@ Depends: librust-proxmox-apt-api-types-1+default-dev, librust-proxmox-config-digest-0.1+default-dev, librust-proxmox-config-digest-0.1+openssl-dev, + librust-proxmox-sys-0.5+default-dev (>= 0.5.7-~~), librust-rfc822-like-0.2+default-dev (>= 0.2.1-~~), librust-serde-1+default-dev, librust-serde-1+derive-dev, librust-serde-json-1+default-dev +Suggests: + librust-proxmox-apt+cache-dev (= ${binary:Version}) Provides: librust-proxmox-apt+default-dev (= ${binary:Version}), librust-proxmox-apt-0-dev (= ${binary:Version}), @@ -51,3 +55,22 @@ Provides: librust-proxmox-apt-0.10.10+default-dev (= ${binary:Version}) Description: Proxmox library for APT - Rust source code Source code for Debianized Rust crate "proxmox-apt" + +Package: librust-proxmox-apt+cache-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-apt-dev (= ${binary:Version}), + librust-apt-pkg-native-0.3+default-dev (>= 0.3.2-~~), + librust-log-0.4+default-dev (>= 0.4.17-~~), + librust-nix-0.26+default-dev (>= 0.26.1-~~), + librust-proxmox-schema-3+default-dev (>= 3.1.1-~~), + librust-regex-1+default-dev (>= 1.5-~~) +Provides: + librust-proxmox-apt-0+cache-dev (= ${binary:Version}), + librust-proxmox-apt-0.10+cache-dev (= ${binary:Version}), + librust-proxmox-apt-0.10.10+cache-dev (= ${binary:Version}) +Description: Proxmox library for APT - feature "cache" + This metapackage enables feature "cache" for the Rust proxmox-apt crate, by + pulling in any additional dependencies needed by that feature. diff --git a/proxmox-apt/src/api.rs b/proxmox-apt/src/api.rs new file mode 100644 index 00000000..af01048e --- /dev/null +++ b/proxmox-apt/src/api.rs @@ -0,0 +1,143 @@ +// API function that work without feature "cache" + +use anyhow::{bail, Error}; + +use proxmox_apt_api_types::{ + APTChangeRepositoryOptions, APTGetChangelogOptions, APTRepositoriesResult, APTRepositoryFile, + APTRepositoryHandle, +}; +use proxmox_config_digest::ConfigDigest; + +use crate::repositories::{APTRepositoryFileImpl, APTRepositoryImpl}; + +/// Retrieve the changelog of the specified package. +pub fn get_changelog(options: &APTGetChangelogOptions) -> Result { + let mut command = std::process::Command::new("apt-get"); + command.arg("changelog"); + command.arg("-qq"); // don't display download progress + if let Some(ver) = &options.version { + command.arg(format!("{}={}", options.name, ver)); + } else { + command.arg(&options.name); + } + let output = proxmox_sys::command::run_command(command, None)?; + + Ok(output) +} + +/// Get APT repository information. +pub fn list_repositories(product: &str) -> Result { + let (files, errors, digest) = crate::repositories::repositories()?; + + let suite = crate::repositories::get_current_release_codename()?; + + let infos = crate::repositories::check_repositories(&files, suite); + let standard_repos = crate::repositories::standard_repositories(&files, product, suite); + + Ok(APTRepositoriesResult { + files, + errors, + digest, + infos, + standard_repos, + }) +} + +/// Add the repository identified by the `handle`. +/// If the repository is already configured, it will be set to enabled. +/// +/// The `digest` parameter asserts that the configuration has not been modified. +pub fn add_repository_handle( + product: &str, + handle: APTRepositoryHandle, + digest: Option, +) -> Result<(), Error> { + let (mut files, errors, current_digest) = crate::repositories::repositories()?; + + current_digest.detect_modification(digest.as_ref())?; + + let suite = crate::repositories::get_current_release_codename()?; + + // check if it's already configured first + for file in files.iter_mut() { + for repo in file.repositories.iter_mut() { + if repo.is_referenced_repository(handle, "pbs", &suite.to_string()) { + if repo.enabled { + return Ok(()); + } + + repo.set_enabled(true); + file.write()?; + + return Ok(()); + } + } + } + + let (repo, path) = crate::repositories::get_standard_repository(handle, product, suite); + + if let Some(error) = errors.iter().find(|error| error.path == path) { + bail!( + "unable to parse existing file {} - {}", + error.path, + error.error, + ); + } + + if let Some(file) = files + .iter_mut() + .find(|file| file.path.as_ref() == Some(&path)) + { + file.repositories.push(repo); + + file.write()?; + } else { + let mut file = match APTRepositoryFile::new(&path)? { + Some(file) => file, + None => bail!("invalid path - {}", path), + }; + + file.repositories.push(repo); + + file.write()?; + } + + Ok(()) +} + +/// Change the properties of the specified repository. +/// +/// The `digest` parameter asserts that the configuration has not been modified. +pub fn change_repository( + path: &str, + index: usize, + options: &APTChangeRepositoryOptions, + digest: Option, +) -> Result<(), Error> { + let (mut files, errors, current_digest) = crate::repositories::repositories()?; + + current_digest.detect_modification(digest.as_ref())?; + + if let Some(error) = errors.iter().find(|error| error.path == path) { + bail!("unable to parse file {} - {}", error.path, error.error); + } + + if let Some(file) = files + .iter_mut() + .find(|file| file.path.as_deref() == Some(path)) + { + if let Some(repo) = file.repositories.get_mut(index) { + if let Some(enabled) = options.enabled { + repo.set_enabled(enabled); + } + + file.write()?; + } else { + bail!("invalid index - {}", index); + } + } else { + bail!("invalid path - {}", path); + } + + Ok(()) +} diff --git a/proxmox-apt/src/cache.rs b/proxmox-apt/src/cache.rs new file mode 100644 index 00000000..03315013 --- /dev/null +++ b/proxmox-apt/src/cache.rs @@ -0,0 +1,301 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::path::Path; + +use anyhow::{bail, format_err, Error}; +use apt_pkg_native::Cache; + +use proxmox_schema::const_regex; +use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions}; + +use proxmox_apt_api_types::APTUpdateInfo; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +/// Some information we cache about the package (update) state, like what pending update version +/// we already notfied an user about +pub struct PkgState { + /// simple map from package name to most recently notified (emailed) version + pub notified: Option>, + /// A list of pending updates + pub package_status: Vec, +} + +pub fn write_pkg_cache>(apt_state_file: P, state: &PkgState) -> Result<(), Error> { + let serialized_state = serde_json::to_string(state)?; + + replace_file( + apt_state_file, + serialized_state.as_bytes(), + CreateOptions::new(), + false, + ) + .map_err(|err| format_err!("Error writing package cache - {}", err))?; + Ok(()) +} + +pub fn read_pkg_state>(apt_state_file: P) -> Result, Error> { + let serialized_state = match file_read_optional_string(apt_state_file) { + Ok(Some(raw)) => raw, + Ok(None) => return Ok(None), + Err(err) => bail!("could not read cached package state file - {}", err), + }; + + serde_json::from_str(&serialized_state) + .map(Some) + .map_err(|err| format_err!("could not parse cached package status - {}", err)) +} + +pub fn pkg_cache_expired>(apt_state_file: P) -> Result { + if let Ok(pbs_cache) = std::fs::metadata(apt_state_file) { + let apt_pkgcache = std::fs::metadata("/var/cache/apt/pkgcache.bin")?; + let dpkg_status = std::fs::metadata("/var/lib/dpkg/status")?; + + let mtime = pbs_cache.modified()?; + + if apt_pkgcache.modified()? <= mtime && dpkg_status.modified()? <= mtime { + return Ok(false); + } + } + Ok(true) +} + +pub fn update_cache>(apt_state_file: P) -> Result { + let apt_state_file = apt_state_file.as_ref(); + // update our cache + let all_upgradeable = list_installed_apt_packages( + |data| { + data.candidate_version == data.active_version + && data.installed_version != Some(data.candidate_version) + }, + None, + ); + + let cache = match read_pkg_state(apt_state_file) { + Ok(Some(mut cache)) => { + cache.package_status = all_upgradeable; + cache + } + _ => PkgState { + notified: None, + package_status: all_upgradeable, + }, + }; + write_pkg_cache(apt_state_file, &cache)?; + Ok(cache) +} + +const_regex! { + VERSION_EPOCH_REGEX = r"^\d+:"; + FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$"; +} + +pub struct FilterData<'a> { + /// package name + pub package: &'a str, + /// this is version info returned by APT + pub installed_version: Option<&'a str>, + pub candidate_version: &'a str, + + /// this is the version info the filter is supposed to check + pub active_version: &'a str, +} + +enum PackagePreSelect { + OnlyInstalled, + OnlyNew, + All, +} + +pub fn list_installed_apt_packages bool>( + filter: F, + only_versions_for: Option<&str>, +) -> Vec { + let mut ret = Vec::new(); + let mut depends = HashSet::new(); + + // note: this is not an 'apt update', it just re-reads the cache from disk + let mut cache = Cache::get_singleton(); + cache.reload(); + + let mut cache_iter = match only_versions_for { + Some(name) => cache.find_by_name(name), + None => cache.iter(), + }; + + loop { + match cache_iter.next() { + Some(view) => { + let di = if only_versions_for.is_some() { + query_detailed_info(PackagePreSelect::All, &filter, view, None) + } else { + query_detailed_info( + PackagePreSelect::OnlyInstalled, + &filter, + view, + Some(&mut depends), + ) + }; + if let Some(info) = di { + ret.push(info); + } + + if only_versions_for.is_some() { + break; + } + } + None => { + drop(cache_iter); + // also loop through missing dependencies, as they would be installed + for pkg in depends.iter() { + let mut iter = cache.find_by_name(pkg); + let view = match iter.next() { + Some(view) => view, + None => continue, // package not found, ignore + }; + + let di = query_detailed_info(PackagePreSelect::OnlyNew, &filter, view, None); + if let Some(info) = di { + ret.push(info); + } + } + break; + } + } + } + + ret +} + +fn query_detailed_info<'a, F, V>( + pre_select: PackagePreSelect, + filter: F, + view: V, + depends: Option<&mut HashSet>, +) -> Option +where + F: Fn(FilterData) -> bool, + V: std::ops::Deref>, +{ + let current_version = view.current_version(); + let candidate_version = view.candidate_version(); + + let (current_version, candidate_version) = match pre_select { + PackagePreSelect::OnlyInstalled => match (current_version, candidate_version) { + (Some(cur), Some(can)) => (Some(cur), can), // package installed and there is an update + (Some(cur), None) => (Some(cur.clone()), cur), // package installed and up-to-date + (None, Some(_)) => return None, // package could be installed + (None, None) => return None, // broken + }, + PackagePreSelect::OnlyNew => match (current_version, candidate_version) { + (Some(_), Some(_)) => return None, + (Some(_), None) => return None, + (None, Some(can)) => (None, can), + (None, None) => return None, + }, + PackagePreSelect::All => match (current_version, candidate_version) { + (Some(cur), Some(can)) => (Some(cur), can), + (Some(cur), None) => (Some(cur.clone()), cur), + (None, Some(can)) => (None, can), + (None, None) => return None, + }, + }; + + // get additional information via nested APT 'iterators' + let mut view_iter = view.versions(); + while let Some(ver) = view_iter.next() { + let package = view.name(); + let version = ver.version(); + let mut origin_res = "unknown".to_owned(); + let mut section_res = "unknown".to_owned(); + let mut priority_res = "unknown".to_owned(); + let mut short_desc = package.clone(); + let mut long_desc = "".to_owned(); + + let fd = FilterData { + package: package.as_str(), + installed_version: current_version.as_deref(), + candidate_version: &candidate_version, + active_version: &version, + }; + + if filter(fd) { + if let Some(section) = ver.section() { + section_res = section; + } + + if let Some(prio) = ver.priority_type() { + priority_res = prio; + } + + // assume every package has only one origin file (not + // origin, but origin *file*, for some reason those seem to + // be different concepts in APT) + let mut origin_iter = ver.origin_iter(); + let origin = origin_iter.next(); + if let Some(origin) = origin { + if let Some(sd) = origin.short_desc() { + short_desc = sd; + } + + if let Some(ld) = origin.long_desc() { + long_desc = ld; + } + + // the package files appear in priority order, meaning + // the one for the candidate version is first - this is fine + // however, as the source package should be the same for all + // versions anyway + let mut pkg_iter = origin.file(); + let pkg_file = pkg_iter.next(); + if let Some(pkg_file) = pkg_file { + if let Some(origin_name) = pkg_file.origin() { + origin_res = origin_name; + } + } + } + + if let Some(depends) = depends { + let mut dep_iter = ver.dep_iter(); + loop { + let dep = match dep_iter.next() { + Some(dep) if dep.dep_type() != "Depends" => continue, + Some(dep) => dep, + None => break, + }; + + let dep_pkg = dep.target_pkg(); + let name = dep_pkg.name(); + + depends.insert(name); + } + } + + return Some(APTUpdateInfo { + package, + title: short_desc, + arch: view.arch(), + description: long_desc, + origin: origin_res, + version: candidate_version.clone(), + old_version: match current_version { + Some(vers) => vers, + None => "".to_owned(), + }, + priority: priority_res, + section: section_res, + extra_info: None, + }); + } + } + + None +} + +pub fn sort_package_list(packages: &mut Vec) { + let cache = apt_pkg_native::Cache::get_singleton(); + packages.sort_by(|left, right| { + cache + .compare_versions(&left.old_version, &right.old_version) + .reverse() + }); +} diff --git a/proxmox-apt/src/cache_api.rs b/proxmox-apt/src/cache_api.rs new file mode 100644 index 00000000..979f47ed --- /dev/null +++ b/proxmox-apt/src/cache_api.rs @@ -0,0 +1,208 @@ +// API function that need feature "cache" +use std::path::Path; + +use anyhow::{bail, format_err, Error}; +use std::os::unix::prelude::OsStrExt; + +use proxmox_apt_api_types::{APTUpdateInfo, APTUpdateOptions}; + +/// List available APT updates +/// +/// Automatically updates an expired package cache. +pub fn list_available_apt_update>( + apt_state_file: P, +) -> Result, Error> { + let apt_state_file = apt_state_file.as_ref(); + if let Ok(false) = crate::cache::pkg_cache_expired(apt_state_file) { + if let Ok(Some(cache)) = crate::cache::read_pkg_state(apt_state_file) { + return Ok(cache.package_status); + } + } + + let cache = crate::cache::update_cache(apt_state_file)?; + + Ok(cache.package_status) +} + +/// Update the APT database +/// +/// You should update the APT proxy configuration before running this. +pub fn update_database>( + apt_state_file: P, + options: &APTUpdateOptions, + send_updates_available: impl Fn(&[&APTUpdateInfo]) -> Result<(), Error>, +) -> Result<(), Error> { + let apt_state_file = apt_state_file.as_ref(); + + let quiet = options.quiet.unwrap_or(false); + let notify = options.notify.unwrap_or(false); + + if !quiet { + log::info!("starting apt-get update") + } + + let mut command = std::process::Command::new("apt-get"); + command.arg("update"); + + // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now. + let output = command + .output() + .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?; + + if !quiet { + log::info!("{}", String::from_utf8(output.stdout)?); + } + + // TODO: improve run_command to allow outputting both, stderr and stdout + if !output.status.success() { + if output.status.code().is_some() { + let msg = String::from_utf8(output.stderr) + .map(|m| { + if m.is_empty() { + String::from("no error message") + } else { + m + } + }) + .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)")); + log::warn!("{msg}"); + } else { + bail!("terminated by signal"); + } + } + + let mut cache = crate::cache::update_cache(apt_state_file)?; + + if notify { + let mut notified = match cache.notified { + Some(notified) => notified, + None => std::collections::HashMap::new(), + }; + let mut to_notify: Vec<&APTUpdateInfo> = Vec::new(); + + for pkg in &cache.package_status { + match notified.insert(pkg.package.to_owned(), pkg.version.to_owned()) { + Some(notified_version) => { + if notified_version != pkg.version { + to_notify.push(pkg); + } + } + None => to_notify.push(pkg), + } + } + if !to_notify.is_empty() { + to_notify.sort_unstable_by_key(|k| &k.package); + send_updates_available(&to_notify)?; + } + cache.notified = Some(notified); + crate::cache::write_pkg_cache(apt_state_file, &cache)?; + } + + Ok(()) +} + +/// Get package information for a list of important product packages. +/// +/// We first list the product virtual package (i.e. `proxmox-backup`), with extra +/// information about the running kernel. +/// +/// Next is the api_server_package, with extra information abnout the running api +/// server version. +/// +/// The list of installed kernel packages follows. +/// +/// We the add an entry for all packages in package_list, even if they are +/// not installed. +pub fn get_package_versions( + product_virtual_package: &str, + api_server_package: &str, + running_api_server_version: &str, + package_list: &[&str], +) -> Result, Error> { + fn unknown_package(package: String, extra_info: Option) -> APTUpdateInfo { + APTUpdateInfo { + package, + title: "unknown".into(), + arch: "unknown".into(), + description: "unknown".into(), + version: "unknown".into(), + old_version: "unknown".into(), + origin: "unknown".into(), + priority: "unknown".into(), + section: "unknown".into(), + extra_info, + } + } + + let mut packages: Vec = Vec::new(); + + let is_kernel = + |name: &str| name.starts_with("pve-kernel-") || name.starts_with("proxmox-kernel"); + + let installed_packages = crate::cache::list_installed_apt_packages( + |filter| { + filter.installed_version == Some(filter.active_version) + && (is_kernel(filter.package) + || (filter.package == product_virtual_package) + || (filter.package == api_server_package) + || package_list.contains(&filter.package)) + }, + None, + ); + + let running_kernel = format!( + "running kernel: {}", + std::str::from_utf8(nix::sys::utsname::uname()?.release().as_bytes())?.to_owned() + ); + + if let Some(product_virtual_package_info) = installed_packages + .iter() + .find(|pkg| pkg.package == product_virtual_package) + { + let mut product_virtual_package_info = product_virtual_package_info.clone(); + product_virtual_package_info.extra_info = Some(running_kernel); + packages.push(product_virtual_package_info); + } else { + packages.push(unknown_package( + product_virtual_package.into(), + Some(running_kernel), + )); + } + + if let Some(api_server_package_info) = installed_packages + .iter() + .find(|pkg| pkg.package == api_server_package) + { + let mut api_server_package_info = api_server_package_info.clone(); + api_server_package_info.extra_info = Some(running_api_server_version.into()); + packages.push(api_server_package_info); + } else { + packages.push(unknown_package( + api_server_package.into(), + Some(running_api_server_version.into()), + )); + } + + let mut kernel_pkgs: Vec = installed_packages + .iter() + .filter(|pkg| is_kernel(&pkg.package)) + .cloned() + .collect(); + + crate::cache::sort_package_list(&mut kernel_pkgs); + + packages.append(&mut kernel_pkgs); + + // add entry for all packages we're interested in, even if not installed + for pkg in package_list.iter() { + if *pkg == product_virtual_package || *pkg == api_server_package { + continue; + } + match installed_packages.iter().find(|item| &item.package == pkg) { + Some(apt_pkg) => packages.push(apt_pkg.to_owned()), + None => packages.push(unknown_package(pkg.to_string(), None)), + } + } + + Ok(packages) +} diff --git a/proxmox-apt/src/lib.rs b/proxmox-apt/src/lib.rs index 60bf1d2a..f25ac90b 100644 --- a/proxmox-apt/src/lib.rs +++ b/proxmox-apt/src/lib.rs @@ -1,5 +1,14 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +mod api; +pub use api::{add_repository_handle, change_repository, get_changelog, list_repositories}; + +#[cfg(feature = "cache")] +pub mod cache; +#[cfg(feature = "cache")] +mod cache_api; +#[cfg(feature = "cache")] +pub use cache_api::{get_package_versions, list_available_apt_update, update_database}; pub mod config; pub mod deb822; pub mod repositories; diff --git a/proxmox-apt/tests/repositories.rs b/proxmox-apt/tests/repositories.rs index 228ef696..e4a94525 100644 --- a/proxmox-apt/tests/repositories.rs +++ b/proxmox-apt/tests/repositories.rs @@ -5,11 +5,13 @@ use anyhow::{bail, format_err, Error}; use proxmox_apt::config::APTConfig; use proxmox_apt::repositories::{ - check_repositories, get_current_release_codename, standard_repositories, APTRepositoryFile, - APTRepositoryHandle, APTRepositoryInfo, APTStandardRepository, DebianCodename, + check_repositories, get_current_release_codename, standard_repositories, DebianCodename, }; use proxmox_apt::repositories::{ - APTRepositoryFileImpl, APTRepositoryHandleImpl, APTRepositoryImpl, APTStandardRepositoryImpl, + APTRepositoryFileImpl, APTRepositoryImpl, APTStandardRepositoryImpl, +}; +use proxmox_apt_api_types::{ + APTRepositoryFile, APTRepositoryHandle, APTRepositoryInfo, APTStandardRepository, }; fn create_clean_directory(path: &PathBuf) -> Result<(), Error> { -- 2.39.2 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel