X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FNetwork%2FSDN%2FControllers%2FEvpnPlugin.pm;h=15b268bb22f483c3e489c16122ccd50ac480fd5a;hb=4bd3d7bf73fccf230552a85d46978a287faa5082;hp=d82de2a7d3df67bceb6b903e79b862a5c8e4ed36;hpb=3caa7687f93302c43dfeca5d9efba29f3ac9c76b;p=pve-network.git diff --git a/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/PVE/Network/SDN/Controllers/EvpnPlugin.pm index d82de2a..15b268b 100644 --- a/PVE/Network/SDN/Controllers/EvpnPlugin.pm +++ b/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -9,6 +9,7 @@ use PVE::Tools qw(run_command file_set_contents file_get_contents); use PVE::Network::SDN::Controllers::Plugin; use PVE::Network::SDN::Zones::Plugin; +use Net::IP; use base('PVE::Network::SDN::Controllers::Plugin'); @@ -21,16 +22,13 @@ sub properties { asn => { type => 'integer', description => "autonomous system number", + minimum => 0, + maximum => 4294967296 }, peers => { description => "peers address list.", type => 'string', format => 'ip-list' }, - 'gateway-nodes' => get_standard_option('pve-node-list'), - 'gateway-external-peers' => { - description => "upstream bgp peers address list.", - type => 'string', format => 'ip-list' - }, }; } @@ -38,117 +36,217 @@ sub options { return { 'asn' => { optional => 0 }, 'peers' => { optional => 0 }, - 'gateway-nodes' => { optional => 1 }, - 'gateway-external-peers' => { optional => 1 }, }; } # Plugin implementation sub generate_controller_config { - my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_; + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; my @peers; @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'}; + my $local_node = PVE::INotify::nodename(); + my $asn = $plugin_config->{asn}; - my $gatewaynodes = $plugin_config->{'gateway-nodes'}; - my @gatewaypeers; - @gatewaypeers = PVE::Tools::split_list($plugin_config->{'gateway-external-peers'}) if $plugin_config->{'gateway-external-peers'}; + my $ebgp = undef; + my $loopback = undef; + my $autortas = undef; + my $bgprouter = find_bgp_controller($local_node, $controller_cfg); + if($bgprouter) { + $ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn}; + $loopback = $bgprouter->{loopback} if $bgprouter->{loopback}; + $asn = $bgprouter->{asn} if $bgprouter->{asn}; + $autortas = $plugin_config->{'asn'} if $ebgp; + } return if !$asn; my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {}; - my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers); + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); - my $is_gateway = undef; - my $local_node = PVE::INotify::nodename(); - - foreach my $gatewaynode (PVE::Tools::split_list($gatewaynodes)) { - $is_gateway = 1 if $gatewaynode eq $local_node; - } + my $remoteas = $ebgp ? "external" : $asn; + #global options my @controller_config = ( "bgp router-id $ifaceip", "no bgp default ipv4-unicast", "coalesce-time 1000", ); + push(@{$bgp->{""}}, @controller_config) if keys %{$bgp} == 0; + + @controller_config = (); + + #VTEP neighbors + push @controller_config, "neighbor VTEP peer-group"; + push @controller_config, "neighbor VTEP remote-as $remoteas"; + push @controller_config, "neighbor VTEP bfd"; + + if($ebgp && $loopback) { + push @controller_config, "neighbor VTEP ebgp-multihop 10"; + push @controller_config, "neighbor VTEP update-source $loopback"; + } + + # VTEP peers foreach my $address (@peers) { next if $address eq $ifaceip; - push @controller_config, "neighbor $address remote-as $asn"; + push @controller_config, "neighbor $address peer-group VTEP"; } - if ($is_gateway) { - foreach my $address (@gatewaypeers) { - push @controller_config, "neighbor $address remote-as external"; - } - } push(@{$bgp->{""}}, @controller_config); + # address-family l2vpn @controller_config = (); - foreach my $address (@peers) { - next if $address eq $ifaceip; - push @controller_config, "neighbor $address activate"; - } + push @controller_config, "neighbor VTEP route-map MAP_VTEP_IN in"; + push @controller_config, "neighbor VTEP route-map MAP_VTEP_OUT out"; + push @controller_config, "neighbor VTEP activate"; push @controller_config, "advertise-all-vni"; + push @controller_config, "autort as $autortas" if $autortas; push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config); - if ($is_gateway) { - # import /32 routes of evpn network from vrf1 to default vrf (for packet return) - @controller_config = map { "neighbor $_ activate" } @gatewaypeers; - - push(@{$bgp->{"address-family"}->{"ipv4 unicast"}}, @controller_config); - push(@{$bgp->{"address-family"}->{"ipv6 unicast"}}, @controller_config); - } + my $routemap = { rule => undef, action => "permit" }; + push(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap ); + push(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap ); return $config; } sub generate_controller_zone_config { - my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_; + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + my $local_node = PVE::INotify::nodename(); my $vrf = "vrf_$id"; my $vrfvxlan = $plugin_config->{'vrf-vxlan'}; + my $exitnodes = $plugin_config->{'exitnodes'}; + my $exitnodes_primary = $plugin_config->{'exitnodes-primary'}; + my $advertisesubnets = $plugin_config->{'advertise-subnets'}; + my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'}; + my $rt_import = [PVE::Tools::split_list($plugin_config->{'rt-import'})] if $plugin_config->{'rt-import'}; + my $asn = $controller->{asn}; - my $gatewaynodes = $controller->{'gateway-nodes'}; + my @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'}; + my $ebgp = undef; + my $loopback = undef; + my $autortas = undef; + my $bgprouter = find_bgp_controller($local_node, $controller_cfg); + if($bgprouter) { + $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn}; + $loopback = $bgprouter->{loopback} if $bgprouter->{loopback}; + $asn = $bgprouter->{asn} if $bgprouter->{asn}; + $autortas = $controller->{'asn'} if $ebgp; + } return if !$vrf || !$vrfvxlan || !$asn; + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + # vrf my @controller_config = (); push @controller_config, "vni $vrfvxlan"; push(@{$config->{frr}->{vrf}->{"$vrf"}}, @controller_config); - push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, "!"); + #main vrf router + @controller_config = (); + push @controller_config, "bgp router-id $ifaceip"; +# push @controller_config, "!"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, @controller_config); + + if ($autortas) { + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target import $autortas:$vrfvxlan"); + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target export $autortas:$vrfvxlan"); + } - my $local_node = PVE::INotify::nodename(); + my $is_gateway = $exitnodes->{$local_node}; - my $is_gateway = grep { $_ eq $local_node } PVE::Tools::split_list($gatewaynodes); if ($is_gateway) { + if(!$exitnodes_primary || $exitnodes_primary eq $local_node) { + #filter default type5 route coming from other exit nodes on primary node or both nodes if no primary is defined. + my $routemap_config = (); + push @{$routemap_config}, "match evpn route-type prefix"; + my $routemap = { rule => $routemap_config, action => "deny" }; + unshift(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap); + } elsif ($exitnodes_primary ne $local_node) { + my $routemap_config = (); + push @{$routemap_config}, "match evpn vni $vrfvxlan"; + push @{$routemap_config}, "match evpn route-type prefix"; + push @{$routemap_config}, "set metric 200"; + my $routemap = { rule => $routemap_config, action => "permit" }; + unshift(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap); + } + + + if (!$exitnodes_local_routing) { + @controller_config = (); + #import /32 routes of evpn network from vrf1 to default vrf (for packet return) + push @controller_config, "import vrf $vrf"; + push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); + push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + + @controller_config = (); + #redistribute connected to be able to route to local vms on the gateway + push @controller_config, "redistribute connected"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + } + @controller_config = (); - #import /32 routes of evpn network from vrf1 to default vrf (for packet return) - push @controller_config, "import vrf $vrf"; - push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); - push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + #add default originate to announce 0.0.0.0/0 type5 route in evpn + push @controller_config, "default-originate ipv4"; + push @controller_config, "default-originate ipv6"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + } elsif ($advertisesubnets) { @controller_config = (); - #redistribute connected to be able to route to local vms on the gateway + #redistribute connected networks push @controller_config, "redistribute connected"; push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); @controller_config = (); - #add default originate to announce 0.0.0.0/0 type5 route in evpn - push @controller_config, "default-originate ipv4"; - push @controller_config, "default-originate ipv6"; + #advertise connected networks type5 route in evpn + push @controller_config, "advertise ipv4 unicast"; + push @controller_config, "advertise ipv6 unicast"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + } + + if($rt_import) { + @controller_config = (); + foreach my $rt (sort @{$rt_import}) { + push @controller_config, "route-target import $rt"; + } push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); } return $config; } +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_; + + my $exitnodes = $zone->{'exitnodes'}; + my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'}; + + return if !$exitnodes_local_routing; + + my $local_node = PVE::INotify::nodename(); + my $is_gateway = $exitnodes->{$local_node}; + + return if !$is_gateway; + + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + my @controller_config = (); + foreach my $subnetid (sort keys %{$subnets}) { + my $subnet = $subnets->{$subnetid}; + my $cidr = $subnet->{cidr}; + push @controller_config, "ip route $cidr 10.255.255.2 xvrf_$zoneid"; + } + push(@{$config->{frr}->{''}}, @controller_config); +} + sub on_delete_hook { my ($class, $controllerid, $zone_cfg) = @_; @@ -165,13 +263,31 @@ sub on_update_hook { # we can only have 1 evpn controller / 1 asn by server + my $controllernb = 0; foreach my $id (keys %{$controller_cfg->{ids}}) { next if $id eq $controllerid; my $controller = $controller_cfg->{ids}->{$id}; - die "only 1 evpn controller can be defined" if $controller->{type} eq "evpn"; + next if $controller->{type} ne "evpn"; + $controllernb++; + die "only 1 global evpn controller can be defined" if $controllernb > 1; + } +} + +sub find_bgp_controller { + my ($nodename, $controller_cfg) = @_; + + my $controller = undef; + foreach my $id (keys %{$controller_cfg->{ids}}) { + $controller = $controller_cfg->{ids}->{$id}; + next if $controller->{type} ne 'bgp'; + next if $controller->{node} ne $nodename; + last; } + + return $controller; } + sub sort_frr_config { my $order = {}; $order->{''} = 0; @@ -241,22 +357,41 @@ sub generate_frr_recurse{ } } -sub write_controller_config { +sub generate_frr_routemap { + my ($final_config, $routemaps) = @_; + + foreach my $id (sort keys %$routemaps) { + + my $routemap = $routemaps->{$id}; + my $order = 0; + foreach my $seq (@$routemap) { + $order++; + next if !defined($seq->{action}); + my @config = (); + push @config, "!"; + push @config, "route-map $id $seq->{action} $order"; + my $rule = $seq->{rule}; + push @config, map { " $_" } @$rule; + push @{$final_config}, @config; + } + } +} +sub generate_controller_rawconfig { my ($class, $plugin_config, $config) = @_; my $nodename = PVE::INotify::nodename(); my $final_config = []; + push @{$final_config}, "frr version 8.2.2"; + push @{$final_config}, "frr defaults datacenter"; + push @{$final_config}, "hostname $nodename"; push @{$final_config}, "log syslog informational"; - push @{$final_config}, "ip forwarding"; - push @{$final_config}, "ipv6 forwarding"; - push @{$final_config}, "frr defaults traditional"; push @{$final_config}, "service integrated-vtysh-config"; - push @{$final_config}, "hostname $nodename"; push @{$final_config}, "!"; if (-e "/etc/frr/frr.conf.local") { generate_frr_recurse($final_config, $config->{frr}->{vrf}, "vrf", 1); + generate_frr_routemap($final_config, $config->{frr_routemap}); push @{$final_config}, "!"; my $local_conf = file_get_contents("/etc/frr/frr.conf.local"); @@ -264,6 +399,7 @@ sub write_controller_config { push @{$final_config}, $local_conf; } else { generate_frr_recurse($final_config, $config->{frr}, undef, 0); + generate_frr_routemap($final_config, $config->{frr_routemap}); } push @{$final_config}, "!"; @@ -272,6 +408,14 @@ sub write_controller_config { my $rawconfig = join("\n", @{$final_config}); + return if !$rawconfig; + return $rawconfig; +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config); return if !$rawconfig; return if !-d "/etc/frr"; @@ -297,7 +441,13 @@ sub reload_controller { }; if (-e $conf_file && -e $bin_path) { - run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err); + eval { + run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err); + }; + if ($@) { + warn "frr reload command fail. Restarting frr."; + eval { run_command(['systemctl', 'restart', 'frr']); }; + } } }