]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
api: define subscription key schema and use it
[proxmox-backup.git] / src / api2 / node / apt.rs
1 use std::collections::HashSet;
2
3 use apt_pkg_native::Cache;
4 use anyhow::{Error, bail, format_err};
5 use serde_json::{json, Value};
6
7 use proxmox::{list_subdirs_api_method, const_regex};
8 use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
9 use proxmox::api::router::{Router, SubdirMap};
10
11 use crate::server::WorkerTask;
12 use crate::tools::http;
13
14 use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
15 use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
16
17 const_regex! {
18 VERSION_EPOCH_REGEX = r"^\d+:";
19 FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";
20 }
21
22 // FIXME: once the 'changelog' API call switches over to 'apt-get changelog' only,
23 // consider removing this function entirely, as it's value is never used anywhere
24 // then (widget-toolkit doesn't use the value either)
25 fn get_changelog_url(
26 package: &str,
27 filename: &str,
28 version: &str,
29 origin: &str,
30 component: &str,
31 ) -> Result<String, Error> {
32 if origin == "" {
33 bail!("no origin available for package {}", package);
34 }
35
36 if origin == "Debian" {
37 let mut command = std::process::Command::new("apt-get");
38 command.arg("changelog");
39 command.arg("--print-uris");
40 command.arg(package);
41 let output = crate::tools::run_command(command, None)?; // format: 'http://foo/bar' package.changelog
42 let output = match output.splitn(2, ' ').next() {
43 Some(output) => {
44 if output.len() < 2 {
45 bail!("invalid output (URI part too short) from 'apt-get changelog --print-uris': {}", output)
46 }
47 output[1..output.len()-1].to_owned()
48 },
49 None => bail!("invalid output from 'apt-get changelog --print-uris': {}", output)
50 };
51 return Ok(output);
52 } else if origin == "Proxmox" {
53 // FIXME: Use above call to 'apt changelog <pkg> --print-uris' as well.
54 // Currently not possible as our packages do not have a URI set in their Release file.
55 let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, "");
56
57 let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) {
58 Some(captures) => {
59 let base_capture = captures.get(1);
60 match base_capture {
61 Some(base_underscore) => base_underscore.as_str().replace("_", "/"),
62 None => bail!("incompatible filename, cannot find regex group")
63 }
64 },
65 None => bail!("incompatible filename, doesn't match regex")
66 };
67
68 return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
69 base, package, version));
70 }
71
72 bail!("unknown origin ({}) or component ({})", origin, component)
73 }
74
75 struct FilterData<'a> {
76 // this is version info returned by APT
77 installed_version: Option<&'a str>,
78 candidate_version: &'a str,
79
80 // this is the version info the filter is supposed to check
81 active_version: &'a str,
82 }
83
84 enum PackagePreSelect {
85 OnlyInstalled,
86 OnlyNew,
87 All,
88 }
89
90 fn list_installed_apt_packages<F: Fn(FilterData) -> bool>(
91 filter: F,
92 only_versions_for: Option<&str>,
93 ) -> Vec<APTUpdateInfo> {
94
95 let mut ret = Vec::new();
96 let mut depends = HashSet::new();
97
98 // note: this is not an 'apt update', it just re-reads the cache from disk
99 let mut cache = Cache::get_singleton();
100 cache.reload();
101
102 let mut cache_iter = match only_versions_for {
103 Some(name) => cache.find_by_name(name),
104 None => cache.iter()
105 };
106
107 loop {
108
109 match cache_iter.next() {
110 Some(view) => {
111 let di = if only_versions_for.is_some() {
112 query_detailed_info(
113 PackagePreSelect::All,
114 &filter,
115 view,
116 None
117 )
118 } else {
119 query_detailed_info(
120 PackagePreSelect::OnlyInstalled,
121 &filter,
122 view,
123 Some(&mut depends)
124 )
125 };
126 if let Some(info) = di {
127 ret.push(info);
128 }
129
130 if only_versions_for.is_some() {
131 break;
132 }
133 },
134 None => {
135 drop(cache_iter);
136 // also loop through missing dependencies, as they would be installed
137 for pkg in depends.iter() {
138 let mut iter = cache.find_by_name(&pkg);
139 let view = match iter.next() {
140 Some(view) => view,
141 None => continue // package not found, ignore
142 };
143
144 let di = query_detailed_info(
145 PackagePreSelect::OnlyNew,
146 &filter,
147 view,
148 None
149 );
150 if let Some(info) = di {
151 ret.push(info);
152 }
153 }
154 break;
155 }
156 }
157 }
158
159 return ret;
160 }
161
162 fn query_detailed_info<'a, F, V>(
163 pre_select: PackagePreSelect,
164 filter: F,
165 view: V,
166 depends: Option<&mut HashSet<String>>,
167 ) -> Option<APTUpdateInfo>
168 where
169 F: Fn(FilterData) -> bool,
170 V: std::ops::Deref<Target = apt_pkg_native::sane::PkgView<'a>>
171 {
172 let current_version = view.current_version();
173 let candidate_version = view.candidate_version();
174
175 let (current_version, candidate_version) = match pre_select {
176 PackagePreSelect::OnlyInstalled => match (current_version, candidate_version) {
177 (Some(cur), Some(can)) => (Some(cur), can), // package installed and there is an update
178 (Some(cur), None) => (Some(cur.clone()), cur), // package installed and up-to-date
179 (None, Some(_)) => return None, // package could be installed
180 (None, None) => return None, // broken
181 },
182 PackagePreSelect::OnlyNew => match (current_version, candidate_version) {
183 (Some(_), Some(_)) => return None,
184 (Some(_), None) => return None,
185 (None, Some(can)) => (None, can),
186 (None, None) => return None,
187 },
188 PackagePreSelect::All => match (current_version, candidate_version) {
189 (Some(cur), Some(can)) => (Some(cur), can),
190 (Some(cur), None) => (Some(cur.clone()), cur),
191 (None, Some(can)) => (None, can),
192 (None, None) => return None,
193 },
194 };
195
196 // get additional information via nested APT 'iterators'
197 let mut view_iter = view.versions();
198 while let Some(ver) = view_iter.next() {
199
200 let package = view.name();
201 let version = ver.version();
202 let mut origin_res = "unknown".to_owned();
203 let mut section_res = "unknown".to_owned();
204 let mut priority_res = "unknown".to_owned();
205 let mut change_log_url = "".to_owned();
206 let mut short_desc = package.clone();
207 let mut long_desc = "".to_owned();
208
209 let fd = FilterData {
210 installed_version: current_version.as_deref(),
211 candidate_version: &candidate_version,
212 active_version: &version,
213 };
214
215 if filter(fd) {
216 if let Some(section) = ver.section() {
217 section_res = section;
218 }
219
220 if let Some(prio) = ver.priority_type() {
221 priority_res = prio;
222 }
223
224 // assume every package has only one origin file (not
225 // origin, but origin *file*, for some reason those seem to
226 // be different concepts in APT)
227 let mut origin_iter = ver.origin_iter();
228 let origin = origin_iter.next();
229 if let Some(origin) = origin {
230
231 if let Some(sd) = origin.short_desc() {
232 short_desc = sd;
233 }
234
235 if let Some(ld) = origin.long_desc() {
236 long_desc = ld;
237 }
238
239 // the package files appear in priority order, meaning
240 // the one for the candidate version is first - this is fine
241 // however, as the source package should be the same for all
242 // versions anyway
243 let mut pkg_iter = origin.file();
244 let pkg_file = pkg_iter.next();
245 if let Some(pkg_file) = pkg_file {
246 if let Some(origin_name) = pkg_file.origin() {
247 origin_res = origin_name;
248 }
249
250 let filename = pkg_file.file_name();
251 let component = pkg_file.component();
252
253 // build changelog URL from gathered information
254 // ignore errors, use empty changelog instead
255 let url = get_changelog_url(&package, &filename,
256 &version, &origin_res, &component);
257 if let Ok(url) = url {
258 change_log_url = url;
259 }
260 }
261 }
262
263 if let Some(depends) = depends {
264 let mut dep_iter = ver.dep_iter();
265 loop {
266 let dep = match dep_iter.next() {
267 Some(dep) if dep.dep_type() != "Depends" => continue,
268 Some(dep) => dep,
269 None => break
270 };
271
272 let dep_pkg = dep.target_pkg();
273 let name = dep_pkg.name();
274
275 depends.insert(name);
276 }
277 }
278
279 return Some(APTUpdateInfo {
280 package,
281 title: short_desc,
282 arch: view.arch(),
283 description: long_desc,
284 change_log_url,
285 origin: origin_res,
286 version: candidate_version.clone(),
287 old_version: match current_version {
288 Some(vers) => vers,
289 None => "".to_owned()
290 },
291 priority: priority_res,
292 section: section_res,
293 });
294 }
295 }
296
297 return None;
298 }
299
300 #[api(
301 input: {
302 properties: {
303 node: {
304 schema: NODE_SCHEMA,
305 },
306 },
307 },
308 returns: {
309 description: "A list of packages with available updates.",
310 type: Array,
311 items: { type: APTUpdateInfo },
312 },
313 access: {
314 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
315 },
316 )]
317 /// List available APT updates
318 fn apt_update_available(_param: Value) -> Result<Value, Error> {
319 let all_upgradeable = list_installed_apt_packages(|data| {
320 data.candidate_version == data.active_version &&
321 data.installed_version != Some(data.candidate_version)
322 }, None);
323 Ok(json!(all_upgradeable))
324 }
325
326 #[api(
327 protected: true,
328 input: {
329 properties: {
330 node: {
331 schema: NODE_SCHEMA,
332 },
333 quiet: {
334 description: "Only produces output suitable for logging, omitting progress indicators.",
335 type: bool,
336 default: false,
337 optional: true,
338 },
339 },
340 },
341 returns: {
342 schema: UPID_SCHEMA,
343 },
344 access: {
345 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
346 },
347 )]
348 /// Update the APT database
349 pub fn apt_update_database(
350 quiet: Option<bool>,
351 rpcenv: &mut dyn RpcEnvironment,
352 ) -> Result<String, Error> {
353
354 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
355 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
356 let quiet = quiet.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_QUIET);
357
358 let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
359 if !quiet { worker.log("starting apt-get update") }
360
361 // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE
362
363 let mut command = std::process::Command::new("apt-get");
364 command.arg("update");
365
366 let output = crate::tools::run_command(command, None)?;
367 if !quiet { worker.log(output) }
368
369 // TODO: add mail notify for new updates like PVE
370
371 Ok(())
372 })?;
373
374 Ok(upid_str)
375 }
376
377 #[api(
378 input: {
379 properties: {
380 node: {
381 schema: NODE_SCHEMA,
382 },
383 name: {
384 description: "Package name to get changelog of.",
385 type: String,
386 },
387 version: {
388 description: "Package version to get changelog of. Omit to use candidate version.",
389 type: String,
390 optional: true,
391 },
392 },
393 },
394 returns: {
395 schema: UPID_SCHEMA,
396 },
397 access: {
398 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
399 },
400 )]
401 /// Retrieve the changelog of the specified package.
402 fn apt_get_changelog(
403 param: Value,
404 ) -> Result<Value, Error> {
405
406 let name = crate::tools::required_string_param(&param, "name")?.to_owned();
407 let version = param["version"].as_str();
408
409 let pkg_info = list_installed_apt_packages(|data| {
410 match version {
411 Some(version) => version == data.active_version,
412 None => data.active_version == data.candidate_version
413 }
414 }, Some(&name));
415
416 if pkg_info.len() == 0 {
417 bail!("Package '{}' not found", name);
418 }
419
420 let changelog_url = &pkg_info[0].change_log_url;
421 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
422 if changelog_url.starts_with("http://download.proxmox.com/") {
423 let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url))
424 .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
425 return Ok(json!(changelog));
426 } else {
427 let mut command = std::process::Command::new("apt-get");
428 command.arg("changelog");
429 command.arg("-qq"); // don't display download progress
430 command.arg(name);
431 let output = crate::tools::run_command(command, None)?;
432 return Ok(json!(output));
433 }
434 }
435
436 const SUBDIRS: SubdirMap = &[
437 ("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
438 ("update", &Router::new()
439 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
440 .post(&API_METHOD_APT_UPDATE_DATABASE)
441 ),
442 ];
443
444 pub const ROUTER: Router = Router::new()
445 .get(&list_subdirs_api_method!(SUBDIRS))
446 .subdirs(SUBDIRS);