]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
f7328b810355d0675672239238c127199a90c218
[proxmox-backup.git] / src / api2 / node / apt.rs
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;
5
6 use proxmox_router::{
7 list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
8 };
9 use proxmox_schema::api;
10 use proxmox_sys::fs::{replace_file, CreateOptions};
11
12 use proxmox_apt::repositories::{
13 APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
14 APTStandardRepository,
15 };
16 use proxmox_http::ProxyConfig;
17
18 use pbs_api_types::{
19 APTUpdateInfo, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
20 UPID_SCHEMA,
21 };
22 use pbs_buildcfg::PROXMOX_BACKUP_SUBSCRIPTION_FN;
23
24 use crate::config::node;
25 use crate::tools::{apt, pbs_simple_http};
26 use proxmox_rest_server::WorkerTask;
27
28 #[api(
29 input: {
30 properties: {
31 node: {
32 schema: NODE_SCHEMA,
33 },
34 },
35 },
36 returns: {
37 description: "A list of packages with available updates.",
38 type: Array,
39 items: {
40 type: APTUpdateInfo
41 },
42 },
43 protected: true,
44 access: {
45 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
46 },
47 )]
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));
53 }
54 }
55
56 let cache = apt::update_cache()?;
57
58 Ok(json!(cache.package_status))
59 }
60
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
63
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)
68 } else {
69 match std::fs::remove_file(PROXY_CFG_FN) {
70 Ok(()) => Ok(()),
71 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
72 Err(err) => bail!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN, err),
73 }
74 }
75 }
76
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()
80 } else {
81 None
82 };
83 update_apt_proxy_config(proxy_config.as_ref())?;
84
85 Ok(proxy_config)
86 }
87
88 fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
89 if !quiet {
90 worker.log_message("starting apt-get update")
91 }
92
93 read_and_update_proxy_config()?;
94
95 let mut command = std::process::Command::new("apt-get");
96 command.arg("update");
97
98 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
99 let output = command
100 .output()
101 .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?;
102
103 if !quiet {
104 worker.log_message(String::from_utf8(output.stdout)?);
105 }
106
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| {
112 if m.is_empty() {
113 String::from("no error message")
114 } else {
115 m
116 }
117 })
118 .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
119 worker.log_warning(msg);
120 } else {
121 bail!("terminated by signal");
122 }
123 }
124 Ok(())
125 }
126
127 #[api(
128 protected: true,
129 input: {
130 properties: {
131 node: {
132 schema: NODE_SCHEMA,
133 },
134 notify: {
135 type: bool,
136 description: r#"Send notification mail about new package updates available to the
137 email address configured for 'root@pam')."#,
138 default: false,
139 optional: true,
140 },
141 quiet: {
142 description: "Only produces output suitable for logging, omitting progress indicators.",
143 type: bool,
144 default: false,
145 optional: true,
146 },
147 },
148 },
149 returns: {
150 schema: UPID_SCHEMA,
151 },
152 access: {
153 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
154 },
155 )]
156 /// Update the APT database
157 pub fn apt_update_database(
158 notify: bool,
159 quiet: bool,
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;
164
165 let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
166 do_apt_update(&worker, quiet)?;
167
168 let mut cache = apt::update_cache()?;
169
170 if notify {
171 let mut notified = match cache.notified {
172 Some(notified) => notified,
173 None => std::collections::HashMap::new(),
174 };
175 let mut to_notify: Vec<&APTUpdateInfo> = Vec::new();
176
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 {
181 to_notify.push(pkg);
182 }
183 }
184 None => to_notify.push(pkg),
185 }
186 }
187 if !to_notify.is_empty() {
188 to_notify.sort_unstable_by_key(|k| &k.package);
189 crate::server::send_updates_available(&to_notify)?;
190 }
191 cache.notified = Some(notified);
192 apt::write_pkg_cache(&cache)?;
193 }
194
195 Ok(())
196 })?;
197
198 Ok(upid_str)
199 }
200
201 #[api(
202 protected: true,
203 input: {
204 properties: {
205 node: {
206 schema: NODE_SCHEMA,
207 },
208 name: {
209 description: "Package name to get changelog of.",
210 type: String,
211 },
212 version: {
213 description: "Package version to get changelog of. Omit to use candidate version.",
214 type: String,
215 optional: true,
216 },
217 },
218 },
219 returns: {
220 schema: UPID_SCHEMA,
221 },
222 access: {
223 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
224 },
225 )]
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(&param, "name")?.to_owned();
229 let version = param["version"].as_str();
230
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,
235 },
236 Some(&name),
237 );
238
239 if pkg_info.is_empty() {
240 bail!("Package '{}' not found", name);
241 }
242
243 let proxy_config = read_and_update_proxy_config()?;
244 let client = pbs_simple_http(proxy_config);
245
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))
250 .map_err(|err| {
251 format_err!(
252 "Error downloading changelog from '{}': {}",
253 changelog_url,
254 err
255 )
256 })?;
257 Ok(json!(changelog))
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],
262 )? {
263 Some(sub) => sub,
264 None => {
265 bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
266 }
267 };
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"),
272 },
273 None => {
274 bail!("cannot retrieve changelog from enterprise repo: no subscription key found")
275 }
276 };
277
278 let mut auth_header = HashMap::new();
279 auth_header.insert(
280 "Authorization".to_owned(),
281 format!("Basic {}", base64::encode(format!("{}:{}", key, id))),
282 );
283
284 let changelog =
285 proxmox_async::runtime::block_on(client.get_string(changelog_url, Some(&auth_header)))
286 .map_err(|err| {
287 format_err!(
288 "Error downloading changelog from '{}': {}",
289 changelog_url,
290 err
291 )
292 })?;
293 Ok(json!(changelog))
294 } else {
295 let mut command = std::process::Command::new("apt-get");
296 command.arg("changelog");
297 command.arg("-qq"); // don't display download progress
298 command.arg(name);
299 let output = proxmox_sys::command::run_command(command, None)?;
300 Ok(json!(output))
301 }
302 }
303
304 #[api(
305 input: {
306 properties: {
307 node: {
308 schema: NODE_SCHEMA,
309 },
310 },
311 },
312 returns: {
313 description: "List of more relevant packages.",
314 type: Array,
315 items: {
316 type: APTUpdateInfo,
317 },
318 },
319 access: {
320 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
321 },
322 )]
323 /// Get package information for important Proxmox Backup Server packages.
324 pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
325 const PACKAGES: &[&str] = &[
326 "ifupdown2",
327 "libjs-extjs",
328 "proxmox-backup",
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",
336 "pve-xtermjs",
337 "smartmontools",
338 "zfsutils-linux",
339 ];
340
341 fn unknown_package(package: String, extra_info: Option<String>) -> APTUpdateInfo {
342 APTUpdateInfo {
343 package,
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(),
353 extra_info,
354 }
355 }
356
357 let is_kernel = |name: &str| name.starts_with("pve-kernel-");
358
359 let mut packages: Vec<APTUpdateInfo> = Vec::new();
360 let pbs_packages = apt::list_installed_apt_packages(
361 |filter| {
362 filter.installed_version == Some(filter.active_version)
363 && (is_kernel(filter.package) || PACKAGES.contains(&filter.package))
364 },
365 None,
366 );
367
368 let running_kernel = format!(
369 "running kernel: {}",
370 std::str::from_utf8(nix::sys::utsname::uname()?.release().as_bytes())?.to_owned()
371 );
372 if let Some(proxmox_backup) = pbs_packages
373 .iter()
374 .find(|pkg| pkg.package == "proxmox-backup")
375 {
376 let mut proxmox_backup = proxmox_backup.clone();
377 proxmox_backup.extra_info = Some(running_kernel);
378 packages.push(proxmox_backup);
379 } else {
380 packages.push(unknown_package(
381 "proxmox-backup".into(),
382 Some(running_kernel),
383 ));
384 }
385
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
390 .iter()
391 .find(|pkg| pkg.package == "proxmox-backup-server")
392 {
393 let mut pkg = pkg.clone();
394 pkg.extra_info = daemon_version_info;
395 packages.push(pkg);
396 } else {
397 packages.push(unknown_package(
398 "proxmox-backup".into(),
399 daemon_version_info,
400 ));
401 }
402
403 let mut kernel_pkgs: Vec<APTUpdateInfo> = pbs_packages
404 .iter()
405 .filter(|pkg| is_kernel(&pkg.package))
406 .cloned()
407 .collect();
408 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
409 {
410 let cache = apt_pkg_native::Cache::get_singleton();
411 kernel_pkgs.sort_by(|left, right| {
412 cache
413 .compare_versions(&left.old_version, &right.old_version)
414 .reverse()
415 });
416 }
417 packages.append(&mut kernel_pkgs);
418
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" {
422 continue;
423 }
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)),
427 }
428 }
429
430 Ok(packages)
431 }
432
433 #[api(
434 input: {
435 properties: {
436 node: {
437 schema: NODE_SCHEMA,
438 },
439 },
440 },
441 returns: {
442 type: Object,
443 description: "Result from parsing the APT repository files in /etc/apt/.",
444 properties: {
445 files: {
446 description: "List of parsed repository files.",
447 type: Array,
448 items: {
449 type: APTRepositoryFile,
450 },
451 },
452 errors: {
453 description: "List of problematic files.",
454 type: Array,
455 items: {
456 type: APTRepositoryFileError,
457 },
458 },
459 digest: {
460 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
461 },
462 infos: {
463 description: "List of additional information/warnings about the repositories.",
464 items: {
465 type: APTRepositoryInfo,
466 },
467 },
468 "standard-repos": {
469 description: "List of standard repositories and their configuration status.",
470 items: {
471 type: APTStandardRepository,
472 },
473 },
474 },
475 },
476 access: {
477 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
478 },
479 )]
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);
484
485 let suite = proxmox_apt::repositories::get_current_release_codename()?;
486
487 let infos = proxmox_apt::repositories::check_repositories(&files, suite);
488 let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pbs", suite);
489
490 Ok(json!({
491 "files": files,
492 "errors": errors,
493 "digest": digest,
494 "infos": infos,
495 "standard-repos": standard_repos,
496 }))
497 }
498
499 #[api(
500 input: {
501 properties: {
502 node: {
503 schema: NODE_SCHEMA,
504 },
505 handle: {
506 type: APTRepositoryHandle,
507 },
508 digest: {
509 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
510 optional: true,
511 },
512 },
513 },
514 protected: true,
515 access: {
516 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
517 },
518 )]
519 /// Add the repository identified by the `handle`.
520 /// If the repository is already configured, it will be set to enabled.
521 ///
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()?;
525
526 let suite = proxmox_apt::repositories::get_current_release_codename()?;
527
528 if let Some(expected_digest) = digest {
529 let current_digest = hex::encode(current_digest);
530 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
531 }
532
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()) {
537 if repo.enabled {
538 return Ok(());
539 }
540
541 repo.set_enabled(true);
542 file.write()?;
543
544 return Ok(());
545 }
546 }
547 }
548
549 let (repo, path) = proxmox_apt::repositories::get_standard_repository(handle, "pbs", suite);
550
551 if let Some(error) = errors.iter().find(|error| error.path == path) {
552 bail!(
553 "unable to parse existing file {} - {}",
554 error.path,
555 error.error,
556 );
557 }
558
559 if let Some(file) = files
560 .iter_mut()
561 .find(|file| file.path.as_ref() == Some(&path))
562 {
563 file.repositories.push(repo);
564
565 file.write()?;
566 } else {
567 let mut file = match APTRepositoryFile::new(&path)? {
568 Some(file) => file,
569 None => bail!("invalid path - {}", path),
570 };
571
572 file.repositories.push(repo);
573
574 file.write()?;
575 }
576
577 Ok(())
578 }
579
580 #[api(
581 input: {
582 properties: {
583 node: {
584 schema: NODE_SCHEMA,
585 },
586 path: {
587 description: "Path to the containing file.",
588 type: String,
589 },
590 index: {
591 description: "Index within the file (starting from 0).",
592 type: usize,
593 },
594 enabled: {
595 description: "Whether the repository should be enabled or not.",
596 type: bool,
597 optional: true,
598 },
599 digest: {
600 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
601 optional: true,
602 },
603 },
604 },
605 protected: true,
606 access: {
607 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
608 },
609 )]
610 /// Change the properties of the specified repository.
611 ///
612 /// The `digest` parameter asserts that the configuration has not been modified.
613 pub fn change_repository(
614 path: String,
615 index: usize,
616 enabled: Option<bool>,
617 digest: Option<String>,
618 ) -> Result<(), Error> {
619 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
620
621 if let Some(expected_digest) = digest {
622 let current_digest = hex::encode(current_digest);
623 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
624 }
625
626 if let Some(error) = errors.iter().find(|error| error.path == path) {
627 bail!("unable to parse file {} - {}", error.path, error.error);
628 }
629
630 if let Some(file) = files
631 .iter_mut()
632 .find(|file| file.path.as_ref() == Some(&path))
633 {
634 if let Some(repo) = file.repositories.get_mut(index) {
635 if let Some(enabled) = enabled {
636 repo.set_enabled(enabled);
637 }
638
639 file.write()?;
640 } else {
641 bail!("invalid index - {}", index);
642 }
643 } else {
644 bail!("invalid path - {}", path);
645 }
646
647 Ok(())
648 }
649
650 const SUBDIRS: SubdirMap = &[
651 (
652 "changelog",
653 &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
654 ),
655 (
656 "repositories",
657 &Router::new()
658 .get(&API_METHOD_GET_REPOSITORIES)
659 .post(&API_METHOD_CHANGE_REPOSITORY)
660 .put(&API_METHOD_ADD_REPOSITORY),
661 ),
662 (
663 "update",
664 &Router::new()
665 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
666 .post(&API_METHOD_APT_UPDATE_DATABASE),
667 ),
668 ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
669 ];
670
671 pub const ROUTER: Router = Router::new()
672 .get(&list_subdirs_api_method!(SUBDIRS))
673 .subdirs(SUBDIRS);