X-Git-Url: https://git.proxmox.com/?p=pve-network.git;a=blobdiff_plain;f=PVE%2FNetwork%2FSDN%2FControllers%2FEvpnPlugin.pm;h=0c498939dc09431dd131e806f7643eedb09b045a;hp=5e0a275d6cf2843d9539a5594890ebe3930c602c;hb=9c24bcc5eb191d6f59541dc714d4c8df672f0925;hpb=56cdcac99d114487873624ad013c9f7d2f74db63 diff --git a/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/PVE/Network/SDN/Controllers/EvpnPlugin.pm index 5e0a275..0c49893 100644 --- a/PVE/Network/SDN/Controllers/EvpnPlugin.pm +++ b/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -2,10 +2,14 @@ package PVE::Network::SDN::Controllers::EvpnPlugin; use strict; use warnings; -use PVE::Network::SDN::Controllers::Plugin; -use PVE::Tools; + use PVE::INotify; use PVE::JSONSchema qw(get_standard_option); +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'); @@ -15,179 +19,264 @@ sub type { sub properties { return { - 'uplink-id' => { + asn => { type => 'integer', - minimum => 1, maximum => 4096, - description => 'Uplink interface', + description => "autonomous system number", + minimum => 0, + maximum => 4294967296 + }, + peers => { + description => "peers address list.", + type => 'string', format => 'ip-list' }, - 'asn' => { - type => 'integer', - description => "autonomous system number", - }, - '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' - }, }; } sub options { - return { - 'uplink-id' => { optional => 0 }, - 'asn' => { optional => 0 }, - 'peers' => { optional => 0 }, - 'gateway-nodes' => { optional => 1 }, - 'gateway-external-peers' => { optional => 1 }, + 'asn' => { optional => 0 }, + 'peers' => { optional => 0 }, }; } # 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 = split(',', $plugin_config->{'peers'}) if $plugin_config->{'peers'}; + 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 $uplink = $plugin_config->{'uplink-id'}; - my $gatewaynodes = $plugin_config->{'gateway-nodes'}; - my @gatewaypeers = split(',', $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 $iface = "uplink$uplink"; - my $ifaceip = ""; + my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {}; - if($uplinks->{$uplink}->{name}) { - $iface = $uplinks->{$uplink}->{name}; - $ifaceip = PVE::Network::SDN::Controllers::Plugin::get_first_local_ipv4_from_interface($iface); - } + 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(); + my $remoteas = $ebgp ? "external" : $asn; - foreach my $gatewaynode (PVE::Tools::split_list($gatewaynodes)) { - $is_gateway = 1 if $gatewaynode eq $local_node; - } + #global options + my @controller_config = ( + "bgp router-id $ifaceip", + "no bgp default ipv4-unicast", + "coalesce-time 1000", + ); - my @controller_config = (); + push(@{$bgp->{""}}, @controller_config) if keys %{$bgp} == 0; - push @controller_config, "bgp router-id $ifaceip"; - push @controller_config, "no bgp default ipv4-unicast"; - push @controller_config, "coalesce-time 1000"; + @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(@{$config->{frr}->{router}->{"bgp $asn"}->{""}}, @controller_config); + 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_OUT out"; + push @controller_config, "neighbor VTEP activate"; push @controller_config, "advertise-all-vni"; - push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); - - if ($is_gateway) { + push @controller_config, "autort as $autortas" if $autortas; + push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config); - @controller_config = (); - #import /32 routes of evpn network from vrf1 to default vrf (for packet return) - foreach my $address (@gatewaypeers) { - push @controller_config, "neighbor $address activate"; - } - push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); - push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); - - } + push(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, []); 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 $vrf = $id; + 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; - #vrf + 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); + #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); - 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; + 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 $is_gateway = $exitnodes->{$local_node}; + if ($is_gateway) { + if($exitnodes_primary && $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"; + unshift(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap_config); + } + + 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) = @_; # verify that zone is associated to this controller foreach my $id (keys %{$zone_cfg->{ids}}) { - my $zone = $zone_cfg->{ids}->{$id}; - die "controller $controllerid is used by $id" - if (defined($zone->{controller}) && $zone->{controller} eq $controllerid); + my $zone = $zone_cfg->{ids}->{$id}; + die "controller $controllerid is used by $id" + if (defined($zone->{controller}) && $zone->{controller} eq $controllerid); } } sub on_update_hook { my ($class, $controllerid, $controller_cfg) = @_; - # verify that asn is not already used by another controller - my $asn = $controller_cfg->{ids}->{$controllerid}->{asn}; + # 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 "asn $asn is already used by $id" - if (defined($controller->{asn}) && $controller->{asn} eq $asn); + my $controller = $controller_cfg->{ids}->{$id}; + 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; @@ -202,11 +291,11 @@ sub sort_frr_config { $a_val = $order->{$a} if defined($order->{$a}); $b_val = $order->{$b} if defined($order->{$b}); - if($a =~ /bgp (\d+)$/) { + if ($a =~ /bgp (\d+)$/) { $a_val = 2; } - if($b =~ /bgp (\d+)$/) { + if ($b =~ /bgp (\d+)$/) { $b_val = 2; } @@ -225,24 +314,24 @@ sub generate_frr_recurse{ $exitkeylist->{vrf} = 1; $exitkeylist->{'address-family'} = 1; - #fix me, make this generic + # FIXME: make this generic my $paddinglevel = undef; - if($level == 1 || $level == 2) { - $paddinglevel = $level - 1; + if ($level == 1 || $level == 2) { + $paddinglevel = $level - 1; } elsif ($level == 3 || $level == 4) { - $paddinglevel = $level - 2; + $paddinglevel = $level - 2; } my $padding = ""; $padding = ' ' x ($paddinglevel) if $paddinglevel; - if (ref $content eq ref {}) { + if (ref $content eq 'HASH') { foreach my $key (sort sort_frr_config keys %$content) { if ($parentkey && defined($keylist->{$parentkey})) { - push @{$final_config}, $padding."!"; - push @{$final_config}, $padding."$parentkey $key"; - } else { - push @{$final_config}, $padding."$key" if $key ne '' && !defined($keylist->{$key}); + push @{$final_config}, $padding."!"; + push @{$final_config}, $padding."$parentkey $key"; + } elsif ($key ne '' && !defined($keylist->{$key})) { + push @{$final_config}, $padding."$key"; } my $option = $content->{$key}; @@ -253,20 +342,52 @@ sub generate_frr_recurse{ } if (ref $content eq 'ARRAY') { - foreach my $value (@$content) { - push @{$final_config}, $padding."$value"; - } + push @{$final_config}, map { $padding . "$_" } @$content; } } -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++; + my @config = (); + push @config, "!"; + push @config, "route-map $id permit $order"; + push @config, map { " $_" } @$seq; + 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.0.1"; + push @{$final_config}, "frr defaults datacenter"; + push @{$final_config}, "hostname $nodename"; push @{$final_config}, "log syslog informational"; + push @{$final_config}, "service integrated-vtysh-config"; push @{$final_config}, "!"; - generate_frr_recurse($final_config, $config->{frr}, undef, 0); + 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"); + chomp ($local_conf); + 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}, "!"; push @{$final_config}, "line vty"; @@ -274,32 +395,46 @@ 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"; - my $frr_config_file = "/etc/frr/frr.conf"; - - my $writefh = IO::File->new($frr_config_file,">"); - print $writefh $rawconfig; - $writefh->close(); + file_set_contents("/etc/frr/frr.conf", $rawconfig); } sub reload_controller { my ($class) = @_; my $conf_file = "/etc/frr/frr.conf"; - my $bin_path = "/usr/bin/vtysh"; + my $bin_path = "/usr/lib/frr/frr-reload.py"; + + if (!-e $bin_path) { + warn "missing $bin_path. Please install frr-pythontools package"; + return; + } my $err = sub { my $line = shift; - if ($line =~ /^line (\S+)/) { - print "$line \n"; + if ($line =~ /ERROR:/) { + warn "$line \n"; } }; if (-e $conf_file && -e $bin_path) { - PVE::Tools::run_command([$bin_path, '-m', '-f', $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']); }; + } } }