]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node/apt.rs
fixup imports in tests and examples
[proxmox-backup.git] / src / api2 / node / apt.rs
CommitLineData
9e61c01c 1use anyhow::{Error, bail, format_err};
a4e86972 2use serde_json::{json, Value};
137a6ebc 3use std::collections::HashMap;
a4e86972 4
e6513bd5 5use proxmox::list_subdirs_api_method;
fa3f0584
TL
6use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
7use proxmox::api::router::{Router, SubdirMap};
440472cb 8use proxmox::tools::fs::{replace_file, CreateOptions};
a4e86972 9
d830804f 10use proxmox_apt::repositories::{
289738dc
FE
11 APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
12 APTStandardRepository,
d830804f 13};
1d781c5b 14use proxmox_http::ProxyConfig;
4229633d 15
a1b71c3c 16use crate::config::node;
fa3f0584 17use crate::server::WorkerTask;
57889533
FG
18use crate::tools::{
19 apt,
20 pbs_simple_http,
21 subscription,
22};
fa3f0584 23use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
d830804f 24use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA};
a4e86972 25
a4e86972
SR
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,
e6513bd5
TL
37 items: {
38 type: APTUpdateInfo
39 },
a4e86972 40 },
33508b12 41 protected: true,
a4e86972
SR
42 access: {
43 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
44 },
45)]
46/// List available APT updates
47fn apt_update_available(_param: Value) -> Result<Value, Error> {
33508b12 48
b92cad09
FG
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 }
33508b12
TL
53 }
54
55 let cache = apt::update_cache()?;
56
38556bf6 57 Ok(json!(cache.package_status))
a4e86972
SR
58}
59
440472cb
DM
60pub 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);
e9c2638f 67 replace_file(PROXY_CFG_FN, data.as_bytes(), CreateOptions::new())
440472cb 68 } else {
e9c2638f
TL
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 }
440472cb 74 }
440472cb
DM
75}
76
77fn 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
b2825575
TL
88fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
89 if !quiet { worker.log("starting apt-get update") }
90
440472cb 91 read_and_update_proxy_config()?;
b2825575
TL
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
fa3f0584 118#[api(
27fde647 119 protected: true,
fa3f0584
TL
120 input: {
121 properties: {
122 node: {
123 schema: NODE_SCHEMA,
124 },
86d60245
TL
125 notify: {
126 type: bool,
d1d74c43 127 description: r#"Send notification mail about new package updates available to the
86d60245 128 email address configured for 'root@pam')."#,
86d60245 129 default: false,
39735609 130 optional: true,
86d60245 131 },
fa3f0584
TL
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
148pub fn apt_update_database(
3e461dec
FG
149 notify: bool,
150 quiet: bool,
fa3f0584
TL
151 rpcenv: &mut dyn RpcEnvironment,
152) -> Result<String, Error> {
153
e6dc35ac 154 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
39735609 155 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
fa3f0584 156
e6dc35ac 157 let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
b2825575 158 do_apt_update(&worker, quiet)?;
86d60245
TL
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() {
0e16f57e 180 to_notify.sort_unstable_by_key(|k| &k.package);
86d60245
TL
181 crate::server::send_updates_available(&to_notify)?;
182 }
183 cache.notified = Some(notified);
184 apt::write_pkg_cache(&cache)?;
185 }
186
fa3f0584
TL
187 Ok(())
188 })?;
189
190 Ok(upid_str)
191}
192
9e61c01c 193#[api(
440472cb 194 protected: true,
9e61c01c
SR
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.
219fn apt_get_changelog(
220 param: Value,
221) -> Result<Value, Error> {
222
3c8c2827 223 let name = pbs_tools::json::required_string_param(&param, "name")?.to_owned();
9e61c01c
SR
224 let version = param["version"].as_str();
225
e6513bd5 226 let pkg_info = apt::list_installed_apt_packages(|data| {
9e61c01c
SR
227 match version {
228 Some(version) => version == data.active_version,
229 None => data.active_version == data.candidate_version
230 }
231 }, Some(&name));
232
3984a5fd 233 if pkg_info.is_empty() {
9e61c01c
SR
234 bail!("Package '{}' not found", name);
235 }
236
440472cb 237 let proxy_config = read_and_update_proxy_config()?;
57889533 238 let mut client = pbs_simple_http(proxy_config);
26153589 239
9e61c01c
SR
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/") {
d420962f 243 let changelog = pbs_runtime::block_on(client.get_string(changelog_url, None))
6eb41487 244 .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
38556bf6 245 Ok(json!(changelog))
137a6ebc
SR
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
d420962f 267 let changelog = pbs_runtime::block_on(client.get_string(changelog_url, Some(&auth_header)))
137a6ebc 268 .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
38556bf6 269 Ok(json!(changelog))
137a6ebc 270
9e61c01c
SR
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)?;
38556bf6 277 Ok(json!(output))
9e61c01c
SR
278 }
279}
280
ed2beb33
TL
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.
5e293f13 301pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
ed2beb33
TL
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
2decf85d 316 fn unknown_package(package: String, extra_info: Option<String>) -> APTUpdateInfo {
ed2beb33
TL
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(),
2decf85d 328 extra_info,
ed2beb33
TL
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 );
2decf85d 342
51ac17b5 343 let running_kernel = format!(
8c62c15f 344 "running kernel: {}",
51ac17b5
ML
345 nix::sys::utsname::uname().release().to_owned()
346 );
bc1e52bc 347 if let Some(proxmox_backup) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup") {
2decf85d 348 let mut proxmox_backup = proxmox_backup.clone();
51ac17b5 349 proxmox_backup.extra_info = Some(running_kernel);
2decf85d 350 packages.push(proxmox_backup);
ed2beb33 351 } else {
bc1e52bc 352 packages.push(unknown_package("proxmox-backup".into(), Some(running_kernel)));
ed2beb33
TL
353 }
354
a12b1be7
WB
355 let version = pbs_buildcfg::PROXMOX_PKG_VERSION;
356 let release = pbs_buildcfg::PROXMOX_PKG_RELEASE;
e754da3a 357 let daemon_version_info = Some(format!("running version: {}.{}", version, release));
bc1e52bc 358 if let Some(pkg) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup-server") {
2decf85d 359 let mut pkg = pkg.clone();
e754da3a 360 pkg.extra_info = daemon_version_info;
2decf85d 361 packages.push(pkg);
e754da3a
TL
362 } else {
363 packages.push(unknown_package("proxmox-backup".into(), daemon_version_info));
ed2beb33
TL
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()),
2decf85d 389 None => packages.push(unknown_package(pkg.to_string(), None)),
ed2beb33
TL
390 }
391 }
392
5e293f13 393 Ok(packages)
ed2beb33
TL
394}
395
d830804f
FE
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.
444pub 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
2eac3594
FE
448 let suite = proxmox_apt::repositories::get_current_release_codename()?;
449
0b12a5a6
FE
450 let infos = proxmox_apt::repositories::check_repositories(&files, suite);
451 let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pbs", suite);
d830804f
FE
452
453 Ok(json!({
454 "files": files,
455 "errors": errors,
456 "digest": digest,
457 "infos": infos,
458 "standard-repos": standard_repos,
459 }))
460}
461
289738dc
FE
462#[api(
463 input: {
464 properties: {
465 node: {
466 schema: NODE_SCHEMA,
467 },
468 handle: {
469 type: APTRepositoryHandle,
470 },
471 digest: {
472 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
473 optional: true,
474 },
475 },
476 },
477 protected: true,
478 access: {
479 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
480 },
481)]
482/// Add the repository identified by the `handle`.
483/// If the repository is already configured, it will be set to enabled.
484///
485/// The `digest` parameter asserts that the configuration has not been modified.
486pub fn add_repository(handle: APTRepositoryHandle, digest: Option<String>) -> Result<(), Error> {
487 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
488
2eac3594
FE
489 let suite = proxmox_apt::repositories::get_current_release_codename()?;
490
289738dc
FE
491 if let Some(expected_digest) = digest {
492 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
493 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
494 }
495
496 // check if it's already configured first
497 for file in files.iter_mut() {
498 for repo in file.repositories.iter_mut() {
0b12a5a6 499 if repo.is_referenced_repository(handle, "pbs", &suite.to_string()) {
289738dc
FE
500 if repo.enabled {
501 return Ok(());
502 }
503
504 repo.set_enabled(true);
505 file.write()?;
506
507 return Ok(());
508 }
509 }
510 }
511
0b12a5a6 512 let (repo, path) = proxmox_apt::repositories::get_standard_repository(handle, "pbs", suite);
289738dc
FE
513
514 if let Some(error) = errors.iter().find(|error| error.path == path) {
515 bail!(
516 "unable to parse existing file {} - {}",
517 error.path,
518 error.error,
519 );
520 }
521
522 if let Some(file) = files.iter_mut().find(|file| file.path == path) {
523 file.repositories.push(repo);
524
525 file.write()?;
526 } else {
527 let mut file = match APTRepositoryFile::new(&path)? {
528 Some(file) => file,
529 None => bail!("invalid path - {}", path),
530 };
531
532 file.repositories.push(repo);
533
534 file.write()?;
535 }
536
537 Ok(())
538}
539
540#[api(
541 input: {
542 properties: {
543 node: {
544 schema: NODE_SCHEMA,
545 },
546 path: {
547 description: "Path to the containing file.",
548 type: String,
549 },
550 index: {
551 description: "Index within the file (starting from 0).",
552 type: usize,
553 },
554 enabled: {
555 description: "Whether the repository should be enabled or not.",
556 type: bool,
557 optional: true,
558 },
559 digest: {
560 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
561 optional: true,
562 },
563 },
564 },
565 protected: true,
566 access: {
567 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
568 },
569)]
570/// Change the properties of the specified repository.
571///
572/// The `digest` parameter asserts that the configuration has not been modified.
573pub fn change_repository(
574 path: String,
575 index: usize,
576 enabled: Option<bool>,
577 digest: Option<String>,
578) -> Result<(), Error> {
579 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
580
581 if let Some(expected_digest) = digest {
582 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
583 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
584 }
585
586 if let Some(error) = errors.iter().find(|error| error.path == path) {
587 bail!("unable to parse file {} - {}", error.path, error.error);
588 }
589
590 if let Some(file) = files.iter_mut().find(|file| file.path == path) {
591 if let Some(repo) = file.repositories.get_mut(index) {
592 if let Some(enabled) = enabled {
593 repo.set_enabled(enabled);
594 }
595
596 file.write()?;
597 } else {
598 bail!("invalid index - {}", index);
599 }
600 } else {
601 bail!("invalid path - {}", path);
602 }
603
604 Ok(())
605}
606
a4e86972 607const SUBDIRS: SubdirMap = &[
9e61c01c 608 ("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
289738dc
FE
609 ("repositories", &Router::new()
610 .get(&API_METHOD_GET_REPOSITORIES)
611 .post(&API_METHOD_CHANGE_REPOSITORY)
612 .put(&API_METHOD_ADD_REPOSITORY)
613 ),
fa3f0584
TL
614 ("update", &Router::new()
615 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
616 .post(&API_METHOD_APT_UPDATE_DATABASE)
617 ),
ed2beb33 618 ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
a4e86972
SR
619];
620
621pub const ROUTER: Router = Router::new()
622 .get(&list_subdirs_api_method!(SUBDIRS))
623 .subdirs(SUBDIRS);