* [pdm-devel] [PATCH datacenter-manager v2 01/12] pdm-api-types: views: add ViewFilterConfig type
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 02/12] pdm-config: views: add support for view-filters Lukas Wagner
` (10 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
This type will be used to define view filters.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
Notes:
Changes since RFC:
- Change config structure, instead of 'include-{x} value' we now do
'include {x}:value'
lib/pdm-api-types/src/lib.rs | 2 +
lib/pdm-api-types/src/views.rs | 165 +++++++++++++++++++++++++++++++++
2 files changed, 167 insertions(+)
create mode 100644 lib/pdm-api-types/src/views.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 2fb61ef7..a7ba6d89 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -106,6 +106,8 @@ pub mod subscription;
pub mod sdn;
+pub mod views;
+
const_regex! {
// just a rough check - dummy acceptor is used before persisting
pub OPENSSL_CIPHERS_REGEX = r"^[0-9A-Za-z_:, +!\-@=.]+$";
diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs
new file mode 100644
index 00000000..b262cc05
--- /dev/null
+++ b/lib/pdm-api-types/src/views.rs
@@ -0,0 +1,165 @@
+use std::{fmt::Display, str::FromStr};
+
+use anyhow::bail;
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, ApiStringFormat, ArraySchema, Schema, StringSchema, Updater};
+
+use crate::{remotes::REMOTE_ID_SCHEMA, resource::ResourceType, VIEW_FILTER_ID_SCHEMA};
+
+#[api(
+ properties: {
+ "id": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ },
+ "include": {
+ schema: FILTER_RULE_LIST_SCHEMA,
+ optional: true,
+ },
+ "exclude": {
+ schema: FILTER_RULE_LIST_SCHEMA,
+ optional: true,
+ }
+ }
+)]
+#[derive(Clone, Default, Deserialize, Serialize, Updater)]
+#[serde(rename_all = "kebab-case")]
+/// View definition
+pub struct ViewFilterConfig {
+ /// View filter name.
+ pub id: String,
+
+ /// List of includes.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub include: Vec<FilterRule>,
+
+ /// List of excludes.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub exclude: Vec<FilterRule>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+/// Filter rule for includes/excludes.
+pub enum FilterRule {
+ /// Match a resource type.
+ ResourceType(ResourceType),
+ /// Match a resource pools (for PVE guests).
+ ResourcePool(String),
+ /// Match a (global) resource ID, e.g. 'remote/<remote>/guest/<vmid>'.
+ ResourceId(String),
+ /// Match a tag (for PVE guests).
+ Tag(String),
+ /// Match a remote.
+ Remote(String),
+}
+
+impl FromStr for FilterRule {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s.split_once(':') {
+ Some(("resource-type", value)) => FilterRule::ResourceType(value.parse()?),
+ Some(("resource-pool", value)) => {
+ // TODO: Define schema and use it to validate.
+ if value.is_empty() {
+ bail!("empty resource-pool rule not allowed");
+ }
+ FilterRule::ResourcePool(value.to_string())
+ }
+ Some(("resource-id", value)) => {
+ // TODO: Define schema and use it to validate.
+ if value.is_empty() {
+ bail!("empty resource-id rule not allowed");
+ }
+ FilterRule::ResourceId(value.to_string())
+ }
+ Some(("tag", value)) => {
+ // TODO: Define schema and use it to validate.
+ if value.is_empty() {
+ bail!("empty tag rule not allowed");
+ }
+ FilterRule::Tag(value.to_string())
+ }
+ Some(("remote", value)) => {
+ let _ = REMOTE_ID_SCHEMA.parse_simple_value(value)?;
+ FilterRule::Remote(value.to_string())
+ }
+ Some((ty, _)) => bail!("invalid type: {ty}"),
+ None => bail!("invalid filter rule: {s}"),
+ })
+ }
+}
+
+// used for serializing below, caution!
+impl Display for FilterRule {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ FilterRule::ResourceType(resource_type) => write!(f, "resource-type:{resource_type}"),
+ FilterRule::ResourcePool(pool) => write!(f, "resource-pool:{pool}"),
+ FilterRule::ResourceId(id) => write!(f, "resource-id:{id}"),
+ FilterRule::Tag(tag) => write!(f, "tag:{tag}"),
+ FilterRule::Remote(remote) => write!(f, "remote:{remote}"),
+ }
+ }
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(FilterRule);
+proxmox_serde::forward_serialize_to_display!(FilterRule);
+
+fn verify_group_filter(input: &str) -> Result<(), anyhow::Error> {
+ FilterRule::from_str(input).map(|_| ())
+}
+
+/// Schema for filter rules.
+pub const FILTER_RULE_SCHEMA: Schema = StringSchema::new("Filter rule for resources.")
+ .format(&ApiStringFormat::VerifyFn(verify_group_filter))
+ .type_text(
+ "resource-type:storage|qemu|lxc|sdn-zone|datastore|node>\
+ |resource-pool:<pool-name>\
+ |tag:<tag-name>\
+ |remote:<remote-name>\
+ |resource-id:<resource-id>",
+ )
+ .schema();
+
+/// Schema for list of filter rules.
+pub const FILTER_RULE_LIST_SCHEMA: Schema =
+ ArraySchema::new("List of filter rules.", &FILTER_RULE_SCHEMA).schema();
+
+#[cfg(test)]
+mod test {
+ use anyhow::Error;
+
+ use crate::views::FilterRule;
+
+ fn parse_and_check_display(input: &str) -> Result<bool, Error> {
+ let rule: FilterRule = input.parse()?;
+
+ Ok(input == format!("{rule}"))
+ }
+
+ #[test]
+ fn test_filter_rule() {
+ assert!(parse_and_check_display("abc").is_err());
+ assert!(parse_and_check_display("abc:").is_err());
+
+ assert!(parse_and_check_display("resource-type:").is_err());
+ assert!(parse_and_check_display("resource-type:lxc").unwrap());
+ assert!(parse_and_check_display("resource-type:qemu").unwrap());
+ assert!(parse_and_check_display("resource-type:abc").is_err());
+
+ assert!(parse_and_check_display("resource-pool:").is_err());
+ assert!(parse_and_check_display("resource-pool:somepool").unwrap());
+
+ assert!(parse_and_check_display("resource-id:").is_err());
+ assert!(parse_and_check_display("resource-id:remote/someremote/guest/100").unwrap());
+
+ assert!(parse_and_check_display("tag:").is_err());
+ assert!(parse_and_check_display("tag:sometag").unwrap());
+
+ assert!(parse_and_check_display("remote:someremote").unwrap());
+ assert!(parse_and_check_display("remote:a").is_err());
+ }
+}
--
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 v2 02/12] pdm-config: views: add support for view-filters
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 01/12] pdm-api-types: views: add ViewFilterConfig type Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 03/12] acl: add '/view' and '/view/{view-id}' as allowed ACL paths Lukas Wagner
` (9 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
This allows to read ViewFilterConfig entries from a new config file at
/etc/proxmox-datacenter-manager/views/filters.cfg.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-api-types/src/lib.rs | 6 ++++
lib/pdm-config/src/lib.rs | 2 +-
lib/pdm-config/src/views.rs | 62 ++++++++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+), 1 deletion(-)
create mode 100644 lib/pdm-config/src/views.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index a7ba6d89..ed284ab2 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -159,6 +159,12 @@ pub const REALM_ID_SCHEMA: Schema = StringSchema::new("Realm name.")
.max_length(32)
.schema();
+pub const VIEW_FILTER_ID_SCHEMA: Schema = StringSchema::new("View filter name.")
+ .format(&PROXMOX_SAFE_ID_FORMAT)
+ .min_length(2)
+ .max_length(32)
+ .schema();
+
pub const VMID_SCHEMA: Schema = IntegerSchema::new("A guest ID").minimum(1).schema();
pub const SNAPSHOT_NAME_SCHEMA: Schema = StringSchema::new("The name of the snapshot")
.format(&PROXMOX_SAFE_ID_FORMAT)
diff --git a/lib/pdm-config/src/lib.rs b/lib/pdm-config/src/lib.rs
index ac398cab..4c490541 100644
--- a/lib/pdm-config/src/lib.rs
+++ b/lib/pdm-config/src/lib.rs
@@ -1,6 +1,5 @@
use anyhow::{format_err, Error};
use nix::unistd::{Gid, Group, Uid, User};
-
pub use pdm_buildcfg::{BACKUP_GROUP_NAME, BACKUP_USER_NAME};
pub mod certificate_config;
@@ -8,6 +7,7 @@ pub mod domains;
pub mod node;
pub mod remotes;
pub mod setup;
+pub mod views;
mod config_version_cache;
pub use config_version_cache::ConfigVersionCache;
diff --git a/lib/pdm-config/src/views.rs b/lib/pdm-config/src/views.rs
new file mode 100644
index 00000000..adeb67b1
--- /dev/null
+++ b/lib/pdm-config/src/views.rs
@@ -0,0 +1,62 @@
+use std::sync::LazyLock;
+
+use anyhow::Error;
+
+use proxmox_schema::ApiType;
+use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
+
+use pdm_api_types::{views::ViewFilterConfig, ConfigDigest, VIEW_FILTER_ID_SCHEMA};
+
+use pdm_buildcfg::configdir;
+
+static VIEW_FILTER_SECTION_NAME: &str = "view-filter";
+
+static CONFIG: LazyLock<SectionConfig> = LazyLock::new(init);
+
+fn init() -> SectionConfig {
+ let mut config = SectionConfig::new(&VIEW_FILTER_ID_SCHEMA);
+
+ let view_plugin = SectionConfigPlugin::new(
+ VIEW_FILTER_SECTION_NAME.to_string(),
+ Some("id".to_string()),
+ ViewFilterConfig::API_SCHEMA.unwrap_object_schema(),
+ );
+ config.register_plugin(view_plugin);
+
+ config
+}
+
+const VIEW_FILTER_CFG_FILENAME: &str = configdir!("/views/filters.cfg");
+
+// TODO: Will be needed once the CRUD API for view filters is implemented.
+// const VIEW_FILTER_CFG_LOCKFILE: &str = configdir!("/views/.filters.lock");
+
+// TODO: Will be needed once the CRUD API for view filters is implemented.
+/// Get exclusive lock
+// fn lock_config() -> Result<ApiLockGuard, Error> {
+// open_api_lockfile(VIEW_FILTER_CFG_LOCKFILE, None, true)
+// }
+
+fn config() -> Result<(SectionConfigData, ConfigDigest), Error> {
+ let content =
+ proxmox_sys::fs::file_read_optional_string(VIEW_FILTER_CFG_FILENAME)?.unwrap_or_default();
+ let digest = ConfigDigest::from_slice(content.as_bytes());
+ let data = CONFIG.parse(VIEW_FILTER_CFG_FILENAME, &content)?;
+ Ok((data, digest))
+}
+
+// TODO: Will be needed once the CRUD API for view filters is implemented.
+// fn save_config(config: &SectionConfigData) -> Result<(), Error> {
+// let raw = CONFIG.write(VIEW_FILTER_CFG_FILENAME, config)?;
+// replace_privileged_config(VIEW_FILTER_CFG_FILENAME, raw.as_bytes())
+// }
+//
+
+/// Get the [`ViewFilterConfig`] entry for a view filter with a given ID.
+///
+/// This will fail if the config file does not exist or if the view filter does not exist.
+pub fn get_view_filter_config(filter_name: &str) -> Result<ViewFilterConfig, Error> {
+ let (cfg, _) = config()?;
+
+ cfg.lookup(VIEW_FILTER_SECTION_NAME, filter_name)
+}
--
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 v2 03/12] acl: add '/view' and '/view/{view-id}' as allowed ACL paths
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 01/12] pdm-api-types: views: add ViewFilterConfig type Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 02/12] pdm-config: views: add support for view-filters Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 04/12] views: add implementation for view filters Lukas Wagner
` (8 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
These paths will be used for ACL objects for views. A view has filter
rules that specify which resources/remotes are included in the view. If
a user has permissions on the corresponding ACL object for the view,
then the privileges are transitively applied to the included resources
as well.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/acl.rs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/server/src/acl.rs b/server/src/acl.rs
index 52a1f972..f5f57c03 100644
--- a/server/src/acl.rs
+++ b/server/src/acl.rs
@@ -150,6 +150,12 @@ impl proxmox_access_control::init::AccessControlConfig for AccessControlConfig {
_ => {}
}
}
+ "view" => {
+ // `/view` and `/view/{view-id}`
+ if components_len <= 2 {
+ return Ok(());
+ }
+ }
_ => {}
}
--
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 v2 04/12] views: add implementation for view filters
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (2 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 03/12] acl: add '/view' and '/view/{view-id}' as allowed ACL paths Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 05/12] views: add tests for view filter implementation Lukas Wagner
` (7 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
This commit adds the filter implementation for the previously defined
ViewFilterConfig type.
There are include/exclude rules for the following properties:
- (global) resource-id
- resource pool
- resource type
- remote
- tags
The rules are interpreted as follows:
- no rules: everything matches
- only includes: included resources match
- only excluded: everything *but* the excluded resources match
- include and exclude: excludes are applied *after* includes, meaning if
one has a `include-remote foo` and `exclude-remote foo` at the same
time, the remote `foo` will never match
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/lib.rs | 1 +
server/src/views/mod.rs | 1 +
server/src/views/view_filter.rs | 182 ++++++++++++++++++++++++++++++++
3 files changed, 184 insertions(+)
create mode 100644 server/src/views/mod.rs
create mode 100644 server/src/views/view_filter.rs
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 964807eb..0f25aa71 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -12,6 +12,7 @@ pub mod remote_tasks;
pub mod remote_updates;
pub mod resource_cache;
pub mod task_utils;
+pub mod views;
pub mod connection;
pub mod pbs_client;
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
new file mode 100644
index 00000000..9a2856a4
--- /dev/null
+++ b/server/src/views/mod.rs
@@ -0,0 +1 @@
+pub mod view_filter;
diff --git a/server/src/views/view_filter.rs b/server/src/views/view_filter.rs
new file mode 100644
index 00000000..4f77e7bf
--- /dev/null
+++ b/server/src/views/view_filter.rs
@@ -0,0 +1,182 @@
+use anyhow::Error;
+
+use pdm_api_types::{
+ resource::{Resource, ResourceType},
+ views::{FilterRule, ViewFilterConfig},
+};
+
+/// Get view filter with a given ID.
+///
+/// Returns an error if the view filter configuration file could not be read, or
+/// if the view filter with the provided ID does not exist.
+pub fn get_view_filter(filter_id: &str) -> Result<ViewFilter, Error> {
+ pdm_config::views::get_view_filter_config(filter_id).map(ViewFilter::new)
+}
+
+/// View filter implementation.
+///
+/// Given a [`ViewFilterConfig`], this struct can be used to check if a resource/remote/node
+/// matches the filter rules.
+#[derive(Clone)]
+pub struct ViewFilter {
+ config: ViewFilterConfig,
+}
+
+impl ViewFilter {
+ /// Create a new [`ViewFiler`].
+ pub fn new(config: ViewFilterConfig) -> Self {
+ Self { config }
+ }
+
+ /// Check if a [`Resource`] matches the filter rules.
+ pub fn resource_matches(&self, remote: &str, resource: &Resource) -> bool {
+ // NOTE: Establishing a cache here is not worth the effort at the moment, evaluation of
+ // rules is *very* fast.
+ //
+ // Some experiments were performed with a cache that works roughly as following:
+ // - HashMap<ViewId, HashMap<ResourceId, bool>> in a mutex
+ // - Cache invalidated if view-filter config digest changed
+ // - Cache invalidated if certain resource fields such as tags or resource pools change
+ // from the last time (also with a digest-based implementation)
+ //
+ // Experimented with the `fake-remote` feature and and 15000 guests showed that
+ // caching was only faster than direct evaluation if the number of rules in the
+ // ViewFilterConfig is *huge* (e.g. >1000 `include-resource-id` entries). But even for those,
+ // direct evaluation was always plenty fast, with evaluation times ~20ms for *all* resources.
+ //
+ // -> for any *realistic* filter config, we should be good with direct evaluation, as long
+ // as we don't add any filter rules which are very expensive to evaluate.
+
+ let resource_data = resource.into();
+
+ self.check_if_included(remote, &resource_data)
+ && !self.check_if_excluded(remote, &resource_data)
+ }
+
+ /// Check if a remote can be safely skipped based on the filter rule definition.
+ ///
+ /// When there are `include remote:<...>` or `exclude remote:<...>` rules, we can use these to
+ /// check if a remote needs to be considered at all.
+ pub fn can_skip_remote(&self, remote: &str) -> bool {
+ let mut has_any_include_remote = false;
+ let mut matches_any_include_remote = false;
+
+ let mut any_other = false;
+
+ for include in &self.config.include {
+ if let FilterRule::Remote(r) = include {
+ has_any_include_remote = true;
+ if r == remote {
+ matches_any_include_remote = true;
+ break;
+ }
+ } else {
+ any_other = true;
+ }
+ }
+
+ let matches_any_exclude_remote = self.config.exclude.iter().any(|e| {
+ if let FilterRule::Remote(r) = e {
+ r == remote
+ } else {
+ false
+ }
+ });
+
+ (has_any_include_remote && !matches_any_include_remote && !any_other)
+ || matches_any_exclude_remote
+ }
+
+ /// Check if a node is matched by the filter rules.
+ ///
+ /// This is equivalent to checking an actual node resource.
+ pub fn is_node_included(&self, remote: &str, node: &str) -> bool {
+ let resource_data = ResourceData {
+ resource_type: ResourceType::Node,
+ tags: None,
+ resource_pool: None,
+ resource_id: &format!("remote/{remote}/node/{node}"),
+ };
+
+ self.check_if_included(remote, &resource_data)
+ && !self.check_if_excluded(remote, &resource_data)
+ }
+
+ /// Returns the name of the view filter.
+ pub fn name(&self) -> &str {
+ &self.config.id
+ }
+
+ fn check_if_included(&self, remote: &str, resource: &ResourceData) -> bool {
+ if self.config.include.is_empty() {
+ // If there are no include rules, any resource is included (unless excluded)
+ return true;
+ }
+
+ check_rules(&self.config.include, remote, resource)
+ }
+
+ fn check_if_excluded(&self, remote: &str, resource: &ResourceData) -> bool {
+ check_rules(&self.config.exclude, remote, resource)
+ }
+}
+
+fn check_rules(rules: &[FilterRule], remote: &str, resource: &ResourceData) -> bool {
+ for rule in rules {
+ let verdict = match rule {
+ FilterRule::ResourceType(resource_type) => resource.resource_type == *resource_type,
+ FilterRule::ResourcePool(pool) => resource.resource_pool == Some(pool),
+ FilterRule::ResourceId(resource_id) => resource.resource_id == resource_id,
+ FilterRule::Tag(tag) => {
+ if let Some(resource_tags) = resource.tags {
+ resource_tags.contains(tag)
+ } else {
+ false
+ }
+ }
+ FilterRule::Remote(included_remote) => included_remote == remote,
+ };
+
+ if verdict {
+ return true;
+ }
+ }
+
+ false
+}
+
+struct ResourceData<'a> {
+ resource_type: ResourceType,
+ tags: Option<&'a [String]>,
+ resource_pool: Option<&'a String>,
+ resource_id: &'a str,
+}
+
+impl<'a> From<&'a Resource> for ResourceData<'a> {
+ fn from(value: &'a Resource) -> Self {
+ match value {
+ Resource::PveQemu(resource) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: Some(&resource.tags),
+ resource_pool: Some(&resource.pool),
+ resource_id: value.global_id(),
+ },
+ Resource::PveLxc(resource) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: Some(&resource.tags),
+ resource_pool: Some(&resource.pool),
+ resource_id: value.global_id(),
+ },
+ Resource::PveNode(_)
+ | Resource::PveSdn(_)
+ | Resource::PbsNode(_)
+ | Resource::PbsDatastore(_)
+ | Resource::PveStorage(_) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: None,
+ resource_pool: None,
+ resource_id: value.global_id(),
+ },
+ }
+ }
+}
--
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 v2 05/12] views: add tests for view filter implementation
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (3 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 04/12] views: add implementation for view filters Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 06/12] api: resources: list: add support for view-filter parameter Lukas Wagner
` (6 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/views/mod.rs | 3 +
server/src/views/tests.rs | 585 ++++++++++++++++++++++++++++++++++++++
2 files changed, 588 insertions(+)
create mode 100644 server/src/views/tests.rs
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
index 9a2856a4..ea9e6de7 100644
--- a/server/src/views/mod.rs
+++ b/server/src/views/mod.rs
@@ -1 +1,4 @@
pub mod view_filter;
+
+#[cfg(test)]
+mod tests;
diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
new file mode 100644
index 00000000..013b301f
--- /dev/null
+++ b/server/src/views/tests.rs
@@ -0,0 +1,585 @@
+use pdm_api_types::{
+ resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType},
+ views::{FilterRule, ViewFilterConfig},
+};
+
+use super::view_filter::ViewFilter;
+
+fn make_storage_resource(remote: &str, node: &str, storage_name: &str) -> Resource {
+ Resource::PveStorage(PveStorageResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/storage/{node}/{storage_name}"),
+ storage: storage_name.into(),
+ node: node.into(),
+ status: "available".into(),
+ })
+}
+
+fn make_qemu_resource(
+ remote: &str,
+ node: &str,
+ vmid: u32,
+ pool: Option<&str>,
+ tags: &[&str],
+) -> Resource {
+ Resource::PveQemu(PveQemuResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/guest/{vmid}"),
+ node: node.into(),
+ status: "available".into(),
+ cpu: 0.0,
+ maxcpu: 0.0,
+ maxmem: 1024,
+ mem: 512,
+ name: format!("vm-{vmid}"),
+ // TODO: Check the API type - i guess it should be an option?
+ pool: pool.map_or_else(String::new, |a| a.into()),
+ tags: tags.iter().map(|tag| String::from(*tag)).collect(),
+ template: false,
+ uptime: 1337,
+ vmid,
+ })
+}
+
+fn make_lxc_resource(
+ remote: &str,
+ node: &str,
+ vmid: u32,
+ pool: Option<&str>,
+ tags: &[&str],
+) -> Resource {
+ Resource::PveLxc(PveLxcResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/guest/{vmid}"),
+ node: node.into(),
+ status: "available".into(),
+ cpu: 0.0,
+ maxcpu: 0.0,
+ maxmem: 1024,
+ mem: 512,
+ name: format!("vm-{vmid}"),
+ // TODO: Check the API type - i guess it should be an option?
+ pool: pool.map_or_else(String::new, |a| a.into()),
+ tags: tags.iter().map(|tag| String::from(*tag)).collect(),
+ template: false,
+ uptime: 1337,
+ vmid,
+ })
+}
+
+fn run_test(config: ViewFilterConfig, tests: &[((&str, &Resource), bool)]) {
+ let filter = ViewFilter::new(config);
+
+ for ((remote_name, resource), expected) in tests {
+ eprintln!("remote: {remote_name}, resource: {resource:?}");
+ assert_eq!(filter.resource_matches(remote_name, resource), *expected);
+ }
+}
+
+const NODE: &str = "somenode";
+const STORAGE: &str = "somestorage";
+const REMOTE: &str = "someremote";
+
+#[test]
+fn include_remotes() {
+ let config = ViewFilterConfig {
+ id: "only-includes".into(),
+ include: vec![
+ FilterRule::Remote("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ ..Default::default()
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ false,
+ ),
+ ],
+ );
+
+ let filter = ViewFilter::new(config);
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(!filter.can_skip_remote("remote-b"));
+ assert!(filter.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn exclude_remotes() {
+ let config = ViewFilterConfig {
+ id: "only-excludes".into(),
+ exclude: vec![
+ FilterRule::Remote("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ ..Default::default()
+ };
+
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ true,
+ ),
+ ],
+ );
+
+ let filter = ViewFilter::new(config);
+
+ assert!(filter.can_skip_remote("remote-a"));
+ assert!(filter.can_skip_remote("remote-b"));
+ assert!(!filter.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn include_exclude_remotes() {
+ let config = ViewFilterConfig {
+ id: "both".into(),
+ include: vec![
+ FilterRule::Remote("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ exclude: vec![
+ FilterRule::Remote("remote-b".into()),
+ FilterRule::Remote("remote-c".into()),
+ ],
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ false,
+ ),
+ ],
+ );
+
+ let filter = ViewFilter::new(config);
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(filter.can_skip_remote("remote-b"));
+ assert!(filter.can_skip_remote("remote-c"));
+ assert!(filter.can_skip_remote("remote-d"));
+}
+
+#[test]
+fn empty_config() {
+ let config = ViewFilterConfig {
+ id: "empty".into(),
+ ..Default::default()
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ ],
+ );
+
+ let filter = ViewFilter::new(config);
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(!filter.can_skip_remote("remote-b"));
+ assert!(!filter.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn include_type() {
+ run_test(
+ ViewFilterConfig {
+ id: "include-resource-type".into(),
+ include: vec![
+ FilterRule::ResourceType(ResourceType::PveStorage),
+ FilterRule::ResourceType(ResourceType::PveQemu),
+ ],
+ ..Default::default()
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn exclude_type() {
+ run_test(
+ ViewFilterConfig {
+ id: "exclude-resource-type".into(),
+ exclude: vec![
+ FilterRule::ResourceType(ResourceType::PveStorage),
+ FilterRule::ResourceType(ResourceType::PveQemu),
+ ],
+ ..Default::default()
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ false,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ false,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ true,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_type() {
+ run_test(
+ ViewFilterConfig {
+ id: "exclude-resource-type".into(),
+ include: vec![FilterRule::ResourceType(ResourceType::PveQemu)],
+ exclude: vec![FilterRule::ResourceType(ResourceType::PveStorage)],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ false,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_tags() {
+ run_test(
+ ViewFilterConfig {
+ id: "include-tags".into(),
+ include: vec![
+ FilterRule::Tag("tag1".to_string()),
+ FilterRule::Tag("tag2".to_string()),
+ ],
+ exclude: vec![FilterRule::Tag("tag3".to_string())],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ // only qemu/lxc can match tags for now
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_qemu_resource(REMOTE, NODE, 100, None, &["tag1", "tag3"]),
+ ),
+ // because tag3 is excluded
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 101, None, &["tag1"]),
+ ),
+ // matches since it's in the includes
+ true,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 102, None, &["tag4"]),
+ ),
+ // Not in includes, can never match
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_resource_pool() {
+ run_test(
+ ViewFilterConfig {
+ id: "pools".into(),
+ include: vec![
+ FilterRule::ResourcePool("pool1".to_string()),
+ FilterRule::ResourcePool("pool2".to_string()),
+ ],
+ exclude: vec![FilterRule::ResourcePool("pool2".to_string())],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ // only qemu/lxc can match pools for now
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_qemu_resource(REMOTE, NODE, 100, Some("pool2"), &[]),
+ ),
+ // because pool2 is excluded (takes precedence over includes)
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 101, Some("pool1"), &[]),
+ ),
+ // matches since it's in the includes
+ true,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 102, Some("pool4"), &[]),
+ ),
+ // Not in includes, can never match
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_resource_id() {
+ run_test(
+ ViewFilterConfig {
+ id: "resource-id".into(),
+ include: vec![
+ FilterRule::ResourceId(format!("remote/{REMOTE}/guest/100")),
+ FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/{STORAGE}")),
+ ],
+ exclude: vec![
+ FilterRule::ResourceId(format!("remote/{REMOTE}/guest/101")),
+ FilterRule::ResourceId("remote/otherremote/guest/101".to_string()),
+ FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/otherstorage")),
+ ],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 102, None, &[])),
+ false,
+ ),
+ (
+ (
+ "otherremote",
+ &make_lxc_resource("otherremote", NODE, 101, None, &[]),
+ ),
+ false,
+ ),
+ (
+ (
+ "yetanoterremote",
+ &make_lxc_resource("yetanotherremote", NODE, 104, None, &[]),
+ ),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn node_included() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "both".into(),
+
+ include: vec![
+ FilterRule::Remote("remote-a".to_string()),
+ FilterRule::ResourceId("remote/someremote/node/test".to_string()),
+ ],
+ exclude: vec![FilterRule::Remote("remote-b".to_string())],
+ });
+
+ assert!(filter.is_node_included("remote-a", "somenode"));
+ assert!(filter.is_node_included("remote-a", "somenode2"));
+ assert!(!filter.is_node_included("remote-b", "somenode"));
+ assert!(!filter.is_node_included("remote-b", "somenode2"));
+ assert!(filter.is_node_included("someremote", "test"));
+
+ assert_eq!(filter.name(), "both");
+}
+
+#[test]
+fn can_skip_remote_if_excluded() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![],
+ exclude: vec![FilterRule::Remote("remote-b".to_string())],
+ });
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(filter.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_if_included() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::Remote("remote-b".to_string())],
+ exclude: vec![],
+ });
+
+ assert!(!filter.can_skip_remote("remote-b"));
+ assert!(filter.can_skip_remote("remote-a"));
+}
+
+#[test]
+fn can_skip_remote_cannot_skip_if_any_other_include() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![
+ FilterRule::Remote("remote-b".to_string()),
+ FilterRule::ResourceId("resource/remote-a/guest/100".to_string()),
+ ],
+ exclude: vec![],
+ });
+
+ assert!(!filter.can_skip_remote("remote-b"));
+ assert!(!filter.can_skip_remote("remote-a"));
+}
+
+#[test]
+fn can_skip_remote_explicit_remote_exclude() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::ResourceId(
+ "resource/remote-a/guest/100".to_string(),
+ )],
+ exclude: vec![FilterRule::Remote("remote-a".to_string())],
+ });
+
+ assert!(filter.can_skip_remote("remote-a"));
+}
+
+#[test]
+fn can_skip_remote_with_empty_config() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![],
+ exclude: vec![],
+ });
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(!filter.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_with_no_remote_includes() {
+ let filter = ViewFilter::new(ViewFilterConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::ResourceId(
+ "resource/remote-a/guest/100".to_string(),
+ )],
+ exclude: vec![],
+ });
+
+ assert!(!filter.can_skip_remote("remote-a"));
+ assert!(!filter.can_skip_remote("remote-b"));
+}
--
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 v2 06/12] api: resources: list: add support for view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (4 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 05/12] views: add tests for view filter implementation Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 07/12] api: resources: top entities: " Lukas Wagner
` (5 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
A view filter allows one to get filtered subset of all resources, based
on filter rules defined in a config file. View filters integrate with
the permission system - if a user has permissions on
/view/{view-filter-id}, then these privileges are transitively applied
to all resources which are matched by the rules. All other permission
checks are replaced if requesting data through a view filter.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/resources.rs | 56 ++++++++++++++++++++++++++++++------
server/src/resource_cache.rs | 3 +-
2 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 81c9d9ae..6feda45b 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -18,7 +18,7 @@ use pdm_api_types::resource::{
use pdm_api_types::subscription::{
NodeSubscriptionInfo, RemoteSubscriptionState, RemoteSubscriptions, SubscriptionLevel,
};
-use pdm_api_types::{Authid, PRIV_RESOURCE_AUDIT};
+use pdm_api_types::{Authid, PRIV_RESOURCE_AUDIT, VIEW_FILTER_ID_SCHEMA};
use pdm_search::{Search, SearchTerm};
use proxmox_access_control::CachedUserInfo;
use proxmox_router::{
@@ -30,8 +30,8 @@ use proxmox_sortable_macro::sortable;
use proxmox_subscription::SubscriptionStatus;
use pve_api_types::{ClusterResource, ClusterResourceType};
-use crate::connection;
use crate::metric_collection::top_entities;
+use crate::{connection, views};
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
@@ -221,6 +221,10 @@ impl From<RemoteWithResources> for RemoteResources {
type: ResourceType,
optional: true,
},
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
}
},
returns: {
@@ -236,10 +240,17 @@ pub async fn get_resources(
max_age: u64,
resource_type: Option<ResourceType>,
search: Option<String>,
+ view_filter: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<RemoteResources>, Error> {
- let remotes_with_resources =
- get_resources_impl(max_age, search, resource_type, Some(rpcenv)).await?;
+ let remotes_with_resources = get_resources_impl(
+ max_age,
+ search,
+ resource_type,
+ view_filter.as_deref(),
+ Some(rpcenv),
+ )
+ .await?;
let resources = remotes_with_resources.into_iter().map(Into::into).collect();
Ok(resources)
}
@@ -276,6 +287,7 @@ pub(crate) async fn get_resources_impl(
max_age: u64,
search: Option<String>,
resource_type: Option<ResourceType>,
+ view_filter: Option<&str>,
rpcenv: Option<&mut dyn RpcEnvironment>,
) -> Result<Vec<RemoteWithResources>, Error> {
let user_info = CachedUserInfo::new()?;
@@ -285,9 +297,15 @@ pub(crate) async fn get_resources_impl(
.get_auth_id()
.ok_or_else(|| format_err!("no authid available"))?
.parse()?;
- if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_AUDIT)? {
+
+ // NOTE: Assumption is that the regular permission check is completely replaced by a check
+ // on the view ACL object *if* a view parameter is passed.
+ if let Some(view_filter) = &view_filter {
+ user_info.check_privs(&auth_id, &["view", view_filter], PRIV_RESOURCE_AUDIT, false)?;
+ } else if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_AUDIT)? {
http_bail!(UNAUTHORIZED, "user has no access to resources");
}
+
opt_auth_id = Some(auth_id);
}
@@ -296,12 +314,24 @@ pub(crate) async fn get_resources_impl(
let filters = search.map(Search::from).unwrap_or_default();
+ let view_filter = view_filter
+ .map(|filter_name| views::view_filter::get_view_filter(&filter_name))
+ .transpose()?;
+
let remotes_only = is_remotes_only(&filters);
for (remote_name, remote) in remotes_config {
if let Some(ref auth_id) = opt_auth_id {
- let remote_privs = user_info.lookup_privs(auth_id, &["resource", &remote_name]);
- if remote_privs & PRIV_RESOURCE_AUDIT == 0 {
+ if view_filter.is_none() {
+ let remote_privs = user_info.lookup_privs(auth_id, &["resource", &remote_name]);
+ if remote_privs & PRIV_RESOURCE_AUDIT == 0 {
+ continue;
+ }
+ }
+ }
+
+ if let Some(view_filter) = &view_filter {
+ if view_filter.can_skip_remote(&remote_name) {
continue;
}
}
@@ -374,6 +404,15 @@ pub(crate) async fn get_resources_impl(
}
}
+ if let Some(filter) = &view_filter {
+ remote_resources.retain_mut(|r| {
+ r.resources
+ .retain(|resource| filter.resource_matches(&r.remote_name, resource));
+
+ !r.resources.is_empty()
+ });
+ }
+
Ok(remote_resources)
}
@@ -405,7 +444,8 @@ pub async fn get_status(
max_age: u64,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<ResourcesStatus, Error> {
- let remotes_with_resources = get_resources_impl(max_age, None, None, Some(rpcenv)).await?;
+ let remotes_with_resources =
+ get_resources_impl(max_age, None, None, None, Some(rpcenv)).await?;
let mut counts = ResourcesStatus::default();
for remote_with_resources in remotes_with_resources {
if let Some(err) = remote_with_resources.error {
diff --git a/server/src/resource_cache.rs b/server/src/resource_cache.rs
index aa20c54e..dc3cbeaf 100644
--- a/server/src/resource_cache.rs
+++ b/server/src/resource_cache.rs
@@ -21,7 +21,8 @@ pub fn start_task() {
async fn resource_caching_task() -> Result<(), Error> {
loop {
if let Err(err) =
- crate::api::resources::get_resources_impl(METRIC_POLL_INTERVALL, None, None, None).await
+ crate::api::resources::get_resources_impl(METRIC_POLL_INTERVALL, None, None, None, None)
+ .await
{
log::error!("could not update resource cache: {err}");
}
--
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 v2 07/12] api: resources: top entities: add support for view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (5 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 06/12] api: resources: list: add support for view-filter parameter Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 08/12] api: resources: status: " Lukas Wagner
` (4 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
A view filter allows one to get filtered subset of all resources, based
on filter rules defined in a config file. View filters integrate with
the permission system - if a user has permissions on
/view/{view-filter-id}, then these privileges are transitively applied
to all resources which are matched by the rules. All other permission
checks are replaced if requesting data through a view filter.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/resources.rs | 41 ++++++++++++++++++--
server/src/metric_collection/top_entities.rs | 5 +++
2 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 6feda45b..ea76d81b 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -623,7 +623,11 @@ pub async fn get_subscription_status(
"timeframe": {
type: RrdTimeframe,
optional: true,
- }
+ },
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
}
},
access: {
@@ -636,6 +640,7 @@ pub async fn get_subscription_status(
/// Returns the top X entities regarding the chosen type
async fn get_top_entities(
timeframe: Option<RrdTimeframe>,
+ view_filter: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<TopEntities, Error> {
let user_info = CachedUserInfo::new()?;
@@ -644,17 +649,45 @@ async fn get_top_entities(
.ok_or_else(|| format_err!("no authid available"))?
.parse()?;
- if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_AUDIT)? {
+ if let Some(view_filter) = &view_filter {
+ user_info.check_privs(&auth_id, &["view", view_filter], PRIV_RESOURCE_AUDIT, false)?;
+ } else if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_AUDIT)? {
http_bail!(FORBIDDEN, "user has no access to resources");
}
+ let view_filter = view_filter
+ .map(|a| views::view_filter::get_view_filter(&a))
+ .transpose()?;
+
let (remotes_config, _) = pdm_config::remotes::config()?;
+
let check_remote_privs = |remote_name: &str| {
- user_info.lookup_privs(&auth_id, &["resource", remote_name]) & PRIV_RESOURCE_AUDIT != 0
+ if let Some(view_filter) = &view_filter {
+ // if `include-remote` or `exclude-remote` are used we can limit the
+ // number of remotes to check.
+ !view_filter.can_skip_remote(remote_name)
+ } else {
+ user_info.lookup_privs(&auth_id, &["resource", remote_name]) & PRIV_RESOURCE_AUDIT != 0
+ }
+ };
+
+ let is_resource_included = |remote: &str, resource: &Resource| {
+ if let Some(view_filter) = &view_filter {
+ view_filter.resource_matches(remote, resource)
+ } else {
+ true
+ }
};
let timeframe = timeframe.unwrap_or(RrdTimeframe::Day);
- let res = top_entities::calculate_top(&remotes_config, timeframe, 10, check_remote_privs);
+ let res = top_entities::calculate_top(
+ &remotes_config,
+ timeframe,
+ 10,
+ check_remote_privs,
+ is_resource_included,
+ );
+
Ok(res)
}
diff --git a/server/src/metric_collection/top_entities.rs b/server/src/metric_collection/top_entities.rs
index 73a3e63f..a91d586c 100644
--- a/server/src/metric_collection/top_entities.rs
+++ b/server/src/metric_collection/top_entities.rs
@@ -37,6 +37,7 @@ pub fn calculate_top(
timeframe: proxmox_rrd_api_types::RrdTimeframe,
num: usize,
check_remote_privs: impl Fn(&str) -> bool,
+ is_resource_included: impl Fn(&str, &Resource) -> bool,
) -> TopEntities {
let mut guest_cpu = Vec::new();
let mut node_cpu = Vec::new();
@@ -51,6 +52,10 @@ pub fn calculate_top(
crate::api::resources::get_cached_resources(remote_name, i64::MAX as u64)
{
for res in data.resources {
+ if !is_resource_included(remote_name, &res) {
+ continue;
+ }
+
let id = res.id().to_string();
let name = format!("pve/{remote_name}/{id}");
match &res {
--
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 v2 08/12] api: resources: status: add support for view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (6 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 07/12] api: resources: top entities: " Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 09/12] api: subscription " Lukas Wagner
` (3 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
A view filter allows one to get filtered subset of all resources, based
on filter rules defined in a config file. View filters integrate with
the permission system - if a user has permissions on
/view/{view-filter-id}, then these privileges are transitively applied
to all resources which are matched by the rules. All other permission
checks are replaced if requesting data through a view filter.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/resources.rs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index ea76d81b..f718ce3e 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -429,6 +429,10 @@ pub(crate) async fn get_resources_impl(
default: 30,
optional: true,
},
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
}
},
returns: {
@@ -442,10 +446,11 @@ pub(crate) async fn get_resources_impl(
/// Return the amount of configured/seen resources by type
pub async fn get_status(
max_age: u64,
+ view_filter: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<ResourcesStatus, Error> {
let remotes_with_resources =
- get_resources_impl(max_age, None, None, None, Some(rpcenv)).await?;
+ get_resources_impl(max_age, None, None, view_filter.as_deref(), Some(rpcenv)).await?;
let mut counts = ResourcesStatus::default();
for remote_with_resources in remotes_with_resources {
if let Some(err) = remote_with_resources.error {
--
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 v2 09/12] api: subscription status: add support for view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (7 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 08/12] api: resources: status: " Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 10/12] api: remote-tasks: " Lukas Wagner
` (2 subsequent siblings)
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
A view filter allows one to get filtered subset of all resources, based
on filter rules defined in a config file. View filters integrate with
the permission system - if a user has permissions on
/view/{view-filter-id}, then these privileges are transitively applied
to all resources which are matched by the rules. All other permission
checks are replaced if requesting data through a view filter.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/resources.rs | 66 ++++++++++++++++++++++++++++++-------
1 file changed, 54 insertions(+), 12 deletions(-)
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index f718ce3e..db1e2c7c 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -552,6 +552,10 @@ pub async fn get_status(
default: false,
description: "If true, includes subscription information per node (with enough privileges)",
},
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
},
},
returns: {
@@ -566,6 +570,7 @@ pub async fn get_status(
pub async fn get_subscription_status(
max_age: u64,
verbose: bool,
+ view_filter: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<RemoteSubscriptions>, Error> {
let (remotes_config, _) = pdm_config::remotes::config()?;
@@ -574,9 +579,19 @@ pub async fn get_subscription_status(
let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
- let allow_all = user_info
- .check_privs(&auth_id, &["resources"], PRIV_RESOURCE_AUDIT, false)
- .is_ok();
+
+ let allow_all = if let Some(view_filter) = &view_filter {
+ user_info.check_privs(&auth_id, &["view", view_filter], PRIV_RESOURCE_AUDIT, false)?;
+ false
+ } else {
+ user_info
+ .check_privs(&auth_id, &["resources"], PRIV_RESOURCE_AUDIT, false)
+ .is_ok()
+ };
+
+ let view_filter = view_filter
+ .map(|filter_name| views::view_filter::get_view_filter(&filter_name))
+ .transpose()?;
let check_priv = |remote_name: &str| -> bool {
user_info
@@ -590,35 +605,62 @@ pub async fn get_subscription_status(
};
for (remote_name, remote) in remotes_config {
- if !allow_all && !check_priv(&remote_name) {
+ if let Some(filter) = &view_filter {
+ if filter.can_skip_remote(&remote_name) {
+ continue;
+ }
+ } else if !allow_all && !check_priv(&remote_name) {
continue;
}
+ let view_filter_clone = view_filter.clone();
+
let future = async move {
let (node_status, error) =
match get_subscription_info_for_remote(&remote, max_age).await {
- Ok(node_status) => (Some(node_status), None),
+ Ok(mut node_status) => {
+ node_status.retain(|node, _| {
+ if let Some(filter) = &view_filter_clone {
+ filter.is_node_included(&remote.id, node)
+ } else {
+ true
+ }
+ });
+ (Some(node_status), None)
+ }
Err(error) => (None, Some(error.to_string())),
};
- let mut state = RemoteSubscriptionState::Unknown;
+ let state = if let Some(node_status) = &node_status {
+ if error.is_some() && view_filter_clone.is_some() {
+ // Don't leak the existence of failed remotes, since we cannot apply
+ // view-filters here.
+ return None;
+ }
- if let Some(node_status) = &node_status {
- state = map_node_subscription_list_to_state(node_status);
- }
+ if node_status.is_empty() {
+ return None;
+ }
- RemoteSubscriptions {
+ map_node_subscription_list_to_state(node_status)
+ } else {
+ RemoteSubscriptionState::Unknown
+ };
+
+ Some(RemoteSubscriptions {
remote: remote_name,
error,
state,
node_status: if verbose { node_status } else { None },
- }
+ })
};
futures.push(future);
}
- Ok(join_all(futures).await)
+ let status = join_all(futures).await.into_iter().flatten().collect();
+
+ Ok(status)
}
// FIXME: make timeframe and count 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 v2 10/12] api: remote-tasks: add support for view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (8 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 09/12] api: subscription " Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 11/12] pdm-client: resource list: add " Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 12/12] pdm-client: top entities: " Lukas Wagner
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
A view filter allows one to get filtered subset of all resources, based
on filter rules defined in a config file. View filters integrate with
the permission system - if a user has permissions on
/view/{view-filter-id}, then these privileges are transitively applied
to all resources which are matched by the rules. All other permission
checks are replaced if requesting data through a view filter.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/remote_tasks.rs | 36 +++++++++++++++++++++++++++----
server/src/remote_tasks/mod.rs | 39 +++++++++++++++++++++++-----------
2 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/server/src/api/remote_tasks.rs b/server/src/api/remote_tasks.rs
index 7b97b9cd..4a68c5d9 100644
--- a/server/src/api/remote_tasks.rs
+++ b/server/src/api/remote_tasks.rs
@@ -4,9 +4,10 @@ use anyhow::Error;
use pdm_api_types::{
remotes::REMOTE_ID_SCHEMA, RemoteUpid, TaskCount, TaskFilters, TaskListItem, TaskStateType,
- TaskStatistics,
+ TaskStatistics, PRIV_RESOURCE_AUDIT, VIEW_FILTER_ID_SCHEMA,
};
-use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
+use proxmox_access_control::CachedUserInfo;
+use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
@@ -41,6 +42,11 @@ const SUBDIRS: SubdirMap = &sorted!([
schema: REMOTE_ID_SCHEMA,
optional: true,
},
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
+
},
},
)]
@@ -48,8 +54,17 @@ const SUBDIRS: SubdirMap = &sorted!([
async fn list_tasks(
filters: TaskFilters,
remote: Option<String>,
+ view_filter: Option<String>,
+ rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<TaskListItem>, Error> {
- let tasks = remote_tasks::get_tasks(filters, remote).await?;
+ let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+ let user_info = CachedUserInfo::new()?;
+
+ if let Some(view_filter) = &view_filter {
+ user_info.check_privs(&auth_id, &["view", view_filter], PRIV_RESOURCE_AUDIT, false)?;
+ }
+
+ let tasks = remote_tasks::get_tasks(filters, remote, view_filter).await?;
Ok(tasks)
}
@@ -70,6 +85,10 @@ async fn list_tasks(
schema: REMOTE_ID_SCHEMA,
optional: true,
},
+ "view-filter": {
+ schema: VIEW_FILTER_ID_SCHEMA,
+ optional: true,
+ },
},
},
)]
@@ -77,8 +96,17 @@ async fn list_tasks(
async fn task_statistics(
filters: TaskFilters,
remote: Option<String>,
+ view_filter: Option<String>,
+ rpcenv: &mut dyn RpcEnvironment,
) -> Result<TaskStatistics, Error> {
- let tasks = remote_tasks::get_tasks(filters, remote).await?;
+ let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+ let user_info = CachedUserInfo::new()?;
+
+ if let Some(view_filter) = &view_filter {
+ user_info.check_privs(&auth_id, &["view", view_filter], PRIV_RESOURCE_AUDIT, false)?;
+ }
+
+ let tasks = remote_tasks::get_tasks(filters, remote, view_filter).await?;
let mut by_type: HashMap<String, TaskCount> = HashMap::new();
let mut by_remote: HashMap<String, TaskCount> = HashMap::new();
diff --git a/server/src/remote_tasks/mod.rs b/server/src/remote_tasks/mod.rs
index c4939742..4d93d9b8 100644
--- a/server/src/remote_tasks/mod.rs
+++ b/server/src/remote_tasks/mod.rs
@@ -9,6 +9,8 @@ pub mod task_cache;
use task_cache::{GetTasks, TaskCache, TaskCacheItem};
+use crate::views;
+
/// Base directory for the remote task cache.
pub const REMOTE_TASKS_DIR: &str = concat!(pdm_buildcfg::PDM_CACHE_DIR_M!(), "/remote-tasks");
@@ -29,7 +31,12 @@ const NUMBER_OF_UNCOMPRESSED_FILES: u32 = 2;
pub async fn get_tasks(
filters: TaskFilters,
remote_filter: Option<String>,
+ view_filter: Option<String>,
) -> Result<Vec<TaskListItem>, Error> {
+ let view_filter = view_filter
+ .map(|filter_name| views::view_filter::get_view_filter(&filter_name))
+ .transpose()?;
+
tokio::task::spawn_blocking(move || {
let cache = get_cache()?.read()?;
@@ -52,21 +59,29 @@ pub async fn get_tasks(
return None;
}
}
+
// TODO: Handle PBS tasks
let pve_upid: Result<PveUpid, Error> = task.upid.upid.parse();
match pve_upid {
- Ok(pve_upid) => Some(TaskListItem {
- upid: task.upid.to_string(),
- node: pve_upid.node,
- pid: pve_upid.pid as i64,
- pstart: pve_upid.pstart,
- starttime: pve_upid.starttime,
- worker_type: pve_upid.worker_type,
- worker_id: None,
- user: pve_upid.auth_id,
- endtime: task.endtime,
- status: task.status,
- }),
+ Ok(pve_upid) => {
+ if let Some(view_filter) = &view_filter {
+ if !view_filter.is_node_included(task.upid.remote(), &pve_upid.node) {
+ return None;
+ }
+ }
+ Some(TaskListItem {
+ upid: task.upid.to_string(),
+ node: pve_upid.node,
+ pid: pve_upid.pid as i64,
+ pstart: pve_upid.pstart,
+ starttime: pve_upid.starttime,
+ worker_type: pve_upid.worker_type,
+ worker_id: None,
+ user: pve_upid.auth_id,
+ endtime: task.endtime,
+ status: task.status,
+ })
+ }
Err(err) => {
log::error!("could not parse UPID: {err:#}");
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* [pdm-devel] [PATCH datacenter-manager v2 11/12] pdm-client: resource list: add view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (9 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 10/12] api: remote-tasks: " Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 12/12] pdm-client: top entities: " Lukas Wagner
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
cli/client/src/resources.rs | 2 +-
lib/pdm-client/src/lib.rs | 9 ++++++++-
ui/src/sdn/zone_tree.rs | 2 +-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/cli/client/src/resources.rs b/cli/client/src/resources.rs
index dbf9f265..bc3a98ba 100644
--- a/cli/client/src/resources.rs
+++ b/cli/client/src/resources.rs
@@ -31,7 +31,7 @@ pub fn cli() -> CommandLineInterface {
)]
/// List all the remotes this instance is managing.
async fn get_resources(max_age: Option<u64>) -> Result<(), Error> {
- let mut resources = client()?.resources(max_age).await?;
+ let mut resources = client()?.resources(max_age, None).await?;
let output_format = env().format_args.output_format;
if output_format == OutputFormat::Text {
if resources.is_empty() {
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 0cab7691..7102ee05 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -863,9 +863,14 @@ impl<T: HttpApiClient> PdmClient<T> {
Ok(self.0.get(&path).await?.expect_json()?.data)
}
- pub async fn resources(&self, max_age: Option<u64>) -> Result<Vec<RemoteResources>, Error> {
+ pub async fn resources(
+ &self,
+ max_age: Option<u64>,
+ view_filter: Option<&str>,
+ ) -> Result<Vec<RemoteResources>, Error> {
let path = ApiPathBuilder::new("/api2/extjs/resources/list")
.maybe_arg("max-age", &max_age)
+ .maybe_arg("view-filter", &view_filter)
.build();
Ok(self.0.get(&path).await?.expect_json()?.data)
}
@@ -874,10 +879,12 @@ impl<T: HttpApiClient> PdmClient<T> {
&self,
max_age: Option<u64>,
resource_type: ResourceType,
+ view_filter: Option<&str>,
) -> Result<Vec<RemoteResources>, Error> {
let path = ApiPathBuilder::new("/api2/extjs/resources/list")
.maybe_arg("max-age", &max_age)
.arg("resource-type", resource_type)
+ .maybe_arg("view-filter", &view_filter)
.build();
Ok(self.0.get(&path).await?.expect_json()?.data)
diff --git a/ui/src/sdn/zone_tree.rs b/ui/src/sdn/zone_tree.rs
index 1f534388..7475483d 100644
--- a/ui/src/sdn/zone_tree.rs
+++ b/ui/src/sdn/zone_tree.rs
@@ -281,7 +281,7 @@ impl LoadableComponent for ZoneTreeComponent {
Box::pin(async move {
let client = pdm_client();
let remote_resources = client
- .resources_by_type(None, ResourceType::PveSdnZone)
+ .resources_by_type(None, ResourceType::PveSdnZone, None)
.await?;
link.send_message(Self::Message::LoadFinished(remote_resources));
--
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 v2 12/12] pdm-client: top entities: add view-filter parameter
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend implementation for view filters Lukas Wagner
` (10 preceding siblings ...)
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 11/12] pdm-client: resource list: add " Lukas Wagner
@ 2025-11-03 12:35 ` Lukas Wagner
11 siblings, 0 replies; 13+ messages in thread
From: Lukas Wagner @ 2025-11-03 12:35 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
lib/pdm-client/src/lib.rs | 10 +++++++---
ui/src/dashboard/mod.rs | 2 +-
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 7102ee05..c9ed18cc 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -956,9 +956,13 @@ impl<T: HttpApiClient> PdmClient<T> {
Ok(self.0.get(&path).await?.expect_json()?.data)
}
- pub async fn get_top_entities(&self) -> Result<TopEntities, Error> {
- let path = "/api2/extjs/resources/top-entities";
- Ok(self.0.get(path).await?.expect_json()?.data)
+ pub async fn get_top_entities(&self, view_filter: Option<&str>) -> Result<TopEntities, Error> {
+ let builder = ApiPathBuilder::new("/api2/extjs/resources/top-entities".to_string())
+ .maybe_arg("view-filter", &view_filter);
+
+ let path = builder.build();
+
+ Ok(self.0.get(&path).await?.expect_json()?.data)
}
pub async fn pve_node_status(&self, remote: &str, node: &str) -> Result<NodeStatus, Error> {
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 07d5cd99..ce64d847 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -319,7 +319,7 @@ impl PdmDashboard {
let top_entities_future = {
let link = link.clone();
async move {
- let res = client.get_top_entities().await;
+ let res = client.get_top_entities(None).await;
link.send_message(Msg::LoadingFinished(LoadingResult::TopEntities(res)));
}
};
--
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