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