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
;
17 APTUpdateInfo
, NODE_SCHEMA
, PROXMOX_CONFIG_DIGEST_SCHEMA
, UPID_SCHEMA
,
18 PRIV_SYS_AUDIT
, PRIV_SYS_MODIFY
,
21 use crate::config
::node
;
22 use proxmox_rest_server
::WorkerTask
;
38 description
: "A list of packages with available updates.",
46 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
49 /// List available APT updates
50 fn apt_update_available(_param
: Value
) -> Result
<Value
, Error
> {
52 if let Ok(false) = apt
::pkg_cache_expired() {
53 if let Ok(Some(cache
)) = apt
::read_pkg_state() {
54 return Ok(json
!(cache
.package_status
));
58 let cache
= apt
::update_cache()?
;
60 Ok(json
!(cache
.package_status
))
63 pub fn update_apt_proxy_config(proxy_config
: Option
<&ProxyConfig
>) -> Result
<(), Error
> {
65 const PROXY_CFG_FN
: &str = "/etc/apt/apt.conf.d/76pveproxy"; // use same file as PVE
67 if let Some(proxy_config
) = proxy_config
{
68 let proxy
= proxy_config
.to_proxy_string()?
;
69 let data
= format
!("Acquire::http::Proxy \"{}\";\n", proxy
);
70 replace_file(PROXY_CFG_FN
, data
.as_bytes(), CreateOptions
::new())
72 match std
::fs
::remove_file(PROXY_CFG_FN
) {
74 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
75 Err(err
) => bail
!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN
, err
),
80 fn read_and_update_proxy_config() -> Result
<Option
<ProxyConfig
>, Error
> {
81 let proxy_config
= if let Ok((node_config
, _digest
)) = node
::config() {
82 node_config
.http_proxy()
86 update_apt_proxy_config(proxy_config
.as_ref())?
;
91 fn do_apt_update(worker
: &WorkerTask
, quiet
: bool
) -> Result
<(), Error
> {
92 if !quiet { worker.log("starting apt-get update") }
94 read_and_update_proxy_config()?
;
96 let mut command
= std
::process
::Command
::new("apt-get");
97 command
.arg("update");
99 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
100 let output
= command
.output()
101 .map_err(|err
| format_err
!("failed to execute {:?} - {}", command
, err
))?
;
104 worker
.log(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
)
111 .map(|m
| if m
.is_empty() { String::from("no error message") }
else { m }
)
112 .unwrap_or_else(|_
| String
::from("non utf8 error message (suppressed)"));
115 bail
!("terminated by signal");
130 description
: r
#"Send notification mail about new package updates available to the
131 email address configured for 'root@pam')."#,
136 description
: "Only produces output suitable for logging, omitting progress indicators.",
147 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
150 /// Update the APT database
151 pub fn apt_update_database(
154 rpcenv
: &mut dyn RpcEnvironment
,
155 ) -> Result
<String
, Error
> {
157 let auth_id
= rpcenv
.get_auth_id().unwrap();
158 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
160 let upid_str
= WorkerTask
::new_thread("aptupdate", None
, auth_id
, to_stdout
, move |worker
| {
161 do_apt_update(&worker
, quiet
)?
;
163 let mut cache
= apt
::update_cache()?
;
166 let mut notified
= match cache
.notified
{
167 Some(notified
) => notified
,
168 None
=> std
::collections
::HashMap
::new(),
170 let mut to_notify
: Vec
<&APTUpdateInfo
> = Vec
::new();
172 for pkg
in &cache
.package_status
{
173 match notified
.insert(pkg
.package
.to_owned(), pkg
.version
.to_owned()) {
174 Some(notified_version
) => {
175 if notified_version
!= pkg
.version
{
179 None
=> to_notify
.push(pkg
),
182 if !to_notify
.is_empty() {
183 to_notify
.sort_unstable_by_key(|k
| &k
.package
);
184 crate::server
::send_updates_available(&to_notify
)?
;
186 cache
.notified
= Some(notified
);
187 apt
::write_pkg_cache(&cache
)?
;
204 description
: "Package name to get changelog of.",
208 description
: "Package version to get changelog of. Omit to use candidate version.",
218 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
221 /// Retrieve the changelog of the specified package.
222 fn apt_get_changelog(
224 ) -> Result
<Value
, Error
> {
226 let name
= pbs_tools
::json
::required_string_param(¶m
, "name")?
.to_owned();
227 let version
= param
["version"].as_str();
229 let pkg_info
= apt
::list_installed_apt_packages(|data
| {
231 Some(version
) => version
== data
.active_version
,
232 None
=> data
.active_version
== data
.candidate_version
236 if pkg_info
.is_empty() {
237 bail
!("Package '{}' not found", name
);
240 let proxy_config
= read_and_update_proxy_config()?
;
241 let mut client
= pbs_simple_http(proxy_config
);
243 let changelog_url
= &pkg_info
[0].change_log_url
;
244 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
245 if changelog_url
.starts_with("http://download.proxmox.com/") {
246 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, None
))
247 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
250 } else if changelog_url
.starts_with("https://enterprise.proxmox.com/") {
251 let sub
= match subscription
::read_subscription()?
{
253 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription info found")
255 let (key
, id
) = match sub
.key
{
258 Some(id
) => (key
, id
),
260 bail
!("cannot retrieve changelog from enterprise repo: no server id found")
263 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription key found")
266 let mut auth_header
= HashMap
::new();
267 auth_header
.insert("Authorization".to_owned(),
268 format
!("Basic {}", base64
::encode(format
!("{}:{}", key
, id
))));
270 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, Some(&auth_header
)))
271 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
275 let mut command
= std
::process
::Command
::new("apt-get");
276 command
.arg("changelog");
277 command
.arg("-qq"); // don't display download progress
279 let output
= pbs_tools
::run_command(command
, None
)?
;
293 description
: "List of more relevant packages.",
300 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
303 /// Get package information for important Proxmox Backup Server packages.
304 pub fn get_versions() -> Result
<Vec
<APTUpdateInfo
>, Error
> {
305 const PACKAGES
: &[&str] = &[
309 "proxmox-backup-docs",
310 "proxmox-backup-client",
311 "proxmox-backup-server",
312 "proxmox-mini-journalreader",
313 "proxmox-widget-toolkit",
319 fn unknown_package(package
: String
, extra_info
: Option
<String
>) -> APTUpdateInfo
{
322 title
: "unknown".into(),
323 arch
: "unknown".into(),
324 description
: "unknown".into(),
325 version
: "unknown".into(),
326 old_version
: "unknown".into(),
327 origin
: "unknown".into(),
328 priority
: "unknown".into(),
329 section
: "unknown".into(),
330 change_log_url
: "unknown".into(),
335 let is_kernel
= |name
: &str| name
.starts_with("pve-kernel-");
337 let mut packages
: Vec
<APTUpdateInfo
> = Vec
::new();
338 let pbs_packages
= apt
::list_installed_apt_packages(
340 filter
.installed_version
== Some(filter
.active_version
)
341 && (is_kernel(filter
.package
) || PACKAGES
.contains(&filter
.package
))
346 let running_kernel
= format
!(
347 "running kernel: {}",
348 nix
::sys
::utsname
::uname().release().to_owned()
350 if let Some(proxmox_backup
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup") {
351 let mut proxmox_backup
= proxmox_backup
.clone();
352 proxmox_backup
.extra_info
= Some(running_kernel
);
353 packages
.push(proxmox_backup
);
355 packages
.push(unknown_package("proxmox-backup".into(), Some(running_kernel
)));
358 let version
= pbs_buildcfg
::PROXMOX_PKG_VERSION
;
359 let release
= pbs_buildcfg
::PROXMOX_PKG_RELEASE
;
360 let daemon_version_info
= Some(format
!("running version: {}.{}", version
, release
));
361 if let Some(pkg
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup-server") {
362 let mut pkg
= pkg
.clone();
363 pkg
.extra_info
= daemon_version_info
;
366 packages
.push(unknown_package("proxmox-backup".into(), daemon_version_info
));
369 let mut kernel_pkgs
: Vec
<APTUpdateInfo
> = pbs_packages
371 .filter(|pkg
| is_kernel(&pkg
.package
))
374 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
376 let cache
= apt_pkg_native
::Cache
::get_singleton();
377 kernel_pkgs
.sort_by(|left
, right
| {
379 .compare_versions(&left
.old_version
, &right
.old_version
)
383 packages
.append(&mut kernel_pkgs
);
385 // add entry for all packages we're interested in, even if not installed
386 for pkg
in PACKAGES
.iter() {
387 if pkg
== &"proxmox-backup" || pkg
== &"proxmox-backup-server" {
390 match pbs_packages
.iter().find(|item
| &item
.package
== pkg
) {
391 Some(apt_pkg
) => packages
.push(apt_pkg
.to_owned()),
392 None
=> packages
.push(unknown_package(pkg
.to_string(), None
)),
409 description
: "Result from parsing the APT repository files in /etc/apt/.",
412 description
: "List of parsed repository files.",
415 type: APTRepositoryFile
,
419 description
: "List of problematic files.",
422 type: APTRepositoryFileError
,
426 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
429 description
: "List of additional information/warnings about the repositories.",
431 type: APTRepositoryInfo
,
435 description
: "List of standard repositories and their configuration status.",
437 type: APTStandardRepository
,
443 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
446 /// Get APT repository information.
447 pub fn get_repositories() -> Result
<Value
, Error
> {
448 let (files
, errors
, digest
) = proxmox_apt
::repositories
::repositories()?
;
449 let digest
= proxmox
::tools
::digest_to_hex(&digest
);
451 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
453 let infos
= proxmox_apt
::repositories
::check_repositories(&files
, suite
);
454 let standard_repos
= proxmox_apt
::repositories
::standard_repositories(&files
, "pbs", suite
);
461 "standard-repos": standard_repos
,
472 type: APTRepositoryHandle
,
475 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
482 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
485 /// Add the repository identified by the `handle`.
486 /// If the repository is already configured, it will be set to enabled.
488 /// The `digest` parameter asserts that the configuration has not been modified.
489 pub fn add_repository(handle
: APTRepositoryHandle
, digest
: Option
<String
>) -> Result
<(), Error
> {
490 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
492 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
494 if let Some(expected_digest
) = digest
{
495 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
496 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
499 // check if it's already configured first
500 for file
in files
.iter_mut() {
501 for repo
in file
.repositories
.iter_mut() {
502 if repo
.is_referenced_repository(handle
, "pbs", &suite
.to_string()) {
507 repo
.set_enabled(true);
515 let (repo
, path
) = proxmox_apt
::repositories
::get_standard_repository(handle
, "pbs", suite
);
517 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
519 "unable to parse existing file {} - {}",
525 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
526 file
.repositories
.push(repo
);
530 let mut file
= match APTRepositoryFile
::new(&path
)?
{
532 None
=> bail
!("invalid path - {}", path
),
535 file
.repositories
.push(repo
);
550 description
: "Path to the containing file.",
554 description
: "Index within the file (starting from 0).",
558 description
: "Whether the repository should be enabled or not.",
563 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
570 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
573 /// Change the properties of the specified repository.
575 /// The `digest` parameter asserts that the configuration has not been modified.
576 pub fn change_repository(
579 enabled
: Option
<bool
>,
580 digest
: Option
<String
>,
581 ) -> Result
<(), Error
> {
582 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
584 if let Some(expected_digest
) = digest
{
585 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
586 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
589 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
590 bail
!("unable to parse file {} - {}", error
.path
, error
.error
);
593 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
594 if let Some(repo
) = file
.repositories
.get_mut(index
) {
595 if let Some(enabled
) = enabled
{
596 repo
.set_enabled(enabled
);
601 bail
!("invalid index - {}", index
);
604 bail
!("invalid path - {}", path
);
610 const SUBDIRS
: SubdirMap
= &[
611 ("changelog", &Router
::new().get(&API_METHOD_APT_GET_CHANGELOG
)),
612 ("repositories", &Router
::new()
613 .get(&API_METHOD_GET_REPOSITORIES
)
614 .post(&API_METHOD_CHANGE_REPOSITORY
)
615 .put(&API_METHOD_ADD_REPOSITORY
)
617 ("update", &Router
::new()
618 .get(&API_METHOD_APT_UPDATE_AVAILABLE
)
619 .post(&API_METHOD_APT_UPDATE_DATABASE
)
621 ("versions", &Router
::new().get(&API_METHOD_GET_VERSIONS
)),
624 pub const ROUTER
: Router
= Router
::new()
625 .get(&list_subdirs_api_method
!(SUBDIRS
))