diff --git a/Cargo.toml b/Cargo.toml index 355217eb..b0881319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ name = "proxmox_backup" path = "src/lib.rs" [dependencies] +apt-pkg-native = "0.3.1" # custom patched version base64 = "0.12" bitflags = "1.2.1" bytes = "0.5" diff --git a/src/api2/node.rs b/src/api2/node.rs index e67cab4e..e8800e4c 100644 --- a/src/api2/node.rs +++ b/src/api2/node.rs @@ -12,8 +12,10 @@ mod status; mod subscription; pub(crate) mod rrd; pub mod disks; +mod apt; pub const SUBDIRS: SubdirMap = &[ + ("apt", &apt::ROUTER), ("disks", &disks::ROUTER), ("dns", &dns::ROUTER), ("journal", &journal::ROUTER), diff --git a/src/api2/node/apt.rs b/src/api2/node/apt.rs new file mode 100644 index 00000000..55d360fe --- /dev/null +++ b/src/api2/node/apt.rs @@ -0,0 +1,211 @@ +use apt_pkg_native::Cache; +use anyhow::{Error, bail}; +use serde_json::{json, Value}; + +use proxmox::{list_subdirs_api_method, const_regex}; +use proxmox::api::{api, Router, Permission, SubdirMap}; + +use crate::config::acl::PRIV_SYS_AUDIT; +use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA}; + +const_regex! { + VERSION_EPOCH_REGEX = r"^\d+:"; + FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$"; +} + +// FIXME: Replace with call to 'apt changelog --print-uris'. Currently +// not possible as our packages do not have a URI set in their Release file +fn get_changelog_url( + package: &str, + filename: &str, + source_pkg: &str, + version: &str, + source_version: &str, + origin: &str, + component: &str, +) -> Result { + if origin == "" { + bail!("no origin available for package {}", package); + } + + if origin == "Debian" { + let source_version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(source_version, ""); + + let prefix = if source_pkg.starts_with("lib") { + source_pkg.get(0..4) + } else { + source_pkg.get(0..1) + }; + + let prefix = match prefix { + Some(p) => p, + None => bail!("cannot get starting characters of package name '{}'", package) + }; + + // note: security updates seem to not always upload a changelog for + // their package version, so this only works *most* of the time + return Ok(format!("https://metadata.ftp-master.debian.org/changelogs/main/{}/{}/{}_{}_changelog", + prefix, source_pkg, source_pkg, source_version)); + + } else if origin == "Proxmox" { + let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, ""); + + let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) { + Some(captures) => { + let base_capture = captures.get(1); + match base_capture { + Some(base_underscore) => base_underscore.as_str().replace("_", "/"), + None => bail!("incompatible filename, cannot find regex group") + } + }, + None => bail!("incompatible filename, doesn't match regex") + }; + + return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog", + base, package, version)); + } + + bail!("unknown origin ({}) or component ({})", origin, component) +} + +fn list_installed_apt_packages bool>(filter: F) + -> Vec { + + let mut ret = Vec::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 = cache.iter(); + + loop { + let view = match cache_iter.next() { + Some(view) => view, + None => break + }; + + let current_version = match view.current_version() { + Some(vers) => vers, + None => continue + }; + let candidate_version = match view.candidate_version() { + Some(vers) => vers, + // if there's no candidate (i.e. no update) get info of currently + // installed version instead + None => current_version.clone() + }; + + let package = view.name(); + if filter(&package, ¤t_version, &candidate_version) { + let mut origin_res = "unknown".to_owned(); + let mut section_res = "unknown".to_owned(); + let mut priority_res = "unknown".to_owned(); + let mut change_log_url = "".to_owned(); + let mut short_desc = package.clone(); + let mut long_desc = "".to_owned(); + + // get additional information via nested APT 'iterators' + let mut view_iter = view.versions(); + while let Some(ver) = view_iter.next() { + if ver.version() == candidate_version { + 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 + 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; + } + + let filename = pkg_file.file_name(); + let source_pkg = ver.source_package(); + let source_ver = ver.source_version(); + let component = pkg_file.component(); + + // build changelog URL from gathered information + // ignore errors, use empty changelog instead + let url = get_changelog_url(&package, &filename, &source_pkg, + &candidate_version, &source_ver, &origin_res, &component); + if let Ok(url) = url { + change_log_url = url; + } + } + } + + break; + } + } + + let info = APTUpdateInfo { + package, + title: short_desc, + arch: view.arch(), + description: long_desc, + change_log_url, + origin: origin_res, + version: candidate_version, + old_version: current_version, + priority: priority_res, + section: section_res, + }; + ret.push(info); + } + } + + return ret; +} + +#[api( + input: { + properties: { + node: { + schema: NODE_SCHEMA, + }, + }, + }, + returns: { + description: "A list of packages with available updates.", + type: Array, + items: { type: APTUpdateInfo }, + }, + access: { + permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false), + }, +)] +/// List available APT updates +fn apt_update_available(_param: Value) -> Result { + let ret = list_installed_apt_packages(|_pkg, cur_ver, can_ver| cur_ver != can_ver); + Ok(json!(ret)) +} + +const SUBDIRS: SubdirMap = &[ + ("update", &Router::new().get(&API_METHOD_APT_UPDATE_AVAILABLE)), +]; + +pub const ROUTER: Router = Router::new() + .get(&list_subdirs_api_method!(SUBDIRS)) + .subdirs(SUBDIRS); diff --git a/src/api2/types.rs b/src/api2/types.rs index 0d0fab3b..f6972c8b 100644 --- a/src/api2/types.rs +++ b/src/api2/types.rs @@ -962,3 +962,30 @@ pub enum RRDTimeFrameResolution { /// 1 week => last 490 days Year = 60*10080, } + +#[api()] +#[derive(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, + /// URL under which the package's changelog can be retrieved + pub change_log_url: String, +}