1 use anyhow
::{Error, bail, format_err}
;
2 use serde_json
::{json, Value}
;
3 use std
::collections
::HashMap
;
5 use proxmox
::tools
::fs
::{replace_file, CreateOptions}
;
7 list_subdirs_api_method
, RpcEnvironment
, RpcEnvironmentType
, Permission
, Router
, SubdirMap
9 use proxmox_schema
::api
;
11 use proxmox_apt
::repositories
::{
12 APTRepositoryFile
, APTRepositoryFileError
, APTRepositoryHandle
, APTRepositoryInfo
,
13 APTStandardRepository
,
15 use proxmox_http
::ProxyConfig
;
18 APTUpdateInfo
, NODE_SCHEMA
, PROXMOX_CONFIG_DIGEST_SCHEMA
, UPID_SCHEMA
,
19 PRIV_SYS_AUDIT
, PRIV_SYS_MODIFY
,
22 use crate::config
::node
;
23 use proxmox_rest_server
::WorkerTask
;
39 description
: "A list of packages with available updates.",
47 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
50 /// List available APT updates
51 fn apt_update_available(_param
: Value
) -> Result
<Value
, Error
> {
53 if let Ok(false) = apt
::pkg_cache_expired() {
54 if let Ok(Some(cache
)) = apt
::read_pkg_state() {
55 return Ok(json
!(cache
.package_status
));
59 let cache
= apt
::update_cache()?
;
61 Ok(json
!(cache
.package_status
))
64 pub fn update_apt_proxy_config(proxy_config
: Option
<&ProxyConfig
>) -> Result
<(), Error
> {
66 const PROXY_CFG_FN
: &str = "/etc/apt/apt.conf.d/76pveproxy"; // use same file as PVE
68 if let Some(proxy_config
) = proxy_config
{
69 let proxy
= proxy_config
.to_proxy_string()?
;
70 let data
= format
!("Acquire::http::Proxy \"{}\";\n", proxy
);
71 replace_file(PROXY_CFG_FN
, data
.as_bytes(), CreateOptions
::new(), false)
73 match std
::fs
::remove_file(PROXY_CFG_FN
) {
75 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
76 Err(err
) => bail
!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN
, err
),
81 fn read_and_update_proxy_config() -> Result
<Option
<ProxyConfig
>, Error
> {
82 let proxy_config
= if let Ok((node_config
, _digest
)) = node
::config() {
83 node_config
.http_proxy()
87 update_apt_proxy_config(proxy_config
.as_ref())?
;
92 fn do_apt_update(worker
: &WorkerTask
, quiet
: bool
) -> Result
<(), Error
> {
93 if !quiet { worker.log_message("starting apt-get update") }
95 read_and_update_proxy_config()?
;
97 let mut command
= std
::process
::Command
::new("apt-get");
98 command
.arg("update");
100 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
101 let output
= command
.output()
102 .map_err(|err
| format_err
!("failed to execute {:?} - {}", command
, err
))?
;
105 worker
.log_message(String
::from_utf8(output
.stdout
)?
);
108 // TODO: improve run_command to allow outputting both, stderr and stdout
109 if !output
.status
.success() {
110 if output
.status
.code().is_some() {
111 let msg
= String
::from_utf8(output
.stderr
)
112 .map(|m
| if m
.is_empty() { String::from("no error message") }
else { m }
)
113 .unwrap_or_else(|_
| String
::from("non utf8 error message (suppressed)"));
114 worker
.log_warning(msg
);
116 bail
!("terminated by signal");
131 description
: r
#"Send notification mail about new package updates available to the
132 email address configured for 'root@pam')."#,
137 description
: "Only produces output suitable for logging, omitting progress indicators.",
148 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
151 /// Update the APT database
152 pub fn apt_update_database(
155 rpcenv
: &mut dyn RpcEnvironment
,
156 ) -> Result
<String
, Error
> {
158 let auth_id
= rpcenv
.get_auth_id().unwrap();
159 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
161 let upid_str
= WorkerTask
::new_thread("aptupdate", None
, auth_id
, to_stdout
, move |worker
| {
162 do_apt_update(&worker
, quiet
)?
;
164 let mut cache
= apt
::update_cache()?
;
167 let mut notified
= match cache
.notified
{
168 Some(notified
) => notified
,
169 None
=> std
::collections
::HashMap
::new(),
171 let mut to_notify
: Vec
<&APTUpdateInfo
> = Vec
::new();
173 for pkg
in &cache
.package_status
{
174 match notified
.insert(pkg
.package
.to_owned(), pkg
.version
.to_owned()) {
175 Some(notified_version
) => {
176 if notified_version
!= pkg
.version
{
180 None
=> to_notify
.push(pkg
),
183 if !to_notify
.is_empty() {
184 to_notify
.sort_unstable_by_key(|k
| &k
.package
);
185 crate::server
::send_updates_available(&to_notify
)?
;
187 cache
.notified
= Some(notified
);
188 apt
::write_pkg_cache(&cache
)?
;
205 description
: "Package name to get changelog of.",
209 description
: "Package version to get changelog of. Omit to use candidate version.",
219 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
222 /// Retrieve the changelog of the specified package.
223 fn apt_get_changelog(
225 ) -> Result
<Value
, Error
> {
227 let name
= pbs_tools
::json
::required_string_param(¶m
, "name")?
.to_owned();
228 let version
= param
["version"].as_str();
230 let pkg_info
= apt
::list_installed_apt_packages(|data
| {
232 Some(version
) => version
== data
.active_version
,
233 None
=> data
.active_version
== data
.candidate_version
237 if pkg_info
.is_empty() {
238 bail
!("Package '{}' not found", name
);
241 let proxy_config
= read_and_update_proxy_config()?
;
242 let mut client
= pbs_simple_http(proxy_config
);
244 let changelog_url
= &pkg_info
[0].change_log_url
;
245 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
246 if changelog_url
.starts_with("http://download.proxmox.com/") {
247 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, None
))
248 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
251 } else if changelog_url
.starts_with("https://enterprise.proxmox.com/") {
252 let sub
= match subscription
::read_subscription()?
{
254 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription info found")
256 let (key
, id
) = match sub
.key
{
259 Some(id
) => (key
, id
),
261 bail
!("cannot retrieve changelog from enterprise repo: no server id found")
264 None
=> bail
!("cannot retrieve changelog from enterprise repo: no subscription key found")
267 let mut auth_header
= HashMap
::new();
268 auth_header
.insert("Authorization".to_owned(),
269 format
!("Basic {}", base64
::encode(format
!("{}:{}", key
, id
))));
271 let changelog
= pbs_runtime
::block_on(client
.get_string(changelog_url
, Some(&auth_header
)))
272 .map_err(|err
| format_err
!("Error downloading changelog from '{}': {}", changelog_url
, err
))?
;
276 let mut command
= std
::process
::Command
::new("apt-get");
277 command
.arg("changelog");
278 command
.arg("-qq"); // don't display download progress
280 let output
= pbs_tools
::run_command(command
, None
)?
;
294 description
: "List of more relevant packages.",
301 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
304 /// Get package information for important Proxmox Backup Server packages.
305 pub fn get_versions() -> Result
<Vec
<APTUpdateInfo
>, Error
> {
306 const PACKAGES
: &[&str] = &[
310 "proxmox-backup-docs",
311 "proxmox-backup-client",
312 "proxmox-backup-server",
313 "proxmox-mini-journalreader",
314 "proxmox-widget-toolkit",
320 fn unknown_package(package
: String
, extra_info
: Option
<String
>) -> APTUpdateInfo
{
323 title
: "unknown".into(),
324 arch
: "unknown".into(),
325 description
: "unknown".into(),
326 version
: "unknown".into(),
327 old_version
: "unknown".into(),
328 origin
: "unknown".into(),
329 priority
: "unknown".into(),
330 section
: "unknown".into(),
331 change_log_url
: "unknown".into(),
336 let is_kernel
= |name
: &str| name
.starts_with("pve-kernel-");
338 let mut packages
: Vec
<APTUpdateInfo
> = Vec
::new();
339 let pbs_packages
= apt
::list_installed_apt_packages(
341 filter
.installed_version
== Some(filter
.active_version
)
342 && (is_kernel(filter
.package
) || PACKAGES
.contains(&filter
.package
))
347 let running_kernel
= format
!(
348 "running kernel: {}",
349 nix
::sys
::utsname
::uname().release().to_owned()
351 if let Some(proxmox_backup
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup") {
352 let mut proxmox_backup
= proxmox_backup
.clone();
353 proxmox_backup
.extra_info
= Some(running_kernel
);
354 packages
.push(proxmox_backup
);
356 packages
.push(unknown_package("proxmox-backup".into(), Some(running_kernel
)));
359 let version
= pbs_buildcfg
::PROXMOX_PKG_VERSION
;
360 let release
= pbs_buildcfg
::PROXMOX_PKG_RELEASE
;
361 let daemon_version_info
= Some(format
!("running version: {}.{}", version
, release
));
362 if let Some(pkg
) = pbs_packages
.iter().find(|pkg
| pkg
.package
== "proxmox-backup-server") {
363 let mut pkg
= pkg
.clone();
364 pkg
.extra_info
= daemon_version_info
;
367 packages
.push(unknown_package("proxmox-backup".into(), daemon_version_info
));
370 let mut kernel_pkgs
: Vec
<APTUpdateInfo
> = pbs_packages
372 .filter(|pkg
| is_kernel(&pkg
.package
))
375 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
377 let cache
= apt_pkg_native
::Cache
::get_singleton();
378 kernel_pkgs
.sort_by(|left
, right
| {
380 .compare_versions(&left
.old_version
, &right
.old_version
)
384 packages
.append(&mut kernel_pkgs
);
386 // add entry for all packages we're interested in, even if not installed
387 for pkg
in PACKAGES
.iter() {
388 if pkg
== &"proxmox-backup" || pkg
== &"proxmox-backup-server" {
391 match pbs_packages
.iter().find(|item
| &item
.package
== pkg
) {
392 Some(apt_pkg
) => packages
.push(apt_pkg
.to_owned()),
393 None
=> packages
.push(unknown_package(pkg
.to_string(), None
)),
410 description
: "Result from parsing the APT repository files in /etc/apt/.",
413 description
: "List of parsed repository files.",
416 type: APTRepositoryFile
,
420 description
: "List of problematic files.",
423 type: APTRepositoryFileError
,
427 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
430 description
: "List of additional information/warnings about the repositories.",
432 type: APTRepositoryInfo
,
436 description
: "List of standard repositories and their configuration status.",
438 type: APTStandardRepository
,
444 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
447 /// Get APT repository information.
448 pub fn get_repositories() -> Result
<Value
, Error
> {
449 let (files
, errors
, digest
) = proxmox_apt
::repositories
::repositories()?
;
450 let digest
= proxmox
::tools
::digest_to_hex(&digest
);
452 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
454 let infos
= proxmox_apt
::repositories
::check_repositories(&files
, suite
);
455 let standard_repos
= proxmox_apt
::repositories
::standard_repositories(&files
, "pbs", suite
);
462 "standard-repos": standard_repos
,
473 type: APTRepositoryHandle
,
476 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
483 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
486 /// Add the repository identified by the `handle`.
487 /// If the repository is already configured, it will be set to enabled.
489 /// The `digest` parameter asserts that the configuration has not been modified.
490 pub fn add_repository(handle
: APTRepositoryHandle
, digest
: Option
<String
>) -> Result
<(), Error
> {
491 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
493 let suite
= proxmox_apt
::repositories
::get_current_release_codename()?
;
495 if let Some(expected_digest
) = digest
{
496 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
497 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
500 // check if it's already configured first
501 for file
in files
.iter_mut() {
502 for repo
in file
.repositories
.iter_mut() {
503 if repo
.is_referenced_repository(handle
, "pbs", &suite
.to_string()) {
508 repo
.set_enabled(true);
516 let (repo
, path
) = proxmox_apt
::repositories
::get_standard_repository(handle
, "pbs", suite
);
518 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
520 "unable to parse existing file {} - {}",
526 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
527 file
.repositories
.push(repo
);
531 let mut file
= match APTRepositoryFile
::new(&path
)?
{
533 None
=> bail
!("invalid path - {}", path
),
536 file
.repositories
.push(repo
);
551 description
: "Path to the containing file.",
555 description
: "Index within the file (starting from 0).",
559 description
: "Whether the repository should be enabled or not.",
564 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
571 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
574 /// Change the properties of the specified repository.
576 /// The `digest` parameter asserts that the configuration has not been modified.
577 pub fn change_repository(
580 enabled
: Option
<bool
>,
581 digest
: Option
<String
>,
582 ) -> Result
<(), Error
> {
583 let (mut files
, errors
, current_digest
) = proxmox_apt
::repositories
::repositories()?
;
585 if let Some(expected_digest
) = digest
{
586 let current_digest
= proxmox
::tools
::digest_to_hex(¤t_digest
);
587 crate::tools
::assert_if_modified(&expected_digest
, ¤t_digest
)?
;
590 if let Some(error
) = errors
.iter().find(|error
| error
.path
== path
) {
591 bail
!("unable to parse file {} - {}", error
.path
, error
.error
);
594 if let Some(file
) = files
.iter_mut().find(|file
| file
.path
== path
) {
595 if let Some(repo
) = file
.repositories
.get_mut(index
) {
596 if let Some(enabled
) = enabled
{
597 repo
.set_enabled(enabled
);
602 bail
!("invalid index - {}", index
);
605 bail
!("invalid path - {}", path
);
611 const SUBDIRS
: SubdirMap
= &[
612 ("changelog", &Router
::new().get(&API_METHOD_APT_GET_CHANGELOG
)),
613 ("repositories", &Router
::new()
614 .get(&API_METHOD_GET_REPOSITORIES
)
615 .post(&API_METHOD_CHANGE_REPOSITORY
)
616 .put(&API_METHOD_ADD_REPOSITORY
)
618 ("update", &Router
::new()
619 .get(&API_METHOD_APT_UPDATE_AVAILABLE
)
620 .post(&API_METHOD_APT_UPDATE_DATABASE
)
622 ("versions", &Router
::new().get(&API_METHOD_GET_VERSIONS
)),
625 pub const ROUTER
: Router
= Router
::new()
626 .get(&list_subdirs_api_method
!(SUBDIRS
))