1 use anyhow
::{bail, format_err, Error}
;
2 use serde_json
::{json, Value}
;
3 use std
::collections
::HashMap
;
4 use std
::os
::unix
::prelude
::OsStrExt
;
7 list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, RpcEnvironmentType
, SubdirMap
,
9 use proxmox_schema
::api
;
10 use proxmox_sys
::fs
::{replace_file, CreateOptions}
;
12 use proxmox_apt
::repositories
::{
13 APTRepositoryFile
, APTRepositoryFileError
, APTRepositoryHandle
, APTRepositoryInfo
,
14 APTStandardRepository
,
16 use proxmox_http
::ProxyConfig
;
19 APTUpdateInfo
, NODE_SCHEMA
, PRIV_SYS_AUDIT
, PRIV_SYS_MODIFY
, PROXMOX_CONFIG_DIGEST_SCHEMA
,
22 use pbs_buildcfg
::PROXMOX_BACKUP_SUBSCRIPTION_FN
;
24 use crate::config
::node
;
25 use crate::tools
::{apt, pbs_simple_http}
;
26 use proxmox_rest_server
::WorkerTask
;
37 description
: "A list of packages with available updates.",
45 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
48 /// List available APT updates
49 fn apt_update_available(_param
: Value
) -> Result
<Value
, Error
> {
50 if let Ok(false) = apt
::pkg_cache_expired() {
51 if let Ok(Some(cache
)) = apt
::read_pkg_state() {
52 return Ok(json
!(cache
.package_status
));
56 let cache
= apt
::update_cache()?
;
58 Ok(json
!(cache
.package_status
))
61 pub fn update_apt_proxy_config(proxy_config
: Option
<&ProxyConfig
>) -> Result
<(), Error
> {
62 const PROXY_CFG_FN
: &str = "/etc/apt/apt.conf.d/76pveproxy"; // use same file as PVE
64 if let Some(proxy_config
) = proxy_config
{
65 let proxy
= proxy_config
.to_proxy_string()?
;
66 let data
= format
!("Acquire::http::Proxy \"{}\";\n", proxy
);
67 replace_file(PROXY_CFG_FN
, data
.as_bytes(), CreateOptions
::new(), false)
69 match std
::fs
::remove_file(PROXY_CFG_FN
) {
71 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
72 Err(err
) => bail
!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN
, err
),
77 fn read_and_update_proxy_config() -> Result
<Option
<ProxyConfig
>, Error
> {
78 let proxy_config
= if let Ok((node_config
, _digest
)) = node
::config() {
79 node_config
.http_proxy()
83 update_apt_proxy_config(proxy_config
.as_ref())?
;
88 fn do_apt_update(worker
: &WorkerTask
, quiet
: bool
) -> Result
<(), Error
> {
90 worker
.log_message("starting apt-get update")
93 read_and_update_proxy_config()?
;
95 let mut command
= std
::process
::Command
::new("apt-get");
96 command
.arg("update");
98 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
101 .map_err(|err
| format_err
!("failed to execute {:?} - {}", command
, err
))?
;
104 worker
.log_message(String
::from_utf8(output
.stdout
)?
);
107 // TODO: improve run_command to allow outputting both, stderr and stdout
108 if !output
.status
.success() {
109 if output
.status
.code().is_some() {
110 let msg
= String
::from_utf8(output
.stderr
)
113 String
::from("no error message")
118 .unwrap_or_else(|_
| String
::from("non utf8 error message (suppressed)"));
119 worker
.log_warning(msg
);
121 bail
!("terminated by signal");
136 description
: r
#"Send notification mail about new package updates available to the
137 email address configured for 'root@pam')."#,
142 description
: "Only produces output suitable for logging, omitting progress indicators.",
153 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
156 /// Update the APT database
157 pub fn apt_update_database(
160 rpcenv
: &mut dyn RpcEnvironment
,
161 ) -> Result
<String
, Error
> {
162 let auth_id
= rpcenv
.get_auth_id().unwrap();
163 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
165 let upid_str
= WorkerTask
::new_thread("aptupdate", None
, auth_id
, to_stdout
, move |worker
| {
166 do_apt_update(&worker
, quiet
)?
;
168 let mut cache
= apt
::update_cache()?
;
171 let mut notified
= match cache
.notified
{
172 Some(notified
) => notified
,
173 None
=> std
::collections
::HashMap
::new(),
175 let mut to_notify
: Vec
<&APTUpdateInfo
> = Vec
::new();
177 for pkg
in &cache
.package_status
{
178 match notified
.insert(pkg
.package
.to_owned(), pkg
.version
.to_owned()) {
179 Some(notified_version
) => {
180 if notified_version
!= pkg
.version
{
184 None
=> to_notify
.push(pkg
),
187 if !to_notify
.is_empty() {
188 to_notify
.sort_unstable_by_key(|k
| &k
.package
);
189 crate::server
::send_updates_available(&to_notify
)?
;
191 cache
.notified
= Some(notified
);
192 apt
::write_pkg_cache(&cache
)?
;
209 description
: "Package name to get changelog of.",
213 description
: "Package version to get changelog of. Omit to use candidate version.",
223 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
226 /// Retrieve the changelog of the specified package.
227 fn apt_get_changelog(param
: Value
) -> Result
<Value
, Error
> {
228 let name
= pbs_tools
::json
::required_string_param(¶m
, "name")?
.to_owned();
229 let version
= param
["version"].as_str();
231 let pkg_info
= apt
::list_installed_apt_packages(
232 |data
| match version
{
233 Some(version
) => version
== data
.active_version
,
234 None
=> data
.active_version
== data
.candidate_version
,
239 if pkg_info
.is_empty() {
240 bail
!("Package '{}' not found", name
);
243 let proxy_config
= read_and_update_proxy_config()?
;
244 let client
= pbs_simple_http(proxy_config
);
246 let changelog_url
= &pkg_info
[0].change_log_url
;
247 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
248 if changelog_url
.starts_with("http://download.proxmox.com/") {
249 let changelog
= proxmox_async
::runtime
::block_on(client
.get_string(changelog_url
, None
))
252 "Error downloading changelog from '{}': {}",
258 } else if changelog_url
.starts_with("https://enterprise.proxmox.com/") {
259 let sub
= match proxmox_subscription
::files
::read_subscription(
260 PROXMOX_BACKUP_SUBSCRIPTION_FN
,
261 &[proxmox_subscription
::files
::DEFAULT_SIGNING_KEY
],
265 bail
!("cannot retrieve changelog from enterprise repo: no subscription info found")
268 let (key
, id
) = match sub
.key
{
269 Some(key
) => match sub
.serverid
{
270 Some(id
) => (key
, id
),
271 None
=> bail
!("cannot retrieve changelog from enterprise repo: no server id found"),
274 bail
!("cannot retrieve changelog from enterprise repo: no subscription key found")
278 let mut auth_header
= HashMap
::new();
280 "Authorization".to_owned(),
281 format
!("Basic {}", base64
::encode(format
!("{}:{}", key
, id
))),
285 proxmox_async
::runtime
::block_on(client
.get_string(changelog_url
, Some(&auth_header
)))
288 "Error downloading changelog from '{}': {}",
295 let mut command
= std
::process
::Command
::new("apt-get");
296 command
.arg("changelog");
297 command
.arg("-qq"); // don't display download progress
299 let output
= proxmox_sys
::command
::run_command(command
, None
)?
;
313 description
: "List of more relevant packages.",
320 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
323 /// Get package information for important Proxmox Backup Server packages.
324 pub fn get_versions() -> Result
<Vec
<APTUpdateInfo
>, Error
> {
325 const PACKAGES
: &[&str] = &[
329 "proxmox-backup-docs",
330 "proxmox-backup-client",
331 "proxmox-backup-server",
332 "proxmox-mail-forward",
333 "proxmox-mini-journalreader",
334 "proxmox-offline-mirror-helper",
335 "proxmox-widget-toolkit",
341 fn unknown_package(package
: String
, extra_info
: Option
<String
>) -> APTUpdateInfo
{
344 title
: "unknown".into(),
345 arch
: "unknown".into(),
346 description
: "unknown".into(),
347 version
: "unknown".into(),
348 old_version
: "unknown".into(),
349 origin
: "unknown".into(),
350 priority
: "unknown".into(),
351 section
: "unknown".into(),
352 change_log_url
: "unknown".into(),
357 let is_kernel
= |name
: &str| name
.starts_with("pve-kernel-");
359 let mut packages
: Vec
<APTUpdateInfo
> = Vec
::new();
360 let pbs_packages
= apt
::list_installed_apt_packages(
362 filter
.installed_version
== Some(filter
.active_version
)
363 && (is_kernel(filter
.package
) || PACKAGES
.contains(&filter
.package
))
368 let running_kernel
= format
!(
369 "running kernel: {}",
370 std
::str::from_utf8(nix
::sys
::utsname
::uname()?
.release().as_bytes())?
.to_owned()
372 if let Some(proxmox_backup
) = pbs_packages
374 .find(|pkg
| pkg
.package
== "proxmox-backup")
376 let mut proxmox_backup
= proxmox_backup
.clone();
377 proxmox_backup
.extra_info
= Some(running_kernel
);
378 packages
.push(proxmox_backup
);
380 packages
.push(unknown_package(
381 "proxmox-backup".into(),
382 Some(running_kernel
),
386 let version
= pbs_buildcfg
::PROXMOX_PKG_VERSION
;
387 let release
= pbs_buildcfg
::PROXMOX_PKG_RELEASE
;
388 let daemon_version_info
= Some(format
!("running version: {}.{}", version
, release
));
389 if let Some(pkg
) = pbs_packages
391 .find(|pkg
| pkg
.package
== "proxmox-backup-server")
393 let mut pkg
= pkg
.clone();
394 pkg
.extra_info
= daemon_version_info
;
397 packages
.push(unknown_package(
398 "proxmox-backup".into(),
403 let mut kernel_pkgs
: Vec
<APTUpdateInfo
> = pbs_packages
405 .filter(|pkg
| is_kernel(&pkg
.package
))
408 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
410 let cache
= apt_pkg_native
::Cache
::get_singleton();
411 kernel_pkgs
.sort_by(|left
, right
| {
413 .compare_versions(&left
.old_version
, &right
.old_version
)
417 packages
.append(&mut kernel_pkgs
);
419 // add entry for all packages we're interested in, even if not installed
420 for pkg
in PACKAGES
.iter() {
421 if pkg
== &"proxmox-backup" || pkg
== &"proxmox-backup-server" {
424 match pbs_packages
.iter().find(|item
| &item
.package
== pkg
) {
425 Some(apt_pkg
) => packages
.push(apt_pkg
.to_owned()),
426 None
=> packages
.push(unknown_package(pkg
.to_string(), None
)),
443 description
: "Result from parsing the APT repository files in /etc/apt/.",
446 description
: "List of parsed repository files.",
449 type: APTRepositoryFile
,
453 description
: "List of problematic files.",
456 type: APTRepositoryFileError
,
460 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
463 description
: "List of additional information/warnings about the repositories.",
465 type: APTRepositoryInfo
,
469 description
: "List of standard repositories and their configuration status.",
471 type: APTStandardRepository
,
477 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
480 /// Get APT repository information.
481 pub fn get_repositories() -> Result
<Value
, Error
> {
482 let (files
, errors
, digest
) = proxmox_apt
::repositories
::repositories()?
;
483 let digest
= hex
::encode(digest
);
485 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
487 let infos
= proxmox_apt
::repositories
::check_repositories(&files
, suite
);
488 let standard_repos
= proxmox_apt
::repositories
::standard_repositories(&files
, "pbs", suite
);
495 "standard-repos": standard_repos
,
506 type: APTRepositoryHandle
,
509 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
516 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
519 /// Add the repository identified by the `handle`.
520 /// If the repository is already configured, it will be set to enabled.
522 /// The `digest` parameter asserts that the configuration has not been modified.
523 pub fn add_repository(handle
: APTRepositoryHandle
, digest
: Option
<String
>) -> Result
<(), Error
> {
524 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
526 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
528 if let Some(expected_digest
) = digest
{
529 let current_digest
= hex
::encode(current_digest
);
530 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
533 // check if it's already configured first
534 for file
in files
.iter_mut() {
535 for repo
in file
.repositories
.iter_mut() {
536 if repo
.is_referenced_repository(handle
, "pbs", &suite
.to_string()) {
541 repo
.set_enabled(true);
549 let (repo
, path
) = proxmox_apt
::repositories
::get_standard_repository(handle
, "pbs", suite
);
551 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
553 "unable to parse existing file {} - {}",
559 if let Some(file
) = files
561 .find(|file
| file
.path
.as_ref() == Some(&path
))
563 file
.repositories
.push(repo
);
567 let mut file
= match APTRepositoryFile
::new(&path
)?
{
569 None
=> bail
!("invalid path - {}", path
),
572 file
.repositories
.push(repo
);
587 description
: "Path to the containing file.",
591 description
: "Index within the file (starting from 0).",
595 description
: "Whether the repository should be enabled or not.",
600 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
607 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
610 /// Change the properties of the specified repository.
612 /// The `digest` parameter asserts that the configuration has not been modified.
613 pub fn change_repository(
616 enabled
: Option
<bool
>,
617 digest
: Option
<String
>,
618 ) -> Result
<(), Error
> {
619 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
621 if let Some(expected_digest
) = digest
{
622 let current_digest
= hex
::encode(current_digest
);
623 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
626 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
627 bail
!("unable to parse file {} - {}", error
.path
, error
.error
);
630 if let Some(file
) = files
632 .find(|file
| file
.path
.as_ref() == Some(&path
))
634 if let Some(repo
) = file
.repositories
.get_mut(index
) {
635 if let Some(enabled
) = enabled
{
636 repo
.set_enabled(enabled
);
641 bail
!("invalid index - {}", index
);
644 bail
!("invalid path - {}", path
);
650 const SUBDIRS
: SubdirMap
= &[
653 &Router
::new().get(&API_METHOD_APT_GET_CHANGELOG
),
658 .get(&API_METHOD_GET_REPOSITORIES
)
659 .post(&API_METHOD_CHANGE_REPOSITORY
)
660 .put(&API_METHOD_ADD_REPOSITORY
),
665 .get(&API_METHOD_APT_UPDATE_AVAILABLE
)
666 .post(&API_METHOD_APT_UPDATE_DATABASE
),
668 ("versions", &Router
::new().get(&API_METHOD_GET_VERSIONS
)),
671 pub const ROUTER
: Router
= Router
::new()
672 .get(&list_subdirs_api_method
!(SUBDIRS
))