public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view
@ 2025-10-23 12:44 Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 01/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
                   ` (11 more replies)
  0 siblings, 12 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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.

Changes since v2:
  In the UI:
  - Show text for collapsed remote tree item
    - "All nodes up-to-date"
    - "Some nodes nodes have pending updates"
    - "Some nodes have pending updates, some nodes unavailable"
    - ...
  - Collapse single-node PVE remotes and PBS remotes into a single item
    to reduce clicks needed
  - Show remote and node in the header of the right-hand update list
  - Some refactoring to make the code a tiny bit nicer

Changes since v1:
  - Incorporated review feedback from Shannon - see commits for details


proxmox-datacenter-manager:

Lukas Wagner (12):
  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
  remote updates: avoid unnecessary clone

 lib/pdm-api-types/src/lib.rs                  |   2 +
 lib/pdm-api-types/src/remote_updates.rs       | 119 ++++
 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 ++
 server/src/pbs_client.rs                      |  51 ++
 server/src/remote_tasks/mod.rs                |  45 +-
 server/src/remote_updates.rs                  | 236 +++++++-
 ui/src/remotes/mod.rs                         |  10 +
 ui/src/remotes/updates.rs                     | 530 ++++++++++++++++++
 18 files changed, 1295 insertions(+), 144 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:
  18 files changed, 1295 insertions(+), 144 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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 01/12] pdm-api-types: add types for remote upgrade summary
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 02/12] remote updates: add cache for remote update availability Lukas Wagner
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v1:
      - Changed order/style of use statements
      - Make RemoteUpdateSummaryWrapper a tuple struct
      - Make NodeUpdateSummaryWrapper a tuple struct
    
    Thank you Shannon!

 lib/pdm-api-types/src/lib.rs            |   2 +
 lib/pdm-api-types/src/remote_updates.rs | 119 ++++++++++++++++++++++++
 2 files changed, 121 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..e42b6a96
--- /dev/null
+++ b/lib/pdm-api-types/src/remote_updates.rs
@@ -0,0 +1,119 @@
+use std::collections::HashMap;
+use std::ops::{Deref, DerefMut};
+
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, ApiType, ObjectSchema};
+
+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(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.0
+    }
+}
+
+impl DerefMut for RemoteUpdateSummaryWrapper {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[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(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.0
+    }
+}
+
+impl DerefMut for NodeUpdateSummaryWrapper {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 02/12] remote updates: add cache for remote update availability
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 01/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 03/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v2:
      - Reset 'status' for remote to Success if it had previously failed but
        then could be polled again successfully

 server/src/remote_updates.rs | 195 +++++++++++++++++++++++++++++++++--
 1 file changed, 189 insertions(+), 6 deletions(-)

diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index f833062c..c7b678b8 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,157 @@ 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) => {
+                entry.status = RemoteUpdateStatus::Success;
+
+                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;
+                entry.nodes.clear();
+                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 +247,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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 03/12] api: add API for retrieving/refreshing the remote update summary
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 01/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 02/12] remote updates: add cache for remote update availability Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 04/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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 update task from the node to the
native PDM task; something we can rather implement later.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v1:
      - Fail with FORBIDDEN instead of UNAUTHORIZED in case of lacking
        permissions

 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..1feef280
--- /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!(FORBIDDEN, "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!(FORBIDDEN, "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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 04/12] unprivileged api daemon: tasks: add remote update refresh task
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (2 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 03/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 05/12] pdm-client: add API methods for remote update summaries Lukas Wagner
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 05/12] pdm-client: add API methods for remote update summaries
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (3 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 04/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 06/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 UTC (permalink / raw)
  To: pdm-devel

Adds methods to retrieve and refresh the update summary.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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, &params).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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 06/12] pbs-client: add bindings for APT-related API calls
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (4 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 05/12] pdm-client: add API methods for remote update summaries Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 07/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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 433b5d4b..49087360 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> {
@@ -267,6 +278,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", &params)
+            .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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 07/12] task cache: use separate functions for tracking PVE and PBS tasks
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (5 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 06/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 08/12] remote updates: add support for PBS remotes Lukas Wagner
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 08/12] remote updates: add support for PBS remotes
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (6 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 07/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 09/12] api: add APT endpoints " Lukas Wagner
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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 ad82ab38..56192c01 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 c7b678b8..cdba7f18 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)
+        }
     }
 }
 
@@ -252,7 +271,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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 09/12] api: add APT endpoints for PBS remotes
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (7 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 08/12] remote updates: add support for PBS remotes Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 10/12] ui: add remote update view Lukas Wagner
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 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>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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 56192c01..a37fadb5 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 1feef280..c1309432 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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 10/12] ui: add remote update view
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (8 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 09/12] api: add APT endpoints " Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 11/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 12/12] remote updates: avoid unnecessary clone Lukas Wagner
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 UTC (permalink / raw)
  To: pdm-devel

This commit adds a new view for showing a global overview about
available updates on managed remotes. The 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>
---

Notes:
    Chances since v2:
      - Show text for collapsed remote tree item
        - "All nodes up-to-date"
        - "Some nodes nodes have pending updates"
        - "Some nodes have pending updates, some nodes unavailable"
        - ...
      - Collapse single-node PVE remotes and PBS remotes into a single item
        to reduce clicks needed
      - Show remote and node in the header of the right-hand update list
      - Some refactoring to make the code a tiny bit nicer
    
    Changes since v1:
     - made RemoteUpdateTreeMsg and UpdateTreeComp private
     - format!(...) var inlining
     - remove unneeded borrows
     - use gloo_utils::window()

 ui/src/remotes/mod.rs     |   3 +
 ui/src/remotes/updates.rs | 530 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 533 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..8294795a
--- /dev/null
+++ b/ui/src/remotes/updates.rs
@@ -0,0 +1,530 @@
+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, CssPaddingBuilder, WidgetStyleBuilder};
+use pwt::widget::{Button, Container, Panel};
+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,
+    flat: bool,
+}
+
+#[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) => {
+                if data.flat {
+                    &data.remote
+                } else {
+                    &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),
+        })
+    }
+}
+
+enum RemoteUpdateTreeMsg {
+    LoadFinished(UpdateSummary),
+    KeySelected(Option<Key>),
+    RefreshAll,
+}
+
+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)
+                .flex(1)
+                .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"))
+                .flex(3)
+                .render_cell(DataTableCellRenderer::new(
+                    move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| match args.record() {
+                        UpdateTreeEntry::Root => {
+                            html!()
+                        }
+                        UpdateTreeEntry::Remote(remote_info) => {
+                            render_remote_summary(remote_info, args.is_expanded()).into()
+                        }
+                        UpdateTreeEntry::Node(info) => render_node_info(info).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() {
+        if remote_summary.nodes.len() == 1 {
+            if let Some((node_name, node_summary)) = remote_summary.nodes.iter().take(1).next() {
+                root.append(UpdateTreeEntry::Node(NodeEntry {
+                    remote: remote_name.clone(),
+                    node: node_name.clone(),
+                    ty: remote_summary.remote_type,
+                    summary: node_summary.clone(),
+                    flat: true,
+                }));
+
+                continue;
+            }
+        }
+
+        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(),
+                flat: false,
+            }));
+        }
+
+        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 {
+        match &self.selected_entry {
+            Some(UpdateTreeEntry::Node(NodeEntry {
+                remote, node, ty, ..
+            })) => {
+                let title: Html = Row::new()
+                    .gap(2)
+                    .class(AlignItems::Baseline)
+                    .with_child(Fa::new("list"))
+                    .with_child(tr!("Update List - {} ({})", remote, node))
+                    .into();
+
+                let base_url = format!("/{ty}/remotes/{remote}/nodes/{node}/apt",);
+                let task_base_url = format!("/{ty}/remotes/{remote}/tasks");
+
+                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/{node}::apt");
+                                if let Some(url) = get_deep_url(link.yew_link(), &remote, None, &id)
+                                {
+                                    let _ = gloo_utils::window().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 _ = gloo_utils::window().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 title: Html = Row::new()
+                    .gap(2)
+                    .class(AlignItems::Baseline)
+                    .with_child(Fa::new("list"))
+                    .with_child(tr!("Update List"))
+                    .into();
+
+                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")
+            }
+        }
+    }
+}
+
+fn render_remote_summary(entry: &RemoteEntry, expanded: bool) -> Row {
+    let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+    match entry.poll_status {
+        RemoteUpdateStatus::Success => {
+            if !expanded {
+                let up_to_date_nodes = entry.number_of_nodes
+                    - entry.number_of_updatable_nodes
+                    - entry.number_of_failed_nodes;
+
+                let text = if entry.number_of_nodes == up_to_date_nodes {
+                    row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::UpToDate));
+                    tr!("All nodes up-to-date")
+                } else if entry.number_of_updatable_nodes > 0 {
+                    row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Updatable));
+
+                    if entry.number_of_failed_nodes > 0 {
+                        row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
+                        // NOTE: This 'summary' line is only shown for remotes with multiple nodes,
+                        // so we don't really have to consider the singular form of 'x out of y
+                        // nodes'
+                        tr!("Some nodes have pending updates, some nodes unavailable")
+                    } else {
+                        tr!("Some nodes have pending updates")
+                    }
+                } else if entry.number_of_failed_nodes > 0 {
+                    row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
+                    tr!("Some nodes unavailable")
+                } else {
+                    String::new()
+                };
+
+                row = row.with_child(text);
+            }
+        }
+        RemoteUpdateStatus::Error => {
+            row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Error));
+            row = row.with_child(tr!("Could not connect to remote"));
+        }
+        RemoteUpdateStatus::Unknown => {
+            row = row.with_child(render_remote_summary_icon(RemoteSummaryIcon::Unknown));
+            row = row.with_child(tr!("Update status unknown"));
+        }
+    }
+
+    row
+}
+
+fn render_node_info(entry: &NodeEntry) -> Row {
+    let (icon, text) = if entry.summary.status == NodeUpdateStatus::Error {
+        let icon = render_remote_summary_icon(RemoteSummaryIcon::Error);
+        let text = if let Some(status) = &entry.summary.status_message {
+            tr!("Failed to retrieve update status: {}", status)
+        } else {
+            tr!("Unknown error")
+        };
+
+        (icon, text)
+    } else if entry.summary.number_of_updates > 0 {
+        (
+            render_remote_summary_icon(RemoteSummaryIcon::Updatable),
+            tr!("One update pending" | "{n} updates pending" % entry.summary.number_of_updates),
+        )
+    } else {
+        (
+            render_remote_summary_icon(RemoteSummaryIcon::UpToDate),
+            tr!("Up-to-date"),
+        )
+    };
+
+    Row::new()
+        .class(css::AlignItems::Baseline)
+        .gap(2)
+        .with_child(icon)
+        .with_child(text)
+}
+
+enum RemoteSummaryIcon {
+    UpToDate,
+    Updatable,
+    Error,
+    Unknown,
+}
+
+fn render_remote_summary_icon(icon: RemoteSummaryIcon) -> Fa {
+    let (icon_class, icon_scheme) = match icon {
+        RemoteSummaryIcon::UpToDate => ("check", FontColor::Success),
+        RemoteSummaryIcon::Error => ("times-circle", FontColor::Error),
+        RemoteSummaryIcon::Updatable => ("refresh", FontColor::Primary),
+        RemoteSummaryIcon::Unknown => ("question-circle-o", FontColor::Primary),
+    };
+
+    Fa::new(icon_class).class(icon_scheme)
+}
-- 
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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 11/12] ui: show new remote update view in the 'Remotes' section
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (9 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 10/12] ui: add remote update view Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 12/12] remote updates: avoid unnecessary clone Lukas Wagner
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@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] 13+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v3 12/12] remote updates: avoid unnecessary clone
  2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
                   ` (10 preceding siblings ...)
  2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 11/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
@ 2025-10-23 12:44 ` Lukas Wagner
  11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-10-23 12:44 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    New in v2

 server/src/api/remote_updates.rs | 2 +-
 server/src/remote_updates.rs     | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
index c1309432..bea2d864 100644
--- a/server/src/api/remote_updates.rs
+++ b/server/src/api/remote_updates.rs
@@ -201,7 +201,7 @@ async fn apt_get_changelog(
     let (config, _digest) = pdm_config::remotes::config()?;
     let remote = get_remote(&config, &remote)?;
 
-    remote_updates::get_changelog(remote.clone(), &node, options.name).await
+    remote_updates::get_changelog(remote, &node, options.name).await
 }
 
 const APT_SUBDIRS: SubdirMap = &[
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index cdba7f18..1a0d3673 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -83,10 +83,10 @@ pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUp
 }
 
 /// Get the changelog for a given package.
-pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Result<String, Error> {
+pub async fn get_changelog(remote: &Remote, node: &str, package: String) -> Result<String, Error> {
     match remote.ty {
         RemoteType::Pve => {
-            let client = connection::make_pve_client(&remote)?;
+            let client = connection::make_pve_client(remote)?;
 
             client
                 .get_package_changelog(node, package, None)
@@ -94,7 +94,7 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
                 .map_err(Into::into)
         }
         RemoteType::Pbs => {
-            let client = connection::make_pbs_client(&remote)?;
+            let client = connection::make_pbs_client(remote)?;
 
             client
                 .get_package_changelog(package, None)
-- 
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] 13+ messages in thread

end of thread, other threads:[~2025-10-23 12:45 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-23 12:44 [pdm-devel] [PATCH datacenter-manager v3 00/12] add global remote update view Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 01/12] pdm-api-types: add types for remote upgrade summary Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 02/12] remote updates: add cache for remote update availability Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 03/12] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 04/12] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 05/12] pdm-client: add API methods for remote update summaries Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 06/12] pbs-client: add bindings for APT-related API calls Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 07/12] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 08/12] remote updates: add support for PBS remotes Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 09/12] api: add APT endpoints " Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 10/12] ui: add remote update view Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 11/12] ui: show new remote update view in the 'Remotes' section Lukas Wagner
2025-10-23 12:44 ` [pdm-devel] [PATCH datacenter-manager v3 12/12] remote updates: avoid unnecessary clone Lukas Wagner

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal