]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node/apt.rs
api: apt: adapt to proxmox-apt back-end changes
[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
d830804f 450 let infos = proxmox_apt::repositories::check_repositories(&files)?;
2eac3594
FE
451 let standard_repos =
452 proxmox_apt::repositories::standard_repositories(&files, "pbs", &suite);
d830804f
FE
453
454 Ok(json!({
455 "files": files,
456 "errors": errors,
457 "digest": digest,
458 "infos": infos,
459 "standard-repos": standard_repos,
460 }))
461}
462
289738dc
FE
463#[api(
464 input: {
465 properties: {
466 node: {
467 schema: NODE_SCHEMA,
468 },
469 handle: {
470 type: APTRepositoryHandle,
471 },
472 digest: {
473 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
474 optional: true,
475 },
476 },
477 },
478 protected: true,
479 access: {
480 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
481 },
482)]
483/// Add the repository identified by the `handle`.
484/// If the repository is already configured, it will be set to enabled.
485///
486/// The `digest` parameter asserts that the configuration has not been modified.
487pub fn add_repository(handle: APTRepositoryHandle, digest: Option<String>) -> Result<(), Error> {
488 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
489
2eac3594
FE
490 let suite = proxmox_apt::repositories::get_current_release_codename()?;
491
289738dc
FE
492 if let Some(expected_digest) = digest {
493 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
494 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
495 }
496
497 // check if it's already configured first
498 for file in files.iter_mut() {
499 for repo in file.repositories.iter_mut() {
2eac3594 500 if repo.is_referenced_repository(handle, "pbs", &suite) {
289738dc
FE
501 if repo.enabled {
502 return Ok(());
503 }
504
505 repo.set_enabled(true);
506 file.write()?;
507
508 return Ok(());
509 }
510 }
511 }
512
2eac3594
FE
513 let (repo, path) =
514 proxmox_apt::repositories::get_standard_repository(handle, "pbs", &suite);
289738dc
FE
515
516 if let Some(error) = errors.iter().find(|error| error.path == path) {
517 bail!(
518 "unable to parse existing file {} - {}",
519 error.path,
520 error.error,
521 );
522 }
523
524 if let Some(file) = files.iter_mut().find(|file| file.path == path) {
525 file.repositories.push(repo);
526
527 file.write()?;
528 } else {
529 let mut file = match APTRepositoryFile::new(&path)? {
530 Some(file) => file,
531 None => bail!("invalid path - {}", path),
532 };
533
534 file.repositories.push(repo);
535
536 file.write()?;
537 }
538
539 Ok(())
540}
541
542#[api(
543 input: {
544 properties: {
545 node: {
546 schema: NODE_SCHEMA,
547 },
548 path: {
549 description: "Path to the containing file.",
550 type: String,
551 },
552 index: {
553 description: "Index within the file (starting from 0).",
554 type: usize,
555 },
556 enabled: {
557 description: "Whether the repository should be enabled or not.",
558 type: bool,
559 optional: true,
560 },
561 digest: {
562 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
563 optional: true,
564 },
565 },
566 },
567 protected: true,
568 access: {
569 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
570 },
571)]
572/// Change the properties of the specified repository.
573///
574/// The `digest` parameter asserts that the configuration has not been modified.
575pub fn change_repository(
576 path: String,
577 index: usize,
578 enabled: Option<bool>,
579 digest: Option<String>,
580) -> Result<(), Error> {
581 let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
582
583 if let Some(expected_digest) = digest {
584 let current_digest = proxmox::tools::digest_to_hex(&current_digest);
585 crate::tools::assert_if_modified(&expected_digest, &current_digest)?;
586 }
587
588 if let Some(error) = errors.iter().find(|error| error.path == path) {
589 bail!("unable to parse file {} - {}", error.path, error.error);
590 }
591
592 if let Some(file) = files.iter_mut().find(|file| file.path == path) {
593 if let Some(repo) = file.repositories.get_mut(index) {
594 if let Some(enabled) = enabled {
595 repo.set_enabled(enabled);
596 }
597
598 file.write()?;
599 } else {
600 bail!("invalid index - {}", index);
601 }
602 } else {
603 bail!("invalid path - {}", path);
604 }
605
606 Ok(())
607}
608
a4e86972 609const SUBDIRS: SubdirMap = &[
9e61c01c 610 ("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
289738dc
FE
611 ("repositories", &Router::new()
612 .get(&API_METHOD_GET_REPOSITORIES)
613 .post(&API_METHOD_CHANGE_REPOSITORY)
614 .put(&API_METHOD_ADD_REPOSITORY)
615 ),
fa3f0584
TL
616 ("update", &Router::new()
617 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
618 .post(&API_METHOD_APT_UPDATE_DATABASE)
619 ),
ed2beb33 620 ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
a4e86972
SR
621];
622
623pub const ROUTER: Router = Router::new()
624 .get(&list_subdirs_api_method!(SUBDIRS))
625 .subdirs(SUBDIRS);