From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 9937F1FF184 for ; Thu, 20 Nov 2025 13:56:28 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 66A687248; Thu, 20 Nov 2025 13:56:34 +0100 (CET) From: Hannes Laimer To: pdm-devel@lists.proxmox.com Date: Thu, 20 Nov 2025 13:55:48 +0100 Message-ID: <20251120125552.366901-9-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251120125552.366901-1-h.laimer@proxmox.com> References: <20251120125552.366901-1-h.laimer@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1763643325233 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.048 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH proxmox-yew-comp v4 4/4] firewall: add rules table X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" Displays the list of firewall rules, this is read-only currently, so it doesn't include any buttons for editing or adding rules. Signed-off-by: Hannes Laimer --- src/firewall/mod.rs | 3 + src/firewall/rules.rs | 264 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 3 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 src/firewall/rules.rs diff --git a/src/firewall/mod.rs b/src/firewall/mod.rs index 379b958..8cc4977 100644 --- a/src/firewall/mod.rs +++ b/src/firewall/mod.rs @@ -4,5 +4,8 @@ pub use context::FirewallContext; mod options_edit; pub use options_edit::EditFirewallOptions; +mod rules; +pub use rules::FirewallRules; + mod log_ratelimit_field; pub use log_ratelimit_field::LogRatelimitField; diff --git a/src/firewall/rules.rs b/src/firewall/rules.rs new file mode 100644 index 0000000..f5cbe0a --- /dev/null +++ b/src/firewall/rules.rs @@ -0,0 +1,264 @@ +use std::rc::Rc; + +use yew::html::{IntoEventCallback, IntoPropValue}; +use yew::virtual_dom::{Key, VComp, VNode}; + +use pwt::prelude::*; +use pwt::state::{Loader, LoaderState, SharedStateObserver, Store}; +use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader}; +use pwt::widget::{Container, Fa}; +use pwt_macros::builder; + +use super::context::FirewallContext; + +/// Properties for displaying firewall rules in a read-only table. +/// +/// Displays the list of firewall rules for a given context (cluster, node, or guest). +/// This is read-only currently, so it doesn't include any buttons for editing or adding rules. +#[derive(Clone, PartialEq, Properties)] +#[builder] +pub struct FirewallRules { + /// The firewall context specifying which level to display rules for (cluster, node, or guest). + #[builder(IntoPropValue, into_prop_value)] + pub context: FirewallContext, + + /// Callback invoked when the component is closed. + #[builder_cb(IntoEventCallback, into_event_callback, ())] + #[prop_or_default] + pub on_close: Option>, +} + +impl FirewallRules { + /// Creates a new `FirewallRules` for displaying cluster-level firewall rules. + /// + /// # Arguments + /// + /// * `remote` - The remote identifier for the PVE cluster. + pub fn cluster(remote: impl Into) -> Self { + yew::props!(Self { + context: FirewallContext::cluster(remote), + }) + } + + /// Creates a new `FirewallRules` for displaying node-level firewall rules. + /// + /// # Arguments + /// + /// * `remote` - The remote identifier for the PVE cluster. + /// * `node` - The node identifier. + pub fn node(remote: impl Into, node: impl Into) -> Self { + yew::props!(Self { + context: FirewallContext::node(remote, node), + }) + } + + /// Creates a new `FirewallRules` for displaying guest-level firewall rules. + /// + /// # Arguments + /// + /// * `remote` - The remote identifier for the PVE cluster. + /// * `node` - The node identifier where the guest is located. + /// * `vmid` - The virtual machine ID. + /// * `vmtype` - The type of guest ("lxc" or "qemu"). + pub fn guest( + remote: impl Into, + node: impl Into, + vmid: u64, + vmtype: impl Into, + ) -> Self { + yew::props!(Self { + context: FirewallContext::guest(remote, node, vmid, vmtype), + }) + } +} + +pub enum FirewallMsg { + DataChange, +} + +#[doc(hidden)] +pub struct ProxmoxFirewallRules { + store: Store, + loader: Loader>, + _listener: SharedStateObserver>>, + columns: Rc>>, +} + +fn pill(text: impl Into) -> Container { + Container::from_tag("span") + .style("display", "inline-block") + .style("margin", "0 1px") + .style("background-color", "var(--pwt-color-neutral-container)") + .style("color", "var(--pwt-color-on-neutral-container)") + .style("border-radius", "var(--pwt-button-corner-shape)") + .style("padding-inline", "var(--pwt-spacer-2)") + .with_child(text.into()) +} + +fn format_firewall_rule(rule: &pve_api_types::ListFirewallRules) -> Html { + let mut parts: Vec = Vec::new(); + + if let Some(iface) = &rule.iface { + parts.push(pill(format!("iface: {iface}")).into()); + } + + if let Some(macro_name) = &rule.r#macro { + parts.push(pill(format!("macro: {macro_name}")).into()); + } + + if let Some(proto) = &rule.proto { + let mut proto_str = proto.to_uppercase(); + if matches!(proto.as_str(), "icmp" | "icmpv6" | "ipv6-icmp") { + if let Some(icmp_type) = &rule.icmp_type { + proto_str = format!("{proto_str}, {icmp_type}"); + } + } + parts.push(pill(format!("proto: {proto_str}")).into()); + } + + let mut push_host_port = + |host: &Option, port: &Option, label: &str| match (host, port) { + (Some(h), Some(p)) => parts.push(pill(format!("{label}: {h}, port: {p}")).into()), + (Some(h), None) => parts.push(pill(format!("{label}: {h}")).into()), + (None, Some(p)) => parts.push(pill(format!("{label}: any, port: {p}")).into()), + _ => {} + }; + + push_host_port(&rule.source, &rule.sport, "src"); + push_host_port(&rule.dest, &rule.dport, "dest"); + + if parts.is_empty() { + return "-".into(); + } + + parts + .into_iter() + .enumerate() + .flat_map(|(i, part)| { + if i == 0 { + vec![part] + } else { + vec![" ".into(), part] + } + }) + .collect::() +} + +impl ProxmoxFirewallRules { + fn update_data(&mut self) { + if let Some(Ok(data)) = &self.loader.read().data { + self.store.set_data((**data).clone()); + } + } + + fn build_columns() -> Rc>> { + Rc::new(vec![ + DataTableColumn::new("") + .width("30px") + .justify("right") + .show_menu(false) + .resizable(false) + .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.pos}) + .into(), + DataTableColumn::new(tr!("On")) + .width("40px") + .justify("center") + .resizable(false) + .render( + |rule: &pve_api_types::ListFirewallRules| match rule.enable { + Some(1) => Fa::new("check").into(), + Some(0) | None => Fa::new("minus").into(), + _ => "-".into(), + }, + ) + .into(), + DataTableColumn::new(tr!("Type")) + .width("80px") + .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.ty}) + .into(), + DataTableColumn::new(tr!("Action")) + .width("100px") + .render(|rule: &pve_api_types::ListFirewallRules| html! {&rule.action}) + .into(), + DataTableColumn::new(tr!("Rule")) + .flex(1) + .render(|rule: &pve_api_types::ListFirewallRules| format_firewall_rule(rule)) + .into(), + DataTableColumn::new(tr!("Comment")) + .width("150px") + .render(|rule: &pve_api_types::ListFirewallRules| { + rule.comment.as_deref().unwrap_or("-").into() + }) + .into(), + ]) + } +} + +impl Component for ProxmoxFirewallRules { + type Message = FirewallMsg; + type Properties = FirewallRules; + + fn create(ctx: &Context) -> Self { + let props = ctx.props(); + + let url: AttrValue = props.context.rules_url().into(); + + let store = Store::with_extract_key(|item: &pve_api_types::ListFirewallRules| { + Key::from(item.pos.to_string()) + }); + + let loader = Loader::new().loader({ + let url = url.clone(); + move || { + let url = url.clone(); + async move { crate::http_get(url.to_string(), None).await } + } + }); + + let _listener = loader.add_listener(ctx.link().callback(|_| FirewallMsg::DataChange)); + + loader.load(); + + let mut me = Self { + store, + loader, + _listener, + columns: Self::build_columns(), + }; + + me.update_data(); + me + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + FirewallMsg::DataChange => { + self.update_data(); + true + } + } + } + + fn view(&self, _ctx: &Context) -> Html { + self.loader.render(|_data| -> Html { + if self.store.data_len() == 0 { + Container::new() + .padding(2) + .with_child(tr!("No firewall rules configured")) + .into() + } else { + DataTable::new(self.columns.clone(), self.store.clone()) + .show_header(true) + .striped(true) + .into() + } + }) + } +} + +impl From for VNode { + fn from(val: FirewallRules) -> Self { + let comp = VComp::new::(Rc::new(val), None); + VNode::from(comp) + } +} diff --git a/src/lib.rs b/src/lib.rs index 88abf93..1fefed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,7 @@ mod rrd_timeframe_selector; pub use rrd_timeframe_selector::{RRDTimeframe, RRDTimeframeSelector}; mod firewall; -pub use firewall::{EditFirewallOptions, FirewallContext}; +pub use firewall::{EditFirewallOptions, FirewallContext, FirewallRules}; mod running_tasks; pub use running_tasks::{ProxmoxRunningTasks, RunningTasks}; -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel