public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall
@ 2025-10-30 14:33 Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 1/5] pve-api-types: update pve-api.json Hannes Laimer
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

This adds a basic UI for displaying the status of the firewall on remotes,
nodes and guests in a tree. Status includes whether the firewall is
enabled and the count of enabled rules. These rules are also shown in a
panel once an enetity in the tree is selected. Firewall options can be
edited, most useful is probably enable/disable, but generally all
options are exposed(since we had the types anyway).

Generally loading the status involves 2 requests per entity, so the PDM
server has to do quite a bit of work collecting all the relevant data.
That is the reason we have multiple status endpoints
 - for all pve remotes
 - for a specific remote
 - for a specific node
a bit more context on the commit adding these endpoints. With these we
can limit the number of requests the PDM potentially has to do. In this
context a cache could also make sense, should be somewhat straight
forward integrating something like Dominik proposed in [1]. But since
these are configs, caches would have to be really short lived, but still,
they could help with different useres requesting the same data at close
to the same time.

Firewall options edit form and the firewall rules tables were added to
yew-comp as they are not necesarrily PDM specific. I tried having them
in a way so it would not be too complicated reusing them in other places
at some point.

This also includes an updated pve-api.json, some api endpoint specs did
require minor adjustments so they'd work with the type generator. This
includes the not yet applied changes in [2]. This also needs [3] to be
present. Generally this is build with the latest master of
proxmox-yew-comp and proxmox-yew-widget-toolkit.

Notes: node or guest firewalls could be enabled, but end up being masked
by the cluster setting. I tried visualizing that by having the checkmark
normal if masked and green if not.

[1] https://lore.proxmox.com/pdm-devel/20251017120315.2723235-1-d.csapak@proxmox.com/
[2] https://lore.proxmox.com/pve-devel/20251023141546.105302-1-h.laimer@proxmox.com/T/#u
[3] https://lore.proxmox.com/yew-devel/20251029173528.378487-1-h.laimer@proxmox.com/T/#u


proxmox:

Hannes Laimer (5):
  pve-api-types: update pve-api.json
  pve-api-types: add get/update firewall options endpoints
  pve-api-types: schema2rust: handle `macro` keyword like we do `type`
  pve-api-types: add list firewall rules endpoints
  pve-api-types: regenerate

 pve-api-types/generate.pl                  |   54 +
 pve-api-types/generator-lib/Schema2Rust.pm |    7 +-
 pve-api-types/pve-api.json                 |  377 +-----
 pve-api-types/src/generated/code.rs        |  206 ++-
 pve-api-types/src/generated/types.rs       | 1364 +++++++++++++++++++-
 5 files changed, 1593 insertions(+), 415 deletions(-)


proxmox-yew-comp:

Hannes Laimer (4):
  form: add helpers for extractig data out of schemas
  firewall: add FirewallContext
  firewall: add options edit form
  firewall: add rules table

 src/firewall/context.rs             | 142 ++++++++++
 src/firewall/log_ratelimit_field.rs | 310 +++++++++++++++++++++
 src/firewall/mod.rs                 |  11 +
 src/firewall/options_edit.rs        | 404 ++++++++++++++++++++++++++++
 src/firewall/rules.rs               | 217 +++++++++++++++
 src/form/mod.rs                     |  70 +++++
 src/lib.rs                          |   3 +
 7 files changed, 1157 insertions(+)
 create mode 100644 src/firewall/context.rs
 create mode 100644 src/firewall/log_ratelimit_field.rs
 create mode 100644 src/firewall/mod.rs
 create mode 100644 src/firewall/options_edit.rs
 create mode 100644 src/firewall/rules.rs


proxmox-datacenter-manager:

Hannes Laimer (4):
  pdm-api-types: add firewall status types
  api: firewall: add option, rules and status endpoints
  pdm-client: add api methods for firewall options, rules and status
    endpoints
  ui: add firewall status tree

 lib/pdm-api-types/src/firewall.rs     | 171 ++++++
 lib/pdm-api-types/src/lib.rs          |   2 +
 lib/pdm-client/src/lib.rs             | 133 +++++
 server/src/api/pve/firewall.rs        | 756 ++++++++++++++++++++++++++
 server/src/api/pve/lxc.rs             |   1 +
 server/src/api/pve/mod.rs             |   3 +
 server/src/api/pve/node.rs            |   1 +
 server/src/api/pve/qemu.rs            |   1 +
 ui/src/remotes/firewall/columns.rs    | 150 +++++
 ui/src/remotes/firewall/mod.rs        |  30 +
 ui/src/remotes/firewall/tree.rs       | 634 +++++++++++++++++++++
 ui/src/remotes/firewall/types.rs      | 284 ++++++++++
 ui/src/remotes/firewall/ui_helpers.rs | 156 ++++++
 ui/src/remotes/mod.rs                 |  10 +
 14 files changed, 2332 insertions(+)
 create mode 100644 lib/pdm-api-types/src/firewall.rs
 create mode 100644 server/src/api/pve/firewall.rs
 create mode 100644 ui/src/remotes/firewall/columns.rs
 create mode 100644 ui/src/remotes/firewall/mod.rs
 create mode 100644 ui/src/remotes/firewall/tree.rs
 create mode 100644 ui/src/remotes/firewall/types.rs
 create mode 100644 ui/src/remotes/firewall/ui_helpers.rs


Summary over all repositories:
  26 files changed, 5082 insertions(+), 415 deletions(-)

-- 
Generated by git-murpp 0.8.1


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [pdm-devel] [PATCH proxmox 1/5] pve-api-types: update pve-api.json
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 2/5] pve-api-types: add get/update firewall options endpoints Hannes Laimer
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

This reflects the latest changes in pve-storage and pve-firewall.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/pve-api.json | 377 ++++---------------------------------
 1 file changed, 33 insertions(+), 344 deletions(-)

diff --git a/pve-api-types/pve-api.json b/pve-api-types/pve-api.json
index e0ad03c1..7a909404 100644
--- a/pve-api-types/pve-api.json
+++ b/pve-api-types/pve-api.json
@@ -4724,33 +4724,41 @@
                                        "returns": {
                                           "properties": {
                                              "action": {
+                                                "description": "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name",
                                                 "type": "string"
                                              },
                                              "comment": {
+                                                "description": "Descriptive comment",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "dest": {
+                                                "description": "Restrict packet destination address",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "dport": {
+                                                "description": "Restrict TCP/UDP destination port",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "enable": {
+                                                "description": "Flag to enable/disable a rule",
                                                 "optional": 1,
                                                 "type": "integer"
                                              },
                                              "icmp-type": {
+                                                "description": "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "iface": {
+                                                "description": "Network interface name. You have to use network configuration key names for VMs and containers",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "ipversion": {
+                                                "description": "IP version (4 or 6) - automatically determined from source/dest addresses",
                                                 "optional": 1,
                                                 "type": "integer"
                                              },
@@ -4771,25 +4779,31 @@
                                                 "type": "string"
                                              },
                                              "macro": {
+                                                "description": "Use predefined standard macro",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "pos": {
+                                                "description": "Rule position in the ruleset",
                                                 "type": "integer"
                                              },
                                              "proto": {
+                                                "description": "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "source": {
+                                                "description": "Restrict packet source address",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "sport": {
+                                                "description": "Restrict TCP/UDP source port",
                                                 "optional": 1,
                                                 "type": "string"
                                              },
                                              "type": {
+                                                "description": "Rule type",
                                                 "type": "string"
                                              }
                                           },
@@ -4990,11 +5004,7 @@
                                  "proxyto": null,
                                  "returns": {
                                     "items": {
-                                       "properties": {
-                                          "pos": {
-                                             "type": "integer"
-                                          }
-                                       },
+                                       "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                        "type": "object"
                                     },
                                     "links": [
@@ -5290,67 +5300,7 @@
                                  },
                                  "proxyto": null,
                                  "returns": {
-                                    "properties": {
-                                       "action": {
-                                          "type": "string"
-                                       },
-                                       "comment": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "dest": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "dport": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "enable": {
-                                          "optional": 1,
-                                          "type": "integer"
-                                       },
-                                       "icmp-type": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "iface": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "ipversion": {
-                                          "optional": 1,
-                                          "type": "integer"
-                                       },
-                                       "log": {
-                                          "description": "Log level for firewall rule",
-                                          "enum": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties/log/enum"],
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "macro": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "pos": {
-                                          "type": "integer"
-                                       },
-                                       "proto": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "source": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "sport": {
-                                          "optional": 1,
-                                          "type": "string"
-                                       },
-                                       "type": {
-                                          "type": "string"
-                                       }
-                                    },
+                                    "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                     "type": "object"
                                  }
                               },
@@ -5509,11 +5459,7 @@
                            "proxyto": null,
                            "returns": {
                               "items": {
-                                 "properties": {
-                                    "pos": {
-                                       "type": "integer"
-                                    }
-                                 },
+                                 "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                  "type": "object"
                               },
                               "links": [
@@ -6227,6 +6173,7 @@
                                     "type": "boolean"
                                  },
                                  "enable": {
+                                    "default": 0,
                                     "description": "Enable or disable the firewall cluster wide.",
                                     "minimum": 0,
                                     "optional": 1,
@@ -11029,67 +10976,7 @@
                                                    },
                                                    "proxyto": null,
                                                    "returns": {
-                                                      "properties": {
-                                                         "action": {
-                                                            "type": "string"
-                                                         },
-                                                         "comment": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dest": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "enable": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "icmp-type": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "iface": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "ipversion": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "log": {
-                                                            "description": "Log level for firewall rule",
-                                                            "enum": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties/log/enum"],
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "macro": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "pos": {
-                                                            "type": "integer"
-                                                         },
-                                                         "proto": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "source": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "sport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "type": {
-                                                            "type": "string"
-                                                         }
-                                                      },
+                                                      "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                       "type": "object"
                                                    }
                                                 },
@@ -11241,11 +11128,7 @@
                                              "proxyto": null,
                                              "returns": {
                                                 "items": {
-                                                   "properties": {
-                                                      "pos": {
-                                                         "type": "integer"
-                                                      }
-                                                   },
+                                                   "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                    "type": "object"
                                                 },
                                                 "links": [
@@ -16634,67 +16517,7 @@
                                                    },
                                                    "proxyto": null,
                                                    "returns": {
-                                                      "properties": {
-                                                         "action": {
-                                                            "type": "string"
-                                                         },
-                                                         "comment": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dest": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "enable": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "icmp-type": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "iface": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "ipversion": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "log": {
-                                                            "description": "Log level for firewall rule",
-                                                            "enum": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties/log/enum"],
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "macro": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "pos": {
-                                                            "type": "integer"
-                                                         },
-                                                         "proto": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "source": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "sport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "type": {
-                                                            "type": "string"
-                                                         }
-                                                      },
+                                                      "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                       "type": "object"
                                                    }
                                                 },
@@ -16858,11 +16681,7 @@
                                              "proxyto": null,
                                              "returns": {
                                                 "items": {
-                                                   "properties": {
-                                                      "pos": {
-                                                         "type": "integer"
-                                                      }
-                                                   },
+                                                   "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                    "type": "object"
                                                 },
                                                 "links": [
@@ -35289,67 +35108,7 @@
                                                    },
                                                    "proxyto": null,
                                                    "returns": {
-                                                      "properties": {
-                                                         "action": {
-                                                            "type": "string"
-                                                         },
-                                                         "comment": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dest": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "dport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "enable": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "icmp-type": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "iface": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "ipversion": {
-                                                            "optional": 1,
-                                                            "type": "integer"
-                                                         },
-                                                         "log": {
-                                                            "description": "Log level for firewall rule",
-                                                            "enum": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties/log/enum"],
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "macro": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "pos": {
-                                                            "type": "integer"
-                                                         },
-                                                         "proto": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "source": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "sport": {
-                                                            "optional": 1,
-                                                            "type": "string"
-                                                         },
-                                                         "type": {
-                                                            "type": "string"
-                                                         }
-                                                      },
+                                                      "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                       "type": "object"
                                                    }
                                                 },
@@ -35513,11 +35272,7 @@
                                              "proxyto": null,
                                              "returns": {
                                                 "items": {
-                                                   "properties": {
-                                                      "pos": {
-                                                         "type": "integer"
-                                                      }
-                                                   },
+                                                   "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                                    "type": "object"
                                                 },
                                                 "links": [
@@ -49048,67 +48803,7 @@
                                        },
                                        "proxyto": "node",
                                        "returns": {
-                                          "properties": {
-                                             "action": {
-                                                "type": "string"
-                                             },
-                                             "comment": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "dest": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "dport": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "enable": {
-                                                "optional": 1,
-                                                "type": "integer"
-                                             },
-                                             "icmp-type": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "iface": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "ipversion": {
-                                                "optional": 1,
-                                                "type": "integer"
-                                             },
-                                             "log": {
-                                                "description": "Log level for firewall rule",
-                                                "enum": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties/log/enum"],
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "macro": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "pos": {
-                                                "type": "integer"
-                                             },
-                                             "proto": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "source": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "sport": {
-                                                "optional": 1,
-                                                "type": "string"
-                                             },
-                                             "type": {
-                                                "type": "string"
-                                             }
-                                          },
+                                          "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                           "type": "object"
                                        }
                                     },
@@ -49270,11 +48965,7 @@
                                  "proxyto": "node",
                                  "returns": {
                                     "items": {
-                                       "properties": {
-                                          "pos": {
-                                             "type": "integer"
-                                          }
-                                       },
+                                       "properties": ("Ref")["/root/0/children/4/children/0/children/0/children/0/info/GET/returns/properties"],
                                        "type": "object"
                                     },
                                     "links": [
@@ -49442,6 +49133,7 @@
                                  "returns": {
                                     "properties": {
                                        "enable": {
+                                          "default": 1,
                                           "description": "Enable host firewall rules.",
                                           "optional": 1,
                                           "type": "boolean"
@@ -52757,12 +52449,6 @@
                               "optional": 1,
                               "type": "string"
                            },
-                           "path": {
-                              "description": "File system path.",
-                              "format": "pve-storage-path",
-                              "optional": 1,
-                              "type": "string"
-                           },
                            "pool": {
                               "description": "Pool.",
                               "optional": 1,
@@ -52935,7 +52621,6 @@
                                  "nfs",
                                  "pbs",
                                  "rbd",
-                                 "xfsrs-example",
                                  "zfs",
                                  "zfspool"
                               ],
@@ -53056,7 +52741,12 @@
                      "nowritecache": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/nowritecache"],
                      "options": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/options"],
                      "password": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/password"],
-                     "path": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/path"],
+                     "path": {
+                        "description": "File system path.",
+                        "format": "pve-storage-path",
+                        "optional": 1,
+                        "type": "string"
+                     },
                      "pool": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/pool"],
                      "port": ("Ref")["/root/2/children/0/info/PUT/parameters/properties/port"],
                      "portal": {
@@ -53110,7 +52800,6 @@
                            "nfs",
                            "pbs",
                            "rbd",
-                           "xfsrs-example",
                            "zfs",
                            "zfspool"
                         ],
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox 2/5] pve-api-types: add get/update firewall options endpoints
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 1/5] pve-api-types: update pve-api.json Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 3/5] pve-api-types: schema2rust: handle `macro` keyword like we do `type` Hannes Laimer
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/generate.pl | 45 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 9ea9aa4f..53558872 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -346,6 +346,51 @@ api(GET => '/nodes/{node}/apt/update', 'list_available_updates', 'return-name' =
 api(POST => '/nodes/{node}/apt/update', 'update_apt_database', 'output-type' => 'PveUpid', 'param-name' => 'AptUpdateParams');
 api(GET => '/nodes/{node}/apt/changelog', 'get_package_changelog', 'output-type' => 'String');
 
+Schema2Rust::generate_enum('FwConntrackHelper', {
+    type => 'string',
+    description => "Firewall conntrack helper.",
+    enum => ['amanda', 'ftp', 'irc', 'netbios-ns', 'pptp', 'sane', 'sip', 'snmp', 'tftp'],
+});
+
+Schema2Rust::register_enum_variant('FirewallLogLevel::err' => 'Error');
+Schema2Rust::generate_enum('FirewallLogLevel', {
+    type => 'string',
+    description => "Firewall log levels.",
+    enum => ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug', 'nolog'],
+    default => 'nolog',
+});
+
+Schema2Rust::generate_enum('FirewallIOPolicy', {
+    type => 'string',
+    description => "Firewall IO policies.",
+    enum => ['ACCEPT', 'DROP', 'REJECT'],
+    default => 'ACCEPT',
+});
+
+Schema2Rust::generate_enum('FirewallFWPolicy', {
+    type => 'string',
+    description => "Firewall IO policies.",
+    enum => ['ACCEPT', 'DROP'],
+    default => 'DROP',
+});
+
+Schema2Rust::register_format('pve-fw-conntrack-helper' => {
+    type => 'FwConntrackHelper',
+    kind => 'array',
+});
+
+# options
+api(GET => '/cluster/firewall/options', 'cluster_firewall_options', 'return-name' => 'ClusterFirewallOptions');
+api(PUT => '/cluster/firewall/options', 'set_cluster_firewall_options', 'param-name' => 'UpdateClusterFirewallOptions');
+
+api(GET => '/nodes/{node}/firewall/options', 'node_firewall_options', 'return-name' => 'NodeFirewallOptions');
+api(PUT => '/nodes/{node}/firewall/options', 'set_node_firewall_options', 'param-name' => 'UpdateNodeFirewallOptions');
+
+api(GET => '/nodes/{node}/lxc/{vmid}/firewall/options', 'lxc_firewall_options', 'return-name' => 'GuestFirewallOptions');
+api(PUT => '/nodes/{node}/lxc/{vmid}/firewall/options', 'set_lxc_firewall_options', 'param-name' => 'UpdateGuestFirewallOptions');
+api(GET => '/nodes/{node}/qemu/{vmid}/firewall/options', 'qemu_firewall_options', 'return-name' => 'GuestFirewallOptions');
+api(PUT => '/nodes/{node}/qemu/{vmid}/firewall/options', 'set_qemu_firewall_options', 'param-name' => 'UpdateGuestFirewallOptions');
+
 Schema2Rust::generate_enum('SdnObjectState', {
     type => 'string',
     description => "The state of an SDN object.",
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox 3/5] pve-api-types: schema2rust: handle `macro` keyword like we do `type`
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 1/5] pve-api-types: update pve-api.json Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 2/5] pve-api-types: add get/update firewall options endpoints Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 4/5] pve-api-types: add list firewall rules endpoints Hannes Laimer
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

Firewall rules have a field named `macro`, this would trip up the
generated rust code. This fixes that.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/generator-lib/Schema2Rust.pm | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/pve-api-types/generator-lib/Schema2Rust.pm b/pve-api-types/generator-lib/Schema2Rust.pm
index c9fba231..f54b3683 100644
--- a/pve-api-types/generator-lib/Schema2Rust.pm
+++ b/pve-api-types/generator-lib/Schema2Rust.pm
@@ -126,8 +126,8 @@ sub api_to_string : prototype($$$$$) {
 
         local $API_TYPE_POS = "$API_TYPE_POS/$key";
 
-        # We need to quote keys with hyphens:
-        my $safe_key = ($key =~ /-/) ? "\"$key\"" : $key;
+        # We need to quote keys with hyphens or reserved keywords:
+        my $safe_key = (($key =~ /-/) || ($key eq 'macro')) ? "\"$key\"" : $key;
 
         if (exists($API_TYPE_OVERRIDES{$API_TYPE_POS})) {
             $value = $API_TYPE_OVERRIDES{$API_TYPE_POS};
@@ -172,7 +172,7 @@ sub api_to_string : prototype($$$$$) {
                 warn "api type extension for $API_TYPE_POS.$key already in schema, skipping\n";
                 next;
             }
-            my $safe_key = ($key =~ /-/) ? "\"$key\"" : $key;
+            my $safe_key = (($key =~ /-/) || ($key eq 'macro')) ? "\"$key\"" : $key;
             my $value = $extra->{$key};
             print {$out} "${indent}$safe_key: $value,\n";
         }
@@ -726,6 +726,7 @@ sub namify_field : prototype($) {
     }
 
     return 'ty' if $out eq 'type';
+    return 'r#macro' if $out eq 'macro';
 
     return $out;
 }
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox 4/5] pve-api-types: add list firewall rules endpoints
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (2 preceding siblings ...)
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 3/5] pve-api-types: schema2rust: handle `macro` keyword like we do `type` Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 5/5] pve-api-types: regenerate Hannes Laimer
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/generate.pl | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 53558872..772ea20d 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -391,6 +391,15 @@ api(PUT => '/nodes/{node}/lxc/{vmid}/firewall/options', 'set_lxc_firewall_option
 api(GET => '/nodes/{node}/qemu/{vmid}/firewall/options', 'qemu_firewall_options', 'return-name' => 'GuestFirewallOptions');
 api(PUT => '/nodes/{node}/qemu/{vmid}/firewall/options', 'set_qemu_firewall_options', 'param-name' => 'UpdateGuestFirewallOptions');
 
+# rules
+api(GET => '/cluster/firewall/rules', 'list_cluster_firewall_rules', 'return-name' => 'ListFirewallRules');
+
+api(GET => '/nodes/{node}/firewall/rules', 'list_node_firewall_rules', 'return-name' => 'ListFirewallRules');
+
+api(GET => '/nodes/{node}/lxc/{vmid}/firewall/rules', 'list_lxc_firewall_rules', 'return-name' => 'ListFirewallRules');
+api(GET => '/nodes/{node}/qemu/{vmid}/firewall/rules', 'list_qemu_firewall_rules', 'return-name' => 'ListFirewallRules');
+Schema2Rust::derive('ListFirewallRules' => 'Clone', 'PartialEq');
+
 Schema2Rust::generate_enum('SdnObjectState', {
     type => 'string',
     description => "The state of an SDN object.",
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox 5/5] pve-api-types: regenerate
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (3 preceding siblings ...)
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 4/5] pve-api-types: add list firewall rules endpoints Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox-yew-comp 1/4] form: add helpers for extractig data out of schemas Hannes Laimer
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |  206 +++-
 pve-api-types/src/generated/types.rs | 1364 ++++++++++++++++++++++++--
 2 files changed, 1502 insertions(+), 68 deletions(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 788e4bf5..ad6af3d1 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -60,9 +60,7 @@
 /// - /cluster/firewall/ipset/{name}
 /// - /cluster/firewall/ipset/{name}/{cidr}
 /// - /cluster/firewall/macros
-/// - /cluster/firewall/options
 /// - /cluster/firewall/refs
-/// - /cluster/firewall/rules
 /// - /cluster/firewall/rules/{pos}
 /// - /cluster/ha
 /// - /cluster/ha/groups
@@ -200,8 +198,6 @@
 /// - /nodes/{node}/execute
 /// - /nodes/{node}/firewall
 /// - /nodes/{node}/firewall/log
-/// - /nodes/{node}/firewall/options
-/// - /nodes/{node}/firewall/rules
 /// - /nodes/{node}/firewall/rules/{pos}
 /// - /nodes/{node}/hardware
 /// - /nodes/{node}/hardware/pci
@@ -220,9 +216,7 @@
 /// - /nodes/{node}/lxc/{vmid}/firewall/ipset/{name}
 /// - /nodes/{node}/lxc/{vmid}/firewall/ipset/{name}/{cidr}
 /// - /nodes/{node}/lxc/{vmid}/firewall/log
-/// - /nodes/{node}/lxc/{vmid}/firewall/options
 /// - /nodes/{node}/lxc/{vmid}/firewall/refs
-/// - /nodes/{node}/lxc/{vmid}/firewall/rules
 /// - /nodes/{node}/lxc/{vmid}/firewall/rules/{pos}
 /// - /nodes/{node}/lxc/{vmid}/interfaces
 /// - /nodes/{node}/lxc/{vmid}/move_volume
@@ -287,9 +281,7 @@
 /// - /nodes/{node}/qemu/{vmid}/firewall/ipset/{name}
 /// - /nodes/{node}/qemu/{vmid}/firewall/ipset/{name}/{cidr}
 /// - /nodes/{node}/qemu/{vmid}/firewall/log
-/// - /nodes/{node}/qemu/{vmid}/firewall/options
 /// - /nodes/{node}/qemu/{vmid}/firewall/refs
-/// - /nodes/{node}/qemu/{vmid}/firewall/rules
 /// - /nodes/{node}/qemu/{vmid}/firewall/rules/{pos}
 /// - /nodes/{node}/qemu/{vmid}/monitor
 /// - /nodes/{node}/qemu/{vmid}/move_disk
@@ -385,6 +377,11 @@ pub trait PveClient {
         Err(Error::Other("cluster_config_join not implemented"))
     }
 
+    /// Get Firewall options.
+    async fn cluster_firewall_options(&self) -> Result<ClusterFirewallOptions, Error> {
+        Err(Error::Other("cluster_firewall_options not implemented"))
+    }
+
     /// Retrieve metrics of the cluster.
     async fn cluster_metrics_export(
         &self,
@@ -481,6 +478,11 @@ pub trait PveClient {
         Err(Error::Other("list_available_updates not implemented"))
     }
 
+    /// List rules.
+    async fn list_cluster_firewall_rules(&self) -> Result<Vec<ListFirewallRules>, Error> {
+        Err(Error::Other("list_cluster_firewall_rules not implemented"))
+    }
+
     /// SDN controllers index.
     async fn list_controllers(
         &self,
@@ -501,6 +503,15 @@ pub trait PveClient {
         Err(Error::Other("list_lxc not implemented"))
     }
 
+    /// List rules.
+    async fn list_lxc_firewall_rules(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<Vec<ListFirewallRules>, Error> {
+        Err(Error::Other("list_lxc_firewall_rules not implemented"))
+    }
+
     /// List available networks
     async fn list_networks(
         &self,
@@ -510,6 +521,11 @@ pub trait PveClient {
         Err(Error::Other("list_networks not implemented"))
     }
 
+    /// List rules.
+    async fn list_node_firewall_rules(&self, node: &str) -> Result<Vec<ListFirewallRules>, Error> {
+        Err(Error::Other("list_node_firewall_rules not implemented"))
+    }
+
     /// Cluster node index.
     async fn list_nodes(&self) -> Result<Vec<ClusterNodeIndexResponse>, Error> {
         Err(Error::Other("list_nodes not implemented"))
@@ -520,6 +536,15 @@ pub trait PveClient {
         Err(Error::Other("list_qemu not implemented"))
     }
 
+    /// List rules.
+    async fn list_qemu_firewall_rules(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<Vec<ListFirewallRules>, Error> {
+        Err(Error::Other("list_qemu_firewall_rules not implemented"))
+    }
+
     /// Get status for all datastores.
     async fn list_storages(
         &self,
@@ -552,6 +577,15 @@ pub trait PveClient {
         Err(Error::Other("list_zones not implemented"))
     }
 
+    /// Get VM firewall options.
+    async fn lxc_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<GuestFirewallOptions, Error> {
+        Err(Error::Other("lxc_firewall_options not implemented"))
+    }
+
     /// Get container configuration.
     async fn lxc_get_config(
         &self,
@@ -588,11 +622,25 @@ pub trait PveClient {
         Err(Error::Other("migrate_qemu not implemented"))
     }
 
+    /// Get host firewall options.
+    async fn node_firewall_options(&self, node: &str) -> Result<NodeFirewallOptions, Error> {
+        Err(Error::Other("node_firewall_options not implemented"))
+    }
+
     /// Read node status
     async fn node_status(&self, node: &str) -> Result<NodeStatus, Error> {
         Err(Error::Other("node_status not implemented"))
     }
 
+    /// Get VM firewall options.
+    async fn qemu_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<GuestFirewallOptions, Error> {
+        Err(Error::Other("qemu_firewall_options not implemented"))
+    }
+
     /// Get the virtual machine configuration with pending configuration changes
     /// applied. Set the 'current' parameter to get the current configuration
     /// instead.
@@ -678,6 +726,43 @@ pub trait PveClient {
         Err(Error::Other("sdn_apply not implemented"))
     }
 
+    /// Set Firewall options.
+    async fn set_cluster_firewall_options(
+        &self,
+        params: UpdateClusterFirewallOptions,
+    ) -> Result<(), Error> {
+        Err(Error::Other("set_cluster_firewall_options not implemented"))
+    }
+
+    /// Set Firewall options.
+    async fn set_lxc_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+        params: UpdateGuestFirewallOptions,
+    ) -> Result<(), Error> {
+        Err(Error::Other("set_lxc_firewall_options not implemented"))
+    }
+
+    /// Set Firewall options.
+    async fn set_node_firewall_options(
+        &self,
+        node: &str,
+        params: UpdateNodeFirewallOptions,
+    ) -> Result<(), Error> {
+        Err(Error::Other("set_node_firewall_options not implemented"))
+    }
+
+    /// Set Firewall options.
+    async fn set_qemu_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+        params: UpdateGuestFirewallOptions,
+    ) -> Result<(), Error> {
+        Err(Error::Other("set_qemu_firewall_options not implemented"))
+    }
+
     /// Shutdown the container. This will trigger a clean shutdown of the
     /// container, see lxc-stop(1) for details.
     async fn shutdown_lxc_async(
@@ -791,6 +876,12 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Get Firewall options.
+    async fn cluster_firewall_options(&self) -> Result<ClusterFirewallOptions, Error> {
+        let url = "/api2/extjs/cluster/firewall/options";
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Retrieve metrics of the cluster.
     async fn cluster_metrics_export(
         &self,
@@ -938,6 +1029,12 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// List rules.
+    async fn list_cluster_firewall_rules(&self) -> Result<Vec<ListFirewallRules>, Error> {
+        let url = "/api2/extjs/cluster/firewall/rules";
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// SDN controllers index.
     async fn list_controllers(
         &self,
@@ -965,6 +1062,16 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// List rules.
+    async fn list_lxc_firewall_rules(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<Vec<ListFirewallRules>, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/lxc/{vmid}/firewall/rules");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// List available networks
     async fn list_networks(
         &self,
@@ -977,6 +1084,12 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// List rules.
+    async fn list_node_firewall_rules(&self, node: &str) -> Result<Vec<ListFirewallRules>, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/firewall/rules");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Cluster node index.
     async fn list_nodes(&self) -> Result<Vec<ClusterNodeIndexResponse>, Error> {
         let url = "/api2/extjs/nodes";
@@ -991,6 +1104,16 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// List rules.
+    async fn list_qemu_firewall_rules(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<Vec<ListFirewallRules>, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/qemu/{vmid}/firewall/rules");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Get status for all datastores.
     async fn list_storages(
         &self,
@@ -1039,6 +1162,16 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Get VM firewall options.
+    async fn lxc_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<GuestFirewallOptions, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/lxc/{vmid}/firewall/options");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Get container configuration.
     async fn lxc_get_config(
         &self,
@@ -1082,12 +1215,28 @@ where
         Ok(self.0.post(url, &params).await?.expect_json()?.data)
     }
 
+    /// Get host firewall options.
+    async fn node_firewall_options(&self, node: &str) -> Result<NodeFirewallOptions, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/firewall/options");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Read node status
     async fn node_status(&self, node: &str) -> Result<NodeStatus, Error> {
         let url = &format!("/api2/extjs/nodes/{node}/status");
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Get VM firewall options.
+    async fn qemu_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+    ) -> Result<GuestFirewallOptions, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/qemu/{vmid}/firewall/options");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Get the virtual machine configuration with pending configuration changes
     /// applied. Set the 'current' parameter to get the current configuration
     /// instead.
@@ -1196,6 +1345,47 @@ where
         Ok(self.0.put(url, &params).await?.expect_json()?.data)
     }
 
+    /// Set Firewall options.
+    async fn set_cluster_firewall_options(
+        &self,
+        params: UpdateClusterFirewallOptions,
+    ) -> Result<(), Error> {
+        let url = "/api2/extjs/cluster/firewall/options";
+        self.0.put(url, &params).await?.nodata()
+    }
+
+    /// Set Firewall options.
+    async fn set_lxc_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+        params: UpdateGuestFirewallOptions,
+    ) -> Result<(), Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/lxc/{vmid}/firewall/options");
+        self.0.put(url, &params).await?.nodata()
+    }
+
+    /// Set Firewall options.
+    async fn set_node_firewall_options(
+        &self,
+        node: &str,
+        params: UpdateNodeFirewallOptions,
+    ) -> Result<(), Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/firewall/options");
+        self.0.put(url, &params).await?.nodata()
+    }
+
+    /// Set Firewall options.
+    async fn set_qemu_firewall_options(
+        &self,
+        node: &str,
+        vmid: u32,
+        params: UpdateGuestFirewallOptions,
+    ) -> Result<(), Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/qemu/{vmid}/firewall/options");
+        self.0.put(url, &params).await?.nodata()
+    }
+
     /// Shutdown the container. This will trigger a clean shutdown of the
     /// container, see lxc-stop(1) for details.
     async fn shutdown_lxc_async(
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index b8b834be..11e2929d 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -226,6 +226,101 @@ mod cluster_resource_content {
     }
 }
 
+#[api(
+    properties: {
+        ebtables: {
+            default: true,
+            optional: true,
+        },
+        enable: {
+            default: 0,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        log_ratelimit: {
+            format: &ApiStringFormat::PropertyString(&ClusterFirewallOptionsLogRatelimit::API_SCHEMA),
+            optional: true,
+            type: String,
+        },
+        policy_forward: {
+            optional: true,
+            type: FirewallFWPolicy,
+        },
+        policy_in: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        policy_out: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ClusterFirewallOptions {
+    /// Enable ebtables rules cluster wide.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ebtables: Option<bool>,
+
+    /// Enable or disable the firewall cluster wide.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<u64>,
+
+    /// Log ratelimiting settings
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_ratelimit: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_forward: Option<FirewallFWPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_in: Option<FirewallIOPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_out: Option<FirewallIOPolicy>,
+}
+
+#[api(
+    default_key: "enable",
+    properties: {
+        burst: {
+            default: 5,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        enable: {
+            default: true,
+        },
+        rate: {
+            default: "1/second",
+            optional: true,
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ClusterFirewallOptionsLogRatelimit {
+    /// Initial burst of packages which will always get logged before the rate
+    /// is applied
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub burst: Option<u64>,
+
+    /// Enable or disable log rate limiting
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub enable: bool,
+
+    /// Frequency with which the burst bucket gets refilled
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub rate: Option<String>,
+}
+
 const_regex! {
 
 CLUSTER_JOIN_INFO_PREFERRED_NODE_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
@@ -1645,6 +1740,204 @@ pub struct CreateZone {
     pub zone: String,
 }
 
+#[api]
+/// Firewall IO policies.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum FirewallFWPolicy {
+    #[serde(rename = "ACCEPT")]
+    /// ACCEPT.
+    Accept,
+    #[serde(rename = "DROP")]
+    #[default]
+    /// DROP.
+    Drop,
+}
+serde_plain::derive_display_from_serialize!(FirewallFWPolicy);
+serde_plain::derive_fromstr_from_deserialize!(FirewallFWPolicy);
+
+#[api]
+/// Firewall IO policies.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum FirewallIOPolicy {
+    #[serde(rename = "ACCEPT")]
+    #[default]
+    /// ACCEPT.
+    Accept,
+    #[serde(rename = "DROP")]
+    /// DROP.
+    Drop,
+    #[serde(rename = "REJECT")]
+    /// REJECT.
+    Reject,
+}
+serde_plain::derive_display_from_serialize!(FirewallIOPolicy);
+serde_plain::derive_fromstr_from_deserialize!(FirewallIOPolicy);
+
+#[api]
+/// Firewall log levels.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum FirewallLogLevel {
+    #[serde(rename = "emerg")]
+    /// emerg.
+    Emerg,
+    #[serde(rename = "alert")]
+    /// alert.
+    Alert,
+    #[serde(rename = "crit")]
+    /// crit.
+    Crit,
+    #[serde(rename = "err")]
+    /// err.
+    Error,
+    #[serde(rename = "warning")]
+    /// warning.
+    Warning,
+    #[serde(rename = "notice")]
+    /// notice.
+    Notice,
+    #[serde(rename = "info")]
+    /// info.
+    Info,
+    #[serde(rename = "debug")]
+    /// debug.
+    Debug,
+    #[serde(rename = "nolog")]
+    #[default]
+    /// nolog.
+    Nolog,
+}
+serde_plain::derive_display_from_serialize!(FirewallLogLevel);
+serde_plain::derive_fromstr_from_deserialize!(FirewallLogLevel);
+
+#[api]
+/// Firewall conntrack helper.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum FwConntrackHelper {
+    #[serde(rename = "amanda")]
+    /// amanda.
+    Amanda,
+    #[serde(rename = "ftp")]
+    /// ftp.
+    Ftp,
+    #[serde(rename = "irc")]
+    /// irc.
+    Irc,
+    #[serde(rename = "netbios-ns")]
+    /// netbios-ns.
+    NetbiosNs,
+    #[serde(rename = "pptp")]
+    /// pptp.
+    Pptp,
+    #[serde(rename = "sane")]
+    /// sane.
+    Sane,
+    #[serde(rename = "sip")]
+    /// sip.
+    Sip,
+    #[serde(rename = "snmp")]
+    /// snmp.
+    Snmp,
+    #[serde(rename = "tftp")]
+    /// tftp.
+    Tftp,
+}
+serde_plain::derive_display_from_serialize!(FwConntrackHelper);
+serde_plain::derive_fromstr_from_deserialize!(FwConntrackHelper);
+
+#[api(
+    properties: {
+        dhcp: {
+            default: false,
+            optional: true,
+        },
+        enable: {
+            default: false,
+            optional: true,
+        },
+        ipfilter: {
+            default: false,
+            optional: true,
+        },
+        log_level_in: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_out: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        macfilter: {
+            default: true,
+            optional: true,
+        },
+        ndp: {
+            default: false,
+            optional: true,
+        },
+        policy_in: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        policy_out: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        radv: {
+            default: false,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct GuestFirewallOptions {
+    /// Enable DHCP.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dhcp: Option<bool>,
+
+    /// Enable/disable firewall rules.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<bool>,
+
+    /// Enable default IP filters. This is equivalent to adding an empty
+    /// ipfilter-net<id> ipset for every interface. Such ipsets implicitly
+    /// contain sane default restrictions such as restricting IPv6 link local
+    /// addresses to the one derived from the interface's MAC address. For
+    /// containers the configured IP addresses will be implicitly added.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipfilter: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_in: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_out: Option<FirewallLogLevel>,
+
+    /// Enable/disable MAC address filter.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub macfilter: Option<bool>,
+
+    /// Enable NDP (Neighbor Discovery Protocol).
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ndp: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_in: Option<FirewallIOPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_out: Option<FirewallIOPolicy>,
+
+    /// Allow sending Router Advertisement.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub radv: Option<bool>,
+}
+
 #[api]
 /// A guest's run state.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -1679,6 +1972,137 @@ pub enum ListControllersType {
 serde_plain::derive_display_from_serialize!(ListControllersType);
 serde_plain::derive_fromstr_from_deserialize!(ListControllersType);
 
+#[api(
+    properties: {
+        action: {
+            type: String,
+        },
+        comment: {
+            optional: true,
+            type: String,
+        },
+        dest: {
+            optional: true,
+            type: String,
+        },
+        dport: {
+            optional: true,
+            type: String,
+        },
+        enable: {
+            optional: true,
+            type: Integer,
+        },
+        "icmp-type": {
+            optional: true,
+            type: String,
+        },
+        iface: {
+            optional: true,
+            type: String,
+        },
+        ipversion: {
+            optional: true,
+            type: Integer,
+        },
+        log: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        "macro": {
+            optional: true,
+            type: String,
+        },
+        pos: {
+            type: Integer,
+        },
+        proto: {
+            optional: true,
+            type: String,
+        },
+        source: {
+            optional: true,
+            type: String,
+        },
+        sport: {
+            optional: true,
+            type: String,
+        },
+        type: {
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct ListFirewallRules {
+    /// Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name
+    pub action: String,
+
+    /// Descriptive comment
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub comment: Option<String>,
+
+    /// Restrict packet destination address
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dest: Option<String>,
+
+    /// Restrict TCP/UDP destination port
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dport: Option<String>,
+
+    /// Flag to enable/disable a rule
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<i64>,
+
+    /// Specify icmp-type. Only valid if proto equals 'icmp' or
+    /// 'icmpv6'/'ipv6-icmp'
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "icmp-type")]
+    pub icmp_type: Option<String>,
+
+    /// Network interface name. You have to use network configuration key names
+    /// for VMs and containers
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub iface: Option<String>,
+
+    /// IP version (4 or 6) - automatically determined from source/dest
+    /// addresses
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipversion: Option<i64>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log: Option<FirewallLogLevel>,
+
+    /// Use predefined standard macro
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "macro")]
+    pub r#macro: Option<String>,
+
+    /// Rule position in the ruleset
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    pub pos: i64,
+
+    /// IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers,
+    /// as defined in '/etc/protocols'
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub proto: Option<String>,
+
+    /// Restrict packet source address
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub source: Option<String>,
+
+    /// Restrict TCP/UDP source port
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub sport: Option<String>,
+
+    /// Rule type
+    #[serde(rename = "type")]
+    pub ty: String,
+}
+
 #[api]
 /// Only list specific interface types.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -3594,29 +4018,131 @@ pub struct MigrateQemu {
     pub with_local_disks: Option<bool>,
 }
 
-const_regex! {
+const NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS: Schema =
+    proxmox_schema::ArraySchema::new("list", &FwConntrackHelper::API_SCHEMA).schema();
 
-NETWORK_INTERFACE_BOND_PRIMARY_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_BRIDGE_PORTS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_IFACE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_OVS_BONDS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_OVS_BRIDGE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_OVS_PORTS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_SLAVES_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
-NETWORK_INTERFACE_VLAN_RAW_DEVICE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+mod node_firewall_options_nf_conntrack_helpers {
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
-}
+    #[doc(hidden)]
+    pub trait Ser: Sized {
+        fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>;
+    }
 
-#[test]
-fn test_regex_compilation_14() {
-    use regex::Regex;
-    let _: &Regex = &NETWORK_INTERFACE_BOND_PRIMARY_RE;
-    let _: &Regex = &NETWORK_INTERFACE_BRIDGE_PORTS_RE;
-    let _: &Regex = &NETWORK_INTERFACE_IFACE_RE;
-    let _: &Regex = &NETWORK_INTERFACE_OVS_BONDS_RE;
-    let _: &Regex = &NETWORK_INTERFACE_OVS_BRIDGE_RE;
-    let _: &Regex = &NETWORK_INTERFACE_OVS_PORTS_RE;
-    let _: &Regex = &NETWORK_INTERFACE_SLAVES_RE;
+    impl<T: Serialize + for<'a> Deserialize<'a>> Ser for Vec<T> {
+        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            super::stringlist::serialize(
+                &self[..],
+                serializer,
+                &super::NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS,
+            )
+        }
+
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            super::stringlist::deserialize(
+                deserializer,
+                &super::NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS,
+            )
+        }
+    }
+
+    impl<T: Ser> Ser for Option<T> {
+        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            match self {
+                None => serializer.serialize_none(),
+                Some(inner) => inner.ser(serializer),
+            }
+        }
+
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            use std::fmt;
+            use std::marker::PhantomData;
+
+            struct V<T: Ser>(PhantomData<T>);
+
+            impl<'de, T: Ser> serde::de::Visitor<'de> for V<T> {
+                type Value = Option<T>;
+
+                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                    f.write_str("an optional string")
+                }
+
+                fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
+                    Ok(None)
+                }
+
+                fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+                where
+                    D: Deserializer<'de>,
+                {
+                    T::de(deserializer).map(Some)
+                }
+
+                fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                    use serde::de::IntoDeserializer;
+                    T::de(value.into_deserializer()).map(Some)
+                }
+            }
+
+            deserializer.deserialize_option(V::<T>(PhantomData))
+        }
+    }
+
+    pub fn serialize<T, S>(this: &T, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+        T: Ser,
+    {
+        this.ser(serializer)
+    }
+
+    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+        T: Ser,
+    {
+        T::de(deserializer)
+    }
+}
+
+const_regex! {
+
+NETWORK_INTERFACE_BOND_PRIMARY_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_BRIDGE_PORTS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_IFACE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_OVS_BONDS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_OVS_BRIDGE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_OVS_PORTS_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_SLAVES_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+NETWORK_INTERFACE_VLAN_RAW_DEVICE_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_14() {
+    use regex::Regex;
+    let _: &Regex = &NETWORK_INTERFACE_BOND_PRIMARY_RE;
+    let _: &Regex = &NETWORK_INTERFACE_BRIDGE_PORTS_RE;
+    let _: &Regex = &NETWORK_INTERFACE_IFACE_RE;
+    let _: &Regex = &NETWORK_INTERFACE_OVS_BONDS_RE;
+    let _: &Regex = &NETWORK_INTERFACE_OVS_BRIDGE_RE;
+    let _: &Regex = &NETWORK_INTERFACE_OVS_PORTS_RE;
+    let _: &Regex = &NETWORK_INTERFACE_SLAVES_RE;
     let _: &Regex = &NETWORK_INTERFACE_VLAN_RAW_DEVICE_RE;
 }
 #[api(
@@ -4223,6 +4749,187 @@ pub enum NetworkInterfaceVlanProtocol {
 serde_plain::derive_display_from_serialize!(NetworkInterfaceVlanProtocol);
 serde_plain::derive_fromstr_from_deserialize!(NetworkInterfaceVlanProtocol);
 
+#[api(
+    properties: {
+        enable: {
+            default: true,
+            optional: true,
+        },
+        log_level_forward: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_in: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_out: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_nf_conntrack: {
+            default: false,
+            optional: true,
+        },
+        ndp: {
+            default: false,
+            optional: true,
+        },
+        nf_conntrack_allow_invalid: {
+            default: false,
+            optional: true,
+        },
+        nf_conntrack_helpers: {
+            default: "",
+            format: &ApiStringFormat::PropertyString(&NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS),
+            optional: true,
+            type: String,
+        },
+        nf_conntrack_max: {
+            default: 262144,
+            minimum: 32768,
+            optional: true,
+            type: Integer,
+        },
+        nf_conntrack_tcp_timeout_established: {
+            default: 432000,
+            minimum: 7875,
+            optional: true,
+            type: Integer,
+        },
+        nf_conntrack_tcp_timeout_syn_recv: {
+            default: 60,
+            maximum: 60,
+            minimum: 30,
+            optional: true,
+            type: Integer,
+        },
+        nftables: {
+            default: false,
+            optional: true,
+        },
+        nosmurfs: {
+            default: false,
+            optional: true,
+        },
+        protection_synflood: {
+            default: false,
+            optional: true,
+        },
+        protection_synflood_burst: {
+            default: 1000,
+            optional: true,
+            type: Integer,
+        },
+        protection_synflood_rate: {
+            default: 200,
+            optional: true,
+            type: Integer,
+        },
+        smurf_log_level: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        tcp_flags_log_level: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        tcpflags: {
+            default: false,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct NodeFirewallOptions {
+    /// Enable host firewall rules.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_forward: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_in: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_out: Option<FirewallLogLevel>,
+
+    /// Enable logging of conntrack information.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_nf_conntrack: Option<bool>,
+
+    /// Enable NDP (Neighbor Discovery Protocol).
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ndp: Option<bool>,
+
+    /// Allow invalid packets on connection tracking.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_allow_invalid: Option<bool>,
+
+    /// Enable conntrack helpers for specific protocols. Supported protocols:
+    /// amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp
+    #[serde(with = "node_firewall_options_nf_conntrack_helpers")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_helpers: Option<Vec<FwConntrackHelper>>,
+
+    /// Maximum number of tracked connections.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_max: Option<u64>,
+
+    /// Conntrack established timeout.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_tcp_timeout_established: Option<u64>,
+
+    /// Conntrack syn recv timeout.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u8")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_tcp_timeout_syn_recv: Option<u8>,
+
+    /// Enable nftables based firewall (tech preview)
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nftables: Option<bool>,
+
+    /// Enable SMURFS filter.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nosmurfs: Option<bool>,
+
+    /// Enable synflood protection
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood: Option<bool>,
+
+    /// Synflood protection rate burst by ip src.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood_burst: Option<i64>,
+
+    /// Synflood protection rate syn/sec by ip src.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood_rate: Option<i64>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub smurf_log_level: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tcp_flags_log_level: Option<FirewallLogLevel>,
+
+    /// Filter illegal combinations of TCP flags.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tcpflags: Option<bool>,
+}
+
 #[api(
     additional_properties: "additional_properties",
     properties: {
@@ -11761,43 +12468,580 @@ pub struct TaskStatus {
     pub user: String,
 }
 
-const_regex! {
+const UPDATE_NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS: Schema =
+    proxmox_schema::ArraySchema::new("list", &FwConntrackHelper::API_SCHEMA).schema();
 
-UPDATE_QEMU_CONFIG_AFFINITY_RE = r##"^(\s*\d+(-\d+)?\s*)(,\s*\d+(-\d+)?\s*)?$"##;
-UPDATE_QEMU_CONFIG_BOOTDISK_RE = r##"^(ide|sata|scsi|virtio|efidisk|tpmstate)\d+$"##;
-UPDATE_QEMU_CONFIG_DELETE_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
-UPDATE_QEMU_CONFIG_IMPORT_WORKING_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
-UPDATE_QEMU_CONFIG_REVERT_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
-UPDATE_QEMU_CONFIG_SSHKEYS_RE = r##"^[-%a-zA-Z0-9_.!~*'()]*$"##;
-UPDATE_QEMU_CONFIG_TAGS_RE = r##"^(?i)[a-z0-9_][a-z0-9_\-+.]*$"##;
-UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
+mod update_node_firewall_options_nf_conntrack_helpers {
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
-}
+    #[doc(hidden)]
+    pub trait Ser: Sized {
+        fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>;
+    }
 
-#[test]
-fn test_regex_compilation_35() {
-    use regex::Regex;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_AFFINITY_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_BOOTDISK_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_DELETE_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_IMPORT_WORKING_STORAGE_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_REVERT_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_SSHKEYS_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_TAGS_RE;
-    let _: &Regex = &UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE;
-}
-#[api(
-    properties: {
-        acpi: {
-            default: true,
-            optional: true,
-        },
-        affinity: {
-            format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_AFFINITY_RE),
-            optional: true,
-            type: String,
-        },
-        agent: {
+    impl<T: Serialize + for<'a> Deserialize<'a>> Ser for Vec<T> {
+        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            super::stringlist::serialize(
+                &self[..],
+                serializer,
+                &super::UPDATE_NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS,
+            )
+        }
+
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            super::stringlist::deserialize(
+                deserializer,
+                &super::UPDATE_NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS,
+            )
+        }
+    }
+
+    impl<T: Ser> Ser for Option<T> {
+        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: Serializer,
+        {
+            match self {
+                None => serializer.serialize_none(),
+                Some(inner) => inner.ser(serializer),
+            }
+        }
+
+        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: Deserializer<'de>,
+        {
+            use std::fmt;
+            use std::marker::PhantomData;
+
+            struct V<T: Ser>(PhantomData<T>);
+
+            impl<'de, T: Ser> serde::de::Visitor<'de> for V<T> {
+                type Value = Option<T>;
+
+                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                    f.write_str("an optional string")
+                }
+
+                fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
+                    Ok(None)
+                }
+
+                fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+                where
+                    D: Deserializer<'de>,
+                {
+                    T::de(deserializer).map(Some)
+                }
+
+                fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                    use serde::de::IntoDeserializer;
+                    T::de(value.into_deserializer()).map(Some)
+                }
+            }
+
+            deserializer.deserialize_option(V::<T>(PhantomData))
+        }
+    }
+
+    pub fn serialize<T, S>(this: &T, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+        T: Ser,
+    {
+        this.ser(serializer)
+    }
+
+    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+        T: Ser,
+    {
+        T::de(deserializer)
+    }
+}
+
+const_regex! {
+
+UPDATE_CLUSTER_FIREWALL_OPTIONS_DELETE_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_35() {
+    use regex::Regex;
+    let _: &Regex = &UPDATE_CLUSTER_FIREWALL_OPTIONS_DELETE_RE;
+}
+#[api(
+    properties: {
+        delete: {
+            items: {
+                description: "List item of type pve-configid.",
+                format: &ApiStringFormat::Pattern(&UPDATE_CLUSTER_FIREWALL_OPTIONS_DELETE_RE),
+                type: String,
+            },
+            optional: true,
+            type: Array,
+        },
+        digest: {
+            max_length: 64,
+            optional: true,
+            type: String,
+        },
+        ebtables: {
+            default: true,
+            optional: true,
+        },
+        enable: {
+            default: 0,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        log_ratelimit: {
+            format: &ApiStringFormat::PropertyString(&ClusterFirewallOptionsLogRatelimit::API_SCHEMA),
+            optional: true,
+            type: String,
+        },
+        policy_forward: {
+            optional: true,
+            type: FirewallFWPolicy,
+        },
+        policy_in: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        policy_out: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct UpdateClusterFirewallOptions {
+    /// A list of settings you want to delete.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub delete: Option<Vec<String>>,
+
+    /// Prevent changes if current configuration file has a different digest.
+    /// This can be used to prevent concurrent modifications.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// Enable ebtables rules cluster wide.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ebtables: Option<bool>,
+
+    /// Enable or disable the firewall cluster wide.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<u64>,
+
+    /// Log ratelimiting settings
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_ratelimit: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_forward: Option<FirewallFWPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_in: Option<FirewallIOPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_out: Option<FirewallIOPolicy>,
+}
+
+const_regex! {
+
+UPDATE_GUEST_FIREWALL_OPTIONS_DELETE_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_36() {
+    use regex::Regex;
+    let _: &Regex = &UPDATE_GUEST_FIREWALL_OPTIONS_DELETE_RE;
+}
+#[api(
+    properties: {
+        delete: {
+            items: {
+                description: "List item of type pve-configid.",
+                format: &ApiStringFormat::Pattern(&UPDATE_GUEST_FIREWALL_OPTIONS_DELETE_RE),
+                type: String,
+            },
+            optional: true,
+            type: Array,
+        },
+        dhcp: {
+            default: false,
+            optional: true,
+        },
+        digest: {
+            max_length: 64,
+            optional: true,
+            type: String,
+        },
+        enable: {
+            default: false,
+            optional: true,
+        },
+        ipfilter: {
+            default: false,
+            optional: true,
+        },
+        log_level_in: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_out: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        macfilter: {
+            default: true,
+            optional: true,
+        },
+        ndp: {
+            default: false,
+            optional: true,
+        },
+        policy_in: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        policy_out: {
+            optional: true,
+            type: FirewallIOPolicy,
+        },
+        radv: {
+            default: false,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct UpdateGuestFirewallOptions {
+    /// A list of settings you want to delete.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub delete: Option<Vec<String>>,
+
+    /// Enable DHCP.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dhcp: Option<bool>,
+
+    /// Prevent changes if current configuration file has a different digest.
+    /// This can be used to prevent concurrent modifications.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// Enable/disable firewall rules.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<bool>,
+
+    /// Enable default IP filters. This is equivalent to adding an empty
+    /// ipfilter-net<id> ipset for every interface. Such ipsets implicitly
+    /// contain sane default restrictions such as restricting IPv6 link local
+    /// addresses to the one derived from the interface's MAC address. For
+    /// containers the configured IP addresses will be implicitly added.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipfilter: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_in: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_out: Option<FirewallLogLevel>,
+
+    /// Enable/disable MAC address filter.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub macfilter: Option<bool>,
+
+    /// Enable NDP (Neighbor Discovery Protocol).
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ndp: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_in: Option<FirewallIOPolicy>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub policy_out: Option<FirewallIOPolicy>,
+
+    /// Allow sending Router Advertisement.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub radv: Option<bool>,
+}
+
+const_regex! {
+
+UPDATE_NODE_FIREWALL_OPTIONS_DELETE_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_37() {
+    use regex::Regex;
+    let _: &Regex = &UPDATE_NODE_FIREWALL_OPTIONS_DELETE_RE;
+}
+#[api(
+    properties: {
+        delete: {
+            items: {
+                description: "List item of type pve-configid.",
+                format: &ApiStringFormat::Pattern(&UPDATE_NODE_FIREWALL_OPTIONS_DELETE_RE),
+                type: String,
+            },
+            optional: true,
+            type: Array,
+        },
+        digest: {
+            max_length: 64,
+            optional: true,
+            type: String,
+        },
+        enable: {
+            default: true,
+            optional: true,
+        },
+        log_level_forward: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_in: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_level_out: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        log_nf_conntrack: {
+            default: false,
+            optional: true,
+        },
+        ndp: {
+            default: false,
+            optional: true,
+        },
+        nf_conntrack_allow_invalid: {
+            default: false,
+            optional: true,
+        },
+        nf_conntrack_helpers: {
+            default: "",
+            format: &ApiStringFormat::PropertyString(&UPDATE_NODE_FIREWALL_OPTIONS_NF_CONNTRACK_HELPERS),
+            optional: true,
+            type: String,
+        },
+        nf_conntrack_max: {
+            default: 262144,
+            minimum: 32768,
+            optional: true,
+            type: Integer,
+        },
+        nf_conntrack_tcp_timeout_established: {
+            default: 432000,
+            minimum: 7875,
+            optional: true,
+            type: Integer,
+        },
+        nf_conntrack_tcp_timeout_syn_recv: {
+            default: 60,
+            maximum: 60,
+            minimum: 30,
+            optional: true,
+            type: Integer,
+        },
+        nftables: {
+            default: false,
+            optional: true,
+        },
+        nosmurfs: {
+            default: false,
+            optional: true,
+        },
+        protection_synflood: {
+            default: false,
+            optional: true,
+        },
+        protection_synflood_burst: {
+            default: 1000,
+            optional: true,
+            type: Integer,
+        },
+        protection_synflood_rate: {
+            default: 200,
+            optional: true,
+            type: Integer,
+        },
+        smurf_log_level: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        tcp_flags_log_level: {
+            optional: true,
+            type: FirewallLogLevel,
+        },
+        tcpflags: {
+            default: false,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct UpdateNodeFirewallOptions {
+    /// A list of settings you want to delete.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub delete: Option<Vec<String>>,
+
+    /// Prevent changes if current configuration file has a different digest.
+    /// This can be used to prevent concurrent modifications.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// Enable host firewall rules.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub enable: Option<bool>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_forward: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_in: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_level_out: Option<FirewallLogLevel>,
+
+    /// Enable logging of conntrack information.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub log_nf_conntrack: Option<bool>,
+
+    /// Enable NDP (Neighbor Discovery Protocol).
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ndp: Option<bool>,
+
+    /// Allow invalid packets on connection tracking.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_allow_invalid: Option<bool>,
+
+    /// Enable conntrack helpers for specific protocols. Supported protocols:
+    /// amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp
+    #[serde(with = "update_node_firewall_options_nf_conntrack_helpers")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_helpers: Option<Vec<FwConntrackHelper>>,
+
+    /// Maximum number of tracked connections.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_max: Option<u64>,
+
+    /// Conntrack established timeout.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_tcp_timeout_established: Option<u64>,
+
+    /// Conntrack syn recv timeout.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u8")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nf_conntrack_tcp_timeout_syn_recv: Option<u8>,
+
+    /// Enable nftables based firewall (tech preview)
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nftables: Option<bool>,
+
+    /// Enable SMURFS filter.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nosmurfs: Option<bool>,
+
+    /// Enable synflood protection
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood: Option<bool>,
+
+    /// Synflood protection rate burst by ip src.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood_burst: Option<i64>,
+
+    /// Synflood protection rate syn/sec by ip src.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub protection_synflood_rate: Option<i64>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub smurf_log_level: Option<FirewallLogLevel>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tcp_flags_log_level: Option<FirewallLogLevel>,
+
+    /// Filter illegal combinations of TCP flags.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tcpflags: Option<bool>,
+}
+
+const_regex! {
+
+UPDATE_QEMU_CONFIG_AFFINITY_RE = r##"^(\s*\d+(-\d+)?\s*)(,\s*\d+(-\d+)?\s*)?$"##;
+UPDATE_QEMU_CONFIG_BOOTDISK_RE = r##"^(ide|sata|scsi|virtio|efidisk|tpmstate)\d+$"##;
+UPDATE_QEMU_CONFIG_DELETE_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
+UPDATE_QEMU_CONFIG_IMPORT_WORKING_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
+UPDATE_QEMU_CONFIG_REVERT_RE = r##"^(?i:[a-z][a-z0-9_-]+)$"##;
+UPDATE_QEMU_CONFIG_SSHKEYS_RE = r##"^[-%a-zA-Z0-9_.!~*'()]*$"##;
+UPDATE_QEMU_CONFIG_TAGS_RE = r##"^(?i)[a-z0-9_][a-z0-9_\-+.]*$"##;
+UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_38() {
+    use regex::Regex;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_AFFINITY_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_BOOTDISK_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_DELETE_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_IMPORT_WORKING_STORAGE_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_REVERT_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_SSHKEYS_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_TAGS_RE;
+    let _: &Regex = &UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE;
+}
+#[api(
+    properties: {
+        acpi: {
+            default: true,
+            optional: true,
+        },
+        affinity: {
+            format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_AFFINITY_RE),
+            optional: true,
+            type: String,
+        },
+        agent: {
             format: &ApiStringFormat::PropertyString(&QemuConfigAgent::API_SCHEMA),
             optional: true,
             type: String,
@@ -12684,7 +13928,7 @@ UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_36() {
+fn test_regex_compilation_39() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE;
 }
@@ -12761,7 +14005,7 @@ UPDATE_QEMU_CONFIG_IDE_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_37() {
+fn test_regex_compilation_40() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_MODEL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_SERIAL_RE;
@@ -13117,7 +14361,7 @@ UPDATE_QEMU_CONFIG_SATA_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_38() {
+fn test_regex_compilation_41() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SIZE_RE;
@@ -13462,7 +14706,7 @@ UPDATE_QEMU_CONFIG_SCSI_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_39() {
+fn test_regex_compilation_42() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SIZE_RE;
@@ -13862,7 +15106,7 @@ UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_40() {
+fn test_regex_compilation_43() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE;
 }
@@ -13918,7 +15162,7 @@ UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_41() {
+fn test_regex_compilation_44() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE;
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-yew-comp 1/4] form: add helpers for extractig data out of schemas
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (4 preceding siblings ...)
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 5/5] pve-api-types: regenerate Hannes Laimer
@ 2025-10-30 14:33 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 2/4] firewall: add FirewallContext Hannes Laimer
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:33 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/form/mod.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/src/form/mod.rs b/src/form/mod.rs
index 0c23053..4e17d5b 100644
--- a/src/form/mod.rs
+++ b/src/form/mod.rs
@@ -74,6 +74,76 @@ pub fn typed_load<T: DeserializeOwned + Serialize>(
     .url(url_cloned)
 }
 
+/// Get a nested field schema from a parent schema by traversing a path
+///
+/// This function recursively traverses through Object schemas and PropertyString schemas
+/// to find the nested field schema at the given path.
+///
+/// # Arguments
+/// * `s` - The parent schema to traverse
+/// * `field` - A vector of field names representing the path to traverse
+///
+/// # Returns
+/// The schema at the specified path, or the input schema if the path cannot be traversed
+pub fn get_field_schema(s: &'static Schema, mut field: Vec<&str>) -> &'static Schema {
+    let Some(looking_for) = field.first() else {
+        return s;
+    };
+
+    if let Schema::Object(s) = s {
+        for (name, _, ss) in s.properties() {
+            if *looking_for == *name {
+                field.remove(0);
+                return get_field_schema(ss, field);
+            }
+        }
+    }
+    if let Schema::String(s) = s {
+        if let Some(proxmox_schema::ApiStringFormat::PropertyString(pss)) = s.format {
+            return get_field_schema(pss, field);
+        }
+    }
+    s
+}
+
+/// Extract the the placeholder for a fields from a schema
+pub fn placeholder_from_schema(schema: &'static proxmox_schema::Schema) -> String {
+    if let proxmox_schema::Schema::String(s) = schema {
+        if let Some(v) = s.default {
+            return v.to_string();
+        }
+    }
+    if let proxmox_schema::Schema::Integer(s) = schema {
+        if let Some(v) = s.default {
+            return v.to_string();
+        }
+    }
+    if let proxmox_schema::Schema::Number(s) = schema {
+        if let Some(v) = s.default {
+            return v.to_string();
+        }
+    }
+    "".to_string()
+}
+
+/// Extract the enum varian items from a schema
+///
+/// Can be used to populate items for a combobox
+pub fn enum_items_from_schema(
+    schema: &'static proxmox_schema::Schema,
+) -> Vec<pwt::prelude::AttrValue> {
+    if let proxmox_schema::Schema::String(s) = schema {
+        if let Some(proxmox_schema::ApiStringFormat::Enum(e)) = s.format {
+            let items: Vec<pwt::prelude::AttrValue> = e
+                .iter()
+                .map(|entry| entry.value.to_string().into())
+                .collect();
+            return items;
+        }
+    }
+    vec![]
+}
+
 /// Convert a property string to separate properties
 ///
 /// This is useful for use in an [`crate::PropertyEditDialog`] when editing parts of a property 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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-yew-comp 2/4] firewall: add FirewallContext
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (5 preceding siblings ...)
  2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox-yew-comp 1/4] form: add helpers for extractig data out of schemas Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 3/4] firewall: add options edit form Hannes Laimer
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

Both the options edit form and the rules table share the same layout for
multiple endpoints/entities. This unifies that so we can use the same
components by just initialising this context depending on where we get
our data from or have to send it to.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/firewall/context.rs | 142 ++++++++++++++++++++++++++++++++++++++++
 src/firewall/mod.rs     |   2 +
 src/lib.rs              |   3 +
 3 files changed, 147 insertions(+)
 create mode 100644 src/firewall/context.rs
 create mode 100644 src/firewall/mod.rs

diff --git a/src/firewall/context.rs b/src/firewall/context.rs
new file mode 100644
index 0000000..6495fa3
--- /dev/null
+++ b/src/firewall/context.rs
@@ -0,0 +1,142 @@
+use crate::percent_encoding::percent_encode_component;
+use pwt::prelude::*;
+
+/// Context defining the scope of firewall configuration (Cluster, Node, or Guest level)
+#[derive(Clone, PartialEq)]
+pub enum FirewallContext {
+    Cluster {
+        remote: AttrValue,
+    },
+    Node {
+        remote: AttrValue,
+        node: AttrValue,
+    },
+    Guest {
+        remote: AttrValue,
+        node: AttrValue,
+        vmid: u64,
+        vmtype: AttrValue,
+    },
+}
+
+impl FirewallContext {
+    pub fn cluster(remote: impl Into<AttrValue>) -> Self {
+        Self::Cluster {
+            remote: remote.into(),
+        }
+    }
+
+    pub fn node(remote: impl Into<AttrValue>, node: impl Into<AttrValue>) -> Self {
+        Self::Node {
+            remote: remote.into(),
+            node: node.into(),
+        }
+    }
+
+    pub fn guest(
+        remote: impl Into<AttrValue>,
+        node: impl Into<AttrValue>,
+        vmid: u64,
+        vmtype: impl Into<AttrValue>,
+    ) -> Self {
+        Self::Guest {
+            remote: remote.into(),
+            node: node.into(),
+            vmid,
+            vmtype: vmtype.into(),
+        }
+    }
+
+    pub fn rules_url(&self) -> String {
+        match self {
+            Self::Cluster { remote } => {
+                format!(
+                    "/pve/remotes/{}/firewall/rules",
+                    percent_encode_component(remote)
+                )
+            }
+            Self::Node { remote, node } => {
+                format!(
+                    "/pve/remotes/{}/nodes/{}/firewall/rules",
+                    percent_encode_component(remote),
+                    percent_encode_component(node)
+                )
+            }
+            Self::Guest {
+                remote,
+                node,
+                vmid,
+                vmtype,
+            } => {
+                let mut url = format!(
+                    "/pve/remotes/{}/{}/{}/firewall/rules",
+                    percent_encode_component(remote),
+                    percent_encode_component(vmtype),
+                    vmid
+                );
+                if !node.is_empty() {
+                    url = format!("{}?node={}", url, percent_encode_component(node));
+                }
+                url
+            }
+        }
+    }
+
+    pub fn options_url(&self) -> String {
+        match self {
+            Self::Cluster { remote } => {
+                format!(
+                    "/pve/remotes/{}/firewall/options",
+                    percent_encode_component(remote)
+                )
+            }
+            Self::Node { remote, node } => {
+                format!(
+                    "/pve/remotes/{}/nodes/{}/firewall/options",
+                    percent_encode_component(remote),
+                    percent_encode_component(node)
+                )
+            }
+            Self::Guest {
+                remote,
+                node,
+                vmid,
+                vmtype,
+            } => {
+                let mut url = format!(
+                    "/pve/remotes/{}/{}/{}/firewall/options",
+                    percent_encode_component(remote),
+                    percent_encode_component(vmtype),
+                    vmid
+                );
+                if !node.is_empty() {
+                    url = format!("{}?node={}", url, percent_encode_component(node));
+                }
+                url
+            }
+        }
+    }
+
+    pub fn title(&self, prefix: &str) -> String {
+        match self {
+            Self::Cluster { remote } => {
+                if !remote.is_empty() {
+                    format!("{}: {}", prefix, remote)
+                } else {
+                    prefix.to_string()
+                }
+            }
+            Self::Node { remote, node } => {
+                format!("{}: {}/{}", prefix, remote, node)
+            }
+            Self::Guest {
+                remote,
+                vmtype,
+                vmid,
+                ..
+            } => {
+                format!("{}: {}/{} {}", prefix, remote, vmtype.to_uppercase(), vmid)
+            }
+        }
+    }
+}
diff --git a/src/firewall/mod.rs b/src/firewall/mod.rs
new file mode 100644
index 0000000..49dcf23
--- /dev/null
+++ b/src/firewall/mod.rs
@@ -0,0 +1,2 @@
+mod context;
+pub use context::FirewallContext;
diff --git a/src/lib.rs b/src/lib.rs
index 3a9e32b..e2e2721 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -129,6 +129,9 @@ mod rrd_timeframe_selector;
 #[cfg(feature = "rrd")]
 pub use rrd_timeframe_selector::{RRDTimeframe, RRDTimeframeSelector};
 
+mod firewall;
+pub use firewall::FirewallContext;
+
 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


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-yew-comp 3/4] firewall: add options edit form
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (6 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 2/4] firewall: add FirewallContext Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 4/4] firewall: add rules table Hannes Laimer
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

This also includes the log-ratelimit field, its value is a property
string.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/firewall/log_ratelimit_field.rs | 310 +++++++++++++++++++++
 src/firewall/mod.rs                 |   6 +
 src/firewall/options_edit.rs        | 404 ++++++++++++++++++++++++++++
 src/lib.rs                          |   2 +-
 4 files changed, 721 insertions(+), 1 deletion(-)
 create mode 100644 src/firewall/log_ratelimit_field.rs
 create mode 100644 src/firewall/options_edit.rs

diff --git a/src/firewall/log_ratelimit_field.rs b/src/firewall/log_ratelimit_field.rs
new file mode 100644
index 0000000..c9daeed
--- /dev/null
+++ b/src/firewall/log_ratelimit_field.rs
@@ -0,0 +1,310 @@
+use anyhow::Error;
+use proxmox_schema::ApiType;
+use serde_json::Value;
+use yew::html::{IntoEventCallback, IntoPropValue};
+
+use pwt::prelude::*;
+use pwt::props::FieldBuilder;
+use pwt::widget::form::{
+    Checkbox, Combobox, ManagedField, ManagedFieldContext, ManagedFieldMaster, ManagedFieldState,
+    Number,
+};
+use pwt::widget::{InputPanel, Row};
+
+use pwt::props::WidgetBuilder;
+use pwt_macros::{builder, widget};
+
+use crate::SchemaValidation;
+
+const TIME_UNITS: &[&str] = &["second", "minute", "hour", "day"];
+
+#[widget(comp=RateFieldImpl, @input)]
+#[derive(Clone, Properties, PartialEq)]
+#[builder]
+pub struct RateField {
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    pub value: Option<AttrValue>,
+
+    #[builder_cb(IntoEventCallback, into_event_callback, String)]
+    #[prop_or_default]
+    pub on_input: Option<Callback<String>>,
+}
+
+impl Default for RateField {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl RateField {
+    pub fn new() -> Self {
+        yew::props!(Self {})
+    }
+}
+
+pub enum RateMsg {
+    ChangeNumber(Option<u64>),
+    ChangeUnit(String),
+}
+
+pub struct RateFieldImpl {
+    number: Option<u64>,
+    unit: String,
+}
+
+impl yew::Component for RateFieldImpl {
+    type Message = RateMsg;
+    type Properties = RateField;
+
+    fn create(ctx: &yew::Context<Self>) -> Self {
+        let mut me = Self {
+            number: None,
+            unit: "second".to_string(),
+        };
+        me.parse_value(&ctx.props().value);
+        me
+    }
+
+    fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            RateMsg::ChangeNumber(number) => {
+                self.number = number;
+            }
+            RateMsg::ChangeUnit(unit) => self.unit = unit,
+        }
+
+        let new_value_str = if let Some(num) = self.number {
+            format!("{}/{}", num, self.unit)
+        } else {
+            String::new()
+        };
+
+        if let Some(callback) = &ctx.props().on_input {
+            callback.emit(new_value_str);
+        }
+
+        true
+    }
+
+    fn changed(&mut self, ctx: &yew::Context<Self>, _old_props: &Self::Properties) -> bool {
+        self.parse_value(&ctx.props().value);
+        true
+    }
+
+    fn view(&self, ctx: &yew::Context<Self>) -> Html {
+        let is_empty = self.number.is_none();
+        let number_value = self.number.map(|n| n.to_string()).unwrap_or_default();
+
+        let units: Vec<AttrValue> = TIME_UNITS.iter().map(|&u| AttrValue::from(u)).collect();
+
+        Row::new()
+            .style("align-items", "center")
+            .gap(1)
+            .with_child(
+                Number::<u64>::new()
+                    .key("rate_number")
+                    .value(number_value)
+                    .placeholder("1")
+                    .min(1)
+                    .on_change(ctx.link().callback(|result: Option<Result<u64, String>>| {
+                        RateMsg::ChangeNumber(result.and_then(|r| r.ok()))
+                    })),
+            )
+            .with_child("/")
+            .with_child(
+                Combobox::new()
+                    .key("rate_unit")
+                    .items(std::rc::Rc::new(units))
+                    .value(self.unit.clone())
+                    .disabled(is_empty)
+                    .required(true)
+                    .on_change(ctx.link().callback(RateMsg::ChangeUnit)),
+            )
+            .into()
+    }
+}
+
+impl RateFieldImpl {
+    fn parse_value(&mut self, value: &Option<AttrValue>) {
+        self.number = None;
+        self.unit = "second".to_string();
+
+        if let Some(v) = value {
+            if !v.is_empty() {
+                if let Some((num, unit)) = v.split_once('/') {
+                    self.number = num.parse::<u64>().ok();
+                    self.unit = unit.to_string();
+                }
+            }
+        }
+    }
+}
+
+#[widget(comp=ManagedFieldMaster<LogRatelimitFieldImpl>, @input)]
+#[derive(Clone, Properties, PartialEq)]
+#[builder]
+pub struct LogRatelimitField {
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    pub default: Option<AttrValue>,
+}
+
+impl Default for LogRatelimitField {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl LogRatelimitField {
+    pub fn new() -> Self {
+        yew::props!(Self {})
+    }
+}
+
+pub enum LogRatelimitMsg {
+    Enable(bool),
+    Rate(String),
+    Burst(Option<u64>),
+}
+
+pub struct LogRatelimitFieldImpl {
+    enable: bool,
+    rate: String,
+    burst: Option<u64>,
+}
+
+impl ManagedField for LogRatelimitFieldImpl {
+    type Message = LogRatelimitMsg;
+    type Properties = LogRatelimitField;
+    type ValidateClosure = ();
+
+    fn validation_args(_props: &Self::Properties) -> Self::ValidateClosure {}
+
+    fn validator(_props: &Self::ValidateClosure, value: &Value) -> Result<Value, Error> {
+        Ok(value.clone())
+    }
+
+    fn setup(props: &Self::Properties) -> ManagedFieldState {
+        let value = Value::Null;
+        let default = match &props.default {
+            Some(d) => Value::String(d.to_string()),
+            None => Value::String(String::new()),
+        };
+        ManagedFieldState::new(value, default)
+    }
+
+    fn value_changed(&mut self, ctx: &ManagedFieldContext<Self>) {
+        let state = ctx.state();
+
+        // Initialize with API defaults (enable=true is the API default)
+        // When the property string is empty, rate and burst are unset
+        self.enable = true;
+        self.rate = String::new();
+        self.burst = None;
+
+        // If value is Null, use the default instead
+        let value_to_parse = match &state.value {
+            Value::Null => &state.default,
+            other => other,
+        };
+
+        if let Value::String(v) = value_to_parse {
+            if !v.is_empty() {
+                match pve_api_types::ClusterFirewallOptionsLogRatelimit::API_SCHEMA
+                    .parse_property_string(v)
+                {
+                    Ok(parsed) => {
+                        match serde_json::from_value::<
+                            pve_api_types::ClusterFirewallOptionsLogRatelimit,
+                        >(parsed)
+                        {
+                            Ok(ratelimit) => {
+                                self.enable = ratelimit.enable;
+                                if let Some(rate) = ratelimit.rate {
+                                    self.rate = rate.clone();
+                                }
+                                self.burst = ratelimit.burst;
+                            }
+                            Err(e) => {
+                                log::error!("Failed to parse log_ratelimit value: {:?}", e);
+                            }
+                        }
+                    }
+                    Err(e) => {
+                        log::error!(
+                            "Failed to parse log_ratelimit property string '{}': {:?}",
+                            v,
+                            e
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    fn create(ctx: &ManagedFieldContext<Self>) -> Self {
+        let mut me = Self {
+            enable: true,
+            rate: String::new(),
+            burst: None,
+        };
+        me.value_changed(ctx);
+        me
+    }
+
+    fn update(&mut self, ctx: &ManagedFieldContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            LogRatelimitMsg::Enable(enable) => self.enable = enable,
+            LogRatelimitMsg::Rate(rate) => self.rate = rate,
+            LogRatelimitMsg::Burst(burst_value) => self.burst = burst_value,
+        }
+
+        let mut parts = Vec::new();
+        parts.push(format!("enable={}", if self.enable { 1 } else { 0 }));
+        if !self.rate.is_empty() {
+            parts.push(format!("rate={}", self.rate));
+        }
+        if let Some(burst) = self.burst {
+            parts.push(format!("burst={}", burst));
+        }
+        let property_string = parts.join(",");
+        let new_value = Value::String(property_string);
+
+        ctx.link().update_value(new_value);
+        true
+    }
+
+    fn view(&self, ctx: &ManagedFieldContext<Self>) -> Html {
+        let props = ctx.props();
+        let base_schema = &pve_api_types::ClusterFirewallOptionsLogRatelimit::API_SCHEMA;
+
+        InputPanel::new()
+            .with_std_props(&props.std_props)
+            .with_field(
+                tr!("Enable"),
+                Checkbox::new()
+                    .key("enable")
+                    .checked(self.enable)
+                    .on_change(ctx.link().callback(LogRatelimitMsg::Enable)),
+            )
+            .with_field(
+                tr!("Rate"),
+                RateField::new()
+                    .key("rate")
+                    .value(self.rate.clone())
+                    .on_input(ctx.link().callback(LogRatelimitMsg::Rate)),
+            )
+            .with_field(
+                tr!("Burst"),
+                Number::<u64>::new()
+                    .key("burst")
+                    .value(self.burst.map(|b| b.to_string()))
+                    .on_change(ctx.link().callback(|result: Option<Result<u64, String>>| {
+                        LogRatelimitMsg::Burst(result.and_then(|r| r.ok()))
+                    }))
+                    .schema(crate::form::get_field_schema(base_schema, vec!["burst"])),
+            )
+            .into()
+    }
+}
diff --git a/src/firewall/mod.rs b/src/firewall/mod.rs
index 49dcf23..379b958 100644
--- a/src/firewall/mod.rs
+++ b/src/firewall/mod.rs
@@ -1,2 +1,8 @@
 mod context;
 pub use context::FirewallContext;
+
+mod options_edit;
+pub use options_edit::EditFirewallOptions;
+
+mod log_ratelimit_field;
+pub use log_ratelimit_field::LogRatelimitField;
diff --git a/src/firewall/options_edit.rs b/src/firewall/options_edit.rs
new file mode 100644
index 0000000..6b0c4e2
--- /dev/null
+++ b/src/firewall/options_edit.rs
@@ -0,0 +1,404 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use proxmox_schema::ApiType;
+use pve_api_types::{ClusterFirewallOptions, GuestFirewallOptions, NodeFirewallOptions};
+use serde_json::Value;
+use yew::html::{IntoEventCallback, IntoPropValue};
+use yew::virtual_dom::{VComp, VNode};
+
+use pwt::prelude::*;
+use pwt::widget::form::{Checkbox, Combobox, FormContext, Number};
+use pwt::widget::InputPanel;
+
+use pwt_macros::builder;
+
+use crate::{form::delete_empty_values, ApiLoadCallback, EditWindow};
+
+use super::{context::FirewallContext, LogRatelimitField};
+
+fn enum_items_from_schema<T: ApiType>(name: &str) -> Vec<AttrValue> {
+    let s = crate::form::get_field_schema(&T::API_SCHEMA, vec![name]);
+    crate::form::enum_items_from_schema(s)
+}
+
+fn placeholder_from_schema<T: ApiType>(name: &str) -> String {
+    let s = crate::form::get_field_schema(&T::API_SCHEMA, vec![name]);
+    crate::form::placeholder_from_schema(s)
+}
+
+fn create_firewall_options_loader<F>(url: AttrValue, transform_fn: F) -> ApiLoadCallback<Value>
+where
+    F: Fn(&mut serde_json::Map<String, Value>) + Clone + 'static,
+{
+    ApiLoadCallback::new(move || {
+        let url = url.clone();
+        let transform_fn = transform_fn.clone();
+        async move {
+            let mut resp = crate::http_get_full(url.to_string(), None).await?;
+            if let serde_json::Value::Object(ref mut map) = resp.data {
+                transform_fn(map);
+            }
+            Ok::<_, anyhow::Error>(resp)
+        }
+    })
+}
+
+async fn update_firewall_options(
+    form_ctx: FormContext,
+    url: AttrValue,
+    fields: &[&str],
+    transform_fn: Option<impl FnOnce(&mut serde_json::Map<String, Value>)>,
+) -> Result<(), Error> {
+    let mut data = form_ctx.get_submit_data();
+
+    if let (Some(transform), serde_json::Value::Object(ref mut map)) = (transform_fn, &mut data) {
+        transform(map);
+    }
+
+    let data = delete_empty_values(&data, fields, true);
+
+    crate::http_put(&url.to_string(), Some(data)).await
+}
+
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct EditFirewallOptions {
+    #[builder(IntoPropValue, into_prop_value)]
+    pub context: FirewallContext,
+
+    #[builder_cb(IntoEventCallback, into_event_callback, ())]
+    #[prop_or_default]
+    pub on_close: Option<Callback<()>>,
+}
+
+impl EditFirewallOptions {
+    pub fn cluster(remote: impl Into<AttrValue>) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::cluster(remote),
+        })
+    }
+
+    pub fn node(remote: impl Into<AttrValue>, node: impl Into<AttrValue>) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::node(remote, node),
+        })
+    }
+
+    pub fn guest(
+        remote: impl Into<AttrValue>,
+        node: impl Into<AttrValue>,
+        vmid: u64,
+        vmtype: impl Into<AttrValue>,
+    ) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::guest(remote, node, vmid, vmtype),
+        })
+    }
+}
+
+pub struct ProxmoxEditFirewallOptions {
+    loader: Option<ApiLoadCallback<Value>>,
+}
+
+impl Component for ProxmoxEditFirewallOptions {
+    type Message = ();
+    type Properties = EditFirewallOptions;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let props = ctx.props();
+        let url: AttrValue = props.context.options_url().into();
+
+        let loader = if !url.is_empty() {
+            Some(create_firewall_options_loader(url, |map| {
+                // Convert enable field from u64 to bool for cluster firewall
+                if let Some(enable_num) = map.get("enable").and_then(|v| v.as_u64()) {
+                    map.insert("enable".into(), serde_json::Value::Bool(enable_num != 0));
+                }
+            }))
+        } else {
+            None
+        };
+
+        Self { loader }
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let props = ctx.props();
+        let url: AttrValue = props.context.options_url().into();
+
+        type ContextConfig<'a> = (
+            String,
+            fn(&FormContext) -> Html,
+            &'a [&'a str],
+            Option<fn(&mut serde_json::Map<String, Value>)>,
+        );
+        let (title, renderer, fields, transform_fn): ContextConfig<'_> = match &props.context {
+            FirewallContext::Cluster { .. } => (
+                props.context.title(&tr!("Edit Cluster Firewall")),
+                edit_cluster_firewall_input_panel,
+                &[
+                    "enable",
+                    "ebtables",
+                    "policy_in",
+                    "policy_out",
+                    "policy_forward",
+                    "log_ratelimit",
+                ],
+                Some(|map: &mut serde_json::Map<String, Value>| {
+                    if let Some(enable) = map.get("enable").and_then(|v| v.as_bool()) {
+                        map.insert("enable".into(), Value::from(if enable { 1 } else { 0 }));
+                    }
+                }),
+            ),
+            FirewallContext::Node { .. } => (
+                props.context.title(&tr!("Edit Node Firewall")),
+                edit_node_firewall_input_panel,
+                &[
+                    "enable",
+                    "ndp",
+                    "log_nf_conntrack",
+                    "nf_conntrack_allow_invalid",
+                    "log_level_in",
+                    "log_level_out",
+                    "log_level_forward",
+                    "nf_conntrack_max",
+                    "nf_conntrack_tcp_timeout_established",
+                    "nf_conntrack_tcp_timeout_syn_recv",
+                ],
+                None,
+            ),
+            FirewallContext::Guest { .. } => (
+                props.context.title(&tr!("Edit Guest Firewall")),
+                edit_guest_firewall_input_panel,
+                &[
+                    "enable",
+                    "dhcp",
+                    "ipfilter",
+                    "macfilter",
+                    "ndp",
+                    "radv",
+                    "policy_in",
+                    "policy_out",
+                    "log_level_in",
+                    "log_level_out",
+                ],
+                None,
+            ),
+        };
+
+        EditWindow::new(title)
+            .loader(self.loader.clone())
+            .on_close(props.on_close.clone())
+            .on_done(props.on_close.clone())
+            .renderer(renderer)
+            .on_submit({
+                let url = url.clone();
+                move |form_ctx: FormContext| {
+                    let url = url.clone();
+                    let fields = fields.to_vec();
+                    async move { update_firewall_options(form_ctx, url, &fields, transform_fn).await }
+                }
+            })
+            .into()
+    }
+}
+
+fn edit_cluster_firewall_input_panel(_form_ctx: &FormContext) -> Html {
+    InputPanel::new()
+        .padding(4)
+        .with_large_field(
+            tr!("Enable Firewall"),
+            Checkbox::new().name("enable").key("enable"),
+        )
+        .with_large_field(
+            tr!("Enable ebtables"),
+            Checkbox::new().name("ebtables").key("ebtables"),
+        )
+        .with_field(
+            tr!("Input Policy"),
+            Combobox::new()
+                .name("policy_in")
+                .key("policy_in")
+                .placeholder(placeholder_from_schema::<ClusterFirewallOptions>(
+                    "policy_in",
+                ))
+                .items(enum_items_from_schema::<ClusterFirewallOptions>("policy_in").into()),
+        )
+        .with_field(
+            tr!("Output Policy"),
+            Combobox::new()
+                .name("policy_out")
+                .key("policy_out")
+                .placeholder(placeholder_from_schema::<ClusterFirewallOptions>(
+                    "policy_out",
+                ))
+                .items(enum_items_from_schema::<ClusterFirewallOptions>("policy_out").into()),
+        )
+        .with_field(
+            tr!("Forward Policy"),
+            Combobox::new()
+                .name("policy_forward")
+                .key("policy_forward")
+                .placeholder(placeholder_from_schema::<ClusterFirewallOptions>(
+                    "policy_forward",
+                ))
+                .items(enum_items_from_schema::<ClusterFirewallOptions>("policy_forward").into()),
+        )
+        .with_large_field(
+            tr!("Log Rate Limiting"),
+            LogRatelimitField::new()
+                .name("log_ratelimit")
+                .key("log_ratelimit"),
+        )
+        .into()
+}
+
+impl From<EditFirewallOptions> for VNode {
+    fn from(val: EditFirewallOptions) -> Self {
+        let comp = VComp::new::<ProxmoxEditFirewallOptions>(Rc::new(val), None);
+        VNode::from(comp)
+    }
+}
+
+fn edit_guest_firewall_input_panel(_form_ctx: &FormContext) -> Html {
+    InputPanel::new()
+        .padding(4)
+        .with_field(
+            tr!("Enable Firewall"),
+            Checkbox::new().name("enable").key("enable"),
+        )
+        .with_right_field(tr!("Enable DHCP"), Checkbox::new().name("dhcp").key("dhcp"))
+        .with_field(
+            tr!("Enable IP Filter"),
+            Checkbox::new().name("ipfilter").key("ipfilter"),
+        )
+        .with_right_field(
+            tr!("Enable MAC Filter"),
+            Checkbox::new().name("macfilter").key("macfilter"),
+        )
+        .with_field(tr!("Enable NDP"), Checkbox::new().name("ndp").key("ndp"))
+        .with_right_field(
+            tr!("Allow Router Advertisement"),
+            Checkbox::new().name("radv").key("radv"),
+        )
+        .with_field(
+            tr!("Input Policy"),
+            Combobox::new()
+                .name("policy_in")
+                .key("policy_in")
+                .placeholder(placeholder_from_schema::<GuestFirewallOptions>("policy_in"))
+                .items(enum_items_from_schema::<GuestFirewallOptions>("policy_in").into()),
+        )
+        .with_right_field(
+            tr!("Output Policy"),
+            Combobox::new()
+                .name("policy_out")
+                .key("policy_out")
+                .placeholder(placeholder_from_schema::<GuestFirewallOptions>(
+                    "policy_out",
+                ))
+                .items(enum_items_from_schema::<GuestFirewallOptions>("policy_out").into()),
+        )
+        .with_field(
+            tr!("Log Level In"),
+            Combobox::new()
+                .name("log_level_in")
+                .key("log_level_in")
+                .placeholder(placeholder_from_schema::<GuestFirewallOptions>(
+                    "log_level_in",
+                ))
+                .items(enum_items_from_schema::<GuestFirewallOptions>("log_level_in").into()),
+        )
+        .with_right_field(
+            tr!("Log Level Out"),
+            Combobox::new()
+                .name("log_level_out")
+                .key("log_level_out")
+                .placeholder(placeholder_from_schema::<GuestFirewallOptions>(
+                    "log_level_out",
+                ))
+                .items(enum_items_from_schema::<GuestFirewallOptions>("log_level_out").into()),
+        )
+        .into()
+}
+
+fn edit_node_firewall_input_panel(_form_ctx: &FormContext) -> Html {
+    InputPanel::new()
+        .padding(4)
+        .with_field(
+            tr!("Enable Firewall"),
+            Checkbox::new().name("enable").key("enable"),
+        )
+        .with_right_field(tr!("Enable NDP"), Checkbox::new().name("ndp").key("ndp"))
+        .with_field(
+            tr!("Log Connection Tracking"),
+            Checkbox::new()
+                .name("log_nf_conntrack")
+                .key("log_nf_conntrack"),
+        )
+        .with_right_field(
+            tr!("Allow Invalid Connections"),
+            Checkbox::new()
+                .name("nf_conntrack_allow_invalid")
+                .key("nf_conntrack_allow_invalid"),
+        )
+        .with_field(
+            tr!("Log Level In"),
+            Combobox::new()
+                .name("log_level_in")
+                .key("log_level_in")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "log_level_in",
+                ))
+                .items(enum_items_from_schema::<NodeFirewallOptions>("log_level_in").into()),
+        )
+        .with_right_field(
+            tr!("Log Level Out"),
+            Combobox::new()
+                .name("log_level_out")
+                .key("log_level_out")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "log_level_out",
+                ))
+                .items(enum_items_from_schema::<NodeFirewallOptions>("log_level_out").into()),
+        )
+        .with_field(
+            tr!("Log Level Forward"),
+            Combobox::new()
+                .name("log_level_forward")
+                .key("log_level_forward")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "log_level_forward",
+                ))
+                .items(enum_items_from_schema::<NodeFirewallOptions>("log_level_forward").into()),
+        )
+        .with_right_field(
+            tr!("Connection Tracking Max"),
+            Number::<u64>::new()
+                .name("nf_conntrack_max")
+                .key("nf_conntrack_max")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "nf_conntrack_max",
+                )),
+        )
+        .with_field(
+            tr!("TCP Timeout Established"),
+            Number::<u64>::new()
+                .name("nf_conntrack_tcp_timeout_established")
+                .key("nf_conntrack_tcp_timeout_established")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "nf_conntrack_tcp_timeout_established",
+                )),
+        )
+        .with_right_field(
+            tr!("TCP Timeout SYN Recv"),
+            Number::<u8>::new()
+                .name("nf_conntrack_tcp_timeout_syn_recv")
+                .key("nf_conntrack_tcp_timeout_syn_recv")
+                .placeholder(placeholder_from_schema::<NodeFirewallOptions>(
+                    "nf_conntrack_tcp_timeout_syn_recv",
+                )),
+        )
+        .into()
+}
diff --git a/src/lib.rs b/src/lib.rs
index e2e2721..852d65d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -130,7 +130,7 @@ mod rrd_timeframe_selector;
 pub use rrd_timeframe_selector::{RRDTimeframe, RRDTimeframeSelector};
 
 mod firewall;
-pub use firewall::FirewallContext;
+pub use firewall::{EditFirewallOptions, FirewallContext};
 
 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


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-yew-comp 4/4] firewall: add rules table
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (7 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 3/4] firewall: add options edit form Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] pdm-api-types: add firewall status types Hannes Laimer
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: 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 <h.laimer@proxmox.com>
---
 src/firewall/mod.rs   |   3 +
 src/firewall/rules.rs | 217 ++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs            |   2 +-
 3 files changed, 221 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..9a96379
--- /dev/null
+++ b/src/firewall/rules.rs
@@ -0,0 +1,217 @@
+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;
+use pwt_macros::builder;
+
+use super::context::FirewallContext;
+
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct FirewallRules {
+    #[builder(IntoPropValue, into_prop_value)]
+    pub context: FirewallContext,
+
+    #[builder_cb(IntoEventCallback, into_event_callback, ())]
+    #[prop_or_default]
+    pub on_close: Option<Callback<()>>,
+}
+
+impl FirewallRules {
+    pub fn cluster(remote: impl Into<AttrValue>) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::cluster(remote),
+        })
+    }
+
+    pub fn node(remote: impl Into<AttrValue>, node: impl Into<AttrValue>) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::node(remote, node),
+        })
+    }
+
+    pub fn guest(
+        remote: impl Into<AttrValue>,
+        node: impl Into<AttrValue>,
+        vmid: u64,
+        vmtype: impl Into<AttrValue>,
+    ) -> Self {
+        yew::props!(Self {
+            context: FirewallContext::guest(remote, node, vmid, vmtype),
+        })
+    }
+}
+
+pub enum FirewallMsg {
+    DataChange,
+}
+
+#[doc(hidden)]
+pub struct ProxmoxFirewallRules {
+    store: Store<pve_api_types::ListFirewallRules>,
+    loader: Loader<Vec<pve_api_types::ListFirewallRules>>,
+    _listener: SharedStateObserver<LoaderState<Vec<pve_api_types::ListFirewallRules>>>,
+}
+
+impl ProxmoxFirewallRules {
+    fn update_data(&mut self) {
+        if let Some(Ok(data)) = &self.loader.read().data {
+            self.store.set_data((**data).clone());
+        }
+    }
+
+    fn columns() -> Rc<Vec<DataTableHeader<pve_api_types::ListFirewallRules>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Enabled"))
+                .width("80px")
+                .render(
+                    |rule: &pve_api_types::ListFirewallRules| match rule.enable {
+                        Some(1) => html! {<i class="fa fa-check"></i>},
+                        Some(0) | None => html! {<i class="fa fa fa-minus"></i>},
+                        _ => html! {"-"},
+                    },
+                )
+                .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!("Macro"))
+                .width("120px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.r#macro.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Interface"))
+                .width("100px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.iface.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Protocol"))
+                .width("100px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.proto.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Source"))
+                .flex(1)
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.source.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("S.Port"))
+                .width("80px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.sport.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Destination"))
+                .flex(1)
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.dest.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("D.Port"))
+                .width("80px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.dport.as_deref().unwrap_or("-").into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Log Level"))
+                .width("100px")
+                .render(|rule: &pve_api_types::ListFirewallRules| {
+                    rule.log
+                        .as_ref()
+                        .map(|l| format!("{:?}", l))
+                        .unwrap_or("-".to_string())
+                        .into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Comment"))
+                .flex(1)
+                .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>) -> 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,
+        };
+
+        me.update_data();
+        me
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            FirewallMsg::DataChange => {
+                self.update_data();
+                true
+            }
+        }
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> 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 {
+                let columns = Self::columns();
+                DataTable::new(columns, self.store.clone())
+                    .show_header(true)
+                    .striped(true)
+                    .into()
+            }
+        })
+    }
+}
+
+impl From<FirewallRules> for VNode {
+    fn from(val: FirewallRules) -> Self {
+        let comp = VComp::new::<ProxmoxFirewallRules>(Rc::new(val), None);
+        VNode::from(comp)
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 852d65d..d7d8c7e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -130,7 +130,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


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] pdm-api-types: add firewall status types
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (8 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 4/4] firewall: add rules table Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] api: firewall: add option, rules and status endpoints Hannes Laimer
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

These types are returned by all the `../firewall/status` endpoints. The
UI also uses them.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 lib/pdm-api-types/src/firewall.rs | 171 ++++++++++++++++++++++++++++++
 lib/pdm-api-types/src/lib.rs      |   2 +
 2 files changed, 173 insertions(+)
 create mode 100644 lib/pdm-api-types/src/firewall.rs

diff --git a/lib/pdm-api-types/src/firewall.rs b/lib/pdm-api-types/src/firewall.rs
new file mode 100644
index 0000000..10357ad
--- /dev/null
+++ b/lib/pdm-api-types/src/firewall.rs
@@ -0,0 +1,171 @@
+use proxmox_schema::{api, Schema};
+use serde::{Deserialize, Serialize};
+
+use crate::remotes::REMOTE_ID_SCHEMA;
+use crate::{NODE_SCHEMA, VMID_SCHEMA};
+
+const FIREWALL_RULES_COUNT: Schema =
+    proxmox_schema::IntegerSchema::new("The total amount of rules present")
+        .minimum(0)
+        .schema();
+
+const FIREWALL_ACTIVE_RULES_COUNT: Schema =
+    proxmox_schema::IntegerSchema::new("The amount of enabled rules")
+        .minimum(0)
+        .schema();
+
+#[api(
+    properties: {
+        all: {
+            schema: FIREWALL_RULES_COUNT,
+        },
+        active: {
+            schema: FIREWALL_ACTIVE_RULES_COUNT,
+        }
+    }
+)]
+/// Count of all rules present and count of all enabled firewall rules.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct RuleStat {
+    pub all: usize,
+    pub active: usize,
+}
+
+#[api(
+    properties: {
+        enabled: {
+            type: bool,
+            description: "True if the firewall is enabled",
+        },
+        rules: {
+            type: RuleStat,
+            flatten: true,
+        },
+    }
+)]
+/// Firewall status.
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+pub struct FirewallStatus {
+    pub enabled: bool,
+    #[serde(flatten)]
+    pub rules: RuleStat,
+}
+
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        status: {
+            type: FirewallStatus,
+            optional: true,
+        },
+        nodes: {
+            description: "Nodes in the cluster",
+            items: {
+                type: NodeFirewallStatus
+            },
+            type: Array,
+        },
+    }
+)]
+/// Firewall status of a PVE remote.
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+pub struct RemoteFirewallStatus {
+    pub remote: String,
+    pub status: Option<FirewallStatus>,
+    pub nodes: Vec<NodeFirewallStatus>,
+}
+
+#[api(
+    properties: {
+        node: {
+            schema: NODE_SCHEMA,
+        },
+        status: {
+            type: FirewallStatus,
+            optional: true,
+        },
+        guests: {
+            description: "Guests on a node",
+            items: {
+                type: GuestFirewallStatus
+            },
+            type: Array,
+        },
+    }
+)]
+/// Firewall status of a node
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct NodeFirewallStatus {
+    pub node: String,
+    pub status: Option<FirewallStatus>,
+    pub guests: Vec<GuestFirewallStatus>,
+}
+
+#[api]
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "lowercase")]
+/// The type of the guest
+pub enum GuestKind {
+    /// Guest is a LXC
+    Lxc,
+    /// Guets is a QEMU
+    Qemu,
+}
+
+impl GuestKind {
+    pub const fn as_str(&self) -> &'static str {
+        match self {
+            GuestKind::Lxc => "lxc",
+            GuestKind::Qemu => "qemu",
+        }
+    }
+}
+
+impl AsRef<str> for GuestKind {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl From<GuestKind> for &'static str {
+    fn from(kind: GuestKind) -> Self {
+        kind.as_str()
+    }
+}
+
+impl From<&GuestKind> for &'static str {
+    fn from(kind: &GuestKind) -> Self {
+        kind.as_str()
+    }
+}
+
+#[api(
+    properties: {
+        vmid: {
+            schema: VMID_SCHEMA,
+        },
+        name: {
+            type: String,
+            description: "Name of the guest.",
+        },
+        status: {
+            type: FirewallStatus,
+            optional: true,
+        },
+        kind: {
+            type: GuestKind,
+        }
+    }
+)]
+/// Firewall status of a guest
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct GuestFirewallStatus {
+    pub vmid: u32,
+    pub name: String,
+    pub status: Option<FirewallStatus>,
+    pub kind: GuestKind,
+}
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 2fb61ef..40f2381 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -94,6 +94,8 @@ pub use proxmox_schema::upid::*;
 mod openid;
 pub use openid::*;
 
+pub mod firewall;
+
 pub mod remotes;
 
 pub mod remote_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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] api: firewall: add option, rules and status endpoints
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (9 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] pdm-api-types: add firewall status types Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] pdm-client: add api methods for firewall options, " Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: add firewall status tree Hannes Laimer
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

This adds the following endpoints
* for all PVE remotes:
 - GET /pve/firewall/status

* for PVE remotes
 - GET pve/remotes/{remote}/firewall/options
 - PUT pve/remotes/{remote}/firewall/options
 - GET pve/remotes/{remote}/firewall/rules
 - GET pve/remotes/{remote}/firewall/status

* for PVE node
 - GET pve/remotes/{remote}/nodes/{node}/firewall/options
 - PUT pve/remotes/{remote}/nodes/{node}/firewall/options
 - GET pve/remotes/{remote}/nodes/{node}/firewall/rules
 - GET pve/remotes/{remote}/nodes/{node}/firewall/status

* for guests (both lxc and qemu)
 - GET pve/remotes/{remote}/[lxc|qemu]/{vmid}/firewall/options
 - PUT pve/remotes/{remote}/[lxc|qemu]/{vmid}/firewall/options
 - GET pve/remotes/{remote}/[lxc|qemu]/{vmid}/firewall/rules

`options` endpoints are for recieving and updating the configured
firewall options for remotes, nodes and guests. Both lxc and qemu guests
share the same type for getting and upating their options.

`rules` endpoints return the list of firewall rules that exist on the
entity. All remotes, nodes and guests return a list with items of the
same type.

`status` endpoints return the firewall status of the entity, this
includes:
 - name/id
 - optional status(enabled, count of enabled rules)
 - list of 'child-statuses', so:
    for pve status (all remotes) -> list of remote-statuses
    for remote status -> list of node-statuses
    for node status -> list of guest-statuses
    for guest status -> no list
 -(only guest) type of guest

Like this we have a way to limit the amount of requests the PDM has to
make in order to collect all the needed data. Given the rather large
amoutn of requests needed to assemble all the data this made more sense
than always loading everything and filtering on the client side.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 server/src/api/pve/firewall.rs | 756 +++++++++++++++++++++++++++++++++
 server/src/api/pve/lxc.rs      |   1 +
 server/src/api/pve/mod.rs      |   3 +
 server/src/api/pve/node.rs     |   1 +
 server/src/api/pve/qemu.rs     |   1 +
 5 files changed, 762 insertions(+)
 create mode 100644 server/src/api/pve/firewall.rs

diff --git a/server/src/api/pve/firewall.rs b/server/src/api/pve/firewall.rs
new file mode 100644
index 0000000..869eb48
--- /dev/null
+++ b/server/src/api/pve/firewall.rs
@@ -0,0 +1,756 @@
+use anyhow::Error;
+use pdm_api_types::{PRIV_RESOURCE_AUDIT, PRIV_RESOURCE_MODIFY, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
+use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
+use pve_api_types::{ClusterResource, ClusterResourceKind, ClusterResourceType};
+use std::sync::Arc;
+
+use pdm_api_types::firewall::{
+    FirewallStatus, GuestFirewallStatus, GuestKind, NodeFirewallStatus, RemoteFirewallStatus,
+    RuleStat,
+};
+use pdm_api_types::remotes::REMOTE_ID_SCHEMA;
+use pdm_api_types::{NODE_SCHEMA, VMID_SCHEMA};
+
+use super::{connect_to_remote, find_node_for_vm};
+use crate::connection::PveClient;
+
+// top-level firewall routers
+pub const PVE_FW_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(PVE_FW_SUBDIRS))
+    .subdirs(PVE_FW_SUBDIRS);
+
+pub const CLUSTER_FW_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(CLUSTER_FW_SUBDIRS))
+    .subdirs(CLUSTER_FW_SUBDIRS);
+
+pub const NODE_FW_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(NODE_FW_SUBDIRS))
+    .subdirs(NODE_FW_SUBDIRS);
+
+pub const LXC_FW_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(LXC_FW_SUBDIRS))
+    .subdirs(LXC_FW_SUBDIRS);
+pub const QEMU_FW_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(QEMU_FW_SUBDIRS))
+    .subdirs(QEMU_FW_SUBDIRS);
+
+// pve
+#[sortable]
+const PVE_FW_SUBDIRS: SubdirMap = &sorted!([("status", &PVE_STATUS_ROUTER),]);
+
+// cluster
+#[sortable]
+const CLUSTER_FW_SUBDIRS: SubdirMap = &sorted!([
+    ("options", &CLUSTER_OPTIONS_ROUTER),
+    ("rules", &CLUSTER_RULES_ROUTER),
+    ("status", &CLUSTER_STATUS_ROUTER),
+]);
+
+// node
+#[sortable]
+const NODE_FW_SUBDIRS: SubdirMap = &sorted!([
+    ("options", &NODE_OPTIONS_ROUTER),
+    ("rules", &NODE_RULES_ROUTER),
+    ("status", &NODE_STATUS_ROUTER),
+]);
+
+// guest
+#[sortable]
+const LXC_FW_SUBDIRS: SubdirMap = &sorted!([
+    ("options", &LXC_OPTIONS_ROUTER),
+    ("rules", &LXC_RULES_ROUTER),
+]);
+#[sortable]
+const QEMU_FW_SUBDIRS: SubdirMap = &sorted!([
+    ("options", &QEMU_OPTIONS_ROUTER),
+    ("rules", &QEMU_RULES_ROUTER),
+]);
+
+// /options
+const CLUSTER_OPTIONS_ROUTER: Router = Router::new()
+    .get(&API_METHOD_CLUSTER_FIREWALL_OPTIONS)
+    .put(&API_METHOD_UPDATE_CLUSTER_FIREWALL_OPTIONS);
+
+const NODE_OPTIONS_ROUTER: Router = Router::new()
+    .get(&API_METHOD_NODE_FIREWALL_OPTIONS)
+    .put(&API_METHOD_UPDATE_NODE_FIREWALL_OPTIONS);
+
+const LXC_OPTIONS_ROUTER: Router = Router::new()
+    .get(&API_METHOD_LXC_FIREWALL_OPTIONS)
+    .put(&API_METHOD_UPDATE_LXC_FIREWALL_OPTIONS);
+const QEMU_OPTIONS_ROUTER: Router = Router::new()
+    .get(&API_METHOD_QEMU_FIREWALL_OPTIONS)
+    .put(&API_METHOD_UPDATE_QEMU_FIREWALL_OPTIONS);
+
+// /rules
+const CLUSTER_RULES_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_FIREWALL_RULES);
+const NODE_RULES_ROUTER: Router = Router::new().get(&API_METHOD_NODE_FIREWALL_RULES);
+const LXC_RULES_ROUTER: Router = Router::new().get(&API_METHOD_LXC_FIREWALL_RULES);
+const QEMU_RULES_ROUTER: Router = Router::new().get(&API_METHOD_QEMU_FIREWALL_RULES);
+
+// /status
+const PVE_STATUS_ROUTER: Router = Router::new().get(&API_METHOD_PVE_FIREWALL_STATUS);
+const CLUSTER_STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_FIREWALL_STATUS);
+const NODE_STATUS_ROUTER: Router = Router::new().get(&API_METHOD_NODE_FIREWALL_STATUS);
+
+async fn load_guests_firewall_status(
+    pve: Arc<PveClient>,
+    node: String,
+    guests: &[ClusterResource],
+) -> Vec<GuestFirewallStatus> {
+    let mut result = vec![];
+
+    let guests: Vec<(u32, String, GuestKind)> = guests
+        .iter()
+        .filter(|g| g.node.as_ref() == Some(&node))
+        .filter_map(|g| {
+            let vmid = g.vmid?;
+            let name = g.name.clone().unwrap_or("".to_string());
+            match g.ty {
+                ClusterResourceType::Lxc => Some((vmid, name, GuestKind::Lxc)),
+                ClusterResourceType::Qemu => Some((vmid, name, GuestKind::Qemu)),
+                _ => None,
+            }
+        })
+        .collect();
+
+    for (vmid, name, kind) in guests {
+        let options_response = match kind {
+            GuestKind::Lxc => pve.lxc_firewall_options(&node, vmid),
+            GuestKind::Qemu => pve.qemu_firewall_options(&node, vmid),
+        };
+        let rules_response = match kind {
+            GuestKind::Lxc => pve.list_lxc_firewall_rules(&node, vmid),
+            GuestKind::Qemu => pve.list_qemu_firewall_rules(&node, vmid),
+        };
+
+        let enabled = options_response
+            .await
+            .map(|opts| opts.enable.unwrap_or_default());
+        let rules = rules_response.await.map(|rules| {
+            let all = rules.len();
+            let active = rules.iter().filter(|r| r.enable == Some(1)).count();
+            RuleStat { all, active }
+        });
+
+        let status = match (enabled, rules) {
+            (Ok(enabled), Ok(rules)) => Some(FirewallStatus { enabled, rules }),
+            _ => None,
+        };
+
+        result.push(GuestFirewallStatus {
+            vmid,
+            name,
+            status,
+            kind,
+        });
+    }
+    result
+}
+
+async fn load_nodes_firewall_status(
+    pve: Arc<PveClient>,
+    nodes: &[ClusterResource],
+    guests: &[ClusterResource],
+) -> Vec<NodeFirewallStatus> {
+    let mut result = vec![];
+    for node in nodes.iter().filter_map(|n| n.node.clone()) {
+        let options_response = pve.node_firewall_options(&node);
+        let rules_response = pve.list_node_firewall_rules(&node);
+
+        let enabled = options_response
+            .await
+            .map(|opts| opts.enable.unwrap_or_default());
+        let rules = rules_response.await.map(|rules| {
+            let all = rules.len();
+            let active = rules.iter().filter(|r| r.enable == Some(1)).count();
+            RuleStat { all, active }
+        });
+
+        let status = match (enabled, rules) {
+            (Ok(enabled), Ok(rules)) => Some(FirewallStatus { enabled, rules }),
+            _ => None,
+        };
+
+        let guests = load_guests_firewall_status(pve.clone(), node.clone(), guests).await;
+        result.push(NodeFirewallStatus {
+            node,
+            status,
+            guests,
+        });
+    }
+    result
+}
+
+#[api(
+    returns: {
+        type: Array,
+        description: "Get firewall status of remotes",
+        items: { type: RemoteFirewallStatus },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get firewall status of all PVE remotes.
+pub async fn pve_firewall_status(
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<RemoteFirewallStatus>, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let remote_ids: Vec<String> = remotes
+        .iter()
+        .filter_map(
+            |(remote, pdm_api_types::remotes::Remote { ty, .. })| match ty {
+                pdm_api_types::remotes::RemoteType::Pve => Some(remote.to_string()),
+                pdm_api_types::remotes::RemoteType::Pbs => None,
+            },
+        )
+        .collect();
+    let mut result = vec![];
+    for id in remote_ids {
+        let fetch = async {
+            let pve = connect_to_remote(&remotes, &id).ok()?;
+            let nodes = pve
+                .cluster_resources(Some(ClusterResourceKind::Node))
+                .await
+                .ok()?;
+            let guests = pve
+                .cluster_resources(Some(ClusterResourceKind::Vm))
+                .await
+                .ok()?;
+            Some((pve, nodes, guests))
+        }
+        .await;
+
+        let Some((pve, nodes, guests)) = fetch else {
+            result.push(RemoteFirewallStatus {
+                remote: id.to_string(),
+                status: None,
+                nodes: vec![],
+            });
+            continue;
+        };
+
+        let options_response = pve.cluster_firewall_options();
+        let rules_response = pve.list_cluster_firewall_rules();
+
+        let enabled = options_response.await.map(|opts| opts.enable != Some(0));
+        let rules = rules_response.await.map(|rules| {
+            let all = rules.len();
+            let active = rules.iter().filter(|r| r.enable == Some(1)).count();
+            RuleStat { all, active }
+        });
+
+        let status = match (enabled, rules) {
+            (Ok(enabled), Ok(rules)) => Some(FirewallStatus { enabled, rules }),
+            _ => None,
+        };
+
+        result.push(RemoteFirewallStatus {
+            remote: id.to_string(),
+            status,
+            nodes: load_nodes_firewall_status(pve, &nodes, &guests).await,
+        });
+    }
+    Ok(result)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "Get firewall options.",
+        items: { type: pve_api_types::ClusterFirewallOptions },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get cluster firewall options.
+pub async fn cluster_firewall_options(
+    remote: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<pve_api_types::ClusterFirewallOptions, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.cluster_firewall_options().await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+        },
+    },
+    returns: {
+        type: RemoteFirewallStatus,
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get firewall status of a specific remote.
+pub async fn cluster_firewall_status(
+    remote: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<RemoteFirewallStatus, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let fetch = async {
+        let pve = connect_to_remote(&remotes, &remote).ok()?;
+        let nodes = pve
+            .cluster_resources(Some(ClusterResourceKind::Node))
+            .await
+            .ok()?;
+        let guests = pve
+            .cluster_resources(Some(ClusterResourceKind::Vm))
+            .await
+            .ok()?;
+        Some((pve, nodes, guests))
+    }
+    .await;
+
+    let Some((pve, nodes, guests)) = fetch else {
+        return Ok(RemoteFirewallStatus {
+            remote,
+            status: None,
+            nodes: vec![],
+        });
+    };
+
+    let options_response = pve.cluster_firewall_options();
+    let rules_response = pve.list_cluster_firewall_rules();
+
+    let enabled = options_response.await.map(|opts| opts.enable != Some(0));
+    let rules = rules_response.await.map(|rules| {
+        let all = rules.len();
+        let active = rules.iter().filter(|r| r.enable == Some(1)).count();
+        RuleStat { all, active }
+    });
+
+    let status = match (enabled, rules) {
+        (Ok(enabled), Ok(rules)) => Some(FirewallStatus { enabled, rules }),
+        _ => None,
+    };
+
+    Ok(RemoteFirewallStatus {
+        remote,
+        status,
+        nodes: load_nodes_firewall_status(pve, &nodes, &guests).await,
+    })
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "Get firewall options.",
+        items: { type: pve_api_types::NodeFirewallOptions },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get nodes firewall options.
+pub async fn node_firewall_options(
+    remote: String,
+    node: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<pve_api_types::NodeFirewallOptions, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.node_firewall_options(&node).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: { schema: NODE_SCHEMA },
+        },
+    },
+    returns: {
+        type: NodeFirewallStatus,
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get firewall status of a specific node.
+pub async fn node_firewall_status(
+    remote: String,
+    node: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<NodeFirewallStatus, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let guests = pve.cluster_resources(Some(ClusterResourceKind::Vm)).await?;
+
+    let options_response = pve.node_firewall_options(&node);
+    let rules_response = pve.list_node_firewall_rules(&node);
+
+    let enabled = options_response
+        .await
+        .map(|opts| opts.enable.unwrap_or_default());
+    let rules = rules_response.await.map(|rules| {
+        let all = rules.len();
+        let active = rules.iter().filter(|r| r.enable == Some(1)).count();
+        RuleStat { all, active }
+    });
+
+    let status = match (enabled, rules) {
+        (Ok(enabled), Ok(rules)) => Some(FirewallStatus { enabled, rules }),
+        _ => None,
+    };
+
+    let guests_status = load_guests_firewall_status(pve, node.clone(), &guests).await;
+
+    Ok(NodeFirewallStatus {
+        node,
+        status,
+        guests: guests_status,
+    })
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "List cluster firewall rules.",
+        items: { type: pve_api_types::ListFirewallRules },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get cluster firewall rules.
+pub async fn cluster_firewall_rules(
+    remote: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.list_cluster_firewall_rules().await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "Get firewall options.",
+        items: { type: pve_api_types::GuestFirewallOptions },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_AUDIT, false),
+    },
+)]
+/// Get LXC firewall options.
+pub async fn lxc_firewall_options(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<pve_api_types::GuestFirewallOptions, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.lxc_firewall_options(&node, vmid).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            update: {
+                type: pve_api_types::UpdateClusterFirewallOptions,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_MODIFY, false),
+    },
+)]
+/// Update cluster firewall configuration
+pub async fn update_cluster_firewall_options(
+    remote: String,
+    update: pve_api_types::UpdateClusterFirewallOptions,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.set_cluster_firewall_options(update).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+            update: {
+                type: pve_api_types::UpdateNodeFirewallOptions,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_MODIFY, false),
+    },
+)]
+/// Update a nodes firewall configuration
+pub async fn update_node_firewall_options(
+    remote: String,
+    node: String,
+    update: pve_api_types::UpdateNodeFirewallOptions,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.set_node_firewall_options(&node, update).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "List node firewall rules.",
+        items: { type: pve_api_types::ListFirewallRules },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Get node firewall rules.
+pub async fn node_firewall_rules(
+    remote: String,
+    node: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    Ok(pve.list_node_firewall_rules(&node).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "Get firewall options.",
+        items: { type: pve_api_types::GuestFirewallOptions },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_AUDIT, false),
+    },
+)]
+/// Get QEMU firewall options.
+pub async fn qemu_firewall_options(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<pve_api_types::GuestFirewallOptions, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.qemu_firewall_options(&node, vmid).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA, },
+            update: {
+                type: pve_api_types::UpdateGuestFirewallOptions,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_MODIFY, false),
+    },
+)]
+/// Update LXC firewall options
+pub async fn update_lxc_firewall_options(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    update: pve_api_types::UpdateGuestFirewallOptions,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.set_lxc_firewall_options(&node, vmid, update).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA, },
+            update: {
+                type: pve_api_types::UpdateGuestFirewallOptions,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_MODIFY, false),
+    },
+)]
+/// Update QEMU firewall options
+pub async fn update_qemu_firewall_options(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    update: pve_api_types::UpdateGuestFirewallOptions,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.set_qemu_firewall_options(&node, vmid, update).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "List LXC firewall rules.",
+        items: { type: pve_api_types::ListFirewallRules },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_AUDIT, false),
+    },
+)]
+/// Get LXC firewall rules.
+pub async fn lxc_firewall_rules(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.list_lxc_firewall_rules(&node, vmid).await?)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            node: {
+                schema: NODE_SCHEMA,
+                optional: true,
+            },
+            vmid: { schema: VMID_SCHEMA },
+        },
+    },
+    returns: {
+        type: Array,
+        description: "List QEMU firewall rules.",
+        items: { type: pve_api_types::ListFirewallRules },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "guest", "{vmid}"], PRIV_RESOURCE_AUDIT, false),
+    },
+)]
+/// Get QEMU firewall rules.
+pub async fn qemu_firewall_rules(
+    remote: String,
+    node: Option<String>,
+    vmid: u32,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+
+    let pve = connect_to_remote(&remotes, &remote)?;
+
+    let node = find_node_for_vm(node, vmid, pve.as_ref()).await?;
+
+    Ok(pve.list_qemu_firewall_rules(&node, vmid).await?)
+}
diff --git a/server/src/api/pve/lxc.rs b/server/src/api/pve/lxc.rs
index 61db8ff..8cd3aa6 100644
--- a/server/src/api/pve/lxc.rs
+++ b/server/src/api/pve/lxc.rs
@@ -33,6 +33,7 @@ const LXC_VM_ROUTER: Router = Router::new()
 #[sortable]
 const LXC_VM_SUBDIRS: SubdirMap = &sorted!([
     ("config", &Router::new().get(&API_METHOD_LXC_GET_CONFIG)),
+    ("firewall", &super::firewall::LXC_FW_ROUTER),
     ("rrddata", &super::rrddata::LXC_RRD_ROUTER),
     ("start", &Router::new().post(&API_METHOD_LXC_START)),
     ("status", &Router::new().get(&API_METHOD_LXC_GET_STATUS)),
diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index fd4ea54..058fefe 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -33,6 +33,7 @@ use crate::connection::PveClient;
 use crate::connection::{self, probe_tls_connection};
 use crate::remote_tasks;
 
+mod firewall;
 mod lxc;
 mod node;
 mod qemu;
@@ -47,6 +48,7 @@ pub const ROUTER: Router = Router::new()
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
     ("remotes", &REMOTES_ROUTER),
+    ("firewall", &firewall::PVE_FW_ROUTER),
     ("probe-tls", &Router::new().post(&API_METHOD_PROBE_TLS)),
     ("scan", &Router::new().post(&API_METHOD_SCAN_REMOTE_PVE)),
     (
@@ -66,6 +68,7 @@ const MAIN_ROUTER: Router = Router::new()
 #[sortable]
 const REMOTE_SUBDIRS: SubdirMap = &sorted!([
     ("lxc", &lxc::ROUTER),
+    ("firewall", &firewall::CLUSTER_FW_ROUTER),
     ("nodes", &NODES_ROUTER),
     ("qemu", &qemu::ROUTER),
     ("resources", &RESOURCES_ROUTER),
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index 301c0b1..3c4fba8 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -16,6 +16,7 @@ pub const ROUTER: Router = Router::new()
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
     ("apt", &crate::api::remote_updates::APT_ROUTER),
+    ("firewall", &super::firewall::NODE_FW_ROUTER),
     ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
     ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
     ("storage", &STORAGE_ROUTER),
diff --git a/server/src/api/pve/qemu.rs b/server/src/api/pve/qemu.rs
index 6158bef..d521467 100644
--- a/server/src/api/pve/qemu.rs
+++ b/server/src/api/pve/qemu.rs
@@ -33,6 +33,7 @@ const QEMU_VM_ROUTER: Router = Router::new()
 #[sortable]
 const QEMU_VM_SUBDIRS: SubdirMap = &sorted!([
     ("config", &Router::new().get(&API_METHOD_QEMU_GET_CONFIG)),
+    ("firewall", &super::firewall::QEMU_FW_ROUTER),
     ("rrddata", &super::rrddata::QEMU_RRD_ROUTER),
     ("start", &Router::new().post(&API_METHOD_QEMU_START)),
     ("status", &Router::new().get(&API_METHOD_QEMU_GET_STATUS)),
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] pdm-client: add api methods for firewall options, rules and status endpoints
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (10 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] api: firewall: add option, rules and status endpoints Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: add firewall status tree Hannes Laimer
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 lib/pdm-client/src/lib.rs | 133 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 133 insertions(+)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 0cab769..e0bdecb 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -391,6 +391,139 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
+    pub async fn pve_get_firewall_status(
+        &self,
+    ) -> Result<Vec<pdm_api_types::firewall::RemoteFirewallStatus>, Error> {
+        let path = "/api2/extjs/pve/firewall/status";
+        Ok(self.0.get(path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_cluster_firewall_status(
+        &self,
+        remote: &str,
+    ) -> Result<pdm_api_types::firewall::RemoteFirewallStatus, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/firewall/status");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_node_firewall_status(
+        &self,
+        remote: &str,
+        node: &str,
+    ) -> Result<pdm_api_types::firewall::NodeFirewallStatus, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/firewall/status");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_cluster_firewall_options(
+        &self,
+        remote: &str,
+    ) -> Result<pve_api_types::NodeFirewallOptions, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/firewall/options");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_node_firewall_options(
+        &self,
+        remote: &str,
+        node: &str,
+    ) -> Result<pve_api_types::NodeFirewallOptions, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/firewall/options");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_lxc_firewall_options(
+        &self,
+        remote: &str,
+        node: Option<&str>,
+        vmid: u32,
+    ) -> Result<pve_api_types::GuestFirewallOptions, Error> {
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/firewall/options"
+        ))
+        .maybe_arg("node", &node)
+        .build();
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_qemu_firewall_options(
+        &self,
+        remote: &str,
+        node: Option<&str>,
+        vmid: u32,
+    ) -> Result<pve_api_types::GuestFirewallOptions, Error> {
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/firewall/options"
+        ))
+        .maybe_arg("node", &node)
+        .build();
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_cluster_firewall_rules(
+        &self,
+        remote: &str,
+    ) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/firewall/rules");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_node_firewall_rules(
+        &self,
+        remote: &str,
+        node: &str,
+    ) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/firewall/rules");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_lxc_firewall_rules(
+        &self,
+        remote: &str,
+        node: Option<&str>,
+        vmid: u32,
+    ) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/lxc/{vmid}/firewall/rules"
+        ))
+        .maybe_arg("node", &node)
+        .build();
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_get_qemu_firewall_rules(
+        &self,
+        remote: &str,
+        node: Option<&str>,
+        vmid: u32,
+    ) -> Result<Vec<pve_api_types::ListFirewallRules>, Error> {
+        let path = ApiPathBuilder::new(format!(
+            "/api2/extjs/pve/remotes/{remote}/qemu/{vmid}/firewall/rules"
+        ))
+        .maybe_arg("node", &node)
+        .build();
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
+    pub async fn pve_set_cluster_firewall_options(
+        &self,
+        remote: &str,
+        update: pve_api_types::UpdateClusterFirewallOptions,
+    ) -> Result<(), Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/firewall/options");
+        self.0.put(&path, &update).await?.nodata()
+    }
+
+    pub async fn pve_set_node_firewall_options(
+        &self,
+        remote: &str,
+        node: &str,
+        update: pve_api_types::UpdateNodeFirewallOptions,
+    ) -> Result<(), Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/firewall/options");
+        self.0.put(&path, &update).await?.nodata()
+    }
+
     pub async fn pve_node_rrddata(
         &self,
         remote: &str,
-- 
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] 14+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: add firewall status tree
  2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
                   ` (11 preceding siblings ...)
  2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] pdm-client: add api methods for firewall options, " Hannes Laimer
@ 2025-10-30 14:34 ` Hannes Laimer
  12 siblings, 0 replies; 14+ messages in thread
From: Hannes Laimer @ 2025-10-30 14:34 UTC (permalink / raw)
  To: pdm-devel

Adds tree displaying the firewall status of remotes, nodes and guests.
Upon selecting an entity in the tree the right panle shows its
configured firewall rules, this is read-only, so rules can't be added,
deleted or modified currently. The tree contains a button that allows to
edit the firewall options of remotes, nodes or guests.

Given the rather large amount of requests it takes PDM to accumulate all
the data this contains comboboxes to select specific remotes or nodes
on a specific remote. This doesn't just filter the results but changes
how data is requested. So is is possible to limit the amount of work the
PDM has to do for each request and improve responsiveness. The text
filter is only for local, already loaded, data, so it won't trigger a new
load.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 ui/src/remotes/firewall/columns.rs    | 150 ++++++
 ui/src/remotes/firewall/mod.rs        |  30 ++
 ui/src/remotes/firewall/tree.rs       | 634 ++++++++++++++++++++++++++
 ui/src/remotes/firewall/types.rs      | 284 ++++++++++++
 ui/src/remotes/firewall/ui_helpers.rs | 156 +++++++
 ui/src/remotes/mod.rs                 |  10 +
 6 files changed, 1264 insertions(+)
 create mode 100644 ui/src/remotes/firewall/columns.rs
 create mode 100644 ui/src/remotes/firewall/mod.rs
 create mode 100644 ui/src/remotes/firewall/tree.rs
 create mode 100644 ui/src/remotes/firewall/types.rs
 create mode 100644 ui/src/remotes/firewall/ui_helpers.rs

diff --git a/ui/src/remotes/firewall/columns.rs b/ui/src/remotes/firewall/columns.rs
new file mode 100644
index 0000000..d05095b
--- /dev/null
+++ b/ui/src/remotes/firewall/columns.rs
@@ -0,0 +1,150 @@
+use proxmox_yew_comp::LoadableComponentContext;
+use pwt::css::AlignItems;
+use pwt::prelude::*;
+use pwt::state::TreeStore;
+use pwt::tr;
+use pwt::widget::data_table::{DataTableColumn, DataTableHeader};
+use pwt::widget::{Container, Fa, Row};
+use std::rc::Rc;
+use yew::Html;
+
+use super::types::{Scope, TreeEntry, ViewState};
+use super::ui_helpers::{
+    render_firewall_status, render_load_error_message, render_rule_stats, render_warning_icon,
+};
+
+use crate::remotes::firewall::tree::FirewallTreeComponent;
+
+pub fn create_columns(
+    ctx: &LoadableComponentContext<FirewallTreeComponent>,
+    store: TreeStore<TreeEntry>,
+    loading: bool,
+    scope: &Scope,
+) -> Rc<Vec<DataTableHeader<TreeEntry>>> {
+    let scope = Rc::new(scope.clone());
+
+    Rc::new(vec![
+        create_name_column(store, loading, scope.clone()),
+        create_enabled_column(scope.clone()),
+        create_rules_column(scope),
+        create_actions_column(ctx),
+    ])
+}
+
+fn create_name_column(
+    store: TreeStore<TreeEntry>,
+    loading: bool,
+    scope: Rc<Scope>,
+) -> DataTableHeader<TreeEntry> {
+    DataTableColumn::new(tr!("Name"))
+        .tree_column(store)
+        .render(move |entry: &TreeEntry| {
+            let (icon, text) = match entry {
+                TreeEntry::Root if loading => {
+                    let loading_text = tr!("Loading {}...", scope.loading_description());
+                    (
+                        Container::from_tag("i").class("pwt-loading-icon"),
+                        loading_text,
+                    )
+                }
+                _ => {
+                    let icon = entry.icon_name();
+                    let text = entry.name();
+                    (
+                        if let Some(icon) = icon {
+                            Container::new().with_child(Fa::new(icon))
+                        } else {
+                            Container::new()
+                        },
+                        text,
+                    )
+                }
+            };
+            Row::new()
+                .class(AlignItems::Baseline)
+                .gap(2)
+                .with_child(icon)
+                .with_child(text)
+                .into()
+        })
+        .into()
+}
+
+fn create_enabled_column(scope: Rc<Scope>) -> DataTableHeader<TreeEntry> {
+    DataTableColumn::new(tr!("Enabled"))
+        .width("40px")
+        .render(move |entry: &TreeEntry| match entry {
+            TreeEntry::Root => Html::default(),
+            TreeEntry::Remote(_) => {
+                if let Some((status, masked)) = entry.firewall_status() {
+                    render_firewall_status(status, masked)
+                } else if matches!(scope.as_ref(), Scope::Node { .. }) {
+                    Html::default()
+                } else {
+                    render_warning_icon()
+                }
+            }
+            _ => {
+                if let Some((status, masked)) = entry.firewall_status() {
+                    render_firewall_status(status, masked)
+                } else {
+                    render_warning_icon()
+                }
+            }
+        })
+        .into()
+}
+
+fn create_rules_column(scope: Rc<Scope>) -> DataTableHeader<TreeEntry> {
+    DataTableColumn::new(tr!("Rules"))
+        .render(move |entry: &TreeEntry| match entry {
+            TreeEntry::Root => Html::default(),
+            TreeEntry::Remote(_) => {
+                if let Some(rules) = entry.rule_stats() {
+                    render_rule_stats(rules)
+                } else if matches!(scope.as_ref(), Scope::Node { .. }) {
+                    Html::default()
+                } else {
+                    render_load_error_message()
+                }
+            }
+            _ => {
+                if let Some(rules) = entry.rule_stats() {
+                    render_rule_stats(rules)
+                } else {
+                    render_load_error_message()
+                }
+            }
+        })
+        .into()
+}
+
+fn create_actions_column(
+    ctx: &LoadableComponentContext<FirewallTreeComponent>,
+) -> DataTableHeader<TreeEntry> {
+    let link = ctx.link().clone();
+
+    DataTableColumn::new(tr!("Actions"))
+        .width("50px")
+        .justify("right")
+        .render(move |entry: &TreeEntry| {
+            if !entry.is_editable() {
+                return Html::default();
+            }
+
+            let view_state = match ViewState::from_entry(entry) {
+                Some(state) => state,
+                None => return Html::default(),
+            };
+
+            let link_clone = link.clone();
+            pwt::widget::Tooltip::new(pwt::widget::ActionIcon::new("fa fa-fw fa-cog").on_activate(
+                move |_| {
+                    link_clone.change_view(Some(view_state.clone()));
+                },
+            ))
+            .tip(tr!("Edit Options"))
+            .into()
+        })
+        .into()
+}
diff --git a/ui/src/remotes/firewall/mod.rs b/ui/src/remotes/firewall/mod.rs
new file mode 100644
index 0000000..c500e1e
--- /dev/null
+++ b/ui/src/remotes/firewall/mod.rs
@@ -0,0 +1,30 @@
+mod columns;
+mod tree;
+mod types;
+mod ui_helpers;
+
+// Re-export public types
+pub use tree::FirewallTreeComponent;
+
+use std::rc::Rc;
+use yew::virtual_dom::{VComp, VNode};
+use yew::Properties;
+
+use proxmox_yew_comp::LoadableComponentMaster;
+
+#[derive(PartialEq, Properties)]
+pub struct FirewallTree {}
+
+impl FirewallTree {
+    pub fn new() -> Self {
+        yew::props!(Self {})
+    }
+}
+
+impl From<FirewallTree> for VNode {
+    fn from(value: FirewallTree) -> Self {
+        let comp =
+            VComp::new::<LoadableComponentMaster<FirewallTreeComponent>>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
diff --git a/ui/src/remotes/firewall/tree.rs b/ui/src/remotes/firewall/tree.rs
new file mode 100644
index 0000000..1dbddee
--- /dev/null
+++ b/ui/src/remotes/firewall/tree.rs
@@ -0,0 +1,634 @@
+use futures::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+use yew::{ContextHandle, Html};
+
+use proxmox_yew_comp::{EditFirewallOptions, LoadableComponent, LoadableComponentContext};
+use pwt::css;
+use pwt::prelude::*;
+use pwt::props::{FieldBuilder, WidgetBuilder};
+use pwt::state::{Selection, TreeStore};
+use pwt::tr;
+use pwt::widget::data_table::DataTable;
+use pwt::widget::form::{Combobox, Field};
+use pwt::widget::{Button, Container, Panel, Toolbar, Trigger};
+
+use crate::RemoteList;
+
+use super::columns::create_columns;
+use super::types::{
+    FirewallError, GuestEntry, LoadState, NodeEntry, RemoteEntry, Scope, TreeEntry, ViewState,
+};
+use super::ui_helpers::{create_panel_title, PanelConfig};
+
+use pdm_api_types::firewall::{GuestKind, RemoteFirewallStatus};
+use std::cmp::Ordering;
+
+fn create_loading_tree() -> pwt::state::SlabTree<TreeEntry> {
+    let mut tree = pwt::state::SlabTree::new();
+    tree.set_root(TreeEntry::Root);
+    tree
+}
+
+fn build_tree_from_remotes(
+    remote_statuses: Vec<RemoteFirewallStatus>,
+) -> pwt::state::SlabTree<TreeEntry> {
+    let mut tree = pwt::state::SlabTree::new();
+    tree.set_root(TreeEntry::Root);
+
+    if let Some(mut root) = tree.root_mut() {
+        root.set_expanded(true);
+
+        for remote_status in remote_statuses {
+            add_remote_to_tree(&mut root, remote_status);
+        }
+    }
+
+    tree
+}
+
+fn add_remote_to_tree(
+    root: &mut pwt::state::SlabTreeNodeMut<TreeEntry>,
+    remote_status: RemoteFirewallStatus,
+) {
+    let remote_name = remote_status.remote.clone();
+    let cluster_fw_status = remote_status.status;
+
+    let cluster_is_enabled = cluster_fw_status
+        .as_ref()
+        .map(|s| s.enabled)
+        .unwrap_or(true);
+
+    let remote_entry = TreeEntry::Remote(RemoteEntry {
+        name: remote_name.clone(),
+        status: cluster_fw_status,
+    });
+
+    let mut remote_handle = root.append(remote_entry);
+    remote_handle.set_expanded(cluster_is_enabled);
+
+    for node_status in remote_status.nodes {
+        let node_name = node_status.node.clone();
+        let node_firewall_status = node_status.status;
+
+        let node_entry = TreeEntry::Node(NodeEntry {
+            remote: remote_name.clone(),
+            name: node_name.clone(),
+            status: node_firewall_status,
+            masked: !cluster_is_enabled,
+        });
+
+        let mut node_handle = remote_handle.append(node_entry);
+        node_handle.set_expanded(!node_status.guests.is_empty());
+
+        for guest in node_status.guests {
+            let guest_entry = GuestEntry::new(
+                guest.clone(),
+                node_name.clone(),
+                remote_name.clone(),
+                !cluster_is_enabled,
+            );
+
+            let tree_entry = match guest.kind {
+                GuestKind::Lxc => TreeEntry::Guest(guest_entry, GuestKind::Lxc),
+                GuestKind::Qemu => TreeEntry::Guest(guest_entry, GuestKind::Qemu),
+            };
+
+            node_handle.append(tree_entry);
+        }
+    }
+}
+
+fn sort_entries(a: &TreeEntry, b: &TreeEntry) -> Ordering {
+    let rank_a = a.sort_rank();
+    let rank_b = b.sort_rank();
+    match rank_a.cmp(&rank_b) {
+        Ordering::Equal => a.name().cmp(&b.name()),
+        other => other,
+    }
+}
+
+pub enum Msg {
+    DataLoaded {
+        generation: usize,
+        data: Vec<pdm_api_types::firewall::RemoteFirewallStatus>,
+    },
+    RemoteListChanged,
+    Reload,
+    FilterChanged(String),
+    ScopeChanged(Scope),
+    RemotesLoaded(Vec<String>),
+    NodesLoaded {
+        generation: usize,
+        nodes: Vec<String>,
+    },
+    SelectionChanged(Option<yew::virtual_dom::Key>),
+    Error(FirewallError),
+    NoOp,
+}
+
+pub struct FirewallTreeComponent {
+    store: TreeStore<TreeEntry>,
+    selection: Selection,
+    _context_listener: ContextHandle<RemoteList>,
+    filter_text: String,
+    scope: Scope,
+    available_remotes: Vec<String>,
+    available_nodes: Vec<String>,
+    options_loading: bool,
+    load_state: LoadState,
+    selected_entry: Option<TreeEntry>,
+}
+
+impl FirewallTreeComponent {
+    fn reset_tree_for_loading(&mut self) {
+        let tree = create_loading_tree();
+        self.store.write().update_root_tree(tree);
+        self.store.write().set_view_root(true);
+        self.clear_selection();
+    }
+
+    fn clear_selection(&mut self) {
+        self.selected_entry = None;
+    }
+
+    fn handle_scope_change(&mut self, ctx: &LoadableComponentContext<Self>, new_scope: Scope) {
+        let remote_changed = self.scope.remote_name() != new_scope.remote_name();
+
+        if remote_changed && new_scope.remote_name().is_some() {
+            self.scope = match &new_scope {
+                Scope::Node { remote, .. } | Scope::Remote { name: remote } => Scope::Remote {
+                    name: remote.clone(),
+                },
+                Scope::All => Scope::All,
+            };
+            self.available_nodes.clear();
+            self.start_node_load(ctx);
+        } else {
+            self.scope = new_scope;
+        }
+
+        self.reset_tree_for_loading();
+        let _generation = self.load_state.start_data_load();
+        ctx.link().send_reload();
+    }
+
+    fn start_node_load(&mut self, ctx: &LoadableComponentContext<Self>) {
+        if let Some(remote) = self.scope.remote_name() {
+            let generation = self.load_state.start_nodes_load();
+            let link = ctx.link().clone();
+            let remote = remote.to_string();
+
+            ctx.link().spawn(async move {
+                match load_nodes_for_remote(remote).await {
+                    Ok((_remote, nodes)) => {
+                        link.send_message(Msg::NodesLoaded { generation, nodes });
+                    }
+                    Err(err) => {
+                        link.send_message(Msg::Error(err));
+                    }
+                }
+            });
+        }
+    }
+
+    fn render_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
+        let columns = create_columns(ctx, self.store.clone(), ctx.loading(), &self.scope);
+
+        let table = DataTable::new(columns, self.store.clone())
+            .selection(self.selection.clone())
+            .striped(false)
+            .borderless(true)
+            .show_header(false)
+            .class(css::FlexFit);
+
+        let title = create_panel_title("shield", tr!("Firewall Status"));
+
+        Panel::new()
+            .class(css::FlexFit)
+            .title(title)
+            .border(true)
+            .min_width(500)
+            .with_child(table)
+            .style("flex", "1 1 0")
+    }
+
+    fn render_rules_panel(&self, _ctx: &LoadableComponentContext<Self>) -> Panel {
+        let config = match &self.selected_entry {
+            Some(entry) => PanelConfig::from_entry(entry),
+            None => PanelConfig::for_no_selection(),
+        };
+
+        config.build()
+    }
+}
+
+impl LoadableComponent for FirewallTreeComponent {
+    type Properties = super::FirewallTree;
+    type Message = Msg;
+    type ViewState = ViewState;
+
+    fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+        let tree = create_loading_tree();
+        let store = TreeStore::new();
+        store.write().update_root_tree(tree);
+        store.write().set_view_root(true);
+
+        let link = ctx.link();
+        let selection = Selection::new().on_select(
+            link.callback(|selection: Selection| Msg::SelectionChanged(selection.selected_key())),
+        );
+
+        let (_, context_listener) = ctx
+            .link()
+            .yew_link()
+            .context(ctx.link().callback(|_: RemoteList| Msg::RemoteListChanged))
+            .expect("No Remote list context provided");
+
+        Self {
+            store,
+            selection,
+            _context_listener: context_listener,
+            filter_text: String::new(),
+            scope: Scope::default(),
+            available_remotes: Vec::new(),
+            available_nodes: Vec::new(),
+            options_loading: true,
+            load_state: LoadState::default(),
+            selected_entry: None,
+        }
+    }
+
+    fn load(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+        let link = ctx.link().clone();
+        let scope = self.scope.clone();
+        let need_remotes = self.available_remotes.is_empty();
+        let generation = self.load_state.data_generation;
+
+        Box::pin(async move {
+            // Load remotes list if needed
+            if need_remotes {
+                match load_remotes().await {
+                    Ok(remotes) => {
+                        link.send_message(Msg::RemotesLoaded(remotes));
+                    }
+                    Err(err) => {
+                        link.send_message(Msg::Error(err));
+                    }
+                }
+            }
+
+            // Load firewall status
+            match load_firewall_status(&scope).await {
+                Ok(data) => {
+                    link.send_message(Msg::DataLoaded { generation, data });
+                }
+                Err(err) => {
+                    link.send_message(Msg::Error(err));
+                }
+            }
+
+            Ok(())
+        })
+    }
+
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::DataLoaded { generation, data } => {
+                if !self.load_state.is_data_current(generation) {
+                    log::debug!(
+                        "Ignoring stale data (generation {} vs current {})",
+                        generation,
+                        self.load_state.data_generation
+                    );
+                    return false;
+                }
+
+                let tree = build_tree_from_remotes(data);
+                self.store.write().set_view_root(false);
+                self.store.write().update_root_tree(tree);
+                self.store.set_sorter(sort_entries);
+                self.load_state.finish_load();
+                self.clear_selection();
+                true
+            }
+            Msg::RemoteListChanged => true,
+            Msg::Reload => {
+                let _generation = self.load_state.start_data_load();
+                ctx.link().send_reload();
+                false
+            }
+            Msg::FilterChanged(filter) => {
+                self.filter_text = filter;
+                if self.filter_text.is_empty() {
+                    self.store.set_filter(None);
+                } else {
+                    let filter_text = Rc::new(self.filter_text.clone());
+                    self.store
+                        .set_filter(move |entry: &TreeEntry| entry.matches_filter(&filter_text));
+                }
+                self.clear_selection();
+                true
+            }
+            Msg::ScopeChanged(new_scope) => {
+                if self.scope != new_scope {
+                    self.handle_scope_change(ctx, new_scope);
+                    true
+                } else {
+                    false
+                }
+            }
+            Msg::RemotesLoaded(remotes) => {
+                self.available_remotes = remotes;
+                self.options_loading = false;
+                true
+            }
+            Msg::NodesLoaded { generation, nodes } => {
+                if !self.load_state.is_nodes_current(generation) {
+                    log::debug!(
+                        "Ignoring stale nodes (generation {} vs current {})",
+                        generation,
+                        self.load_state.nodes_generation
+                    );
+                    return false;
+                }
+                self.available_nodes = nodes;
+                true
+            }
+            Msg::SelectionChanged(key) => {
+                if let Some(key) = key {
+                    let read_guard = self.store.read();
+                    if let Some(node_ref) = read_guard.lookup_node(&key) {
+                        self.selected_entry = Some(node_ref.record().clone());
+                    } else {
+                        self.selected_entry = None;
+                    }
+                } else {
+                    self.selected_entry = None;
+                }
+                true
+            }
+            Msg::Error(err) => {
+                log::error!("{}", err);
+                self.load_state.finish_load();
+                true
+            }
+            Msg::NoOp => false,
+        }
+    }
+
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        Container::new()
+            .class("pwt-content-spacer")
+            .class(css::FlexFit)
+            .class("pwt-flex-direction-row")
+            .with_child(self.render_tree_panel(ctx))
+            .with_child(self.render_rules_panel(ctx))
+            .into()
+    }
+
+    fn dialog_view(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+        view_state: &Self::ViewState,
+    ) -> Option<Html> {
+        let dialog = match view_state {
+            ViewState::EditRemote { remote } => EditFirewallOptions::cluster(remote.to_string())
+                .on_close(ctx.link().change_view_callback(|_| None))
+                .into(),
+            ViewState::EditNode { remote, node } => {
+                EditFirewallOptions::node(remote.to_string(), node.to_string())
+                    .on_close(ctx.link().change_view_callback(|_| None))
+                    .into()
+            }
+            ViewState::EditGuest {
+                remote,
+                node,
+                vmid,
+                ty,
+            } => {
+                let vmtype: &str = ty.into();
+                EditFirewallOptions::guest(
+                    remote.to_string(),
+                    node.to_string(),
+                    *vmid as u64,
+                    vmtype,
+                )
+                .on_close(ctx.link().change_view_callback(|_| None))
+                .into()
+            }
+        };
+
+        Some(dialog)
+    }
+
+    fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+        let link = ctx.link();
+
+        Some(
+            Toolbar::new()
+                .border_bottom(true)
+                .with_child(
+                    Field::new()
+                        .value(self.filter_text.clone())
+                        .with_trigger(
+                            Trigger::new(if !self.filter_text.is_empty() {
+                                "fa fa-times"
+                            } else {
+                                ""
+                            })
+                            .on_activate(link.callback(|_| Msg::FilterChanged(String::new()))),
+                            true,
+                        )
+                        .placeholder(tr!("Filter"))
+                        .on_input(link.callback(Msg::FilterChanged)),
+                )
+                .with_child(create_remote_combobox(
+                    ctx,
+                    &self.available_remotes,
+                    self.options_loading,
+                    &self.scope,
+                ))
+                .with_child(create_node_combobox(
+                    ctx,
+                    &self.available_nodes,
+                    self.options_loading,
+                    &self.scope,
+                ))
+                .with_flex_spacer()
+                .with_child(Button::refresh(ctx.loading()).onclick(link.callback(|_| Msg::Reload)))
+                .into(),
+        )
+    }
+}
+
+fn create_remote_combobox(
+    ctx: &LoadableComponentContext<FirewallTreeComponent>,
+    available_remotes: &[String],
+    options_loading: bool,
+    current_scope: &Scope,
+) -> Html {
+    if options_loading {
+        return Combobox::new()
+            .items(Rc::new(vec![]))
+            .placeholder(tr!("Loading..."))
+            .disabled(true)
+            .key("remote-combobox-loading")
+            .on_change(ctx.link().callback(|_: String| Msg::NoOp))
+            .into();
+    }
+
+    let items: Vec<yew::AttrValue> = available_remotes
+        .iter()
+        .map(|remote| yew::AttrValue::from(remote.clone()))
+        .collect();
+
+    let current_value = current_scope
+        .remote_name()
+        .map(|s| yew::AttrValue::from(s.to_string()));
+
+    Combobox::new()
+        .items(Rc::new(items))
+        .default(current_value)
+        .placeholder(tr!("All remotes"))
+        .disabled(false)
+        .key("remote-combobox")
+        .on_change(ctx.link().callback(move |value: String| {
+            if value.is_empty() {
+                Msg::ScopeChanged(Scope::All)
+            } else {
+                Msg::ScopeChanged(Scope::Remote { name: value })
+            }
+        }))
+        .into()
+}
+
+fn create_node_combobox(
+    ctx: &LoadableComponentContext<FirewallTreeComponent>,
+    available_nodes: &[String],
+    options_loading: bool,
+    current_scope: &Scope,
+) -> Html {
+    let selected_remote = current_scope.remote_name();
+
+    let items: Vec<yew::AttrValue> = if selected_remote.is_some() {
+        available_nodes
+            .iter()
+            .map(|node| yew::AttrValue::from(node.clone()))
+            .collect()
+    } else {
+        Vec::new()
+    };
+
+    let current_value = current_scope
+        .node_name()
+        .map(|s| yew::AttrValue::from(s.to_string()));
+
+    let has_nodes = !available_nodes.is_empty();
+    let is_enabled = selected_remote.is_some() && !options_loading && has_nodes;
+    let key = format!("node-combobox-{:?}", selected_remote);
+
+    let selected_remote_owned = selected_remote.map(String::from);
+
+    Combobox::new()
+        .items(Rc::new(items))
+        .default(current_value)
+        .placeholder(tr!("All nodes"))
+        .disabled(!is_enabled)
+        .key(key)
+        .on_change(ctx.link().callback(move |value: String| {
+            if value.is_empty() {
+                if let Some(ref remote) = selected_remote_owned {
+                    Msg::ScopeChanged(Scope::Remote {
+                        name: remote.clone(),
+                    })
+                } else {
+                    Msg::ScopeChanged(Scope::All)
+                }
+            } else if let Some(ref remote) = selected_remote_owned {
+                Msg::ScopeChanged(Scope::Node {
+                    remote: remote.clone(),
+                    name: value,
+                })
+            } else {
+                Msg::ScopeChanged(Scope::All)
+            }
+        }))
+        .into()
+}
+
+async fn load_firewall_status(
+    scope: &Scope,
+) -> Result<Vec<pdm_api_types::firewall::RemoteFirewallStatus>, FirewallError> {
+    match scope {
+        Scope::All => crate::pdm_client()
+            .pve_get_firewall_status()
+            .await
+            .map_err(|e| FirewallError::StatusLoadFailed {
+                scope: scope.clone(),
+                message: e.to_string(),
+            }),
+        Scope::Remote { name } => {
+            let remote_status = crate::pdm_client()
+                .pve_cluster_firewall_status(name)
+                .await
+                .map_err(|e| FirewallError::StatusLoadFailed {
+                    scope: scope.clone(),
+                    message: e.to_string(),
+                })?;
+            Ok(vec![remote_status])
+        }
+        Scope::Node { remote, name } => {
+            let node_status = crate::pdm_client()
+                .pve_node_firewall_status(remote, name)
+                .await
+                .map_err(|e| FirewallError::StatusLoadFailed {
+                    scope: scope.clone(),
+                    message: e.to_string(),
+                })?;
+
+            // Wrap node status in a remote status structure
+            let remote_status = pdm_api_types::firewall::RemoteFirewallStatus {
+                remote: remote.clone(),
+                status: None,
+                nodes: vec![node_status],
+            };
+            Ok(vec![remote_status])
+        }
+    }
+}
+
+async fn load_remotes() -> Result<Vec<String>, FirewallError> {
+    let remotes = crate::pdm_client()
+        .list_remotes()
+        .await
+        .map_err(|e| FirewallError::RemoteListLoadFailed(e.to_string()))?;
+
+    Ok(remotes.into_iter().map(|r| r.id).collect())
+}
+
+async fn load_nodes_for_remote(remote: String) -> Result<(String, Vec<String>), FirewallError> {
+    let resources = crate::pdm_client()
+        .pve_cluster_resources(&remote, Some(pdm_client::types::ClusterResourceKind::Node))
+        .await
+        .map_err(|e| FirewallError::NodesLoadFailed {
+            remote: remote.clone(),
+            message: e.to_string(),
+        })?;
+
+    let nodes = resources
+        .into_iter()
+        .filter_map(|resource| {
+            if let pdm_api_types::resource::PveResource::Node(node) = resource {
+                Some(node.node)
+            } else {
+                None
+            }
+        })
+        .collect();
+
+    Ok((remote, nodes))
+}
diff --git a/ui/src/remotes/firewall/types.rs b/ui/src/remotes/firewall/types.rs
new file mode 100644
index 0000000..84aa657
--- /dev/null
+++ b/ui/src/remotes/firewall/types.rs
@@ -0,0 +1,284 @@
+use pdm_api_types::firewall::{FirewallStatus, GuestFirewallStatus, GuestKind, RuleStat};
+use pwt::props::ExtractPrimaryKey;
+use std::fmt;
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum Scope {
+    /// Show all remotes, nodes, and guests
+    All,
+    /// Show specific remote with all its nodes and guests
+    Remote { name: String },
+    /// Show specific node with all its guests
+    Node { remote: String, name: String },
+}
+
+impl Default for Scope {
+    fn default() -> Self {
+        Self::All
+    }
+}
+
+impl Scope {
+    pub fn remote_name(&self) -> Option<&str> {
+        match self {
+            Self::All => None,
+            Self::Remote { name } | Self::Node { remote: name, .. } => Some(name),
+        }
+    }
+
+    pub fn node_name(&self) -> Option<&str> {
+        match self {
+            Self::Node { name, .. } => Some(name),
+            _ => None,
+        }
+    }
+
+    pub fn loading_description(&self) -> String {
+        match self {
+            Self::All => "all remotes".to_string(),
+            Self::Remote { name } => format!("remote {}", name),
+            Self::Node { remote, name } => format!("node {}/{}", remote, name),
+        }
+    }
+}
+
+impl fmt::Display for Scope {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::All => write!(f, "All"),
+            Self::Remote { name } => write!(f, "{}", name),
+            Self::Node { remote, name } => write!(f, "{}/{}", remote, name),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct LoadState {
+    pub data_generation: usize,
+    pub nodes_generation: usize,
+    pub is_loading: bool,
+}
+
+impl Default for LoadState {
+    fn default() -> Self {
+        Self {
+            data_generation: 0,
+            nodes_generation: 0,
+            is_loading: false,
+        }
+    }
+}
+
+impl LoadState {
+    pub fn start_data_load(&mut self) -> usize {
+        self.data_generation = self.data_generation.wrapping_add(1);
+        self.is_loading = true;
+        self.data_generation
+    }
+
+    pub fn start_nodes_load(&mut self) -> usize {
+        self.nodes_generation = self.nodes_generation.wrapping_add(1);
+        self.nodes_generation
+    }
+
+    pub fn finish_load(&mut self) {
+        self.is_loading = false;
+    }
+
+    pub fn is_data_current(&self, generation: usize) -> bool {
+        generation == self.data_generation
+    }
+
+    pub fn is_nodes_current(&self, generation: usize) -> bool {
+        generation == self.nodes_generation
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct GuestEntry {
+    pub guest: GuestFirewallStatus,
+    pub node: String,
+    pub remote: String,
+    pub masked: bool,
+}
+
+impl GuestEntry {
+    pub fn new(guest: GuestFirewallStatus, node: String, remote: String, masked: bool) -> Self {
+        Self {
+            guest,
+            node,
+            remote,
+            masked,
+        }
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub enum TreeEntry {
+    Root,
+    Remote(RemoteEntry),
+    Node(NodeEntry),
+    Guest(GuestEntry, GuestKind),
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct RemoteEntry {
+    pub name: String,
+    pub status: Option<FirewallStatus>,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct NodeEntry {
+    pub remote: String,
+    pub name: String,
+    pub status: Option<FirewallStatus>,
+    pub masked: bool,
+}
+
+impl TreeEntry {
+    pub fn name(&self) -> String {
+        match self {
+            Self::Root => String::new(),
+            Self::Remote(entry) => entry.name.clone(),
+            Self::Node(entry) => entry.name.clone(),
+            Self::Guest(guest, _) => {
+                format!("{} ({})", guest.guest.vmid, guest.guest.name)
+            }
+        }
+    }
+
+    pub fn matches_filter(&self, filter_text: &str) -> bool {
+        let text = filter_text.to_lowercase();
+
+        match self {
+            Self::Root | Self::Remote(..) | Self::Node(..) => true,
+            Self::Guest(guest, kind) => {
+                let type_name = kind.as_str();
+                guest.guest.name.to_lowercase().contains(&text)
+                    || guest.guest.vmid.to_string().contains(&text)
+                    || type_name.contains(&text)
+                    || guest.node.to_lowercase().contains(&text)
+                    || guest.remote.to_lowercase().contains(&text)
+            }
+        }
+    }
+
+    pub fn icon_name(&self) -> Option<&'static str> {
+        match self {
+            Self::Remote(..) => Some("server"),
+            Self::Node(..) => Some("building"),
+            Self::Guest(_, GuestKind::Lxc) => Some("cube"),
+            Self::Guest(_, GuestKind::Qemu) => Some("desktop"),
+            Self::Root => None,
+        }
+    }
+
+    pub fn is_editable(&self) -> bool {
+        !matches!(self, Self::Root)
+    }
+
+    pub fn firewall_status(&self) -> Option<(&FirewallStatus, bool)> {
+        match self {
+            Self::Remote(entry) => entry.status.as_ref().map(|s| (s, false)),
+            Self::Node(entry) => entry.status.as_ref().map(|s| (s, entry.masked)),
+            Self::Guest(guest, _) => guest.guest.status.as_ref().map(|s| (s, guest.masked)),
+            Self::Root => None,
+        }
+    }
+
+    pub fn rule_stats(&self) -> Option<&RuleStat> {
+        self.firewall_status().map(|(status, _)| &status.rules)
+    }
+
+    pub fn sort_rank(&self) -> u8 {
+        match self {
+            Self::Root => 0,
+            Self::Remote(..) => 1,
+            Self::Node(..) => 2,
+            Self::Guest(_, GuestKind::Lxc) => 3,
+            Self::Guest(_, GuestKind::Qemu) => 4,
+        }
+    }
+}
+
+impl ExtractPrimaryKey for TreeEntry {
+    fn extract_key(&self) -> yew::virtual_dom::Key {
+        use yew::virtual_dom::Key;
+        match self {
+            Self::Root => Key::from("root"),
+            Self::Remote(entry) => Key::from(format!("remote-{}", entry.name)),
+            Self::Node(entry) => Key::from(format!("{}/{}", entry.remote, entry.name)),
+            Self::Guest(guest, _) => Key::from(format!(
+                "{}/{}/{}",
+                guest.remote, guest.node, guest.guest.vmid
+            )),
+        }
+    }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum ViewState {
+    EditRemote {
+        remote: String,
+    },
+    EditNode {
+        remote: String,
+        node: String,
+    },
+    EditGuest {
+        remote: String,
+        node: String,
+        vmid: u32,
+        ty: GuestKind,
+    },
+}
+
+impl ViewState {
+    pub fn from_entry(entry: &TreeEntry) -> Option<Self> {
+        match entry {
+            TreeEntry::Remote(e) => Some(Self::EditRemote {
+                remote: e.name.clone(),
+            }),
+            TreeEntry::Node(e) => Some(Self::EditNode {
+                remote: e.remote.clone(),
+                node: e.name.clone(),
+            }),
+            TreeEntry::Guest(guest, _) => Some(Self::EditGuest {
+                remote: guest.remote.clone(),
+                node: guest.node.clone(),
+                vmid: guest.guest.vmid,
+                ty: guest.guest.kind,
+            }),
+            TreeEntry::Root => None,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum FirewallError {
+    RemoteListLoadFailed(String),
+    StatusLoadFailed { scope: Scope, message: String },
+    NodesLoadFailed { remote: String, message: String },
+}
+
+impl fmt::Display for FirewallError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::RemoteListLoadFailed(msg) => {
+                write!(f, "Failed to load remote list: {}", msg)
+            }
+            Self::StatusLoadFailed { scope, message } => {
+                write!(
+                    f,
+                    "Failed to load firewall status for {}: {}",
+                    scope, message
+                )
+            }
+            Self::NodesLoadFailed { remote, message } => {
+                write!(f, "Failed to load nodes for remote {}: {}", remote, message)
+            }
+        }
+    }
+}
+
+impl std::error::Error for FirewallError {}
diff --git a/ui/src/remotes/firewall/ui_helpers.rs b/ui/src/remotes/firewall/ui_helpers.rs
new file mode 100644
index 0000000..f9be466
--- /dev/null
+++ b/ui/src/remotes/firewall/ui_helpers.rs
@@ -0,0 +1,156 @@
+use pdm_api_types::firewall::{FirewallStatus, GuestKind, RuleStat};
+use pwt::css::{AlignItems, FontColor};
+use pwt::prelude::*;
+use pwt::tr;
+use pwt::widget::{Container, Fa, Panel, Row};
+use yew::{html, Html};
+
+use super::types::TreeEntry;
+
+pub fn render_firewall_status(status: &FirewallStatus, masked: bool) -> Html {
+    if status.enabled {
+        let check = if !masked {
+            Fa::new("check").class(FontColor::Success)
+        } else {
+            Fa::new("check")
+        };
+        Row::new()
+            .class(AlignItems::Baseline)
+            .gap(2)
+            .with_child(check)
+            .into()
+    } else {
+        Row::new()
+            .class(AlignItems::Baseline)
+            .gap(2)
+            .with_child(Fa::new("minus"))
+            .into()
+    }
+}
+
+pub fn render_rule_stats(rules: &RuleStat) -> Html {
+    if rules.all == 0 {
+        return Html::default();
+    }
+    Row::new()
+        .with_child(format!(
+            "{} out of {} rules enabled",
+            rules.active, rules.all
+        ))
+        .into()
+}
+
+pub fn render_warning_icon() -> Html {
+    Row::new()
+        .with_child(Fa::new("exclamation-triangle").class(FontColor::Warning))
+        .into()
+}
+
+pub fn render_load_error_message() -> Html {
+    Row::new()
+        .with_child(tr!("Could not load firewall status"))
+        .into()
+}
+
+pub fn create_panel_title(icon_name: &str, title_text: String) -> Html {
+    Row::new()
+        .gap(2)
+        .class(AlignItems::Baseline)
+        .with_child(Fa::new(icon_name))
+        .with_child(title_text)
+        .into()
+}
+
+pub fn create_rules_panel(title: Html, key: String, content: Html) -> Panel {
+    Panel::new()
+        .class(pwt::css::FlexFit)
+        .title(title)
+        .border(true)
+        .min_width(500)
+        .with_child(Container::new().key(key).with_child(content))
+        .style("flex", "1 1 0")
+}
+
+pub struct PanelConfig {
+    pub title: Html,
+    pub key: String,
+    pub content: Html,
+}
+
+impl PanelConfig {
+    pub fn for_remote(remote: &str) -> Self {
+        Self {
+            title: create_panel_title("list", tr!("Cluster Firewall Rules - {}", remote)),
+            key: format!("cluster-{}", remote),
+            content: proxmox_yew_comp::FirewallRules::cluster(remote.to_string()).into(),
+        }
+    }
+
+    pub fn for_node(remote: &str, node: &str) -> Self {
+        Self {
+            title: create_panel_title("list", tr!("Node Firewall Rules - {}/{}", remote, node)),
+            key: format!("node-{}-{}", remote, node),
+            content: proxmox_yew_comp::FirewallRules::node(remote.to_string(), node.to_string())
+                .into(),
+        }
+    }
+
+    pub fn for_guest(remote: &str, node: &str, vmid: u32, kind: GuestKind) -> Self {
+        let vmtype = kind.as_str();
+        Self {
+            title: create_panel_title(
+                "list",
+                tr!(
+                    "Guest Firewall Rules - {}/{}/{} {}",
+                    remote,
+                    node,
+                    vmtype.to_uppercase(),
+                    vmid
+                ),
+            ),
+            key: format!("guest-{}-{}-{}-{}", remote, node, vmid, vmtype),
+            content: proxmox_yew_comp::FirewallRules::guest(
+                remote.to_string(),
+                node.to_string(),
+                vmid as u64,
+                vmtype,
+            )
+            .into(),
+        }
+    }
+
+    pub fn for_no_selection() -> Self {
+        let header = tr!("No entry selected");
+        let msg = tr!("Select a firewall entry to show its rules.");
+
+        let content = pwt::widget::Column::new()
+            .class(pwt::css::FlexFit)
+            .padding(2)
+            .class(AlignItems::Center)
+            .class(pwt::css::TextAlign::Center)
+            .with_child(html! {<h1 class="pwt-font-headline-medium">{header}</h1>})
+            .with_child(Container::new().with_child(msg))
+            .into();
+
+        Self {
+            title: create_panel_title("list", tr!("Firewall Rules")),
+            key: String::new(),
+            content,
+        }
+    }
+
+    pub fn from_entry(entry: &TreeEntry) -> Self {
+        match entry {
+            TreeEntry::Remote(remote_entry) => Self::for_remote(&remote_entry.name),
+            TreeEntry::Node(node_entry) => Self::for_node(&node_entry.remote, &node_entry.name),
+            TreeEntry::Guest(guest, kind) => {
+                Self::for_guest(&guest.remote, &guest.node, guest.guest.vmid, *kind)
+            }
+            TreeEntry::Root => Self::for_no_selection(),
+        }
+    }
+
+    pub fn build(self) -> Panel {
+        create_rules_panel(self.title, self.key, self.content)
+    }
+}
diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index 5e06b2c..603077c 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -27,6 +27,9 @@ pub use tasks::RemoteTaskList;
 mod updates;
 pub use updates::UpdateTree;
 
+mod firewall;
+pub use firewall::FirewallTree;
+
 use yew::{function_component, Html};
 
 use pwt::prelude::*;
@@ -63,6 +66,13 @@ pub fn system_configuration() -> Html {
                 .label(tr!("Updates"))
                 .icon_class("fa fa-refresh"),
             |_| UpdateTree::new().into(),
+        )
+        .with_item_builder(
+            TabBarItem::new()
+                .key("firewall")
+                .label(tr!("Firewall"))
+                .icon_class("fa fa-shield"),
+            |_| FirewallTree::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] 14+ messages in thread

end of thread, other threads:[~2025-10-30 14:34 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-30 14:33 [pdm-devel] [PATCH proxmox{, -yew-comp, -datacenter-manager} 00/13] add basic integration of PVE firewall Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 1/5] pve-api-types: update pve-api.json Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 2/5] pve-api-types: add get/update firewall options endpoints Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 3/5] pve-api-types: schema2rust: handle `macro` keyword like we do `type` Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 4/5] pve-api-types: add list firewall rules endpoints Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox 5/5] pve-api-types: regenerate Hannes Laimer
2025-10-30 14:33 ` [pdm-devel] [PATCH proxmox-yew-comp 1/4] form: add helpers for extractig data out of schemas Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 2/4] firewall: add FirewallContext Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 3/4] firewall: add options edit form Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-yew-comp 4/4] firewall: add rules table Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] pdm-api-types: add firewall status types Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] api: firewall: add option, rules and status endpoints Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] pdm-client: add api methods for firewall options, " Hannes Laimer
2025-10-30 14:34 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: add firewall status tree Hannes Laimer

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