use Data::Dumper;
use JSON;
+use PVE::Network::SDN::Zones;
+
use PVE::Tools qw(extract_param dir_glob_regex run_command);
use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
-use PVE::Network::SDN::Plugin;
-use PVE::Network::SDN::VnetPlugin;
-use PVE::Network::SDN::VlanPlugin;
-use PVE::Network::SDN::VxlanPlugin;
-use PVE::Network::SDN::FaucetPlugin;
-use PVE::Network::SDN::FaucetControllerPlugin;
-use PVE::Network::SDN::EvpnPlugin;
-use PVE::Network::SDN::EvpnControllerPlugin;
-use PVE::Network::SDN::QinQPlugin;
-
-PVE::Network::SDN::VnetPlugin->register();
-PVE::Network::SDN::VlanPlugin->register();
-PVE::Network::SDN::VxlanPlugin->register();
-PVE::Network::SDN::FaucetControllerPlugin->register();
-PVE::Network::SDN::FaucetPlugin->register();
-PVE::Network::SDN::EvpnPlugin->register();
-PVE::Network::SDN::EvpnControllerPlugin->register();
-PVE::Network::SDN::QinQPlugin->register();
-PVE::Network::SDN::Plugin->init();
-
-
-sub sdn_config {
- my ($cfg, $sdnid, $noerr) = @_;
-
- die "no sdn ID specified\n" if !$sdnid;
-
- my $scfg = $cfg->{ids}->{$sdnid};
- die "sdn '$sdnid' does not exists\n" if (!$noerr && !$scfg);
-
- return $scfg;
-}
-
-sub config {
- my $config = cfs_read_file("sdn.cfg.new");
- $config = cfs_read_file("sdn.cfg") if !keys %{$config->{ids}};
- return $config;
-}
-
-sub write_config {
- my ($cfg) = @_;
-
- cfs_write_file("sdn.cfg.new", $cfg);
-}
-
-sub lock_sdn_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file("sdn.cfg.new", undef, $code);
- if (my $err = $@) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
-}
-
-sub sdn_ids {
- my ($cfg) = @_;
-
- return keys %{$cfg->{ids}};
-}
-sub complete_sdn {
- my ($cmdname, $pname, $cvalue) = @_;
-
- my $cfg = PVE::Network::SDN::config();
-
- return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_ids($cfg) ];
-}
+# improve me : move status code inside plugins ?
sub ifquery_check {
return $interfaces;
}
-
-sub generate_etc_network_config {
-
- my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
- return if !$sdn_cfg;
-
- #read main config for physical interfaces
- my $current_config_file = "/etc/network/interfaces";
- my $fh = IO::File->new($current_config_file);
- my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
- $fh->close();
-
- #check uplinks
- my $uplinks = {};
- foreach my $id (keys %{$interfaces_config->{ifaces}}) {
- my $interface = $interfaces_config->{ifaces}->{$id};
- if (my $uplink = $interface->{'uplink-id'}) {
- die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
- $interface->{name} = $id;
- $uplinks->{$interface->{'uplink-id'}} = $interface;
- }
- }
-
- my $vnet_cfg = undef;
- my $transport_cfg = undef;
-
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- if ($sdn_cfg->{ids}->{$id}->{type} eq 'vnet') {
- $vnet_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
- } else {
- $transport_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
- }
- }
-
- #generate configuration
- my $config = {};
- foreach my $id (keys %{$vnet_cfg->{ids}}) {
- my $vnet = $vnet_cfg->{ids}->{$id};
- my $zone = $vnet->{transportzone};
-
- if(!$zone) {
- warn "can't generate vnet $vnet : zone $zone don't exist";
- next;
- }
-
- my $plugin_config = $transport_cfg->{ids}->{$zone};
-
- if (!defined($plugin_config)) {
- warn "can't generate vnet $vnet : zone $zone don't exist";
- next;
- }
-
- my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
- $plugin->generate_sdn_config($plugin_config, $zone, $id, $vnet, $uplinks, $config);
- }
-
- my $raw_network_config = "";
- foreach my $iface (keys %$config) {
- $raw_network_config .= "\n";
- $raw_network_config .= "auto $iface\n";
- $raw_network_config .= "iface $iface\n";
- foreach my $option (@{$config->{$iface}}) {
- $raw_network_config .= "\t$option\n";
- }
- }
-
- return $raw_network_config;
-}
-
-sub generate_controller_config {
-
- my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
- return if !$sdn_cfg;
-
- #read main config for physical interfaces
- my $current_config_file = "/etc/network/interfaces";
- my $fh = IO::File->new($current_config_file);
- my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
- $fh->close();
-
- #check uplinks
- my $uplinks = {};
- foreach my $id (keys %{$interfaces_config->{ifaces}}) {
- my $interface = $interfaces_config->{ifaces}->{$id};
- if (my $uplink = $interface->{'uplink-id'}) {
- die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
- $interface->{name} = $id;
- $uplinks->{$interface->{'uplink-id'}} = $interface;
- }
- }
-
- #generate configuration
- my $config = {};
-
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $plugin_config = $sdn_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
- my $pd = $plugin->plugindata();
- my $role = $pd->{role};
- if ($role eq 'controller') {
- $plugin->generate_controller_config($plugin_config, $plugin_config, $id, $uplinks, $config);
- } elsif ($role eq 'transport') {
- my $controllerid = $plugin_config->{controller};
- if ($controllerid) {
- my $controller = $sdn_cfg->{ids}->{$controllerid};
- if ($controller) {
- my $controller_plugin = PVE::Network::SDN::Plugin->lookup($controller->{type});
- $controller_plugin->generate_controller_transport_config($plugin_config, $controller, $id, $uplinks, $config);
- }
- }
- } elsif ($role eq 'vnet') {
- my $transportid = $plugin_config->{transportzone};
- if ($transportid) {
- my $transport = $sdn_cfg->{ids}->{$transportid};
- if ($transport) {
- my $controllerid = $transport->{controller};
- if ($controllerid) {
- my $controller = $sdn_cfg->{ids}->{$controllerid};
- if ($controller) {
- my $controller_plugin = PVE::Network::SDN::Plugin->lookup($controller->{type});
- $controller_plugin->generate_controller_vnet_config($plugin_config, $controller, $transportid, $id, $config);
- }
- }
- }
- }
- }
- }
-
- return $config;
-}
-
-
-sub reload_controller {
-
- my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
- return if !$sdn_cfg;
-
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $plugin_config = $sdn_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
- my $pd = $plugin->plugindata();
- my $role = $pd->{role};
- if ($role eq 'controller') {
- $plugin->reload_controller();
- }
- }
-}
-
-sub write_etc_network_config {
- my ($rawconfig) = @_;
-
- return if !$rawconfig;
- my $sdn_interfaces_file = "/etc/network/interfaces.d/sdn";
-
- my $writefh = IO::File->new($sdn_interfaces_file,">");
- print $writefh $rawconfig;
- $writefh->close();
-}
-
-sub write_controller_config {
- my ($config) = @_;
-
- my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
- return if !$sdn_cfg;
-
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $plugin_config = $sdn_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
- my $pd = $plugin->plugindata();
- my $role = $pd->{role};
- if ($role eq 'controller') {
- $plugin->write_controller_config($plugin_config, $config);
- }
- }
-}
-
sub status {
- my $cluster_sdn_file = "/etc/pve/sdn.cfg";
- my $local_sdn_file = "/etc/network/interfaces.d/sdn";
- my $err_config = undef;
-
- return if !-e $cluster_sdn_file;
-
- if (!-e $local_sdn_file) {
- warn "local sdn network configuration is not yet generated, please reload";
- $err_config = 'pending';
- } else {
- # fixme : use some kind of versioning info?
- my $cluster_sdn_timestamp = (stat($cluster_sdn_file))[9];
- my $local_sdn_timestamp = (stat($local_sdn_file))[9];
-
- if ($local_sdn_timestamp < $cluster_sdn_timestamp) {
- warn "local sdn network configuration is too old, please reload";
- $err_config = 'unknown';
- }
- }
-
- my $status = ifquery_check();
-
- my $network_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
- my $vnet_cfg = undef;
- my $transport_cfg = undef;
-
- my $vnet_status = {};
- my $transport_status = {};
-
- foreach my $id (keys %{$network_cfg->{ids}}) {
- if ($network_cfg->{ids}->{$id}->{type} eq 'vnet') {
- my $transportzone = $network_cfg->{ids}->{$id}->{transportzone};
- $vnet_status->{$id}->{transportzone} = $transportzone;
- $transport_status->{$transportzone}->{status} = 'available' if !defined($transport_status->{$transportzone}->{status});
-
- if($err_config) {
- $vnet_status->{$id}->{status} = $err_config;
- $transport_status->{$transportzone}->{status} = $err_config;
- } elsif ($status->{$id}->{status} && $status->{$id}->{status} eq 'pass') {
- $vnet_status->{$id}->{status} = 'available';
- my $bridgeport = $status->{$id}->{config}->{'bridge-ports'};
-
- if ($status->{$bridgeport}->{status} && $status->{$bridgeport}->{status} ne 'pass') {
- $vnet_status->{$id}->{status} = 'error';
- $transport_status->{$transportzone}->{status} = 'error';
- }
- } else {
- $vnet_status->{$id}->{status} = 'error';
- $transport_status->{$transportzone}->{status} = 'error';
- }
- }
- }
+ my ($transport_status, $vnet_status) = PVE::Network::SDN::Zones::status();
return($transport_status, $vnet_status);
}
--- /dev/null
+package PVE::Network::SDN::Controllers;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use JSON;
+
+use PVE::Tools qw(extract_param dir_glob_regex run_command);
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+
+use PVE::Network::SDN::Vnets;
+use PVE::Network::SDN::Zones;
+
+use PVE::Network::SDN::Controllers::FrrEvpnPlugin;
+use PVE::Network::SDN::Controllers::FaucetPlugin;
+use PVE::Network::SDN::Controllers::Plugin;
+PVE::Network::SDN::Controllers::FrrEvpnPlugin->register();
+PVE::Network::SDN::Controllers::FaucetPlugin->register();
+PVE::Network::SDN::Controllers::Plugin->init();
+
+
+sub sdn_controllers_config {
+ my ($cfg, $id, $noerr) = @_;
+
+ die "no sdn controller ID specified\n" if !$id;
+
+ my $scfg = $cfg->{ids}->{$id};
+ die "sdn '$id' does not exists\n" if (!$noerr && !$scfg);
+
+ return $scfg;
+}
+
+sub config {
+ my $config = cfs_read_file("sdn/controllers.cfg.new");
+ $config = cfs_read_file("sdn/controllers.cfg") if !keys %{$config->{ids}};
+ return $config;
+}
+
+sub write_config {
+ my ($cfg) = @_;
+
+ cfs_write_file("sdn/controllers.cfg.new", $cfg);
+}
+
+sub lock_sdn_controllers_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file("sdn/controllers.cfg.new", undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub sdn_controllers_ids {
+ my ($cfg) = @_;
+
+ return keys %{$cfg->{ids}};
+}
+
+sub complete_sdn_controller {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = PVE::Network::SDN::config();
+
+ return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_controllers_ids($cfg) ];
+}
+
+sub generate_controller_config {
+
+ my $vnet_cfg = PVE::Cluster::cfs_read_file('sdn/vnets.cfg');
+ my $transport_cfg = PVE::Cluster::cfs_read_file('sdn/zones.cfg');
+ my $controller_cfg = PVE::Cluster::cfs_read_file('sdn/controllers.cfg');
+ return if !$vnet_cfg && !$transport_cfg && !$controller_cfg;
+
+ #read main config for physical interfaces
+ my $current_config_file = "/etc/network/interfaces";
+ my $fh = IO::File->new($current_config_file);
+ my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
+ $fh->close();
+
+ #check uplinks
+ my $uplinks = {};
+ foreach my $id (keys %{$interfaces_config->{ifaces}}) {
+ my $interface = $interfaces_config->{ifaces}->{$id};
+ if (my $uplink = $interface->{'uplink-id'}) {
+ die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
+ $interface->{name} = $id;
+ $uplinks->{$interface->{'uplink-id'}} = $interface;
+ }
+ }
+
+ #generate configuration
+ my $config = {};
+
+ foreach my $id (keys %{$controller_cfg->{ids}}) {
+ my $plugin_config = $controller_cfg->{ids}->{$id};
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
+ $plugin->generate_controller_config($plugin_config, $plugin_config, $id, $uplinks, $config);
+ }
+
+ foreach my $id (keys %{$transport_cfg->{ids}}) {
+ my $plugin_config = $transport_cfg->{ids}->{$id};
+ my $controllerid = $plugin_config->{controller};
+ next if !$controllerid;
+ my $controller = $transport_cfg->{ids}->{$controllerid};
+ if ($controller) {
+ my $controller_plugin = PVE::Network::SDN::Controllers::Plugin->lookup($controller->{type});
+ $controller_plugin->generate_controller_transport_config($plugin_config, $controller, $id, $uplinks, $config);
+ }
+ }
+
+ foreach my $id (keys %{$vnet_cfg->{ids}}) {
+ my $plugin_config = $vnet_cfg->{ids}->{$id};
+ my $transportid = $plugin_config->{transportzone};
+ next if !$transportid;
+ my $transport = $transport_cfg->{ids}->{$transportid};
+ next if !$transport;
+ my $controllerid = $transport->{controller};
+ next if !$controllerid;
+ my $controller = $controller_cfg->{ids}->{$controllerid};
+ if ($controller) {
+ my $controller_plugin = PVE::Network::SDN::Controllers::Plugin->lookup($controller->{type});
+ $controller_plugin->generate_controller_vnet_config($plugin_config, $controller, $transportid, $id, $config);
+ }
+ }
+
+ return $config;
+}
+
+
+sub reload_controller {
+
+ my $controller_cfg = PVE::Cluster::cfs_read_file('sdn/controllers.cfg');
+ return if !$controller_cfg;
+
+ foreach my $id (keys %{$controller_cfg->{ids}}) {
+ my $plugin_config = $controller_cfg->{ids}->{$id};
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
+ $plugin->reload_controller();
+ }
+}
+
+sub write_controller_config {
+ my ($config) = @_;
+
+ my $controller_cfg = PVE::Cluster::cfs_read_file('sdn/controllers.cfg');
+ return if !$controller_cfg;
+
+ foreach my $id (keys %{$controller_cfg->{ids}}) {
+ my $plugin_config = $controller_cfg->{ids}->{$id};
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
+ $plugin->write_controller_config($plugin_config, $config);
+ }
+}
+
+1;
+
--- /dev/null
+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, $router, $id, $uplinks, $config) = @_;
+
+}
+
+sub generate_controller_transport_config {
+ my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
+
+ my $dpid = $plugin_config->{'dp-id'};
+ my $dphex = printf("%x",$dpid);
+
+ my $transport_config = {
+ dp_id => $dphex,
+ hardware => "Open vSwitch",
+ };
+
+ $config->{faucet}->{dps}->{$id} = $transport_config;
+
+}
+
+
+sub generate_controller_vnet_config {
+ my ($class, $plugin_config, $controller, $transportid, $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}->{$transportid}->{vlans}} , $vnetid);
+
+}
+
+sub on_delete_hook {
+ my ($class, $routerid, $sdn_cfg) = @_;
+
+}
+
+sub on_update_hook {
+ my ($class, $routerid, $sdn_cfg) = @_;
+
+}
+
+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;
+
--- /dev/null
+package PVE::Network::SDN::Controllers::FrrEvpnPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Controllers::Plugin;
+use PVE::Tools;
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base('PVE::Network::SDN::Controllers::Plugin');
+
+sub type {
+ return 'frrevpn';
+}
+
+sub properties {
+ return {
+ 'uplink-id' => {
+ type => 'integer',
+ minimum => 1, maximum => 4096,
+ description => 'Uplink interface',
+ },
+ '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 },
+ };
+}
+
+# Plugin implementation
+sub generate_controller_config {
+ my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
+
+ my @peers = split(',', $plugin_config->{'peers'}) if $plugin_config->{'peers'};
+
+ 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'};
+
+ return if !$asn;
+
+ my $iface = "uplink$uplink";
+ my $ifaceip = "";
+
+ if($uplinks->{$uplink}->{name}) {
+ $iface = $uplinks->{$uplink}->{name};
+ $ifaceip = PVE::Network::SDN::Controllers::Plugin::get_first_local_ipv4_from_interface($iface);
+ }
+
+ 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 @router_config = ();
+
+ push @router_config, "bgp router-id $ifaceip";
+ push @router_config, "no bgp default ipv4-unicast";
+ push @router_config, "coalesce-time 1000";
+
+ foreach my $address (@peers) {
+ next if $address eq $ifaceip;
+ push @router_config, "neighbor $address remote-as $asn";
+ }
+
+ if ($is_gateway) {
+ foreach my $address (@gatewaypeers) {
+ push @router_config, "neighbor $address remote-as external";
+ }
+ }
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{""}}, @router_config);
+
+ @router_config = ();
+ foreach my $address (@peers) {
+ next if $address eq $ifaceip;
+ push @router_config, "neighbor $address activate";
+ }
+ push @router_config, "advertise-all-vni";
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"l2vpn evpn"}}, @router_config);
+
+ if ($is_gateway) {
+
+ @router_config = ();
+ #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
+ #frr 7.1 tag is bugged -> works fine with 7.1 stable branch(20190829-02-g6ba76bbc1)
+ #https://github.com/FRRouting/frr/issues/4905
+ foreach my $address (@gatewaypeers) {
+ push @router_config, "neighbor $address activate";
+ }
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
+
+ }
+
+ return $config;
+}
+
+sub generate_controller_transport_config {
+ my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
+
+ my $vrf = $plugin_config->{'vrf'};
+ my $vrfvxlan = $plugin_config->{'vrf-vxlan'};
+ my $asn = $router->{asn};
+ my $gatewaynodes = $router->{'gateway-nodes'};
+
+ return if !$vrf || !$vrfvxlan || !$asn;
+
+ #vrf
+ my @router_config = ();
+ push @router_config, "vni $vrfvxlan";
+ push(@{$config->{frr}->{vrf}->{"$vrf"}}, @router_config);
+
+ @router_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 ($is_gateway) {
+
+ @router_config = ();
+ #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
+ #frr 7.1 tag is bugged -> works fine with 7.1 stable branch(20190829-02-g6ba76bbc1)
+ #https://github.com/FRRouting/frr/issues/4905
+ push @router_config, "import vrf $vrf";
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
+ push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
+
+ @router_config = ();
+ #redistribute connected to be able to route to local vms on the gateway
+ push @router_config, "redistribute connected";
+ push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
+ push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
+
+ @router_config = ();
+ #add default originate to announce 0.0.0.0/0 type5 route in evpn
+ push @router_config, "default-originate ipv4";
+ push @router_config, "default-originate ipv6";
+ push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @router_config);
+ }
+
+ return $config;
+}
+
+sub on_delete_hook {
+ my ($class, $routerid, $sdn_cfg) = @_;
+
+ # verify that transport is associated to this router
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ die "router $routerid is used by $id"
+ if (defined($sdn->{router}) && $sdn->{router} eq $routerid);
+ }
+}
+
+sub on_update_hook {
+ my ($class, $routerid, $sdn_cfg) = @_;
+
+ # verify that asn is not already used by another router
+ my $asn = $sdn_cfg->{ids}->{$routerid}->{asn};
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ next if $id eq $routerid;
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ die "asn $asn is already used by $id"
+ if (defined($sdn->{asn}) && $sdn->{asn} eq $asn);
+ }
+}
+
+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;
+
+ #fix me, 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 ref {}) {
+ 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});
+ }
+
+ my $option = $content->{$key};
+ generate_frr_recurse($final_config, $option, $key, $level+1);
+
+ push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
+ }
+ }
+
+ if (ref $content eq 'ARRAY') {
+ foreach my $value (@$content) {
+ push @{$final_config}, $padding."$value";
+ }
+ }
+}
+
+sub write_controller_config {
+ my ($class, $plugin_config, $config) = @_;
+
+ my $final_config = [];
+ push @{$final_config}, "log syslog informational";
+ push @{$final_config}, "!";
+
+ generate_frr_recurse($final_config, $config->{frr}, undef, 0);
+
+ push @{$final_config}, "!";
+ push @{$final_config}, "line vty";
+ push @{$final_config}, "!";
+
+ my $rawconfig = join("\n", @{$final_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();
+}
+
+sub reload_controller {
+ my ($class) = @_;
+
+ my $conf_file = "/etc/frr/frr.conf";
+ my $bin_path = "/usr/bin/vtysh";
+
+ my $err = sub {
+ my $line = shift;
+ if ($line =~ /^line (\S+)/) {
+ print "$line \n";
+ }
+ };
+
+ if (-e $conf_file && -e $bin_path) {
+ PVE::Tools::run_command([$bin_path, '-m', '-f', $conf_file], outfunc => {}, errfunc => $err);
+ }
+}
+
+1;
+
+
--- /dev/null
+SOURCES=Plugin.pm FaucetPlugin.pm FrrEvpnPlugin.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
--- /dev/null
+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(@_); });
+
+PVE::Cluster::cfs_register_file('sdn/controllers.cfg.new',
+ 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 "SDN controller object ID '$id' contains illegal characters\n";
+ }
+ 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, $router, $id, $uplinks, $config) = @_;
+
+ die "please implement inside plugin";
+}
+
+sub generate_controller_vnet_config {
+ my ($class, $plugin_config, $controller, $transportid, $vnetid, $config) = @_;
+
+}
+
+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, $sndid, $scfg) = @_;
+
+ # do nothing by default
+}
+
+sub on_update_hook {
+ my ($class, $sdnid, $scfg) = @_;
+
+ # do nothing by default
+}
+
+#helpers
+
+#to be move to Network.pm helper
+sub get_first_local_ipv4_from_interface {
+ my ($interface) = @_;
+
+ my $cmd = ['/sbin/ip', 'address', 'show', 'dev', $interface];
+
+ my $IP = "";
+
+ my $code = sub {
+ my $line = shift;
+
+ if ($line =~ m!^\s*inet\s+($PVE::Tools::IPRE)(?:/\d+|\s+peer\s+)!) {
+ $IP = $1;
+ return;
+ }
+ };
+
+ PVE::Tools::run_command($cmd, outfunc => $code);
+
+ return $IP;
+}
+
+1;
+++ /dev/null
-package PVE::Network::SDN::EvpnControllerPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::Plugin;
-use PVE::Tools;
-use PVE::INotify;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base('PVE::Network::SDN::Plugin');
-
-sub type {
- return 'evpncontroller';
-}
-
-sub plugindata {
- return {
- role => 'controller',
- };
-}
-
-sub properties {
- return {
- '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 },
- };
-}
-
-# Plugin implementation
-sub generate_controller_config {
- my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
-
- my @peers = split(',', $plugin_config->{'peers'}) if $plugin_config->{'peers'};
-
- 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'};
-
- return if !$asn;
-
- my $iface = "uplink$uplink";
- my $ifaceip = "";
-
- if($uplinks->{$uplink}->{name}) {
- $iface = $uplinks->{$uplink}->{name};
- $ifaceip = PVE::Network::SDN::Plugin::get_first_local_ipv4_from_interface($iface);
- }
-
- 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 @router_config = ();
-
- push @router_config, "bgp router-id $ifaceip";
- push @router_config, "no bgp default ipv4-unicast";
- push @router_config, "coalesce-time 1000";
-
- foreach my $address (@peers) {
- next if $address eq $ifaceip;
- push @router_config, "neighbor $address remote-as $asn";
- }
-
- if ($is_gateway) {
- foreach my $address (@gatewaypeers) {
- push @router_config, "neighbor $address remote-as external";
- }
- }
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{""}}, @router_config);
-
- @router_config = ();
- foreach my $address (@peers) {
- next if $address eq $ifaceip;
- push @router_config, "neighbor $address activate";
- }
- push @router_config, "advertise-all-vni";
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"l2vpn evpn"}}, @router_config);
-
- if ($is_gateway) {
-
- @router_config = ();
- #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
- #frr 7.1 tag is bugged -> works fine with 7.1 stable branch(20190829-02-g6ba76bbc1)
- #https://github.com/FRRouting/frr/issues/4905
- foreach my $address (@gatewaypeers) {
- push @router_config, "neighbor $address activate";
- }
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
-
- }
-
- return $config;
-}
-
-sub generate_controller_transport_config {
- my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
-
- my $vrf = $plugin_config->{'vrf'};
- my $vrfvxlan = $plugin_config->{'vrf-vxlan'};
- my $asn = $router->{asn};
- my $gatewaynodes = $router->{'gateway-nodes'};
-
- return if !$vrf || !$vrfvxlan || !$asn;
-
- #vrf
- my @router_config = ();
- push @router_config, "vni $vrfvxlan";
- push(@{$config->{frr}->{vrf}->{"$vrf"}}, @router_config);
-
- @router_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 ($is_gateway) {
-
- @router_config = ();
- #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
- #frr 7.1 tag is bugged -> works fine with 7.1 stable branch(20190829-02-g6ba76bbc1)
- #https://github.com/FRRouting/frr/issues/4905
- push @router_config, "import vrf $vrf";
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
- push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
-
- @router_config = ();
- #redistribute connected to be able to route to local vms on the gateway
- push @router_config, "redistribute connected";
- push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @router_config);
- push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @router_config);
-
- @router_config = ();
- #add default originate to announce 0.0.0.0/0 type5 route in evpn
- push @router_config, "default-originate ipv4";
- push @router_config, "default-originate ipv6";
- push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @router_config);
- }
-
- return $config;
-}
-
-sub on_delete_hook {
- my ($class, $routerid, $sdn_cfg) = @_;
-
- # verify that transport is associated to this router
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- die "router $routerid is used by $id"
- if (defined($sdn->{router}) && $sdn->{router} eq $routerid);
- }
-}
-
-sub on_update_hook {
- my ($class, $routerid, $sdn_cfg) = @_;
-
- # verify that asn is not already used by another router
- my $asn = $sdn_cfg->{ids}->{$routerid}->{asn};
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- next if $id eq $routerid;
- my $sdn = $sdn_cfg->{ids}->{$id};
- die "asn $asn is already used by $id"
- if (defined($sdn->{asn}) && $sdn->{asn} eq $asn);
- }
-}
-
-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;
-
- #fix me, 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 ref {}) {
- 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});
- }
-
- my $option = $content->{$key};
- generate_frr_recurse($final_config, $option, $key, $level+1);
-
- push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
- }
- }
-
- if (ref $content eq 'ARRAY') {
- foreach my $value (@$content) {
- push @{$final_config}, $padding."$value";
- }
- }
-}
-
-sub write_controller_config {
- my ($class, $plugin_config, $config) = @_;
-
- my $final_config = [];
- push @{$final_config}, "log syslog informational";
- push @{$final_config}, "!";
-
- generate_frr_recurse($final_config, $config->{frr}, undef, 0);
-
- push @{$final_config}, "!";
- push @{$final_config}, "line vty";
- push @{$final_config}, "!";
-
- my $rawconfig = join("\n", @{$final_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();
-}
-
-sub reload_controller {
- my ($class) = @_;
-
- my $conf_file = "/etc/frr/frr.conf";
- my $bin_path = "/usr/bin/vtysh";
-
- my $err = sub {
- my $line = shift;
- if ($line =~ /^line (\S+)/) {
- print "$line \n";
- }
- };
-
- if (-e $conf_file && -e $bin_path) {
- PVE::Tools::run_command([$bin_path, '-m', '-f', $conf_file], outfunc => {}, errfunc => $err);
- }
-}
-
-1;
-
-
+++ /dev/null
-package PVE::Network::SDN::EvpnPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::Plugin;
-use PVE::Tools qw($IPV4RE);
-use PVE::INotify;
-
-use base('PVE::Network::SDN::VxlanPlugin');
-
-sub type {
- return 'evpn';
-}
-
-sub plugindata {
- return {
- role => 'transport',
- };
-}
-
-sub properties {
- return {
- 'vrf' => {
- description => "vrf name.",
- type => 'string', #fixme: format
- },
- 'vrf-vxlan' => {
- type => 'integer',
- description => "l3vni.",
- },
- 'controller' => {
- type => 'string',
- description => "Frr router name",
- },
- };
-}
-
-sub options {
-
- return {
- 'uplink-id' => { optional => 0 },
- 'vxlan-allowed' => { optional => 1 },
- 'vrf' => { optional => 0 },
- 'vrf-vxlan' => { optional => 0 },
- 'controller' => { optional => 0 },
- };
-}
-
-# Plugin implementation
-sub generate_sdn_config {
- my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
-
- my $tag = $vnet->{tag};
- my $alias = $vnet->{alias};
- my $ipv4 = $vnet->{ipv4};
- my $ipv6 = $vnet->{ipv6};
- my $mac = $vnet->{mac};
-
- my $uplink = $plugin_config->{'uplink-id'};
- my $vxlanallowed = $plugin_config->{'vxlan-allowed'};
- my $vrf = $plugin_config->{'vrf'};
- my $vrfvxlan = $plugin_config->{'vrf-vxlan'};
-
- die "missing vxlan tag" if !$tag;
- my $iface = "uplink$uplink";
- my $ifaceip = "";
-
- if($uplinks->{$uplink}->{name}) {
- $iface = $uplinks->{$uplink}->{name};
- $ifaceip = PVE::Network::SDN::Plugin::get_first_local_ipv4_from_interface($iface);
- }
-
- my $mtu = 1450;
- $mtu = $uplinks->{$uplink}->{mtu} - 50 if $uplinks->{$uplink}->{mtu};
- $mtu = $vnet->{mtu} if $vnet->{mtu};
-
- #vxlan interface
- my @iface_config = ();
- push @iface_config, "vxlan-id $tag";
-
- push @iface_config, "vxlan-local-tunnelip $ifaceip" if $ifaceip;
- push @iface_config, "bridge-learning off";
- push @iface_config, "bridge-arp-nd-suppress on";
-
- push @iface_config, "mtu $mtu" if $mtu;
- push(@{$config->{"vxlan$vnetid"}}, @iface_config) if !$config->{"vxlan$vnetid"};
-
- #vnet bridge
- @iface_config = ();
- push @iface_config, "address $ipv4" if $ipv4;
- push @iface_config, "address $ipv6" if $ipv6;
- push @iface_config, "hwaddress $mac" if $mac;
- push @iface_config, "bridge_ports vxlan$vnetid";
- push @iface_config, "bridge_stp off";
- push @iface_config, "bridge_fd 0";
- push @iface_config, "mtu $mtu" if $mtu;
- push @iface_config, "alias $alias" if $alias;
- push @iface_config, "ip-forward on" if $ipv4;
- push @iface_config, "ip6-forward on" if $ipv6;
- push @iface_config, "arp-accept on" if $ipv4||$ipv6;
- push @iface_config, "vrf $vrf" if $vrf;
- push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
-
- if ($vrf) {
- #vrf interface
- @iface_config = ();
- push @iface_config, "vrf-table auto";
- push(@{$config->{$vrf}}, @iface_config) if !$config->{$vrf};
-
- if ($vrfvxlan) {
- #l3vni vxlan interface
- my $iface_vxlan = "vxlan$vrf";
- @iface_config = ();
- push @iface_config, "vxlan-id $vrfvxlan";
- push @iface_config, "vxlan-local-tunnelip $ifaceip" if $ifaceip;
- push @iface_config, "bridge-learning off";
- push @iface_config, "bridge-arp-nd-suppress on";
- push @iface_config, "mtu $mtu" if $mtu;
- push(@{$config->{$iface_vxlan}}, @iface_config) if !$config->{$iface_vxlan};
-
- #l3vni bridge
- my $brvrf = "br$vrf";
- @iface_config = ();
- push @iface_config, "bridge-ports $iface_vxlan";
- push @iface_config, "bridge_stp off";
- push @iface_config, "bridge_fd 0";
- push @iface_config, "mtu $mtu" if $mtu;
- push @iface_config, "vrf $vrf";
- push(@{$config->{$brvrf}}, @iface_config) if !$config->{$brvrf};
- }
- }
-
- return $config;
-}
-
-sub on_update_hook {
- my ($class, $transportid, $sdn_cfg) = @_;
-
- my $transport = $sdn_cfg->{ids}->{$transportid};
-
- # verify that vxlan-allowed don't conflict with another vxlan-allowed transport
-
- # verify that vxlan-allowed is matching currently vnet tag in this transport
- my $vxlanallowed = $transport->{'vxlan-allowed'};
- if ($vxlanallowed) {
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
- if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
- my $tag = $sdn->{tag};
- eval {
- PVE::Network::SDN::Plugin::parse_tag_number_or_range($vxlanallowed, '16777216', $tag);
- };
- if($@) {
- die "vnet $id - vlan $tag is not allowed in transport $transportid";
- }
- }
- }
- }
- }
-
- # verify that router exist
- if (defined($sdn_cfg->{ids}->{$transportid}->{router})) {
- my $router = $sdn_cfg->{ids}->{$transportid}->{router};
- if (!defined($sdn_cfg->{ids}->{$router})) {
- die "router $router don't exist";
- } else {
- die "$router is not a router type" if $sdn_cfg->{ids}->{$router}->{type} ne 'frr';
- }
-
- #vrf && vrf-vxlan need to be defined with router
- my $vrf = $sdn_cfg->{ids}->{$transportid}->{vrf};
- if (!defined($vrf)) {
- die "missing vrf option";
- } else {
- # verify that vrf is not already declared in another transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- next if $id eq $transportid;
- die "vrf $vrf is already declared in $id"
- if (defined($sdn_cfg->{ids}->{$id}->{vrf}) && $sdn_cfg->{ids}->{$id}->{vrf} eq $vrf);
- }
- }
-
- my $vrfvxlan = $sdn_cfg->{ids}->{$transportid}->{'vrf-vxlan'};
- if (!defined($vrfvxlan)) {
- die "missing vrf-vxlan option";
- } else {
- # verify that vrf-vxlan is not already declared in another transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- next if $id eq $transportid;
- die "vrf-vxlan $vrfvxlan is already declared in $id"
- if (defined($sdn_cfg->{ids}->{$id}->{'vrf-vxlan'}) && $sdn_cfg->{ids}->{$id}->{'vrf-vxlan'} eq $vrfvxlan);
- }
- }
- }
-}
-
-1;
-
-
+++ /dev/null
-package PVE::Network::SDN::FaucetControllerPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::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::Plugin');
-
-sub type {
- return 'faucetcontroller';
-}
-
-sub plugindata {
- return {
- role => 'controller',
- };
-}
-
-sub properties {
- return {
- };
-}
-
-# Plugin implementation
-sub generate_controller_config {
- my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
-
-}
-
-sub generate_controller_transport_config {
- my ($class, $plugin_config, $router, $id, $uplinks, $config) = @_;
-
- my $dpid = $plugin_config->{'dp-id'};
- my $dphex = printf("%x",$dpid);
-
- my $transport_config = {
- dp_id => $dphex,
- hardware => "Open vSwitch",
- };
-
- $config->{faucet}->{dps}->{$id} = $transport_config;
-
-}
-
-
-sub generate_controller_vnet_config {
- my ($class, $plugin_config, $controller, $transportid, $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}->{$transportid}->{vlans}} , $vnetid);
-
-}
-
-sub on_delete_hook {
- my ($class, $routerid, $sdn_cfg) = @_;
-
-}
-
-sub on_update_hook {
- my ($class, $routerid, $sdn_cfg) = @_;
-
-}
-
-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;
-
+++ /dev/null
-package PVE::Network::SDN::FaucetPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::VlanPlugin;
-
-use base('PVE::Network::SDN::VlanPlugin');
-
-sub type {
- return 'faucet';
-}
-
-sub plugindata {
- return {
- role => 'transport',
- };
-}
-
-sub properties {
- return {
- 'dp-id' => {
- type => 'integer',
- description => 'Faucet dataplane id',
- },
- };
-}
-
-sub options {
-
- return {
- 'dp-id' => { optional => 0 },
- 'uplink-id' => { optional => 0 },
- 'controller' => { optional => 0 },
- 'vlan-allowed' => { optional => 1 },
- };
-}
-
-# Plugin implementation
-sub generate_sdn_config {
- my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
-
- my $mtu = $vnet->{mtu};
- my $uplink = $plugin_config->{'uplink-id'};
- my $dpid = $plugin_config->{'dp-id'};
- my $dphex = printf("%x",$dpid); #fixme :should be 16characters hex
-
- my $iface = $uplinks->{$uplink}->{name};
- $iface = "uplink${uplink}" if !$iface;
-
- #tagged interface
- my @iface_config = ();
- push @iface_config, "ovs_type OVSPort";
- push @iface_config, "ovs_bridge $zoneid";
- push @iface_config, "ovs_mtu $mtu" if $mtu;
- push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
-
- #vnet bridge
- @iface_config = ();
- push @iface_config, "ovs_port $iface";
- push @iface_config, "ovs_type OVSBridge";
- push @iface_config, "ovs_mtu $mtu" if $mtu;
-
- push @iface_config, "ovs_extra set bridge $zoneid other-config:datapath-id=$dphex";
- push @iface_config, "ovs_extra set bridge $zoneid other-config:disable-in-band=true";
- push @iface_config, "ovs_extra set bridge $zoneid fail_mode=secure";
- push @iface_config, "ovs_extra set-controller $vnetid tcp:127.0.0.1:6653";
-
- push(@{$config->{$zoneid}}, @iface_config) if !$config->{$zoneid};
-
- return $config;
-}
-
-
-1;
-
-
-SOURCES=Plugin.pm VnetPlugin.pm VlanPlugin.pm VxlanPlugin.pm FaucetControllerPlugin.pm FaucetPlugin.pm EvpnPlugin.pm EvpnControllerPlugin.pm QinQPlugin.pm
+SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
.PHONY: install
install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/$$i; done
+ make -C Controllers install
+ make -C Zones install
+++ /dev/null
-package PVE::Network::SDN::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.cfg',
- sub { __PACKAGE__->parse_config(@_); });
-
-PVE::Cluster::cfs_register_file('sdn.cfg.new',
- sub { __PACKAGE__->parse_config(@_); },
- sub { __PACKAGE__->write_config(@_); });
-
-PVE::JSONSchema::register_standard_option('pve-sdn-id', {
- description => "The SDN object identifier.",
- type => 'string', format => 'pve-sdn-id',
-});
-
-PVE::JSONSchema::register_format('pve-sdn-id', \&parse_sdn_id);
-sub parse_sdn_id {
- my ($sdnid, $noerr) = @_;
-
- if ($sdnid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
- return undef if $noerr;
- die "SDN object ID '$sdnid' contains illegal characters\n";
- }
- return $sdnid;
-}
-
-my $defaultData = {
-
- propertyList => {
- type => {
- description => "Plugin type.",
- type => 'string', format => 'pve-configid',
- type => 'string',
- },
- sdn => get_standard_option('pve-sdn-id',
- { completion => \&PVE::Network::SDN::complete_sdn }),
- },
-};
-
-sub private {
- return $defaultData;
-}
-
-sub parse_section_header {
- my ($class, $line) = @_;
-
- if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
- my ($type, $sdnid) = (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, $sdnid, $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, $router, $id, $uplinks, $config) = @_;
-
- die "please implement inside plugin";
-}
-
-sub generate_controller_vnet_config {
- my ($class, $plugin_config, $controller, $transportid, $vnetid, $config) = @_;
-
-}
-
-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, $sndid, $scfg) = @_;
-
- # do nothing by default
-}
-
-sub on_update_hook {
- my ($class, $sdnid, $scfg) = @_;
-
- # do nothing by default
-}
-
-#helpers
-sub parse_tag_number_or_range {
- my ($str, $max, $tag) = @_;
-
- my @elements = split(/,/, $str);
- my $count = 0;
- my $allowed = undef;
-
- die "extraneous commas in list\n" if $str ne join(',', @elements);
- foreach my $item (@elements) {
- if ($item =~ m/^([0-9]+)-([0-9]+)$/) {
- $count += 2;
- my ($port1, $port2) = ($1, $2);
- die "invalid port '$port1'\n" if $port1 > $max;
- die "invalid port '$port2'\n" if $port2 > $max;
- die "backwards range '$port1:$port2' not allowed, did you mean '$port2:$port1'?\n" if $port1 > $port2;
-
- if ($tag && $tag >= $port1 && $tag <= $port2){
- $allowed = 1;
- last;
- }
-
- } elsif ($item =~ m/^([0-9]+)$/) {
- $count += 1;
- my $port = $1;
- die "invalid port '$port'\n" if $port > $max;
-
- if ($tag && $tag == $port){
- $allowed = 1;
- last;
- }
- }
- }
- die "tag $tag is not allowed" if $tag && !$allowed;
-
- return (scalar(@elements) > 1);
-}
-
-#to be move to Network.pm helper
-sub get_first_local_ipv4_from_interface {
- my ($interface) = @_;
-
- my $cmd = ['/sbin/ip', 'address', 'show', 'dev', $interface];
-
- my $IP = "";
-
- my $code = sub {
- my $line = shift;
-
- if ($line =~ m!^\s*inet\s+($PVE::Tools::IPRE)(?:/\d+|\s+peer\s+)!) {
- $IP = $1;
- return;
- }
- };
-
- PVE::Tools::run_command($cmd, outfunc => $code);
-
- return $IP;
-}
-
-1;
+++ /dev/null
-package PVE::Network::SDN::QinQPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::VlanPlugin;
-
-use base('PVE::Network::SDN::VlanPlugin');
-
-sub type {
- return 'qinq';
-}
-
-sub plugindata {
- return {
- role => 'transport',
- };
-}
-
-sub properties {
- return {
- 'vlan-protocol' => {
- type => 'string',
- enum => ['802.1q', '802.1ad'],
- default => '802.1q',
- optional => 1,
- description => "vlan protocol",
- }
- };
-}
-
-sub options {
-
- return {
- 'uplink-id' => { optional => 0 },
- 'tag' => { optional => 0 },
- 'vlan-allowed' => { optional => 1 },
- 'vlan-protocol' => { optional => 1 },
- };
-}
-
-# Plugin implementation
-sub generate_sdn_config {
- my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
-
- my $tag = $vnet->{tag};
- my $transport_tag = $plugin_config->{tag};
- my $mtu = $vnet->{mtu};
- my $alias = $vnet->{alias};
- my $vlanprotocol = $plugin_config->{'vlan-protocol'};
- my $uplink = $plugin_config->{'uplink-id'};
- my $vlanallowed = $plugin_config->{'vlan-allowed'};
-
- die "missing vlan tag" if !$tag;
- die "missing transport vlan tag" if !$transport_tag;
-
- my $iface = $uplinks->{$uplink}->{name};
- $iface = "uplink${uplink}" if !$iface;
- $iface .= ".$transport_tag";
-
- #tagged interface
- my @iface_config = ();
- push @iface_config, "vlan-protocol $vlanprotocol" if $vlanprotocol;
- push @iface_config, "mtu $mtu" if $mtu;
- push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
-
- $iface .= ".$tag";
- #vnet bridge
- @iface_config = ();
- push @iface_config, "bridge_ports $iface";
- push @iface_config, "bridge_stp off";
- push @iface_config, "bridge_fd 0";
- push @iface_config, "mtu $mtu" if $mtu;
- push @iface_config, "alias $alias" if $alias;
- push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
-
- return $config;
-}
-
-1;
-
-
+++ /dev/null
-package PVE::Network::SDN::VlanPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::Plugin;
-
-use base('PVE::Network::SDN::Plugin');
-
-sub type {
- return 'vlan';
-}
-
-sub plugindata {
- return {
- role => 'transport',
- };
-}
-
-PVE::JSONSchema::register_format('pve-sdn-vlanrange', \&pve_verify_sdn_vlanrange);
-sub pve_verify_sdn_vlanrange {
- my ($vlanstr) = @_;
-
- PVE::Network::SDN::Plugin::parse_tag_number_or_range($vlanstr, '4096');
-
- return $vlanstr;
-}
-
-sub properties {
- return {
- 'uplink-id' => {
- type => 'integer',
- minimum => 1, maximum => 4096,
- description => 'Uplink interface',
- },
- 'vlan-allowed' => {
- type => 'string', format => 'pve-sdn-vlanrange',
- description => "Allowed vlan range",
- },
- };
-}
-
-sub options {
-
- return {
- 'uplink-id' => { optional => 0 },
- 'vlan-allowed' => { optional => 1 },
- };
-}
-
-# Plugin implementation
-sub generate_sdn_config {
- my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
-
- my $tag = $vnet->{tag};
- my $mtu = $vnet->{mtu};
- my $alias = $vnet->{alias};
- my $uplink = $plugin_config->{'uplink-id'};
-
- die "missing vlan tag" if !$tag;
-
- my $iface = $uplinks->{$uplink}->{name};
- $iface = "uplink${uplink}" if !$iface;
- $iface .= ".$tag";
-
- #tagged interface
- my @iface_config = ();
- push @iface_config, "mtu $mtu" if $mtu;
- push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
-
- #vnet bridge
- @iface_config = ();
- push @iface_config, "bridge_ports $iface";
- push @iface_config, "bridge_stp off";
- push @iface_config, "bridge_fd 0";
- push @iface_config, "mtu $mtu" if $mtu;
- push @iface_config, "alias $alias" if $alias;
- push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
-
- return $config;
-}
-
-sub on_delete_hook {
- my ($class, $transportid, $sdn_cfg) = @_;
-
- # verify that no vnet are associated to this transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- die "transport $transportid is used by vnet $id"
- if ($sdn->{type} eq 'vnet' && defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid);
- }
-}
-
-sub on_update_hook {
- my ($class, $transportid, $sdn_cfg) = @_;
-
- my $transport = $sdn_cfg->{ids}->{$transportid};
-
- # verify that vlan-allowed don't conflict with another vlan-allowed transport
-
- # verify that vlan-allowed is matching currently vnet tag in this transport
- my $vlanallowed = $transport->{'vlan-allowed'};
- if ($vlanallowed) {
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
- if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
- my $tag = $sdn->{tag};
- eval {
- PVE::Network::SDN::Plugin::parse_tag_number_or_range($vlanallowed, '4096', $tag);
- };
- if($@) {
- die "vlan $tag is not allowed in transport $transportid";
- }
- }
- }
- }
- }
-}
-
-1;
-
-
use strict;
use warnings;
-use PVE::Network::SDN::Plugin;
-use base('PVE::Network::SDN::Plugin');
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+use base qw(PVE::SectionConfig);
+use PVE::JSONSchema qw(get_standard_option);
-use PVE::Cluster;
+PVE::Cluster::cfs_register_file('sdn/vnets.cfg',
+ sub { __PACKAGE__->parse_config(@_); });
+
+PVE::Cluster::cfs_register_file('sdn/vnets.cfg.new',
+ sub { __PACKAGE__->parse_config(@_); },
+ sub { __PACKAGE__->write_config(@_); });
+
+PVE::JSONSchema::register_standard_option('pve-sdn-vnet-id', {
+ description => "The SDN vnet object identifier.",
+ type => 'string', format => 'pve-sdn-vnet-id',
+});
+
+PVE::JSONSchema::register_format('pve-sdn-vnet-id', \&parse_sdn_vnet_id);
+sub parse_sdn_vnet_id {
+ my ($id, $noerr) = @_;
+
+ if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
+ return undef if $noerr;
+ die "SDN object vnet ID '$id' contains illegal characters\n";
+ }
+ return $id;
+}
+
+my $defaultData = {
+
+ propertyList => {
+ vnet => get_standard_option('pve-sdn-vnet-id',
+ { completion => \&PVE::Network::SDN::Vnets::complete_sdn_vnet }),
+ },
+};
sub type {
return 'vnet';
}
-sub plugindata {
- return {
- role => 'vnet',
- };
+sub private {
+ return $defaultData;
}
sub properties {
return {
- transportzone => {
+ zone => {
type => 'string',
- description => "transportzone id",
+ description => "zone id",
},
+ type => {
+ description => "Type",
+ optional => 1,
+ },
tag => {
type => 'integer',
description => "vlan or vxlan id",
sub options {
return {
- transportzone => { optional => 0},
+ zone => { optional => 0},
tag => { optional => 0},
alias => { optional => 1 },
ipv4 => { optional => 1 },
--- /dev/null
+package PVE::Network::SDN::Vnets;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+
+
+use PVE::Network::SDN::VnetPlugin;
+PVE::Network::SDN::VnetPlugin->register();
+PVE::Network::SDN::VnetPlugin->init();
+
+sub sdn_vnets_config {
+ my ($cfg, $id, $noerr) = @_;
+
+ die "no sdn vnet ID specified\n" if !$id;
+
+ my $scfg = $cfg->{ids}->{$id};
+ die "sdn vnet '$id' does not exists\n" if (!$noerr && !$scfg);
+
+ return $scfg;
+}
+
+sub config {
+ my $config = cfs_read_file("sdn/vnets.cfg.new");
+ $config = cfs_read_file("sdn/vnets.cfg") if !keys %{$config->{ids}};
+ return $config;
+}
+
+sub write_config {
+ my ($cfg) = @_;
+
+ cfs_write_file("sdn/vnets.cfg.new", $cfg);
+}
+
+sub lock_sdn_vnets_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file("sdn/vnets.cfg.new", undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub sdn_vnets_ids {
+ my ($cfg) = @_;
+
+ return keys %{$cfg->{ids}};
+}
+
+sub complete_sdn_vnet {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = PVE::Network::SDN::Vnets::config();
+
+ return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Vnets::sdn_vnet_ids($cfg) ];
+}
+
+1;
+++ /dev/null
-package PVE::Network::SDN::VxlanPlugin;
-
-use strict;
-use warnings;
-use PVE::Network::SDN::Plugin;
-use PVE::Tools qw($IPV4RE);
-use PVE::INotify;
-
-use base('PVE::Network::SDN::Plugin');
-
-PVE::JSONSchema::register_format('pve-sdn-vxlanrange', \&pve_verify_sdn_vxlanrange);
-sub pve_verify_sdn_vxlanrange {
- my ($vxlanstr) = @_;
-
- PVE::Network::SDN::Plugin::parse_tag_number_or_range($vxlanstr, '16777216');
-
- return $vxlanstr;
-}
-
-PVE::JSONSchema::register_format('ipv4-multicast', \&parse_ipv4_multicast);
-sub parse_ipv4_multicast {
- my ($ipv4, $noerr) = @_;
-
- if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
- return undef if $noerr;
- die "value does not look like a valid multicast IPv4 address\n";
- }
-
- if ($ipv4 =~ m/^(\d+)\.\d+.\d+.\d+/) {
- if($1 < 224 || $1 > 239) {
- return undef if $noerr;
- die "value does not look like a valid multicast IPv4 address\n";
- }
- }
-
- return $ipv4;
-}
-
-sub type {
- return 'vxlan';
-}
-
-sub plugindata {
- return {
- role => 'transport',
- };
-}
-
-sub properties {
- return {
- 'vxlan-allowed' => {
- type => 'string', format => 'pve-sdn-vxlanrange',
- description => "Allowed vlan range",
- },
- 'multicast-address' => {
- description => "Multicast address.",
- type => 'string', format => 'ipv4-multicast'
- },
- 'unicast-address' => {
- description => "Unicast peers address ip list.",
- type => 'string', format => 'ip-list'
- },
- };
-}
-
-sub options {
-
- return {
- 'uplink-id' => { optional => 0 },
- 'multicast-address' => { optional => 1 },
- 'unicast-address' => { optional => 1 },
- 'vxlan-allowed' => { optional => 1 },
- };
-}
-
-# Plugin implementation
-sub generate_sdn_config {
- my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
-
- my $tag = $vnet->{tag};
- my $alias = $vnet->{alias};
- my $ipv4 = $vnet->{ipv4};
- my $ipv6 = $vnet->{ipv6};
- my $mac = $vnet->{mac};
- my $multicastaddress = $plugin_config->{'multicast-address'};
- my @unicastaddress = split(',', $plugin_config->{'unicast-address'}) if $plugin_config->{'unicast-address'};
-
- my $uplink = $plugin_config->{'uplink-id'};
- my $vxlanallowed = $plugin_config->{'vxlan-allowed'};
-
- die "missing vxlan tag" if !$tag;
- my $iface = "uplink$uplink";
- my $ifaceip = "";
-
- if($uplinks->{$uplink}->{name}) {
- $iface = $uplinks->{$uplink}->{name};
- $ifaceip = PVE::Network::SDN::Plugin::get_first_local_ipv4_from_interface($iface);
- }
-
- my $mtu = 1450;
- $mtu = $uplinks->{$uplink}->{mtu} - 50 if $uplinks->{$uplink}->{mtu};
- $mtu = $vnet->{mtu} if $vnet->{mtu};
-
- #vxlan interface
- my @iface_config = ();
- push @iface_config, "vxlan-id $tag";
-
- if($multicastaddress) {
- push @iface_config, "vxlan-svcnodeip $multicastaddress";
- push @iface_config, "vxlan-physdev $iface";
- } elsif (@unicastaddress) {
-
- foreach my $address (@unicastaddress) {
- next if $address eq $ifaceip;
- push @iface_config, "vxlan_remoteip $address";
- }
- }
-
- push @iface_config, "mtu $mtu" if $mtu;
- push(@{$config->{"vxlan$vnetid"}}, @iface_config) if !$config->{"vxlan$vnetid"};
-
- #vnet bridge
- @iface_config = ();
- push @iface_config, "address $ipv4" if $ipv4;
- push @iface_config, "address $ipv6" if $ipv6;
- push @iface_config, "hwaddress $mac" if $mac;
- push @iface_config, "bridge_ports vxlan$vnetid";
- push @iface_config, "bridge_stp off";
- push @iface_config, "bridge_fd 0";
- push @iface_config, "mtu $mtu" if $mtu;
- push @iface_config, "alias $alias" if $alias;
- push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
-
- return $config;
-}
-
-sub on_delete_hook {
- my ($class, $transportid, $sdn_cfg) = @_;
-
- # verify that no vnet are associated to this transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- die "transport $transportid is used by vnet $id"
- if ($sdn->{type} eq 'vnet' && defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid);
- }
-}
-
-sub on_update_hook {
- my ($class, $transportid, $sdn_cfg) = @_;
-
- my $transport = $sdn_cfg->{ids}->{$transportid};
-
- # verify that vxlan-allowed don't conflict with another vxlan-allowed transport
-
- # verify that vxlan-allowed is matching currently vnet tag in this transport
- my $vxlanallowed = $transport->{'vxlan-allowed'};
- if ($vxlanallowed) {
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- my $sdn = $sdn_cfg->{ids}->{$id};
- if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
- if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
- my $tag = $sdn->{tag};
- eval {
- PVE::Network::SDN::Plugin::parse_tag_number_or_range($vxlanallowed, '16777216', $tag);
- };
- if($@) {
- die "vnet $id - vlan $tag is not allowed in transport $transportid";
- }
- }
- }
- }
- }
-
- # verify that router exist
- if (defined($sdn_cfg->{ids}->{$transportid}->{router})) {
- my $router = $sdn_cfg->{ids}->{$transportid}->{router};
- if (!defined($sdn_cfg->{ids}->{$router})) {
- die "router $router don't exist";
- } else {
- die "$router is not a router type" if $sdn_cfg->{ids}->{$router}->{type} ne 'frr';
- }
-
- #vrf && vrf-vxlan need to be defined with router
- my $vrf = $sdn_cfg->{ids}->{$transportid}->{vrf};
- if (!defined($vrf)) {
- die "missing vrf option";
- } else {
- # verify that vrf is not already declared in another transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- next if $id eq $transportid;
- die "vrf $vrf is already declared in $id"
- if (defined($sdn_cfg->{ids}->{$id}->{vrf}) && $sdn_cfg->{ids}->{$id}->{vrf} eq $vrf);
- }
- }
-
- my $vrfvxlan = $sdn_cfg->{ids}->{$transportid}->{'vrf-vxlan'};
- if (!defined($vrfvxlan)) {
- die "missing vrf-vxlan option";
- } else {
- # verify that vrf-vxlan is not already declared in another transport
- foreach my $id (keys %{$sdn_cfg->{ids}}) {
- next if $id eq $transportid;
- die "vrf-vxlan $vrfvxlan is already declared in $id"
- if (defined($sdn_cfg->{ids}->{$id}->{'vrf-vxlan'}) && $sdn_cfg->{ids}->{$id}->{'vrf-vxlan'} eq $vrfvxlan);
- }
- }
- }
-}
-
-1;
-
-
--- /dev/null
+package PVE::Network::SDN::Zones;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use JSON;
+
+use PVE::Tools qw(extract_param dir_glob_regex run_command);
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+
+use PVE::Network::SDN::Vnets;
+use PVE::Network::SDN::Zones::VlanPlugin;
+use PVE::Network::SDN::Zones::QinQPlugin;
+use PVE::Network::SDN::Zones::VxlanPlugin;
+use PVE::Network::SDN::Zones::EvpnPlugin;
+use PVE::Network::SDN::Zones::FaucetPlugin;
+use PVE::Network::SDN::Zones::Plugin;
+
+PVE::Network::SDN::Zones::VlanPlugin->register();
+PVE::Network::SDN::Zones::QinQPlugin->register();
+PVE::Network::SDN::Zones::VxlanPlugin->register();
+PVE::Network::SDN::Zones::EvpnPlugin->register();
+PVE::Network::SDN::Zones::FaucetPlugin->register();
+PVE::Network::SDN::Zones::Plugin->init();
+
+
+sub sdn_zones_config {
+ my ($cfg, $id, $noerr) = @_;
+
+ die "no sdn zone ID specified\n" if !$id;
+
+ my $scfg = $cfg->{ids}->{$id};
+ die "sdn '$id' does not exists\n" if (!$noerr && !$scfg);
+
+ return $scfg;
+}
+
+sub config {
+ my $config = cfs_read_file("sdn/zones.cfg.new");
+ $config = cfs_read_file("sdn/zones.cfg") if !keys %{$config->{ids}};
+ return $config;
+}
+
+sub write_config {
+ my ($cfg) = @_;
+
+ cfs_write_file("sdn/zones.cfg.new", $cfg);
+}
+
+sub lock_sdn_zones_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file("sdn/zones.cfg.new", undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub sdn_zones_ids {
+ my ($cfg) = @_;
+
+ return keys %{$cfg->{ids}};
+}
+
+sub complete_sdn_zone {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = PVE::Network::SDN::config();
+
+ return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_zones_ids($cfg) ];
+}
+
+
+sub generate_etc_network_config {
+
+ my $vnet_cfg = PVE::Cluster::cfs_read_file('sdn/vnets.cfg');
+ my $transport_cfg = PVE::Cluster::cfs_read_file('sdn/zones.cfg');
+ return if !$vnet_cfg && !$transport_cfg;
+
+ #read main config for physical interfaces
+ my $current_config_file = "/etc/network/interfaces";
+ my $fh = IO::File->new($current_config_file);
+ my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
+ $fh->close();
+
+ #check uplinks
+ my $uplinks = {};
+ foreach my $id (keys %{$interfaces_config->{ifaces}}) {
+ my $interface = $interfaces_config->{ifaces}->{$id};
+ if (my $uplink = $interface->{'uplink-id'}) {
+ die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
+ $interface->{name} = $id;
+ $uplinks->{$interface->{'uplink-id'}} = $interface;
+ }
+ }
+
+ #generate configuration
+ my $config = {};
+ foreach my $id (keys %{$vnet_cfg->{ids}}) {
+ my $vnet = $vnet_cfg->{ids}->{$id};
+ my $zone = $vnet->{transportzone};
+
+ if(!$zone) {
+ warn "can't generate vnet $vnet : zone $zone don't exist";
+ next;
+ }
+
+ my $plugin_config = $transport_cfg->{ids}->{$zone};
+
+ if (!defined($plugin_config)) {
+ warn "can't generate vnet $vnet : zone $zone don't exist";
+ next;
+ }
+
+ my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type});
+ $plugin->generate_sdn_config($plugin_config, $zone, $id, $vnet, $uplinks, $config);
+ }
+
+ my $raw_network_config = "";
+ foreach my $iface (keys %$config) {
+ $raw_network_config .= "\n";
+ $raw_network_config .= "auto $iface\n";
+ $raw_network_config .= "iface $iface\n";
+ foreach my $option (@{$config->{$iface}}) {
+ $raw_network_config .= "\t$option\n";
+ }
+ }
+
+ return $raw_network_config;
+}
+
+sub write_etc_network_config {
+ my ($rawconfig) = @_;
+
+ return if !$rawconfig;
+ my $sdn_interfaces_file = "/etc/network/interfaces.d/sdn";
+
+ my $writefh = IO::File->new($sdn_interfaces_file,">");
+ print $writefh $rawconfig;
+ $writefh->close();
+}
+
+sub ifquery_check {
+
+ my $cmd = ['ifquery', '-a', '-c', '-o','json'];
+
+ my $result = '';
+ my $reader = sub { $result .= shift };
+
+ eval {
+ run_command($cmd, outfunc => $reader);
+ };
+
+ my $resultjson = decode_json($result);
+ my $interfaces = {};
+
+ foreach my $interface (@$resultjson) {
+ my $name = $interface->{name};
+ $interfaces->{$name} = {
+ status => $interface->{status},
+ config => $interface->{config},
+ config_status => $interface->{config_status},
+ };
+ }
+
+ return $interfaces;
+}
+
+# improve me : move status code inside plugins ?
+sub status {
+
+ my $cluster_vnet_file = "/etc/pve/sdn/vnets.cfg";
+ my $cluster_transport_file = "/etc/pve/sdn/zones.cfg";
+ my $local_sdn_file = "/etc/network/interfaces.d/sdn";
+ my $err_config = undef;
+
+ return if !-e $cluster_vnet_file && !-e $cluster_transport_file;
+
+ if (!-e $local_sdn_file) {
+ warn "local sdn network configuration is not yet generated, please reload";
+ $err_config = 'pending';
+ } else {
+ # fixme : use some kind of versioning info?
+ my $cluster_vnet_timestamp = (stat($cluster_vnet_file))[9];
+ my $cluster_transport_timestamp = (stat($cluster_transport_file))[9];
+ my $local_sdn_timestamp = (stat($local_sdn_file))[9];
+
+ if ($local_sdn_timestamp < $cluster_vnet_timestamp || $local_sdn_timestamp < $cluster_transport_timestamp) {
+ warn "local sdn network configuration is too old, please reload";
+ $err_config = 'pending';
+ }
+ }
+
+ my $status = ifquery_check();
+
+ my $vnet_cfg = PVE::Cluster::cfs_read_file('sdn/vnets.cfg');
+
+ my $vnet_status = {};
+ my $transport_status = {};
+
+ foreach my $id (keys %{$vnet_cfg->{ids}}) {
+ my $transportzone = $vnet_cfg->{ids}->{$id}->{transportzone};
+ $vnet_status->{$id}->{transportzone} = $transportzone;
+ $transport_status->{$transportzone}->{status} = 'available' if !defined($transport_status->{$transportzone}->{status});
+
+ if($err_config) {
+ $vnet_status->{$id}->{status} = $err_config;
+ $transport_status->{$transportzone}->{status} = $err_config;
+ } elsif ($status->{$id}->{status} && $status->{$id}->{status} eq 'pass') {
+ $vnet_status->{$id}->{status} = 'available';
+ my $bridgeport = $status->{$id}->{config}->{'bridge-ports'};
+
+ if ($status->{$bridgeport}->{status} && $status->{$bridgeport}->{status} ne 'pass') {
+ $vnet_status->{$id}->{status} = 'error';
+ $transport_status->{$transportzone}->{status} = 'error';
+ }
+ } else {
+ $vnet_status->{$id}->{status} = 'error';
+ $transport_status->{$transportzone}->{status} = 'error';
+ }
+ }
+ return($transport_status, $vnet_status);
+}
+
+1;
+
--- /dev/null
+package PVE::Network::SDN::Zones::EvpnPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Zones::VxlanPlugin;
+use PVE::Tools qw($IPV4RE);
+use PVE::INotify;
+
+use base('PVE::Network::SDN::Zones::VxlanPlugin');
+
+sub type {
+ return 'evpn';
+}
+
+sub plugindata {
+ return {
+ role => 'transport',
+ };
+}
+
+sub properties {
+ return {
+ 'vrf' => {
+ description => "vrf name.",
+ type => 'string', #fixme: format
+ },
+ 'vrf-vxlan' => {
+ type => 'integer',
+ description => "l3vni.",
+ },
+ 'controller' => {
+ type => 'string',
+ description => "Frr router name",
+ },
+ };
+}
+
+sub options {
+
+ return {
+ 'uplink-id' => { optional => 0 },
+ 'vxlan-allowed' => { optional => 1 },
+ 'vrf' => { optional => 0 },
+ 'vrf-vxlan' => { optional => 0 },
+ 'controller' => { optional => 0 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
+
+ my $tag = $vnet->{tag};
+ my $alias = $vnet->{alias};
+ my $ipv4 = $vnet->{ipv4};
+ my $ipv6 = $vnet->{ipv6};
+ my $mac = $vnet->{mac};
+
+ my $uplink = $plugin_config->{'uplink-id'};
+ my $vxlanallowed = $plugin_config->{'vxlan-allowed'};
+ my $vrf = $plugin_config->{'vrf'};
+ my $vrfvxlan = $plugin_config->{'vrf-vxlan'};
+
+ die "missing vxlan tag" if !$tag;
+ my $iface = "uplink$uplink";
+ my $ifaceip = "";
+
+ if($uplinks->{$uplink}->{name}) {
+ $iface = $uplinks->{$uplink}->{name};
+ $ifaceip = PVE::Network::SDN::Zones::Plugin::get_first_local_ipv4_from_interface($iface);
+ }
+
+ my $mtu = 1450;
+ $mtu = $uplinks->{$uplink}->{mtu} - 50 if $uplinks->{$uplink}->{mtu};
+ $mtu = $vnet->{mtu} if $vnet->{mtu};
+
+ #vxlan interface
+ my @iface_config = ();
+ push @iface_config, "vxlan-id $tag";
+
+ push @iface_config, "vxlan-local-tunnelip $ifaceip" if $ifaceip;
+ push @iface_config, "bridge-learning off";
+ push @iface_config, "bridge-arp-nd-suppress on";
+
+ push @iface_config, "mtu $mtu" if $mtu;
+ push(@{$config->{"vxlan$vnetid"}}, @iface_config) if !$config->{"vxlan$vnetid"};
+
+ #vnet bridge
+ @iface_config = ();
+ push @iface_config, "address $ipv4" if $ipv4;
+ push @iface_config, "address $ipv6" if $ipv6;
+ push @iface_config, "hwaddress $mac" if $mac;
+ push @iface_config, "bridge_ports vxlan$vnetid";
+ push @iface_config, "bridge_stp off";
+ push @iface_config, "bridge_fd 0";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push @iface_config, "alias $alias" if $alias;
+ push @iface_config, "ip-forward on" if $ipv4;
+ push @iface_config, "ip6-forward on" if $ipv6;
+ push @iface_config, "arp-accept on" if $ipv4||$ipv6;
+ push @iface_config, "vrf $vrf" if $vrf;
+ push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
+
+ if ($vrf) {
+ #vrf interface
+ @iface_config = ();
+ push @iface_config, "vrf-table auto";
+ push(@{$config->{$vrf}}, @iface_config) if !$config->{$vrf};
+
+ if ($vrfvxlan) {
+ #l3vni vxlan interface
+ my $iface_vxlan = "vxlan$vrf";
+ @iface_config = ();
+ push @iface_config, "vxlan-id $vrfvxlan";
+ push @iface_config, "vxlan-local-tunnelip $ifaceip" if $ifaceip;
+ push @iface_config, "bridge-learning off";
+ push @iface_config, "bridge-arp-nd-suppress on";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push(@{$config->{$iface_vxlan}}, @iface_config) if !$config->{$iface_vxlan};
+
+ #l3vni bridge
+ my $brvrf = "br$vrf";
+ @iface_config = ();
+ push @iface_config, "bridge-ports $iface_vxlan";
+ push @iface_config, "bridge_stp off";
+ push @iface_config, "bridge_fd 0";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push @iface_config, "vrf $vrf";
+ push(@{$config->{$brvrf}}, @iface_config) if !$config->{$brvrf};
+ }
+ }
+
+ return $config;
+}
+
+sub on_update_hook {
+ my ($class, $transportid, $sdn_cfg) = @_;
+
+ my $transport = $sdn_cfg->{ids}->{$transportid};
+
+ # verify that vxlan-allowed don't conflict with another vxlan-allowed transport
+
+ # verify that vxlan-allowed is matching currently vnet tag in this transport
+ my $vxlanallowed = $transport->{'vxlan-allowed'};
+ if ($vxlanallowed) {
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
+ if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
+ my $tag = $sdn->{tag};
+ eval {
+ PVE::Network::SDN::Zones::Plugin::parse_tag_number_or_range($vxlanallowed, '16777216', $tag);
+ };
+ if($@) {
+ die "vnet $id - vlan $tag is not allowed in transport $transportid";
+ }
+ }
+ }
+ }
+ }
+
+ # verify that router exist
+ if (defined($sdn_cfg->{ids}->{$transportid}->{router})) {
+ my $router = $sdn_cfg->{ids}->{$transportid}->{router};
+ if (!defined($sdn_cfg->{ids}->{$router})) {
+ die "router $router don't exist";
+ } else {
+ die "$router is not a router type" if $sdn_cfg->{ids}->{$router}->{type} ne 'frr';
+ }
+
+ #vrf && vrf-vxlan need to be defined with router
+ my $vrf = $sdn_cfg->{ids}->{$transportid}->{vrf};
+ if (!defined($vrf)) {
+ die "missing vrf option";
+ } else {
+ # verify that vrf is not already declared in another transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ next if $id eq $transportid;
+ die "vrf $vrf is already declared in $id"
+ if (defined($sdn_cfg->{ids}->{$id}->{vrf}) && $sdn_cfg->{ids}->{$id}->{vrf} eq $vrf);
+ }
+ }
+
+ my $vrfvxlan = $sdn_cfg->{ids}->{$transportid}->{'vrf-vxlan'};
+ if (!defined($vrfvxlan)) {
+ die "missing vrf-vxlan option";
+ } else {
+ # verify that vrf-vxlan is not already declared in another transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ next if $id eq $transportid;
+ die "vrf-vxlan $vrfvxlan is already declared in $id"
+ if (defined($sdn_cfg->{ids}->{$id}->{'vrf-vxlan'}) && $sdn_cfg->{ids}->{$id}->{'vrf-vxlan'} eq $vrfvxlan);
+ }
+ }
+ }
+}
+
+1;
+
+
--- /dev/null
+package PVE::Network::SDN::Zones::FaucetPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Zones::VlanPlugin;
+
+use base('PVE::Network::SDN::Zones::VlanPlugin');
+
+sub type {
+ return 'faucet';
+}
+
+sub plugindata {
+ return {
+ role => 'transport',
+ };
+}
+
+sub properties {
+ return {
+ 'dp-id' => {
+ type => 'integer',
+ description => 'Faucet dataplane id',
+ },
+ };
+}
+
+sub options {
+
+ return {
+ 'dp-id' => { optional => 0 },
+ 'uplink-id' => { optional => 0 },
+ 'controller' => { optional => 0 },
+ 'vlan-allowed' => { optional => 1 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
+
+ my $mtu = $vnet->{mtu};
+ my $uplink = $plugin_config->{'uplink-id'};
+ my $dpid = $plugin_config->{'dp-id'};
+ my $dphex = printf("%x",$dpid); #fixme :should be 16characters hex
+
+ my $iface = $uplinks->{$uplink}->{name};
+ $iface = "uplink${uplink}" if !$iface;
+
+ #tagged interface
+ my @iface_config = ();
+ push @iface_config, "ovs_type OVSPort";
+ push @iface_config, "ovs_bridge $zoneid";
+ push @iface_config, "ovs_mtu $mtu" if $mtu;
+ push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
+
+ #vnet bridge
+ @iface_config = ();
+ push @iface_config, "ovs_port $iface";
+ push @iface_config, "ovs_type OVSBridge";
+ push @iface_config, "ovs_mtu $mtu" if $mtu;
+
+ push @iface_config, "ovs_extra set bridge $zoneid other-config:datapath-id=$dphex";
+ push @iface_config, "ovs_extra set bridge $zoneid other-config:disable-in-band=true";
+ push @iface_config, "ovs_extra set bridge $zoneid fail_mode=secure";
+ push @iface_config, "ovs_extra set-controller $vnetid tcp:127.0.0.1:6653";
+
+ push(@{$config->{$zoneid}}, @iface_config) if !$config->{$zoneid};
+
+ return $config;
+}
+
+
+1;
+
+
--- /dev/null
+SOURCES=Plugin.pm VlanPlugin.pm VxlanPlugin.pm FaucetPlugin.pm EvpnPlugin.pm QinQPlugin.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Zones/$$i; done
--- /dev/null
+package PVE::Network::SDN::Zones::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/zones.cfg',
+ sub { __PACKAGE__->parse_config(@_); });
+
+PVE::Cluster::cfs_register_file('sdn/zones.cfg.new',
+ sub { __PACKAGE__->parse_config(@_); },
+ sub { __PACKAGE__->write_config(@_); });
+
+PVE::JSONSchema::register_standard_option('pve-sdn-zone-id', {
+ description => "The SDN zone object identifier.",
+ type => 'string', format => 'pve-sdn-zone-id',
+});
+
+PVE::JSONSchema::register_format('pve-sdn-zone-id', \&parse_sdn_zone_id);
+sub parse_sdn_zone_id {
+ my ($id, $noerr) = @_;
+
+ if ($id !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
+ return undef if $noerr;
+ die "SDN zone object ID '$id' contains illegal characters\n";
+ }
+ return $id;
+}
+
+my $defaultData = {
+
+ propertyList => {
+ type => {
+ description => "Plugin type.",
+ type => 'string', format => 'pve-configid',
+ type => 'string',
+ },
+ zone => get_standard_option('pve-sdn-zone-id',
+ { completion => \&PVE::Network::SDN::Zones::complete_sdn_zone }),
+ },
+};
+
+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, $router, $id, $uplinks, $config) = @_;
+
+ die "please implement inside plugin";
+}
+
+sub generate_controller_vnet_config {
+ my ($class, $plugin_config, $controller, $transportid, $vnetid, $config) = @_;
+
+}
+
+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, $sndid, $scfg) = @_;
+
+ # do nothing by default
+}
+
+sub on_update_hook {
+ my ($class, $sdnid, $scfg) = @_;
+
+ # do nothing by default
+}
+
+#helpers
+sub parse_tag_number_or_range {
+ my ($str, $max, $tag) = @_;
+
+ my @elements = split(/,/, $str);
+ my $count = 0;
+ my $allowed = undef;
+
+ die "extraneous commas in list\n" if $str ne join(',', @elements);
+ foreach my $item (@elements) {
+ if ($item =~ m/^([0-9]+)-([0-9]+)$/) {
+ $count += 2;
+ my ($port1, $port2) = ($1, $2);
+ die "invalid port '$port1'\n" if $port1 > $max;
+ die "invalid port '$port2'\n" if $port2 > $max;
+ die "backwards range '$port1:$port2' not allowed, did you mean '$port2:$port1'?\n" if $port1 > $port2;
+
+ if ($tag && $tag >= $port1 && $tag <= $port2){
+ $allowed = 1;
+ last;
+ }
+
+ } elsif ($item =~ m/^([0-9]+)$/) {
+ $count += 1;
+ my $port = $1;
+ die "invalid port '$port'\n" if $port > $max;
+
+ if ($tag && $tag == $port){
+ $allowed = 1;
+ last;
+ }
+ }
+ }
+ die "tag $tag is not allowed" if $tag && !$allowed;
+
+ return (scalar(@elements) > 1);
+}
+
+#to be move to Network.pm helper
+sub get_first_local_ipv4_from_interface {
+ my ($interface) = @_;
+
+ my $cmd = ['/sbin/ip', 'address', 'show', 'dev', $interface];
+
+ my $IP = "";
+
+ my $code = sub {
+ my $line = shift;
+
+ if ($line =~ m!^\s*inet\s+($PVE::Tools::IPRE)(?:/\d+|\s+peer\s+)!) {
+ $IP = $1;
+ return;
+ }
+ };
+
+ PVE::Tools::run_command($cmd, outfunc => $code);
+
+ return $IP;
+}
+
+1;
--- /dev/null
+package PVE::Network::SDN::Zones::QinQPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Zones::VlanPlugin;
+
+use base('PVE::Network::SDN::Zones::VlanPlugin');
+
+sub type {
+ return 'qinq';
+}
+
+sub properties {
+ return {
+ tag => {
+ type => 'integer',
+ description => "vlan tag",
+ },
+ 'vlan-protocol' => {
+ type => 'string',
+ enum => ['802.1q', '802.1ad'],
+ default => '802.1q',
+ optional => 1,
+ description => "vlan protocol",
+ }
+ };
+}
+
+sub options {
+
+ return {
+ 'uplink-id' => { optional => 0 },
+ 'tag' => { optional => 0 },
+ 'vlan-allowed' => { optional => 1 },
+ 'vlan-protocol' => { optional => 1 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
+
+ my $tag = $vnet->{tag};
+ my $transport_tag = $plugin_config->{tag};
+ my $mtu = $vnet->{mtu};
+ my $alias = $vnet->{alias};
+ my $vlanprotocol = $plugin_config->{'vlan-protocol'};
+ my $uplink = $plugin_config->{'uplink-id'};
+ my $vlanallowed = $plugin_config->{'vlan-allowed'};
+
+ die "missing vlan tag" if !$tag;
+ die "missing transport vlan tag" if !$transport_tag;
+
+ my $iface = $uplinks->{$uplink}->{name};
+ $iface = "uplink${uplink}" if !$iface;
+ $iface .= ".$transport_tag";
+
+ #tagged interface
+ my @iface_config = ();
+ push @iface_config, "vlan-protocol $vlanprotocol" if $vlanprotocol;
+ push @iface_config, "mtu $mtu" if $mtu;
+ push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
+
+ $iface .= ".$tag";
+ #vnet bridge
+ @iface_config = ();
+ push @iface_config, "bridge_ports $iface";
+ push @iface_config, "bridge_stp off";
+ push @iface_config, "bridge_fd 0";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push @iface_config, "alias $alias" if $alias;
+ push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
+
+ return $config;
+}
+
+1;
+
+
--- /dev/null
+package PVE::Network::SDN::Zones::VlanPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Zones::Plugin;
+
+use base('PVE::Network::SDN::Zones::Plugin');
+
+sub type {
+ return 'vlan';
+}
+
+PVE::JSONSchema::register_format('pve-sdn-vlanrange', \&pve_verify_sdn_vlanrange);
+sub pve_verify_sdn_vlanrange {
+ my ($vlanstr) = @_;
+
+ PVE::Network::SDN::Zones::Plugin::parse_tag_number_or_range($vlanstr, '4096');
+
+ return $vlanstr;
+}
+
+sub properties {
+ return {
+ 'uplink-id' => {
+ type => 'integer',
+ minimum => 1, maximum => 4096,
+ description => 'Uplink interface',
+ },
+ 'vlan-allowed' => {
+ type => 'string', format => 'pve-sdn-vlanrange',
+ description => "Allowed vlan range",
+ },
+ };
+}
+
+sub options {
+
+ return {
+ 'uplink-id' => { optional => 0 },
+ 'vlan-allowed' => { optional => 1 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
+
+ my $tag = $vnet->{tag};
+ my $mtu = $vnet->{mtu};
+ my $alias = $vnet->{alias};
+ my $uplink = $plugin_config->{'uplink-id'};
+
+ die "missing vlan tag" if !$tag;
+
+ my $iface = $uplinks->{$uplink}->{name};
+ $iface = "uplink${uplink}" if !$iface;
+ $iface .= ".$tag";
+
+ #tagged interface
+ my @iface_config = ();
+ push @iface_config, "mtu $mtu" if $mtu;
+ push(@{$config->{$iface}}, @iface_config) if !$config->{$iface};
+
+ #vnet bridge
+ @iface_config = ();
+ push @iface_config, "bridge_ports $iface";
+ push @iface_config, "bridge_stp off";
+ push @iface_config, "bridge_fd 0";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push @iface_config, "alias $alias" if $alias;
+ push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
+
+ return $config;
+}
+
+sub on_delete_hook {
+ my ($class, $transportid, $sdn_cfg) = @_;
+
+ # verify that no vnet are associated to this transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ die "transport $transportid is used by vnet $id"
+ if ($sdn->{type} eq 'vnet' && defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid);
+ }
+}
+
+sub on_update_hook {
+ my ($class, $transportid, $sdn_cfg) = @_;
+
+ my $transport = $sdn_cfg->{ids}->{$transportid};
+
+ # verify that vlan-allowed don't conflict with another vlan-allowed transport
+
+ # verify that vlan-allowed is matching currently vnet tag in this transport
+ my $vlanallowed = $transport->{'vlan-allowed'};
+ if ($vlanallowed) {
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
+ if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
+ my $tag = $sdn->{tag};
+ eval {
+ PVE::Network::SDN::Zones::Plugin::parse_tag_number_or_range($vlanallowed, '4096', $tag);
+ };
+ if($@) {
+ die "vlan $tag is not allowed in transport $transportid";
+ }
+ }
+ }
+ }
+ }
+}
+
+1;
+
+
--- /dev/null
+package PVE::Network::SDN::Zones::VxlanPlugin;
+
+use strict;
+use warnings;
+use PVE::Network::SDN::Zones::Plugin;
+use PVE::Tools qw($IPV4RE);
+use PVE::INotify;
+
+use base('PVE::Network::SDN::Zones::Plugin');
+
+PVE::JSONSchema::register_format('pve-sdn-vxlanrange', \&pve_verify_sdn_vxlanrange);
+sub pve_verify_sdn_vxlanrange {
+ my ($vxlanstr) = @_;
+
+ PVE::Network::SDN::Zones::Plugin::parse_tag_number_or_range($vxlanstr, '16777216');
+
+ return $vxlanstr;
+}
+
+PVE::JSONSchema::register_format('ipv4-multicast', \&parse_ipv4_multicast);
+sub parse_ipv4_multicast {
+ my ($ipv4, $noerr) = @_;
+
+ if ($ipv4 !~ m/^(?:$IPV4RE)$/) {
+ return undef if $noerr;
+ die "value does not look like a valid multicast IPv4 address\n";
+ }
+
+ if ($ipv4 =~ m/^(\d+)\.\d+.\d+.\d+/) {
+ if($1 < 224 || $1 > 239) {
+ return undef if $noerr;
+ die "value does not look like a valid multicast IPv4 address\n";
+ }
+ }
+
+ return $ipv4;
+}
+
+sub type {
+ return 'vxlan';
+}
+
+sub properties {
+ return {
+ 'vxlan-allowed' => {
+ type => 'string', format => 'pve-sdn-vxlanrange',
+ description => "Allowed vlan range",
+ },
+ 'multicast-address' => {
+ description => "Multicast address.",
+ type => 'string', format => 'ipv4-multicast'
+ },
+ 'unicast-address' => {
+ description => "Unicast peers address ip list.",
+ type => 'string', format => 'ip-list'
+ },
+ };
+}
+
+sub options {
+
+ return {
+ 'uplink-id' => { optional => 0 },
+ 'multicast-address' => { optional => 1 },
+ 'unicast-address' => { optional => 1 },
+ 'vxlan-allowed' => { optional => 1 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $config) = @_;
+
+ my $tag = $vnet->{tag};
+ my $alias = $vnet->{alias};
+ my $ipv4 = $vnet->{ipv4};
+ my $ipv6 = $vnet->{ipv6};
+ my $mac = $vnet->{mac};
+ my $multicastaddress = $plugin_config->{'multicast-address'};
+ my @unicastaddress = split(',', $plugin_config->{'unicast-address'}) if $plugin_config->{'unicast-address'};
+
+ my $uplink = $plugin_config->{'uplink-id'};
+ my $vxlanallowed = $plugin_config->{'vxlan-allowed'};
+
+ die "missing vxlan tag" if !$tag;
+ my $iface = "uplink$uplink";
+ my $ifaceip = "";
+
+ if($uplinks->{$uplink}->{name}) {
+ $iface = $uplinks->{$uplink}->{name};
+ $ifaceip = PVE::Network::SDN::Zones::Plugin::get_first_local_ipv4_from_interface($iface);
+ }
+
+ my $mtu = 1450;
+ $mtu = $uplinks->{$uplink}->{mtu} - 50 if $uplinks->{$uplink}->{mtu};
+ $mtu = $vnet->{mtu} if $vnet->{mtu};
+
+ #vxlan interface
+ my @iface_config = ();
+ push @iface_config, "vxlan-id $tag";
+
+ if($multicastaddress) {
+ push @iface_config, "vxlan-svcnodeip $multicastaddress";
+ push @iface_config, "vxlan-physdev $iface";
+ } elsif (@unicastaddress) {
+
+ foreach my $address (@unicastaddress) {
+ next if $address eq $ifaceip;
+ push @iface_config, "vxlan_remoteip $address";
+ }
+ }
+
+ push @iface_config, "mtu $mtu" if $mtu;
+ push(@{$config->{"vxlan$vnetid"}}, @iface_config) if !$config->{"vxlan$vnetid"};
+
+ #vnet bridge
+ @iface_config = ();
+ push @iface_config, "address $ipv4" if $ipv4;
+ push @iface_config, "address $ipv6" if $ipv6;
+ push @iface_config, "hwaddress $mac" if $mac;
+ push @iface_config, "bridge_ports vxlan$vnetid";
+ push @iface_config, "bridge_stp off";
+ push @iface_config, "bridge_fd 0";
+ push @iface_config, "mtu $mtu" if $mtu;
+ push @iface_config, "alias $alias" if $alias;
+ push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid};
+
+ return $config;
+}
+
+sub on_delete_hook {
+ my ($class, $transportid, $sdn_cfg) = @_;
+
+ # verify that no vnet are associated to this transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ die "transport $transportid is used by vnet $id"
+ if ($sdn->{type} eq 'vnet' && defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid);
+ }
+}
+
+sub on_update_hook {
+ my ($class, $transportid, $sdn_cfg) = @_;
+
+ my $transport = $sdn_cfg->{ids}->{$transportid};
+
+ # verify that vxlan-allowed don't conflict with another vxlan-allowed transport
+
+ # verify that vxlan-allowed is matching currently vnet tag in this transport
+ my $vxlanallowed = $transport->{'vxlan-allowed'};
+ if ($vxlanallowed) {
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ my $sdn = $sdn_cfg->{ids}->{$id};
+ if ($sdn->{type} eq 'vnet' && defined($sdn->{tag})) {
+ if(defined($sdn->{transportzone}) && $sdn->{transportzone} eq $transportid) {
+ my $tag = $sdn->{tag};
+ eval {
+ PVE::Network::SDN::Zones::Plugin::parse_tag_number_or_range($vxlanallowed, '16777216', $tag);
+ };
+ if($@) {
+ die "vnet $id - vlan $tag is not allowed in transport $transportid";
+ }
+ }
+ }
+ }
+ }
+
+ # verify that router exist
+ if (defined($sdn_cfg->{ids}->{$transportid}->{router})) {
+ my $router = $sdn_cfg->{ids}->{$transportid}->{router};
+ if (!defined($sdn_cfg->{ids}->{$router})) {
+ die "router $router don't exist";
+ } else {
+ die "$router is not a router type" if $sdn_cfg->{ids}->{$router}->{type} ne 'frr';
+ }
+
+ #vrf && vrf-vxlan need to be defined with router
+ my $vrf = $sdn_cfg->{ids}->{$transportid}->{vrf};
+ if (!defined($vrf)) {
+ die "missing vrf option";
+ } else {
+ # verify that vrf is not already declared in another transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ next if $id eq $transportid;
+ die "vrf $vrf is already declared in $id"
+ if (defined($sdn_cfg->{ids}->{$id}->{vrf}) && $sdn_cfg->{ids}->{$id}->{vrf} eq $vrf);
+ }
+ }
+
+ my $vrfvxlan = $sdn_cfg->{ids}->{$transportid}->{'vrf-vxlan'};
+ if (!defined($vrfvxlan)) {
+ die "missing vrf-vxlan option";
+ } else {
+ # verify that vrf-vxlan is not already declared in another transport
+ foreach my $id (keys %{$sdn_cfg->{ids}}) {
+ next if $id eq $transportid;
+ die "vrf-vxlan $vrfvxlan is already declared in $id"
+ if (defined($sdn_cfg->{ids}->{$id}->{'vrf-vxlan'}) && $sdn_cfg->{ids}->{$id}->{'vrf-vxlan'} eq $vrfvxlan);
+ }
+ }
+ }
+}
+
+1;
+
+
use File::Copy;
use PVE::Cluster qw(cfs_read_file);
-use PVE::Network::SDN;
+use PVE::Network::SDN::Zones;
+use PVE::Network::SDN::Controllers;
use Data::Dumper;
-
-my $network_config = PVE::Network::SDN::generate_etc_network_config();
-PVE::Network::SDN::write_etc_network_config($network_config);
-print "/etc/network/interfaces\n";
+my $network_config = PVE::Network::SDN::Zones::generate_etc_network_config();
+PVE::Network::SDN::Zones::write_etc_network_config($network_config);
+print "/etc/network/interfaces.d/sdn\n";
print $network_config;
print "\n";
-my $controller_config = PVE::Network::SDN::generate_controller_config();
+my $controller_config = PVE::Network::SDN::Controllers::generate_controller_config();
if ($controller_config) {
print Dumper($controller_config);
- PVE::Network::SDN::write_controller_config($controller_config);
- print "/etc/frr/frr.conf\n";
+ PVE::Network::SDN::Controllers::write_controller_config($controller_config);
}