]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
a31e73a143943413d14725cba6fe0471e71ac94a
[proxmox-backup.git] / src / api2 / node / apt.rs
1 use anyhow::{Error, bail, format_err};
2 use serde_json::{json, Value};
3 use std::collections::HashMap;
4
5 use proxmox::tools::fs::{replace_file, CreateOptions};
6 use proxmox_router::{
7 list_subdirs_api_method, RpcEnvironment, RpcEnvironmentType, Permission, Router, SubdirMap
8 };
9 use proxmox_schema::api;
10
11 use proxmox_apt::repositories::{
12 APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
13 APTStandardRepository,
14 };
15 use proxmox_http::ProxyConfig;
16
17 use pbs_api_types::{
18 APTUpdateInfo, NODE_SCHEMA, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA,
19 PRIV_SYS_AUDIT, PRIV_SYS_MODIFY,
20 };
21
22 use crate::config::node;
23 use proxmox_rest_server::WorkerTask;
24 use crate::tools::{
25 apt,
26 pbs_simple_http,
27 subscription,
28 };
29
30 #[api(
31 input: {
32 properties: {
33 node: {
34 schema: NODE_SCHEMA,
35 },
36 },
37 },
38 returns: {
39 description: "A list of packages with available updates.",
40 type: Array,
41 items: {
42 type: APTUpdateInfo
43 },
44 },
45 protected: true,
46 access: {
47 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
48 },
49 )]
50 /// List available APT updates
51 fn apt_update_available(_param: Value) -> Result<Value, Error> {
52
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));
56 }
57 }
58
59 let cache = apt::update_cache()?;
60
61 Ok(json!(cache.package_status))
62 }
63
64 pub fn update_apt_proxy_config(proxy_config: Option<&ProxyConfig>) -> Result<(), Error> {
65
66 const PROXY_CFG_FN: &str = "/etc/apt/apt.conf.d/76pveproxy"; // use same file as PVE
67
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)
72 } else {
73 match std::fs::remove_file(PROXY_CFG_FN) {
74 Ok(()) => Ok(()),
75 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
76 Err(err) => bail!("failed to remove proxy config '{}' - {}", PROXY_CFG_FN, err),
77 }
78 }
79 }
80
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()
84 } else {
85 None
86 };
87 update_apt_proxy_config(proxy_config.as_ref())?;
88
89 Ok(proxy_config)
90 }
91
92 fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
93 if !quiet { worker.log_message("starting apt-get update") }
94
95 read_and_update_proxy_config()?;
96
97 let mut command = std::process::Command::new("apt-get");
98 command.arg("update");
99
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))?;
103
104 if !quiet {
105 worker.log_message(String::from_utf8(output.stdout)?);
106 }
107
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);
115 } else {
116 bail!("terminated by signal");
117 }
118 }
119 Ok(())
120 }
121
122 #[api(
123 protected: true,
124 input: {
125 properties: {
126 node: {
127 schema: NODE_SCHEMA,
128 },
129 notify: {
130 type: bool,
131 description: r#"Send notification mail about new package updates available to the
132 email address configured for 'root@pam')."#,
133 default: false,
134 optional: true,
135 },
136 quiet: {
137 description: "Only produces output suitable for logging, omitting progress indicators.",
138 type: bool,
139 default: false,
140 optional: true,
141 },
142 },
143 },
144 returns: {
145 schema: UPID_SCHEMA,
146 },
147 access: {
148 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
149 },
150 )]
151 /// Update the APT database
152 pub fn apt_update_database(
153 notify: bool,
154 quiet: bool,
155 rpcenv: &mut dyn RpcEnvironment,
156 ) -> Result<String, Error> {
157
158 let auth_id = rpcenv.get_auth_id().unwrap();
159 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
160
161 let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
162 do_apt_update(&worker, quiet)?;
163
164 let mut cache = apt::update_cache()?;
165
166 if notify {
167 let mut notified = match cache.notified {
168 Some(notified) => notified,
169 None => std::collections::HashMap::new(),
170 };
171 let mut to_notify: Vec<&APTUpdateInfo> = Vec::new();
172
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 {
177 to_notify.push(pkg);
178 }
179 },
180 None => to_notify.push(pkg),
181 }
182 }
183 if !to_notify.is_empty() {
184 to_notify.sort_unstable_by_key(|k| &k.package);
185 crate::server::send_updates_available(&to_notify)?;
186 }
187 cache.notified = Some(notified);
188 apt::write_pkg_cache(&cache)?;
189 }
190
191 Ok(())
192 })?;
193
194 Ok(upid_str)
195 }
196
197 #[api(
198 protected: true,
199 input: {
200 properties: {
201 node: {
202 schema: NODE_SCHEMA,
203 },
204 name: {
205 description: "Package name to get changelog of.",
206 type: String,
207 },
208 version: {
209 description: "Package version to get changelog of. Omit to use candidate version.",
210 type: String,
211 optional: true,
212 },
213 },
214 },
215 returns: {
216 schema: UPID_SCHEMA,
217 },
218 access: {
219 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
220 },
221 )]
222 /// Retrieve the changelog of the specified package.
223 fn apt_get_changelog(
224 param: Value,
225 ) -> Result<Value, Error> {
226
227 let name = pbs_tools::json::required_string_param(&param, "name")?.to_owned();
228 let version = param["version"].as_str();
229
230 let pkg_info = apt::list_installed_apt_packages(|data| {
231 match version {
232 Some(version) => version == data.active_version,
233 None => data.active_version == data.candidate_version
234 }
235 }, Some(&name));
236
237 if pkg_info.is_empty() {
238 bail!("Package '{}' not found", name);
239 }
240
241 let proxy_config = read_and_update_proxy_config()?;
242 let mut client = pbs_simple_http(proxy_config);
243
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))?;
249 Ok(json!(changelog))
250
251 } else if changelog_url.starts_with("https://enterprise.proxmox.com/") {
252 let sub = match subscription::read_subscription()? {
253 Some(sub) => sub,
254 None => bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
255 };
256 let (key, id) = match sub.key {
257 Some(key) => {
258 match sub.serverid {
259 Some(id) => (key, id),
260 None =>
261 bail!("cannot retrieve changelog from enterprise repo: no server id found")
262 }
263 },
264 None => bail!("cannot retrieve changelog from enterprise repo: no subscription key found")
265 };
266
267 let mut auth_header = HashMap::new();
268 auth_header.insert("Authorization".to_owned(),
269 format!("Basic {}", base64::encode(format!("{}:{}", key, id))));
270
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))?;
273 Ok(json!(changelog))
274
275 } else {
276 let mut command = std::process::Command::new("apt-get");
277 command.arg("changelog");
278 command.arg("-qq"); // don't display download progress
279 command.arg(name);
280 let output = pbs_tools::run_command(command, None)?;
281 Ok(json!(output))
282 }
283 }
284
285 #[api(
286 input: {
287 properties: {
288 node: {
289 schema: NODE_SCHEMA,
290 },
291 },
292 },
293 returns: {
294 description: "List of more relevant packages.",
295 type: Array,
296 items: {
297 type: APTUpdateInfo,
298 },
299 },
300 access: {
301 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
302 },
303 )]
304 /// Get package information for important Proxmox Backup Server packages.
305 pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
306 const PACKAGES: &[&str] = &[
307 "ifupdown2",
308 "libjs-extjs",
309 "proxmox-backup",
310 "proxmox-backup-docs",
311 "proxmox-backup-client",
312 "proxmox-backup-server",
313 "proxmox-mini-journalreader",
314 "proxmox-widget-toolkit",
315 "pve-xtermjs",
316 "smartmontools",
317 "zfsutils-linux",
318 ];
319
320 fn unknown_package(package: String, extra_info: Option<String>) -> APTUpdateInfo {
321 APTUpdateInfo {
322 package,
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(),
332 extra_info,
333 }
334 }
335
336 let is_kernel = |name: &str| name.starts_with("pve-kernel-");
337
338 let mut packages: Vec<APTUpdateInfo> = Vec::new();
339 let pbs_packages = apt::list_installed_apt_packages(
340 |filter| {
341 filter.installed_version == Some(filter.active_version)
342 && (is_kernel(filter.package) || PACKAGES.contains(&filter.package))
343 },
344 None,
345 );
346
347 let running_kernel = format!(
348 "running kernel: {}",
349 nix::sys::utsname::uname().release().to_owned()
350 );
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);
355 } else {
356 packages.push(unknown_package("proxmox-backup".into(), Some(running_kernel)));
357 }
358
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;
365 packages.push(pkg);
366 } else {
367 packages.push(unknown_package("proxmox-backup".into(), daemon_version_info));
368 }
369
370 let mut kernel_pkgs: Vec<APTUpdateInfo> = pbs_packages
371 .iter()
372 .filter(|pkg| is_kernel(&pkg.package))
373 .cloned()
374 .collect();
375 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
376 {
377 let cache = apt_pkg_native::Cache::get_singleton();
378 kernel_pkgs.sort_by(|left, right| {
379 cache
380 .compare_versions(&left.old_version, &right.old_version)
381 .reverse()
382 });
383 }
384 packages.append(&mut kernel_pkgs);
385
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" {
389 continue;
390 }
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)),
394 }
395 }
396
397 Ok(packages)
398 }
399
400 #[api(
401 input: {
402 properties: {
403 node: {
404 schema: NODE_SCHEMA,
405 },
406 },
407 },
408 returns: {
409 type: Object,
410 description: "Result from parsing the APT repository files in /etc/apt/.",
411 properties: {
412 files: {
413 description: "List of parsed repository files.",
414 type: Array,
415 items: {
416 type: APTRepositoryFile,
417 },
418 },
419 errors: {
420 description: "List of problematic files.",
421 type: Array,
422 items: {
423 type: APTRepositoryFileError,
424 },
425 },
426 digest: {
427 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
428 },
429 infos: {
430 description: "List of additional information/warnings about the repositories.",
431 items: {
432 type: APTRepositoryInfo,
433 },
434 },
435 "standard-repos": {
436 description: "List of standard repositories and their configuration status.",
437 items: {
438 type: APTStandardRepository,
439 },
440 },
441 },
442 },
443 access: {
444 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
445 },
446 )]
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);
451
452 let suite = proxmox_apt::repositories::get_current_release_codename()?;
453
454 let infos = proxmox_apt::repositories::check_repositories(&files, suite);
455 let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pbs", suite);
456
457 Ok(json!({
458 "files": files,
459 "errors": errors,
460 "digest": digest,
461 "infos": infos,
462 "standard-repos": standard_repos,
463 }))
464 }
465
466 #[api(
467 input: {
468 properties: {
469 node: {
470 schema: NODE_SCHEMA,
471 },
472 handle: {
473 type: APTRepositoryHandle,
474 },
475 digest: {
476 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
477 optional: true,
478 },
479 },
480 },
481 protected: true,
482 access: {
483 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
484 },
485 )]
486 /// Add the repository identified by the `handle`.
487 /// If the repository is already configured, it will be set to enabled.
488 ///
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()?;
492
493 let suite = proxmox_apt::repositories::get_current_release_codename()?;
494
495 if let Some(expected_digest) = digest {
496 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
497 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
498 }
499
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()) {
504 if repo.enabled {
505 return Ok(());
506 }
507
508 repo.set_enabled(true);
509 file.write()?;
510
511 return Ok(());
512 }
513 }
514 }
515
516 let (repo, path) = proxmox_apt::repositories::get_standard_repository(handle, "pbs", suite);
517
518 if let Some(error) = errors.iter().find(|error| error.path == path) {
519 bail!(
520 "unable to parse existing file {} - {}",
521 error.path,
522 error.error,
523 );
524 }
525
526 if let Some(file) = files.iter_mut().find(|file| file.path == path) {
527 file.repositories.push(repo);
528
529 file.write()?;
530 } else {
531 let mut file = match APTRepositoryFile::new(&path)? {
532 Some(file) => file,
533 None => bail!("invalid path - {}", path),
534 };
535
536 file.repositories.push(repo);
537
538 file.write()?;
539 }
540
541 Ok(())
542 }
543
544 #[api(
545 input: {
546 properties: {
547 node: {
548 schema: NODE_SCHEMA,
549 },
550 path: {
551 description: "Path to the containing file.",
552 type: String,
553 },
554 index: {
555 description: "Index within the file (starting from 0).",
556 type: usize,
557 },
558 enabled: {
559 description: "Whether the repository should be enabled or not.",
560 type: bool,
561 optional: true,
562 },
563 digest: {
564 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
565 optional: true,
566 },
567 },
568 },
569 protected: true,
570 access: {
571 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
572 },
573 )]
574 /// Change the properties of the specified repository.
575 ///
576 /// The `digest` parameter asserts that the configuration has not been modified.
577 pub fn change_repository(
578 path: String,
579 index: usize,
580 enabled: Option<bool>,
581 digest: Option<String>,
582 ) -> Result<(), Error> {
583 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
584
585 if let Some(expected_digest) = digest {
586 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
587 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
588 }
589
590 if let Some(error) = errors.iter().find(|error| error.path == path) {
591 bail!("unable to parse file {} - {}", error.path, error.error);
592 }
593
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);
598 }
599
600 file.write()?;
601 } else {
602 bail!("invalid index - {}", index);
603 }
604 } else {
605 bail!("invalid path - {}", path);
606 }
607
608 Ok(())
609 }
610
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)
617 ),
618 ("update", &Router::new()
619 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
620 .post(&API_METHOD_APT_UPDATE_DATABASE)
621 ),
622 ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
623 ];
624
625 pub const ROUTER: Router = Router::new()
626 .get(&list_subdirs_api_method!(SUBDIRS))
627 .subdirs(SUBDIRS);