* [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view
@ 2025-10-15 12:46 Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
` (13 more replies)
0 siblings, 14 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:46 UTC (permalink / raw)
To: pdm-devel
This series adds a new tab under "Remotes" called "Updates". It provides a
summary regarding the system update availability for all managed remotes.
Potential follow-up work:
- The "Refresh all" button, powered by the '/remote-updates/refresh' API
only retrieves a fresh list of available updates at the moment, but does not
invoke 'apt update' on the remote. The latter could be useful, either
always or if explicitly requested, but we probably should 'stream'
the node's task log to the PDM task log, so one can see the actual
progress and/or any problems.
- Remote task cache / task tracking needs a bit of work to correctly handle
PBS tasks, therefore the 'Update' (which *does* invoke 'apt update' on the
remote node) button for a *SINGLE* node does not work yet for PBS.
proxmox-datacenter-manager:
Lukas Wagner (12):
metric collection task: tests: add missing parameter for
cluster_metric_export
pdm-api-types: add types for remote upgrade summary
remote updates: add cache for remote update availability
api: add API for retrieving/refreshing the remote update summary
unprivileged api daemon: tasks: add remote update refresh task
pdm-client: add API methods for remote update summaries
pbs-client: add bindings for APT-related API calls
task cache: use separate functions for tracking PVE and PBS tasks
remote updates: add support for PBS remotes
api: add APT endpoints for PBS remotes
ui: add remote update view
ui: show new remote update view in the 'Remotes' section
lib/pdm-api-types/src/lib.rs | 2 +
lib/pdm-api-types/src/remote_updates.rs | 126 +++++
lib/pdm-client/src/lib.rs | 22 +
server/src/api/mod.rs | 3 +
server/src/api/pbs/mod.rs | 19 +-
server/src/api/pbs/node.rs | 9 +
server/src/api/pve/apt.rs | 119 ----
server/src/api/pve/mod.rs | 4 +-
server/src/api/pve/node.rs | 2 +-
server/src/api/remote_updates.rs | 222 ++++++++
server/src/bin/proxmox-datacenter-api/main.rs | 1 +
.../bin/proxmox-datacenter-api/tasks/mod.rs | 1 +
.../tasks/remote_updates.rs | 44 ++
.../src/metric_collection/collection_task.rs | 1 +
server/src/pbs_client.rs | 51 ++
server/src/remote_tasks/mod.rs | 45 +-
server/src/remote_updates.rs | 229 +++++++-
ui/src/remotes/mod.rs | 10 +
ui/src/remotes/updates.rs | 531 ++++++++++++++++++
19 files changed, 1299 insertions(+), 142 deletions(-)
create mode 100644 lib/pdm-api-types/src/remote_updates.rs
create mode 100644 server/src/api/pbs/node.rs
delete mode 100644 server/src/api/pve/apt.rs
create mode 100644 server/src/api/remote_updates.rs
create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
create mode 100644 ui/src/remotes/updates.rs
Summary over all repositories:
19 files changed, 1299 insertions(+), 142 deletions(-)
--
Generated by murpp 0.9.0
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
` (12 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/metric_collection/collection_task.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/src/metric_collection/collection_task.rs b/server/src/metric_collection/collection_task.rs
index cfa54283..a6c84433 100644
--- a/server/src/metric_collection/collection_task.rs
+++ b/server/src/metric_collection/collection_task.rs
@@ -457,6 +457,7 @@ pub(super) mod tests {
&self,
_history: Option<bool>,
_local_only: Option<bool>,
+ _node_list: Option<String>,
start_time: Option<i64>,
) -> Result<ClusterMetrics, proxmox_client::Error> {
if self.fail {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/12] remote updates: add cache for remote update availability Lukas Wagner
` (11 subsequent siblings)
13 siblings, 1 reply; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
This type contains the number of available updates per remote node,
without any details. This is useful for a global "give me the update
availability for all managed nodes" API endpoint.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-api-types/src/lib.rs | 2 +
lib/pdm-api-types/src/remote_updates.rs | 126 ++++++++++++++++++++++++
2 files changed, 128 insertions(+)
create mode 100644 lib/pdm-api-types/src/remote_updates.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index a3566142..2fb61ef7 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -96,6 +96,8 @@ pub use openid::*;
pub mod remotes;
+pub mod remote_updates;
+
pub mod resource;
pub mod rrddata;
diff --git a/lib/pdm-api-types/src/remote_updates.rs b/lib/pdm-api-types/src/remote_updates.rs
new file mode 100644
index 00000000..d04a7a79
--- /dev/null
+++ b/lib/pdm-api-types/src/remote_updates.rs
@@ -0,0 +1,126 @@
+use std::{
+ collections::HashMap,
+ ops::{Deref, DerefMut},
+};
+
+use proxmox_schema::{api, ApiType, ObjectSchema};
+use serde::{Deserialize, Serialize};
+
+use crate::remotes::RemoteType;
+
+#[api]
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+/// Update summary for all remotes.
+pub struct UpdateSummary {
+ /// Map containing the update summary each remote.
+ pub remotes: RemoteUpdateSummaryWrapper,
+}
+
+// This is a hack to allow actual 'maps' (mapping remote name to per-remote data)
+// within the realms of our API macro.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct RemoteUpdateSummaryWrapper {
+ #[serde(flatten)]
+ remotes: HashMap<String, RemoteUpdateSummary>,
+}
+
+impl ApiType for RemoteUpdateSummaryWrapper {
+ const API_SCHEMA: proxmox_schema::Schema =
+ ObjectSchema::new("Map of per-remote update summaries", &[])
+ .additional_properties(true)
+ .schema();
+}
+
+impl Deref for RemoteUpdateSummaryWrapper {
+ type Target = HashMap<String, RemoteUpdateSummary>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.remotes
+ }
+}
+
+impl DerefMut for RemoteUpdateSummaryWrapper {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.remotes
+ }
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+/// Update summary for a single remote.
+pub struct RemoteUpdateSummary {
+ /// Map containing the update summary for each node of this remote.
+ pub nodes: NodeUpdateSummaryWrapper,
+ pub remote_type: RemoteType,
+ pub status: RemoteUpdateStatus,
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Status for the entire remote.
+pub enum RemoteUpdateStatus {
+ /// Successfully polled remote.
+ Success,
+ /// Remote could not be polled.
+ Error,
+ /// Remote has not been polled yet.
+ Unknown,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct NodeUpdateSummaryWrapper {
+ #[serde(flatten)]
+ nodes: HashMap<String, NodeUpdateSummary>,
+}
+
+impl ApiType for NodeUpdateSummaryWrapper {
+ const API_SCHEMA: proxmox_schema::Schema =
+ ObjectSchema::new("Map of per-node update summaries", &[])
+ .additional_properties(true)
+ .schema();
+}
+
+impl Deref for NodeUpdateSummaryWrapper {
+ type Target = HashMap<String, NodeUpdateSummary>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.nodes
+ }
+}
+
+impl DerefMut for NodeUpdateSummaryWrapper {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.nodes
+ }
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Status for the entire remote.
+pub enum NodeUpdateStatus {
+ /// Successfully polled node.
+ Success,
+ /// Node could not be polled.
+ Error,
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Per-node update summary.
+pub struct NodeUpdateSummary {
+ /// Number of available updates.
+ pub number_of_updates: u32,
+ /// Unix timestamp of the last refresh.
+ pub last_refresh: i64,
+ /// Status
+ pub status: NodeUpdateStatus,
+ /// Status message (e.g. error message)
+ pub status_message: Option<String>,
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 03/12] remote updates: add cache for remote update availability
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
` (10 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
The cache will be filled by either the `refresh_summary_cache` function,
or when the list of updates for a single node is requested.
The cache contains only the summary (number of updates) and not the full
list of packages. We can always choose to add the latter later if we
need it.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/remote_updates.rs | 192 +++++++++++++++++++++++++++++++++--
1 file changed, 186 insertions(+), 6 deletions(-)
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index f833062c..ab5845bd 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -1,20 +1,53 @@
+use std::fs::File;
+use std::io::ErrorKind;
+
use anyhow::{bail, Error};
-use pdm_api_types::RemoteUpid;
+use serde::{Deserialize, Serialize};
use proxmox_apt_api_types::APTUpdateInfo;
+use pdm_api_types::remote_updates::{
+ NodeUpdateStatus, NodeUpdateSummary, NodeUpdateSummaryWrapper, RemoteUpdateStatus,
+ RemoteUpdateSummary, UpdateSummary,
+};
use pdm_api_types::remotes::{Remote, RemoteType};
+use pdm_api_types::RemoteUpid;
+use pdm_buildcfg::PDM_CACHE_DIR_M;
use crate::api::pve::new_remote_upid;
use crate::connection;
+use crate::parallel_fetcher::{NodeResults, ParallelFetcher};
+
+pub const UPDATE_CACHE: &str = concat!(PDM_CACHE_DIR_M!(), "/remote-updates.json");
+
+#[derive(Clone, Default, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+struct NodeUpdateInfo {
+ updates: Vec<APTUpdateInfo>,
+ last_refresh: i64,
+}
+
+impl From<NodeUpdateInfo> for NodeUpdateSummary {
+ fn from(value: NodeUpdateInfo) -> Self {
+ Self {
+ number_of_updates: value.updates.len() as u32,
+ last_refresh: value.last_refresh,
+ status: NodeUpdateStatus::Success,
+ status_message: None,
+ }
+ }
+}
/// Return a list of available updates for a given remote node.
pub async fn list_available_updates(
remote: Remote,
node: &str,
) -> Result<Vec<APTUpdateInfo>, Error> {
- let updates = fetch_available_updates(remote, node.to_string()).await?;
- Ok(updates)
+ let updates = fetch_available_updates((), remote.clone(), node.to_string()).await?;
+
+ update_cached_summary_for_node(remote, node.into(), updates.clone().into()).await?;
+
+ Ok(updates.updates)
}
/// Trigger `apt update` on a remote node.
@@ -52,10 +85,154 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
}
}
-async fn fetch_available_updates(
+/// Get update summary for all managed remotes.
+pub fn get_available_updates_summary() -> Result<UpdateSummary, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+
+ let cache_content = get_cached_summary_or_default()?;
+
+ let mut summary = UpdateSummary::default();
+
+ for (remote_name, remote) in &config {
+ match cache_content.remotes.get(remote_name) {
+ Some(remote_summary) => {
+ summary
+ .remotes
+ .insert(remote_name.into(), remote_summary.clone());
+ }
+ None => {
+ summary.remotes.insert(
+ remote_name.into(),
+ RemoteUpdateSummary {
+ nodes: NodeUpdateSummaryWrapper::default(),
+ remote_type: remote.ty,
+ status: RemoteUpdateStatus::Unknown,
+ },
+ );
+ }
+ }
+ }
+
+ Ok(summary)
+}
+
+fn get_cached_summary_or_default() -> Result<UpdateSummary, Error> {
+ match File::open(UPDATE_CACHE) {
+ Ok(file) => {
+ let content = match serde_json::from_reader(file) {
+ Ok(cache_content) => cache_content,
+ Err(err) => {
+ log::error!("failed to deserialize remote update cache: {err:#}");
+ Default::default()
+ }
+ };
+
+ Ok(content)
+ }
+ Err(err) if err.kind() == ErrorKind::NotFound => Ok(Default::default()),
+ Err(err) => Err(err.into()),
+ }
+}
+
+async fn update_cached_summary_for_node(
remote: Remote,
node: String,
-) -> Result<Vec<APTUpdateInfo>, Error> {
+ node_data: NodeUpdateSummary,
+) -> Result<(), Error> {
+ let mut file = File::open(UPDATE_CACHE)?;
+ let mut cache_content: UpdateSummary = serde_json::from_reader(&mut file)?;
+ let remote_entry =
+ cache_content
+ .remotes
+ .entry(remote.id)
+ .or_insert_with(|| RemoteUpdateSummary {
+ nodes: Default::default(),
+ remote_type: remote.ty,
+ status: RemoteUpdateStatus::Success,
+ });
+
+ remote_entry.nodes.insert(node, node_data);
+
+ let options = proxmox_product_config::default_create_options();
+ proxmox_sys::fs::replace_file(
+ UPDATE_CACHE,
+ &serde_json::to_vec(&cache_content)?,
+ options,
+ true,
+ )?;
+
+ Ok(())
+}
+
+/// Refresh the remote update cache.
+pub async fn refresh_update_summary_cache(remotes: Vec<Remote>) -> Result<(), Error> {
+ let fetcher = ParallelFetcher::new(());
+
+ let fetch_results = fetcher
+ .do_for_all_remote_nodes(remotes.clone().into_iter(), fetch_available_updates)
+ .await;
+
+ let mut content = get_cached_summary_or_default()?;
+
+ for (remote_name, result) in fetch_results.remote_results {
+ let entry = content
+ .remotes
+ .entry(remote_name.clone())
+ .or_insert_with(|| {
+ // unwrap: remote name came from the same config, should be safe.
+ // TODO: Include type in ParallelFetcher results - should be much more efficient.
+ let remote_type = remotes.iter().find(|r| r.id == remote_name).unwrap().ty;
+
+ RemoteUpdateSummary {
+ nodes: Default::default(),
+ remote_type,
+ status: RemoteUpdateStatus::Success,
+ }
+ });
+
+ match result {
+ Ok(remote_result) => {
+ for (node_name, node_result) in remote_result.node_results {
+ match node_result {
+ Ok(NodeResults { data, .. }) => {
+ entry.nodes.insert(node_name, data.into());
+ }
+ Err(err) => {
+ // Could not fetch updates from node
+ entry.nodes.insert(
+ node_name.clone(),
+ NodeUpdateSummary {
+ number_of_updates: 0,
+ last_refresh: 0,
+ status: NodeUpdateStatus::Error,
+ status_message: Some(format!("{err:#}")),
+ },
+ );
+ log::error!(
+ "could not fetch available updates from node '{node_name}': {err}"
+ );
+ }
+ }
+ }
+ }
+ Err(err) => {
+ entry.status = RemoteUpdateStatus::Error;
+ log::error!("could not fetch available updates from remote '{remote_name}': {err}");
+ }
+ }
+ }
+
+ let options = proxmox_product_config::default_create_options();
+ proxmox_sys::fs::replace_file(UPDATE_CACHE, &serde_json::to_vec(&content)?, options, true)?;
+
+ Ok(())
+}
+
+async fn fetch_available_updates(
+ _context: (),
+ remote: Remote,
+ node: String,
+) -> Result<NodeUpdateInfo, Error> {
match remote.ty {
RemoteType::Pve => {
let client = connection::make_pve_client(&remote)?;
@@ -67,7 +244,10 @@ async fn fetch_available_updates(
.map(map_pve_update_info)
.collect();
- Ok(updates)
+ Ok(NodeUpdateInfo {
+ last_refresh: proxmox_time::epoch_i64(),
+ updates,
+ })
}
RemoteType::Pbs => bail!("PBS is not supported yet"),
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (2 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/12] remote updates: add cache for remote update availability Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-17 7:44 ` Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
` (9 subsequent siblings)
13 siblings, 2 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
This commit adds two new endpoints, namely
GET /remote-updates/summary
POST /remote-updates/refresh
The first one is used to retrieve the update summary (the data is taken
from the cache), the second one can be used to proactively refresh the
summary in the cache (starts a worker task, since this could take a
while). Note that we only retrieve the up-to-date list of packages from
the remote, but do *not* trigger an `apt update` right now. Could make
sense to do the latter as well, but then we probably should
stream/forward the task logs for the upgrade task from the node to the
native PDM task; something we can rather implement later.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/mod.rs | 3 +
server/src/api/remote_updates.rs | 108 +++++++++++++++++++++++++++++++
2 files changed, 111 insertions(+)
create mode 100644 server/src/api/remote_updates.rs
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index 02ee0ecf..6a7a65a2 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -14,6 +14,7 @@ pub mod nodes;
pub mod pbs;
pub mod pve;
pub mod remote_tasks;
+pub mod remote_updates;
pub mod remotes;
pub mod resources;
mod rrd_common;
@@ -31,6 +32,8 @@ const SUBDIRS: SubdirMap = &sorted!([
("resources", &resources::ROUTER),
("nodes", &nodes::ROUTER),
("remote-tasks", &remote_tasks::ROUTER),
+ // TODO: There might be a better place for this endpoint.
+ ("remote-updates", &remote_updates::ROUTER),
("sdn", &sdn::ROUTER),
("version", &Router::new().get(&API_METHOD_VERSION)),
]);
diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
new file mode 100644
index 00000000..724b705a
--- /dev/null
+++ b/server/src/api/remote_updates.rs
@@ -0,0 +1,108 @@
+//! API for getting a remote update update summary.
+
+use anyhow::Error;
+
+use pdm_api_types::remote_updates::UpdateSummary;
+use pdm_api_types::remotes::Remote;
+use pdm_api_types::{PRIV_RESOURCE_MODIFY, UPID};
+use proxmox_access_control::CachedUserInfo;
+use proxmox_rest_server::WorkerTask;
+use proxmox_router::{
+ http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
+
+use crate::remote_updates;
+
+pub const ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+ ("summary", &Router::new().get(&API_METHOD_UPDATE_SUMMARY)),
+ (
+ "refresh",
+ &Router::new().post(&API_METHOD_REFRESH_REMOTE_UPDATE_SUMMARIES)
+ ),
+]);
+
+#[api(
+ access: {
+ permission: &Permission::Anybody,
+ description: "Resource.Modify privileges are needed on /resource/{remote}",
+ },
+)]
+/// Return available update summary for managed remote nodes.
+pub fn update_summary(rpcenv: &mut dyn RpcEnvironment) -> Result<UpdateSummary, Error> {
+ let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+ let user_info = CachedUserInfo::new()?;
+
+ if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
+ http_bail!(UNAUTHORIZED, "user has no access to resources");
+ }
+
+ let mut update_summary = remote_updates::get_available_updates_summary()?;
+
+ update_summary.remotes.retain(|remote_name, _| {
+ user_info
+ .check_privs(
+ &auth_id,
+ &["resource", remote_name],
+ PRIV_RESOURCE_MODIFY,
+ false,
+ )
+ .is_ok()
+ });
+
+ Ok(update_summary)
+}
+
+#[api(
+ access: {
+ permission: &Permission::Anybody,
+ description: "Resource.Modify privileges are needed on /resource/{remote}",
+ },
+)]
+/// Refresh the update summary of all remotes.
+pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Result<UPID, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+
+ let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+ let user_info = CachedUserInfo::new()?;
+
+ if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
+ http_bail!(UNAUTHORIZED, "user has no access to resources");
+ }
+
+ let remotes: Vec<Remote> = config
+ .into_iter()
+ .filter_map(|(remote_name, remote)| {
+ user_info
+ .check_privs(
+ &auth_id,
+ &["resource", &remote_name],
+ PRIV_RESOURCE_MODIFY,
+ false,
+ )
+ .is_ok()
+ .then_some(remote)
+ })
+ .collect();
+
+ let upid_str = WorkerTask::spawn(
+ "refresh-remote-updates",
+ None,
+ auth_id.to_string(),
+ true,
+ |_worker| async {
+ // TODO: Add more verbose logging per remote/node, so we can actually see something
+ // interesting in the task log.
+ remote_updates::refresh_update_summary_cache(remotes).await?;
+ Ok(())
+ },
+ )?;
+
+ upid_str.parse()
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 05/12] unprivileged api daemon: tasks: add remote update refresh task
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (3 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/12] pdm-client: add API methods for remote update summaries Lukas Wagner
` (8 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
This commit adds a very simple refresh tasks that runs at relatively
long interval. The task polls all remotes for available updates and puts
the results in the update cache.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/bin/proxmox-datacenter-api/main.rs | 1 +
.../bin/proxmox-datacenter-api/tasks/mod.rs | 1 +
.../tasks/remote_updates.rs | 44 +++++++++++++++++++
3 files changed, 46 insertions(+)
create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
diff --git a/server/src/bin/proxmox-datacenter-api/main.rs b/server/src/bin/proxmox-datacenter-api/main.rs
index c0f0e3a4..420a3b4d 100644
--- a/server/src/bin/proxmox-datacenter-api/main.rs
+++ b/server/src/bin/proxmox-datacenter-api/main.rs
@@ -377,6 +377,7 @@ async fn run(debug: bool) -> Result<(), Error> {
tasks::remote_node_mapping::start_task();
resource_cache::start_task();
tasks::remote_tasks::start_task()?;
+ tasks::remote_updates::start_task()?;
server.await?;
log::info!("server shutting down, waiting for active workers to complete");
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
index a6b1f439..f4d1d3a1 100644
--- a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
+++ b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
@@ -1,2 +1,3 @@
pub mod remote_node_mapping;
pub mod remote_tasks;
+pub mod remote_updates;
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs b/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
new file mode 100644
index 00000000..bd9e178d
--- /dev/null
+++ b/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
@@ -0,0 +1,44 @@
+use anyhow::Error;
+
+use server::{remote_updates, task_utils};
+
+const REFRESH_TIME: u64 = 6 * 3600;
+
+/// Start the remote task fetching task
+pub fn start_task() -> Result<(), Error> {
+ tokio::spawn(async move {
+ let task_scheduler = std::pin::pin!(RemoteUpdateRefreshTask {}.run());
+ let abort_future = std::pin::pin!(proxmox_daemon::shutdown_future());
+ futures::future::select(task_scheduler, abort_future).await;
+ });
+
+ Ok(())
+}
+
+struct RemoteUpdateRefreshTask {}
+
+impl RemoteUpdateRefreshTask {
+ async fn run(self) {
+ loop {
+ self.refresh().await;
+ self.wait_for_refresh().await;
+ }
+ }
+
+ async fn refresh(&self) {
+ if let Err(err) = self.do_refresh().await {
+ log::error!("could not refresh remote update cache: {err:#}");
+ }
+ }
+
+ async fn do_refresh(&self) -> Result<(), Error> {
+ let (config, _digest) = tokio::task::spawn_blocking(pdm_config::remotes::config).await??;
+ remote_updates::refresh_update_summary_cache(config.into_iter().map(|(_, r)| r).collect())
+ .await
+ }
+
+ async fn wait_for_refresh(&self) {
+ let instant = task_utils::next_aligned_instant(REFRESH_TIME);
+ tokio::time::sleep_until(instant.into()).await;
+ }
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 06/12] pdm-client: add API methods for remote update summaries
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (4 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
` (7 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
Adds methods to retrieve and refresh the update summary.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-client/src/lib.rs | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 2f36fab1..0cab7691 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -1109,6 +1109,28 @@ impl<T: HttpApiClient> PdmClient<T> {
}
Ok(self.0.post(&path, ¶ms).await?.expect_json()?.data)
}
+
+ /// Get remote update summary.
+ pub async fn remote_update_summary(
+ &self,
+ ) -> Result<pdm_api_types::remote_updates::UpdateSummary, Error> {
+ Ok(self
+ .0
+ .get("/api2/extjs/remote-updates/summary")
+ .await?
+ .expect_json()?
+ .data)
+ }
+
+ /// Refresh remote update summary.
+ pub async fn refresh_remote_update_summary(&self) -> Result<pdm_api_types::UPID, Error> {
+ Ok(self
+ .0
+ .post_without_body("/api2/extjs/remote-updates/refresh")
+ .await?
+ .expect_json()?
+ .data)
+ }
}
/// Builder for migration parameters.
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 07/12] pbs-client: add bindings for APT-related API calls
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (5 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/12] pdm-client: add API methods for remote update summaries Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
` (6 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
This commit adds bindings for the following API methods:
GET /node/<node>/apt/update
POST /node/<node>/apt/update
GET /node/<node>/apt/changelog
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/pbs_client.rs | 51 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/server/src/pbs_client.rs b/server/src/pbs_client.rs
index d8278c8a..f106c82b 100644
--- a/server/src/pbs_client.rs
+++ b/server/src/pbs_client.rs
@@ -114,6 +114,17 @@ pub struct DatstoreListNamespaces {
pub max_depth: Option<usize>,
}
+#[api]
+/// Parameters for updating the APT database
+#[derive(serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct AptUpdateParams {
+ /// Send notification in case of new updates.
+ pub notify: Option<bool>,
+ /// Don't show progress information in the output.
+ pub quiet: Option<bool>,
+}
+
impl PbsClient {
/// API version details, including some parts of the global datacenter config.
pub async fn version(&self) -> Result<pve_api_types::VersionResponse, Error> {
@@ -268,6 +279,46 @@ impl PbsClient {
.expect_json()?
.data)
}
+
+ /// Return a list of available system updates.
+ pub async fn list_available_updates(&self) -> Result<Vec<pbs_api_types::APTUpdateInfo>, Error> {
+ Ok(self
+ .0
+ .get("/api2/extjs/nodes/localhost/apt/update")
+ .await?
+ .expect_json()?
+ .data)
+ }
+
+ /// Update the APT database.
+ pub async fn update_apt_database(
+ &self,
+ params: AptUpdateParams,
+ ) -> Result<pbs_api_types::UPID, Error> {
+ Ok(self
+ .0
+ .post("/api2/extjs/nodes/localhost/apt/update", ¶ms)
+ .await?
+ .expect_json()?
+ .data)
+ }
+
+ /// Get changelog for a single package.
+ ///
+ /// `package`: Package name to get the changelog of.
+ /// `version`: Package version to get changelog of. Omit to use candidate version.
+ pub async fn get_package_changelog(
+ &self,
+ package: String,
+ version: Option<String>,
+ ) -> Result<String, Error> {
+ let path = ApiPathBuilder::new("/api2/extjs/nodes/localhost/apt/changelog")
+ .arg("name", &package)
+ .maybe_arg("version", &version)
+ .build();
+
+ Ok(self.0.get(&path).await?.expect_json()?.data)
+ }
}
#[derive(Deserialize)]
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 08/12] task cache: use separate functions for tracking PVE and PBS tasks
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (6 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/12] remote updates: add support for PBS remotes Lukas Wagner
` (5 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
At the callsite we always know which type of remote it is, so there is
no need to deduce the type from the UPID itself.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/pve/mod.rs | 3 +--
server/src/remote_tasks/mod.rs | 45 ++++++++++++++++++++++++++++------
2 files changed, 39 insertions(+), 9 deletions(-)
diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index 0de82323..a94ab8d0 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -84,8 +84,7 @@ const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
// converts a remote + PveUpid into a RemoteUpid and starts tracking it
pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
- let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
- remote_tasks::track_running_task(remote_upid.clone()).await?;
+ let remote_upid = remote_tasks::track_running_pve_task(remote, upid).await?;
Ok(remote_upid)
}
diff --git a/server/src/remote_tasks/mod.rs b/server/src/remote_tasks/mod.rs
index b3adebe2..c4939742 100644
--- a/server/src/remote_tasks/mod.rs
+++ b/server/src/remote_tasks/mod.rs
@@ -126,22 +126,53 @@ pub async fn get_tasks(
.await?
}
-/// Insert a newly created tasks into the list of tracked tasks.
+/// Insert a newly created PVE task into the list of tracked tasks.
///
/// Any tracked task will be polled with a short interval until the task
/// has finished.
-pub async fn track_running_task(task: RemoteUpid) -> Result<(), Error> {
+///
+/// This function returns the [`RemoteUpid`] of the tracked PVE task.
+pub async fn track_running_pve_task(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
tokio::task::spawn_blocking(move || {
+ let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
let cache = get_cache()?.write()?;
- // TODO:: Handle PBS tasks correctly.
- let pve_upid: pve_api_types::PveUpid = task.upid.parse()?;
+
let task = TaskCacheItem {
- upid: task.clone(),
- starttime: pve_upid.starttime,
+ upid: remote_upid.clone(),
+ starttime: upid.starttime,
status: None,
endtime: None,
};
- cache.add_tracked_task(task)
+ cache.add_tracked_task(task)?;
+
+ Ok(remote_upid)
+ })
+ .await?
+}
+
+/// Insert a newly created PBS task into the list of tracked tasks.
+///
+/// Any tracked task will be polled with a short interval until the task
+/// has finished.
+///
+/// This function returns the [`RemoteUpid`] of the tracked PBS task.
+pub async fn track_running_pbs_task(
+ remote: String,
+ upid: pbs_api_types::UPID,
+) -> Result<RemoteUpid, Error> {
+ tokio::task::spawn_blocking(move || {
+ let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
+ let cache = get_cache()?.write()?;
+
+ let task = TaskCacheItem {
+ upid: remote_upid.clone(),
+ starttime: upid.starttime,
+ status: None,
+ endtime: None,
+ };
+ cache.add_tracked_task(task)?;
+
+ Ok(remote_upid)
})
.await?
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 09/12] remote updates: add support for PBS remotes
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (7 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/12] api: add APT endpoints " Lukas Wagner
` (4 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
The 'update_apt_database' function starts a new tracked remote task. The
remote task infrastructure needs a bit more work to work with PBS UPIDs,
so the code is commented out for now.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/pbs/mod.rs | 15 ++++++++++++++-
server/src/remote_updates.rs | 37 +++++++++++++++++++++++++++++++-----
2 files changed, 46 insertions(+), 6 deletions(-)
diff --git a/server/src/api/pbs/mod.rs b/server/src/api/pbs/mod.rs
index dc31f620..b3bb126c 100644
--- a/server/src/api/pbs/mod.rs
+++ b/server/src/api/pbs/mod.rs
@@ -9,13 +9,17 @@ use proxmox_sortable_macro::sortable;
use pdm_api_types::remotes::{
NodeUrl, Remote, RemoteListEntry, RemoteType, TlsProbeOutcome, REMOTE_ID_SCHEMA,
};
-use pdm_api_types::{Authid, HOST_OPTIONAL_PORT_FORMAT, PRIV_RESOURCE_AUDIT, PRIV_SYS_MODIFY};
+use pdm_api_types::{
+ Authid, RemoteUpid, HOST_OPTIONAL_PORT_FORMAT, PRIV_RESOURCE_AUDIT, PRIV_SYS_MODIFY,
+};
use crate::{
connection::{self, probe_tls_connection},
pbs_client::{self, get_remote, PbsClient},
};
+use crate::remote_tasks;
+
mod rrddata;
pub const ROUTER: Router = Router::new()
@@ -65,6 +69,15 @@ const DATASTORE_ITEM_SUBDIRS: SubdirMap = &sorted!([
),
]);
+// converts a remote + pbs_api_types::UPID into a RemoteUpid and starts tracking it
+pub async fn new_remote_upid(
+ remote: String,
+ upid: pbs_api_types::UPID,
+) -> Result<RemoteUpid, Error> {
+ let remote_upid = remote_tasks::track_running_pbs_task(remote, upid).await?;
+ Ok(remote_upid)
+}
+
#[api(
returns: {
type: Array,
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index ab5845bd..4c70495b 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -14,7 +14,6 @@ use pdm_api_types::remotes::{Remote, RemoteType};
use pdm_api_types::RemoteUpid;
use pdm_buildcfg::PDM_CACHE_DIR_M;
-use crate::api::pve::new_remote_upid;
use crate::connection;
use crate::parallel_fetcher::{NodeResults, ParallelFetcher};
@@ -64,9 +63,22 @@ pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUp
};
let upid = client.update_apt_database(node, params).await?;
- new_remote_upid(remote.id.clone(), upid).await
+ crate::api::pve::new_remote_upid(remote.id.clone(), upid).await
+ }
+ RemoteType::Pbs => {
+ // let client = connection::make_pbs_client(remote)?;
+ //
+ // let params = crate::pbs_client::AptUpdateParams {
+ // notify: Some(false),
+ // quiet: Some(false),
+ // };
+ // let upid = client.update_apt_database(params).await?;
+ //
+ // crate::api::pbs::new_remote_upid(remote.id.clone(), upid).await
+ // TODO: task infrastructure for PBS not finished yet, uncomment once
+ // this is done.
+ bail!("PBS is not supported yet");
}
- RemoteType::Pbs => bail!("PBS is not supported yet"),
}
}
@@ -81,7 +93,14 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
.await
.map_err(Into::into)
}
- RemoteType::Pbs => bail!("PBS is not supported yet"),
+ RemoteType::Pbs => {
+ let client = connection::make_pbs_client(&remote)?;
+
+ client
+ .get_package_changelog(package, None)
+ .await
+ .map_err(Into::into)
+ }
}
}
@@ -249,7 +268,15 @@ async fn fetch_available_updates(
updates,
})
}
- RemoteType::Pbs => bail!("PBS is not supported yet"),
+ RemoteType::Pbs => {
+ let client = connection::make_pbs_client(&remote)?;
+ let updates = client.list_available_updates().await?;
+
+ Ok(NodeUpdateInfo {
+ last_refresh: proxmox_time::epoch_i64(),
+ updates,
+ })
+ }
}
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 10/12] api: add APT endpoints for PBS remotes
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (8 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/12] remote updates: add support for PBS remotes Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view Lukas Wagner
` (3 subsequent siblings)
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
Since the endpoints are exactly identical as the PVE ones, we move the
implementation to a shared module and use the same for both.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/pbs/mod.rs | 4 ++
server/src/api/pbs/node.rs | 9 +++
server/src/api/pve/apt.rs | 119 -------------------------------
server/src/api/pve/mod.rs | 1 -
server/src/api/pve/node.rs | 2 +-
server/src/api/remote_updates.rs | 118 +++++++++++++++++++++++++++++-
6 files changed, 130 insertions(+), 123 deletions(-)
create mode 100644 server/src/api/pbs/node.rs
delete mode 100644 server/src/api/pve/apt.rs
diff --git a/server/src/api/pbs/mod.rs b/server/src/api/pbs/mod.rs
index b3bb126c..43bd1a28 100644
--- a/server/src/api/pbs/mod.rs
+++ b/server/src/api/pbs/mod.rs
@@ -20,6 +20,7 @@ use crate::{
use crate::remote_tasks;
+mod node;
mod rrddata;
pub const ROUTER: Router = Router::new()
@@ -41,8 +42,11 @@ pub const MAIN_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(REMOTE_SUBDIRS))
.subdirs(REMOTE_SUBDIRS);
+const NODES_ROUTER: Router = Router::new().match_all("node", &node::ROUTER);
+
#[sortable]
const REMOTE_SUBDIRS: SubdirMap = &sorted!([
+ ("nodes", &NODES_ROUTER),
("status", &Router::new().get(&API_METHOD_GET_STATUS)),
("rrddata", &rrddata::PBS_NODE_RRD_ROUTER),
("datastore", &DATASTORE_ROUTER)
diff --git a/server/src/api/pbs/node.rs b/server/src/api/pbs/node.rs
new file mode 100644
index 00000000..286e1a11
--- /dev/null
+++ b/server/src/api/pbs/node.rs
@@ -0,0 +1,9 @@
+use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
+use proxmox_sortable_macro::sortable;
+
+pub const ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([("apt", &crate::api::remote_updates::APT_ROUTER),]);
diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
deleted file mode 100644
index f5027fb8..00000000
--- a/server/src/api/pve/apt.rs
+++ /dev/null
@@ -1,119 +0,0 @@
-use anyhow::Error;
-
-use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
-use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
-use proxmox_schema::api;
-use proxmox_schema::api_types::NODE_SCHEMA;
-
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
-
-use crate::{api::remotes::get_remote, remote_updates};
-
-#[api(
- input: {
- properties: {
- remote: {
- schema: REMOTE_ID_SCHEMA,
- },
- node: {
- schema: NODE_SCHEMA,
- },
- },
- },
- returns: {
- description: "A list of packages with available updates.",
- type: Array,
- items: {
- type: APTUpdateInfo
- },
- },
- access: {
- permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
- },
-)]
-/// List available APT updates for a remote PVE node.
-async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
- let (config, _digest) = pdm_config::remotes::config()?;
- let remote = get_remote(&config, &remote)?;
-
- let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
-
- Ok(updates)
-}
-
-#[api(
- input: {
- properties: {
- remote: {
- schema: REMOTE_ID_SCHEMA,
- },
- node: {
- schema: NODE_SCHEMA,
- },
- },
- },
- access: {
- permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
- },
-)]
-/// Update the APT database of a remote PVE node.
-pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
- let (config, _digest) = pdm_config::remotes::config()?;
- let remote = get_remote(&config, &remote)?;
-
- let upid = remote_updates::update_apt_database(remote, &node).await?;
-
- Ok(upid)
-}
-
-#[api(
- input: {
- properties: {
- remote: {
- schema: REMOTE_ID_SCHEMA,
- },
- node: {
- schema: NODE_SCHEMA,
- },
- options: {
- type: APTGetChangelogOptions,
- flatten: true,
- },
- },
- },
- returns: {
- description: "The Package changelog.",
- type: String,
- },
- access: {
- permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
- },
-)]
-/// Retrieve the changelog of the specified package for a remote PVE node.
-async fn apt_get_changelog(
- remote: String,
- node: String,
- options: APTGetChangelogOptions,
-) -> Result<String, Error> {
- let (config, _digest) = pdm_config::remotes::config()?;
- let remote = get_remote(&config, &remote)?;
-
- remote_updates::get_changelog(remote.clone(), &node, options.name).await
-}
-
-const SUBDIRS: SubdirMap = &[
- (
- "changelog",
- &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
- ),
- (
- "update",
- &Router::new()
- .get(&API_METHOD_APT_UPDATE_AVAILABLE)
- .post(&API_METHOD_APT_UPDATE_DATABASE),
- ),
-];
-
-pub const ROUTER: Router = Router::new()
- .get(&list_subdirs_api_method!(SUBDIRS))
- .subdirs(SUBDIRS);
diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index a94ab8d0..fd4ea542 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -33,7 +33,6 @@ use crate::connection::PveClient;
use crate::connection::{self, probe_tls_connection};
use crate::remote_tasks;
-mod apt;
mod lxc;
mod node;
mod qemu;
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index 1924e252..301c0b19 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -15,7 +15,7 @@ pub const ROUTER: Router = Router::new()
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
- ("apt", &super::apt::ROUTER),
+ ("apt", &crate::api::remote_updates::APT_ROUTER),
("rrddata", &super::rrddata::NODE_RRD_ROUTER),
("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
("storage", &STORAGE_ROUTER),
diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
index 724b705a..5907259f 100644
--- a/server/src/api/remote_updates.rs
+++ b/server/src/api/remote_updates.rs
@@ -2,10 +2,13 @@
use anyhow::Error;
-use pdm_api_types::remote_updates::UpdateSummary;
use pdm_api_types::remotes::Remote;
-use pdm_api_types::{PRIV_RESOURCE_MODIFY, UPID};
+use pdm_api_types::{
+ remote_updates::UpdateSummary, remotes::REMOTE_ID_SCHEMA, RemoteUpid, NODE_SCHEMA,
+ PRIV_RESOURCE_MODIFY, UPID,
+};
use proxmox_access_control::CachedUserInfo;
+use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
use proxmox_rest_server::WorkerTask;
use proxmox_router::{
http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
@@ -15,6 +18,8 @@ use proxmox_sortable_macro::sortable;
use crate::remote_updates;
+use super::remotes::get_remote;
+
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
@@ -106,3 +111,112 @@ pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Resul
upid_str.parse()
}
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ description: "A list of packages with available updates.",
+ type: Array,
+ items: {
+ type: APTUpdateInfo
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// List available APT updates for a remote PVE node.
+async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
+
+ Ok(updates)
+}
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// Update the APT database of a remote PVE node.
+pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ let upid = remote_updates::update_apt_database(remote, &node).await?;
+
+ Ok(upid)
+}
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ options: {
+ type: APTGetChangelogOptions,
+ flatten: true,
+ },
+ },
+ },
+ returns: {
+ description: "The Package changelog.",
+ type: String,
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// Retrieve the changelog of the specified package for a remote PVE node.
+async fn apt_get_changelog(
+ remote: String,
+ node: String,
+ options: APTGetChangelogOptions,
+) -> Result<String, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ remote_updates::get_changelog(remote.clone(), &node, options.name).await
+}
+
+const APT_SUBDIRS: SubdirMap = &[
+ (
+ "changelog",
+ &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
+ ),
+ (
+ "update",
+ &Router::new()
+ .get(&API_METHOD_APT_UPDATE_AVAILABLE)
+ .post(&API_METHOD_APT_UPDATE_DATABASE),
+ ),
+];
+
+pub const APT_ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(APT_SUBDIRS))
+ .subdirs(APT_SUBDIRS);
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (9 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/12] api: add APT endpoints " Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
` (2 subsequent siblings)
13 siblings, 1 reply; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
This commit adds a new view for showing a global overview about
available updates on managed remotes. Thew view is split in the middle.
On the left side, we display a tree view showing all remotes and nodes,
on the right side we show a list of available updates for any node
selected in the tree.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/remotes/mod.rs | 3 +
ui/src/remotes/updates.rs | 531 ++++++++++++++++++++++++++++++++++++++
2 files changed, 534 insertions(+)
create mode 100644 ui/src/remotes/updates.rs
diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index 83b3331b..cce21563 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -24,6 +24,9 @@ pub use config::{create_remote, RemoteConfigPanel};
mod tasks;
pub use tasks::RemoteTaskList;
+mod updates;
+pub use updates::UpdateTree;
+
use yew::{function_component, Html};
use pwt::prelude::*;
diff --git a/ui/src/remotes/updates.rs b/ui/src/remotes/updates.rs
new file mode 100644
index 00000000..1a2e9e25
--- /dev/null
+++ b/ui/src/remotes/updates.rs
@@ -0,0 +1,531 @@
+use std::cmp::Ordering;
+use std::ops::Deref;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use futures::Future;
+use yew::virtual_dom::{Key, VComp, VNode};
+use yew::{html, Html, Properties};
+
+use pdm_api_types::remote_updates::{
+ NodeUpdateStatus, NodeUpdateSummary, RemoteUpdateStatus, UpdateSummary,
+};
+use pdm_api_types::remotes::RemoteType;
+use pwt::css::{AlignItems, FlexFit, TextAlign};
+use pwt::widget::data_table::{DataTableCellRenderArgs, DataTableCellRenderer};
+
+use proxmox_yew_comp::{
+ AptPackageManager, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+};
+use pwt::props::{CssBorderBuilder, CssMarginBuilder, CssPaddingBuilder, WidgetStyleBuilder};
+use pwt::widget::{Button, Container, Panel, Tooltip};
+use pwt::{
+ css,
+ css::FontColor,
+ props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder},
+ state::{Selection, SlabTree, TreeStore},
+ tr,
+ widget::{
+ data_table::{DataTable, DataTableColumn, DataTableHeader},
+ Column, Fa, Row,
+ },
+};
+
+use crate::{get_deep_url, get_deep_url_low_level, pdm_client};
+
+#[derive(PartialEq, Properties)]
+pub struct UpdateTree {}
+
+impl UpdateTree {
+ pub fn new() -> Self {
+ yew::props!(Self {})
+ }
+}
+
+impl From<UpdateTree> for VNode {
+ fn from(value: UpdateTree) -> Self {
+ let comp = VComp::new::<LoadableComponentMaster<UpdateTreeComponent>>(Rc::new(value), None);
+ VNode::from(comp)
+ }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct RemoteEntry {
+ remote: String,
+ ty: RemoteType,
+ number_of_failed_nodes: u32,
+ number_of_nodes: u32,
+ number_of_updatable_nodes: u32,
+ poll_status: RemoteUpdateStatus,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct NodeEntry {
+ remote: String,
+ node: String,
+ ty: RemoteType,
+ summary: NodeUpdateSummary,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+enum UpdateTreeEntry {
+ Root,
+ Remote(RemoteEntry),
+ Node(NodeEntry),
+}
+
+impl UpdateTreeEntry {
+ fn name(&self) -> &str {
+ match &self {
+ Self::Root => "",
+ Self::Remote(data) => &data.remote,
+ Self::Node(data) => &data.node,
+ }
+ }
+}
+
+impl ExtractPrimaryKey for UpdateTreeEntry {
+ fn extract_key(&self) -> yew::virtual_dom::Key {
+ Key::from(match self {
+ UpdateTreeEntry::Root => "/".to_string(),
+ UpdateTreeEntry::Remote(data) => format!("/{}", data.remote),
+ UpdateTreeEntry::Node(data) => format!("/{}/{}", data.remote, data.node),
+ })
+ }
+}
+
+pub enum RemoteUpdateTreeMsg {
+ LoadFinished(UpdateSummary),
+ KeySelected(Option<Key>),
+ RefreshAll,
+}
+
+pub struct UpdateTreeComponent {
+ store: TreeStore<UpdateTreeEntry>,
+ selection: Selection,
+ selected_entry: Option<UpdateTreeEntry>,
+}
+
+fn default_sorter(a: &UpdateTreeEntry, b: &UpdateTreeEntry) -> Ordering {
+ a.name().cmp(b.name())
+}
+
+impl UpdateTreeComponent {
+ fn columns(
+ _ctx: &LoadableComponentContext<Self>,
+ store: TreeStore<UpdateTreeEntry>,
+ ) -> Rc<Vec<DataTableHeader<UpdateTreeEntry>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Name"))
+ .tree_column(store)
+ .width("200px")
+ .render(|entry: &UpdateTreeEntry| {
+ let icon = match entry {
+ UpdateTreeEntry::Remote(_) => Some("server"),
+ UpdateTreeEntry::Node(_) => Some("building"),
+ _ => None,
+ };
+
+ Row::new()
+ .class(css::AlignItems::Baseline)
+ .gap(2)
+ .with_optional_child(icon.map(|icon| Fa::new(icon)))
+ .with_child(entry.name())
+ .into()
+ })
+ .sorter(default_sorter)
+ .into(),
+ DataTableColumn::new(tr!("Status"))
+ .render_cell(DataTableCellRenderer::new(
+ move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
+ let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+ match args.record() {
+ UpdateTreeEntry::Remote(remote_info) => match remote_info.poll_status {
+ RemoteUpdateStatus::Unknown => {
+ row = row.with_child(render_remote_status_icon(
+ RemoteUpdateStatus::Unknown,
+ ));
+ }
+ RemoteUpdateStatus::Success => {
+ if !args.is_expanded() {
+ let up_to_date_nodes = remote_info.number_of_nodes
+ - remote_info.number_of_updatable_nodes
+ - remote_info.number_of_failed_nodes;
+
+ if up_to_date_nodes > 0 {
+ row = row.with_child(render_remote_summary_counter(
+ up_to_date_nodes,
+ RemoteSummaryIcon::UpToDate,
+ ));
+ }
+
+ if remote_info.number_of_updatable_nodes > 0 {
+ row = row.with_child(render_remote_summary_counter(
+ remote_info.number_of_updatable_nodes,
+ RemoteSummaryIcon::Updatable,
+ ));
+ }
+
+ if remote_info.number_of_failed_nodes > 0 {
+ row = row.with_child(render_remote_summary_counter(
+ remote_info.number_of_failed_nodes,
+ RemoteSummaryIcon::Error,
+ ));
+ }
+ }
+ }
+ RemoteUpdateStatus::Error => {
+ row = row.with_child(render_remote_status_icon(
+ RemoteUpdateStatus::Error,
+ ));
+ }
+ },
+ UpdateTreeEntry::Node(info) => {
+ if info.summary.status == NodeUpdateStatus::Error {
+ row = row.with_child(
+ Fa::new("times-circle").class(FontColor::Error),
+ );
+ row = row.with_child(tr!("Could not get update info"));
+ } else if info.summary.number_of_updates > 0 {
+ row = row
+ .with_child(Fa::new("refresh").class(FontColor::Primary));
+ row = row.with_child(tr!(
+ "{0} updates are available",
+ info.summary.number_of_updates
+ ));
+ } else {
+ row =
+ row.with_child(Fa::new("check").class(FontColor::Success));
+ row = row.with_child(tr!("Up-to-date"));
+ }
+ }
+ _ => {}
+ }
+
+ row.into()
+ },
+ ))
+ .into(),
+ ])
+ }
+}
+
+fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTreeEntry> {
+ let mut tree = SlabTree::new();
+
+ let mut root = tree.set_root(UpdateTreeEntry::Root);
+ root.set_expanded(true);
+
+ for (remote_name, remote_summary) in update_summary.remotes.deref() {
+ let mut remote_entry = root.append(UpdateTreeEntry::Remote(RemoteEntry {
+ remote: remote_name.clone(),
+ ty: remote_summary.remote_type,
+ number_of_nodes: 0,
+ number_of_updatable_nodes: 0,
+ number_of_failed_nodes: 0,
+ poll_status: remote_summary.status.clone(),
+ }));
+ remote_entry.set_expanded(false);
+
+ let number_of_nodes = remote_summary.nodes.len();
+ let mut number_of_updatable_nodes = 0;
+ let mut number_of_failed_nodes = 0;
+
+ for (node_name, node_summary) in remote_summary.nodes.deref() {
+ match node_summary.status {
+ NodeUpdateStatus::Success => {
+ if node_summary.number_of_updates > 0 {
+ number_of_updatable_nodes += 1;
+ }
+ }
+ NodeUpdateStatus::Error => {
+ number_of_failed_nodes += 1;
+ }
+ }
+
+ remote_entry.append(UpdateTreeEntry::Node(NodeEntry {
+ remote: remote_name.clone(),
+ node: node_name.clone(),
+ ty: remote_summary.remote_type,
+ summary: node_summary.clone(),
+ }));
+ }
+
+ if let UpdateTreeEntry::Remote(info) = remote_entry.record_mut() {
+ info.number_of_updatable_nodes = number_of_updatable_nodes;
+ info.number_of_nodes = number_of_nodes as u32;
+ info.number_of_failed_nodes = number_of_failed_nodes as u32;
+ }
+ }
+
+ tree
+}
+
+impl LoadableComponent for UpdateTreeComponent {
+ type Properties = UpdateTree;
+ type Message = RemoteUpdateTreeMsg;
+ type ViewState = ();
+
+ fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+ let link = ctx.link();
+
+ let store = TreeStore::new().view_root(false);
+ store.set_sorter(default_sorter);
+
+ link.repeated_load(5000);
+
+ let selection = Selection::new().on_select(link.callback(|selection: Selection| {
+ RemoteUpdateTreeMsg::KeySelected(selection.selected_key())
+ }));
+
+ Self {
+ store: store.clone(),
+ selection,
+ selected_entry: None,
+ }
+ }
+
+ fn load(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+ let link = ctx.link().clone();
+
+ Box::pin(async move {
+ let client = pdm_client();
+
+ let updates = client.remote_update_summary().await?;
+ link.send_message(Self::Message::LoadFinished(updates));
+
+ Ok(())
+ })
+ }
+
+ fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Self::Message::LoadFinished(updates) => {
+ let data = build_store_from_response(updates);
+ self.store.write().update_root_tree(data);
+ self.store.set_sorter(default_sorter);
+
+ return true;
+ }
+ Self::Message::KeySelected(key) => {
+ if let Some(key) = key {
+ let read_guard = self.store.read();
+ let node_ref = read_guard.lookup_node(&key).unwrap();
+ let record = node_ref.record();
+
+ self.selected_entry = Some(record.clone());
+
+ return true;
+ }
+ }
+ Self::Message::RefreshAll => {
+ let link = ctx.link();
+
+ link.clone().spawn(async move {
+ let client = pdm_client();
+
+ match client.refresh_remote_update_summary().await {
+ Ok(upid) => {
+ link.show_task_progres(upid.to_string());
+ }
+ Err(err) => {
+ link.show_error(tr!("Could not refresh update status."), err, false);
+ }
+ }
+ });
+ }
+ }
+
+ false
+ }
+
+ fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> yew::Html {
+ Container::new()
+ .class("pwt-content-spacer")
+ .class(FlexFit)
+ .class("pwt-flex-direction-row")
+ .with_child(self.render_update_tree_panel(ctx))
+ .with_child(self.render_update_list_panel(ctx))
+ .into()
+ }
+}
+
+impl UpdateTreeComponent {
+ fn render_update_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
+ let table = DataTable::new(Self::columns(ctx, self.store.clone()), self.store.clone())
+ .selection(self.selection.clone())
+ .striped(false)
+ .borderless(true)
+ .show_header(false)
+ .class(css::FlexFit);
+
+ let refresh_all_button = Button::new(tr!("Refresh all")).on_activate({
+ let link = ctx.link().clone();
+ move |_| {
+ link.send_message(RemoteUpdateTreeMsg::RefreshAll);
+ }
+ });
+
+ let title: Html = Row::new()
+ .gap(2)
+ .class(AlignItems::Baseline)
+ .with_child(Fa::new("refresh"))
+ .with_child(tr!("Remote System Updates"))
+ .into();
+
+ Panel::new()
+ .min_width(500)
+ .title(title)
+ .with_tool(refresh_all_button)
+ .style("flex", "1 1 0")
+ .class(FlexFit)
+ .border(true)
+ .with_child(table)
+ }
+
+ fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
+ let title: Html = Row::new()
+ .gap(2)
+ .class(AlignItems::Baseline)
+ .with_child(Fa::new("list"))
+ .with_child(tr!("Update List"))
+ .into();
+
+ match &self.selected_entry {
+ // (Some(remote), Some(remote_type), Some(node)) => {
+ Some(UpdateTreeEntry::Node(NodeEntry {
+ remote, node, ty, ..
+ })) => {
+ let base_url = format!("/{ty}/remotes/{remote}/nodes/{node}/apt",);
+ let task_base_url = format!("/{ty}/remotes/{}/tasks", remote);
+
+ let apt = AptPackageManager::new()
+ .base_url(base_url)
+ .task_base_url(task_base_url)
+ .enable_upgrade(true)
+ .on_upgrade({
+ let remote = remote.clone();
+ let link = ctx.link().clone();
+ let remote = remote.clone();
+ let node = node.clone();
+ let ty = *ty;
+
+ move |_| match ty {
+ RemoteType::Pve => {
+ let id = format!("node/{}::apt", node);
+ if let Some(url) =
+ get_deep_url(&link.yew_link(), &remote, None, &id)
+ {
+ let _ = web_sys::window().unwrap().open_with_url(&url.href());
+ }
+ }
+ RemoteType::Pbs => {
+ let hash = "#pbsServerAdministration:updates";
+ if let Some(url) =
+ get_deep_url_low_level(&link.yew_link(), &remote, None, &hash)
+ {
+ let _ = web_sys::window().unwrap().open_with_url(&url.href());
+ }
+ }
+ }
+ });
+
+ Panel::new()
+ .class(FlexFit)
+ .title(title)
+ .border(true)
+ .min_width(500)
+ .with_child(apt)
+ .style("flex", "1 1 0")
+ }
+ _ => {
+ let header = tr!("No node selected");
+ let msg = tr!("Select a node to show available updates.");
+
+ let select_node_msg = Column::new()
+ .class(FlexFit)
+ .padding(2)
+ .class(AlignItems::Center)
+ .class(TextAlign::Center)
+ .with_child(html! {<h1 class="pwt-font-headline-medium">{header}</h1>})
+ .with_child(Container::new().with_child(msg));
+
+ Panel::new()
+ .class(FlexFit)
+ .title(title)
+ .border(true)
+ .min_width(500)
+ .with_child(select_node_msg)
+ .style("flex", "1 1 0")
+ }
+ }
+ }
+}
+
+enum RemoteSummaryIcon {
+ UpToDate,
+ Updatable,
+ Error,
+}
+
+fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
+ let (icon_class, icon_scheme, state_text) = match task_class {
+ RemoteSummaryIcon::UpToDate => (
+ "check",
+ FontColor::Success,
+ tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
+ ),
+ RemoteSummaryIcon::Error => (
+ "times-circle",
+ FontColor::Error,
+ tr!("Failed to retrieve update info for one node."
+ | "Failed to retrieve update info for {n} nodes." % count),
+ ),
+ RemoteSummaryIcon::Updatable => (
+ "refresh",
+ FontColor::Primary,
+ tr!("One node has updates available." | "{n} nodes have updates available." % count),
+ ),
+ };
+
+ let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
+
+ Tooltip::new(
+ Container::from_tag("span")
+ .with_child(icon)
+ .with_child(count)
+ .margin_end(5),
+ )
+ .tip(state_text)
+ .into()
+}
+
+fn render_remote_status_icon(task_class: RemoteUpdateStatus) -> Html {
+ let (icon_class, icon_scheme, state_text) = match task_class {
+ RemoteUpdateStatus::Success => (
+ "check",
+ FontColor::Success,
+ tr!("All nodes of this remote are up-to-date."),
+ ),
+ RemoteUpdateStatus::Error => (
+ "times-circle",
+ FontColor::Error,
+ tr!("Could not retrieve update info for remote."),
+ ),
+ RemoteUpdateStatus::Unknown => (
+ "question-circle-o",
+ FontColor::Primary,
+ tr!("The update status is not known."),
+ ),
+ };
+
+ let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
+
+ Tooltip::new(Container::from_tag("span").with_child(icon).margin_end(5))
+ .tip(state_text)
+ .into()
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 12/12] ui: show new remote update view in the 'Remotes' section
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (10 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view Lukas Wagner
@ 2025-10-15 12:47 ` Lukas Wagner
2025-10-17 10:15 ` [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Shannon Sterz
2025-10-17 12:14 ` [pdm-devel] superseded: " Lukas Wagner
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-15 12:47 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/remotes/mod.rs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index cce21563..5e06b2ce 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -56,6 +56,13 @@ pub fn system_configuration() -> Html {
.label(tr!("Tasks"))
.icon_class("fa fa-book"),
|_| RemoteTaskList::new().into(),
+ )
+ .with_item_builder(
+ TabBarItem::new()
+ .key("updates")
+ .label(tr!("Updates"))
+ .icon_class("fa fa-refresh"),
+ |_| UpdateTree::new().into(),
);
NavigationContainer::new().with_child(panel).into()
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
@ 2025-10-17 7:44 ` Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
1 sibling, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-17 7:44 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel
On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
> +#[api(
> + access: {
> + permission: &Permission::Anybody,
> + description: "Resource.Modify privileges are needed on /resource/{remote}",
> + },
> +)]
> +/// Return available update summary for managed remote nodes.
> +pub fn update_summary(rpcenv: &mut dyn RpcEnvironment) -> Result<UpdateSummary, Error> {
> + let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
> + let user_info = CachedUserInfo::new()?;
> +
> + if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
> + http_bail!(UNAUTHORIZED, "user has no access to resources");
Just read the discussion regarding the usage of FORBIDDEN vs
UNAUTHORIZED - will change this to FORBIDDEN in a v2 (after any other
review feedback, just so to avoid noise on the list)
> + }
> +
> + let mut update_summary = remote_updates::get_available_updates_summary()?;
> +
> + update_summary.remotes.retain(|remote_name, _| {
> + user_info
> + .check_privs(
> + &auth_id,
> + &["resource", remote_name],
> + PRIV_RESOURCE_MODIFY,
> + false,
> + )
> + .is_ok()
> + });
> +
> + Ok(update_summary)
> +}
> +
> +#[api(
> + access: {
> + permission: &Permission::Anybody,
> + description: "Resource.Modify privileges are needed on /resource/{remote}",
> + },
> +)]
> +/// Refresh the update summary of all remotes.
> +pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Result<UPID, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> +
> + let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
> + let user_info = CachedUserInfo::new()?;
> +
> + if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
> + http_bail!(UNAUTHORIZED, "user has no access to resources");
Same here.
> + }
> +
> + let remotes: Vec<Remote> = config
> + .into_iter()
> + .filter_map(|(remote_name, remote)| {
> + user_info
> + .check_privs(
> + &auth_id,
> + &["resource", &remote_name],
> + PRIV_RESOURCE_MODIFY,
> + false,
> + )
> + .is_ok()
> + .then_some(remote)
> + })
> + .collect();
> +
> + let upid_str = WorkerTask::spawn(
> + "refresh-remote-updates",
> + None,
> + auth_id.to_string(),
> + true,
> + |_worker| async {
> + // TODO: Add more verbose logging per remote/node, so we can actually see something
> + // interesting in the task log.
> + remote_updates::refresh_update_summary_cache(remotes).await?;
> + Ok(())
> + },
> + )?;
> +
> + upid_str.parse()
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (11 preceding siblings ...)
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
@ 2025-10-17 10:15 ` Shannon Sterz
2025-10-17 12:14 ` [pdm-devel] superseded: " Lukas Wagner
13 siblings, 0 replies; 22+ messages in thread
From: Shannon Sterz @ 2025-10-17 10:15 UTC (permalink / raw)
To: Lukas Wagner; +Cc: Proxmox Datacenter Manager development discussion
On Wed Oct 15, 2025 at 2:46 PM CEST, Lukas Wagner wrote:
> This series adds a new tab under "Remotes" called "Updates". It provides a
> summary regarding the system update availability for all managed remotes.
>
> Potential follow-up work:
> - The "Refresh all" button, powered by the '/remote-updates/refresh' API
> only retrieves a fresh list of available updates at the moment, but does not
> invoke 'apt update' on the remote. The latter could be useful, either
> always or if explicitly requested, but we probably should 'stream'
> the node's task log to the PDM task log, so one can see the actual
> progress and/or any problems.
>
> - Remote task cache / task tracking needs a bit of work to correctly handle
> PBS tasks, therefore the 'Update' (which *does* invoke 'apt update' on the
> remote node) button for a *SINGLE* node does not work yet for PBS.
>
>
> proxmox-datacenter-manager:
>
> Lukas Wagner (12):
> metric collection task: tests: add missing parameter for
> cluster_metric_export
> pdm-api-types: add types for remote upgrade summary
> remote updates: add cache for remote update availability
> api: add API for retrieving/refreshing the remote update summary
> unprivileged api daemon: tasks: add remote update refresh task
> pdm-client: add API methods for remote update summaries
> pbs-client: add bindings for APT-related API calls
> task cache: use separate functions for tracking PVE and PBS tasks
> remote updates: add support for PBS remotes
> api: add APT endpoints for PBS remotes
> ui: add remote update view
> ui: show new remote update view in the 'Remotes' section
>
> lib/pdm-api-types/src/lib.rs | 2 +
> lib/pdm-api-types/src/remote_updates.rs | 126 +++++
> lib/pdm-client/src/lib.rs | 22 +
> server/src/api/mod.rs | 3 +
> server/src/api/pbs/mod.rs | 19 +-
> server/src/api/pbs/node.rs | 9 +
> server/src/api/pve/apt.rs | 119 ----
> server/src/api/pve/mod.rs | 4 +-
> server/src/api/pve/node.rs | 2 +-
> server/src/api/remote_updates.rs | 222 ++++++++
> server/src/bin/proxmox-datacenter-api/main.rs | 1 +
> .../bin/proxmox-datacenter-api/tasks/mod.rs | 1 +
> .../tasks/remote_updates.rs | 44 ++
> .../src/metric_collection/collection_task.rs | 1 +
> server/src/pbs_client.rs | 51 ++
> server/src/remote_tasks/mod.rs | 45 +-
> server/src/remote_updates.rs | 229 +++++++-
> ui/src/remotes/mod.rs | 10 +
> ui/src/remotes/updates.rs | 531 ++++++++++++++++++
> 19 files changed, 1299 insertions(+), 142 deletions(-)
> create mode 100644 lib/pdm-api-types/src/remote_updates.rs
> create mode 100644 server/src/api/pbs/node.rs
> delete mode 100644 server/src/api/pve/apt.rs
> create mode 100644 server/src/api/remote_updates.rs
> create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
> create mode 100644 ui/src/remotes/updates.rs
>
>
> Summary over all repositories:
> 19 files changed, 1299 insertions(+), 142 deletions(-)
had a couple of minor improvements for some patches, but nothing that
blocks this in my opinion (most are simple follow-ups for the ui). since
you already pointed out the usage of UNAUTHORIZED instead of FORBIDDEN
yourself, i didn't send my comments on that here again. so consider
this:
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
@ 2025-10-17 10:15 ` Shannon Sterz
2025-10-17 11:12 ` Lukas Wagner
2025-10-17 11:52 ` Lukas Wagner
0 siblings, 2 replies; 22+ messages in thread
From: Shannon Sterz @ 2025-10-17 10:15 UTC (permalink / raw)
To: Lukas Wagner; +Cc: Proxmox Datacenter Manager development discussion
On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
> This type contains the number of available updates per remote node,
> without any details. This is useful for a global "give me the update
> availability for all managed nodes" API endpoint.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> lib/pdm-api-types/src/lib.rs | 2 +
> lib/pdm-api-types/src/remote_updates.rs | 126 ++++++++++++++++++++++++
> 2 files changed, 128 insertions(+)
> create mode 100644 lib/pdm-api-types/src/remote_updates.rs
>
> diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
> index a3566142..2fb61ef7 100644
> --- a/lib/pdm-api-types/src/lib.rs
> +++ b/lib/pdm-api-types/src/lib.rs
> @@ -96,6 +96,8 @@ pub use openid::*;
>
> pub mod remotes;
>
> +pub mod remote_updates;
> +
> pub mod resource;
>
> pub mod rrddata;
> diff --git a/lib/pdm-api-types/src/remote_updates.rs b/lib/pdm-api-types/src/remote_updates.rs
> new file mode 100644
> index 00000000..d04a7a79
> --- /dev/null
> +++ b/lib/pdm-api-types/src/remote_updates.rs
> @@ -0,0 +1,126 @@
> +use std::{
> + collections::HashMap,
> + ops::{Deref, DerefMut},
> +};
> +
nit: i think we prefer module level imports these days. so this should
be
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
but no hard feelings from my side and certainly not a blocker
> +use proxmox_schema::{api, ApiType, ObjectSchema};
> +use serde::{Deserialize, Serialize};
> +
nit: ordering of use statements should be std -> generic crates (serde,
anyhow etc.) -> proxmox* -> project; each separate by an empty line, to
my knowledge.
so:
```
use serde::{Deserialize, Serialize};
use proxmox_schema::{api, ApiType, ObjectSchema};
```
again not a
blocker, just saw it and thought id mention it
> +use crate::remotes::RemoteType;
> +
> +#[api]
> +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
> +#[serde(rename_all = "kebab-case")]
> +/// Update summary for all remotes.
> +pub struct UpdateSummary {
> + /// Map containing the update summary each remote.
> + pub remotes: RemoteUpdateSummaryWrapper,
> +}
> +
> +// This is a hack to allow actual 'maps' (mapping remote name to per-remote data)
> +// within the realms of our API macro.
> +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
> +#[serde(rename_all = "kebab-case")]
> +pub struct RemoteUpdateSummaryWrapper {
> + #[serde(flatten)]
> + remotes: HashMap<String, RemoteUpdateSummary>,
> +}
just wondering whether you have considered changing this to
pub struct RemoteUpdateSummaryWrapper(HashMap<String, RemoteUpdateSummary>);
would save you a `flatten` from what i can tell?
> +impl ApiType for RemoteUpdateSummaryWrapper {
> + const API_SCHEMA: proxmox_schema::Schema =
> + ObjectSchema::new("Map of per-remote update summaries", &[])
> + .additional_properties(true)
> + .schema();
> +}
> +
> +impl Deref for RemoteUpdateSummaryWrapper {
> + type Target = HashMap<String, RemoteUpdateSummary>;
> +
> + fn deref(&self) -> &Self::Target {
> + &self.remotes
> + }
> +}
> +
> +impl DerefMut for RemoteUpdateSummaryWrapper {
> + fn deref_mut(&mut self) -> &mut Self::Target {
> + &mut self.remotes
> + }
> +}
> +
> +#[api]
> +#[derive(Clone, Debug, Deserialize, Serialize)]
> +#[serde(rename_all = "kebab-case")]
> +/// Update summary for a single remote.
> +pub struct RemoteUpdateSummary {
> + /// Map containing the update summary for each node of this remote.
> + pub nodes: NodeUpdateSummaryWrapper,
> + pub remote_type: RemoteType,
> + pub status: RemoteUpdateStatus,
> +}
> +
> +#[api]
> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Status for the entire remote.
> +pub enum RemoteUpdateStatus {
> + /// Successfully polled remote.
> + Success,
> + /// Remote could not be polled.
> + Error,
> + /// Remote has not been polled yet.
> + Unknown,
> +}
> +
> +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
> +#[serde(rename_all = "kebab-case")]
> +pub struct NodeUpdateSummaryWrapper {
> + #[serde(flatten)]
> + nodes: HashMap<String, NodeUpdateSummary>,
> +}
> +
> +impl ApiType for NodeUpdateSummaryWrapper {
> + const API_SCHEMA: proxmox_schema::Schema =
> + ObjectSchema::new("Map of per-node update summaries", &[])
> + .additional_properties(true)
> + .schema();
> +}
> +
> +impl Deref for NodeUpdateSummaryWrapper {
> + type Target = HashMap<String, NodeUpdateSummary>;
> +
> + fn deref(&self) -> &Self::Target {
> + &self.nodes
> + }
> +}
> +
> +impl DerefMut for NodeUpdateSummaryWrapper {
> + fn deref_mut(&mut self) -> &mut Self::Target {
> + &mut self.nodes
> + }
> +}
> +
> +#[api]
> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Status for the entire remote.
> +pub enum NodeUpdateStatus {
> + /// Successfully polled node.
> + Success,
> + /// Node could not be polled.
> + Error,
> +}
> +
> +#[api]
> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Per-node update summary.
> +pub struct NodeUpdateSummary {
> + /// Number of available updates.
> + pub number_of_updates: u32,
> + /// Unix timestamp of the last refresh.
> + pub last_refresh: i64,
> + /// Status
> + pub status: NodeUpdateStatus,
> + /// Status message (e.g. error message)
> + pub status_message: Option<String>,
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
2025-10-17 7:44 ` Lukas Wagner
@ 2025-10-17 10:15 ` Shannon Sterz
2025-10-17 11:00 ` Lukas Wagner
1 sibling, 1 reply; 22+ messages in thread
From: Shannon Sterz @ 2025-10-17 10:15 UTC (permalink / raw)
To: Lukas Wagner; +Cc: Proxmox Datacenter Manager development discussion
On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
> This commit adds two new endpoints, namely
> GET /remote-updates/summary
> POST /remote-updates/refresh
>
> The first one is used to retrieve the update summary (the data is taken
> from the cache), the second one can be used to proactively refresh the
> summary in the cache (starts a worker task, since this could take a
> while). Note that we only retrieve the up-to-date list of packages from
> the remote, but do *not* trigger an `apt update` right now. Could make
> sense to do the latter as well, but then we probably should
> stream/forward the task logs for the upgrade task from the node to the
maybe i'm misunderstanding, but do you mean "update task" here? since
you talk about triggering an `apt update` before. triggering an actual
upgrade here seems a little risky and probably needs extra safe-guards?
> native PDM task; something we can rather implement later.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> server/src/api/mod.rs | 3 +
> server/src/api/remote_updates.rs | 108 +++++++++++++++++++++++++++++++
> 2 files changed, 111 insertions(+)
> create mode 100644 server/src/api/remote_updates.rs
>
> diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
> index 02ee0ecf..6a7a65a2 100644
> --- a/server/src/api/mod.rs
> +++ b/server/src/api/mod.rs
> @@ -14,6 +14,7 @@ pub mod nodes;
> pub mod pbs;
> pub mod pve;
> pub mod remote_tasks;
> +pub mod remote_updates;
> pub mod remotes;
> pub mod resources;
> mod rrd_common;
> @@ -31,6 +32,8 @@ const SUBDIRS: SubdirMap = &sorted!([
> ("resources", &resources::ROUTER),
> ("nodes", &nodes::ROUTER),
> ("remote-tasks", &remote_tasks::ROUTER),
> + // TODO: There might be a better place for this endpoint.
> + ("remote-updates", &remote_updates::ROUTER),
> ("sdn", &sdn::ROUTER),
> ("version", &Router::new().get(&API_METHOD_VERSION)),
> ]);
> diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
> new file mode 100644
> index 00000000..724b705a
> --- /dev/null
> +++ b/server/src/api/remote_updates.rs
> @@ -0,0 +1,108 @@
> +//! API for getting a remote update update summary.
> +
> +use anyhow::Error;
> +
> +use pdm_api_types::remote_updates::UpdateSummary;
> +use pdm_api_types::remotes::Remote;
> +use pdm_api_types::{PRIV_RESOURCE_MODIFY, UPID};
> +use proxmox_access_control::CachedUserInfo;
> +use proxmox_rest_server::WorkerTask;
> +use proxmox_router::{
> + http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
> +};
> +use proxmox_schema::api;
> +use proxmox_sortable_macro::sortable;
> +
> +use crate::remote_updates;
> +
> +pub const ROUTER: Router = Router::new()
> + .get(&list_subdirs_api_method!(SUBDIRS))
> + .subdirs(SUBDIRS);
> +
> +#[sortable]
> +const SUBDIRS: SubdirMap = &sorted!([
> + ("summary", &Router::new().get(&API_METHOD_UPDATE_SUMMARY)),
> + (
> + "refresh",
> + &Router::new().post(&API_METHOD_REFRESH_REMOTE_UPDATE_SUMMARIES)
> + ),
> +]);
> +
> +#[api(
> + access: {
> + permission: &Permission::Anybody,
> + description: "Resource.Modify privileges are needed on /resource/{remote}",
> + },
> +)]
> +/// Return available update summary for managed remote nodes.
> +pub fn update_summary(rpcenv: &mut dyn RpcEnvironment) -> Result<UpdateSummary, Error> {
> + let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
> + let user_info = CachedUserInfo::new()?;
> +
> + if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
> + http_bail!(UNAUTHORIZED, "user has no access to resources");
> + }
> +
> + let mut update_summary = remote_updates::get_available_updates_summary()?;
> +
> + update_summary.remotes.retain(|remote_name, _| {
> + user_info
> + .check_privs(
> + &auth_id,
> + &["resource", remote_name],
> + PRIV_RESOURCE_MODIFY,
> + false,
> + )
> + .is_ok()
> + });
> +
> + Ok(update_summary)
> +}
> +
> +#[api(
> + access: {
> + permission: &Permission::Anybody,
> + description: "Resource.Modify privileges are needed on /resource/{remote}",
> + },
> +)]
> +/// Refresh the update summary of all remotes.
> +pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Result<UPID, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> +
> + let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
> + let user_info = CachedUserInfo::new()?;
> +
> + if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
> + http_bail!(UNAUTHORIZED, "user has no access to resources");
> + }
> +
> + let remotes: Vec<Remote> = config
> + .into_iter()
> + .filter_map(|(remote_name, remote)| {
> + user_info
> + .check_privs(
> + &auth_id,
> + &["resource", &remote_name],
> + PRIV_RESOURCE_MODIFY,
> + false,
> + )
> + .is_ok()
> + .then_some(remote)
> + })
> + .collect();
> +
> + let upid_str = WorkerTask::spawn(
> + "refresh-remote-updates",
> + None,
> + auth_id.to_string(),
> + true,
> + |_worker| async {
> + // TODO: Add more verbose logging per remote/node, so we can actually see something
> + // interesting in the task log.
> + remote_updates::refresh_update_summary_cache(remotes).await?;
> + Ok(())
> + },
> + )?;
> +
> + upid_str.parse()
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view Lukas Wagner
@ 2025-10-17 10:15 ` Shannon Sterz
0 siblings, 0 replies; 22+ messages in thread
From: Shannon Sterz @ 2025-10-17 10:15 UTC (permalink / raw)
To: Lukas Wagner; +Cc: Proxmox Datacenter Manager development discussion
On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
> This commit adds a new view for showing a global overview about
> available updates on managed remotes. Thew view is split in the middle.
nit: typo here, should be "The" not "Thew"
> On the left side, we display a tree view showing all remotes and nodes,
> on the right side we show a list of available updates for any node
> selected in the tree.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> ui/src/remotes/mod.rs | 3 +
> ui/src/remotes/updates.rs | 531 ++++++++++++++++++++++++++++++++++++++
> 2 files changed, 534 insertions(+)
> create mode 100644 ui/src/remotes/updates.rs
>
> diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
> index 83b3331b..cce21563 100644
> --- a/ui/src/remotes/mod.rs
> +++ b/ui/src/remotes/mod.rs
> @@ -24,6 +24,9 @@ pub use config::{create_remote, RemoteConfigPanel};
> mod tasks;
> pub use tasks::RemoteTaskList;
>
> +mod updates;
> +pub use updates::UpdateTree;
> +
> use yew::{function_component, Html};
>
> use pwt::prelude::*;
> diff --git a/ui/src/remotes/updates.rs b/ui/src/remotes/updates.rs
> new file mode 100644
> index 00000000..1a2e9e25
> --- /dev/null
> +++ b/ui/src/remotes/updates.rs
> @@ -0,0 +1,531 @@
> +use std::cmp::Ordering;
> +use std::ops::Deref;
> +use std::pin::Pin;
> +use std::rc::Rc;
> +
> +use futures::Future;
> +use yew::virtual_dom::{Key, VComp, VNode};
> +use yew::{html, Html, Properties};
> +
> +use pdm_api_types::remote_updates::{
> + NodeUpdateStatus, NodeUpdateSummary, RemoteUpdateStatus, UpdateSummary,
> +};
> +use pdm_api_types::remotes::RemoteType;
> +use pwt::css::{AlignItems, FlexFit, TextAlign};
> +use pwt::widget::data_table::{DataTableCellRenderArgs, DataTableCellRenderer};
> +
> +use proxmox_yew_comp::{
> + AptPackageManager, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
> +};
> +use pwt::props::{CssBorderBuilder, CssMarginBuilder, CssPaddingBuilder, WidgetStyleBuilder};
> +use pwt::widget::{Button, Container, Panel, Tooltip};
> +use pwt::{
> + css,
> + css::FontColor,
> + props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder},
> + state::{Selection, SlabTree, TreeStore},
> + tr,
> + widget::{
> + data_table::{DataTable, DataTableColumn, DataTableHeader},
> + Column, Fa, Row,
> + },
> +};
> +
> +use crate::{get_deep_url, get_deep_url_low_level, pdm_client};
> +
> +#[derive(PartialEq, Properties)]
> +pub struct UpdateTree {}
> +
> +impl UpdateTree {
> + pub fn new() -> Self {
> + yew::props!(Self {})
> + }
> +}
> +
> +impl From<UpdateTree> for VNode {
> + fn from(value: UpdateTree) -> Self {
> + let comp = VComp::new::<LoadableComponentMaster<UpdateTreeComponent>>(Rc::new(value), None);
> + VNode::from(comp)
> + }
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +struct RemoteEntry {
> + remote: String,
> + ty: RemoteType,
> + number_of_failed_nodes: u32,
> + number_of_nodes: u32,
> + number_of_updatable_nodes: u32,
> + poll_status: RemoteUpdateStatus,
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +struct NodeEntry {
> + remote: String,
> + node: String,
> + ty: RemoteType,
> + summary: NodeUpdateSummary,
> +}
> +
> +#[derive(Clone, PartialEq, Debug)]
> +enum UpdateTreeEntry {
> + Root,
> + Remote(RemoteEntry),
> + Node(NodeEntry),
> +}
> +
> +impl UpdateTreeEntry {
> + fn name(&self) -> &str {
> + match &self {
> + Self::Root => "",
> + Self::Remote(data) => &data.remote,
> + Self::Node(data) => &data.node,
> + }
> + }
> +}
> +
> +impl ExtractPrimaryKey for UpdateTreeEntry {
> + fn extract_key(&self) -> yew::virtual_dom::Key {
> + Key::from(match self {
> + UpdateTreeEntry::Root => "/".to_string(),
> + UpdateTreeEntry::Remote(data) => format!("/{}", data.remote),
> + UpdateTreeEntry::Node(data) => format!("/{}/{}", data.remote, data.node),
> + })
> + }
> +}
> +
> +pub enum RemoteUpdateTreeMsg {
> + LoadFinished(UpdateSummary),
> + KeySelected(Option<Key>),
> + RefreshAll,
> +}
i know this is pre-existing and also a theme throughout the yew stuff,
but i'd call making message enums public an anti-pattern. there isn't
really a point to having them outside of the component where they
matter. imo, the `pub` here should be dropped.
> +
> +pub struct UpdateTreeComponent {
> + store: TreeStore<UpdateTreeEntry>,
> + selection: Selection,
> + selected_entry: Option<UpdateTreeEntry>,
> +}
> +
> +fn default_sorter(a: &UpdateTreeEntry, b: &UpdateTreeEntry) -> Ordering {
> + a.name().cmp(b.name())
> +}
> +
> +impl UpdateTreeComponent {
> + fn columns(
> + _ctx: &LoadableComponentContext<Self>,
> + store: TreeStore<UpdateTreeEntry>,
> + ) -> Rc<Vec<DataTableHeader<UpdateTreeEntry>>> {
> + Rc::new(vec![
> + DataTableColumn::new(tr!("Name"))
> + .tree_column(store)
> + .width("200px")
> + .render(|entry: &UpdateTreeEntry| {
> + let icon = match entry {
> + UpdateTreeEntry::Remote(_) => Some("server"),
> + UpdateTreeEntry::Node(_) => Some("building"),
> + _ => None,
> + };
> +
> + Row::new()
> + .class(css::AlignItems::Baseline)
> + .gap(2)
> + .with_optional_child(icon.map(|icon| Fa::new(icon)))
> + .with_child(entry.name())
> + .into()
> + })
> + .sorter(default_sorter)
> + .into(),
> + DataTableColumn::new(tr!("Status"))
> + .render_cell(DataTableCellRenderer::new(
> + move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
> + let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
> +
> + match args.record() {
> + UpdateTreeEntry::Remote(remote_info) => match remote_info.poll_status {
> + RemoteUpdateStatus::Unknown => {
> + row = row.with_child(render_remote_status_icon(
> + RemoteUpdateStatus::Unknown,
> + ));
> + }
> + RemoteUpdateStatus::Success => {
> + if !args.is_expanded() {
> + let up_to_date_nodes = remote_info.number_of_nodes
> + - remote_info.number_of_updatable_nodes
> + - remote_info.number_of_failed_nodes;
> +
> + if up_to_date_nodes > 0 {
> + row = row.with_child(render_remote_summary_counter(
> + up_to_date_nodes,
> + RemoteSummaryIcon::UpToDate,
> + ));
> + }
> +
> + if remote_info.number_of_updatable_nodes > 0 {
> + row = row.with_child(render_remote_summary_counter(
> + remote_info.number_of_updatable_nodes,
> + RemoteSummaryIcon::Updatable,
> + ));
> + }
> +
> + if remote_info.number_of_failed_nodes > 0 {
> + row = row.with_child(render_remote_summary_counter(
> + remote_info.number_of_failed_nodes,
> + RemoteSummaryIcon::Error,
> + ));
> + }
> + }
> + }
> + RemoteUpdateStatus::Error => {
> + row = row.with_child(render_remote_status_icon(
> + RemoteUpdateStatus::Error,
> + ));
> + }
> + },
> + UpdateTreeEntry::Node(info) => {
> + if info.summary.status == NodeUpdateStatus::Error {
> + row = row.with_child(
> + Fa::new("times-circle").class(FontColor::Error),
> + );
> + row = row.with_child(tr!("Could not get update info"));
> + } else if info.summary.number_of_updates > 0 {
> + row = row
> + .with_child(Fa::new("refresh").class(FontColor::Primary));
> + row = row.with_child(tr!(
> + "{0} updates are available",
> + info.summary.number_of_updates
> + ));
> + } else {
> + row =
> + row.with_child(Fa::new("check").class(FontColor::Success));
> + row = row.with_child(tr!("Up-to-date"));
> + }
> + }
> + _ => {}
> + }
> +
> + row.into()
> + },
> + ))
> + .into(),
> + ])
> + }
> +}
> +
> +fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTreeEntry> {
> + let mut tree = SlabTree::new();
> +
> + let mut root = tree.set_root(UpdateTreeEntry::Root);
> + root.set_expanded(true);
> +
> + for (remote_name, remote_summary) in update_summary.remotes.deref() {
> + let mut remote_entry = root.append(UpdateTreeEntry::Remote(RemoteEntry {
> + remote: remote_name.clone(),
> + ty: remote_summary.remote_type,
> + number_of_nodes: 0,
> + number_of_updatable_nodes: 0,
> + number_of_failed_nodes: 0,
> + poll_status: remote_summary.status.clone(),
> + }));
> + remote_entry.set_expanded(false);
> +
> + let number_of_nodes = remote_summary.nodes.len();
> + let mut number_of_updatable_nodes = 0;
> + let mut number_of_failed_nodes = 0;
> +
> + for (node_name, node_summary) in remote_summary.nodes.deref() {
> + match node_summary.status {
> + NodeUpdateStatus::Success => {
> + if node_summary.number_of_updates > 0 {
> + number_of_updatable_nodes += 1;
> + }
> + }
> + NodeUpdateStatus::Error => {
> + number_of_failed_nodes += 1;
> + }
> + }
> +
> + remote_entry.append(UpdateTreeEntry::Node(NodeEntry {
> + remote: remote_name.clone(),
> + node: node_name.clone(),
> + ty: remote_summary.remote_type,
> + summary: node_summary.clone(),
> + }));
> + }
> +
> + if let UpdateTreeEntry::Remote(info) = remote_entry.record_mut() {
> + info.number_of_updatable_nodes = number_of_updatable_nodes;
> + info.number_of_nodes = number_of_nodes as u32;
> + info.number_of_failed_nodes = number_of_failed_nodes as u32;
> + }
> + }
> +
> + tree
> +}
> +
> +impl LoadableComponent for UpdateTreeComponent {
> + type Properties = UpdateTree;
> + type Message = RemoteUpdateTreeMsg;
> + type ViewState = ();
> +
> + fn create(ctx: &LoadableComponentContext<Self>) -> Self {
> + let link = ctx.link();
> +
> + let store = TreeStore::new().view_root(false);
> + store.set_sorter(default_sorter);
> +
> + link.repeated_load(5000);
> +
> + let selection = Selection::new().on_select(link.callback(|selection: Selection| {
> + RemoteUpdateTreeMsg::KeySelected(selection.selected_key())
> + }));
> +
> + Self {
> + store: store.clone(),
> + selection,
> + selected_entry: None,
> + }
> + }
> +
> + fn load(
> + &self,
> + ctx: &LoadableComponentContext<Self>,
> + ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
> + let link = ctx.link().clone();
> +
> + Box::pin(async move {
> + let client = pdm_client();
> +
> + let updates = client.remote_update_summary().await?;
> + link.send_message(Self::Message::LoadFinished(updates));
> +
> + Ok(())
> + })
> + }
> +
> + fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
> + match msg {
> + Self::Message::LoadFinished(updates) => {
> + let data = build_store_from_response(updates);
> + self.store.write().update_root_tree(data);
> + self.store.set_sorter(default_sorter);
> +
> + return true;
> + }
> + Self::Message::KeySelected(key) => {
> + if let Some(key) = key {
> + let read_guard = self.store.read();
> + let node_ref = read_guard.lookup_node(&key).unwrap();
> + let record = node_ref.record();
> +
> + self.selected_entry = Some(record.clone());
> +
> + return true;
> + }
> + }
> + Self::Message::RefreshAll => {
> + let link = ctx.link();
> +
> + link.clone().spawn(async move {
> + let client = pdm_client();
> +
> + match client.refresh_remote_update_summary().await {
> + Ok(upid) => {
> + link.show_task_progres(upid.to_string());
> + }
> + Err(err) => {
> + link.show_error(tr!("Could not refresh update status."), err, false);
> + }
> + }
> + });
> + }
> + }
> +
> + false
> + }
> +
> + fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> yew::Html {
> + Container::new()
> + .class("pwt-content-spacer")
> + .class(FlexFit)
> + .class("pwt-flex-direction-row")
> + .with_child(self.render_update_tree_panel(ctx))
> + .with_child(self.render_update_list_panel(ctx))
> + .into()
haven't tested this, but why not just use a Row here?
> + }
> +}
> +
> +impl UpdateTreeComponent {
> + fn render_update_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
> + let table = DataTable::new(Self::columns(ctx, self.store.clone()), self.store.clone())
> + .selection(self.selection.clone())
> + .striped(false)
> + .borderless(true)
> + .show_header(false)
> + .class(css::FlexFit);
> +
> + let refresh_all_button = Button::new(tr!("Refresh all")).on_activate({
> + let link = ctx.link().clone();
> + move |_| {
> + link.send_message(RemoteUpdateTreeMsg::RefreshAll);
> + }
> + });
> +
> + let title: Html = Row::new()
> + .gap(2)
> + .class(AlignItems::Baseline)
> + .with_child(Fa::new("refresh"))
> + .with_child(tr!("Remote System Updates"))
> + .into();
> +
> + Panel::new()
> + .min_width(500)
> + .title(title)
> + .with_tool(refresh_all_button)
> + .style("flex", "1 1 0")
> + .class(FlexFit)
> + .border(true)
> + .with_child(table)
> + }
> +
> + fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
> + let title: Html = Row::new()
> + .gap(2)
> + .class(AlignItems::Baseline)
> + .with_child(Fa::new("list"))
> + .with_child(tr!("Update List"))
> + .into();
> +
> + match &self.selected_entry {
> + // (Some(remote), Some(remote_type), Some(node)) => {
> + Some(UpdateTreeEntry::Node(NodeEntry {
> + remote, node, ty, ..
> + })) => {
> + let base_url = format!("/{ty}/remotes/{remote}/nodes/{node}/apt",);
> + let task_base_url = format!("/{ty}/remotes/{}/tasks", remote);
nit: remote should be inlined here
> +
> + let apt = AptPackageManager::new()
> + .base_url(base_url)
> + .task_base_url(task_base_url)
> + .enable_upgrade(true)
> + .on_upgrade({
> + let remote = remote.clone();
> + let link = ctx.link().clone();
> + let remote = remote.clone();
> + let node = node.clone();
> + let ty = *ty;
> +
> + move |_| match ty {
> + RemoteType::Pve => {
> + let id = format!("node/{}::apt", node);
same for node here
> + if let Some(url) =
> + get_deep_url(&link.yew_link(), &remote, None, &id)
you don't need references here for the first parameter, `yew_link()
already returns a reference
> + {
> + let _ = web_sys::window().unwrap().open_with_url(&url.href());
gloo_utils::window() will safe you an unwrap here :)
> + }
> + }
> + RemoteType::Pbs => {
> + let hash = "#pbsServerAdministration:updates";
> + if let Some(url) =
> + get_deep_url_low_level(&link.yew_link(), &remote, None, &hash)
see comment about yew_link() above, and you can drop the extra reference
for hash here too
> + {
> + let _ = web_sys::window().unwrap().open_with_url(&url.href());
see comment about gloo_utils above
> + }
> + }
> + }
> + });
> +
> + Panel::new()
> + .class(FlexFit)
> + .title(title)
> + .border(true)
> + .min_width(500)
> + .with_child(apt)
> + .style("flex", "1 1 0")
> + }
> + _ => {
> + let header = tr!("No node selected");
> + let msg = tr!("Select a node to show available updates.");
> +
> + let select_node_msg = Column::new()
> + .class(FlexFit)
> + .padding(2)
> + .class(AlignItems::Center)
> + .class(TextAlign::Center)
> + .with_child(html! {<h1 class="pwt-font-headline-medium">{header}</h1>})
> + .with_child(Container::new().with_child(msg));
> +
> + Panel::new()
> + .class(FlexFit)
> + .title(title)
> + .border(true)
> + .min_width(500)
> + .with_child(select_node_msg)
> + .style("flex", "1 1 0")
> + }
> + }
> + }
> +}
> +
> +enum RemoteSummaryIcon {
> + UpToDate,
> + Updatable,
> + Error,
> +}
> +
> +fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
> + let (icon_class, icon_scheme, state_text) = match task_class {
> + RemoteSummaryIcon::UpToDate => (
> + "check",
> + FontColor::Success,
> + tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
> + ),
> + RemoteSummaryIcon::Error => (
> + "times-circle",
> + FontColor::Error,
> + tr!("Failed to retrieve update info for one node."
> + | "Failed to retrieve update info for {n} nodes." % count),
> + ),
> + RemoteSummaryIcon::Updatable => (
> + "refresh",
> + FontColor::Primary,
> + tr!("One node has updates available." | "{n} nodes have updates available." % count),
> + ),
> + };
> +
> + let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
> +
> + Tooltip::new(
> + Container::from_tag("span")
> + .with_child(icon)
> + .with_child(count)
> + .margin_end(5),
> + )
> + .tip(state_text)
> + .into()
> +}
> +
> +fn render_remote_status_icon(task_class: RemoteUpdateStatus) -> Html {
> + let (icon_class, icon_scheme, state_text) = match task_class {
> + RemoteUpdateStatus::Success => (
> + "check",
> + FontColor::Success,
> + tr!("All nodes of this remote are up-to-date."),
> + ),
> + RemoteUpdateStatus::Error => (
> + "times-circle",
> + FontColor::Error,
> + tr!("Could not retrieve update info for remote."),
> + ),
> + RemoteUpdateStatus::Unknown => (
> + "question-circle-o",
> + FontColor::Primary,
> + tr!("The update status is not known."),
> + ),
> + };
> +
> + let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
> +
> + Tooltip::new(Container::from_tag("span").with_child(icon).margin_end(5))
> + .tip(state_text)
> + .into()
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary
2025-10-17 10:15 ` Shannon Sterz
@ 2025-10-17 11:00 ` Lukas Wagner
0 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-17 11:00 UTC (permalink / raw)
To: Shannon Sterz, Lukas Wagner
Cc: Proxmox Datacenter Manager development discussion
On Fri Oct 17, 2025 at 12:15 PM CEST, Shannon Sterz wrote:
> On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
>> This commit adds two new endpoints, namely
>> GET /remote-updates/summary
>> POST /remote-updates/refresh
>>
>> The first one is used to retrieve the update summary (the data is taken
>> from the cache), the second one can be used to proactively refresh the
>> summary in the cache (starts a worker task, since this could take a
>> while). Note that we only retrieve the up-to-date list of packages from
>> the remote, but do *not* trigger an `apt update` right now. Could make
>> sense to do the latter as well, but then we probably should
>> stream/forward the task logs for the upgrade task from the node to the
>
> maybe i'm misunderstanding, but do you mean "update task" here? since
> you talk about triggering an `apt update` before. triggering an actual
> upgrade here seems a little risky and probably needs extra safe-guards?
>
Yes, sorry, my mistake, I of course mean 'update task'.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary
2025-10-17 10:15 ` Shannon Sterz
@ 2025-10-17 11:12 ` Lukas Wagner
2025-10-17 11:52 ` Lukas Wagner
1 sibling, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-17 11:12 UTC (permalink / raw)
To: Shannon Sterz, Lukas Wagner
Cc: Proxmox Datacenter Manager development discussion
On Fri Oct 17, 2025 at 12:15 PM CEST, Shannon Sterz wrote:
> On Wed Oct 15, 2025 at 2:47 PM CEST, Lukas Wagner wrote:
>> This type contains the number of available updates per remote node,
>> without any details. This is useful for a global "give me the update
>> availability for all managed nodes" API endpoint.
>>
>> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
>> ---
>> lib/pdm-api-types/src/lib.rs | 2 +
>> lib/pdm-api-types/src/remote_updates.rs | 126 ++++++++++++++++++++++++
>> 2 files changed, 128 insertions(+)
>> create mode 100644 lib/pdm-api-types/src/remote_updates.rs
>>
>> diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
>> index a3566142..2fb61ef7 100644
>> --- a/lib/pdm-api-types/src/lib.rs
>> +++ b/lib/pdm-api-types/src/lib.rs
>> @@ -96,6 +96,8 @@ pub use openid::*;
>>
>> pub mod remotes;
>>
>> +pub mod remote_updates;
>> +
>> pub mod resource;
>>
>> pub mod rrddata;
>> diff --git a/lib/pdm-api-types/src/remote_updates.rs b/lib/pdm-api-types/src/remote_updates.rs
>> new file mode 100644
>> index 00000000..d04a7a79
>> --- /dev/null
>> +++ b/lib/pdm-api-types/src/remote_updates.rs
>> @@ -0,0 +1,126 @@
>> +use std::{
>> + collections::HashMap,
>> + ops::{Deref, DerefMut},
>> +};
>> +
>
> nit: i think we prefer module level imports these days. so this should
> be
>
> use std::collections::HashMap;
> use std::ops::{Deref, DerefMut};
>
> but no hard feelings from my side and certainly not a blocker
>
Right, thanks for the hint! Although I have to say that I'm not a fan of
enforcing this 'rule' or 'guideline' as long as we cannot teach rustfmt
to 'do the right thing'. 99% of my 'use' statements are produced by
rust-analyzer and I'm not a fan of having to micro-manage these, if I'm
perfectly honest.
>> +use proxmox_schema::{api, ApiType, ObjectSchema};
>> +use serde::{Deserialize, Serialize};
>> +
>
> nit: ordering of use statements should be std -> generic crates (serde,
> anyhow etc.) -> proxmox* -> project; each separate by an empty line, to
> my knowledge.
>
> so:
>
> ```
> use serde::{Deserialize, Serialize};
>
> use proxmox_schema::{api, ApiType, ObjectSchema};
> ```
>
> again not a
> blocker, just saw it and thought id mention it
Thanks for the reminder! Usually I check for this nowadays, but
sometimes I forget to. I wonder if we could create custom linter
script/tool checking for these - maybe using the treesitter library.
>
>> +use crate::remotes::RemoteType; + +#[api] +#[derive(Clone, Debug,
>> Default, Deserialize, Serialize)] +#[serde(rename_all =
>> "kebab-case")] +/// Update summary for all remotes. +pub struct
>> UpdateSummary { + /// Map containing the update summary each
>> remote. + pub remotes: RemoteUpdateSummaryWrapper, +} + +// This
>> is a hack to allow actual 'maps' (mapping remote name to per-remote
>> data) +// within the realms of our API macro. +#[derive(Clone, Debug,
>> Default, Deserialize, Serialize)] +#[serde(rename_all =
>> "kebab-case")] +pub struct RemoteUpdateSummaryWrapper { +
>> #[serde(flatten)] + remotes: HashMap<String, RemoteUpdateSummary>,
>> +}
>
> just wondering whether you have considered changing this to
>
> pub struct RemoteUpdateSummaryWrapper(HashMap<String,
> RemoteUpdateSummary>);
>
> would save you a `flatten` from what i can tell?
>
Thanks for the idea, I did not really think about this before. But then
again it does not really have any benefits apart from not having to
write the 'flatten' statement, does it?
>> +impl ApiType for RemoteUpdateSummaryWrapper { + const API_SCHEMA:
>> proxmox_schema::Schema = + ObjectSchema::new("Map of
>> per-remote update summaries", &[]) + .additional_properties(true) +
>> .schema(); +} + +impl Deref for RemoteUpdateSummaryWrapper { +
>> type Target = HashMap<String, RemoteUpdateSummary>; + + fn
>> deref(&self) -> &Self::Target { + &self.remotes + } +} + +impl
>> DerefMut for RemoteUpdateSummaryWrapper { + fn deref_mut(&mut
>> self) -> &mut Self::Target { + &mut self.remotes + } +} +
>> +#[api] +#[derive(Clone, Debug, Deserialize, Serialize)]
>> +#[serde(rename_all = "kebab-case")] +/// Update summary for a single
>> remote. +pub struct RemoteUpdateSummary { + /// Map containing the
>> update summary for each node of this remote. + pub nodes:
>> NodeUpdateSummaryWrapper, + pub remote_type: RemoteType, + pub
>> status: RemoteUpdateStatus, +} + +#[api] +#[derive(Clone, Debug,
>> Deserialize, Serialize, PartialEq)] +#[serde(rename_all =
>> "kebab-case")] +/// Status for the entire remote. +pub enum
>> RemoteUpdateStatus { + /// Successfully polled remote. +
>> Success, + /// Remote could not be polled. + Error, + ///
>> Remote has not been polled yet. + Unknown, +} + +#[derive(Clone,
>> Debug, Default, Deserialize, Serialize)] +#[serde(rename_all =
>> "kebab-case")] +pub struct NodeUpdateSummaryWrapper { +
>> #[serde(flatten)] + nodes: HashMap<String, NodeUpdateSummary>, +}
>> + +impl ApiType for NodeUpdateSummaryWrapper { + const API_SCHEMA:
>> proxmox_schema::Schema = + ObjectSchema::new("Map of per-node
>> update summaries", &[]) + .additional_properties(true) +
>> .schema(); +} + +impl Deref for NodeUpdateSummaryWrapper { + type
>> Target = HashMap<String, NodeUpdateSummary>; + + fn deref(&self)
>> -> &Self::Target { + &self.nodes + } +} + +impl DerefMut
>> for NodeUpdateSummaryWrapper { + fn deref_mut(&mut self) -> &mut
>> Self::Target { + &mut self.nodes + } +} + +#[api]
>> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
>> +#[serde(rename_all = "kebab-case")] +/// Status for the entire
>> remote. +pub enum NodeUpdateStatus { + /// Successfully polled
>> node. + Success, + /// Node could not be polled. + Error, +}
>> + +#[api] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
>> +#[serde(rename_all = "kebab-case")] +/// Per-node update summary.
>> +pub struct NodeUpdateSummary { + /// Number of available updates.
>> + pub number_of_updates: u32, + /// Unix timestamp of the last
>> refresh. + pub last_refresh: i64, + /// Status + pub status:
>> NodeUpdateStatus, + /// Status message (e.g. error message) + pub
>> status_message: Option<String>, +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary
2025-10-17 10:15 ` Shannon Sterz
2025-10-17 11:12 ` Lukas Wagner
@ 2025-10-17 11:52 ` Lukas Wagner
1 sibling, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-17 11:52 UTC (permalink / raw)
To: Shannon Sterz, Lukas Wagner
Cc: Proxmox Datacenter Manager development discussion
Incorporated all of your suggestions for v2!
Thanks :)
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
* [pdm-devel] superseded: [PATCH proxmox-datacenter-manager 00/12] add global remote update view
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
` (12 preceding siblings ...)
2025-10-17 10:15 ` [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Shannon Sterz
@ 2025-10-17 12:14 ` Lukas Wagner
13 siblings, 0 replies; 22+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:14 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion
Superseded-by: https://lore.proxmox.com/pdm-devel/20251017121009.212499-1-l.wagner@proxmox.com/T/#t
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2025-10-17 12:14 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-15 12:46 [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/12] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-17 11:12 ` Lukas Wagner
2025-10-17 11:52 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/12] remote updates: add cache for remote update availability Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
2025-10-17 7:44 ` Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-17 11:00 ` Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/12] pdm-client: add API methods for remote update summaries Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/12] remote updates: add support for PBS remotes Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/12] api: add APT endpoints " Lukas Wagner
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/12] ui: add remote update view Lukas Wagner
2025-10-17 10:15 ` Shannon Sterz
2025-10-15 12:47 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
2025-10-17 10:15 ` [pdm-devel] [PATCH proxmox-datacenter-manager 00/12] add global remote update view Shannon Sterz
2025-10-17 12:14 ` [pdm-devel] superseded: " Lukas Wagner
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.