]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/apt.rs
typo fixes all over the place
[proxmox-backup.git] / src / api2 / node / apt.rs
1 use anyhow::{Error, bail, format_err};
2 use serde_json::{json, Value};
3 use std::collections::HashMap;
4
5 use proxmox::list_subdirs_api_method;
6 use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
7 use proxmox::api::router::{Router, SubdirMap};
8
9 use crate::server::WorkerTask;
10 use crate::tools::{apt, http, subscription};
11
12 use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
13 use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
14
15 #[api(
16 input: {
17 properties: {
18 node: {
19 schema: NODE_SCHEMA,
20 },
21 },
22 },
23 returns: {
24 description: "A list of packages with available updates.",
25 type: Array,
26 items: {
27 type: APTUpdateInfo
28 },
29 },
30 protected: true,
31 access: {
32 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
33 },
34 )]
35 /// List available APT updates
36 fn apt_update_available(_param: Value) -> Result<Value, Error> {
37
38 if let Ok(false) = apt::pkg_cache_expired() {
39 if let Ok(Some(cache)) = apt::read_pkg_state() {
40 return Ok(json!(cache.package_status));
41 }
42 }
43
44 let cache = apt::update_cache()?;
45
46 Ok(json!(cache.package_status))
47 }
48
49 fn do_apt_update(worker: &WorkerTask, quiet: bool) -> Result<(), Error> {
50 if !quiet { worker.log("starting apt-get update") }
51
52 // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE
53
54 let mut command = std::process::Command::new("apt-get");
55 command.arg("update");
56
57 // apt "errors" quite easily, and run_command is a bit rigid, so handle this inline for now.
58 let output = command.output()
59 .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?;
60
61 if !quiet {
62 worker.log(String::from_utf8(output.stdout)?);
63 }
64
65 // TODO: improve run_command to allow outputting both, stderr and stdout
66 if !output.status.success() {
67 if output.status.code().is_some() {
68 let msg = String::from_utf8(output.stderr)
69 .map(|m| if m.is_empty() { String::from("no error message") } else { m })
70 .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
71 worker.warn(msg);
72 } else {
73 bail!("terminated by signal");
74 }
75 }
76 Ok(())
77 }
78
79 #[api(
80 protected: true,
81 input: {
82 properties: {
83 node: {
84 schema: NODE_SCHEMA,
85 },
86 notify: {
87 type: bool,
88 description: r#"Send notification mail about new package updates available to the
89 email address configured for 'root@pam')."#,
90 default: false,
91 optional: true,
92 },
93 quiet: {
94 description: "Only produces output suitable for logging, omitting progress indicators.",
95 type: bool,
96 default: false,
97 optional: true,
98 },
99 },
100 },
101 returns: {
102 schema: UPID_SCHEMA,
103 },
104 access: {
105 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
106 },
107 )]
108 /// Update the APT database
109 pub fn apt_update_database(
110 notify: bool,
111 quiet: bool,
112 rpcenv: &mut dyn RpcEnvironment,
113 ) -> Result<String, Error> {
114
115 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
116 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
117
118 let upid_str = WorkerTask::new_thread("aptupdate", None, auth_id, to_stdout, move |worker| {
119 do_apt_update(&worker, quiet)?;
120
121 let mut cache = apt::update_cache()?;
122
123 if notify {
124 let mut notified = match cache.notified {
125 Some(notified) => notified,
126 None => std::collections::HashMap::new(),
127 };
128 let mut to_notify: Vec<&APTUpdateInfo> = Vec::new();
129
130 for pkg in &cache.package_status {
131 match notified.insert(pkg.package.to_owned(), pkg.version.to_owned()) {
132 Some(notified_version) => {
133 if notified_version != pkg.version {
134 to_notify.push(pkg);
135 }
136 },
137 None => to_notify.push(pkg),
138 }
139 }
140 if !to_notify.is_empty() {
141 to_notify.sort_unstable_by_key(|k| &k.package);
142 crate::server::send_updates_available(&to_notify)?;
143 }
144 cache.notified = Some(notified);
145 apt::write_pkg_cache(&cache)?;
146 }
147
148 Ok(())
149 })?;
150
151 Ok(upid_str)
152 }
153
154 #[api(
155 input: {
156 properties: {
157 node: {
158 schema: NODE_SCHEMA,
159 },
160 name: {
161 description: "Package name to get changelog of.",
162 type: String,
163 },
164 version: {
165 description: "Package version to get changelog of. Omit to use candidate version.",
166 type: String,
167 optional: true,
168 },
169 },
170 },
171 returns: {
172 schema: UPID_SCHEMA,
173 },
174 access: {
175 permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
176 },
177 )]
178 /// Retrieve the changelog of the specified package.
179 fn apt_get_changelog(
180 param: Value,
181 ) -> Result<Value, Error> {
182
183 let name = crate::tools::required_string_param(&param, "name")?.to_owned();
184 let version = param["version"].as_str();
185
186 let pkg_info = apt::list_installed_apt_packages(|data| {
187 match version {
188 Some(version) => version == data.active_version,
189 None => data.active_version == data.candidate_version
190 }
191 }, Some(&name));
192
193 if pkg_info.is_empty() {
194 bail!("Package '{}' not found", name);
195 }
196
197 let changelog_url = &pkg_info[0].change_log_url;
198 // FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
199 if changelog_url.starts_with("http://download.proxmox.com/") {
200 let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url, None))
201 .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
202 Ok(json!(changelog))
203
204 } else if changelog_url.starts_with("https://enterprise.proxmox.com/") {
205 let sub = match subscription::read_subscription()? {
206 Some(sub) => sub,
207 None => bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
208 };
209 let (key, id) = match sub.key {
210 Some(key) => {
211 match sub.serverid {
212 Some(id) => (key, id),
213 None =>
214 bail!("cannot retrieve changelog from enterprise repo: no server id found")
215 }
216 },
217 None => bail!("cannot retrieve changelog from enterprise repo: no subscription key found")
218 };
219
220 let mut auth_header = HashMap::new();
221 auth_header.insert("Authorization".to_owned(),
222 format!("Basic {}", base64::encode(format!("{}:{}", key, id))));
223
224 let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url, Some(&auth_header)))
225 .map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
226 Ok(json!(changelog))
227
228 } else {
229 let mut command = std::process::Command::new("apt-get");
230 command.arg("changelog");
231 command.arg("-qq"); // don't display download progress
232 command.arg(name);
233 let output = crate::tools::run_command(command, None)?;
234 Ok(json!(output))
235 }
236 }
237
238 #[api(
239 input: {
240 properties: {
241 node: {
242 schema: NODE_SCHEMA,
243 },
244 },
245 },
246 returns: {
247 description: "List of more relevant packages.",
248 type: Array,
249 items: {
250 type: APTUpdateInfo,
251 },
252 },
253 access: {
254 permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
255 },
256 )]
257 /// Get package information for important Proxmox Backup Server packages.
258 pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
259 const PACKAGES: &[&str] = &[
260 "ifupdown2",
261 "libjs-extjs",
262 "proxmox-backup",
263 "proxmox-backup-docs",
264 "proxmox-backup-client",
265 "proxmox-backup-server",
266 "proxmox-mini-journalreader",
267 "proxmox-widget-toolkit",
268 "pve-xtermjs",
269 "smartmontools",
270 "zfsutils-linux",
271 ];
272
273 fn unknown_package(package: String, extra_info: Option<String>) -> APTUpdateInfo {
274 APTUpdateInfo {
275 package,
276 title: "unknown".into(),
277 arch: "unknown".into(),
278 description: "unknown".into(),
279 version: "unknown".into(),
280 old_version: "unknown".into(),
281 origin: "unknown".into(),
282 priority: "unknown".into(),
283 section: "unknown".into(),
284 change_log_url: "unknown".into(),
285 extra_info,
286 }
287 }
288
289 let is_kernel = |name: &str| name.starts_with("pve-kernel-");
290
291 let mut packages: Vec<APTUpdateInfo> = Vec::new();
292 let pbs_packages = apt::list_installed_apt_packages(
293 |filter| {
294 filter.installed_version == Some(filter.active_version)
295 && (is_kernel(filter.package) || PACKAGES.contains(&filter.package))
296 },
297 None,
298 );
299
300 let running_kernel = format!(
301 "running kernel: {}",
302 nix::sys::utsname::uname().release().to_owned()
303 );
304 if let Some(proxmox_backup) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup") {
305 let mut proxmox_backup = proxmox_backup.clone();
306 proxmox_backup.extra_info = Some(running_kernel);
307 packages.push(proxmox_backup);
308 } else {
309 packages.push(unknown_package("proxmox-backup".into(), Some(running_kernel)));
310 }
311
312 let version = crate::api2::version::PROXMOX_PKG_VERSION;
313 let release = crate::api2::version::PROXMOX_PKG_RELEASE;
314 let daemon_version_info = Some(format!("running version: {}.{}", version, release));
315 if let Some(pkg) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup-server") {
316 let mut pkg = pkg.clone();
317 pkg.extra_info = daemon_version_info;
318 packages.push(pkg);
319 } else {
320 packages.push(unknown_package("proxmox-backup".into(), daemon_version_info));
321 }
322
323 let mut kernel_pkgs: Vec<APTUpdateInfo> = pbs_packages
324 .iter()
325 .filter(|pkg| is_kernel(&pkg.package))
326 .cloned()
327 .collect();
328 // make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
329 {
330 let cache = apt_pkg_native::Cache::get_singleton();
331 kernel_pkgs.sort_by(|left, right| {
332 cache
333 .compare_versions(&left.old_version, &right.old_version)
334 .reverse()
335 });
336 }
337 packages.append(&mut kernel_pkgs);
338
339 // add entry for all packages we're interested in, even if not installed
340 for pkg in PACKAGES.iter() {
341 if pkg == &"proxmox-backup" || pkg == &"proxmox-backup-server" {
342 continue;
343 }
344 match pbs_packages.iter().find(|item| &item.package == pkg) {
345 Some(apt_pkg) => packages.push(apt_pkg.to_owned()),
346 None => packages.push(unknown_package(pkg.to_string(), None)),
347 }
348 }
349
350 Ok(packages)
351 }
352
353 const SUBDIRS: SubdirMap = &[
354 ("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
355 ("update", &Router::new()
356 .get(&API_METHOD_APT_UPDATE_AVAILABLE)
357 .post(&API_METHOD_APT_UPDATE_DATABASE)
358 ),
359 ("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
360 ];
361
362 pub const ROUTER: Router = Router::new()
363 .get(&list_subdirs_api_method!(SUBDIRS))
364 .subdirs(SUBDIRS);