summaryrefslogtreecommitdiff
path: root/src/PVE/Network/SDN/Controllers
diff options
context:
space:
mode:
authorThomas Lamprecht <t.lamprecht@proxmox.com>2023-05-25 18:10:14 +0200
committerThomas Lamprecht <t.lamprecht@proxmox.com>2023-05-25 18:18:57 +0200
commit6029cbb071c3722c717eebbafaf1b373f3edaadc (patch)
tree456d7aff44d2ae220d1671f77da7528174d53fe6 /src/PVE/Network/SDN/Controllers
parentcead0f28af4aceee83af6636d4f5ffb2d2f6c6b1 (diff)
separate packaging and source build system
like almost all of our repos do nowadays, modern git can detect such things on rebase so in development stuff should be hopefully not too much affected by this. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Diffstat (limited to 'src/PVE/Network/SDN/Controllers')
-rw-r--r--src/PVE/Network/SDN/Controllers/BgpPlugin.pm183
-rw-r--r--src/PVE/Network/SDN/Controllers/EvpnPlugin.pm542
-rw-r--r--src/PVE/Network/SDN/Controllers/FaucetPlugin.pm97
-rw-r--r--src/PVE/Network/SDN/Controllers/Makefile8
-rw-r--r--src/PVE/Network/SDN/Controllers/Plugin.pm121
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;