]>
Commit | Line | Data |
---|---|---|
e6513bd5 | 1 | use std::collections::HashSet; |
86d60245 | 2 | use std::collections::HashMap; |
e6513bd5 | 3 | |
33508b12 | 4 | use anyhow::{Error, bail, format_err}; |
e6513bd5 TL |
5 | use apt_pkg_native::Cache; |
6 | ||
7 | use proxmox::const_regex; | |
33508b12 | 8 | use proxmox::tools::fs::{file_read_optional_string, replace_file, CreateOptions}; |
e6513bd5 TL |
9 | |
10 | use crate::api2::types::APTUpdateInfo; | |
11 | ||
33508b12 TL |
12 | const APT_PKG_STATE_FN: &str = "/var/lib/proxmox-backup/pkg-state.json"; |
13 | ||
14 | #[derive(Debug, serde::Serialize, serde::Deserialize)] | |
86d60245 TL |
15 | /// Some information we cache about the package (update) state, like what pending update version |
16 | /// we already notfied an user about | |
33508b12 | 17 | pub struct PkgState { |
86d60245 TL |
18 | /// simple map from package name to most recently notified (emailed) version |
19 | pub notified: Option<HashMap<String, String>>, | |
33508b12 TL |
20 | /// A list of pending updates |
21 | pub package_status: Vec<APTUpdateInfo>, | |
22 | } | |
23 | ||
24 | pub fn write_pkg_cache(state: &PkgState) -> Result<(), Error> { | |
25 | let serialized_state = serde_json::to_string(state)?; | |
26 | ||
27 | replace_file(APT_PKG_STATE_FN, &serialized_state.as_bytes(), CreateOptions::new()) | |
28 | .map_err(|err| format_err!("Error writing package cache - {}", err))?; | |
29 | Ok(()) | |
30 | } | |
31 | ||
32 | pub fn read_pkg_state() -> Result<Option<PkgState>, Error> { | |
33 | let serialized_state = match file_read_optional_string(&APT_PKG_STATE_FN) { | |
34 | Ok(Some(raw)) => raw, | |
35 | Ok(None) => return Ok(None), | |
36 | Err(err) => bail!("could not read cached package state file - {}", err), | |
37 | }; | |
38 | ||
39 | serde_json::from_str(&serialized_state) | |
22a9189e | 40 | .map(Some) |
33508b12 TL |
41 | .map_err(|err| format_err!("could not parse cached package status - {}", err)) |
42 | } | |
43 | ||
44 | pub fn pkg_cache_expired () -> Result<bool, Error> { | |
45 | if let Ok(pbs_cache) = std::fs::metadata(APT_PKG_STATE_FN) { | |
46 | let apt_pkgcache = std::fs::metadata("/var/cache/apt/pkgcache.bin")?; | |
47 | let dpkg_status = std::fs::metadata("/var/lib/dpkg/status")?; | |
48 | ||
49 | let mtime = pbs_cache.modified()?; | |
50 | ||
51 | if apt_pkgcache.modified()? <= mtime && dpkg_status.modified()? <= mtime { | |
52 | return Ok(false); | |
53 | } | |
54 | } | |
55 | Ok(true) | |
56 | } | |
57 | ||
58 | pub fn update_cache() -> Result<PkgState, Error> { | |
59 | // update our cache | |
60 | let all_upgradeable = list_installed_apt_packages(|data| { | |
61 | data.candidate_version == data.active_version && | |
62 | data.installed_version != Some(data.candidate_version) | |
63 | }, None); | |
64 | ||
65 | let cache = match read_pkg_state() { | |
66 | Ok(Some(mut cache)) => { | |
67 | cache.package_status = all_upgradeable; | |
68 | cache | |
69 | }, | |
70 | _ => PkgState { | |
86d60245 | 71 | notified: None, |
33508b12 TL |
72 | package_status: all_upgradeable, |
73 | }, | |
74 | }; | |
75 | write_pkg_cache(&cache)?; | |
76 | Ok(cache) | |
77 | } | |
78 | ||
79 | ||
e6513bd5 TL |
80 | const_regex! { |
81 | VERSION_EPOCH_REGEX = r"^\d+:"; | |
82 | FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$"; | |
83 | } | |
84 | ||
85 | // FIXME: once the 'changelog' API call switches over to 'apt-get changelog' only, | |
86 | // consider removing this function entirely, as it's value is never used anywhere | |
87 | // then (widget-toolkit doesn't use the value either) | |
88 | fn get_changelog_url( | |
89 | package: &str, | |
90 | filename: &str, | |
91 | version: &str, | |
92 | origin: &str, | |
93 | component: &str, | |
94 | ) -> Result<String, Error> { | |
95 | if origin == "" { | |
96 | bail!("no origin available for package {}", package); | |
97 | } | |
98 | ||
99 | if origin == "Debian" { | |
100 | let mut command = std::process::Command::new("apt-get"); | |
101 | command.arg("changelog"); | |
102 | command.arg("--print-uris"); | |
103 | command.arg(package); | |
104 | let output = crate::tools::run_command(command, None)?; // format: 'http://foo/bar' package.changelog | |
105 | let output = match output.splitn(2, ' ').next() { | |
106 | Some(output) => { | |
107 | if output.len() < 2 { | |
108 | bail!("invalid output (URI part too short) from 'apt-get changelog --print-uris': {}", output) | |
109 | } | |
110 | output[1..output.len()-1].to_owned() | |
111 | }, | |
112 | None => bail!("invalid output from 'apt-get changelog --print-uris': {}", output) | |
113 | }; | |
114 | return Ok(output); | |
115 | } else if origin == "Proxmox" { | |
116 | // FIXME: Use above call to 'apt changelog <pkg> --print-uris' as well. | |
117 | // Currently not possible as our packages do not have a URI set in their Release file. | |
118 | let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, ""); | |
119 | ||
120 | let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) { | |
121 | Some(captures) => { | |
122 | let base_capture = captures.get(1); | |
123 | match base_capture { | |
124 | Some(base_underscore) => base_underscore.as_str().replace("_", "/"), | |
125 | None => bail!("incompatible filename, cannot find regex group") | |
126 | } | |
127 | }, | |
128 | None => bail!("incompatible filename, doesn't match regex") | |
129 | }; | |
130 | ||
137a6ebc SR |
131 | if component == "pbs-enterprise" { |
132 | return Ok(format!("https://enterprise.proxmox.com/{}/{}_{}.changelog", | |
133 | base, package, version)); | |
134 | } else { | |
135 | return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog", | |
136 | base, package, version)); | |
137 | } | |
e6513bd5 TL |
138 | } |
139 | ||
140 | bail!("unknown origin ({}) or component ({})", origin, component) | |
141 | } | |
142 | ||
143 | pub struct FilterData<'a> { | |
38260cdd TL |
144 | /// package name |
145 | pub package: &'a str, | |
146 | /// this is version info returned by APT | |
e6513bd5 TL |
147 | pub installed_version: Option<&'a str>, |
148 | pub candidate_version: &'a str, | |
149 | ||
38260cdd | 150 | /// this is the version info the filter is supposed to check |
e6513bd5 TL |
151 | pub active_version: &'a str, |
152 | } | |
153 | ||
154 | enum PackagePreSelect { | |
155 | OnlyInstalled, | |
156 | OnlyNew, | |
157 | All, | |
158 | } | |
159 | ||
160 | pub fn list_installed_apt_packages<F: Fn(FilterData) -> bool>( | |
161 | filter: F, | |
162 | only_versions_for: Option<&str>, | |
163 | ) -> Vec<APTUpdateInfo> { | |
164 | ||
165 | let mut ret = Vec::new(); | |
166 | let mut depends = HashSet::new(); | |
167 | ||
168 | // note: this is not an 'apt update', it just re-reads the cache from disk | |
169 | let mut cache = Cache::get_singleton(); | |
170 | cache.reload(); | |
171 | ||
172 | let mut cache_iter = match only_versions_for { | |
173 | Some(name) => cache.find_by_name(name), | |
174 | None => cache.iter() | |
175 | }; | |
176 | ||
177 | loop { | |
178 | ||
179 | match cache_iter.next() { | |
180 | Some(view) => { | |
181 | let di = if only_versions_for.is_some() { | |
182 | query_detailed_info( | |
183 | PackagePreSelect::All, | |
184 | &filter, | |
185 | view, | |
186 | None | |
187 | ) | |
188 | } else { | |
189 | query_detailed_info( | |
190 | PackagePreSelect::OnlyInstalled, | |
191 | &filter, | |
192 | view, | |
193 | Some(&mut depends) | |
194 | ) | |
195 | }; | |
196 | if let Some(info) = di { | |
197 | ret.push(info); | |
198 | } | |
199 | ||
200 | if only_versions_for.is_some() { | |
201 | break; | |
202 | } | |
203 | }, | |
204 | None => { | |
205 | drop(cache_iter); | |
206 | // also loop through missing dependencies, as they would be installed | |
207 | for pkg in depends.iter() { | |
208 | let mut iter = cache.find_by_name(&pkg); | |
209 | let view = match iter.next() { | |
210 | Some(view) => view, | |
211 | None => continue // package not found, ignore | |
212 | }; | |
213 | ||
214 | let di = query_detailed_info( | |
215 | PackagePreSelect::OnlyNew, | |
216 | &filter, | |
217 | view, | |
218 | None | |
219 | ); | |
220 | if let Some(info) = di { | |
221 | ret.push(info); | |
222 | } | |
223 | } | |
224 | break; | |
225 | } | |
226 | } | |
227 | } | |
228 | ||
229 | return ret; | |
230 | } | |
231 | ||
232 | fn query_detailed_info<'a, F, V>( | |
233 | pre_select: PackagePreSelect, | |
234 | filter: F, | |
235 | view: V, | |
236 | depends: Option<&mut HashSet<String>>, | |
237 | ) -> Option<APTUpdateInfo> | |
238 | where | |
239 | F: Fn(FilterData) -> bool, | |
240 | V: std::ops::Deref<Target = apt_pkg_native::sane::PkgView<'a>> | |
241 | { | |
242 | let current_version = view.current_version(); | |
243 | let candidate_version = view.candidate_version(); | |
244 | ||
245 | let (current_version, candidate_version) = match pre_select { | |
246 | PackagePreSelect::OnlyInstalled => match (current_version, candidate_version) { | |
247 | (Some(cur), Some(can)) => (Some(cur), can), // package installed and there is an update | |
248 | (Some(cur), None) => (Some(cur.clone()), cur), // package installed and up-to-date | |
249 | (None, Some(_)) => return None, // package could be installed | |
250 | (None, None) => return None, // broken | |
251 | }, | |
252 | PackagePreSelect::OnlyNew => match (current_version, candidate_version) { | |
253 | (Some(_), Some(_)) => return None, | |
254 | (Some(_), None) => return None, | |
255 | (None, Some(can)) => (None, can), | |
256 | (None, None) => return None, | |
257 | }, | |
258 | PackagePreSelect::All => match (current_version, candidate_version) { | |
259 | (Some(cur), Some(can)) => (Some(cur), can), | |
260 | (Some(cur), None) => (Some(cur.clone()), cur), | |
261 | (None, Some(can)) => (None, can), | |
262 | (None, None) => return None, | |
263 | }, | |
264 | }; | |
265 | ||
266 | // get additional information via nested APT 'iterators' | |
267 | let mut view_iter = view.versions(); | |
268 | while let Some(ver) = view_iter.next() { | |
269 | ||
270 | let package = view.name(); | |
271 | let version = ver.version(); | |
272 | let mut origin_res = "unknown".to_owned(); | |
273 | let mut section_res = "unknown".to_owned(); | |
274 | let mut priority_res = "unknown".to_owned(); | |
275 | let mut change_log_url = "".to_owned(); | |
276 | let mut short_desc = package.clone(); | |
277 | let mut long_desc = "".to_owned(); | |
278 | ||
279 | let fd = FilterData { | |
38260cdd | 280 | package: package.as_str(), |
e6513bd5 TL |
281 | installed_version: current_version.as_deref(), |
282 | candidate_version: &candidate_version, | |
283 | active_version: &version, | |
284 | }; | |
285 | ||
286 | if filter(fd) { | |
287 | if let Some(section) = ver.section() { | |
288 | section_res = section; | |
289 | } | |
290 | ||
291 | if let Some(prio) = ver.priority_type() { | |
292 | priority_res = prio; | |
293 | } | |
294 | ||
295 | // assume every package has only one origin file (not | |
296 | // origin, but origin *file*, for some reason those seem to | |
297 | // be different concepts in APT) | |
298 | let mut origin_iter = ver.origin_iter(); | |
299 | let origin = origin_iter.next(); | |
300 | if let Some(origin) = origin { | |
301 | ||
302 | if let Some(sd) = origin.short_desc() { | |
303 | short_desc = sd; | |
304 | } | |
305 | ||
306 | if let Some(ld) = origin.long_desc() { | |
307 | long_desc = ld; | |
308 | } | |
309 | ||
310 | // the package files appear in priority order, meaning | |
311 | // the one for the candidate version is first - this is fine | |
312 | // however, as the source package should be the same for all | |
313 | // versions anyway | |
314 | let mut pkg_iter = origin.file(); | |
315 | let pkg_file = pkg_iter.next(); | |
316 | if let Some(pkg_file) = pkg_file { | |
317 | if let Some(origin_name) = pkg_file.origin() { | |
318 | origin_res = origin_name; | |
319 | } | |
320 | ||
321 | let filename = pkg_file.file_name(); | |
322 | let component = pkg_file.component(); | |
323 | ||
324 | // build changelog URL from gathered information | |
325 | // ignore errors, use empty changelog instead | |
326 | let url = get_changelog_url(&package, &filename, | |
327 | &version, &origin_res, &component); | |
328 | if let Ok(url) = url { | |
329 | change_log_url = url; | |
330 | } | |
331 | } | |
332 | } | |
333 | ||
334 | if let Some(depends) = depends { | |
335 | let mut dep_iter = ver.dep_iter(); | |
336 | loop { | |
337 | let dep = match dep_iter.next() { | |
338 | Some(dep) if dep.dep_type() != "Depends" => continue, | |
339 | Some(dep) => dep, | |
340 | None => break | |
341 | }; | |
342 | ||
343 | let dep_pkg = dep.target_pkg(); | |
344 | let name = dep_pkg.name(); | |
345 | ||
346 | depends.insert(name); | |
347 | } | |
348 | } | |
349 | ||
350 | return Some(APTUpdateInfo { | |
351 | package, | |
352 | title: short_desc, | |
353 | arch: view.arch(), | |
354 | description: long_desc, | |
355 | change_log_url, | |
356 | origin: origin_res, | |
357 | version: candidate_version.clone(), | |
358 | old_version: match current_version { | |
359 | Some(vers) => vers, | |
360 | None => "".to_owned() | |
361 | }, | |
362 | priority: priority_res, | |
363 | section: section_res, | |
2decf85d | 364 | extra_info: None, |
e6513bd5 TL |
365 | }); |
366 | } | |
367 | } | |
368 | ||
369 | return None; | |
370 | } |