use new apt/apt-api-types crate
This commit is contained in:
parent
da2002eadb
commit
55e7bef4d2
@ -53,7 +53,9 @@ path = "src/lib.rs"
|
||||
|
||||
[workspace.dependencies]
|
||||
# proxmox workspace
|
||||
proxmox-apt = "0.10.5"
|
||||
proxmox-config-digest = "0.1.0"
|
||||
proxmox-apt = { version = "0.11", features = [ "cache" ] }
|
||||
proxmox-apt-api-types = "1.0"
|
||||
proxmox-async = "0.4"
|
||||
proxmox-auth-api = "0.4"
|
||||
proxmox-borrow = "1"
|
||||
@ -203,9 +205,11 @@ zstd.workspace = true
|
||||
|
||||
# proxmox workspace
|
||||
proxmox-apt.workspace = true
|
||||
proxmox-apt-api-types.workspace = true
|
||||
proxmox-async.workspace = true
|
||||
proxmox-auth-api = { workspace = true, features = [ "api", "pam-authenticator" ] }
|
||||
proxmox-compression.workspace = true
|
||||
proxmox-config-digest.workspace = true
|
||||
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
|
||||
proxmox-human-byte.workspace = true
|
||||
proxmox-io.workspace = true
|
||||
|
@ -16,6 +16,7 @@ serde.workspace = true
|
||||
serde_plain.workspace = true
|
||||
|
||||
proxmox-auth-api = { workspace = true, features = [ "api-types" ] }
|
||||
proxmox-apt-api-types.workspace = true
|
||||
proxmox-human-byte.workspace = true
|
||||
proxmox-lang.workspace=true
|
||||
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
|
||||
|
@ -52,6 +52,13 @@ pub use proxmox_schema::api_types::{SYSTEMD_DATETIME_FORMAT, TIME_ZONE_SCHEMA};
|
||||
|
||||
use proxmox_schema::api_types::{DNS_NAME_STR, IPRE_BRACKET_STR};
|
||||
|
||||
// re-export APT API types
|
||||
pub use proxmox_apt_api_types::{
|
||||
APTChangeRepositoryOptions, APTGetChangelogOptions, APTRepositoriesResult, APTRepositoryFile,
|
||||
APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo, APTStandardRepository,
|
||||
APTUpdateInfo, APTUpdateOptions,
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub const BACKUP_ID_RE: &str = r"[A-Za-z0-9_][A-Za-z0-9._\-]*";
|
||||
|
||||
@ -249,34 +256,6 @@ pub const PASSWORD_HINT_SCHEMA: Schema = StringSchema::new("Password hint.")
|
||||
.max_length(64)
|
||||
.schema();
|
||||
|
||||
#[api()]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
/// Describes a package for which an update is available.
|
||||
pub struct APTUpdateInfo {
|
||||
/// Package name
|
||||
pub package: String,
|
||||
/// Package title
|
||||
pub title: String,
|
||||
/// Package architecture
|
||||
pub arch: String,
|
||||
/// Human readable package description
|
||||
pub description: String,
|
||||
/// New version to be updated to
|
||||
pub version: String,
|
||||
/// Old version currently installed
|
||||
pub old_version: String,
|
||||
/// Package origin
|
||||
pub origin: String,
|
||||
/// Package priority in human-readable form
|
||||
pub priority: String,
|
||||
/// Package section
|
||||
pub section: String,
|
||||
/// Custom extra field for additional package information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
}
|
||||
|
||||
#[api()]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
@ -98,6 +98,8 @@ pub const PROXMOX_BACKUP_KERNEL_FN: &str =
|
||||
|
||||
pub const PROXMOX_BACKUP_SUBSCRIPTION_FN: &str = configdir!("/subscription");
|
||||
|
||||
pub const APT_PKG_STATE_FN: &str = concat!(PROXMOX_BACKUP_STATE_DIR_M!(), "/pkg-state.json");
|
||||
|
||||
/// Prepend configuration directory to a file name
|
||||
///
|
||||
/// This is a simply way to get the full path for configuration files.
|
||||
|
@ -1,26 +1,21 @@
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde_json::{json, Value};
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use anyhow::{bail, Error};
|
||||
|
||||
use proxmox_config_digest::ConfigDigest;
|
||||
use proxmox_router::{
|
||||
list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
|
||||
};
|
||||
use proxmox_schema::api;
|
||||
use proxmox_sys::fs::{replace_file, CreateOptions};
|
||||
|
||||
use proxmox_apt::repositories::{
|
||||
APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
|
||||
APTStandardRepository,
|
||||
use proxmox_apt_api_types::{
|
||||
APTChangeRepositoryOptions, APTGetChangelogOptions, APTRepositoriesResult, APTRepositoryHandle,
|
||||
APTUpdateInfo, APTUpdateOptions,
|
||||
};
|
||||
use proxmox_http::ProxyConfig;
|
||||
|
||||
use pbs_api_types::{
|
||||
APTUpdateInfo, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||
UPID_SCHEMA,
|
||||
};
|
||||
use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, UPID_SCHEMA};
|
||||
|
||||
use crate::config::node;
|
||||
use crate::tools::apt;
|
||||
use proxmox_rest_server::WorkerTask;
|
||||
|
||||
#[api(
|
||||
@ -44,16 +39,8 @@ use proxmox_rest_server::WorkerTask;
|
||||
},
|
||||
)]
|
||||
/// List available APT updates
|
||||
fn apt_update_available(_param: Value) -> Result<Value, Error> {
|
||||
if let Ok(false) = apt::pkg_cache_expired() {
|
||||
if let Ok(Some(cache)) = apt::read_pkg_state() {
|
||||
return Ok(json!(cache.package_status));
|
||||
}
|
||||
}
|
||||
|
||||
let cache = apt::update_cache()?;
|
||||
|
||||
Ok(json!(cache.package_status))
|
||||
pub fn apt_update_available() -> Result<Vec<APTUpdateInfo>, Error> {
|
||||
proxmox_apt::list_available_apt_update(pbs_buildcfg::APT_PKG_STATE_FN)
|
||||
}
|
||||
|
||||
pub fn update_apt_proxy_config(proxy_config: Option<&ProxyConfig>) -> Result<(), Error> {
|
||||
@ -83,45 +70,6 @@ fn read_and_update_proxy_config() -> Result<Option<ProxyConfig>, Error> {
|
||||
Ok(proxy_config)
|
||||
}
|
||||
|
||||
fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
|
||||
if !quiet {
|
||||
worker.log_message("starting apt-get update")
|
||||
}
|
||||
|
||||
read_and_update_proxy_config()?;
|
||||
|
||||
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 {
|
||||
worker.log_message(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)"));
|
||||
worker.log_warning(msg);
|
||||
} else {
|
||||
bail!("terminated by signal");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
protected: true,
|
||||
input: {
|
||||
@ -129,19 +77,10 @@ fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
|
||||
node: {
|
||||
schema: NODE_SCHEMA,
|
||||
},
|
||||
notify: {
|
||||
type: bool,
|
||||
description: r#"Send notification mail about new package updates available to the
|
||||
email address configured for 'root@pam')."#,
|
||||
default: false,
|
||||
optional: true,
|
||||
},
|
||||
quiet: {
|
||||
description: "Only produces output suitable for logging, omitting progress indicators.",
|
||||
type: bool,
|
||||
default: false,
|
||||
optional: true,
|
||||
},
|
||||
options: {
|
||||
type: APTUpdateOptions,
|
||||
flatten: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
returns: {
|
||||
@ -153,40 +92,22 @@ fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
|
||||
)]
|
||||
/// Update the APT database
|
||||
pub fn apt_update_database(
|
||||
notify: bool,
|
||||
quiet: bool,
|
||||
options: APTUpdateOptions,
|
||||
rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<String, Error> {
|
||||
let auth_id = rpcenv.get_auth_id().unwrap();
|
||||
let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
|
||||
|
||||
let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
|
||||
do_apt_update(&worker, quiet)?;
|
||||
|
||||
let mut cache = apt::update_cache()?;
|
||||
|
||||
if notify {
|
||||
let mut notified = cache.notified.unwrap_or_default();
|
||||
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);
|
||||
crate::server::send_updates_available(&to_notify)?;
|
||||
}
|
||||
cache.notified = Some(notified);
|
||||
apt::write_pkg_cache(&cache)?;
|
||||
}
|
||||
|
||||
let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |_worker| {
|
||||
read_and_update_proxy_config()?;
|
||||
proxmox_apt::update_database(
|
||||
pbs_buildcfg::APT_PKG_STATE_FN,
|
||||
&options,
|
||||
|updates: &[&APTUpdateInfo]| {
|
||||
crate::server::send_updates_available(updates)?;
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@ -200,14 +121,9 @@ pub fn apt_update_database(
|
||||
node: {
|
||||
schema: NODE_SCHEMA,
|
||||
},
|
||||
name: {
|
||||
description: "Package name to get changelog of.",
|
||||
type: String,
|
||||
},
|
||||
version: {
|
||||
description: "Package version to get changelog of. Omit to use candidate version.",
|
||||
type: String,
|
||||
optional: true,
|
||||
options: {
|
||||
type: APTGetChangelogOptions,
|
||||
flatten: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -219,17 +135,8 @@ pub fn apt_update_database(
|
||||
},
|
||||
)]
|
||||
/// Retrieve the changelog of the specified package.
|
||||
fn apt_get_changelog(name: String, version: Option<String>) -> Result<Value, Error> {
|
||||
let mut command = std::process::Command::new("apt-get");
|
||||
command.arg("changelog");
|
||||
command.arg("-qq"); // don't display download progress
|
||||
if let Some(ver) = version {
|
||||
command.arg(format!("{name}={ver}"));
|
||||
} else {
|
||||
command.arg(name);
|
||||
}
|
||||
let output = proxmox_sys::command::run_command(command, None)?;
|
||||
Ok(json!(output))
|
||||
fn apt_get_changelog(options: APTGetChangelogOptions) -> Result<String, Error> {
|
||||
proxmox_apt::get_changelog(&options)
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -256,10 +163,8 @@ pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
|
||||
const PACKAGES: &[&str] = &[
|
||||
"ifupdown2",
|
||||
"libjs-extjs",
|
||||
"proxmox-backup",
|
||||
"proxmox-backup-docs",
|
||||
"proxmox-backup-client",
|
||||
"proxmox-backup-server",
|
||||
"proxmox-mail-forward",
|
||||
"proxmox-mini-journalreader",
|
||||
"proxmox-offline-mirror-helper",
|
||||
@ -269,96 +174,16 @@ pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
|
||||
"zfsutils-linux",
|
||||
];
|
||||
|
||||
fn unknown_package(package: String, extra_info: Option<String>) -> 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 is_kernel =
|
||||
|name: &str| name.starts_with("pve-kernel-") || name.starts_with("proxmox-kernel");
|
||||
|
||||
let mut packages: Vec<APTUpdateInfo> = Vec::new();
|
||||
let pbs_packages = apt::list_installed_apt_packages(
|
||||
|filter| {
|
||||
filter.installed_version == Some(filter.active_version)
|
||||
&& (is_kernel(filter.package) || PACKAGES.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(proxmox_backup) = pbs_packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.package == "proxmox-backup")
|
||||
{
|
||||
let mut proxmox_backup = proxmox_backup.clone();
|
||||
proxmox_backup.extra_info = Some(running_kernel);
|
||||
packages.push(proxmox_backup);
|
||||
} else {
|
||||
packages.push(unknown_package(
|
||||
"proxmox-backup".into(),
|
||||
Some(running_kernel),
|
||||
));
|
||||
}
|
||||
|
||||
let version = pbs_buildcfg::PROXMOX_PKG_VERSION;
|
||||
let release = pbs_buildcfg::PROXMOX_PKG_RELEASE;
|
||||
let daemon_version_info = Some(format!("running version: {}.{}", version, release));
|
||||
if let Some(pkg) = pbs_packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.package == "proxmox-backup-server")
|
||||
{
|
||||
let mut pkg = pkg.clone();
|
||||
pkg.extra_info = daemon_version_info;
|
||||
packages.push(pkg);
|
||||
} else {
|
||||
packages.push(unknown_package(
|
||||
"proxmox-backup".into(),
|
||||
daemon_version_info,
|
||||
));
|
||||
}
|
||||
let running_daemon_version = format!("running version: {version}.{release}");
|
||||
|
||||
let mut kernel_pkgs: Vec<APTUpdateInfo> = pbs_packages
|
||||
.iter()
|
||||
.filter(|pkg| is_kernel(&pkg.package))
|
||||
.cloned()
|
||||
.collect();
|
||||
// make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
|
||||
{
|
||||
let cache = apt_pkg_native::Cache::get_singleton();
|
||||
kernel_pkgs.sort_by(|left, right| {
|
||||
cache
|
||||
.compare_versions(&left.old_version, &right.old_version)
|
||||
.reverse()
|
||||
});
|
||||
}
|
||||
packages.append(&mut kernel_pkgs);
|
||||
|
||||
// add entry for all packages we're interested in, even if not installed
|
||||
for pkg in PACKAGES.iter() {
|
||||
if pkg == &"proxmox-backup" || pkg == &"proxmox-backup-server" {
|
||||
continue;
|
||||
}
|
||||
match pbs_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)
|
||||
proxmox_apt::get_package_versions(
|
||||
"proxmox-backup",
|
||||
"proxmox-backup-server",
|
||||
&running_daemon_version,
|
||||
&PACKAGES,
|
||||
)
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -370,61 +195,15 @@ pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
|
||||
},
|
||||
},
|
||||
returns: {
|
||||
type: Object,
|
||||
description: "Result from parsing the APT repository files in /etc/apt/.",
|
||||
properties: {
|
||||
files: {
|
||||
description: "List of parsed repository files.",
|
||||
type: Array,
|
||||
items: {
|
||||
type: APTRepositoryFile,
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
description: "List of problematic files.",
|
||||
type: Array,
|
||||
items: {
|
||||
type: APTRepositoryFileError,
|
||||
},
|
||||
},
|
||||
digest: {
|
||||
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||
},
|
||||
infos: {
|
||||
description: "List of additional information/warnings about the repositories.",
|
||||
items: {
|
||||
type: APTRepositoryInfo,
|
||||
},
|
||||
},
|
||||
"standard-repos": {
|
||||
description: "List of standard repositories and their configuration status.",
|
||||
items: {
|
||||
type: APTStandardRepository,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: APTRepositoriesResult,
|
||||
},
|
||||
access: {
|
||||
permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
|
||||
},
|
||||
)]
|
||||
/// Get APT repository information.
|
||||
pub fn get_repositories() -> Result<Value, Error> {
|
||||
let (files, errors, digest) = proxmox_apt::repositories::repositories()?;
|
||||
let digest = hex::encode(digest);
|
||||
|
||||
let suite = proxmox_apt::repositories::get_current_release_codename()?;
|
||||
|
||||
let infos = proxmox_apt::repositories::check_repositories(&files, suite);
|
||||
let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pbs", suite);
|
||||
|
||||
Ok(json!({
|
||||
"files": files,
|
||||
"errors": errors,
|
||||
"digest": digest,
|
||||
"infos": infos,
|
||||
"standard-repos": standard_repos,
|
||||
}))
|
||||
pub fn get_repositories() -> Result<APTRepositoriesResult, Error> {
|
||||
proxmox_apt::list_repositories("pbs")
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -437,7 +216,7 @@ pub fn get_repositories() -> Result<Value, Error> {
|
||||
type: APTRepositoryHandle,
|
||||
},
|
||||
digest: {
|
||||
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||
type: ConfigDigest,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
@ -451,61 +230,11 @@ pub fn get_repositories() -> Result<Value, Error> {
|
||||
/// 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: APTRepositoryHandle, digest: Option<String>) -> Result<(), Error> {
|
||||
let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
|
||||
|
||||
let suite = proxmox_apt::repositories::get_current_release_codename()?;
|
||||
|
||||
if let Some(expected_digest) = digest {
|
||||
let current_digest = hex::encode(current_digest);
|
||||
crate::tools::assert_if_modified(&expected_digest, ¤t_digest)?;
|
||||
}
|
||||
|
||||
// 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) = proxmox_apt::repositories::get_standard_repository(handle, "pbs", 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(())
|
||||
pub fn add_repository(
|
||||
handle: APTRepositoryHandle,
|
||||
digest: Option<ConfigDigest>,
|
||||
) -> Result<(), Error> {
|
||||
proxmox_apt::add_repository_handle("pbs", handle, digest)
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -522,13 +251,12 @@ pub fn add_repository(handle: APTRepositoryHandle, digest: Option<String>) -> Re
|
||||
description: "Index within the file (starting from 0).",
|
||||
type: usize,
|
||||
},
|
||||
enabled: {
|
||||
description: "Whether the repository should be enabled or not.",
|
||||
type: bool,
|
||||
optional: true,
|
||||
options: {
|
||||
type: APTChangeRepositoryOptions,
|
||||
flatten: true,
|
||||
},
|
||||
digest: {
|
||||
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||
type: ConfigDigest,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
@ -544,38 +272,10 @@ pub fn add_repository(handle: APTRepositoryHandle, digest: Option<String>) -> Re
|
||||
pub fn change_repository(
|
||||
path: String,
|
||||
index: usize,
|
||||
enabled: Option<bool>,
|
||||
digest: Option<String>,
|
||||
options: APTChangeRepositoryOptions,
|
||||
digest: Option<ConfigDigest>,
|
||||
) -> Result<(), Error> {
|
||||
let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
|
||||
|
||||
if let Some(expected_digest) = digest {
|
||||
let current_digest = hex::encode(current_digest);
|
||||
crate::tools::assert_if_modified(&expected_digest, ¤t_digest)?;
|
||||
}
|
||||
|
||||
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_ref() == Some(&path))
|
||||
{
|
||||
if let Some(repo) = file.repositories.get_mut(index) {
|
||||
if let Some(enabled) = enabled {
|
||||
repo.set_enabled(enabled);
|
||||
}
|
||||
|
||||
file.write()?;
|
||||
} else {
|
||||
bail!("invalid index - {}", index);
|
||||
}
|
||||
} else {
|
||||
bail!("invalid path - {}", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
proxmox_apt::change_repository(&path, index, &options, digest)
|
||||
}
|
||||
|
||||
const SUBDIRS: SubdirMap = &[
|
||||
|
@ -5,7 +5,8 @@ use anyhow::{format_err, Error};
|
||||
use regex::Regex;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
|
||||
use proxmox_apt::repositories::{self, APTRepositoryFile, APTRepositoryPackageType};
|
||||
use proxmox_apt::repositories;
|
||||
use proxmox_apt_api_types::{APTRepositoryFile, APTRepositoryPackageType};
|
||||
use proxmox_backup::api2::node::apt;
|
||||
|
||||
const OLD_SUITE: &str = "bullseye";
|
||||
@ -50,19 +51,18 @@ impl Checker {
|
||||
fn check_upgradable_packages(&mut self) -> Result<(), Error> {
|
||||
self.output.log_info("Checking for package updates..")?;
|
||||
|
||||
let result = Self::get_upgradable_packages();
|
||||
let result = apt::apt_update_available();
|
||||
match result {
|
||||
Err(err) => {
|
||||
self.output.log_warn(format!("{err}"))?;
|
||||
self.output
|
||||
.log_fail("unable to retrieve list of package updates!")?;
|
||||
}
|
||||
Ok(cache) => {
|
||||
if cache.package_status.is_empty() {
|
||||
Ok(package_status) => {
|
||||
if package_status.is_empty() {
|
||||
self.output.log_pass("all packages up-to-date")?;
|
||||
} else {
|
||||
let pkgs = cache
|
||||
.package_status
|
||||
let pkgs = package_status
|
||||
.iter()
|
||||
.map(|pkg| pkg.package.clone())
|
||||
.collect::<Vec<String>>()
|
||||
@ -452,20 +452,6 @@ impl Checker {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_upgradable_packages() -> Result<proxmox_backup::tools::apt::PkgState, Error> {
|
||||
let cache = if let Ok(false) = proxmox_backup::tools::apt::pkg_cache_expired() {
|
||||
if let Ok(Some(cache)) = proxmox_backup::tools::apt::read_pkg_state() {
|
||||
cache
|
||||
} else {
|
||||
proxmox_backup::tools::apt::update_cache()?
|
||||
}
|
||||
} else {
|
||||
proxmox_backup::tools::apt::update_cache()?
|
||||
};
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
293
src/tools/apt.rs
293
src/tools/apt.rs
@ -1,293 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
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 pbs_api_types::APTUpdateInfo;
|
||||
use pbs_buildcfg::PROXMOX_BACKUP_STATE_DIR_M;
|
||||
|
||||
const APT_PKG_STATE_FN: &str = concat!(PROXMOX_BACKUP_STATE_DIR_M!(), "/pkg-state.json");
|
||||
|
||||
#[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<HashMap<String, String>>,
|
||||
/// A list of pending updates
|
||||
pub package_status: Vec<APTUpdateInfo>,
|
||||
}
|
||||
|
||||
pub fn write_pkg_cache(state: &PkgState) -> Result<(), Error> {
|
||||
let serialized_state = serde_json::to_string(state)?;
|
||||
|
||||
replace_file(
|
||||
APT_PKG_STATE_FN,
|
||||
serialized_state.as_bytes(),
|
||||
CreateOptions::new(),
|
||||
false,
|
||||
)
|
||||
.map_err(|err| format_err!("Error writing package cache - {}", err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_pkg_state() -> Result<Option<PkgState>, Error> {
|
||||
let serialized_state = match file_read_optional_string(APT_PKG_STATE_FN) {
|
||||
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() -> Result<bool, Error> {
|
||||
if let Ok(pbs_cache) = std::fs::metadata(APT_PKG_STATE_FN) {
|
||||
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() -> Result<PkgState, Error> {
|
||||
// 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() {
|
||||
Ok(Some(mut cache)) => {
|
||||
cache.package_status = all_upgradeable;
|
||||
cache
|
||||
}
|
||||
_ => PkgState {
|
||||
notified: None,
|
||||
package_status: all_upgradeable,
|
||||
},
|
||||
};
|
||||
write_pkg_cache(&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<F: Fn(FilterData) -> bool>(
|
||||
filter: F,
|
||||
only_versions_for: Option<&str>,
|
||||
) -> Vec<APTUpdateInfo> {
|
||||
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<String>>,
|
||||
) -> Option<APTUpdateInfo>
|
||||
where
|
||||
F: Fn(FilterData) -> bool,
|
||||
V: std::ops::Deref<Target = apt_pkg_native::sane::PkgView<'a>>,
|
||||
{
|
||||
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
|
||||
}
|
@ -6,7 +6,6 @@ use anyhow::{bail, Error};
|
||||
|
||||
use proxmox_http::{client::Client, HttpOptions, ProxyConfig};
|
||||
|
||||
pub mod apt;
|
||||
pub mod config;
|
||||
pub mod disks;
|
||||
pub mod fs;
|
||||
|
Loading…
x
Reference in New Issue
Block a user