]> git.proxmox.com Git - pve-network.git/blob - src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
controllers: evpn: bugfix: use prefix-list in route-map instead evpn match
[pve-network.git] / src / PVE / Network / SDN / Controllers / EvpnPlugin.pm
1 package PVE::Network::SDN::Controllers::EvpnPlugin;
2
3 use strict;
4 use warnings;
5
6 use PVE::INotify;
7 use PVE::JSONSchema qw(get_standard_option);
8 use PVE::Tools qw(run_command file_set_contents file_get_contents);
9 use PVE::RESTEnvironment qw(log_warn);
10
11 use PVE::Network::SDN::Controllers::Plugin;
12 use PVE::Network::SDN::Zones::Plugin;
13 use Net::IP;
14
15 use base('PVE::Network::SDN::Controllers::Plugin');
16
17 sub type {
18 return 'evpn';
19 }
20
21 sub properties {
22 return {
23 asn => {
24 type => 'integer',
25 description => "autonomous system number",
26 minimum => 0,
27 maximum => 4294967296
28 },
29 peers => {
30 description => "peers address list.",
31 type => 'string', format => 'ip-list'
32 },
33 };
34 }
35
36 sub options {
37 return {
38 'asn' => { optional => 0 },
39 'peers' => { optional => 0 },
40 };
41 }
42
43 # Plugin implementation
44 sub generate_controller_config {
45 my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_;
46
47 my @peers;
48 @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
49
50 my $local_node = PVE::INotify::nodename();
51
52 my $asn = $plugin_config->{asn};
53 my $ebgp = undef;
54 my $loopback = undef;
55 my $autortas = undef;
56 my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
57 my $isisrouter = find_isis_controller($local_node, $controller_cfg);
58
59 if ($bgprouter) {
60 $ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn};
61 $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
62 $asn = $bgprouter->{asn} if $bgprouter->{asn};
63 $autortas = $plugin_config->{'asn'} if $ebgp;
64 } elsif ($isisrouter) {
65 $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
66 }
67
68 return if !$asn;
69
70 my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
71
72 my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
73
74 my $remoteas = $ebgp ? "external" : $asn;
75
76 #global options
77 my @controller_config = (
78 "bgp router-id $ifaceip",
79 "no bgp hard-administrative-reset",
80 "no bgp graceful-restart notification",
81 "no bgp default ipv4-unicast",
82 "coalesce-time 1000",
83 );
84
85 push(@{$bgp->{""}}, @controller_config) if keys %{$bgp} == 0;
86
87 @controller_config = ();
88
89 #VTEP neighbors
90 push @controller_config, "neighbor VTEP peer-group";
91 push @controller_config, "neighbor VTEP remote-as $remoteas";
92 push @controller_config, "neighbor VTEP bfd";
93
94 push @controller_config, "neighbor VTEP ebgp-multihop 10" if $ebgp && $loopback;
95 push @controller_config, "neighbor VTEP update-source $loopback" if $loopback;
96
97 # VTEP peers
98 foreach my $address (@peers) {
99 next if $address eq $ifaceip;
100 push @controller_config, "neighbor $address peer-group VTEP";
101 }
102
103 push(@{$bgp->{""}}, @controller_config);
104
105 # address-family l2vpn
106 @controller_config = ();
107 push @controller_config, "neighbor VTEP route-map MAP_VTEP_IN in";
108 push @controller_config, "neighbor VTEP route-map MAP_VTEP_OUT out";
109 push @controller_config, "neighbor VTEP activate";
110 push @controller_config, "advertise-all-vni";
111 push @controller_config, "autort as $autortas" if $autortas;
112 push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
113
114 my $routemap = { rule => undef, action => "permit" };
115 push(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap );
116 push(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap );
117
118 return $config;
119 }
120
121 sub generate_controller_zone_config {
122 my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_;
123
124 my $local_node = PVE::INotify::nodename();
125
126 my $vrf = "vrf_$id";
127 my $vrfvxlan = $plugin_config->{'vrf-vxlan'};
128 my $exitnodes = $plugin_config->{'exitnodes'};
129 my $exitnodes_primary = $plugin_config->{'exitnodes-primary'};
130 my $advertisesubnets = $plugin_config->{'advertise-subnets'};
131 my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'};
132 my $rt_import;
133 $rt_import = [PVE::Tools::split_list($plugin_config->{'rt-import'})] if $plugin_config->{'rt-import'};
134
135 my $asn = $controller->{asn};
136 my @peers;
137 @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'};
138 my $ebgp = undef;
139 my $loopback = undef;
140 my $autortas = undef;
141 my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
142 my $isisrouter = find_isis_controller($local_node, $controller_cfg);
143
144 if($bgprouter) {
145 $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn};
146 $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
147 $asn = $bgprouter->{asn} if $bgprouter->{asn};
148 $autortas = $controller->{'asn'} if $ebgp;
149 } elsif ($isisrouter) {
150 $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
151 }
152
153 return if !$vrf || !$vrfvxlan || !$asn;
154
155 my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
156 my $is_gateway = $exitnodes->{$local_node};
157
158 # vrf
159 my @controller_config = ();
160 push @controller_config, "vni $vrfvxlan";
161 #avoid to routes between nodes through the exit nodes
162 #null routes subnets of other zones
163 if ($is_gateway) {
164 my $subnets = PVE::Network::SDN::Vnets::get_subnets();
165 foreach my $subnetid (sort keys %{$subnets}) {
166 my $subnet = $subnets->{$subnetid};
167 my $cidr = $subnet->{cidr};
168 my $zone = $subnet->{zone};
169 push @controller_config, "ip route $cidr null0" if $zone ne $id;
170 }
171 }
172
173 push(@{$config->{frr}->{vrf}->{"$vrf"}}, @controller_config);
174
175 #main vrf router
176 @controller_config = ();
177 push @controller_config, "bgp router-id $ifaceip";
178 push @controller_config, "no bgp hard-administrative-reset";
179 push @controller_config, "no bgp graceful-restart notification";
180
181 # push @controller_config, "!";
182 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, @controller_config);
183
184 if ($autortas) {
185 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target import $autortas:$vrfvxlan");
186 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target export $autortas:$vrfvxlan");
187 }
188
189 if ($is_gateway) {
190
191 $config->{frr_prefix_list}->{'only_default'}->{1} = "permit 0.0.0.0/0";
192 $config->{frr_prefix_list_v6}->{'only_default_v6'}->{1} = "permit ::/0";
193
194 if (!$exitnodes_primary || $exitnodes_primary eq $local_node) {
195 #filter default route coming from other exit nodes on primary node or both nodes if no primary is defined.
196 my $routemap_config_v6 = ();
197 push @{$routemap_config_v6}, "match ip address prefix-list only_default_v6";
198 my $routemap_v6 = { rule => $routemap_config_v6, action => "deny" };
199 unshift(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap_v6);
200
201 my $routemap_config = ();
202 push @{$routemap_config}, "match ip address prefix-list only_default";
203 my $routemap = { rule => $routemap_config, action => "deny" };
204 unshift(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap);
205
206 } elsif ($exitnodes_primary ne $local_node) {
207 my $routemap_config_v6 = ();
208 push @{$routemap_config_v6}, "match ipv6 address prefix-list only_default_v6";
209 push @{$routemap_config_v6}, "set metric 200";
210 my $routemap_v6 = { rule => $routemap_config_v6, action => "permit" };
211 unshift(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap_v6);
212
213 my $routemap_config = ();
214 push @{$routemap_config}, "match ip address prefix-list only_default";
215 push @{$routemap_config}, "set metric 200";
216 my $routemap = { rule => $routemap_config, action => "permit" };
217 unshift(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap);
218 }
219
220 if (!$exitnodes_local_routing) {
221 @controller_config = ();
222 #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
223 push @controller_config, "import vrf $vrf";
224 push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config);
225 push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config);
226
227 @controller_config = ();
228 #redistribute connected to be able to route to local vms on the gateway
229 push @controller_config, "redistribute connected";
230 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config);
231 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config);
232 }
233
234 @controller_config = ();
235 #add default originate to announce 0.0.0.0/0 type5 route in evpn
236 push @controller_config, "default-originate ipv4";
237 push @controller_config, "default-originate ipv6";
238 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
239 } elsif ($advertisesubnets) {
240
241 @controller_config = ();
242 #redistribute connected networks
243 push @controller_config, "redistribute connected";
244 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config);
245 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config);
246
247 @controller_config = ();
248 #advertise connected networks type5 route in evpn
249 push @controller_config, "advertise ipv4 unicast";
250 push @controller_config, "advertise ipv6 unicast";
251 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
252 }
253
254 if ($rt_import) {
255 @controller_config = ();
256 foreach my $rt (sort @{$rt_import}) {
257 push @controller_config, "route-target import $rt";
258 }
259 push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
260 }
261
262 return $config;
263 }
264
265 sub generate_controller_vnet_config {
266 my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_;
267
268 my $exitnodes = $zone->{'exitnodes'};
269 my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'};
270
271 return if !$exitnodes_local_routing;
272
273 my $local_node = PVE::INotify::nodename();
274 my $is_gateway = $exitnodes->{$local_node};
275
276 return if !$is_gateway;
277
278 my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
279 my @controller_config = ();
280 foreach my $subnetid (sort keys %{$subnets}) {
281 my $subnet = $subnets->{$subnetid};
282 my $cidr = $subnet->{cidr};
283 push @controller_config, "ip route $cidr 10.255.255.2 xvrf_$zoneid";
284 }
285 push(@{$config->{frr_ip_protocol}}, @controller_config);
286 }
287
288 sub on_delete_hook {
289 my ($class, $controllerid, $zone_cfg) = @_;
290
291 # verify that zone is associated to this controller
292 foreach my $id (keys %{$zone_cfg->{ids}}) {
293 my $zone = $zone_cfg->{ids}->{$id};
294 die "controller $controllerid is used by $id"
295 if (defined($zone->{controller}) && $zone->{controller} eq $controllerid);
296 }
297 }
298
299 sub on_update_hook {
300 my ($class, $controllerid, $controller_cfg) = @_;
301
302 # we can only have 1 evpn controller / 1 asn by server
303
304 my $controllernb = 0;
305 foreach my $id (keys %{$controller_cfg->{ids}}) {
306 next if $id eq $controllerid;
307 my $controller = $controller_cfg->{ids}->{$id};
308 next if $controller->{type} ne "evpn";
309 $controllernb++;
310 die "only 1 global evpn controller can be defined" if $controllernb >= 1;
311 }
312 }
313
314 sub find_bgp_controller {
315 my ($nodename, $controller_cfg) = @_;
316
317 my $res = undef;
318 foreach my $id (keys %{$controller_cfg->{ids}}) {
319 my $controller = $controller_cfg->{ids}->{$id};
320 next if $controller->{type} ne 'bgp';
321 next if $controller->{node} ne $nodename;
322 $res = $controller;
323 last;
324 }
325 return $res;
326 }
327
328 sub find_isis_controller {
329 my ($nodename, $controller_cfg) = @_;
330
331 my $res = undef;
332 foreach my $id (keys %{$controller_cfg->{ids}}) {
333 my $controller = $controller_cfg->{ids}->{$id};
334 next if $controller->{type} ne 'isis';
335 next if $controller->{node} ne $nodename;
336 $res = $controller;
337 last;
338 }
339 return $res;
340 }
341
342 sub generate_frr_recurse{
343 my ($final_config, $content, $parentkey, $level) = @_;
344
345 my $keylist = {};
346 $keylist->{'address-family'} = 1;
347 $keylist->{router} = 1;
348
349 my $exitkeylist = {};
350 $exitkeylist->{'address-family'} = 1;
351
352 my $simple_exitkeylist = {};
353 $simple_exitkeylist->{router} = 1;
354
355 # FIXME: make this generic
356 my $paddinglevel = undef;
357 if ($level == 1 || $level == 2) {
358 $paddinglevel = $level - 1;
359 } elsif ($level == 3 || $level == 4) {
360 $paddinglevel = $level - 2;
361 }
362
363 my $padding = "";
364 $padding = ' ' x ($paddinglevel) if $paddinglevel;
365
366 if (ref $content eq 'HASH') {
367 foreach my $key (sort keys %$content) {
368 next if $key eq 'vrf';
369 if ($parentkey && defined($keylist->{$parentkey})) {
370 push @{$final_config}, $padding."!";
371 push @{$final_config}, $padding."$parentkey $key";
372 } elsif ($key ne '' && !defined($keylist->{$key})) {
373 push @{$final_config}, $padding."$key";
374 }
375
376 my $option = $content->{$key};
377 generate_frr_recurse($final_config, $option, $key, $level+1);
378
379 push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
380 push @{$final_config}, $padding."exit" if $parentkey && defined($simple_exitkeylist->{$parentkey});
381 }
382 }
383
384 if (ref $content eq 'ARRAY') {
385 push @{$final_config}, map { $padding . "$_" } @$content;
386 }
387 }
388
389 sub generate_frr_vrf {
390 my ($final_config, $vrfs) = @_;
391
392 return if !$vrfs;
393
394 my @config = ();
395
396 foreach my $id (sort keys %$vrfs) {
397 my $vrf = $vrfs->{$id};
398 push @config, "!";
399 push @config, "vrf $id";
400 foreach my $rule (@$vrf) {
401 push @config, " $rule";
402
403 }
404 push @config, "exit-vrf";
405 }
406
407 push @{$final_config}, @config;
408 }
409
410 sub generate_frr_ip_protocol {
411 my ($final_config, $ips) = @_;
412
413 return if !$ips;
414
415 my @config = ();
416 push @{$final_config}, "!";
417 foreach my $rule (sort @$ips) {
418 push @{$final_config}, $rule;
419 }
420
421 }
422
423 sub generate_frr_interfaces {
424 my ($final_config, $interfaces) = @_;
425
426 foreach my $k (sort keys %$interfaces) {
427 my $iface = $interfaces->{$k};
428 push @{$final_config}, "!";
429 push @{$final_config}, "interface $k";
430 foreach my $rule (sort @$iface) {
431 push @{$final_config}, " $rule";
432 }
433 }
434 }
435
436 sub generate_frr_routemap {
437 my ($final_config, $routemaps) = @_;
438
439 foreach my $id (sort keys %$routemaps) {
440
441 my $routemap = $routemaps->{$id};
442 my $order = 0;
443 foreach my $seq (@$routemap) {
444 $order++;
445 next if !defined($seq->{action});
446 my @config = ();
447 push @config, "!";
448 push @config, "route-map $id $seq->{action} $order";
449 my $rule = $seq->{rule};
450 push @config, map { " $_" } @$rule;
451 push @{$final_config}, @config;
452 push @{$final_config}, "exit";
453 }
454 }
455 }
456
457 sub generate_frr_list {
458 my ($final_config, $lists, $type) = @_;
459
460 my $config = [];
461
462 for my $id (sort keys %$lists) {
463 my $list = $lists->{$id};
464
465 for my $seq (sort keys %$list) {
466 my $rule = $list->{$seq};
467 push @$config, "$type $id seq $seq $rule";
468 }
469 }
470
471 if (@$config > 0) {
472 push @{$final_config}, "!", @$config;
473 }
474 }
475
476 sub generate_controller_rawconfig {
477 my ($class, $plugin_config, $config) = @_;
478
479 my $nodename = PVE::INotify::nodename();
480
481 my $final_config = [];
482 push @{$final_config}, "frr version 8.5.1";
483 push @{$final_config}, "frr defaults datacenter";
484 push @{$final_config}, "hostname $nodename";
485 push @{$final_config}, "log syslog informational";
486 push @{$final_config}, "service integrated-vtysh-config";
487 push @{$final_config}, "!";
488
489 if (-e "/etc/frr/frr.conf.local") {
490 my $local_conf = file_get_contents("/etc/frr/frr.conf.local");
491 parse_merge_frr_local_config($config, $local_conf);
492 }
493
494 generate_frr_vrf($final_config, $config->{frr}->{vrf});
495 generate_frr_interfaces($final_config, $config->{frr_interfaces});
496 generate_frr_recurse($final_config, $config->{frr}, undef, 0);
497 generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
498 generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
499 generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
500 generate_frr_routemap($final_config, $config->{frr_routemap});
501 generate_frr_ip_protocol($final_config, $config->{frr_ip_protocol});
502
503 push @{$final_config}, "!";
504 push @{$final_config}, "line vty";
505 push @{$final_config}, "!";
506
507 my $rawconfig = join("\n", @{$final_config});
508
509 return if !$rawconfig;
510 return $rawconfig;
511 }
512
513 sub parse_merge_frr_local_config {
514 my ($config, $local_conf) = @_;
515
516 my $section = \$config->{""};
517 my $router = undef;
518 my $routemap = undef;
519 my $routemap_config = ();
520 my $routemap_action = undef;
521
522 while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
523 my $line = $1;
524 $line =~ s/^\s+|\s+$//g;
525
526 if ($line =~ m/^router (.+)$/) {
527 $router = $1;
528 $section = \$config->{'frr'}->{'router'}->{$router}->{""};
529 next;
530 } elsif ($line =~ m/^vrf (.+)$/) {
531 $section = \$config->{'frr'}->{'vrf'}->{$1};
532 next;
533 } elsif ($line =~ m/^interface (.+)$/) {
534 $section = \$config->{'frr_interfaces'}->{$1};
535 next;
536 } elsif ($line =~ m/address-family (.+)$/) {
537 $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
538 next;
539 } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
540 $routemap = $1;
541 $routemap_config = ();
542 $routemap_action = $2;
543 $section = \$config->{'frr_routemap'}->{$routemap};
544 next;
545 } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
546 $config->{'frr_access_list'}->{$1}->{$2} = $3;
547 next;
548 } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
549 $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
550 next;
551 } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
552 $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
553 next;
554 } elsif($line =~ m/^exit-address-family$/) {
555 next;
556 } elsif($line =~ m/^exit$/) {
557 if($router) {
558 $section = \$config->{''};
559 $router = undef;
560 } elsif($routemap) {
561 push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
562 $section = \$config->{''};
563 $routemap = undef;
564 $routemap_action = undef;
565 $routemap_config = ();
566 }
567 next;
568 } elsif($line =~ m/!/) {
569 next;
570 }
571
572 next if !$section;
573 if($routemap) {
574 push(@{$routemap_config}, $line);
575 } else {
576 push(@{$$section}, $line);
577 }
578 }
579 }
580
581 sub write_controller_config {
582 my ($class, $plugin_config, $config) = @_;
583
584 my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config);
585 return if !$rawconfig;
586 return if !-d "/etc/frr";
587
588 file_set_contents("/etc/frr/frr.conf", $rawconfig);
589 }
590
591 sub reload_controller {
592 my ($class) = @_;
593
594 my $conf_file = "/etc/frr/frr.conf";
595 my $bin_path = "/usr/lib/frr/frr-reload.py";
596
597 if (!-e $bin_path) {
598 log_warn("missing $bin_path. Please install frr-pythontools package");
599 return;
600 }
601
602 my $err = sub {
603 my $line = shift;
604 if ($line =~ /ERROR:/) {
605 warn "$line \n";
606 }
607 };
608
609 if (-e $conf_file && -e $bin_path) {
610 eval {
611 run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err);
612 };
613 if ($@) {
614 warn "frr reload command fail. Restarting frr.";
615 eval { run_command(['systemctl', 'restart', 'frr']); };
616 }
617 }
618 }
619
620 1;
621
622