use anyhow::{Error, bail, format_err}; use serde_json::{json, Value}; use proxmox::list_subdirs_api_method; use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission}; use proxmox::api::router::{Router, SubdirMap}; use crate::server::WorkerTask; use crate::tools::{apt, http}; use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA}; #[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 { match apt::pkg_cache_expired() { Ok(false) => { if let Ok(Some(cache)) = apt::read_pkg_state() { return Ok(json!(cache.package_status)); } }, _ => (), } let cache = apt::update_cache()?; return Ok(json!(cache.package_status)); } fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> { if !quiet { worker.log("starting apt-get update") } // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE 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(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.warn(msg); } else { bail!("terminated by signal"); } } Ok(()) } #[api( protected: true, input: { properties: { node: { schema: NODE_SCHEMA, }, notify: { type: bool, description: r#"Send notification mail about new package updates availanle to the email address configured for 'root@pam')."#, optional: true, default: false, }, quiet: { description: "Only produces output suitable for logging, omitting progress indicators.", type: bool, default: false, optional: true, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false), }, )] /// Update the APT database pub fn apt_update_database( notify: Option, quiet: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; // FIXME: change to non-option in signature and drop below once we have proxmox-api-macro 0.2.3 let quiet = quiet.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_QUIET); let notify = notify.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_NOTIFY); 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( input: { properties: { 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, }, }, }, returns: { schema: UPID_SCHEMA, }, access: { permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false), }, )] /// Retrieve the changelog of the specified package. fn apt_get_changelog( param: Value, ) -> Result { 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)); if pkg_info.len() == 0 { bail!("Package '{}' not found", name); } 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 = crate::tools::runtime::block_on(http::get_string(changelog_url)) .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?; return 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)?; return Ok(json!(output)); } } #[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 { 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) -> 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(), } } let is_kernel = |name: &str| name.starts_with("pve-kernel-"); let mut packages: Vec = 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, ); if let Some(proxmox_backup) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup") { packages.push(proxmox_backup.to_owned()); } else { packages.push(unknown_package("proxmox-backup".into())); } if let Some(pkg) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup-server") { packages.push(pkg.to_owned()); } let mut kernel_pkgs: Vec = 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())), } } Ok(json!(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() .get(&list_subdirs_api_method!(SUBDIRS)) .subdirs(SUBDIRS);