From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 8C32C64EA3 for ; Tue, 21 Jul 2020 13:41:17 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 895EC17E12 for ; Tue, 21 Jul 2020 13:41:17 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 0FD2C17E0A for ; Tue, 21 Jul 2020 13:41:16 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id D2D9B432D1 for ; Tue, 21 Jul 2020 13:41:15 +0200 (CEST) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Tue, 21 Jul 2020 13:41:07 +0200 Message-Id: <20200721114108.11603-2-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200721114108.11603-1-s.reiter@proxmox.com> References: <20200721114108.11603-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.021 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [lib.rs, types.rs, apt.rs, proxmox.com, node.rs] Subject: [pbs-devel] [PATCH v2 backup 2/2] add .../apt/update API call 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: , X-List-Received-Date: Tue, 21 Jul 2020 11:41:17 -0000 Depends on patched apt-pkg-native-rs. Changelog-URL detection is inspired by PVE perl code for now, though marked with fixme to use 'apt changelog' later on, if/when our repos have APT-compatible changelogs set up. list_installed_apt_packages iterates all packages and creates an APTUpdateInfo with detailed information for every package matched by the given filter Fn. Sadly, libapt-pkg has some questionable design choices regarding their use of 'iterators', which means quite a bit of nesting... Signed-off-by: Stefan Reiter --- v2: * Include feedback from Fabian: ** Update Cargo.toml with now packaged apt-pkg-native (still no update on my PR) ** Use proxmox.com changelog logic for all origin=Proxmox packages, not just pbs ** Change serde naming to PascalCase for APTUpdateInfo ** Add FIXME to changelog detection ** Update comments Cargo.toml | 1 + src/api2/node.rs | 2 + src/api2/node/apt.rs | 211 +++++++++++++++++++++++++++++++++++++++++++ src/api2/types.rs | 27 ++++++ 4 files changed, 241 insertions(+) create mode 100644 src/api2/node/apt.rs 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, +} -- 2.20.1