public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2 04/12] views: add implementation for view filters
Date: Mon,  3 Nov 2025 13:35:13 +0100	[thread overview]
Message-ID: <20251103123521.266258-5-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251103123521.266258-1-l.wagner@proxmox.com>

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


  parent reply	other threads:[~2025-11-03 12:35 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend " 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 ` [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 [this message]
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 ` [pdm-devel] [PATCH datacenter-manager v2 06/12] api: resources: list: add support for view-filter parameter Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 07/12] api: resources: top entities: " Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 08/12] api: resources: status: " Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 09/12] api: subscription " Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 10/12] api: remote-tasks: " 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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251103123521.266258-5-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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