1 use std
::collections
::HashSet
;
3 use apt_pkg_native
::Cache
;
4 use anyhow
::{Error, bail, format_err}
;
5 use serde_json
::{json, Value}
;
7 use proxmox
::{list_subdirs_api_method, const_regex}
;
8 use proxmox
::api
::{api, RpcEnvironment, RpcEnvironmentType, Permission}
;
9 use proxmox
::api
::router
::{Router, SubdirMap}
;
11 use crate::server
::WorkerTask
;
12 use crate::tools
::http
;
14 use crate::config
::acl
::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}
;
15 use crate::api2
::types
::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA}
;
18 VERSION_EPOCH_REGEX
= r
"^\d+:";
19 FILENAME_EXTRACT_REGEX
= r
"^.*/.*?_(.*)_Packages$";
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)
31 ) -> Result
<String
, Error
> {
33 bail
!("no origin available for package {}", package
);
36 if origin
== "Debian" {
37 let mut command
= std
::process
::Command
::new("apt-get");
38 command
.arg("changelog");
39 command
.arg("--print-uris");
41 let output
= crate::tools
::run_command(command
, None
)?
; // format: 'http://foo/bar' package.changelog
42 let output
= match output
.splitn(2, ' '
).next() {
45 bail
!("invalid output (URI part too short) from 'apt-get changelog --print-uris': {}", output
)
47 output
[1..output
.len()-1].to_owned()
49 None
=> bail
!("invalid output from 'apt-get changelog --print-uris': {}", 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
, "");
57 let base
= match (FILENAME_EXTRACT_REGEX
.regex_obj
)().captures(filename
) {
59 let base_capture
= captures
.get(1);
61 Some(base_underscore
) => base_underscore
.as_str().replace("_", "/"),
62 None
=> bail
!("incompatible filename, cannot find regex group")
65 None
=> bail
!("incompatible filename, doesn't match regex")
68 return Ok(format
!("http://download.proxmox.com/{}/{}_{}.changelog",
69 base
, package
, version
));
72 bail
!("unknown origin ({}) or component ({})", origin
, component
)
75 struct FilterData
<'a
> {
76 // this is version info returned by APT
77 installed_version
: Option
<&'a
str>,
78 candidate_version
: &'a
str,
80 // this is the version info the filter is supposed to check
81 active_version
: &'a
str,
84 enum PackagePreSelect
{
90 fn list_installed_apt_packages
<F
: Fn(FilterData
) -> bool
>(
92 only_versions_for
: Option
<&str>,
93 ) -> Vec
<APTUpdateInfo
> {
95 let mut ret
= Vec
::new();
96 let mut depends
= HashSet
::new();
98 // note: this is not an 'apt update', it just re-reads the cache from disk
99 let mut cache
= Cache
::get_singleton();
102 let mut cache_iter
= match only_versions_for
{
103 Some(name
) => cache
.find_by_name(name
),
109 match cache_iter
.next() {
111 let di
= if only_versions_for
.is_some() {
113 PackagePreSelect
::All
,
120 PackagePreSelect
::OnlyInstalled
,
126 if let Some(info
) = di
{
130 if only_versions_for
.is_some() {
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() {
141 None
=> continue // package not found, ignore
144 let di
= query_detailed_info(
145 PackagePreSelect
::OnlyNew
,
150 if let Some(info
) = di
{
162 fn query_detailed_info
<'a
, F
, V
>(
163 pre_select
: PackagePreSelect
,
166 depends
: Option
<&mut HashSet
<String
>>,
167 ) -> Option
<APTUpdateInfo
>
169 F
: Fn(FilterData
) -> bool
,
170 V
: std
::ops
::Deref
<Target
= apt_pkg_native
::sane
::PkgView
<'a
>>
172 let current_version
= view
.current_version();
173 let candidate_version
= view
.candidate_version();
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
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
,
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
,
196 // get additional information via nested APT 'iterators'
197 let mut view_iter
= view
.versions();
198 while let Some(ver
) = view_iter
.next() {
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();
209 let fd
= FilterData
{
210 installed_version
: current_version
.as_deref(),
211 candidate_version
: &candidate_version
,
212 active_version
: &version
,
216 if let Some(section
) = ver
.section() {
217 section_res
= section
;
220 if let Some(prio
) = ver
.priority_type() {
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
{
231 if let Some(sd
) = origin
.short_desc() {
235 if let Some(ld
) = origin
.long_desc() {
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
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
;
250 let filename
= pkg_file
.file_name();
251 let component
= pkg_file
.component();
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
;
263 if let Some(depends
) = depends
{
264 let mut dep_iter
= ver
.dep_iter();
266 let dep
= match dep_iter
.next() {
267 Some(dep
) if dep
.dep_type() != "Depends" => continue,
272 let dep_pkg
= dep
.target_pkg();
273 let name
= dep_pkg
.name();
275 depends
.insert(name
);
279 return Some(APTUpdateInfo
{
283 description
: long_desc
,
286 version
: candidate_version
.clone(),
287 old_version
: match current_version
{
289 None
=> "".to_owned()
291 priority
: priority_res
,
292 section
: section_res
,
309 description
: "A list of packages with available updates.",
311 items
: { type: APTUpdateInfo }
,
314 permission
: &Permission
::Privilege(&[], PRIV_SYS_AUDIT
, false),
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
)
323 Ok(json
!(all_upgradeable
))
334 description
: "Only produces output suitable for logging, omitting progress indicators.",
345 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
348 /// Update the APT database
349 pub fn apt_update_database(
351 rpcenv
: &mut dyn RpcEnvironment
,
352 ) -> Result
<String
, Error
> {
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
);
358 let upid_str
= WorkerTask
::new_thread("aptupdate", None
, auth_id
, to_stdout
, move |worker
| {
359 if !quiet { worker.log("starting apt-get update") }
361 // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE
363 let mut command
= std
::process
::Command
::new("apt-get");
364 command
.arg("update");
366 let output
= crate::tools
::run_command(command
, None
)?
;
367 if !quiet { worker.log(output) }
369 // TODO: add mail notify for new updates like PVE
384 description
: "Package name to get changelog of.",
388 description
: "Package version to get changelog of. Omit to use candidate version.",
398 permission
: &Permission
::Privilege(&[], PRIV_SYS_MODIFY
, false),
401 /// Retrieve the changelog of the specified package.
402 fn apt_get_changelog(
404 ) -> Result
<Value
, Error
> {
406 let name
= crate::tools
::required_string_param(¶m
, "name")?
.to_owned();
407 let version
= param
["version"].as_str();
409 let pkg_info
= list_installed_apt_packages(|data
| {
411 Some(version
) => version
== data
.active_version
,
412 None
=> data
.active_version
== data
.candidate_version
416 if pkg_info
.len() == 0 {
417 bail
!("Package '{}' not found", name
);
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
));
427 let mut command
= std
::process
::Command
::new("apt-get");
428 command
.arg("changelog");
429 command
.arg("-qq"); // don't display download progress
431 let output
= crate::tools
::run_command(command
, None
)?
;
432 return Ok(json
!(output
));
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
)
444 pub const ROUTER
: Router
= Router
::new()
445 .get(&list_subdirs_api_method
!(SUBDIRS
))