diff options
Diffstat (limited to 'src/PVE/Network/SDN/Controllers')
| -rw-r--r-- | src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 183 | ||||
| -rw-r--r-- | src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 542 | ||||
| -rw-r--r-- | src/PVE/Network/SDN/Controllers/FaucetPlugin.pm | 97 | ||||
| -rw-r--r-- | src/PVE/Network/SDN/Controllers/Makefile | 8 | ||||
| -rw-r--r-- | src/PVE/Network/SDN/Controllers/Plugin.pm | 121 |
5 files changed, 951 insertions, 0 deletions
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm new file mode 100644 index 0000000..0b8cf1a --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm @@ -0,0 +1,183 @@ +package PVE::Network::SDN::Controllers::BgpPlugin; + +use strict; +use warnings; + +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'); + +sub type { + return 'bgp'; +} + +sub properties { + return { + 'bgp-multipath-as-path-relax' => { + type => 'boolean', + optional => 1, + }, + ebgp => { + type => 'boolean', + optional => 1, + description => "Enable ebgp. (remote-as external)", + }, + 'ebgp-multihop' => { + type => 'integer', + optional => 1, + }, + loopback => { + description => "source loopback interface.", + type => 'string' + }, + node => get_standard_option('pve-node'), + }; +} + +sub options { + return { + 'node' => { optional => 0 }, + 'asn' => { optional => 0 }, + 'peers' => { optional => 0 }, + 'bgp-multipath-as-path-relax' => { optional => 1 }, + 'ebgp' => { optional => 1 }, + 'ebgp-multihop' => { optional => 1 }, + 'loopback' => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_controller_config { + my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_; + + my @peers; + @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'}; + + my $asn = $plugin_config->{asn}; + my $ebgp = $plugin_config->{ebgp}; + my $ebgp_multihop = $plugin_config->{'ebgp-multihop'}; + my $loopback = $plugin_config->{loopback}; + my $multipath_relax = $plugin_config->{'bgp-multipath-as-path-relax'}; + + my $local_node = PVE::INotify::nodename(); + + + return if !$asn; + return if $local_node ne $plugin_config->{node}; + + my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {}; + + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + + 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 = (); + if($ebgp) { + push @controller_config, "bgp disable-ebgp-connected-route-check" if $loopback; + } + + push @controller_config, "bgp bestpath as-path multipath-relax" if $multipath_relax; + + #BGP neighbors + if(@peers) { + push @controller_config, "neighbor BGP peer-group"; + push @controller_config, "neighbor BGP remote-as $remoteas"; + push @controller_config, "neighbor BGP bfd"; + push @controller_config, "neighbor BGP ebgp-multihop $ebgp_multihop" if $ebgp && $ebgp_multihop; + } + + # BGP peers + foreach my $address (@peers) { + push @controller_config, "neighbor $address peer-group BGP"; + } + push(@{$bgp->{""}}, @controller_config); + + # address-family unicast + if (@peers) { + my $ipversion = Net::IP::ip_is_ipv6($ifaceip) ? "ipv6" : "ipv4"; + my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "/128" : "32"; + + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "network $ifaceip/$mask") if $loopback; + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "neighbor BGP activate"); + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "neighbor BGP soft-reconfiguration inbound"); + } + + if ($loopback) { + $config->{frr_prefix_list}->{loopbacks_ips}->{10} = "permit 0.0.0.0/0 le 32"; + push(@{$config->{frr}->{''}}, "ip protocol bgp route-map correct_src"); + + my $routemap_config = (); + push @{$routemap_config}, "match ip address prefix-list loopbacks_ips"; + push @{$routemap_config}, "set src $ifaceip"; + my $routemap = { rule => $routemap_config, action => "permit" }; + push(@{$config->{frr_routemap}->{'correct_src'}}, $routemap); + } + + return $config; +} + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $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); + } +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # we can only have 1 bgp controller by node + my $local_node = PVE::INotify::nodename(); + my $controllernb = 0; + foreach my $id (keys %{$controller_cfg->{ids}}) { + next if $id eq $controllerid; + my $controller = $controller_cfg->{ids}->{$id}; + next if $controller->{type} ne "bgp"; + next if $controller->{node} ne $local_node; + $controllernb++; + die "only 1 bgp controller can be defined" if $controllernb > 1; + } +} + +sub generate_controller_rawconfig { + my ($class, $plugin_config, $config) = @_; + return ""; +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + return; +} + +sub reload_controller { + my ($class) = @_; + return; +} + +1; + + diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm new file mode 100644 index 0000000..727aeaa --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -0,0 +1,542 @@ +package PVE::Network::SDN::Controllers::EvpnPlugin; + +use strict; +use warnings; + +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'); + +sub type { + return 'evpn'; +} + +sub properties { + return { + asn => { + type => 'integer', + description => "autonomous system number", + minimum => 0, + maximum => 4294967296 + }, + peers => { + description => "peers address list.", + type => 'string', format => 'ip-list' + }, + }; +} + +sub options { + return { + 'asn' => { optional => 0 }, + 'peers' => { optional => 0 }, + }; +} + +# Plugin implementation +sub generate_controller_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 $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, $loopback); + + 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 peer-group VTEP"; + } + + push(@{$bgp->{""}}, @controller_config); + + # address-family l2vpn + @controller_config = (); + 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); + + 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, $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; + $rt_import = [PVE::Tools::split_list($plugin_config->{'rt-import'})] if $plugin_config->{'rt-import'}; + + my $asn = $controller->{asn}; + my @peers; + @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); + + #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 $is_gateway = $exitnodes->{$local_node}; + + 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 = (); + #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 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 = (); + #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); + } +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # 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}; + 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; + $order->{'vrf'} = 1; + $order->{'ipv4 unicast'} = 1; + $order->{'ipv6 unicast'} = 2; + $order->{'l2vpn evpn'} = 3; + + my $a_val = 100; + my $b_val = 100; + + $a_val = $order->{$a} if defined($order->{$a}); + $b_val = $order->{$b} if defined($order->{$b}); + + if ($a =~ /bgp (\d+)$/) { + $a_val = 2; + } + + if ($b =~ /bgp (\d+)$/) { + $b_val = 2; + } + + return $a_val <=> $b_val; +} + +sub generate_frr_recurse{ + my ($final_config, $content, $parentkey, $level) = @_; + + my $keylist = {}; + $keylist->{vrf} = 1; + $keylist->{'address-family'} = 1; + $keylist->{router} = 1; + + my $exitkeylist = {}; + $exitkeylist->{vrf} = 1; + $exitkeylist->{'address-family'} = 1; + + my $simple_exitkeylist = {}; + $simple_exitkeylist->{router} = 1; + + # FIXME: make this generic + my $paddinglevel = undef; + if ($level == 1 || $level == 2) { + $paddinglevel = $level - 1; + } elsif ($level == 3 || $level == 4) { + $paddinglevel = $level - 2; + } + + my $padding = ""; + $padding = ' ' x ($paddinglevel) if $paddinglevel; + + 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"; + } elsif ($key ne '' && !defined($keylist->{$key})) { + push @{$final_config}, $padding."$key"; + } + + my $option = $content->{$key}; + generate_frr_recurse($final_config, $option, $key, $level+1); + + push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey}); + push @{$final_config}, $padding."exit" if $parentkey && defined($simple_exitkeylist->{$parentkey}); + } + } + + if (ref $content eq 'ARRAY') { + push @{$final_config}, map { $padding . "$_" } @$content; + } +} + +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; + push @{$final_config}, "exit"; + } + } +} + +sub generate_frr_list { + my ($final_config, $lists, $type) = @_; + + my $config = []; + + for my $id (sort keys %$lists) { + my $list = $lists->{$id}; + + for my $seq (sort keys %$list) { + my $rule = $list->{$seq}; + push @$config, "$type $id seq $seq $rule"; + } + } + + if (@$config > 0) { + 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}, "service integrated-vtysh-config"; + push @{$final_config}, "!"; + + if (-e "/etc/frr/frr.conf.local") { + my $local_conf = file_get_contents("/etc/frr/frr.conf.local"); + parse_merge_frr_local_config($config, $local_conf); + } + + generate_frr_recurse($final_config, $config->{frr}, undef, 0); + generate_frr_list($final_config, $config->{frr_access_list}, "access-list"); + generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list"); + generate_frr_routemap($final_config, $config->{frr_routemap}); + + push @{$final_config}, "!"; + push @{$final_config}, "line vty"; + push @{$final_config}, "!"; + + my $rawconfig = join("\n", @{$final_config}); + + return if !$rawconfig; + return $rawconfig; +} + +sub parse_merge_frr_local_config { + my ($config, $local_conf) = @_; + + my $section = \$config->{""}; + my $router = undef; + my $routemap = undef; + my $routemap_config = (); + my $routemap_action = undef; + + while ($local_conf =~ /^\s*(.+?)\s*$/gm) { + my $line = $1; + $line =~ s/^\s+|\s+$//g; + + if ($line =~ m/^router (.+)$/) { + $router = $1; + $section = \$config->{'frr'}->{'router'}->{$router}->{""}; + next; + } elsif ($line =~ m/^vrf (.+)$/) { + $section = \$config->{'frr'}->{'vrf'}->{$1}; + next; + } elsif ($line =~ m/address-family (.+)$/) { + $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1}; + next; + } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) { + $routemap = $1; + $routemap_config = (); + $routemap_action = $2; + $section = \$config->{'frr_routemap'}->{$routemap}; + next; + } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) { + $config->{'frr_access_list'}->{$1}->{$2} = $3; + next; + } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) { + $config->{'frr_prefix_list'}->{$1}->{$2} = $3; + next; + } elsif($line =~ m/^exit-address-family$/) { + next; + } elsif($line =~ m/^exit$/) { + if($router) { + $section = \$config->{''}; + $router = undef; + } elsif($routemap) { + push(@{$$section}, { rule => $routemap_config, action => $routemap_action }); + $section = \$config->{''}; + $routemap = undef; + $routemap_action = undef; + $routemap_config = (); + } + next; + } elsif($line =~ m/!/) { + next; + } + + next if !$section; + if($routemap) { + push(@{$routemap_config}, $line); + } else { + push(@{$$section}, $line); + } + } +} + +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"; + + file_set_contents("/etc/frr/frr.conf", $rawconfig); +} + +sub reload_controller { + my ($class) = @_; + + my $conf_file = "/etc/frr/frr.conf"; + 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 =~ /ERROR:/) { + warn "$line \n"; + } + }; + + if (-e $conf_file && -e $bin_path) { + 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']); }; + } + } +} + +1; + + diff --git a/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm b/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm new file mode 100644 index 0000000..4f3bb5c --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm @@ -0,0 +1,97 @@ +package PVE::Network::SDN::Controllers::FaucetPlugin; + +use strict; +use warnings; +use PVE::Network::SDN::Controllers::Plugin; +use PVE::Tools; +use PVE::INotify; +use PVE::JSONSchema qw(get_standard_option); +use CPAN::Meta::YAML; +use Encode; + +use base('PVE::Network::SDN::Controllers::Plugin'); + +sub type { + return 'faucet'; +} + +sub properties { + return { + }; +} + +# Plugin implementation +sub generate_controller_config { + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; + +} + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + my $dpid = $plugin_config->{'dp-id'}; + my $dphex = printf("%x",$dpid); + + my $zone_config = { + dp_id => $dphex, + hardware => "Open vSwitch", + }; + + $config->{faucet}->{dps}->{$id} = $zone_config; + +} + + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_; + + my $mac = $plugin_config->{mac}; + my $ipv4 = $plugin_config->{ipv4}; + my $ipv6 = $plugin_config->{ipv6}; + my $tag = $plugin_config->{tag}; + my $alias = $plugin_config->{alias}; + + my @ips = (); + push @ips, $ipv4 if $ipv4; + push @ips, $ipv6 if $ipv6; + + my $vlan_config = { vid => $tag }; + + $vlan_config->{description} = $alias if $alias; + $vlan_config->{faucet_mac} = $mac if $mac; + $vlan_config->{faucet_vips} = \@ips if scalar @ips > 0; + + $config->{faucet}->{vlans}->{$vnetid} = $vlan_config; + + push(@{$config->{faucet}->{routers}->{$zoneid}->{vlans}} , $vnetid); + +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + my $rawconfig = encode('UTF-8', CPAN::Meta::YAML::Dump($config->{faucet})); + + return if !$rawconfig; + return if !-d "/etc/faucet"; + + my $frr_config_file = "/etc/faucet/faucet.yaml"; + + my $writefh = IO::File->new($frr_config_file,">"); + print $writefh $rawconfig; + $writefh->close(); +} + +sub reload_controller { + my ($class) = @_; + + my $conf_file = "/etc/faucet/faucet.yaml"; + my $bin_path = "/usr/bin/faucet"; + + if (-e $conf_file && -e $bin_path) { + PVE::Tools::run_command(['systemctl', 'reload', 'faucet']); + } +} + +1; + diff --git a/src/PVE/Network/SDN/Controllers/Makefile b/src/PVE/Network/SDN/Controllers/Makefile new file mode 100644 index 0000000..11686a3 --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Controllers/$$i; done diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm new file mode 100644 index 0000000..c1c2cfd --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/Plugin.pm @@ -0,0 +1,121 @@ +package PVE::Network::SDN::Controllers::Plugin; + +use strict; +use warnings; + +use PVE::Tools; +use PVE::JSONSchema; +use PVE::Cluster; + +use Data::Dumper; +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/controllers.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); } +); + +PVE::JSONSchema::register_standard_option('pve-sdn-controller-id', { + description => "The SDN controller object identifier.", + type => 'string', format => 'pve-sdn-controller-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-controller-id', \&parse_sdn_controller_id); +sub parse_sdn_controller_id { + my ($id, $noerr) = @_; + + if ($id !~ m/^[a-z][a-z0-9_-]*[a-z0-9]$/i) { + return undef if $noerr; + die "controller ID '$id' contains illegal characters\n"; + } + die "controller ID '$id' can't be more length than 64 characters\n" if length($id) > 64; + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + type => 'string', + }, + controller => get_standard_option('pve-sdn-controller-id', + { completion => \&PVE::Network::SDN::complete_sdn_controller }), + }, +}; + +sub private { + return $defaultData; +} + +sub parse_section_header { + my ($class, $line) = @_; + + if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { + my ($type, $id) = (lc($1), $2); + my $errmsg = undef; # set if you want to skip whole section + eval { PVE::JSONSchema::pve_verify_configid($type); }; + $errmsg = $@ if $@; + my $config = {}; # to return additional attributes + return ($type, $id, $errmsg, $config); + } + return undef; +} + +sub generate_sdn_config { + my ($class, $plugin_config, $node, $data, $ctime) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_config { + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; + + die "please implement inside plugin"; +} + + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zoneid, $vnetid, $config) = @_; + +} + +sub generate_controller_rawconfig { + my ($class, $plugin_config, $config) = @_; + + die "please implement inside plugin"; +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + die "please implement inside plugin"; +} + +sub controller_reload { + my ($class) = @_; + + die "please implement inside plugin"; +} + +sub on_delete_hook { + my ($class, $controllerid, $zone_cfg) = @_; + + # do nothing by default +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # do nothing by default +} + +1; |
