1 use anyhow
::{Error, bail, format_err}
;
2 use serde_json
::{json, Value}
;
3 use std
::collections
::HashMap
;
5 use proxmox
::list_subdirs_api_method
;
6 use proxmox
::api
::{api, RpcEnvironment, RpcEnvironmentType, Permission}
;
7 use proxmox
::api
::router
::{Router, SubdirMap}
;
8 use proxmox
::tools
::fs
::{replace_file, CreateOptions}
;
10 use proxmox_apt
::repositories
::{
11 APTRepositoryFile
, APTRepositoryFileError
, APTRepositoryHandle
, APTRepositoryInfo
,
12 APTStandardRepository
,
14 use proxmox_http
::ProxyConfig
;
16 use crate::config
::node
;
17 use crate::server
::WorkerTask
;
23 use crate::config
::acl
::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}
;
24 use crate::api2
::types
::{Authid, APTUpdateInfo, NODE_SCHEMA, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA}
;
35 description
: "A list of packages with available updates.",
43 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
46 /// List available APT updates
47 fn apt_update_available(_param
: Value
) -> Result
<Value
, Error
> {
49 if let Ok(false) = apt
::pkg_cache_expired() {
50 if let Ok(Some(cache
)) = apt
::read_pkg_state() {
51 return Ok(json
!(cache
.package_status
));
55 let cache
= apt
::update_cache()?
;
57 Ok(json
!(cache
.package_status
))
60 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())
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
> {
89 if !quiet { worker.log("starting apt-get update") }
91 read_and_update_proxy_config()?
;
93 let mut command
= std
::process
::Command
::new("apt-get");
94 command
.arg("update");
96 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
97 let output
= command
.output()
98 .map_err(|err
| format_err
!("failed to execute {:?} - {}", command
, err
))?
;
101 worker
.log(String
::from_utf8(output
.stdout
)?
);
104 // TODO: improve run_command to allow outputting both, stderr and stdout
105 if !output
.status
.success() {
106 if output
.status
.code().is_some() {
107 let msg
= String
::from_utf8(output
.stderr
)
108 .map(|m
| if m
.is_empty() { String::from("no error message") }
else { m }
)
109 .unwrap_or_else(|_
| String
::from("non utf8 error message (suppressed)"));
112 bail
!("terminated by signal");
127 description
: r
#"Send notification mail about new package updates available to the
128 email address configured for 'root@pam')."#,
133 description
: "Only produces output suitable for logging, omitting progress indicators.",
144 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
147 /// Update the APT database
148 pub fn apt_update_database(
151 rpcenv
: &mut dyn RpcEnvironment
,
152 ) -> Result
<String
, Error
> {
154 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
155 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
157 let upid_str
= WorkerTask
::new_thread("aptupdate", None
, auth_id
, to_stdout
, move |worker
| {
158 do_apt_update(&worker
, quiet
)?
;
160 let mut cache
= apt
::update_cache()?
;
163 let mut notified
= match cache
.notified
{
164 Some(notified
) => notified
,
165 None
=> std
::collections
::HashMap
::new(),
167 let mut to_notify
: Vec
<&APTUpdateInfo
> = Vec
::new();
169 for pkg
in &cache
.package_status
{
170 match notified
.insert(pkg
.package
.to_owned(), pkg
.version
.to_owned()) {
171 Some(notified_version
) => {
172 if notified_version
!= pkg
.version
{
176 None
=> to_notify
.push(pkg
),
179 if !to_notify
.is_empty() {
180 to_notify
.sort_unstable_by_key(|k
| &k
.package
);
181 crate::server
::send_updates_available(&to_notify
)?
;
183 cache
.notified
= Some(notified
);
184 apt
::write_pkg_cache(&cache
)?
;
201 description
: "Package name to get changelog of.",
205 description
: "Package version to get changelog of. Omit to use candidate version.",
215 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
218 /// Retrieve the changelog of the specified package.
219 fn apt_get_changelog(
221 ) -> Result
<Value
, Error
> {
223 let name
= crate::tools
::required_string_param(¶m
, "name")?
.to_owned();
224 let version
= param
["version"].as_str();
226 let pkg_info
= apt
::list_installed_apt_packages(|data
| {
228 Some(version
) => version
== data
.active_version
,
229 None
=> data
.active_version
== data
.candidate_version
233 if pkg_info
.is_empty() {
234 bail
!("Package '{}' not found", name
);
237 let proxy_config
= read_and_update_proxy_config()?
;
238 let mut client
= pbs_simple_http(proxy_config
);
240 let changelog_url
= &pkg_info
[0].change_log_url
;
241 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
242 if changelog_url
.starts_with("http://download.proxmox.com/") {
243 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, None
))
244 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
247 } else if changelog_url
.starts_with("https://enterprise.proxmox.com/") {
248 let sub
= match subscription
::read_subscription()?
{
250 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription info found")
252 let (key
, id
) = match sub
.key
{
255 Some(id
) => (key
, id
),
257 bail
!("cannot retrieve changelog from enterprise repo: no server id found")
260 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription key found")
263 let mut auth_header
= HashMap
::new();
264 auth_header
.insert("Authorization".to_owned(),
265 format
!("Basic {}", base64
::encode(format
!("{}:{}", key
, id
))));
267 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, Some(&auth_header
)))
268 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
272 let mut command
= std
::process
::Command
::new("apt-get");
273 command
.arg("changelog");
274 command
.arg("-qq"); // don't display download progress
276 let output
= crate::tools
::run_command(command
, None
)?
;
290 description
: "List of more relevant packages.",
297 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
300 /// Get package information for important Proxmox Backup Server packages.
301 pub fn get_versions() -> Result
<Vec
<APTUpdateInfo
>, Error
> {
302 const PACKAGES
: &[&str] = &[
306 "proxmox-backup-docs",
307 "proxmox-backup-client",
308 "proxmox-backup-server",
309 "proxmox-mini-journalreader",
310 "proxmox-widget-toolkit",
316 fn unknown_package(package
: String
, extra_info
: Option
<String
>) -> APTUpdateInfo
{
319 title
: "unknown".into(),
320 arch
: "unknown".into(),
321 description
: "unknown".into(),
322 version
: "unknown".into(),
323 old_version
: "unknown".into(),
324 origin
: "unknown".into(),
325 priority
: "unknown".into(),
326 section
: "unknown".into(),
327 change_log_url
: "unknown".into(),
332 let is_kernel
= |name
: &str| name
.starts_with("pve-kernel-");
334 let mut packages
: Vec
<APTUpdateInfo
> = Vec
::new();
335 let pbs_packages
= apt
::list_installed_apt_packages(
337 filter
.installed_version
== Some(filter
.active_version
)
338 && (is_kernel(filter
.package
) || PACKAGES
.contains(&filter
.package
))
343 let running_kernel
= format
!(
344 "running kernel: {}",
345 nix
::sys
::utsname
::uname().release().to_owned()
347 if let Some(proxmox_backup
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup") {
348 let mut proxmox_backup
= proxmox_backup
.clone();
349 proxmox_backup
.extra_info
= Some(running_kernel
);
350 packages
.push(proxmox_backup
);
352 packages
.push(unknown_package("proxmox-backup".into(), Some(running_kernel
)));
355 let version
= pbs_buildcfg
::PROXMOX_PKG_VERSION
;
356 let release
= pbs_buildcfg
::PROXMOX_PKG_RELEASE
;
357 let daemon_version_info
= Some(format
!("running version: {}.{}", version
, release
));
358 if let Some(pkg
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup-server") {
359 let mut pkg
= pkg
.clone();
360 pkg
.extra_info
= daemon_version_info
;
363 packages
.push(unknown_package("proxmox-backup".into(), daemon_version_info
));
366 let mut kernel_pkgs
: Vec
<APTUpdateInfo
> = pbs_packages
368 .filter(|pkg
| is_kernel(&pkg
.package
))
371 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
373 let cache
= apt_pkg_native
::Cache
::get_singleton();
374 kernel_pkgs
.sort_by(|left
, right
| {
376 .compare_versions(&left
.old_version
, &right
.old_version
)
380 packages
.append(&mut kernel_pkgs
);
382 // add entry for all packages we're interested in, even if not installed
383 for pkg
in PACKAGES
.iter() {
384 if pkg
== &"proxmox-backup" || pkg
== &"proxmox-backup-server" {
387 match pbs_packages
.iter().find(|item
| &item
.package
== pkg
) {
388 Some(apt_pkg
) => packages
.push(apt_pkg
.to_owned()),
389 None
=> packages
.push(unknown_package(pkg
.to_string(), None
)),
406 description
: "Result from parsing the APT repository files in /etc/apt/.",
409 description
: "List of parsed repository files.",
412 type: APTRepositoryFile
,
416 description
: "List of problematic files.",
419 type: APTRepositoryFileError
,
423 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
426 description
: "List of additional information/warnings about the repositories.",
428 type: APTRepositoryInfo
,
432 description
: "List of standard repositories and their configuration status.",
434 type: APTStandardRepository
,
440 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
443 /// Get APT repository information.
444 pub fn get_repositories() -> Result
<Value
, Error
> {
445 let (files
, errors
, digest
) = proxmox_apt
::repositories
::repositories()?
;
446 let digest
= proxmox
::tools
::digest_to_hex(&digest
);
448 let infos
= proxmox_apt
::repositories
::check_repositories(&files
)?
;
449 let standard_repos
= proxmox_apt
::repositories
::standard_repositories("pbs", &files
);
456 "standard-repos": standard_repos
,
467 type: APTRepositoryHandle
,
470 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
477 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
480 /// Add the repository identified by the `handle`.
481 /// If the repository is already configured, it will be set to enabled.
483 /// The `digest` parameter asserts that the configuration has not been modified.
484 pub fn add_repository(handle
: APTRepositoryHandle
, digest
: Option
<String
>) -> Result
<(), Error
> {
485 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
487 if let Some(expected_digest
) = digest
{
488 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
489 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
492 // check if it's already configured first
493 for file
in files
.iter_mut() {
494 for repo
in file
.repositories
.iter_mut() {
495 if repo
.is_referenced_repository(handle
, "pbs") {
500 repo
.set_enabled(true);
508 let (repo
, path
) = proxmox_apt
::repositories
::get_standard_repository(handle
, "pbs")?
;
510 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
512 "unable to parse existing file {} - {}",
518 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
519 file
.repositories
.push(repo
);
523 let mut file
= match APTRepositoryFile
::new(&path
)?
{
525 None
=> bail
!("invalid path - {}", path
),
528 file
.repositories
.push(repo
);
543 description
: "Path to the containing file.",
547 description
: "Index within the file (starting from 0).",
551 description
: "Whether the repository should be enabled or not.",
556 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
563 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
566 /// Change the properties of the specified repository.
568 /// The `digest` parameter asserts that the configuration has not been modified.
569 pub fn change_repository(
572 enabled
: Option
<bool
>,
573 digest
: Option
<String
>,
574 ) -> Result
<(), Error
> {
575 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
577 if let Some(expected_digest
) = digest
{
578 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
579 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
582 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
583 bail
!("unable to parse file {} - {}", error
.path
, error
.error
);
586 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
587 if let Some(repo
) = file
.repositories
.get_mut(index
) {
588 if let Some(enabled
) = enabled
{
589 repo
.set_enabled(enabled
);
594 bail
!("invalid index - {}", index
);
597 bail
!("invalid path - {}", path
);
603 const SUBDIRS
: SubdirMap
= &[
604 ("changelog", &Router
::new().get(&API_METHOD_APT_GET_CHANGELOG
)),
605 ("repositories", &Router
::new()
606 .get(&API_METHOD_GET_REPOSITORIES
)
607 .post(&API_METHOD_CHANGE_REPOSITORY
)
608 .put(&API_METHOD_ADD_REPOSITORY
)
610 ("update", &Router
::new()
611 .get(&API_METHOD_APT_UPDATE_AVAILABLE
)
612 .post(&API_METHOD_APT_UPDATE_DATABASE
)
614 ("versions", &Router
::new().get(&API_METHOD_GET_VERSIONS
)),
617 pub const ROUTER
: Router
= Router
::new()
618 .get(&list_subdirs_api_method
!(SUBDIRS
))