From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network 07/10] cli: add pvesdn cli tool for managing frr template overrides
Date: Tue, 3 Feb 2026 17:01:25 +0100 [thread overview]
Message-ID: <20260203160246.353351-19-g.goller@proxmox.com> (raw)
In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com>
This introduces a new cli tool to help users customize frr configuration
templates by overriding default templates, viewing differences, and
resetting modifications when needed.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
debian/libpve-network-api-perl.install | 1 +
debian/libpve-network-perl.install | 4 +
src/Makefile | 2 +-
src/PVE/CLI/Makefile | 7 +
src/PVE/CLI/pvesdn.pm | 252 +++++++++++++++++++++++++
src/PVE/Makefile | 1 +
src/bin/Makefile | 69 +++++++
src/bin/pvesdn | 8 +
8 files changed, 343 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/CLI/Makefile
create mode 100644 src/PVE/CLI/pvesdn.pm
create mode 100644 src/bin/Makefile
create mode 100755 src/bin/pvesdn
diff --git a/debian/libpve-network-api-perl.install b/debian/libpve-network-api-perl.install
index c48f1c76f9f7..1f5ed3eaeb05 100644
--- a/debian/libpve-network-api-perl.install
+++ b/debian/libpve-network-api-perl.install
@@ -1 +1,2 @@
usr/share/perl5/PVE/API2
+usr/share/perl5/PVE/CLI
diff --git a/debian/libpve-network-perl.install b/debian/libpve-network-perl.install
index 4e63c1ff9374..f344b8c85e13 100644
--- a/debian/libpve-network-perl.install
+++ b/debian/libpve-network-perl.install
@@ -1,2 +1,6 @@
lib/systemd/system/dnsmasq@.service.d/00-dnsmasq-after-networking.conf /usr/lib/systemd/system/dnsmasq@.service.d/
usr/share/perl5/PVE/Network
+usr/bin/pvesdn
+usr/share/man/man1/pvesdn.1
+usr/share/bash-completion/completions/pvesdn
+usr/share/zsh/vendor-completions/_pvesdn
diff --git a/src/Makefile b/src/Makefile
index c4056b480251..cbd9a507ae5e 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,4 +1,4 @@
-SUBDIRS := PVE services
+SUBDIRS := PVE services bin
all:
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
diff --git a/src/PVE/CLI/Makefile b/src/PVE/CLI/Makefile
new file mode 100644
index 000000000000..5058945a716b
--- /dev/null
+++ b/src/PVE/CLI/Makefile
@@ -0,0 +1,7 @@
+SOURCES=pvesdn.pm
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/CLI/$$i; done
diff --git a/src/PVE/CLI/pvesdn.pm b/src/PVE/CLI/pvesdn.pm
new file mode 100644
index 000000000000..ebb0b60715c9
--- /dev/null
+++ b/src/PVE/CLI/pvesdn.pm
@@ -0,0 +1,252 @@
+package PVE::CLI::pvesdn;
+
+use strict;
+use warnings;
+
+use File::Path;
+use File::Temp qw(tempfile);
+use File::Copy;
+
+use PVE::RPCEnvironment;
+use PVE::Tools qw(run_command);
+
+use PVE::RS::SDN;
+
+use base qw(PVE::CLIHandler);
+
+sub setup_environment {
+ PVE::RPCEnvironment->setup_default_cli_env();
+}
+
+my $TEMPLATE_OVERRIDE_DIR = "/etc/proxmox-frr/templates";
+
+__PACKAGE__->register_method({
+ name => 'override',
+ path => 'override',
+ method => 'GET',
+ description => "Override FRR templates.",
+ parameters => {
+ properties => {
+ protocol => {
+ description =>
+ "Specifies the FRR routing protocol (e.g., 'bgp', 'ospf') or template file (e.g., 'access_lists.jinja') to copy to the override directory for customization.",
+ type => 'string',
+ },
+ },
+
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+ my @template_files = ();
+
+ if ($param->{protocol} eq 'openfabric') {
+ push(@template_files, 'frr.conf.jinja');
+ push(@template_files, 'fabricd.jinja');
+ push(@template_files, 'protocol_routemaps.jinja');
+ push(@template_files, 'route_maps.jinja');
+ push(@template_files, 'access_lists.jinja');
+ push(@template_files, 'interface.jinja');
+ } elsif ($param->{protocol} eq 'ospf') {
+ push(@template_files, 'frr.conf.jinja');
+ push(@template_files, 'ospfd.jinja');
+ push(@template_files, 'protocol_routemaps.jinja');
+ push(@template_files, 'route_maps.jinja');
+ push(@template_files, 'access_lists.jinja');
+ push(@template_files, 'interface.jinja');
+ } elsif ($param->{protocol} eq 'isis') {
+ push(@template_files, 'frr.conf.jinja');
+ push(@template_files, 'isisd.jinja');
+ push(@template_files, 'interface.jinja');
+ } elsif ($param->{protocol} eq 'bgp') {
+ push(@template_files, 'frr.conf.jinja');
+ push(@template_files, 'bgpd.jinja');
+ push(@template_files, 'bgp_router.jinja');
+ push(@template_files, 'route_maps.jinja');
+ push(@template_files, 'access_lists.jinja');
+ push(@template_files, 'prefix_lists.jinja');
+ push(@template_files, 'ip_routes.jinja');
+ } else {
+ push(@template_files, $param->{protocol});
+ }
+
+ File::Path::make_path($TEMPLATE_OVERRIDE_DIR);
+
+ foreach my $template (@template_files) {
+ my $filepath = "$TEMPLATE_OVERRIDE_DIR/$template";
+
+ open(my $fh, '>', $filepath) or die "Could not open file '$filepath': $!\n";
+
+ my $template_content = PVE::RS::SDN::get_template($template);
+ if (!defined($template_content)) {
+ die "Template '$template' not found\n";
+ }
+ print $fh $template_content;
+ close $fh;
+
+ print "Created override file: $filepath\n";
+ }
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'show',
+ path => 'show',
+ method => 'GET',
+ description => "Show FRR template.",
+ parameters => {
+ properties => {
+ "template-name" => {
+ description => "Name of the FRR template (e.g. 'bgpd.jinja').",
+ type => 'string',
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $template_name = $param->{"template-name"};
+ my $template = PVE::RS::SDN::get_template($template_name);
+ if (defined($template)) {
+ print($template);
+ } else {
+ die("Template '$template_name' not found\n");
+ }
+ return undef;
+ },
+});
+
+sub write_to_template_file {
+ my ($filename, $content) = @_;
+ if ($filename =~ m/^([\w_-]+\.jinja)$/) {
+ my $safe_filename = $1;
+
+ # create backup
+ my $filepath = "$TEMPLATE_OVERRIDE_DIR/$safe_filename";
+ my $backup_path = "$filepath-bak";
+ if (-f $filepath) {
+ copy($filepath, $backup_path) or die "Could not create backup: $!\n";
+ }
+
+ open(my $fh, '>', $filepath) or die "Could not open file '$filepath': $!\n";
+ print $fh $content;
+ close $fh;
+ }
+ return undef;
+}
+
+__PACKAGE__->register_method({
+ name => 'reset',
+ path => 'reset',
+ method => 'GET',
+ description => "Reset a single or all override files by copying the packaged version over.",
+ parameters => {
+ properties => {
+ name => {
+ description => "Name of the FRR template (e.g. 'bgpd.jinja').",
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ if (defined($param->{name})) {
+ my $template = PVE::RS::SDN::get_template($param->{name});
+ if (defined($template)) {
+ print(
+ "Resetting the /etc/proxmox-frr/templates/$param->{name} file - continue (y/N)? "
+ );
+ my $answer = <STDIN>;
+ my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
+ die "Aborting reset as requested\n" if !$continue;
+
+ write_to_template_file($param->{name}, $template);
+ print("Reset template: $param->{name}\n");
+ } else {
+ die("Template '$param->{name}' not found\n");
+ }
+ } else {
+ print(
+ "Resetting all template files in /etc/proxmox-frr/templates/ - continue (y/N)? ");
+ my $answer = <STDIN>;
+ my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
+ die "Aborting reset as requested\n" if !$continue;
+
+ opendir(my $dh, $TEMPLATE_OVERRIDE_DIR) or die "Cannot open directory: $!\n";
+ my @files = grep { -f "$TEMPLATE_OVERRIDE_DIR/$_" } readdir($dh);
+ closedir($dh);
+
+ foreach my $file (@files) {
+ my $packaged_content = PVE::RS::SDN::get_template($file);
+ next unless $packaged_content;
+
+ write_to_template_file($file, $packaged_content);
+ print("Reset template: $file\n");
+ }
+ }
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'diff',
+ path => 'diff',
+ method => 'GET',
+ description => "Show the difference between the override templates and packaged templates.",
+ parameters => {
+ properties => {},
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ opendir(my $dh, $TEMPLATE_OVERRIDE_DIR) or die "Cannot open directory: $!\n";
+ my @files = grep { -f "$TEMPLATE_OVERRIDE_DIR/$_" } readdir($dh);
+ closedir($dh);
+
+ foreach my $file (@files) {
+ # Untaint filename for use with run_command in taint mode
+ next unless $file =~ m/^([\w.-]+)$/;
+ my $safe_file = $1;
+
+ my $override_path = "$TEMPLATE_OVERRIDE_DIR/$safe_file";
+ my $packaged_content = PVE::RS::SDN::get_template($safe_file);
+ next unless $packaged_content;
+
+ my ($temp_fh, $temp_filename) = tempfile();
+ print $temp_fh $packaged_content;
+
+ eval {
+ run_command(
+ [
+ "/usr/bin/diff",
+ "--color=always",
+ "-N",
+ "-u",
+ "$override_path",
+ "$temp_filename",
+ ],
+ );
+ };
+ close($temp_fh);
+ }
+ return undef;
+ },
+});
+
+our $cmddef = {
+ template => {
+ override => [__PACKAGE__, 'override', ['protocol']],
+ show => [__PACKAGE__, 'show', ['template-name']],
+ diff => [__PACKAGE__, 'diff', []],
+ reset => [__PACKAGE__, 'reset', []],
+ },
+};
+
+1;
+
diff --git a/src/PVE/Makefile b/src/PVE/Makefile
index 7f1cf985465f..d56158823099 100644
--- a/src/PVE/Makefile
+++ b/src/PVE/Makefile
@@ -4,5 +4,6 @@ all:
install:
make -C Network install
make -C API2 install
+ make -C CLI install
clean:
diff --git a/src/bin/Makefile b/src/bin/Makefile
new file mode 100644
index 000000000000..ed539f5e3f94
--- /dev/null
+++ b/src/bin/Makefile
@@ -0,0 +1,69 @@
+PERL_DOC_INC_DIRS=..
+-include /usr/share/pve-doc-generator/pve-doc-generator.mk
+
+
+CLITOOLS = \
+ pvesdn \
+
+CLI_MANS = \
+ $(addsuffix .1, $(CLITOOLS)) \
+
+BASH_COMPLETIONS = \
+ $(addsuffix .bash-completion, $(CLITOOLS)) \
+
+ZSH_COMPLETIONS = \
+ $(addsuffix .zsh-completion, $(CLITOOLS)) \
+
+BINDIR=/usr/bin
+MAN1DIR=/usr/share/man/man1
+BASHCOMPLDIR=/usr/share/bash-completion/completions
+ZSHCOMPLDIR=/usr/share/zsh/vendor-completions
+DESTDIR=
+
+all: $(CLI_MANS)
+
+%.1: %.1.pod
+ rm -f $@
+ cat $<|pod2man -n $* -s 1 -r $(VERSION) -c"Proxmox Documentation" - >$@.tmp
+ mv $@.tmp $@
+
+%.1.pod:
+ podselect $* > $@.tmp
+ mv $@.tmp $@
+
+.PHONY: tidy
+tidy:
+ echo $(CLITOOLS) | xargs -n4 -P0 proxmox-perltidy
+
+pvesdn.api-verified:
+ touch $@
+
+pvesdn.bash-completion:
+ echo "# bash completion for pvesdn" > $@.tmp
+ echo "complete -C 'pvesdn bashcomplete' pvesdn" >> $@.tmp
+ mv $@.tmp $@
+
+pvesdn.zsh-completion:
+ echo "#compdef pvesdn" > $@.tmp
+ echo "" >> $@.tmp
+ mv $@.tmp $@
+
+.PHONY: check
+check: $(addsuffix .api-verified, $(CLITOOLS))
+ rm -f *.service-api-verified *.api-verified
+
+.PHONY: install
+install: $(CLITOOLS) $(CLI_MANS) $(BASH_COMPLETIONS) $(ZSH_COMPLETIONS)
+ install -d $(DESTDIR)$(BINDIR)
+ install -m 0755 $(CLITOOLS) $(DESTDIR)$(BINDIR)
+ install -d $(DESTDIR)$(MAN1DIR)
+ install -m 0644 $(CLI_MANS) $(DESTDIR)$(MAN1DIR)
+ for i in $(CLITOOLS); do install -m 0644 -D $$i.bash-completion $(DESTDIR)$(BASHCOMPLDIR)/$$i; done
+ for i in $(CLITOOLS); do install -m 0644 -D $$i.zsh-completion $(DESTDIR)$(ZSHCOMPLDIR)/_$$i; done
+
+.PHONY: clean
+clean:
+ rm -f *.xml.tmp *.1 *.5 *.8 *{synopsis,opts}.adoc docinfo.xml *.tmp
+ rm -f *~ *.tmp $(CLI_MANS) *.1.pod *.8.pod
+ rm -f *.bash-completion *.zsh-completion *.service-zsh-completion
+
diff --git a/src/bin/pvesdn b/src/bin/pvesdn
new file mode 100755
index 000000000000..a95e596793b0
--- /dev/null
+++ b/src/bin/pvesdn
@@ -0,0 +1,8 @@
+#!/usr/bin/perl -T
+
+use strict;
+use warnings;
+
+use PVE::CLI::pvesdn;
+
+PVE::CLI::pvesdn->run_cli_handler();
--
2.47.3
next prev parent reply other threads:[~2026-02-03 16:04 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-03 16:01 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} 00/23] Generate frr config using jinja templates and rust types Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 1/9] ve-config: firewall: cargo fmt Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 2/9] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 3/9] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 4/9] sdn-types: support variable-length NET identifier Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 5/9] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 6/9] frr: add isis configuration and templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 7/9] frr: support custom frr configuration lines Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 9/9] frr: store frr template content as a const map Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 1/2] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 2/2] sdn: add method to get a frr template Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 01/10] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 02/10] sdn: tests: add missing comment " Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 03/10] tests: use Test::Differences to make test assertions Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 04/10] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 05/10] tests: rearrange some statements in the frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-02-03 16:01 ` Gabriel Goller [this message]
2026-02-03 16:01 ` [PATCH pve-network 08/10] debian: handle user modifications to FRR templates via ucf Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 09/10] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 10/10] test: add test for frr.conf.local merging Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-manager 1/1] sdn: add dry-run view for sdn apply Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-docs 1/1] docs: add man page for the `pvesdn` cli Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260203160246.353351-19-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.