]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
apt: allow filter to select different package version
[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, RpcEnvironment, RpcEnvironmentType, Permission};
7 use proxmox::api::router::{Router, SubdirMap};
8
9 use crate::server::WorkerTask;
10
11 use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
12 use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA, Userid, UPID_SCHEMA};
13
14 const_regex! {
15 VERSION_EPOCH_REGEX = r"^\d+:";
16 FILENAME_EXTRACT_REGEX = r"^.*/.*?_(.*)_Packages$";
17 }
18
19 // FIXME: Replace with call to 'apt changelog <pkg> --print-uris'. Currently
20 // not possible as our packages do not have a URI set in their Release file
21 fn get_changelog_url(
22 package: &str,
23 filename: &str,
24 source_pkg: &str,
25 version: &str,
26 source_version: &str,
27 origin: &str,
28 component: &str,
29 ) -> Result<String, Error> {
30 if origin == "" {
31 bail!("no origin available for package {}", package);
32 }
33
34 if origin == "Debian" {
35 let source_version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(source_version, "");
36
37 let prefix = if source_pkg.starts_with("lib") {
38 source_pkg.get(0..4)
39 } else {
40 source_pkg.get(0..1)
41 };
42
43 let prefix = match prefix {
44 Some(p) => p,
45 None => bail!("cannot get starting characters of package name '{}'", package)
46 };
47
48 // note: security updates seem to not always upload a changelog for
49 // their package version, so this only works *most* of the time
50 return Ok(format!("https://metadata.ftp-master.debian.org/changelogs/main/{}/{}/{}_{}_changelog",
51 prefix, source_pkg, source_pkg, source_version));
52
53 } else if origin == "Proxmox" {
54 let version = (VERSION_EPOCH_REGEX.regex_obj)().replace_all(version, "");
55
56 let base = match (FILENAME_EXTRACT_REGEX.regex_obj)().captures(filename) {
57 Some(captures) => {
58 let base_capture = captures.get(1);
59 match base_capture {
60 Some(base_underscore) => base_underscore.as_str().replace("_", "/"),
61 None => bail!("incompatible filename, cannot find regex group")
62 }
63 },
64 None => bail!("incompatible filename, doesn't match regex")
65 };
66
67 return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
68 base, package, version));
69 }
70
71 bail!("unknown origin ({}) or component ({})", origin, component)
72 }
73
74 struct FilterData<'a> {
75 // this is version info returned by APT
76 installed_version: &'a str,
77 candidate_version: &'a str,
78
79 // this is the version info the filter is supposed to check
80 active_version: &'a str,
81 }
82
83 fn list_installed_apt_packages<F: Fn(FilterData) -> bool>(filter: F)
84 -> Vec<APTUpdateInfo> {
85
86 let mut ret = Vec::new();
87
88 // note: this is not an 'apt update', it just re-reads the cache from disk
89 let mut cache = Cache::get_singleton();
90 cache.reload();
91
92 let mut cache_iter = cache.iter();
93
94 loop {
95 let view = match cache_iter.next() {
96 Some(view) => view,
97 None => break
98 };
99
100 let current_version = view.current_version();
101 let candidate_version = view.candidate_version();
102
103 let (current_version, candidate_version) = match (current_version, candidate_version) {
104 (Some(cur), Some(can)) => (cur, can), // package installed and there is an update
105 (Some(cur), None) => (cur.clone(), cur), // package installed and up-to-date
106 (None, Some(_)) => continue, // package could be installed
107 (None, None) => continue, // broken
108 };
109
110 // get additional information via nested APT 'iterators'
111 let mut view_iter = view.versions();
112 while let Some(ver) = view_iter.next() {
113
114 let package = view.name();
115 let version = ver.version();
116 let mut origin_res = "unknown".to_owned();
117 let mut section_res = "unknown".to_owned();
118 let mut priority_res = "unknown".to_owned();
119 let mut change_log_url = "".to_owned();
120 let mut short_desc = package.clone();
121 let mut long_desc = "".to_owned();
122
123 let fd = FilterData {
124 installed_version: &current_version,
125 candidate_version: &candidate_version,
126 active_version: &version,
127 };
128
129 if filter(fd) {
130 if let Some(section) = ver.section() {
131 section_res = section;
132 }
133
134 if let Some(prio) = ver.priority_type() {
135 priority_res = prio;
136 }
137
138 // assume every package has only one origin file (not
139 // origin, but origin *file*, for some reason those seem to
140 // be different concepts in APT)
141 let mut origin_iter = ver.origin_iter();
142 let origin = origin_iter.next();
143 if let Some(origin) = origin {
144
145 if let Some(sd) = origin.short_desc() {
146 short_desc = sd;
147 }
148
149 if let Some(ld) = origin.long_desc() {
150 long_desc = ld;
151 }
152
153 // the package files appear in priority order, meaning
154 // the one for the candidate version is first - this is fine
155 // however, as the source package should be the same for all
156 // versions anyway
157 let mut pkg_iter = origin.file();
158 let pkg_file = pkg_iter.next();
159 if let Some(pkg_file) = pkg_file {
160 if let Some(origin_name) = pkg_file.origin() {
161 origin_res = origin_name;
162 }
163
164 let filename = pkg_file.file_name();
165 let source_pkg = ver.source_package();
166 let source_ver = ver.source_version();
167 let component = pkg_file.component();
168
169 // build changelog URL from gathered information
170 // ignore errors, use empty changelog instead
171 let url = get_changelog_url(&package, &filename, &source_pkg,
172 &version, &source_ver, &origin_res, &component);
173 if let Ok(url) = url {
174 change_log_url = url;
175 }
176 }
177 }
178
179 let info = APTUpdateInfo {
180 package,
181 title: short_desc,
182 arch: view.arch(),
183 description: long_desc,
184 change_log_url,
185 origin: origin_res,
186 version: candidate_version.clone(),
187 old_version: current_version.clone(),
188 priority: priority_res,
189 section: section_res,
190 };
191 ret.push(info);
192 }
193 }
194 }
195
196 return ret;
197 }
198
199 #[api(
200 input: {
201 properties: {
202 node: {
203 schema: NODE_SCHEMA,
204 },
205 },
206 },
207 returns: {
208 description: "A list of packages with available updates.",
209 type: Array,
210 items: { type: APTUpdateInfo },
211 },
212 access: {
213 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
214 },
215 )]
216 /// List available APT updates
217 fn apt_update_available(_param: Value) -> Result<Value, Error> {
218 let all_upgradeable = list_installed_apt_packages(|data|
219 data.candidate_version == data.active_version &&
220 data.installed_version != data.candidate_version
221 );
222 Ok(json!(all_upgradeable))
223 }
224
225 #[api(
226 protected: true,
227 input: {
228 properties: {
229 node: {
230 schema: NODE_SCHEMA,
231 },
232 quiet: {
233 description: "Only produces output suitable for logging, omitting progress indicators.",
234 type: bool,
235 default: false,
236 optional: true,
237 },
238 },
239 },
240 returns: {
241 schema: UPID_SCHEMA,
242 },
243 access: {
244 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
245 },
246 )]
247 /// Update the APT database
248 pub fn apt_update_database(
249 quiet: Option<bool>,
250 rpcenv: &mut dyn RpcEnvironment,
251 ) -> Result<String, Error> {
252
253 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
254 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
255 let quiet = quiet.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_QUIET);
256
257 let upid_str = WorkerTask::new_thread("aptupdate", None, userid, to_stdout, move |worker| {
258 if !quiet { worker.log("starting apt-get update") }
259
260 // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE
261
262 let mut command = std::process::Command::new("apt-get");
263 command.arg("update");
264
265 let output = crate::tools::run_command(command, None)?;
266 if !quiet { worker.log(output) }
267
268 // TODO: add mail notify for new updates like PVE
269
270 Ok(())
271 })?;
272
273 Ok(upid_str)
274 }
275
276 const SUBDIRS: SubdirMap = &[
277 ("update", &Router::new()
278 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
279 .post(&API_METHOD_APT_UPDATE_DATABASE)
280 ),
281 ];
282
283 pub const ROUTER: Router = Router::new()
284 .get(&list_subdirs_api_method!(SUBDIRS))
285 .subdirs(SUBDIRS);