On 3/2/26 1:56 PM, Gabriel Goller wrote:
> The frr object in perl which stores the whole frr config is now also
> modeled in rust, so it changed a bit. Adjust the frr.conf.local merging
> code so that the frr.conf.local is still merged correctly. This makes
> use of the `custom_frr_config` properties scattered in many rust types.
> So if we encounter a line in frr.conf.local that we need to merge, we
> just throw it into this string vec and render it as-is.
> 
> Co-authored-by: Stefan Hanreich <[email protected]>
> Signed-off-by: Gabriel Goller <[email protected]>
> ---
>  src/PVE/Network/SDN/Frr.pm | 202 ++++++++++++++++++++++++++++++-------
>  1 file changed, 168 insertions(+), 34 deletions(-)
> 
> diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
> index b572f4536004..af3074017ddf 100644
> --- a/src/PVE/Network/SDN/Frr.pm
> +++ b/src/PVE/Network/SDN/Frr.pm
> @@ -271,70 +271,204 @@ sub append_local_config {
>      return if !$local_config;
>  
>      my $section = \$frr_config->{""};
> -    my $router = undef;
> +    my $isis_router_name = undef;
> +    my $bgp_router_asn = undef;
> +    my $bgp_router_vrf = undef;
>      my $routemap = undef;
> -    my $routemap_config = ();
> -    my $routemap_action = undef;
> +    my $interface = undef;
> +    my $vrf = undef;
> +    my $new_block = 0;
> +    my $new_af_block = 0;
>  
> -    while ($local_config =~ /^\s*(.+?)\s*$/gm) {
> +    while ($local_config =~ /^(.+?)\s*$/gm) {
>          my $line = $1;
> -        $line =~ s/^\s+|\s+$//g;
> -
> -        if ($line =~ m/^router (.+)$/) {
> -            $router = $1;
> -            $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""};

after this change merging now only works for isis and bgp routers? Did a
quick check with other routing protocols in frr.conf.local and the
routers seem to be missing.

> +        $line =~ s/\s+$//g;
> +
> +        if ($line =~ m/^router isis (.+)$/) {
> +            $isis_router_name = $1;
> +            if (defined 
> $frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}) {

calls to defined should use parentheses (several occurences below)

> +                $section =
> +                    
> \($frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}
> +                        ->{'custom_frr_config'} //= []);
> +            } else {
> +                $new_block = 1;
> +                push(
> +                    $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> +                    "router isis $isis_router_name",
> +                );
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
> +            next;
> +        } elsif ($line =~ m/^router bgp (\S+)(?: vrf (.+))?$/) {
> +            $bgp_router_asn = $1;
> +            $bgp_router_vrf = $2 // 'default';
> +
> +            my $config_line =
> +                defined($2)
> +                ? "router bgp $bgp_router_asn vrf $bgp_router_vrf"
> +                : "router bgp $bgp_router_asn";
> +

this is only required in the else branch?

> +            if (
> +                defined 
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                and 
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> +                eq $bgp_router_asn
> +            ) {
> +                $section =
> +                    
> \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                        ->{'custom_frr_config'} //= []);
> +            } else {
> +                $new_block = 1;
> +                push(
> +                    $frr_config->{'frr'}->{'custom_frr_config'}->@*, 
> $config_line,
> +                );
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
>              next;
>          } elsif ($line =~ m/^vrf (.+)$/) {
> -            $section = \$frr_config->{'frr'}->{'vrf'}->{$1};
> +            $vrf = $1;
> +            if (defined $frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}) {
> +                $section = 
> \$frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}->{'custom_frr_config'};
> +            } else {
> +                $new_block = 1;
> +                push($frr_config->{'frr'}->{'custom_frr_config'}->@*, "vrf 
> $vrf");
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
>              next;
>          } elsif ($line =~ m/^interface (.+)$/) {
> -            $section = \$frr_config->{'frr_interfaces'}->{$1};
> +            $interface = $1;
> +            if (defined 
> $frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}) {

trying to override an existing IS-IS interface doesn't work for me, e.g.
with the following IS-IS controller:

isis: isisberserker
        isis-domain 1
        isis-ifaces ens19,ens20
        isis-net 49.0000.1234.0000.00
        node berserker

and the following frr.conf.local:

interface ens20
  isis circuit-type level-2-only
exit
!

I get an deserialization error:

error: invalid type: Option value, expected a sequence


This seems to be because the generated $frr_config has

    'custom_frr_config' => undef

in its top-level.

> +                $section = 
> \($frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}
> +                    ->{'custom_frr_config'} //= []);
> +            } else {
> +                $new_block = 1;
> +                push(
> +                    $frr_config->{'frr'}->{'custom_frr_config'}->@*, 
> "interface $interface",
> +                );
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
>              next;
>          } elsif ($line =~ m/^bgp community-list (.+)$/) {
> -            push(@{ $frr_config->{'frr_bgp_community_list'} }, $line);
> +            push(@{ $frr_config->{'frr'}->{'custom_frr_config'} }, $line);
>              next;
>          } elsif ($line =~ m/address-family (.+)$/) {
> -            $section = 
> \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
> +            # convert the address family from frr (e.g. l2vpn evpn) into the 
> rust property (e.g. l2vpn_evpn)
> +            my $address_family_unchanged = $1;
> +            my $address_family = $1 =~ s/ /_/gr;
> +
> +            if (
> +                defined 
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                and 
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> +                eq $bgp_router_asn
> +            ) {
> +                if (
> +                    defined 
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                    ->{'address_families'}->{$address_family}
> +                ) {
> +                    $section =
> +                        
> \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                            
> ->{'address_families'}->{$address_family}->{custom_frr_config} //= []);
> +                } else {
> +                    $new_af_block = 1;
> +                    push(
> +                        
> $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                            ->{'custom_frr_config'}->@*,
> +                        " address-family $address_family_unchanged",
> +                    );
> +                    $section = 
> \$frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                        ->{'custom_frr_config'};
> +                }
> +            } else {
> +                $new_af_block = 1;
> +                push(
> +                    $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> +                    " address-family $address_family_unchanged",
> +                );
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
>              next;
>          } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
>              $routemap = $1;
> -            $routemap_config = ();
> -            $routemap_action = $2;
> -            $section = \$frr_config->{'frr_routemap'}->{$routemap};
> +            my $routemap_action = $2;
> +            my $seq_number = $3;
> +            if (defined $frr_config->{'frr'}->{'routemaps'}->{$routemap}) {
> +                my $index = 0;
> +                foreach my $single_routemap 
> ($frr_config->{'frr'}->{'routemaps'}->{$routemap}->@*) {

for is preferred over foreach, see:
https://pve.proxmox.com/wiki/Perl_Style_Guide#Perl_syntax_choices

> +                    if (
> +                        $single_routemap->{'seq'} == $seq_number
> +                        && $single_routemap->{'action'} eq $routemap_action
> +                    ) {
> +                        last;
> +                    }
> +                    $index++;
> +                }
> +                if ($index < scalar @{ 
> $frr_config->{'frr'}->{'routemaps'}->{$routemap} }) {
> +                    $section = 
> \($frr_config->{'frr'}->{'routemaps'}->{$routemap}->[$index]
> +                        ->{'custom_frr_config'} //= []);
> +                } else {
> +                    $new_block = 1;
> +                    push(
> +                        $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> +                        "route-map $routemap $routemap_action $seq_number",
> +                    );
> +                    $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +                }
> +            } else {
> +                $new_block = 1;
> +                push(
> +                    $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> +                    "route-map $routemap $routemap_action $seq_number",
> +                );
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +            }
>              next;

adding entries for an existing route-map (i.e. MAP_VTEP_IN) like so:

route-map MAP_VTEP_IN deny 1
exit
!
route-map MAP_VTEP_IN permit 1
exit
!
route-map MAP_VTEP_IN permit 1
 match community cm-prefmod-400
 set local-preference 400
exit
!
route-map MAP_VTEP_IN permit 1
 match ip address 10
 set local-preference 200
exit
!

merges the route-map configuration with this patch, if the verdict is
the same:

route-map MAP_VTEP_IN permit 1
 match community cm-prefmod-400
 set local-preference 400
 match ip address 10
 set local-preference 200
exit
!

the section with the same seq nr, but different verdict gets
additionally added:

route-map MAP_VTEP_IN deny 1
exit
!

------------------------------------------

with the old merging logic, they were added as separate sections, with
increasing numbers:

route-map MAP_VTEP_IN permit 1
exit
!
route-map MAP_VTEP_IN deny 2
exit
!
route-map MAP_VTEP_IN permit 3
exit
!
route-map MAP_VTEP_IN permit 4
 match community cm-prefmod-400
 set local-preference 400
exit
!
route-map MAP_VTEP_IN permit 5
 match ip address 10
 set local-preference 200
exit
!

Is this intentional? Imo this is quite the breaking change, particularly
because route-maps are probably used relatively often in the frr.conf.local?

>          } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
> -            $frr_config->{'frr_access_list'}->{$1}->{$2} = $3;
> +            push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
>              next;
>          } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
> -            $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3;
> +            push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
>              next;
>          } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
> -            $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
> +            push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
>              next;
> -        } elsif ($line =~ m/^exit-address-family$/) {
> +        } elsif ($line =~ m/exit-address-family$/) {
> +            if ($new_af_block) {
> +                push(@{$$section}, $line);
> +                $section = 
> \$frr_config->{'frr'}->{'bgp'}->{'custom_frr_config'};
> +            } else {
> +                $section =
> +                    
> \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> +                        ->{'custom_frr_config'} //= []);
> +            }
> +            $new_af_block = 0;
>              next;
> -        } elsif ($line =~ m/^exit$/) {
> -            if ($router) {
> -                $section = \$frr_config->{''};
> -                $router = undef;
> -            } elsif ($routemap) {
> -                push(@{$$section}, { rule => $routemap_config, action => 
> $routemap_action });
> -                $section = \$frr_config->{''};
> +        } elsif ($line =~ m/^exit/) {
> +            if ($bgp_router_vrf || $vrf || $interface || $routemap || 
> $isis_router_name) {
> +                # this means we just added a new 
> router/vrf/interface/routemap
> +                if ($new_block) {
> +                    push(@{$$section}, $line);
> +                    push(@{$$section}, "!");
> +                }
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +                # we can't stack these, so exit out of all of them 
> (technically we can have a vrf inside of a router bgp block, but we don't 
> support that)
> +                $isis_router_name = undef;
> +                $bgp_router_vrf = undef;
> +                $bgp_router_asn = undef;
> +                $vrf = undef;
> +                $interface = undef;
>                  $routemap = undef;
> -                $routemap_action = undef;
> -                $routemap_config = ();
> +            } else {
> +                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> +                push(@{$$section}, $line);
> +                push(@{$$section}, "!");
>              }
> +            $new_block = 0;
>              next;
>          } elsif ($line =~ m/!/) {
>              next;
>          }
>  
>          next if !$section;
> -        if ($routemap) {
> -            push(@{$routemap_config}, $line);
> -        } else {
> -            push(@{$$section}, $line);
> -        }
> +        push(@{$$section}, $line);
>      }
>  }
>  




Reply via email to