public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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 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