]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node/apt.rs
api: apt update must run protected
[proxmox-backup.git] / src / api2 / node / apt.rs
CommitLineData
a4e86972
SR
1use apt_pkg_native::Cache;
2use anyhow::{Error, bail};
3use serde_json::{json, Value};
4
5use proxmox::{list_subdirs_api_method, const_regex};
fa3f0584
TL
6use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
7use proxmox::api::router::{Router, SubdirMap};
a4e86972 8
fa3f0584
TL
9use crate::server::WorkerTask;
10
11use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
12use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
a4e86972
SR
13
14const_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
21fn 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
74fn 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, &current_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
203fn 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
231pub 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 259const 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
266pub const ROUTER: Router = Router::new()
267 .get(&list_subdirs_api_method!(SUBDIRS))
268 .subdirs(SUBDIRS);