]>
Commit | Line | Data |
---|---|---|
a4e86972 SR |
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}; | |
fa3f0584 TL |
6 | use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission}; |
7 | use proxmox::api::router::{Router, SubdirMap}; | |
a4e86972 | 8 | |
fa3f0584 TL |
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, UPID_SCHEMA}; | |
a4e86972 SR |
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 | fn list_installed_apt_packages<F: Fn(&str, &str, &str) -> bool>(filter: F) | |
75 | -> Vec<APTUpdateInfo> { | |
76 | ||
77 | let mut ret = Vec::new(); | |
78 | ||
79 | // note: this is not an 'apt update', it just re-reads the cache from disk | |
80 | let mut cache = Cache::get_singleton(); | |
81 | cache.reload(); | |
82 | ||
83 | let mut cache_iter = cache.iter(); | |
84 | ||
85 | loop { | |
86 | let view = match cache_iter.next() { | |
87 | Some(view) => view, | |
88 | None => break | |
89 | }; | |
90 | ||
91 | let current_version = match view.current_version() { | |
92 | Some(vers) => vers, | |
93 | None => continue | |
94 | }; | |
95 | let candidate_version = match view.candidate_version() { | |
96 | Some(vers) => vers, | |
97 | // if there's no candidate (i.e. no update) get info of currently | |
98 | // installed version instead | |
99 | None => current_version.clone() | |
100 | }; | |
101 | ||
102 | let package = view.name(); | |
103 | if filter(&package, ¤t_version, &candidate_version) { | |
104 | let mut origin_res = "unknown".to_owned(); | |
105 | let mut section_res = "unknown".to_owned(); | |
106 | let mut priority_res = "unknown".to_owned(); | |
107 | let mut change_log_url = "".to_owned(); | |
108 | let mut short_desc = package.clone(); | |
109 | let mut long_desc = "".to_owned(); | |
110 | ||
111 | // get additional information via nested APT 'iterators' | |
112 | let mut view_iter = view.versions(); | |
113 | while let Some(ver) = view_iter.next() { | |
114 | if ver.version() == candidate_version { | |
115 | if let Some(section) = ver.section() { | |
116 | section_res = section; | |
117 | } | |
118 | ||
119 | if let Some(prio) = ver.priority_type() { | |
120 | priority_res = prio; | |
121 | } | |
122 | ||
123 | // assume every package has only one origin file (not | |
124 | // origin, but origin *file*, for some reason those seem to | |
125 | // be different concepts in APT) | |
126 | let mut origin_iter = ver.origin_iter(); | |
127 | let origin = origin_iter.next(); | |
128 | if let Some(origin) = origin { | |
129 | ||
130 | if let Some(sd) = origin.short_desc() { | |
131 | short_desc = sd; | |
132 | } | |
133 | ||
134 | if let Some(ld) = origin.long_desc() { | |
135 | long_desc = ld; | |
136 | } | |
137 | ||
138 | // the package files appear in priority order, meaning | |
139 | // the one for the candidate version is first | |
140 | let mut pkg_iter = origin.file(); | |
141 | let pkg_file = pkg_iter.next(); | |
142 | if let Some(pkg_file) = pkg_file { | |
143 | if let Some(origin_name) = pkg_file.origin() { | |
144 | origin_res = origin_name; | |
145 | } | |
146 | ||
147 | let filename = pkg_file.file_name(); | |
148 | let source_pkg = ver.source_package(); | |
149 | let source_ver = ver.source_version(); | |
150 | let component = pkg_file.component(); | |
151 | ||
152 | // build changelog URL from gathered information | |
153 | // ignore errors, use empty changelog instead | |
154 | let url = get_changelog_url(&package, &filename, &source_pkg, | |
155 | &candidate_version, &source_ver, &origin_res, &component); | |
156 | if let Ok(url) = url { | |
157 | change_log_url = url; | |
158 | } | |
159 | } | |
160 | } | |
161 | ||
162 | break; | |
163 | } | |
164 | } | |
165 | ||
166 | let info = APTUpdateInfo { | |
167 | package, | |
168 | title: short_desc, | |
169 | arch: view.arch(), | |
170 | description: long_desc, | |
171 | change_log_url, | |
172 | origin: origin_res, | |
173 | version: candidate_version, | |
174 | old_version: current_version, | |
175 | priority: priority_res, | |
176 | section: section_res, | |
177 | }; | |
178 | ret.push(info); | |
179 | } | |
180 | } | |
181 | ||
182 | return ret; | |
183 | } | |
184 | ||
185 | #[api( | |
186 | input: { | |
187 | properties: { | |
188 | node: { | |
189 | schema: NODE_SCHEMA, | |
190 | }, | |
191 | }, | |
192 | }, | |
193 | returns: { | |
194 | description: "A list of packages with available updates.", | |
195 | type: Array, | |
196 | items: { type: APTUpdateInfo }, | |
197 | }, | |
198 | access: { | |
199 | permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false), | |
200 | }, | |
201 | )] | |
202 | /// List available APT updates | |
203 | fn apt_update_available(_param: Value) -> Result<Value, Error> { | |
204 | let ret = list_installed_apt_packages(|_pkg, cur_ver, can_ver| cur_ver != can_ver); | |
205 | Ok(json!(ret)) | |
206 | } | |
207 | ||
fa3f0584 | 208 | #[api( |
27fde647 | 209 | protected: true, |
fa3f0584 TL |
210 | input: { |
211 | properties: { | |
212 | node: { | |
213 | schema: NODE_SCHEMA, | |
214 | }, | |
215 | quiet: { | |
216 | description: "Only produces output suitable for logging, omitting progress indicators.", | |
217 | type: bool, | |
218 | default: false, | |
219 | optional: true, | |
220 | }, | |
221 | }, | |
222 | }, | |
223 | returns: { | |
224 | schema: UPID_SCHEMA, | |
225 | }, | |
226 | access: { | |
227 | permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false), | |
228 | }, | |
229 | )] | |
230 | /// Update the APT database | |
231 | pub fn apt_update_database( | |
232 | quiet: Option<bool>, | |
233 | rpcenv: &mut dyn RpcEnvironment, | |
234 | ) -> Result<String, Error> { | |
235 | ||
236 | let username = rpcenv.get_user().unwrap(); | |
237 | let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; | |
238 | let quiet = quiet.unwrap_or(false); | |
239 | ||
240 | let upid_str = WorkerTask::new_thread("aptupdate", None, &username.clone(), to_stdout, move |worker| { | |
241 | if !quiet { worker.log("starting apt-get update") } | |
242 | ||
243 | // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE | |
244 | ||
245 | let mut command = std::process::Command::new("apt-get"); | |
246 | command.arg("update"); | |
247 | ||
248 | let output = crate::tools::run_command(command, None)?; | |
249 | if !quiet { worker.log(output) } | |
250 | ||
251 | // TODO: add mail notify for new updates like PVE | |
252 | ||
253 | Ok(()) | |
254 | })?; | |
255 | ||
256 | Ok(upid_str) | |
257 | } | |
258 | ||
a4e86972 | 259 | const SUBDIRS: SubdirMap = &[ |
fa3f0584 TL |
260 | ("update", &Router::new() |
261 | .get(&API_METHOD_APT_UPDATE_AVAILABLE) | |
262 | .post(&API_METHOD_APT_UPDATE_DATABASE) | |
263 | ), | |
a4e86972 SR |
264 | ]; |
265 | ||
266 | pub const ROUTER: Router = Router::new() | |
267 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
268 | .subdirs(SUBDIRS); |