-use apt_pkg_native::Cache;
-use anyhow::{Error, bail};
+use anyhow::{Error, bail, format_err};
use serde_json::{json, Value};
+use std::collections::HashMap;
-use proxmox::{list_subdirs_api_method, const_regex};
+use proxmox::list_subdirs_api_method;
use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
use proxmox::api::router::{Router, SubdirMap};
+use proxmox::tools::fs::{replace_file, CreateOptions};
-use crate::server::WorkerTask;
+use proxmox_http::ProxyConfig;
+use crate::config::node;
+use crate::server::WorkerTask;
+use crate::tools::{
+ apt,
+ pbs_simple_http,
+ subscription,
+};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
-use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
+use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
-const_regex! {
- VERSION_EPOCH_REGEX = r"^\d+:";
- FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";
-}
+#[api(
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ description: "A list of packages with available updates.",
+ type: Array,
+ items: {
+ type: APTUpdateInfo
+ },
+ },
+ protected: true,
+ access: {
+ permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// List available APT updates
+fn apt_update_available(_param: Value) -> Result<Value, Error> {
-// FIXME: Replace with call to 'apt changelog <pkg> --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<String, Error> {
- if origin == "" {
- bail!("no origin available for package {}", package);
+ if let Ok(false) = apt::pkg_cache_expired() {
+ if let Ok(Some(cache)) = apt::read_pkg_state() {
+ return Ok(json!(cache.package_status));
+ }
}
- 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)
- };
+ let cache = apt::update_cache()?;
- // 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));
+ Ok(json!(cache.package_status))
+}
- } else if origin == "Proxmox" {
- let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, "");
+pub fn update_apt_proxy_config(proxy_config: Option<&ProxyConfig>) -> Result<(), Error> {
- 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")
- };
+ const PROXY_CFG_FN: &str = "/etc/apt/apt.conf.d/76pveproxy"; // use same file as PVE
- return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
- base, package, version));
+ if let Some(proxy_config) = proxy_config {
+ let proxy = proxy_config.to_proxy_string()?;
+ let data = format!("Acquire::http::Proxy \"{}\";\n", proxy);
+ replace_file(PROXY_CFG_FN, data.as_bytes(), CreateOptions::new())
+ } else {
+ match std::fs::remove_file(PROXY_CFG_FN) {
+ Ok(()) => Ok(()),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
+ Err(err) => bail!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN, err),
+ }
}
-
- bail!("unknown origin ({}) or component ({})", origin, component)
}
-fn list_installed_apt_packages<F: Fn(&str, &str, &str) -> bool>(filter: F)
- -> Vec<APTUpdateInfo> {
+fn read_and_update_proxy_config() -> Result<Option<ProxyConfig>, Error> {
+ let proxy_config = if let Ok((node_config, _digest)) = node::config() {
+ node_config.http_proxy()
+ } else {
+ None
+ };
+ update_apt_proxy_config(proxy_config.as_ref())?;
- 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();
+ Ok(proxy_config)
+}
- let mut cache_iter = cache.iter();
+fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
+ if !quiet { worker.log("starting apt-get update") }
- loop {
- let view = match cache_iter.next() {
- Some(view) => view,
- None => break
- };
+ read_and_update_proxy_config()?;
- 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;
- }
+ let mut command = std::process::Command::new("apt-get");
+ command.arg("update");
- // 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;
- }
- }
- }
+ // 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))?;
- break;
- }
- }
+ if !quiet {
+ worker.log(String::from_utf8(output.stdout)?);
+ }
- 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);
+ // 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.warn(msg);
+ } else {
+ bail!("terminated by signal");
}
}
-
- return ret;
+ Ok(())
}
#[api(
+ protected: true,
input: {
properties: {
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,
+ },
},
},
returns: {
- description: "A list of packages with available updates.",
- type: Array,
- items: { type: APTUpdateInfo },
+ schema: UPID_SCHEMA,
},
access: {
- permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
+ permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
},
)]
-/// List available APT updates
-fn apt_update_available(_param: Value) -> Result<Value, Error> {
- let ret = list_installed_apt_packages(|_pkg, cur_ver, can_ver| cur_ver != can_ver);
- Ok(json!(ret))
+/// Update the APT database
+pub fn apt_update_database(
+ notify: bool,
+ quiet: bool,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<String, Error> {
+
+ let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+ 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 = 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);
+ crate::server::send_updates_available(&to_notify)?;
+ }
+ cache.notified = Some(notified);
+ apt::write_pkg_cache(&cache)?;
+ }
+
+ Ok(())
+ })?;
+
+ Ok(upid_str)
}
#[api(
node: {
schema: NODE_SCHEMA,
},
- quiet: {
- description: "Only produces output suitable for logging, omitting progress indicators.",
- type: bool,
- default: false,
+ 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,
},
},
permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
},
)]
-/// Update the APT database
-pub fn apt_update_database(
- quiet: Option<bool>,
- rpcenv: &mut dyn RpcEnvironment,
-) -> Result<String, Error> {
+/// Retrieve the changelog of the specified package.
+fn apt_get_changelog(
+ param: Value,
+) -> Result<Value, Error> {
+
+ let name = crate::tools::required_string_param(¶m, "name")?.to_owned();
+ let version = param["version"].as_str();
+
+ let pkg_info = apt::list_installed_apt_packages(|data| {
+ match version {
+ Some(version) => version == data.active_version,
+ None => data.active_version == data.candidate_version
+ }
+ }, Some(&name));
- let username = rpcenv.get_user().unwrap();
- let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
- let quiet = quiet.unwrap_or(false);
+ if pkg_info.is_empty() {
+ bail!("Package '{}' not found", name);
+ }
- let upid_str = WorkerTask::new_thread("aptupdate", None, &username.clone(), to_stdout, move |worker| {
- if !quiet { worker.log("starting apt-get update") }
+ let proxy_config = read_and_update_proxy_config()?;
+ let mut client = pbs_simple_http(proxy_config);
- // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE
+ let changelog_url = &pkg_info[0].change_log_url;
+ // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
+ if changelog_url.starts_with("http://download.proxmox.com/") {
+ let changelog = pbs_runtime::block_on(client.get_string(changelog_url, None))
+ .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
+ Ok(json!(changelog))
- let mut command = std::process::Command::new("apt-get");
- command.arg("update");
+ } else if changelog_url.starts_with("https://enterprise.proxmox.com/") {
+ let sub = match subscription::read_subscription()? {
+ Some(sub) => sub,
+ None => bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
+ };
+ let (key, id) = match sub.key {
+ Some(key) => {
+ match sub.serverid {
+ Some(id) => (key, id),
+ None =>
+ bail!("cannot retrieve changelog from enterprise repo: no server id found")
+ }
+ },
+ None => bail!("cannot retrieve changelog from enterprise repo: no subscription key found")
+ };
+
+ let mut auth_header = HashMap::new();
+ auth_header.insert("Authorization".to_owned(),
+ format!("Basic {}", base64::encode(format!("{}:{}", key, id))));
+
+ let changelog = pbs_runtime::block_on(client.get_string(changelog_url, Some(&auth_header)))
+ .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
+ Ok(json!(changelog))
+ } else {
+ let mut command = std::process::Command::new("apt-get");
+ command.arg("changelog");
+ command.arg("-qq"); // don't display download progress
+ command.arg(name);
let output = crate::tools::run_command(command, None)?;
- if !quiet { worker.log(output) }
+ Ok(json!(output))
+ }
+}
- // TODO: add mail notify for new updates like PVE
+#[api(
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ description: "List of more relevant packages.",
+ type: Array,
+ items: {
+ type: APTUpdateInfo,
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// Get package information for important Proxmox Backup Server packages.
+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-mini-journalreader",
+ "proxmox-widget-toolkit",
+ "pve-xtermjs",
+ "smartmontools",
+ "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(),
+ change_log_url: "unknown".into(),
+ extra_info,
+ }
+ }
- Ok(())
- })?;
+ let is_kernel = |name: &str| name.starts_with("pve-kernel-");
- Ok(upid_str)
+ 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: {}",
+ nix::sys::utsname::uname().release().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 = crate::api2::version::PROXMOX_PKG_VERSION;
+ let release = crate::api2::version::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 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)
}
const SUBDIRS: SubdirMap = &[
+ ("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
("update", &Router::new()
.get(&API_METHOD_APT_UPDATE_AVAILABLE)
.post(&API_METHOD_APT_UPDATE_DATABASE)
),
+ ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
];
pub const ROUTER: Router = Router::new()