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