]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
add .../apt/update API call
[proxmox-backup.git] / src / api2 / node / apt.rs
1 use apt_pkg_native::Cache;
2 use anyhow::{Error, bail};
3 use serde_json::{json, Value};
4
5 use proxmox::{list_subdirs_api_method, const_regex};
6 use proxmox::api::{api, Router, Permission, SubdirMap};
7
8 use crate::config::acl::PRIV_SYS_AUDIT;
9 use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA};
10
11 const_regex! {
12 VERSION_EPOCH_REGEX = r"^\d+:";
13 FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";
14 }
15
16 // FIXME: Replace with call to 'apt changelog <pkg> --print-uris'. Currently
17 // not possible as our packages do not have a URI set in their Release file
18 fn get_changelog_url(
19 package: &str,
20 filename: &str,
21 source_pkg: &str,
22 version: &str,
23 source_version: &str,
24 origin: &str,
25 component: &str,
26 ) -> Result<String, Error> {
27 if origin == "" {
28 bail!("no origin available for package {}", package);
29 }
30
31 if origin == "Debian" {
32 let source_version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(source_version, "");
33
34 let prefix = if source_pkg.starts_with("lib") {
35 source_pkg.get(0..4)
36 } else {
37 source_pkg.get(0..1)
38 };
39
40 let prefix = match prefix {
41 Some(p) => p,
42 None => bail!("cannot get starting characters of package name '{}'", package)
43 };
44
45 // note: security updates seem to not always upload a changelog for
46 // their package version, so this only works *most* of the time
47 return Ok(format!("https://metadata.ftp-master.debian.org/changelogs/main/{}/{}/{}_{}_changelog",
48 prefix, source_pkg, source_pkg, source_version));
49
50 } else if origin == "Proxmox" {
51 let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, "");
52
53 let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) {
54 Some(captures) => {
55 let base_capture = captures.get(1);
56 match base_capture {
57 Some(base_underscore) => base_underscore.as_str().replace("_", "/"),
58 None => bail!("incompatible filename, cannot find regex group")
59 }
60 },
61 None => bail!("incompatible filename, doesn't match regex")
62 };
63
64 return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
65 base, package, version));
66 }
67
68 bail!("unknown origin ({}) or component ({})", origin, component)
69 }
70
71 fn list_installed_apt_packages<F: Fn(&str, &str, &str) -> bool>(filter: F)
72 -> Vec<APTUpdateInfo> {
73
74 let mut ret = Vec::new();
75
76 // note: this is not an 'apt update', it just re-reads the cache from disk
77 let mut cache = Cache::get_singleton();
78 cache.reload();
79
80 let mut cache_iter = cache.iter();
81
82 loop {
83 let view = match cache_iter.next() {
84 Some(view) => view,
85 None => break
86 };
87
88 let current_version = match view.current_version() {
89 Some(vers) => vers,
90 None => continue
91 };
92 let candidate_version = match view.candidate_version() {
93 Some(vers) => vers,
94 // if there's no candidate (i.e. no update) get info of currently
95 // installed version instead
96 None => current_version.clone()
97 };
98
99 let package = view.name();
100 if filter(&package, &current_version, &candidate_version) {
101 let mut origin_res = "unknown".to_owned();
102 let mut section_res = "unknown".to_owned();
103 let mut priority_res = "unknown".to_owned();
104 let mut change_log_url = "".to_owned();
105 let mut short_desc = package.clone();
106 let mut long_desc = "".to_owned();
107
108 // get additional information via nested APT 'iterators'
109 let mut view_iter = view.versions();
110 while let Some(ver) = view_iter.next() {
111 if ver.version() == candidate_version {
112 if let Some(section) = ver.section() {
113 section_res = section;
114 }
115
116 if let Some(prio) = ver.priority_type() {
117 priority_res = prio;
118 }
119
120 // assume every package has only one origin file (not
121 // origin, but origin *file*, for some reason those seem to
122 // be different concepts in APT)
123 let mut origin_iter = ver.origin_iter();
124 let origin = origin_iter.next();
125 if let Some(origin) = origin {
126
127 if let Some(sd) = origin.short_desc() {
128 short_desc = sd;
129 }
130
131 if let Some(ld) = origin.long_desc() {
132 long_desc = ld;
133 }
134
135 // the package files appear in priority order, meaning
136 // the one for the candidate version is first
137 let mut pkg_iter = origin.file();
138 let pkg_file = pkg_iter.next();
139 if let Some(pkg_file) = pkg_file {
140 if let Some(origin_name) = pkg_file.origin() {
141 origin_res = origin_name;
142 }
143
144 let filename = pkg_file.file_name();
145 let source_pkg = ver.source_package();
146 let source_ver = ver.source_version();
147 let component = pkg_file.component();
148
149 // build changelog URL from gathered information
150 // ignore errors, use empty changelog instead
151 let url = get_changelog_url(&package, &filename, &source_pkg,
152 &candidate_version, &source_ver, &origin_res, &component);
153 if let Ok(url) = url {
154 change_log_url = url;
155 }
156 }
157 }
158
159 break;
160 }
161 }
162
163 let info = APTUpdateInfo {
164 package,
165 title: short_desc,
166 arch: view.arch(),
167 description: long_desc,
168 change_log_url,
169 origin: origin_res,
170 version: candidate_version,
171 old_version: current_version,
172 priority: priority_res,
173 section: section_res,
174 };
175 ret.push(info);
176 }
177 }
178
179 return ret;
180 }
181
182 #[api(
183 input: {
184 properties: {
185 node: {
186 schema: NODE_SCHEMA,
187 },
188 },
189 },
190 returns: {
191 description: "A list of packages with available updates.",
192 type: Array,
193 items: { type: APTUpdateInfo },
194 },
195 access: {
196 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
197 },
198 )]
199 /// List available APT updates
200 fn apt_update_available(_param: Value) -> Result<Value, Error> {
201 let ret = list_installed_apt_packages(|_pkg, cur_ver, can_ver| cur_ver != can_ver);
202 Ok(json!(ret))
203 }
204
205 const SUBDIRS: SubdirMap = &[
206 ("update", &Router::new().get(&API_METHOD_APT_UPDATE_AVAILABLE)),
207 ];
208
209 pub const ROUTER: Router = Router::new()
210 .get(&list_subdirs_api_method!(SUBDIRS))
211 .subdirs(SUBDIRS);