diff options
Diffstat (limited to 'src/PVE')
46 files changed, 7544 insertions, 0 deletions
diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile new file mode 100644 index 0000000..28b2830 --- /dev/null +++ b/src/PVE/API2/Makefile @@ -0,0 +1,4 @@ + +.PHONY: install +install: + make -C Network install diff --git a/src/PVE/API2/Network/Makefile b/src/PVE/API2/Network/Makefile new file mode 100644 index 0000000..396f79d --- /dev/null +++ b/src/PVE/API2/Network/Makefile @@ -0,0 +1,9 @@ +SOURCES=SDN.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/$$i; done + make -C SDN install diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm new file mode 100644 index 0000000..f129d60 --- /dev/null +++ b/src/PVE/API2/Network/SDN.pm @@ -0,0 +1,144 @@ +package PVE::API2::Network::SDN; + +use strict; +use warnings; + +use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file); +use PVE::Exception qw(raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RESTHandler; +use PVE::RPCEnvironment; +use PVE::SafeSyslog; +use PVE::Tools qw(run_command); +use PVE::Network::SDN; + +use PVE::API2::Network::SDN::Controllers; +use PVE::API2::Network::SDN::Vnets; +use PVE::API2::Network::SDN::Zones; +use PVE::API2::Network::SDN::Ipams; +use PVE::API2::Network::SDN::Dns; + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Vnets", + path => 'vnets', +}); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Zones", + path => 'zones', +}); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Controllers", + path => 'controllers', +}); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Ipams", + path => 'ipams', +}); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Dns", + path => 'dns', +}); + +__PACKAGE__->register_method({ + name => 'index', + path => '', + method => 'GET', + description => "Directory index.", + permissions => { + check => ['perm', '/', [ 'SDN.Audit' ]], + }, + parameters => { + additionalProperties => 0, + properties => {}, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + id => { type => 'string' }, + }, + }, + links => [ { rel => 'child', href => "{id}" } ], + }, + code => sub { + my ($param) = @_; + + my $res = [ + { id => 'vnets' }, + { id => 'zones' }, + { id => 'controllers' }, + { id => 'ipams' }, + { id => 'dns' }, + ]; + + return $res; + }}); + +my $create_reload_network_worker = sub { + my ($nodename) = @_; + + # FIXME: how to proxy to final node ? + my $upid; + run_command(['pvesh', 'set', "/nodes/$nodename/network"], outfunc => sub { + my $line = shift; + if ($line =~ /^["']?(UPID:[^\s"']+)["']?$/) { + $upid = $1; + } + }); + #my $upid = PVE::API2::Network->reload_network_config(node => $nodename}); + my $res = PVE::Tools::upid_decode($upid); + + return $res->{pid}; +}; + +__PACKAGE__->register_method ({ + name => 'reload', + protected => 1, + path => '', + method => 'PUT', + description => "Apply sdn controller changes && reload.", + permissions => { + check => ['perm', '/sdn', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + PVE::Network::SDN::commit_config(); + + my $code = sub { + $rpcenv->{type} = 'priv'; # to start tasks in background + PVE::Cluster::check_cfs_quorum(); + my $nodelist = PVE::Cluster::get_nodelist(); + for my $node (@$nodelist) { + my $pid = eval { $create_reload_network_worker->($node) }; + warn $@ if $@; + } + + # FIXME: use libpve-apiclient (like in cluster join) to create + # tasks and moitor the tasks. + + return; + }; + + return $rpcenv->fork_worker('reloadnetworkall', undef, $authuser, $code); + + }}); + + +1; diff --git a/src/PVE/API2/Network/SDN/Controllers.pm b/src/PVE/API2/Network/SDN/Controllers.pm new file mode 100644 index 0000000..d8f18ab --- /dev/null +++ b/src/PVE/API2/Network/SDN/Controllers.pm @@ -0,0 +1,290 @@ +package PVE::API2::Network::SDN::Controllers; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Zones; +use PVE::Network::SDN::Controllers; +use PVE::Network::SDN::Controllers::Plugin; +use PVE::Network::SDN::Controllers::EvpnPlugin; +use PVE::Network::SDN::Controllers::BgpPlugin; +use PVE::Network::SDN::Controllers::FaucetPlugin; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $sdn_controllers_type_enum = PVE::Network::SDN::Controllers::Plugin->lookup_types(); + +my $api_sdn_controllers_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Controllers::sdn_controllers_config($cfg, $id)); + $scfg->{controller} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN controllers index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/controllers/<controller>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list sdn controllers of specific type", + type => 'string', + enum => $sdn_controllers_type_enum, + optional => 1, + }, + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { controller => { type => 'string' }, + type => { type => 'string' }, + state => { type => 'string', optional => 1 }, + pending => { optional => 1}, + }, + }, + links => [ { rel => 'child', href => "{controller}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Controllers::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'controllers'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{controllers}; + } else { + $cfg = PVE::Network::SDN::Controllers::config(); + } + + my @sids = PVE::Network::SDN::Controllers::sdn_controllers_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/controllers/$id", $privs, 1); + + my $scfg = &$api_sdn_controllers_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config = $cfg->{ids}->{$id}; + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{controller}', + method => 'GET', + description => "Read sdn controller configuration.", + permissions => { + check => ['perm', '/sdn/controllers/{controller}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + controller => get_standard_option('pve-sdn-controller-id'), + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Controllers::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'controllers'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{controllers}; + } else { + $cfg = PVE::Network::SDN::Controllers::config(); + } + + return &$api_sdn_controllers_config($cfg, $param->{controller}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn controller object.", + permissions => { + check => ['perm', '/sdn/controllers', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Controllers::Plugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'controller'); + + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + # create /etc/pve/sdn directory + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Controllers::sdn_controllers_config($controller_cfg, $id, 1)) { + die "sdn controller object ID '$id' already defined\n"; + } + + $controller_cfg->{ids}->{$id} = $opts; + $plugin->on_update_hook($id, $controller_cfg); + + PVE::Network::SDN::Controllers::write_config($controller_cfg); + + }, "create sdn controller object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{controller}', + method => 'PUT', + description => "Update sdn controller object configuration.", + permissions => { + check => ['perm', '/sdn/controllers', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Controllers::Plugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'controller'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + + PVE::SectionConfig::assert_if_modified($controller_cfg, $digest); + + my $scfg = PVE::Network::SDN::Controllers::sdn_controllers_config($controller_cfg, $id); + + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($scfg->{type}); + my $opts = $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} = $opts->{$k}; + } + + $plugin->on_update_hook($id, $controller_cfg); + + PVE::Network::SDN::Controllers::write_config($controller_cfg); + + + }, "update sdn controller object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{controller}', + method => 'DELETE', + description => "Delete sdn controller object configuration.", + permissions => { + check => ['perm', '/sdn/controllers', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + controller => get_standard_option('pve-sdn-controller-id', { + completion => \&PVE::Network::SDN::Controllers::complete_sdn_controllers, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'controller'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Controllers::config(); + + my $scfg = PVE::Network::SDN::Controllers::sdn_controllers_config($cfg, $id); + + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($scfg->{type}); + + my $zone_cfg = PVE::Network::SDN::Zones::config(); + + $plugin->on_delete_hook($id, $zone_cfg); + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Controllers::write_config($cfg); + + }, "delete sdn controller object failed"); + + + return undef; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Dns.pm b/src/PVE/API2/Network/SDN/Dns.pm new file mode 100644 index 0000000..3d08552 --- /dev/null +++ b/src/PVE/API2/Network/SDN/Dns.pm @@ -0,0 +1,242 @@ +package PVE::API2::Network::SDN::Dns; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Dns; +use PVE::Network::SDN::Dns::Plugin; +use PVE::Network::SDN::Dns::PowerdnsPlugin; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $sdn_dns_type_enum = PVE::Network::SDN::Dns::Plugin->lookup_types(); + +my $api_sdn_dns_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Dns::sdn_dns_config($cfg, $id)); + $scfg->{dns} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN dns index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/dns/<dns>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list sdn dns of specific type", + type => 'string', + enum => $sdn_dns_type_enum, + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { dns => { type => 'string'}, + type => { type => 'string'}, + }, + }, + links => [ { rel => 'child', href => "{dns}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + + my $cfg = PVE::Network::SDN::Dns::config(); + + my @sids = PVE::Network::SDN::Dns::sdn_dns_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/dns/$id", $privs, 1); + + my $scfg = &$api_sdn_dns_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config = $cfg->{ids}->{$id}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{dns}', + method => 'GET', + description => "Read sdn dns configuration.", + permissions => { + check => ['perm', '/sdn/dns/{dns}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + dns => get_standard_option('pve-sdn-dns-id'), + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = PVE::Network::SDN::Dns::config(); + + return &$api_sdn_dns_config($cfg, $param->{dns}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn dns object.", + permissions => { + check => ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Dns::Plugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'dns'); + + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + # create /etc/pve/sdn directory + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Dns::sdn_dns_config($dns_cfg, $id, 1)) { + die "sdn dns object ID '$id' already defined\n"; + } + + $dns_cfg->{ids}->{$id} = $opts; + + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($opts->{type}); + $plugin->on_update_hook($opts); + + PVE::Network::SDN::Dns::write_config($dns_cfg); + + }, "create sdn dns object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{dns}', + method => 'PUT', + description => "Update sdn dns object configuration.", + permissions => { + check => ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Dns::Plugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'dns'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + + PVE::SectionConfig::assert_if_modified($dns_cfg, $digest); + + my $scfg = PVE::Network::SDN::Dns::sdn_dns_config($dns_cfg, $id); + + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($scfg->{type}); + my $opts = $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} = $opts->{$k}; + } + + $plugin->on_update_hook($scfg); + + PVE::Network::SDN::Dns::write_config($dns_cfg); + + }, "update sdn dns object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{dns}', + method => 'DELETE', + description => "Delete sdn dns object configuration.", + permissions => { + check => ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + dns => get_standard_option('pve-sdn-dns-id', { + completion => \&PVE::Network::SDN::Dns::complete_sdn_dns, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'dns'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Dns::config(); + + my $scfg = PVE::Network::SDN::Dns::sdn_dns_config($cfg, $id); + + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($scfg->{type}); + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Dns::write_config($cfg); + + }, "delete sdn dns object failed"); + + return undef; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Ipams.pm b/src/PVE/API2/Network/SDN/Ipams.pm new file mode 100644 index 0000000..6410e8e --- /dev/null +++ b/src/PVE/API2/Network/SDN/Ipams.pm @@ -0,0 +1,248 @@ +package PVE::API2::Network::SDN::Ipams; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Ipams::Plugin; +use PVE::Network::SDN::Ipams::PVEPlugin; +use PVE::Network::SDN::Ipams::PhpIpamPlugin; +use PVE::Network::SDN::Ipams::NetboxPlugin; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $sdn_ipams_type_enum = PVE::Network::SDN::Ipams::Plugin->lookup_types(); + +my $api_sdn_ipams_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Ipams::sdn_ipams_config($cfg, $id)); + $scfg->{ipam} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN ipams index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/ipams/<ipam>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list sdn ipams of specific type", + type => 'string', + enum => $sdn_ipams_type_enum, + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { ipam => { type => 'string'}, + type => { type => 'string'}, + }, + }, + links => [ { rel => 'child', href => "{ipam}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + + my $cfg = PVE::Network::SDN::Ipams::config(); + + my @sids = PVE::Network::SDN::Ipams::sdn_ipams_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/ipams/$id", $privs, 1); + + my $scfg = &$api_sdn_ipams_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config = $cfg->{ids}->{$id}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{ipam}', + method => 'GET', + description => "Read sdn ipam configuration.", + permissions => { + check => ['perm', '/sdn/ipams/{ipam}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + ipam => get_standard_option('pve-sdn-ipam-id'), + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = PVE::Network::SDN::Ipams::config(); + + return &$api_sdn_ipams_config($cfg, $param->{ipam}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn ipam object.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Ipams::Plugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'ipam'); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + # create /etc/pve/sdn directory + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, $id, 1)) { + die "sdn ipam object ID '$id' already defined\n"; + } + + $ipam_cfg->{ids}->{$id} = $opts; + + my $plugin_config = $opts; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->on_update_hook($plugin_config); + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "create sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{ipam}', + method => 'PUT', + description => "Update sdn ipam object configuration.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Ipams::Plugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'ipam'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + + PVE::SectionConfig::assert_if_modified($ipam_cfg, $digest); + + my $scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, $id); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{type}); + my $opts = $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} = $opts->{$k}; + } + + $plugin->on_update_hook($scfg); + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "update sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{ipam}', + method => 'DELETE', + description => "Delete sdn ipam object configuration.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + ipam => get_standard_option('pve-sdn-ipam-id', { + completion => \&PVE::Network::SDN::Ipams::complete_sdn_ipams, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'ipam'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Ipams::config(); + + my $scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($cfg, $id); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{type}); + + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Ipams::write_config($cfg); + + }, "delete sdn zone object failed"); + + return undef; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile new file mode 100644 index 0000000..3683fa4 --- /dev/null +++ b/src/PVE/API2/Network/SDN/Makefile @@ -0,0 +1,10 @@ +SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done + make -C Zones install + diff --git a/src/PVE/API2/Network/SDN/Subnets.pm b/src/PVE/API2/Network/SDN/Subnets.pm new file mode 100644 index 0000000..377a568 --- /dev/null +++ b/src/PVE/API2/Network/SDN/Subnets.pm @@ -0,0 +1,303 @@ +package PVE::API2::Network::SDN::Subnets; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Exception qw(raise raise_param_exc); +use PVE::Network::SDN; +use PVE::Network::SDN::Subnets; +use PVE::Network::SDN::SubnetPlugin; +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::Zones; +use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Ipams::Plugin; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $api_sdn_subnets_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id)); + $scfg->{subnet} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN subnets index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/subnets/<subnet>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + vnet => get_standard_option('pve-sdn-vnet-id'), + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => {}, + }, + links => [ { rel => 'child', href => "{subnet}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $vnetid = $param->{vnet}; + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Subnets::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'subnets'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{subnets}; + } else { + $cfg = PVE::Network::SDN::Subnets::config(); + } + + my @sids = PVE::Network::SDN::Subnets::sdn_subnets_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/vnets/$vnetid/subnets/$id", $privs, 1); + + my $scfg = &$api_sdn_subnets_config($cfg, $id); + next if !$scfg->{vnet} || $scfg->{vnet} ne $vnetid; + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{subnet}', + method => 'GET', + description => "Read sdn subnet configuration.", + permissions => { + check => ['perm', '/sdn/vnets/{vnet}/subnets/{subnet}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + vnet => get_standard_option('pve-sdn-vnet-id'), + subnet => get_standard_option('pve-sdn-subnet-id', { + completion => \&PVE::Network::SDN::Subnets::complete_sdn_subnets, + }), + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Subnets::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'subnets'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{subnets}; + } else { + $cfg = PVE::Network::SDN::Subnets::config(); + } + + my $scfg = &$api_sdn_subnets_config($cfg, $param->{subnet}); + + raise_param_exc({ vnet => "wrong vnet"}) if $param->{vnet} ne $scfg->{vnet}; + + return $scfg; + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn subnet object.", + permissions => { + check => ['perm', '/sdn/vnets/{vnet}/subnets', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::SubnetPlugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $cidr = extract_param($param, 'subnet'); + + # create /etc/pve/sdn directory + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn") if ! -d '/etc/pve/sdn'; + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Subnets::config(); + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + my $vnet = $param->{vnet}; + my $zoneid = $vnet_cfg->{ids}->{$vnet}->{zone}; + my $zone = $zone_cfg->{ids}->{$zoneid}; + my $id = $cidr =~ s/\//-/r; + $id = "$zoneid-$id"; + + my $opts = PVE::Network::SDN::SubnetPlugin->check_config($id, $param, 1, 1); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id, 1)) { + die "sdn subnet object ID '$id' already defined\n"; + } + + $cfg->{ids}->{$id} = $opts; + + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id); + PVE::Network::SDN::SubnetPlugin->on_update_hook($zone, $id, $subnet); + + PVE::Network::SDN::Subnets::write_config($cfg); + + }, "create sdn subnet object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{subnet}', + method => 'PUT', + description => "Update sdn subnet object configuration.", + permissions => { + check => ['perm', '/sdn/vnets/{vnet}/subnets', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::SubnetPlugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'subnet'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Subnets::config(); + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + my $vnet = $param->{vnet}; + my $zoneid = $vnet_cfg->{ids}->{$vnet}->{zone}; + my $zone = $zone_cfg->{ids}->{$zoneid}; + + my $scfg = &$api_sdn_subnets_config($cfg, $id); + + PVE::SectionConfig::assert_if_modified($cfg, $digest); + + my $opts = PVE::Network::SDN::SubnetPlugin->check_config($id, $param, 0, 1); + $cfg->{ids}->{$id} = $opts; + + raise_param_exc({ ipam => "you can't change ipam"}) if $opts->{ipam} && $scfg->{ipam} && $opts->{ipam} ne $scfg->{ipam}; + + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id); + PVE::Network::SDN::SubnetPlugin->on_update_hook($zone, $id, $subnet, $scfg); + + PVE::Network::SDN::Subnets::write_config($cfg); + + }, "update sdn subnet object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{subnet}', + method => 'DELETE', + description => "Delete sdn subnet object configuration.", + permissions => { + check => ['perm', '/sdn/vnets/{vnet}/subnets', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + vnet => get_standard_option('pve-sdn-vnet-id'), + subnet => get_standard_option('pve-sdn-subnet-id', { + completion => \&PVE::Network::SDN::Subnets::complete_sdn_subnets, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'subnet'); + + PVE::Network::SDN::lock_sdn_config( + sub { + my $cfg = PVE::Network::SDN::Subnets::config(); + + my $scfg = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id, 1); + + my $vnets_cfg = PVE::Network::SDN::Vnets::config(); + + PVE::Network::SDN::SubnetPlugin->on_delete_hook($id, $cfg, $vnets_cfg); + + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $vnet = $param->{vnet}; + my $zoneid = $vnets_cfg->{ids}->{$vnet}->{zone}; + my $zone = $zone_cfg->{ids}->{$zoneid}; + + PVE::Network::SDN::Subnets::del_subnet($zone, $id, $scfg); + + delete $cfg->{ids}->{$id}; + + PVE::Network::SDN::Subnets::write_config($cfg); + + }, "delete sdn subnet object failed"); + + + return undef; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Vnets.pm b/src/PVE/API2/Network/SDN/Vnets.pm new file mode 100644 index 0000000..811a2e8 --- /dev/null +++ b/src/PVE/API2/Network/SDN/Vnets.pm @@ -0,0 +1,292 @@ +package PVE::API2::Network::SDN::Vnets; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Zones; +use PVE::Network::SDN::Zones::Plugin; +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::VnetPlugin; +use PVE::Network::SDN::Subnets; +use PVE::API2::Network::SDN::Subnets; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; +use PVE::Exception qw(raise raise_param_exc); + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Subnets", + path => '{vnet}/subnets', +}); + +my $api_sdn_vnets_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Vnets::sdn_vnets_config($cfg, $id)); + $scfg->{vnet} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +my $api_sdn_vnets_deleted_config = sub { + my ($cfg, $running_cfg, $id) = @_; + + if (!$cfg->{ids}->{$id}) { + + my $vnet_cfg = dclone(PVE::Network::SDN::Vnets::sdn_vnets_config($running_cfg->{vnets}, $id)); + $vnet_cfg->{state} = "deleted"; + $vnet_cfg->{vnet} = $id; + return $vnet_cfg; + } +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN vnets index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate'" + ." permissions on '/sdn/vnets/<vnet>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => {}, + }, + links => [ { rel => 'child', href => "{vnet}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Vnets::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'vnets'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{vnets}; + } else { + $cfg = PVE::Network::SDN::Vnets::config(); + } + + my @sids = PVE::Network::SDN::Vnets::sdn_vnets_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/vnets/$id", $privs, 1); + + my $scfg = &$api_sdn_vnets_config($cfg, $id); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{vnet}', + method => 'GET', + description => "Read sdn vnet configuration.", + permissions => { + check => ['perm', '/sdn/vnets/{vnet}', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + vnet => get_standard_option('pve-sdn-vnet-id', { + completion => \&PVE::Network::SDN::Vnets::complete_sdn_vnets, + }), + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = {}; + if($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Vnets::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'vnets'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{vnets}; + } else { + $cfg = PVE::Network::SDN::Vnets::config(); + } + + return $api_sdn_vnets_config->($cfg, $param->{vnet}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn vnet object.", + permissions => { + check => ['perm', '/sdn/vnets', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::VnetPlugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'vnet'); + + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config(sub { + my $cfg = PVE::Network::SDN::Vnets::config(); + my $opts = PVE::Network::SDN::VnetPlugin->check_config($id, $param, 1, 1); + + if (PVE::Network::SDN::Vnets::sdn_vnets_config($cfg, $id, 1)) { + die "sdn vnet object ID '$id' already defined\n"; + } + $cfg->{ids}->{$id} = $opts; + + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $zoneid = $cfg->{ids}->{$id}->{zone}; + my $plugin_config = $zone_cfg->{ids}->{$zoneid}; + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + $plugin->vnet_update_hook($cfg, $id, $zone_cfg); + + PVE::Network::SDN::VnetPlugin->on_update_hook($id, $cfg); + + PVE::Network::SDN::Vnets::write_config($cfg); + + }, "create sdn vnet object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{vnet}', + method => 'PUT', + description => "Update sdn vnet object configuration.", + permissions => { + check => ['perm', '/sdn/vnets', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::VnetPlugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'vnet'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config(sub { + my $cfg = PVE::Network::SDN::Vnets::config(); + + PVE::SectionConfig::assert_if_modified($cfg, $digest); + + + my $opts = PVE::Network::SDN::VnetPlugin->check_config($id, $param, 0, 1); + raise_param_exc({ zone => "missing zone"}) if !$opts->{zone}; + my $subnets = PVE::Network::SDN::Vnets::get_subnets($id); + raise_param_exc({ zone => "can't change zone if subnets exists"}) if($subnets && $opts->{zone} ne $cfg->{ids}->{$id}->{zone}); + + $cfg->{ids}->{$id} = $opts; + + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $zoneid = $cfg->{ids}->{$id}->{zone}; + my $plugin_config = $zone_cfg->{ids}->{$zoneid}; + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + $plugin->vnet_update_hook($cfg, $id, $zone_cfg); + + PVE::Network::SDN::VnetPlugin->on_update_hook($id, $cfg); + + PVE::Network::SDN::Vnets::write_config($cfg); + + }, "update sdn vnet object failed"); + + return undef; + } +}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{vnet}', + method => 'DELETE', + description => "Delete sdn vnet object configuration.", + permissions => { + check => ['perm', '/sdn/vnets', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + vnet => get_standard_option('pve-sdn-vnet-id', { + completion => \&PVE::Network::SDN::Vnets::complete_sdn_vnets, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'vnet'); + + PVE::Network::SDN::lock_sdn_config(sub { + my $cfg = PVE::Network::SDN::Vnets::config(); + my $scfg = PVE::Network::SDN::Vnets::sdn_vnets_config($cfg, $id); # check if exists + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + + PVE::Network::SDN::VnetPlugin->on_delete_hook($id, $vnet_cfg); + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Vnets::write_config($cfg); + + }, "delete sdn vnet object failed"); + + + return undef; + } +}); + +1; diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm new file mode 100644 index 0000000..6e53240 --- /dev/null +++ b/src/PVE/API2/Network/SDN/Zones.pm @@ -0,0 +1,351 @@ +package PVE::API2::Network::SDN::Zones; + +use strict; +use warnings; + +use Storable qw(dclone); + +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Exception qw(raise raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); + +use PVE::Network::SDN::Dns; +use PVE::Network::SDN::Subnets; +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN; + +use PVE::Network::SDN::Zones::EvpnPlugin; +use PVE::Network::SDN::Zones::FaucetPlugin; +use PVE::Network::SDN::Zones::Plugin; +use PVE::Network::SDN::Zones::QinQPlugin; +use PVE::Network::SDN::Zones::SimplePlugin; +use PVE::Network::SDN::Zones::VlanPlugin; +use PVE::Network::SDN::Zones::VxlanPlugin; +use PVE::Network::SDN::Zones; + +use PVE::RESTHandler; +use base qw(PVE::RESTHandler); + +my $sdn_zones_type_enum = PVE::Network::SDN::Zones::Plugin->lookup_types(); + +my $api_sdn_zones_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Zones::sdn_zones_config($cfg, $id)); + $scfg->{zone} = $id; + $scfg->{digest} = $cfg->{digest}; + + if ($scfg->{nodes}) { + $scfg->{nodes} = PVE::Network::SDN::encode_value($scfg->{type}, 'nodes', $scfg->{nodes}); + } + + if ($scfg->{exitnodes}) { + $scfg->{exitnodes} = PVE::Network::SDN::encode_value($scfg->{type}, 'exitnodes', $scfg->{exitnodes}); + } + + my $pending = $scfg->{pending}; + if ($pending->{nodes}) { + $pending->{nodes} = PVE::Network::SDN::encode_value($scfg->{type}, 'nodes', $pending->{nodes}); + } + + if ($pending->{exitnodes}) { + $pending->{exitnodes} = PVE::Network::SDN::encode_value($scfg->{type}, 'exitnodes', $pending->{exitnodes}); + } + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN zones index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones/<zone>'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list SDN zones of specific type", + type => 'string', + enum => $sdn_zones_type_enum, + optional => 1, + }, + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { zone => { type => 'string'}, + type => { type => 'string'}, + mtu => { type => 'integer', optional => 1 }, + dns => { type => 'string', optional => 1}, + reversedns => { type => 'string', optional => 1}, + dnszone => { type => 'string', optional => 1}, + ipam => { type => 'string', optional => 1}, + pending => { optional => 1}, + state => { type => 'string', optional => 1}, + nodes => { type => 'string', optional => 1}, + }, + }, + links => [ { rel => 'child', href => "{zone}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $cfg = {}; + if ($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Zones::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'zones'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{zones}; + } else { + $cfg = PVE::Network::SDN::Zones::config(); + } + + my @sids = PVE::Network::SDN::Zones::sdn_zones_ids($cfg); + my $res = []; + for my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/zones/$id", $privs, 1); + + my $scfg = &$api_sdn_zones_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config = $cfg->{ids}->{$id}; + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{zone}', + method => 'GET', + description => "Read sdn zone configuration.", + permissions => { + check => ['perm', '/sdn/zones/{zone}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + zone => get_standard_option('pve-sdn-zone-id'), + running => { + type => 'boolean', + optional => 1, + description => "Display running config.", + }, + pending => { + type => 'boolean', + optional => 1, + description => "Display pending config.", + } + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = {}; + if ($param->{pending}) { + my $running_cfg = PVE::Network::SDN::running_config(); + my $config = PVE::Network::SDN::Zones::config(); + $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'zones'); + } elsif ($param->{running}) { + my $running_cfg = PVE::Network::SDN::running_config(); + $cfg = $running_cfg->{zones}; + } else { + $cfg = PVE::Network::SDN::Zones::config(); + } + + return &$api_sdn_zones_config($cfg, $param->{zone}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn zone object.", + permissions => { + check => ['perm', '/sdn/zones', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Zones::Plugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'zone'); + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config(sub { + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + my $dns_cfg = PVE::Network::SDN::Dns::config(); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Zones::sdn_zones_config($zone_cfg, $id, 1)) { + die "sdn zone object ID '$id' already defined\n"; + } + + my $dnsserver = $opts->{dns}; + raise_param_exc({ dns => "$dnsserver don't exist"}) + if $dnsserver && !$dns_cfg->{ids}->{$dnsserver}; + + my $reversednsserver = $opts->{reversedns}; + raise_param_exc({ reversedns => "$reversednsserver don't exist"}) + if $reversednsserver && !$dns_cfg->{ids}->{$reversednsserver}; + + my $dnszone = $opts->{dnszone}; + raise_param_exc({ dnszone => "missing dns server"}) + if $dnszone && !$dnsserver; + + my $ipam = $opts->{ipam}; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + raise_param_exc({ ipam => "$ipam not existing"}) if $ipam && !$ipam_cfg->{ids}->{$ipam}; + + $zone_cfg->{ids}->{$id} = $opts; + $plugin->on_update_hook($id, $zone_cfg, $controller_cfg); + + PVE::Network::SDN::Zones::write_config($zone_cfg); + + }, "create sdn zone object failed"); + + return; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{zone}', + method => 'PUT', + description => "Update sdn zone object configuration.", + permissions => { + check => ['perm', '/sdn/zones', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Zones::Plugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'zone'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config(sub { + my $zone_cfg = PVE::Network::SDN::Zones::config(); + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + my $dns_cfg = PVE::Network::SDN::Dns::config(); + + PVE::SectionConfig::assert_if_modified($zone_cfg, $digest); + + my $scfg = PVE::Network::SDN::Zones::sdn_zones_config($zone_cfg, $id); + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($scfg->{type}); + my $opts = $plugin->check_config($id, $param, 0, 1); + + if ($opts->{ipam} && !$scfg->{ipam} || $opts->{ipam} ne $scfg->{ipam}) { + + # don't allow ipam change if subnet are defined for now, need to implement resync ipam content + my $subnets_cfg = PVE::Network::SDN::Subnets::config(); + for my $subnetid (sort keys %{$subnets_cfg->{ids}}) { + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($subnets_cfg, $subnetid); + raise_param_exc({ ipam => "can't change ipam if a subnet is already defined in this zone"}) + if $subnet->{zone} eq $id; + } + } + + $zone_cfg->{ids}->{$id} = $opts; + + my $dnsserver = $opts->{dns}; + raise_param_exc({ dns => "$dnsserver don't exist"}) if $dnsserver && !$dns_cfg->{ids}->{$dnsserver}; + + my $reversednsserver = $opts->{reversedns}; + raise_param_exc({ reversedns => "$reversednsserver don't exist"}) if $reversednsserver && !$dns_cfg->{ids}->{$reversednsserver}; + + my $dnszone = $opts->{dnszone}; + raise_param_exc({ dnszone => "missing dns server"}) if $dnszone && !$dnsserver; + + my $ipam = $opts->{ipam}; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + raise_param_exc({ ipam => "$ipam not existing"}) if $ipam && !$ipam_cfg->{ids}->{$ipam}; + + $plugin->on_update_hook($id, $zone_cfg, $controller_cfg); + + PVE::Network::SDN::Zones::write_config($zone_cfg); + + }, "update sdn zone object failed"); + + return; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{zone}', + method => 'DELETE', + description => "Delete sdn zone object configuration.", + permissions => { + check => ['perm', '/sdn/zones', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + zone => get_standard_option('pve-sdn-zone-id', { + completion => \&PVE::Network::SDN::Zones::complete_sdn_zones, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'zone'); + + PVE::Network::SDN::lock_sdn_config(sub { + my $cfg = PVE::Network::SDN::Zones::config(); + my $scfg = PVE::Network::SDN::Zones::sdn_zones_config($cfg, $id); + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($scfg->{type}); + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + + $plugin->on_delete_hook($id, $vnet_cfg); + + delete $cfg->{ids}->{$id}; + + PVE::Network::SDN::Zones::write_config($cfg); + }, "delete sdn zone object failed"); + + return; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Zones/Content.pm b/src/PVE/API2/Network/SDN/Zones/Content.pm new file mode 100644 index 0000000..66f49df --- /dev/null +++ b/src/PVE/API2/Network/SDN/Zones/Content.pm @@ -0,0 +1,85 @@ +package PVE::API2::Network::SDN::Zones::Content; + +use strict; +use warnings; +use Data::Dumper; + +use PVE::SafeSyslog; +use PVE::Cluster; +use PVE::INotify; +use PVE::Exception qw(raise_param_exc); +use PVE::RPCEnvironment; +use PVE::RESTHandler; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Network::SDN; + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "List zone content.", + permissions => { + check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit'], any => 1], + }, + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + zone => get_standard_option('pve-sdn-zone-id', { + completion => \&PVE::Network::SDN::Zones::complete_sdn_zone, + }), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + vnet => { + description => "Vnet identifier.", + type => 'string', + }, + status => { + description => "Status.", + type => 'string', + optional => 1, + }, + statusmsg => { + description => "Status details", + type => 'string', + optional => 1, + }, + }, + }, + links => [ { rel => 'child', href => "{vnet}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $zoneid = $param->{zone}; + + my $res = []; + + my ($zone_status, $vnet_status) = PVE::Network::SDN::status(); + + foreach my $id (keys %{$vnet_status}) { + if ($vnet_status->{$id}->{zone} eq $zoneid) { + my $item->{vnet} = $id; + $item->{status} = $vnet_status->{$id}->{'status'}; + $item->{statusmsg} = $vnet_status->{$id}->{'statusmsg'}; + push @$res,$item; + } + } + + return $res; + }}); + +1; diff --git a/src/PVE/API2/Network/SDN/Zones/Makefile b/src/PVE/API2/Network/SDN/Zones/Makefile new file mode 100644 index 0000000..9b0a42b --- /dev/null +++ b/src/PVE/API2/Network/SDN/Zones/Makefile @@ -0,0 +1,8 @@ +SOURCES=Status.pm Content.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Zones/$$i; done diff --git a/src/PVE/API2/Network/SDN/Zones/Status.pm b/src/PVE/API2/Network/SDN/Zones/Status.pm new file mode 100644 index 0000000..17de68f --- /dev/null +++ b/src/PVE/API2/Network/SDN/Zones/Status.pm @@ -0,0 +1,111 @@ +package PVE::API2::Network::SDN::Zones::Status; + +use strict; +use warnings; + +use File::Path; +use File::Basename; +use PVE::Tools; +use PVE::INotify; +use PVE::Cluster; +use PVE::API2::Network::SDN::Zones::Content; +use PVE::RESTHandler; +use PVE::RPCEnvironment; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Exception qw(raise_param_exc); + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Zones::Content", + path => '{zone}/content', +}); + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "Get status for all zones.", + permissions => { + description => "Only list entries where you have 'SDN.Audit'", + user => 'all', + }, + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node') + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + zone => get_standard_option('pve-sdn-zone-id'), + status => { + description => "Status of zone", + type => 'string', + enum => ['available', 'pending', 'error'], + }, + }, + }, + links => [ { rel => 'child', href => "{zone}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $localnode = PVE::INotify::nodename(); + + my $res = []; + + my ($zone_status, $vnet_status) = PVE::Network::SDN::status(); + + foreach my $id (sort keys %{$zone_status}) { + my $item->{zone} = $id; + $item->{status} = $zone_status->{$id}->{'status'}; + push @$res, $item; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'diridx', + path => '{zone}', + method => 'GET', + description => "", + permissions => { + check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit'], any => 1], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + zone => get_standard_option('pve-sdn-zone-id'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + subdir => { type => 'string' }, + }, + }, + links => [ { rel => 'child', href => "{subdir}" } ], + }, + code => sub { + my ($param) = @_; + my $res = [ + { subdir => 'content' }, + ]; + + return $res; + }}); + +1; diff --git a/src/PVE/Makefile b/src/PVE/Makefile new file mode 100644 index 0000000..7f1cf98 --- /dev/null +++ b/src/PVE/Makefile @@ -0,0 +1,8 @@ +all: + +.PHONY: install +install: + make -C Network install + make -C API2 install + +clean: diff --git a/src/PVE/Network/Makefile b/src/PVE/Network/Makefile new file mode 100644 index 0000000..277e19c --- /dev/null +++ b/src/PVE/Network/Makefile @@ -0,0 +1,9 @@ +SOURCES=SDN.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/$$i; done + make -C SDN install diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm new file mode 100644 index 0000000..b95dd5b --- /dev/null +++ b/src/PVE/Network/SDN.pm @@ -0,0 +1,283 @@ +package PVE::Network::SDN; + +use strict; +use warnings; + +use Data::Dumper; +use JSON; + +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::Zones; +use PVE::Network::SDN::Controllers; +use PVE::Network::SDN::Subnets; + +use PVE::Tools qw(extract_param dir_glob_regex run_command); +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); + + +my $running_cfg = "sdn/.running-config"; + +my $parse_running_cfg = sub { + my ($filename, $raw) = @_; + + my $cfg = {}; + + return $cfg if !defined($raw) || $raw eq ''; + + eval { + $cfg = from_json($raw); + }; + return {} if $@; + + return $cfg; +}; + +my $write_running_cfg = sub { + my ($filename, $cfg) = @_; + + my $json = to_json($cfg); + + return $json; +}; + +PVE::Cluster::cfs_register_file($running_cfg, $parse_running_cfg, $write_running_cfg); + + +# improve me : move status code inside plugins ? + +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; +} + +sub status { + + my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status(); + return($zone_status, $vnet_status); +} + +sub running_config { + return cfs_read_file($running_cfg); +} + +sub pending_config { + my ($running_cfg, $cfg, $type) = @_; + + my $pending = {}; + + my $running_objects = $running_cfg->{$type}->{ids}; + my $config_objects = $cfg->{ids}; + + foreach my $id (sort keys %{$running_objects}) { + my $running_object = $running_objects->{$id}; + my $config_object = $config_objects->{$id}; + foreach my $key (sort keys %{$running_object}) { + $pending->{$id}->{$key} = $running_object->{$key}; + if(!keys %{$config_object}) { + $pending->{$id}->{state} = "deleted"; + } elsif (!defined($config_object->{$key})) { + $pending->{$id}->{"pending"}->{$key} = 'deleted'; + $pending->{$id}->{state} = "changed"; + } elsif (PVE::Network::SDN::encode_value(undef, $key, $running_object->{$key}) + ne PVE::Network::SDN::encode_value(undef, $key, $config_object->{$key})) { + $pending->{$id}->{state} = "changed"; + } + } + $pending->{$id}->{"pending"} = {} if $pending->{$id}->{state} && !defined($pending->{$id}->{"pending"}); + } + + foreach my $id (sort keys %{$config_objects}) { + my $running_object = $running_objects->{$id}; + my $config_object = $config_objects->{$id}; + + foreach my $key (sort keys %{$config_object}) { + my $config_value = PVE::Network::SDN::encode_value(undef, $key, $config_object->{$key}) if $config_object->{$key}; + my $running_value = PVE::Network::SDN::encode_value(undef, $key, $running_object->{$key}) if $running_object->{$key}; + if($key eq 'type' || $key eq 'vnet') { + $pending->{$id}->{$key} = $config_value; + } else { + $pending->{$id}->{"pending"}->{$key} = $config_value if !defined($running_value) || ($config_value ne $running_value); + } + if(!keys %{$running_object}) { + $pending->{$id}->{state} = "new"; + } elsif (!defined($running_value) && defined($config_value)) { + $pending->{$id}->{state} = "changed"; + } + } + $pending->{$id}->{"pending"} = {} if $pending->{$id}->{state} && !defined($pending->{$id}->{"pending"}); + } + + return {ids => $pending}; + +} + +sub commit_config { + + my $cfg = cfs_read_file($running_cfg); + my $version = $cfg->{version}; + + if ($version) { + $version++; + } else { + $version = 1; + } + + my $vnets_cfg = PVE::Network::SDN::Vnets::config(); + my $zones_cfg = PVE::Network::SDN::Zones::config(); + my $controllers_cfg = PVE::Network::SDN::Controllers::config(); + my $subnets_cfg = PVE::Network::SDN::Subnets::config(); + + my $vnets = { ids => $vnets_cfg->{ids} }; + my $zones = { ids => $zones_cfg->{ids} }; + my $controllers = { ids => $controllers_cfg->{ids} }; + my $subnets = { ids => $subnets_cfg->{ids} }; + + $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets }; + + cfs_write_file($running_cfg, $cfg); +} + +sub lock_sdn_config { + my ($code, $errmsg) = @_; + + cfs_lock_file($running_cfg, undef, $code); + + if (my $err = $@) { + $errmsg ? die "$errmsg: $err" : die $err; + } +} + +sub get_local_vnets { + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $nodename = PVE::INotify::nodename(); + + my $cfg = PVE::Network::SDN::running_config(); + my $vnets_cfg = $cfg->{vnets}; + my $zones_cfg = $cfg->{zones}; + + my @vnetids = PVE::Network::SDN::Vnets::sdn_vnets_ids($vnets_cfg); + + my $vnets = {}; + + foreach my $vnetid (@vnetids) { + + my $vnet = PVE::Network::SDN::Vnets::sdn_vnets_config($vnets_cfg, $vnetid); + my $zoneid = $vnet->{zone}; + my $comments = $vnet->{alias}; + + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + + next if !$zoneid; + next if !$rpcenv->check_any($authuser, "/sdn/zones/$zoneid", $privs, 1) && !$rpcenv->check_any($authuser, "/sdn/vnets/$vnetid", $privs, 1); + + my $zone_config = PVE::Network::SDN::Zones::sdn_zones_config($zones_cfg, $zoneid); + + next if defined($zone_config->{nodes}) && !$zone_config->{nodes}->{$nodename}; + my $ipam = $zone_config->{ipam} ? 1 : 0; + my $vlanaware = $vnet->{vlanaware} ? 1 : 0; + $vnets->{$vnetid} = { type => 'vnet', active => '1', ipam => $ipam, vlanaware => $vlanaware, comments => $comments }; + } + + return $vnets; +} + +sub generate_zone_config { + my $raw_config = PVE::Network::SDN::Zones::generate_etc_network_config(); + PVE::Network::SDN::Zones::write_etc_network_config($raw_config); +} + +sub generate_controller_config { + my ($reload) = @_; + + my $raw_config = PVE::Network::SDN::Controllers::generate_controller_config(); + PVE::Network::SDN::Controllers::write_controller_config($raw_config); + + PVE::Network::SDN::Controllers::reload_controller() if $reload; +} + +sub encode_value { + my ($type, $key, $value) = @_; + + if ($key eq 'nodes' || $key eq 'exitnodes') { + if(ref($value) eq 'HASH') { + return join(',', sort keys(%$value)); + } else { + return $value; + } + } + + return $value; +} + + +#helpers +sub api_request { + my ($method, $url, $headers, $data) = @_; + + my $encoded_data = to_json($data) if $data; + + my $req = HTTP::Request->new($method,$url, $headers, $encoded_data); + + my $ua = LWP::UserAgent->new(protocols_allowed => ['http', 'https'], timeout => 30); + my $proxy = undef; + + if ($proxy) { + $ua->proxy(['http', 'https'], $proxy); + } else { + $ua->env_proxy; + } + + $ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00); + + my $response = $ua->request($req); + my $code = $response->code; + + if ($code !~ /^2(\d+)$/) { + my $msg = $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } + + my $raw = ''; + if (defined($response->decoded_content)) { + $raw = $response->decoded_content; + } else { + $raw = $response->content; + } + + return if $raw eq ''; + + my $json = ''; + eval { + $json = from_json($raw); + }; + die "api response is not a json" if $@; + + return $json; +} + +1; diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm new file mode 100644 index 0000000..a23048e --- /dev/null +++ b/src/PVE/Network/SDN/Controllers.pm @@ -0,0 +1,181 @@ +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::EvpnPlugin; +use PVE::Network::SDN::Controllers::BgpPlugin; +use PVE::Network::SDN::Controllers::FaucetPlugin; +use PVE::Network::SDN::Controllers::Plugin; +PVE::Network::SDN::Controllers::EvpnPlugin->register(); +PVE::Network::SDN::Controllers::BgpPlugin->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 exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/controllers.cfg"); + $config = cfs_read_file("sdn/controllers.cfg") if !keys %{$config->{ids}}; + return $config; +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/controllers.cfg", $cfg); +} + +sub lock_sdn_controllers_config { + my ($code, $errmsg) = @_; + + cfs_lock_file("sdn/controllers.cfg", undef, $code); + if (my $err = $@) { + $errmsg ? die "$errmsg: $err" : die $err; + } +} + +sub sdn_controllers_ids { + my ($cfg) = @_; + + return sort keys %{$cfg->{ids}}; +} + +sub complete_sdn_controller { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::running_config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_controllers_ids($cfg) ]; +} + +sub generate_controller_config { + + my $cfg = PVE::Network::SDN::running_config(); + my $vnet_cfg = $cfg->{vnets}; + my $zone_cfg = $cfg->{zones}; + my $controller_cfg = $cfg->{controllers}; + + return if !$vnet_cfg && !$zone_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 (sort 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, $controller_cfg, $id, $uplinks, $config); + } + + foreach my $id (sort keys %{$zone_cfg->{ids}}) { + my $plugin_config = $zone_cfg->{ids}->{$id}; + my $controllerid = $plugin_config->{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_zone_config($plugin_config, $controller, $controller_cfg, $id, $uplinks, $config); + } + } + + foreach my $id (sort keys %{$vnet_cfg->{ids}}) { + my $plugin_config = $vnet_cfg->{ids}->{$id}; + my $zoneid = $plugin_config->{zone}; + next if !$zoneid; + my $zone = $zone_cfg->{ids}->{$zoneid}; + next if !$zone; + my $controllerid = $zone->{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, $zone, $zoneid, $id, $config); + } + } + + return $config; +} + + +sub reload_controller { + + my $cfg = PVE::Network::SDN::running_config(); + my $controller_cfg = $cfg->{controllers}; + + 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 generate_controller_rawconfig { + my ($config) = @_; + + my $cfg = PVE::Network::SDN::running_config(); + my $controller_cfg = $cfg->{controllers}; + return if !$controller_cfg; + + my $rawconfig = ""; + 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}); + $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config); + } + return $rawconfig; +} + +sub write_controller_config { + my ($config) = @_; + + my $cfg = PVE::Network::SDN::running_config(); + my $controller_cfg = $cfg->{controllers}; + 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; + diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm new file mode 100644 index 0000000..0b8cf1a --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm @@ -0,0 +1,183 @@ +package PVE::Network::SDN::Controllers::BgpPlugin; + +use strict; +use warnings; + +use PVE::INotify; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Tools qw(run_command file_set_contents file_get_contents); + +use PVE::Network::SDN::Controllers::Plugin; +use PVE::Network::SDN::Zones::Plugin; +use Net::IP; + +use base('PVE::Network::SDN::Controllers::Plugin'); + +sub type { + return 'bgp'; +} + +sub properties { + return { + 'bgp-multipath-as-path-relax' => { + type => 'boolean', + optional => 1, + }, + ebgp => { + type => 'boolean', + optional => 1, + description => "Enable ebgp. (remote-as external)", + }, + 'ebgp-multihop' => { + type => 'integer', + optional => 1, + }, + loopback => { + description => "source loopback interface.", + type => 'string' + }, + node => get_standard_option('pve-node'), + }; +} + +sub options { + return { + 'node' => { optional => 0 }, + 'asn' => { optional => 0 }, + 'peers' => { optional => 0 }, + 'bgp-multipath-as-path-relax' => { optional => 1 }, + 'ebgp' => { optional => 1 }, + 'ebgp-multihop' => { optional => 1 }, + 'loopback' => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_controller_config { + my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_; + + my @peers; + @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'}; + + my $asn = $plugin_config->{asn}; + my $ebgp = $plugin_config->{ebgp}; + my $ebgp_multihop = $plugin_config->{'ebgp-multihop'}; + my $loopback = $plugin_config->{loopback}; + my $multipath_relax = $plugin_config->{'bgp-multipath-as-path-relax'}; + + my $local_node = PVE::INotify::nodename(); + + + return if !$asn; + return if $local_node ne $plugin_config->{node}; + + my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {}; + + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + + my $remoteas = $ebgp ? "external" : $asn; + + #global options + my @controller_config = ( + "bgp router-id $ifaceip", + "no bgp default ipv4-unicast", + "coalesce-time 1000" + ); + + push(@{$bgp->{""}}, @controller_config) if keys %{$bgp} == 0; + + @controller_config = (); + if($ebgp) { + push @controller_config, "bgp disable-ebgp-connected-route-check" if $loopback; + } + + push @controller_config, "bgp bestpath as-path multipath-relax" if $multipath_relax; + + #BGP neighbors + if(@peers) { + push @controller_config, "neighbor BGP peer-group"; + push @controller_config, "neighbor BGP remote-as $remoteas"; + push @controller_config, "neighbor BGP bfd"; + push @controller_config, "neighbor BGP ebgp-multihop $ebgp_multihop" if $ebgp && $ebgp_multihop; + } + + # BGP peers + foreach my $address (@peers) { + push @controller_config, "neighbor $address peer-group BGP"; + } + push(@{$bgp->{""}}, @controller_config); + + # address-family unicast + if (@peers) { + my $ipversion = Net::IP::ip_is_ipv6($ifaceip) ? "ipv6" : "ipv4"; + my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "/128" : "32"; + + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "network $ifaceip/$mask") if $loopback; + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "neighbor BGP activate"); + push(@{$bgp->{"address-family"}->{"$ipversion unicast"}}, "neighbor BGP soft-reconfiguration inbound"); + } + + if ($loopback) { + $config->{frr_prefix_list}->{loopbacks_ips}->{10} = "permit 0.0.0.0/0 le 32"; + push(@{$config->{frr}->{''}}, "ip protocol bgp route-map correct_src"); + + my $routemap_config = (); + push @{$routemap_config}, "match ip address prefix-list loopbacks_ips"; + push @{$routemap_config}, "set src $ifaceip"; + my $routemap = { rule => $routemap_config, action => "permit" }; + push(@{$config->{frr_routemap}->{'correct_src'}}, $routemap); + } + + return $config; +} + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + +} + +sub on_delete_hook { + my ($class, $controllerid, $zone_cfg) = @_; + + # verify that zone is associated to this controller + foreach my $id (keys %{$zone_cfg->{ids}}) { + my $zone = $zone_cfg->{ids}->{$id}; + die "controller $controllerid is used by $id" + if (defined($zone->{controller}) && $zone->{controller} eq $controllerid); + } +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # we can only have 1 bgp controller by node + my $local_node = PVE::INotify::nodename(); + my $controllernb = 0; + foreach my $id (keys %{$controller_cfg->{ids}}) { + next if $id eq $controllerid; + my $controller = $controller_cfg->{ids}->{$id}; + next if $controller->{type} ne "bgp"; + next if $controller->{node} ne $local_node; + $controllernb++; + die "only 1 bgp controller can be defined" if $controllernb > 1; + } +} + +sub generate_controller_rawconfig { + my ($class, $plugin_config, $config) = @_; + return ""; +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + return; +} + +sub reload_controller { + my ($class) = @_; + return; +} + +1; + + diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm new file mode 100644 index 0000000..727aeaa --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -0,0 +1,542 @@ +package PVE::Network::SDN::Controllers::EvpnPlugin; + +use strict; +use warnings; + +use PVE::INotify; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Tools qw(run_command file_set_contents file_get_contents); + +use PVE::Network::SDN::Controllers::Plugin; +use PVE::Network::SDN::Zones::Plugin; +use Net::IP; + +use base('PVE::Network::SDN::Controllers::Plugin'); + +sub type { + return 'evpn'; +} + +sub properties { + return { + asn => { + type => 'integer', + description => "autonomous system number", + minimum => 0, + maximum => 4294967296 + }, + peers => { + description => "peers address list.", + type => 'string', format => 'ip-list' + }, + }; +} + +sub options { + return { + 'asn' => { optional => 0 }, + 'peers' => { optional => 0 }, + }; +} + +# Plugin implementation +sub generate_controller_config { + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; + + my @peers; + @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'}; + + my $local_node = PVE::INotify::nodename(); + + my $asn = $plugin_config->{asn}; + my $ebgp = undef; + my $loopback = undef; + my $autortas = undef; + my $bgprouter = find_bgp_controller($local_node, $controller_cfg); + if ($bgprouter) { + $ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn}; + $loopback = $bgprouter->{loopback} if $bgprouter->{loopback}; + $asn = $bgprouter->{asn} if $bgprouter->{asn}; + $autortas = $plugin_config->{'asn'} if $ebgp; + } + + return if !$asn; + + my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {}; + + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + + my $remoteas = $ebgp ? "external" : $asn; + + #global options + my @controller_config = ( + "bgp router-id $ifaceip", + "no bgp default ipv4-unicast", + "coalesce-time 1000", + ); + + push(@{$bgp->{""}}, @controller_config) if keys %{$bgp} == 0; + + @controller_config = (); + + #VTEP neighbors + push @controller_config, "neighbor VTEP peer-group"; + push @controller_config, "neighbor VTEP remote-as $remoteas"; + push @controller_config, "neighbor VTEP bfd"; + + if($ebgp && $loopback) { + push @controller_config, "neighbor VTEP ebgp-multihop 10"; + push @controller_config, "neighbor VTEP update-source $loopback"; + } + + # VTEP peers + foreach my $address (@peers) { + next if $address eq $ifaceip; + push @controller_config, "neighbor $address peer-group VTEP"; + } + + push(@{$bgp->{""}}, @controller_config); + + # address-family l2vpn + @controller_config = (); + push @controller_config, "neighbor VTEP route-map MAP_VTEP_IN in"; + push @controller_config, "neighbor VTEP route-map MAP_VTEP_OUT out"; + push @controller_config, "neighbor VTEP activate"; + push @controller_config, "advertise-all-vni"; + push @controller_config, "autort as $autortas" if $autortas; + push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + + my $routemap = { rule => undef, action => "permit" }; + push(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap ); + push(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap ); + + return $config; +} + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + my $local_node = PVE::INotify::nodename(); + + my $vrf = "vrf_$id"; + my $vrfvxlan = $plugin_config->{'vrf-vxlan'}; + my $exitnodes = $plugin_config->{'exitnodes'}; + my $exitnodes_primary = $plugin_config->{'exitnodes-primary'}; + my $advertisesubnets = $plugin_config->{'advertise-subnets'}; + my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'}; + my $rt_import; + $rt_import = [PVE::Tools::split_list($plugin_config->{'rt-import'})] if $plugin_config->{'rt-import'}; + + my $asn = $controller->{asn}; + my @peers; + @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'}; + my $ebgp = undef; + my $loopback = undef; + my $autortas = undef; + my $bgprouter = find_bgp_controller($local_node, $controller_cfg); + if($bgprouter) { + $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn}; + $loopback = $bgprouter->{loopback} if $bgprouter->{loopback}; + $asn = $bgprouter->{asn} if $bgprouter->{asn}; + $autortas = $controller->{'asn'} if $ebgp; + } + + return if !$vrf || !$vrfvxlan || !$asn; + + my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + + # vrf + my @controller_config = (); + push @controller_config, "vni $vrfvxlan"; + push(@{$config->{frr}->{vrf}->{"$vrf"}}, @controller_config); + + #main vrf router + @controller_config = (); + push @controller_config, "bgp router-id $ifaceip"; +# push @controller_config, "!"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, @controller_config); + + if ($autortas) { + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target import $autortas:$vrfvxlan"); + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, "route-target export $autortas:$vrfvxlan"); + } + + my $is_gateway = $exitnodes->{$local_node}; + + if ($is_gateway) { + + if (!$exitnodes_primary || $exitnodes_primary eq $local_node) { + #filter default type5 route coming from other exit nodes on primary node or both nodes if no primary is defined. + my $routemap_config = (); + push @{$routemap_config}, "match evpn route-type prefix"; + my $routemap = { rule => $routemap_config, action => "deny" }; + unshift(@{$config->{frr_routemap}->{'MAP_VTEP_IN'}}, $routemap); + } elsif ($exitnodes_primary ne $local_node) { + my $routemap_config = (); + push @{$routemap_config}, "match evpn vni $vrfvxlan"; + push @{$routemap_config}, "match evpn route-type prefix"; + push @{$routemap_config}, "set metric 200"; + my $routemap = { rule => $routemap_config, action => "permit" }; + unshift(@{$config->{frr_routemap}->{'MAP_VTEP_OUT'}}, $routemap); + } + + + if (!$exitnodes_local_routing) { + @controller_config = (); + #import /32 routes of evpn network from vrf1 to default vrf (for packet return) + push @controller_config, "import vrf $vrf"; + push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); + push(@{$config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + + @controller_config = (); + #redistribute connected to be able to route to local vms on the gateway + push @controller_config, "redistribute connected"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + } + + @controller_config = (); + #add default originate to announce 0.0.0.0/0 type5 route in evpn + push @controller_config, "default-originate ipv4"; + push @controller_config, "default-originate ipv6"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + } elsif ($advertisesubnets) { + + @controller_config = (); + #redistribute connected networks + push @controller_config, "redistribute connected"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv4 unicast"}}, @controller_config); + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"ipv6 unicast"}}, @controller_config); + + @controller_config = (); + #advertise connected networks type5 route in evpn + push @controller_config, "advertise ipv4 unicast"; + push @controller_config, "advertise ipv6 unicast"; + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + } + + if ($rt_import) { + @controller_config = (); + foreach my $rt (sort @{$rt_import}) { + push @controller_config, "route-target import $rt"; + } + push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}->{"l2vpn evpn"}}, @controller_config); + } + + return $config; +} + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_; + + my $exitnodes = $zone->{'exitnodes'}; + my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'}; + + return if !$exitnodes_local_routing; + + my $local_node = PVE::INotify::nodename(); + my $is_gateway = $exitnodes->{$local_node}; + + return if !$is_gateway; + + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + my @controller_config = (); + foreach my $subnetid (sort keys %{$subnets}) { + my $subnet = $subnets->{$subnetid}; + my $cidr = $subnet->{cidr}; + push @controller_config, "ip route $cidr 10.255.255.2 xvrf_$zoneid"; + } + push(@{$config->{frr}->{''}}, @controller_config); +} + +sub on_delete_hook { + my ($class, $controllerid, $zone_cfg) = @_; + + # verify that zone is associated to this controller + foreach my $id (keys %{$zone_cfg->{ids}}) { + my $zone = $zone_cfg->{ids}->{$id}; + die "controller $controllerid is used by $id" + if (defined($zone->{controller}) && $zone->{controller} eq $controllerid); + } +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # we can only have 1 evpn controller / 1 asn by server + + my $controllernb = 0; + foreach my $id (keys %{$controller_cfg->{ids}}) { + next if $id eq $controllerid; + my $controller = $controller_cfg->{ids}->{$id}; + next if $controller->{type} ne "evpn"; + $controllernb++; + die "only 1 global evpn controller can be defined" if $controllernb >= 1; + } +} + +sub find_bgp_controller { + my ($nodename, $controller_cfg) = @_; + + my $controller = undef; + foreach my $id (keys %{$controller_cfg->{ids}}) { + $controller = $controller_cfg->{ids}->{$id}; + next if $controller->{type} ne 'bgp'; + next if $controller->{node} ne $nodename; + last; + } + + return $controller; +} + + +sub sort_frr_config { + my $order = {}; + $order->{''} = 0; + $order->{'vrf'} = 1; + $order->{'ipv4 unicast'} = 1; + $order->{'ipv6 unicast'} = 2; + $order->{'l2vpn evpn'} = 3; + + my $a_val = 100; + my $b_val = 100; + + $a_val = $order->{$a} if defined($order->{$a}); + $b_val = $order->{$b} if defined($order->{$b}); + + if ($a =~ /bgp (\d+)$/) { + $a_val = 2; + } + + if ($b =~ /bgp (\d+)$/) { + $b_val = 2; + } + + return $a_val <=> $b_val; +} + +sub generate_frr_recurse{ + my ($final_config, $content, $parentkey, $level) = @_; + + my $keylist = {}; + $keylist->{vrf} = 1; + $keylist->{'address-family'} = 1; + $keylist->{router} = 1; + + my $exitkeylist = {}; + $exitkeylist->{vrf} = 1; + $exitkeylist->{'address-family'} = 1; + + my $simple_exitkeylist = {}; + $simple_exitkeylist->{router} = 1; + + # FIXME: make this generic + my $paddinglevel = undef; + if ($level == 1 || $level == 2) { + $paddinglevel = $level - 1; + } elsif ($level == 3 || $level == 4) { + $paddinglevel = $level - 2; + } + + my $padding = ""; + $padding = ' ' x ($paddinglevel) if $paddinglevel; + + if (ref $content eq 'HASH') { + foreach my $key (sort sort_frr_config keys %$content) { + if ($parentkey && defined($keylist->{$parentkey})) { + push @{$final_config}, $padding."!"; + push @{$final_config}, $padding."$parentkey $key"; + } elsif ($key ne '' && !defined($keylist->{$key})) { + push @{$final_config}, $padding."$key"; + } + + my $option = $content->{$key}; + generate_frr_recurse($final_config, $option, $key, $level+1); + + push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey}); + push @{$final_config}, $padding."exit" if $parentkey && defined($simple_exitkeylist->{$parentkey}); + } + } + + if (ref $content eq 'ARRAY') { + push @{$final_config}, map { $padding . "$_" } @$content; + } +} + +sub generate_frr_routemap { + my ($final_config, $routemaps) = @_; + + foreach my $id (sort keys %$routemaps) { + + my $routemap = $routemaps->{$id}; + my $order = 0; + foreach my $seq (@$routemap) { + $order++; + next if !defined($seq->{action}); + my @config = (); + push @config, "!"; + push @config, "route-map $id $seq->{action} $order"; + my $rule = $seq->{rule}; + push @config, map { " $_" } @$rule; + push @{$final_config}, @config; + push @{$final_config}, "exit"; + } + } +} + +sub generate_frr_list { + my ($final_config, $lists, $type) = @_; + + my $config = []; + + for my $id (sort keys %$lists) { + my $list = $lists->{$id}; + + for my $seq (sort keys %$list) { + my $rule = $list->{$seq}; + push @$config, "$type $id seq $seq $rule"; + } + } + + if (@$config > 0) { + push @{$final_config}, "!", @$config; + } +} + +sub generate_controller_rawconfig { + my ($class, $plugin_config, $config) = @_; + + my $nodename = PVE::INotify::nodename(); + + my $final_config = []; + push @{$final_config}, "frr version 8.2.2"; + push @{$final_config}, "frr defaults datacenter"; + push @{$final_config}, "hostname $nodename"; + push @{$final_config}, "log syslog informational"; + push @{$final_config}, "service integrated-vtysh-config"; + push @{$final_config}, "!"; + + if (-e "/etc/frr/frr.conf.local") { + my $local_conf = file_get_contents("/etc/frr/frr.conf.local"); + parse_merge_frr_local_config($config, $local_conf); + } + + generate_frr_recurse($final_config, $config->{frr}, undef, 0); + generate_frr_list($final_config, $config->{frr_access_list}, "access-list"); + generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list"); + generate_frr_routemap($final_config, $config->{frr_routemap}); + + push @{$final_config}, "!"; + push @{$final_config}, "line vty"; + push @{$final_config}, "!"; + + my $rawconfig = join("\n", @{$final_config}); + + return if !$rawconfig; + return $rawconfig; +} + +sub parse_merge_frr_local_config { + my ($config, $local_conf) = @_; + + my $section = \$config->{""}; + my $router = undef; + my $routemap = undef; + my $routemap_config = (); + my $routemap_action = undef; + + while ($local_conf =~ /^\s*(.+?)\s*$/gm) { + my $line = $1; + $line =~ s/^\s+|\s+$//g; + + if ($line =~ m/^router (.+)$/) { + $router = $1; + $section = \$config->{'frr'}->{'router'}->{$router}->{""}; + next; + } elsif ($line =~ m/^vrf (.+)$/) { + $section = \$config->{'frr'}->{'vrf'}->{$1}; + next; + } elsif ($line =~ m/address-family (.+)$/) { + $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1}; + next; + } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) { + $routemap = $1; + $routemap_config = (); + $routemap_action = $2; + $section = \$config->{'frr_routemap'}->{$routemap}; + next; + } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) { + $config->{'frr_access_list'}->{$1}->{$2} = $3; + next; + } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) { + $config->{'frr_prefix_list'}->{$1}->{$2} = $3; + next; + } elsif($line =~ m/^exit-address-family$/) { + next; + } elsif($line =~ m/^exit$/) { + if($router) { + $section = \$config->{''}; + $router = undef; + } elsif($routemap) { + push(@{$$section}, { rule => $routemap_config, action => $routemap_action }); + $section = \$config->{''}; + $routemap = undef; + $routemap_action = undef; + $routemap_config = (); + } + next; + } elsif($line =~ m/!/) { + next; + } + + next if !$section; + if($routemap) { + push(@{$routemap_config}, $line); + } else { + push(@{$$section}, $line); + } + } +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config); + return if !$rawconfig; + return if !-d "/etc/frr"; + + file_set_contents("/etc/frr/frr.conf", $rawconfig); +} + +sub reload_controller { + my ($class) = @_; + + my $conf_file = "/etc/frr/frr.conf"; + my $bin_path = "/usr/lib/frr/frr-reload.py"; + + if (!-e $bin_path) { + warn "missing $bin_path. Please install frr-pythontools package"; + return; + } + + my $err = sub { + my $line = shift; + if ($line =~ /ERROR:/) { + warn "$line \n"; + } + }; + + if (-e $conf_file && -e $bin_path) { + eval { + run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err); + }; + if ($@) { + warn "frr reload command fail. Restarting frr."; + eval { run_command(['systemctl', 'restart', 'frr']); }; + } + } +} + +1; + + diff --git a/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm b/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm new file mode 100644 index 0000000..4f3bb5c --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/FaucetPlugin.pm @@ -0,0 +1,97 @@ +package PVE::Network::SDN::Controllers::FaucetPlugin; + +use strict; +use warnings; +use PVE::Network::SDN::Controllers::Plugin; +use PVE::Tools; +use PVE::INotify; +use PVE::JSONSchema qw(get_standard_option); +use CPAN::Meta::YAML; +use Encode; + +use base('PVE::Network::SDN::Controllers::Plugin'); + +sub type { + return 'faucet'; +} + +sub properties { + return { + }; +} + +# Plugin implementation +sub generate_controller_config { + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; + +} + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + my $dpid = $plugin_config->{'dp-id'}; + my $dphex = printf("%x",$dpid); + + my $zone_config = { + dp_id => $dphex, + hardware => "Open vSwitch", + }; + + $config->{faucet}->{dps}->{$id} = $zone_config; + +} + + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_; + + my $mac = $plugin_config->{mac}; + my $ipv4 = $plugin_config->{ipv4}; + my $ipv6 = $plugin_config->{ipv6}; + my $tag = $plugin_config->{tag}; + my $alias = $plugin_config->{alias}; + + my @ips = (); + push @ips, $ipv4 if $ipv4; + push @ips, $ipv6 if $ipv6; + + my $vlan_config = { vid => $tag }; + + $vlan_config->{description} = $alias if $alias; + $vlan_config->{faucet_mac} = $mac if $mac; + $vlan_config->{faucet_vips} = \@ips if scalar @ips > 0; + + $config->{faucet}->{vlans}->{$vnetid} = $vlan_config; + + push(@{$config->{faucet}->{routers}->{$zoneid}->{vlans}} , $vnetid); + +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + my $rawconfig = encode('UTF-8', CPAN::Meta::YAML::Dump($config->{faucet})); + + return if !$rawconfig; + return if !-d "/etc/faucet"; + + my $frr_config_file = "/etc/faucet/faucet.yaml"; + + my $writefh = IO::File->new($frr_config_file,">"); + print $writefh $rawconfig; + $writefh->close(); +} + +sub reload_controller { + my ($class) = @_; + + my $conf_file = "/etc/faucet/faucet.yaml"; + my $bin_path = "/usr/bin/faucet"; + + if (-e $conf_file && -e $bin_path) { + PVE::Tools::run_command(['systemctl', 'reload', 'faucet']); + } +} + +1; + diff --git a/src/PVE/Network/SDN/Controllers/Makefile b/src/PVE/Network/SDN/Controllers/Makefile new file mode 100644 index 0000000..11686a3 --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Controllers/$$i; done diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm new file mode 100644 index 0000000..c1c2cfd --- /dev/null +++ b/src/PVE/Network/SDN/Controllers/Plugin.pm @@ -0,0 +1,121 @@ +package PVE::Network::SDN::Controllers::Plugin; + +use strict; +use warnings; + +use PVE::Tools; +use PVE::JSONSchema; +use PVE::Cluster; + +use Data::Dumper; +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/controllers.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); } +); + +PVE::JSONSchema::register_standard_option('pve-sdn-controller-id', { + description => "The SDN controller object identifier.", + type => 'string', format => 'pve-sdn-controller-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-controller-id', \&parse_sdn_controller_id); +sub parse_sdn_controller_id { + my ($id, $noerr) = @_; + + if ($id !~ m/^[a-z][a-z0-9_-]*[a-z0-9]$/i) { + return undef if $noerr; + die "controller ID '$id' contains illegal characters\n"; + } + die "controller ID '$id' can't be more length than 64 characters\n" if length($id) > 64; + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + type => 'string', + }, + controller => get_standard_option('pve-sdn-controller-id', + { completion => \&PVE::Network::SDN::complete_sdn_controller }), + }, +}; + +sub private { + return $defaultData; +} + +sub parse_section_header { + my ($class, $line) = @_; + + if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { + my ($type, $id) = (lc($1), $2); + my $errmsg = undef; # set if you want to skip whole section + eval { PVE::JSONSchema::pve_verify_configid($type); }; + $errmsg = $@ if $@; + my $config = {}; # to return additional attributes + return ($type, $id, $errmsg, $config); + } + return undef; +} + +sub generate_sdn_config { + my ($class, $plugin_config, $node, $data, $ctime) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_config { + my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_; + + die "please implement inside plugin"; +} + + +sub generate_controller_zone_config { + my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zoneid, $vnetid, $config) = @_; + +} + +sub generate_controller_rawconfig { + my ($class, $plugin_config, $config) = @_; + + die "please implement inside plugin"; +} + +sub write_controller_config { + my ($class, $plugin_config, $config) = @_; + + die "please implement inside plugin"; +} + +sub controller_reload { + my ($class) = @_; + + die "please implement inside plugin"; +} + +sub on_delete_hook { + my ($class, $controllerid, $zone_cfg) = @_; + + # do nothing by default +} + +sub on_update_hook { + my ($class, $controllerid, $controller_cfg) = @_; + + # do nothing by default +} + +1; diff --git a/src/PVE/Network/SDN/Dns.pm b/src/PVE/Network/SDN/Dns.pm new file mode 100644 index 0000000..c2e153a --- /dev/null +++ b/src/PVE/Network/SDN/Dns.pm @@ -0,0 +1,57 @@ +package PVE::Network::SDN::Dns; + +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; + +use PVE::Network::SDN::Dns::PowerdnsPlugin; +use PVE::Network::SDN::Dns::Plugin; + +PVE::Network::SDN::Dns::PowerdnsPlugin->register(); +PVE::Network::SDN::Dns::Plugin->init(); + + +sub sdn_dns_config { + my ($cfg, $id, $noerr) = @_; + + die "no sdn dns ID specified\n" if !$id; + + my $scfg = $cfg->{ids}->{$id}; + die "sdn '$id' does not exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/dns.cfg"); + return $config; +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/dns.cfg", $cfg); +} + +sub sdn_dns_ids { + my ($cfg) = @_; + + return keys %{$cfg->{ids}}; +} + +sub complete_sdn_dns { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::Dns::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Dns::sdn_dns_ids($cfg) ]; +} + +1; + diff --git a/src/PVE/Network/SDN/Dns/Makefile b/src/PVE/Network/SDN/Dns/Makefile new file mode 100644 index 0000000..81cd2a1 --- /dev/null +++ b/src/PVE/Network/SDN/Dns/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm PowerdnsPlugin.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Dns/$$i; done diff --git a/src/PVE/Network/SDN/Dns/Plugin.pm b/src/PVE/Network/SDN/Dns/Plugin.pm new file mode 100644 index 0000000..07d0be1 --- /dev/null +++ b/src/PVE/Network/SDN/Dns/Plugin.pm @@ -0,0 +1,109 @@ +package PVE::Network::SDN::Dns::Plugin; + +use strict; +use warnings; + +use PVE::Tools qw(run_command); +use PVE::JSONSchema; +use PVE::Cluster; +use HTTP::Request; +use LWP::UserAgent; + +use Data::Dumper; +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/dns.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); }); + +PVE::JSONSchema::register_standard_option('pve-sdn-dns-id', { + description => "The SDN dns object identifier.", + type => 'string', format => 'pve-sdn-dns-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-dns-id', \&parse_sdn_dns_id); +sub parse_sdn_dns_id { + my ($id, $noerr) = @_; + + if ($id !~ m/^[a-z][a-z0-9]*[a-z0-9]$/i) { + return undef if $noerr; + die "dns ID '$id' contains illegal characters\n"; + } + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + }, + ttl => { type => 'integer', optional => 1 }, + reversev6mask => { type => 'integer', optional => 1 }, + dns => get_standard_option('pve-sdn-dns-id', + { completion => \&PVE::Network::SDN::Dns::complete_sdn_dns }), + }, +}; + +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 add_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub add_ptr_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub del_ptr_record { + my ($class, $plugin_config, $zone, $ip, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub del_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub verify_zone { + my ($class, $plugin_config, $zone, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub get_reversedns_zone { + my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_; + + die "please implement inside plugin"; +} + +sub on_update_hook { + my ($class, $plugin_config) = @_; +} + +1; diff --git a/src/PVE/Network/SDN/Dns/PowerdnsPlugin.pm b/src/PVE/Network/SDN/Dns/PowerdnsPlugin.pm new file mode 100644 index 0000000..096d131 --- /dev/null +++ b/src/PVE/Network/SDN/Dns/PowerdnsPlugin.pm @@ -0,0 +1,329 @@ +package PVE::Network::SDN::Dns::PowerdnsPlugin; + +use strict; +use warnings; +use PVE::INotify; +use PVE::Cluster; +use PVE::Tools; +use JSON; +use Net::IP; +use NetAddr::IP qw(:lower); +use base('PVE::Network::SDN::Dns::Plugin'); + +sub type { + return 'powerdns'; +} + +sub properties { + return { + url => { + type => 'string', + }, + key => { + type => 'string', + }, + reversemaskv6 => { + type => 'integer' + }, + }; +} + +sub options { + + return { + url => { optional => 0}, + key => { optional => 0 }, + ttl => { optional => 1 }, + reversemaskv6 => { optional => 1, description => "force a different netmask for the ipv6 reverse zone name." }, + + }; +} + +# Plugin implementation + +sub add_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $ttl = $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + + my $type = Net::IP::ip_is_ipv6($ip) ? "AAAA" : "A"; + my $fqdn = $hostname.".".$zone."."; + + my $zonecontent = get_zone_content($plugin_config, $zone); + my $existing_rrset = get_zone_rrset($zonecontent, $fqdn); + + my $final_records = []; + my $foundrecord = undef; + foreach my $record (@{$existing_rrset->{records}}) { + if($record->{content} eq $ip) { + $foundrecord = 1; + next; + } + push @$final_records, $record; + } + return if $foundrecord; + + my $record = { content => $ip, + disabled => JSON::false, + name => $fqdn, + type => $type, + priority => 0 }; + + push @$final_records, $record; + + my $rrset = { name => $fqdn, + type => $type, + ttl => $ttl, + changetype => "REPLACE", + records => $final_records }; + + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error add $fqdn to zone $zone: $@" if !$noerr; + } +} + +sub add_ptr_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $ttl = $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + $hostname .= "."; + + my $reverseip = Net::IP->new($ip)->reverse_ip(); + + my $type = "PTR"; + + my $record = { content => $hostname, + disabled => JSON::false, + name => $reverseip, + type => $type, + priority => 0 }; + + my $rrset = { name => $reverseip, + type => $type, + ttl => $ttl, + changetype => "REPLACE", + records => [ $record ] }; + + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error add $reverseip to zone $zone: $@" if !$noerr; + } +} + +sub del_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_; + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + my $fqdn = $hostname.".".$zone."."; + my $type = Net::IP::ip_is_ipv6($ip) ? "AAAA" : "A"; + + my $zonecontent = get_zone_content($plugin_config, $zone); + my $existing_rrset = get_zone_rrset($zonecontent, $fqdn); + + my $final_records = []; + my $foundrecord = undef; + foreach my $record (@{$existing_rrset->{records}}) { + if ($record->{content} eq $ip) { + $foundrecord = 1; + next; + } + push @$final_records, $record; + } + return if !$foundrecord; + + my $rrset = {}; + + if (scalar (@{$final_records}) > 0) { + #if we still have other records, we rewrite them without removed ip + $rrset = { name => $fqdn, + type => $type, + ttl => $existing_rrset->{ttl}, + changetype => "REPLACE", + records => $final_records }; + + } else { + + $rrset = { name => $fqdn, + type => $type, + changetype => "DELETE", + records => [] }; + } + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error delete $fqdn from zone $zone: $@" if !$noerr; + } +} + +sub del_ptr_record { + my ($class, $plugin_config, $zone, $ip, $noerr) = @_; + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + + my $reverseip = Net::IP->new($ip)->reverse_ip(); + + my $type = "PTR"; + + my $rrset = { name => $reverseip, + type => $type, + changetype => "DELETE", + records => [] }; + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error delete $reverseip from zone $zone: $@" if !$noerr; + } +} + +sub verify_zone { + my ($class, $plugin_config, $zone, $noerr) = @_; + + #verify that api is working + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + + eval { + PVE::Network::SDN::api_request("GET", "$url/zones/$zone?rrsets=false", $headers); + }; + + if ($@) { + die "can't read zone $zone: $@" if !$noerr; + } +} + +sub get_reversedns_zone { + my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_; + + my $cidr = $subnet->{cidr}; + my $mask = $subnet->{mask}; + + my $zone = ""; + + if (Net::IP::ip_is_ipv4($ip)) { + my ($ipblock1, $ipblock2, $ipblock3, $ipblock4) = split(/\./, $ip); + + my $ipv4 = NetAddr::IP->new($cidr); + #private addresse #powerdns built-in private zone : serve-rfc1918 + if($ipv4->is_rfc1918()) { + if ($ipblock1 == 192) { + $zone = "168.192.in-addr.arpa."; + } elsif ($ipblock1 == 172) { + $zone = "16-31.172.in-addr.arpa."; + } elsif ($ipblock1 == 10) { + $zone = "10.in-addr.arpa."; + } + + } else { + #public ipv4 : RIPE,ARIN,AFRNIC + #. Delegations can be managed in IPv4 on bit boundaries (/8, /16 or /24s), and IPv6 networks can be managed on nibble boundaries (every 4 bits of the IPv6 address) + #One or more /24 type zones need to be created if your address space has a prefix length between /17 and /24. + # If your prefix length is between /16 and /9 you will have to request one or more delegations for /16 type zones. + + if ($mask <= 24) { + $zone = "$ipblock3.$ipblock2.$ipblock1.in-addr.arpa."; + } elsif ($mask <= 16) { + $zone = "$ipblock2.$ipblock1.in-addr.arpa."; + } elsif ($mask <= 8) { + $zone = "$ipblock1.in-addr.arpa."; + } + } + } else { + $mask = $plugin_config->{reversemaskv6} if $plugin_config->{reversemaskv6}; + die "reverse dns zone mask need to be a multiple of 4" if ($mask % 4); + my $networkv6 = NetAddr::IP->new($cidr)->network(); + $zone = Net::IP->new($networkv6)->reverse_ip(); + } + + return $zone; +} + + +sub on_update_hook { + my ($class, $plugin_config) = @_; + + #verify that api is working + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + + eval { + PVE::Network::SDN::api_request("GET", "$url", $headers); + }; + + if ($@) { + die "dns api error: $@"; + } +} + + +sub get_zone_content { + my ($plugin_config, $zone) = @_; + + #verify that api is working + + my $url = $plugin_config->{url}; + my $key = $plugin_config->{key}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key]; + + my $result = undef; + eval { + $result = PVE::Network::SDN::api_request("GET", "$url/zones/$zone", $headers); + }; + + if ($@) { + die "can't read zone $zone: $@"; + } + return $result; +} + +sub get_zone_rrset { + my ($zonecontent, $name) = @_; + + my $rrsetresult = undef; + foreach my $rrset (@{$zonecontent->{rrsets}}) { + next if $rrset->{name} ne $name; + $rrsetresult = $rrset; + last; + } + return $rrsetresult; +} + +1; + + diff --git a/src/PVE/Network/SDN/Ipams.pm b/src/PVE/Network/SDN/Ipams.pm new file mode 100644 index 0000000..e8a4b0b --- /dev/null +++ b/src/PVE/Network/SDN/Ipams.pm @@ -0,0 +1,69 @@ +package PVE::Network::SDN::Ipams; + +use strict; +use warnings; + +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; + +use PVE::Network::SDN::Ipams::PVEPlugin; +use PVE::Network::SDN::Ipams::NetboxPlugin; +use PVE::Network::SDN::Ipams::PhpIpamPlugin; +use PVE::Network::SDN::Ipams::Plugin; + +PVE::Network::SDN::Ipams::PVEPlugin->register(); +PVE::Network::SDN::Ipams::NetboxPlugin->register(); +PVE::Network::SDN::Ipams::PhpIpamPlugin->register(); +PVE::Network::SDN::Ipams::Plugin->init(); + + +sub sdn_ipams_config { + my ($cfg, $id, $noerr) = @_; + + die "no sdn ipam ID specified\n" if !$id; + + my $scfg = $cfg->{ids}->{$id}; + die "sdn '$id' does not exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/ipams.cfg"); + #add default internal pve + $config->{ids}->{pve}->{type} = 'pve'; + return $config; +} + +sub get_plugin_config { + my ($vnet) = @_; + my $ipamid = $vnet->{ipam}; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + return $ipam_cfg->{ids}->{$ipamid}; +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/ipams.cfg", $cfg); +} + +sub sdn_ipams_ids { + my ($cfg) = @_; + + return keys %{$cfg->{ids}}; +} + +sub complete_sdn_vnet { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::Ipams::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Vnets::sdn_ipams_ids($cfg) ]; +} + +1; + diff --git a/src/PVE/Network/SDN/Ipams/Makefile b/src/PVE/Network/SDN/Ipams/Makefile new file mode 100644 index 0000000..4e7d65f --- /dev/null +++ b/src/PVE/Network/SDN/Ipams/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm PhpIpamPlugin.pm NetboxPlugin.pm PVEPlugin.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Ipams/$$i; done diff --git a/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm b/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm new file mode 100644 index 0000000..f0e7168 --- /dev/null +++ b/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm @@ -0,0 +1,226 @@ +package PVE::Network::SDN::Ipams::NetboxPlugin; + +use strict; +use warnings; +use PVE::INotify; +use PVE::Cluster; +use PVE::Tools; + +use base('PVE::Network::SDN::Ipams::Plugin'); + +sub type { + return 'netbox'; +} + +sub properties { + return { + }; +} + +sub options { + + return { + url => { optional => 0}, + token => { optional => 0 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $gateway = $subnet->{gateway}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + + my $params = { prefix => $cidr }; + + eval { + my $result = PVE::Network::SDN::api_request("POST", "$url/ipam/prefixes/", $headers, $params); + }; + if ($@) { + die "error add subnet to ipam: $@" if !$noerr; + } + } + +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $gateway = $subnet->{gateway}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + return if !$internalid; + + return; #fixme: check that prefix is empty exluding gateway, before delete + + eval { + PVE::Network::SDN::api_request("DELETE", "$url/ipam/prefixes/$internalid/", $headers); + }; + if ($@) { + die "error deleting subnet from ipam: $@" if !$noerr; + } + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + + my $mask = $subnet->{mask}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + $description .= " mac:$mac" if $mac && $description; + + my $params = { address => "$ip/$mask", dns_name => $hostname, description => $description }; + + eval { + PVE::Network::SDN::api_request("POST", "$url/ipam/ip-addresses/", $headers, $params); + }; + + if ($@) { + if($is_gateway) { + die "error add subnet ip to ipam: ip $ip already exist: $@" if !is_ip_gateway($url, $ip, $headers) && !$noerr; + } else { + die "error add subnet ip to ipam: ip already exist: $@" if !$noerr; + } + } +} + +sub update_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + + my $mask = $subnet->{mask}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + $description .= " mac:$mac" if $mac && $description; + + my $params = { address => "$ip/$mask", dns_name => $hostname, description => $description }; + + my $ip_id = get_ip_id($url, $ip, $headers); + die "can't find ip $ip in ipam" if !$ip_id; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/ipam/ip-addresses/$ip_id/", $headers, $params); + }; + if ($@) { + die "error update ip $ip : $@" if !$noerr; + } +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + $description .= " mac:$mac" if $mac && $description; + + my $params = { dns_name => $hostname, description => $description }; + + my $ip = undef; + eval { + my $result = PVE::Network::SDN::api_request("POST", "$url/ipam/prefixes/$internalid/available-ips/", $headers, $params); + $ip = $result->{address}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@" if !$noerr; + } + + return $ip; +} + +sub del_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_; + + return if !$ip; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $ip_id = get_ip_id($url, $ip, $headers); + die "can't find ip $ip in ipam" if !$ip_id; + + eval { + PVE::Network::SDN::api_request("DELETE", "$url/ipam/ip-addresses/$ip_id/", $headers); + }; + if ($@) { + die "error delete ip $ip : $@" if !$noerr; + } +} + +sub verify_api { + my ($class, $plugin_config) = @_; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + + eval { + PVE::Network::SDN::api_request("GET", "$url/ipam/aggregates/", $headers); + }; + if ($@) { + die "Can't connect to netbox api: $@"; + } +} + +sub on_update_hook { + my ($class, $plugin_config) = @_; + + PVE::Network::SDN::Ipams::NetboxPlugin::verify_api($class, $plugin_config); +} + +#helpers + +sub get_prefix_id { + my ($url, $cidr, $headers) = @_; + + my $result = PVE::Network::SDN::api_request("GET", "$url/ipam/prefixes/?q=$cidr", $headers); + my $data = @{$result->{results}}[0]; + my $internalid = $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::api_request("GET", "$url/ipam/ip-addresses/?q=$ip", $headers); + my $data = @{$result->{results}}[0]; + my $ip_id = $data->{id}; + return $ip_id; +} + +sub is_ip_gateway { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::api_request("GET", "$url/addresses/search/$ip", $headers); + my $data = @{$result->{data}}[0]; + my $description = $data->{description}; + my $is_gateway = 1 if $description eq 'gateway'; + return $is_gateway; +} + +1; + + diff --git a/src/PVE/Network/SDN/Ipams/PVEPlugin.pm b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm new file mode 100644 index 0000000..3e8ffc5 --- /dev/null +++ b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm @@ -0,0 +1,210 @@ +package PVE::Network::SDN::Ipams::PVEPlugin; + +use strict; +use warnings; +use PVE::INotify; +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_register_file cfs_lock_file); +use PVE::Tools; +use JSON; +use NetAddr::IP qw(:lower); + +use Net::IP; +use Digest::SHA; + +use base('PVE::Network::SDN::Ipams::Plugin'); + + +my $ipamdb_file = "priv/ipam.db"; + +PVE::Cluster::cfs_register_file($ipamdb_file, + sub { PVE::Network::SDN::Ipams::PVEPlugin->parse_config(@_); }, + sub { PVE::Network::SDN::Ipams::PVEPlugin->write_config(@_); }); + +sub type { + return 'pve'; +} + +sub properties { +} + +sub options { +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnet->{cidr}; + my $zone = $subnet->{zone}; + my $gateway = $subnet->{gateway}; + + + cfs_lock_file($ipamdb_file, undef, sub { + my $db = {}; + $db = read_db(); + + $db->{zones}->{$zone} = {} if !$db->{zones}->{$zone}; + my $zonedb = $db->{zones}->{$zone}; + + if(!$zonedb->{subnets}->{$cidr}) { + #create subnet + $zonedb->{subnets}->{$cidr}->{ips} = {}; + write_db($db); + } + }); + die "$@" if $@; +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnet->{cidr}; + my $zone = $subnet->{zone}; + + cfs_lock_file($ipamdb_file, undef, sub { + + my $db = read_db(); + + my $dbzone = $db->{zones}->{$zone}; + die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone; + my $dbsubnet = $dbzone->{subnets}->{$cidr}; + die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet; + + die "cannot delete subnet '$cidr', not empty\n" if keys %{$dbsubnet->{ips}} > 0; + + delete $dbzone->{subnets}->{$cidr}; + + write_db($db); + }); + die "$@" if $@; + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway) = @_; + + my $cidr = $subnet->{cidr}; + my $zone = $subnet->{zone}; + + cfs_lock_file($ipamdb_file, undef, sub { + + my $db = read_db(); + my $dbzone = $db->{zones}->{$zone}; + die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone; + my $dbsubnet = $dbzone->{subnets}->{$cidr}; + die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet; + + die "IP '$ip' already exist\n" if (!$is_gateway && defined($dbsubnet->{ips}->{$ip})) || ($is_gateway && defined($dbsubnet->{ips}->{$ip}) && !defined($dbsubnet->{ips}->{$ip}->{gateway})); + $dbsubnet->{ips}->{$ip} = {}; + $dbsubnet->{ips}->{$ip} = {gateway => 1} if $is_gateway; + + write_db($db); + }); + die "$@" if $@; +} + +sub update_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway) = @_; + return; +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description) = @_; + + my $cidr = $subnet->{cidr}; + my $network = $subnet->{network}; + my $zone = $subnet->{zone}; + my $mask = $subnet->{mask}; + my $freeip = undef; + + cfs_lock_file($ipamdb_file, undef, sub { + + my $db = read_db(); + my $dbzone = $db->{zones}->{$zone}; + die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone; + my $dbsubnet = $dbzone->{subnets}->{$cidr}; + die "subnet '$cidr' doesn't exist in IPAM DB" if !$dbsubnet; + + if (Net::IP::ip_is_ipv4($network) && $mask == 32) { + die "cannot find free IP in subnet '$cidr'\n" if defined($dbsubnet->{ips}->{$network}); + $freeip = $network; + } else { + my $iplist = NetAddr::IP->new($cidr); + my $lastip = $iplist->last()->canon(); + $iplist++ if Net::IP::ip_is_ipv4($network); #skip network address for ipv4 + while(1) { + my $ip = $iplist->canon(); + if (defined($dbsubnet->{ips}->{$ip})) { + last if $ip eq $lastip; + $iplist++; + next; + } + $freeip = $ip; + last; + } + } + + die "can't find free ip in subnet '$cidr'\n" if !$freeip; + + $dbsubnet->{ips}->{$freeip} = {}; + + write_db($db); + }); + die "$@" if $@; + + return "$freeip/$mask"; +} + +sub del_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_; + + my $cidr = $subnet->{cidr}; + my $zone = $subnet->{zone}; + + cfs_lock_file($ipamdb_file, undef, sub { + + my $db = read_db(); + die "zone $zone don't exist in ipam db" if !$db->{zones}->{$zone}; + my $dbzone = $db->{zones}->{$zone}; + die "subnet $cidr don't exist in ipam db" if !$dbzone->{subnets}->{$cidr}; + my $dbsubnet = $dbzone->{subnets}->{$cidr}; + + die "IP '$ip' does not exist in IPAM DB\n" if !defined($dbsubnet->{ips}->{$ip}); + delete $dbsubnet->{ips}->{$ip}; + + write_db($db); + }); + die "$@" if $@; +} + +#helpers + +sub read_db { + my $db = cfs_read_file($ipamdb_file); + return $db; +} + +sub write_db { + my ($cfg) = @_; + + my $json = to_json($cfg); + cfs_write_file($ipamdb_file, $json); +} + +sub write_config { + my ($class, $filename, $cfg) = @_; + + return $cfg; +} + +sub parse_config { + my ($class, $filename, $raw) = @_; + + $raw = '{}' if !defined($raw) ||$raw eq ''; + my $cfg = from_json($raw); + + return $cfg; +} + +1; diff --git a/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm b/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm new file mode 100644 index 0000000..ad5286b --- /dev/null +++ b/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm @@ -0,0 +1,259 @@ +package PVE::Network::SDN::Ipams::PhpIpamPlugin; + +use strict; +use warnings; +use PVE::INotify; +use PVE::Cluster; +use PVE::Tools; + +use base('PVE::Network::SDN::Ipams::Plugin'); + +sub type { + return 'phpipam'; +} + +sub properties { + return { + url => { + type => 'string', + }, + token => { + type => 'string', + }, + section => { + type => 'integer', + }, + }; +} + +sub options { + + return { + url => { optional => 0}, + token => { optional => 0 }, + section => { optional => 0 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $network = $subnet->{network}; + my $mask = $subnet->{mask}; + + my $gateway = $subnet->{gateway}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + #search subnet + my $internalid = get_prefix_id($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + my $params = { subnet => $network, + mask => $mask, + sectionId => $section, + }; + + eval { + PVE::Network::SDN::api_request("POST", "$url/subnets/", $headers, $params); + }; + if ($@) { + die "error add subnet to ipam: $@" if !$noerr; + } + } + +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + return if !$internalid; + + return; #fixme: check that prefix is empty exluding gateway, before delete + + eval { + PVE::Network::SDN::api_request("DELETE", "$url/subnets/$internalid", $headers); + }; + if ($@) { + die "error deleting subnet from ipam: $@" if !$noerr; + } + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + + my $params = { ip => $ip, + subnetId => $internalid, + hostname => $hostname, + description => $description, + }; + $params->{is_gateway} = 1 if $is_gateway; + $params->{mac} = $mac if $mac; + + eval { + PVE::Network::SDN::api_request("POST", "$url/addresses/", $headers, $params); + }; + + if ($@) { + if($is_gateway) { + die "error add subnet ip to ipam: ip $ip already exist: $@" if !is_ip_gateway($url, $ip, $headers) && !$noerr; + } else { + die "error add subnet ip to ipam: ip $ip already exist: $@" if !$noerr; + } + } +} + +sub update_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $ip_id = get_ip_id($url, $ip, $headers); + die "can't find ip addresse in ipam" if !$ip_id; + + my $params = { + hostname => $hostname, + description => $description, + }; + $params->{is_gateway} = 1 if $is_gateway; + $params->{mac} = $mac if $mac; + + eval { + PVE::Network::SDN::api_request("PATCH", "$url/addresses/$ip_id", $headers, $params); + }; + + if ($@) { + die "ipam: error update subnet ip $ip: $@" if !$noerr; + } +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $mask = $subnet->{mask}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + + my $params = { hostname => $hostname, + description => $description, + }; + + $params->{mac} = $mac if $mac; + + my $ip = undef; + eval { + my $result = PVE::Network::SDN::api_request("POST", "$url/addresses/first_free/$internalid/", $headers, $params); + $ip = $result->{data}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@" if !$noerr; + } + + return "$ip/$mask" if $ip && $mask; +} + +sub del_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_; + + return if !$ip; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $ip_id = get_ip_id($url, $ip, $headers); + return if !$ip_id; + + eval { + PVE::Network::SDN::api_request("DELETE", "$url/addresses/$ip_id", $headers); + }; + if ($@) { + die "error delete ip $ip: $@" if !$noerr; + } +} + +sub verify_api { + my ($class, $plugin_config) = @_; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $sectionid = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + eval { + PVE::Network::SDN::api_request("GET", "$url/sections/$sectionid", $headers); + }; + if ($@) { + die "Can't connect to phpipam api: $@"; + } +} + +sub on_update_hook { + my ($class, $plugin_config) = @_; + + PVE::Network::SDN::Ipams::PhpIpamPlugin::verify_api($class, $plugin_config); +} + + +#helpers + +sub get_prefix_id { + my ($url, $cidr, $headers) = @_; + + my $result = PVE::Network::SDN::api_request("GET", "$url/subnets/cidr/$cidr", $headers); + my $data = @{$result->{data}}[0]; + my $internalid = $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::api_request("GET", "$url/addresses/search/$ip", $headers); + my $data = @{$result->{data}}[0]; + my $ip_id = $data->{id}; + return $ip_id; +} + +sub is_ip_gateway { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::api_request("GET", "$url/addresses/search/$ip", $headers); + my $data = @{$result->{data}}[0]; + my $is_gateway = $data->{is_gateway}; + return $is_gateway; +} + +1; + + diff --git a/src/PVE/Network/SDN/Ipams/Plugin.pm b/src/PVE/Network/SDN/Ipams/Plugin.pm new file mode 100644 index 0000000..c96eeda --- /dev/null +++ b/src/PVE/Network/SDN/Ipams/Plugin.pm @@ -0,0 +1,111 @@ +package PVE::Network::SDN::Ipams::Plugin; + +use strict; +use warnings; + +use PVE::Tools qw(run_command); +use PVE::JSONSchema; +use PVE::Cluster; +use HTTP::Request; +use LWP::UserAgent; +use JSON; + +use Data::Dumper; +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/ipams.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); }); + +PVE::JSONSchema::register_standard_option('pve-sdn-ipam-id', { + description => "The SDN ipam object identifier.", + type => 'string', format => 'pve-sdn-ipam-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-ipam-id', \&parse_sdn_ipam_id); +sub parse_sdn_ipam_id { + my ($id, $noerr) = @_; + + if ($id !~ m/^[a-z][a-z0-9]*[a-z0-9]$/i) { + return undef if $noerr; + die "ipam ID '$id' contains illegal characters\n"; + } + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + type => 'string', + }, + ipam => get_standard_option('pve-sdn-ipam-id', + { completion => \&PVE::Network::SDN::Ipams::complete_sdn_ipam }), + }, +}; + +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 add_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub update_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + # only update ip attributes (mac,hostname,..). Don't change the ip addresses itself, as some ipam + # don't allow ip address change without del/add + + die "please implement inside plugin"; +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub del_ip { + my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_; + + die "please implement inside plugin"; +} + +sub on_update_hook { + my ($class, $plugin_config) = @_; +} + +1; diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile new file mode 100644 index 0000000..92cfcd0 --- /dev/null +++ b/src/PVE/Network/SDN/Makefile @@ -0,0 +1,13 @@ +SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.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 + make -C Ipams install + make -C Dns install + diff --git a/src/PVE/Network/SDN/SubnetPlugin.pm b/src/PVE/Network/SDN/SubnetPlugin.pm new file mode 100644 index 0000000..15b370f --- /dev/null +++ b/src/PVE/Network/SDN/SubnetPlugin.pm @@ -0,0 +1,166 @@ +package PVE::Network::SDN::SubnetPlugin; + +use strict; +use warnings; + +use Net::IP; +use Net::Subnet qw(subnet_matcher); + +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Exception qw(raise raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); +use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Vnets; + +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/subnets.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); }); + +PVE::JSONSchema::register_standard_option('pve-sdn-subnet-id', { + description => "The SDN subnet object identifier.", + type => 'string', format => 'pve-sdn-subnet-id', + type => 'string' +}); + +PVE::JSONSchema::register_format('pve-sdn-subnet-id', \&parse_sdn_subnet_id); +sub parse_sdn_subnet_id { + my ($id, $noerr) = @_; + + my $cidr = ""; + if($id =~ /\//) { + $cidr = $id; + } else { + my ($zone, $ip, $mask) = split(/-/, $id); + $cidr = "$ip/$mask"; + } + + if (!(PVE::JSONSchema::pve_verify_cidrv4($cidr, 1) || + PVE::JSONSchema::pve_verify_cidrv6($cidr, 1))) + { + return undef if $noerr; + die "value does not look like a valid CIDR network\n"; + } + return $id; +} + +my $defaultData = { + + propertyList => { + subnet => get_standard_option('pve-sdn-subnet-id', + { completion => \&PVE::Network::SDN::Subnets::complete_sdn_subnet }), + }, +}; + +sub type { + return 'subnet'; +} + +sub private { + return $defaultData; +} + +sub properties { + return { + vnet => { + type => 'string', + description => "associated vnet", + }, + gateway => { + type => 'string', format => 'ip', + description => "Subnet Gateway: Will be assign on vnet for layer3 zones", + }, + snat => { + type => 'boolean', + description => "enable masquerade for this subnet if pve-firewall", + }, +# #cloudinit, dhcp options +# routes => { +# type => 'string', +# description => "static routes [network=<network>:gateway=<ip>,network=<network>:gateway=<ip>,... ]", +# }, + dnszoneprefix => { + type => 'string', format => 'dns-name', + description => "dns domain zone prefix ex: 'adm' -> <hostname>.adm.mydomain.com", + }, + }; +} + +sub options { + return { + vnet => { optional => 0 }, + gateway => { optional => 1 }, +# routes => { optional => 1 }, + snat => { optional => 1 }, + dnszoneprefix => { optional => 1 }, + }; +} + +sub on_update_hook { + my ($class, $zone, $subnetid, $subnet, $old_subnet) = @_; + + my $cidr = $subnet->{cidr}; + my $mask = $subnet->{mask}; + + my $subnet_matcher = subnet_matcher($cidr); + + my $vnetid = $subnet->{vnet}; + my $gateway = $subnet->{gateway}; + my $ipam = $zone->{ipam}; + my $dns = $zone->{dns}; + my $dnszone = $zone->{dnszone}; + my $reversedns = $zone->{reversedns}; + + my $old_gateway = $old_subnet->{gateway} if $old_subnet; + my $mac = undef; + + if($vnetid) { + my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid); + raise_param_exc({ vnet => "$vnetid don't exist"}) if !$vnet; + raise_param_exc({ vnet => "you can't add a subnet on a vlanaware vnet"}) if $vnet->{vlanaware}; + $mac = $vnet->{mac}; + } + + my $pointopoint = 1 if Net::IP::ip_is_ipv4($gateway) && $mask == 32; + + #for /32 pointopoint, we allow gateway outside the subnet + raise_param_exc({ gateway => "$gateway is not in subnet $cidr"}) if $gateway && !$subnet_matcher->($gateway) && !$pointopoint; + + + if ($ipam) { + PVE::Network::SDN::Subnets::add_subnet($zone, $subnetid, $subnet); + + #don't register gateway for pointopoint + return if $pointopoint; + + #delete gateway on removal + if (!defined($gateway) && $old_gateway) { + eval { + PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $old_subnet, $old_gateway); + }; + warn if $@; + } + if(!$old_gateway || $gateway && $gateway ne $old_gateway) { + my $hostname = "$vnetid-gw"; + my $description = "gateway"; + PVE::Network::SDN::Subnets::add_ip($zone, $subnetid, $subnet, $gateway, $hostname, $mac, $description, 1); + } + + #delete old gateway after update + if($gateway && $old_gateway && $gateway ne $old_gateway) { + eval { + PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $old_subnet, $old_gateway); + }; + warn if $@; + } + } +} + +sub on_delete_hook { + my ($class, $subnetid, $subnet_cfg, $vnet_cfg) = @_; + + return; +} + +1; diff --git a/src/PVE/Network/SDN/Subnets.pm b/src/PVE/Network/SDN/Subnets.pm new file mode 100644 index 0000000..6bb42e5 --- /dev/null +++ b/src/PVE/Network/SDN/Subnets.pm @@ -0,0 +1,375 @@ +package PVE::Network::SDN::Subnets; + +use strict; +use warnings; + +use Net::Subnet qw(subnet_matcher); +use Net::IP; +use NetAddr::IP qw(:lower); + +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network::SDN::Dns; +use PVE::Network::SDN::Ipams; + +use PVE::Network::SDN::SubnetPlugin; +PVE::Network::SDN::SubnetPlugin->register(); +PVE::Network::SDN::SubnetPlugin->init(); + +sub sdn_subnets_config { + my ($cfg, $id, $noerr) = @_; + + die "no sdn subnet ID specified\n" if !$id; + + my $scfg = $cfg->{ids}->{$id}; + die "sdn subnet '$id' does not exist\n" if (!$noerr && !$scfg); + + if($scfg) { + my ($zone, $network, $mask) = split(/-/, $id); + $scfg->{cidr} = "$network/$mask"; + $scfg->{zone} = $zone; + $scfg->{network} = $network; + $scfg->{mask} = $mask; + } + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/subnets.cfg"); +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/subnets.cfg", $cfg); +} + +sub sdn_subnets_ids { + my ($cfg) = @_; + + return sort keys %{$cfg->{ids}}; +} + +sub complete_sdn_subnet { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::Subnets::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Subnets::sdn_subnets_ids($cfg) ]; +} + +sub get_subnet { + my ($subnetid, $running) = @_; + + my $cfg = {}; + if($running) { + my $cfg = PVE::Network::SDN::running_config(); + $cfg = $cfg->{subnets}; + } else { + $cfg = PVE::Network::SDN::Subnets::config(); + } + + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $subnetid, 1); + return $subnet; +} + +sub find_ip_subnet { + my ($ip, $mask, $subnets) = @_; + + my $subnet = undef; + my $subnetid = undef; + + foreach my $id (sort keys %{$subnets}) { + + next if $mask ne $subnets->{$id}->{mask}; + my $cidr = $subnets->{$id}->{cidr}; + my $subnet_matcher = subnet_matcher($cidr); + next if !$subnet_matcher->($ip); + $subnet = $subnets->{$id}; + $subnetid = $id; + last; + } + die "can't find any subnet for ip $ip" if !$subnet; + + return ($subnetid, $subnet); +} + +sub verify_dns_zone { + my ($zone, $dns) = @_; + + return if !$zone || !$dns; + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->verify_zone($plugin_config, $zone); +} + +sub get_reversedns_zone { + my ($subnetid, $subnet, $dns, $ip) = @_; + + return if !$subnetid || !$dns || !$ip; + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->get_reversedns_zone($plugin_config, $subnetid, $subnet, $ip); +} + +sub add_dns_record { + my ($zone, $dns, $hostname, $ip) = @_; + return if !$zone || !$dns || !$hostname || !$ip; + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->add_a_record($plugin_config, $zone, $hostname, $ip); + +} + +sub add_dns_ptr_record { + my ($reversezone, $zone, $dns, $hostname, $ip) = @_; + + return if !$zone || !$reversezone || !$dns || !$hostname || !$ip; + + $hostname .= ".$zone"; + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->add_ptr_record($plugin_config, $reversezone, $hostname, $ip); +} + +sub del_dns_record { + my ($zone, $dns, $hostname, $ip) = @_; + + return if !$zone || !$dns || !$hostname || !$ip; + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->del_a_record($plugin_config, $zone, $hostname, $ip); +} + +sub del_dns_ptr_record { + my ($reversezone, $dns, $ip) = @_; + + return if !$reversezone || !$dns || !$ip; + + my $dns_cfg = PVE::Network::SDN::Dns::config(); + my $plugin_config = $dns_cfg->{ids}->{$dns}; + my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type}); + $plugin->del_ptr_record($plugin_config, $reversezone, $ip); +} + +sub add_subnet { + my ($zone, $subnetid, $subnet) = @_; + + my $ipam = $zone->{ipam}; + return if !$ipam; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipam}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->add_subnet($plugin_config, $subnetid, $subnet); +} + +sub del_subnet { + my ($zone, $subnetid, $subnet) = @_; + + my $ipam = $zone->{ipam}; + return if !$ipam; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipam}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->del_subnet($plugin_config, $subnetid, $subnet); +} + +sub next_free_ip { + my ($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns) = @_; + + my $cidr = undef; + my $ip = undef; + $description = '' if !$description; + + my $ipamid = $zone->{ipam}; + my $dns = $zone->{dns}; + my $dnszone = $zone->{dnszone}; + my $reversedns = $zone->{reversedns}; + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + #verify dns zones before ipam + verify_dns_zone($dnszone, $dns) if !$skipdns; + + if($ipamid) { + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipamid}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + eval { + $cidr = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet, $hostname, $mac, $description); + ($ip, undef) = split(/\//, $cidr); + }; + die $@ if $@; + } + + eval { + my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip); + + if(!$skipdns) { + #add dns + add_dns_record($dnszone, $dns, $hostname, $ip); + #add reverse dns + add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip); + } + }; + if ($@) { + #rollback + my $err = $@; + eval { + PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $subnet, $ip, $hostname) + }; + die $err; + } + return $cidr; +} + +sub add_ip { + my ($zone, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $skipdns) = @_; + + return if !$subnet || !$ip; + + my $ipaddr = NetAddr::IP->new($ip); + $ip = $ipaddr->canon(); + + my $ipamid = $zone->{ipam}; + my $dns = $zone->{dns}; + my $dnszone = $zone->{dnszone}; + my $reversedns = $zone->{reversedns}; + my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip); + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + #verify dns zones before ipam + if(!$skipdns) { + verify_dns_zone($dnszone, $dns); + verify_dns_zone($reversednszone, $reversedns); + } + + if ($ipamid) { + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipamid}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + + eval { + $plugin->add_ip($plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway); + }; + die $@ if $@; + } + + eval { + if(!$skipdns) { + #add dns + add_dns_record($dnszone, $dns, $hostname, $ip); + #add reverse dns + add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip); + } + }; + if ($@) { + #rollback + my $err = $@; + eval { + PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $subnet, $ip, $hostname) + }; + die $err; + } +} + +sub update_ip { + my ($zone, $subnetid, $subnet, $ip, $hostname, $oldhostname, $mac, $description, $skipdns) = @_; + + return if !$subnet || !$ip; + + my $ipaddr = NetAddr::IP->new($ip); + $ip = $ipaddr->canon(); + + my $ipamid = $zone->{ipam}; + my $dns = $zone->{dns}; + my $dnszone = $zone->{dnszone}; + my $reversedns = $zone->{reversedns}; + my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip); + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + #verify dns zones before ipam + if(!$skipdns) { + verify_dns_zone($dnszone, $dns); + verify_dns_zone($reversednszone, $reversedns); + } + + if ($ipamid) { + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipamid}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + eval { + $plugin->update_ip($plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description); + }; + die $@ if $@; + } + + return if $hostname eq $oldhostname; + + eval { + if(!$skipdns) { + #add dns + del_dns_record($dnszone, $dns, $oldhostname, $ip); + add_dns_record($dnszone, $dns, $hostname, $ip); + #add reverse dns + del_dns_ptr_record($reversednszone, $reversedns, $ip); + add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip); + } + }; +} + +sub del_ip { + my ($zone, $subnetid, $subnet, $ip, $hostname, $skipdns) = @_; + + return if !$subnet || !$ip; + + my $ipaddr = NetAddr::IP->new($ip); + $ip = $ipaddr->canon(); + + my $ipamid = $zone->{ipam}; + my $dns = $zone->{dns}; + my $dnszone = $zone->{dnszone}; + my $reversedns = $zone->{reversedns}; + my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip); + my $dnszoneprefix = $subnet->{dnszoneprefix}; + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + if(!$skipdns) { + verify_dns_zone($dnszone, $dns); + verify_dns_zone($reversednszone, $reversedns); + } + + if ($ipamid) { + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $plugin_config = $ipam_cfg->{ids}->{$ipamid}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->del_ip($plugin_config, $subnetid, $subnet, $ip); + } + + eval { + if(!$skipdns) { + del_dns_record($dnszone, $dns, $hostname, $ip); + del_dns_ptr_record($reversednszone, $reversedns, $ip); + } + }; + if ($@) { + warn $@; + } +} + +1; diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm new file mode 100644 index 0000000..062904c --- /dev/null +++ b/src/PVE/Network/SDN/VnetPlugin.pm @@ -0,0 +1,109 @@ +package PVE::Network::SDN::VnetPlugin; + +use strict; +use warnings; + +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Exception qw(raise raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); + +use PVE::SectionConfig; +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/vnets.cfg', + 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 "vnet ID '$id' contains illegal characters\n"; + } + die "vnet ID '$id' can't be more length than 8 characters\n" if length($id) > 8; + 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 private { + return $defaultData; +} + +sub properties { + return { + zone => { + type => 'string', + description => "zone id", + }, + type => { + description => "Type", + optional => 1, + }, + tag => { + type => 'integer', + description => "vlan or vxlan id", + }, + vlanaware => { + type => 'boolean', + description => 'Allow vm VLANs to pass through this vnet.', + }, + alias => { + type => 'string', + description => "alias name of the vnet", + pattern => qr/[\(\)-_.\w\d\s]{0,256}/i, + maxLength => 256, + optional => 1, + }, + }; +} + +sub options { + return { + zone => { optional => 0}, + tag => { optional => 1}, + alias => { optional => 1 }, + vlanaware => { optional => 1 }, + }; +} + +sub on_delete_hook { + my ($class, $vnetid, $vnet_cfg) = @_; + + #verify if subnets are associated + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid); + raise_param_exc({ vnet => "Can't delete vnet if subnets exists"}) if $subnets; +} + +sub on_update_hook { + my ($class, $vnetid, $vnet_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + my $tag = $vnet->{tag}; + my $vlanaware = $vnet->{vlanaware}; + + #don't allow vlanaware change if subnets are defined + if($vnet->{vlanaware}) { + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid); + raise_param_exc({ vlanaware => "vlanaware vnet is not compatible with subnets"}) if $subnets; + } +} + +1; diff --git a/src/PVE/Network/SDN/Vnets.pm b/src/PVE/Network/SDN/Vnets.pm new file mode 100644 index 0000000..0b32c58 --- /dev/null +++ b/src/PVE/Network/SDN/Vnets.pm @@ -0,0 +1,163 @@ +package PVE::Network::SDN::Vnets; + +use strict; +use warnings; + +use Net::IP; + +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Subnets; +use PVE::Network::SDN::Zones; + +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 exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + return cfs_read_file("sdn/vnets.cfg"); +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/vnets.cfg", $cfg); +} + +sub sdn_vnets_ids { + my ($cfg) = @_; + + return sort 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) ]; +} + +sub get_vnet { + my ($vnetid, $running) = @_; + + return if !$vnetid; + + my $scfg = {}; + if($running) { + my $cfg = PVE::Network::SDN::running_config(); + $scfg = $cfg->{vnets}; + } else { + $scfg = PVE::Network::SDN::Vnets::config(); + } + + my $vnet = PVE::Network::SDN::Vnets::sdn_vnets_config($scfg, $vnetid, 1); + + return $vnet; +} + +sub get_subnets { + my ($vnetid) = @_; + + return if !$vnetid; + + my $subnets = undef; + my $subnets_cfg = PVE::Network::SDN::Subnets::config(); + foreach my $subnetid (sort keys %{$subnets_cfg->{ids}}) { + my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($subnets_cfg, $subnetid); + next if !$subnet->{vnet} || $subnet->{vnet} ne $vnetid; + $subnets->{$subnetid} = $subnet; + } + return $subnets; + +} + +sub get_subnet_from_vnet_cidr { + my ($vnetid, $cidr) = @_; + + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid); + my $zoneid = $vnet->{zone}; + my $zone = PVE::Network::SDN::Zones::get_zone($zoneid); + + my ($ip, $mask) = split(/\//, $cidr); + die "ip address is not in cidr format" if !$mask; + + my ($subnetid, $subnet) = PVE::Network::SDN::Subnets::find_ip_subnet($ip, $mask, $subnets); + + return ($zone, $subnetid, $subnet, $ip); +} + +sub get_next_free_cidr { + my ($vnetid, $hostname, $mac, $description, $ipversion, $skipdns) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid); + my $zoneid = $vnet->{zone}; + my $zone = PVE::Network::SDN::Zones::get_zone($zoneid); + + return if !$zone->{ipam}; + + $ipversion = 4 if !$ipversion; + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + my $ip = undef; + my $subnetcount = 0; + + foreach my $subnetid (sort keys %{$subnets}) { + my $subnet = $subnets->{$subnetid}; + my $network = $subnet->{network}; + + next if $ipversion != Net::IP::ip_get_version($network); + $subnetcount++; + + eval { + $ip = PVE::Network::SDN::Subnets::next_free_ip($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns); + }; + warn $@ if $@; + last if $ip; + } + die "can't find any free ip" if !$ip && $subnetcount > 0; + + return $ip; +} + +sub add_cidr { + my ($vnetid, $cidr, $hostname, $mac, $description, $skipdns) = @_; + + return if !$vnetid; + + my ($zone, $subnetid, $subnet, $ip) = PVE::Network::SDN::Vnets::get_subnet_from_vnet_cidr($vnetid, $cidr); + PVE::Network::SDN::Subnets::add_ip($zone, $subnetid, $subnet, $ip, $hostname, $mac, $description, undef, $skipdns); +} + +sub update_cidr { + my ($vnetid, $cidr, $hostname, $oldhostname, $mac, $description, $skipdns) = @_; + + return if !$vnetid; + + my ($zone, $subnetid, $subnet, $ip) = PVE::Network::SDN::Vnets::get_subnet_from_vnet_cidr($vnetid, $cidr); + PVE::Network::SDN::Subnets::update_ip($zone, $subnetid, $subnet, $ip, $hostname, $oldhostname, $mac, $description, $skipdns); +} + +sub del_cidr { + my ($vnetid, $cidr, $hostname, $skipdns) = @_; + + return if !$vnetid; + + my ($zone, $subnetid, $subnet, $ip) = PVE::Network::SDN::Vnets::get_subnet_from_vnet_cidr($vnetid, $cidr); + PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $subnet, $ip, $hostname, $skipdns); +} + + + +1; diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm new file mode 100644 index 0000000..f8e40b1 --- /dev/null +++ b/src/PVE/Network/SDN/Zones.pm @@ -0,0 +1,357 @@ +package PVE::Network::SDN::Zones; + +use strict; +use warnings; + +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; + +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::SimplePlugin; +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::SimplePlugin->register(); +PVE::Network::SDN::Zones::Plugin->init(); + +my $local_network_sdn_file = "/etc/network/interfaces.d/sdn"; + +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 exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/zones.cfg"); + return $config; +} + +sub get_plugin_config { + my ($vnet) = @_; + my $zoneid = $vnet->{zone}; + my $zone_cfg = PVE::Network::SDN::Zones::config(); + return $zone_cfg->{ids}->{$zoneid}; +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/zones.cfg", $cfg); +} + +sub sdn_zones_ids { + my ($cfg) = @_; + + return sort keys %{$cfg->{ids}}; +} + +sub complete_sdn_zone { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::running_config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_zones_ids($cfg) ]; +} + +sub get_zone { + my ($zoneid, $running) = @_; + + my $cfg = {}; + if($running) { + my $cfg = PVE::Network::SDN::running_config(); + $cfg = $cfg->{vnets}; + } else { + $cfg = PVE::Network::SDN::Zones::config(); + } + + my $zone = PVE::Network::SDN::Zones::sdn_zones_config($cfg, $zoneid, 1); + + return $zone; +} + + +sub generate_etc_network_config { + + my $cfg = PVE::Network::SDN::running_config(); + + my $version = $cfg->{version}; + my $vnet_cfg = $cfg->{vnets}; + my $zone_cfg = $cfg->{zones}; + my $subnet_cfg = $cfg->{subnets}; + my $controller_cfg = $cfg->{controllers}; + return if !$vnet_cfg && !$zone_cfg; + + my $interfaces_config = PVE::INotify::read_file('interfaces'); + + #generate configuration + my $config = {}; + my $nodename = PVE::INotify::nodename(); + + for my $id (sort keys %{$vnet_cfg->{ids}}) { + my $vnet = $vnet_cfg->{ids}->{$id}; + my $zone = $vnet->{zone}; + + if (!$zone) { + warn "can't generate vnet '$id': no zone assigned!\n"; + next; + } + + my $plugin_config = $zone_cfg->{ids}->{$zone}; + + if (!defined($plugin_config)) { + warn "can't generate vnet '$id': zone $zone don't exist\n"; + next; + } + + next if defined($plugin_config->{nodes}) && !$plugin_config->{nodes}->{$nodename}; + + my $controller; + if (my $controllerid = $plugin_config->{controller}) { + $controller = $controller_cfg->{ids}->{$controllerid}; + } + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + eval { + $plugin->generate_sdn_config($plugin_config, $zone, $id, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config); + }; + if (my $err = $@) { + warn "zone $zone : vnet $id : $err\n"; + next; + } + } + + my $raw_network_config = "\#version:$version\n"; + foreach my $iface (sort 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 $writefh = IO::File->new($local_network_sdn_file,">"); + print $writefh $rawconfig; + $writefh->close(); +} + +sub read_etc_network_config_version { + my $versionstr = PVE::Tools::file_read_firstline($local_network_sdn_file); + + return if !defined($versionstr); + + if ($versionstr =~ m/^\#version:(\d+)$/) { + return $1; + } +} + +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; +} + +my $warned_about_reload; + +sub status { + + my $err_config = undef; + + my $local_version = PVE::Network::SDN::Zones::read_etc_network_config_version(); + my $cfg = PVE::Network::SDN::running_config(); + my $sdn_version = $cfg->{version}; + + return if !$sdn_version; + + if (!$local_version) { + $err_config = "local sdn network configuration is not yet generated, please reload"; + if (!$warned_about_reload) { + $warned_about_reload = 1; + warn "$err_config\n"; + } + } elsif ($local_version < $sdn_version) { + $err_config = "local sdn network configuration is too old, please reload"; + if (!$warned_about_reload) { + $warned_about_reload = 1; + warn "$err_config\n"; + } + } else { + $warned_about_reload = 0; + } + + my $status = ifquery_check(); + + my $vnet_cfg = $cfg->{vnets}; + my $zone_cfg = $cfg->{zones}; + my $nodename = PVE::INotify::nodename(); + + my $vnet_status = {}; + my $zone_status = {}; + + for my $id (sort keys %{$zone_cfg->{ids}}) { + next if defined($zone_cfg->{ids}->{$id}->{nodes}) && !$zone_cfg->{ids}->{$id}->{nodes}->{$nodename}; + $zone_status->{$id}->{status} = $err_config ? 'pending' : 'available'; + } + + foreach my $id (sort keys %{$vnet_cfg->{ids}}) { + my $vnet = $vnet_cfg->{ids}->{$id}; + my $zone = $vnet->{zone}; + next if !defined($zone); + + my $plugin_config = $zone_cfg->{ids}->{$zone}; + + if (!defined($plugin_config)) { + $vnet_status->{$id}->{status} = 'error'; + $vnet_status->{$id}->{statusmsg} = "unknown zone '$zone' configured"; + next; + } + + next if defined($plugin_config->{nodes}) && !$plugin_config->{nodes}->{$nodename}; + + $vnet_status->{$id}->{zone} = $zone; + $vnet_status->{$id}->{status} = 'available'; + + if ($err_config) { + $vnet_status->{$id}->{status} = 'pending'; + $vnet_status->{$id}->{statusmsg} = $err_config; + next; + } + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + my $err_msg = $plugin->status($plugin_config, $zone, $id, $vnet, $status); + if (@{$err_msg} > 0) { + $vnet_status->{$id}->{status} = 'error'; + $vnet_status->{$id}->{statusmsg} = join(',', @{$err_msg}); + $zone_status->{$id}->{status} = 'error'; + } + } + + return ($zone_status, $vnet_status); +} + +sub tap_create { + my ($iface, $bridge) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); + if (!$vnet) { # fallback for classic bridge + PVE::Network::tap_create($iface, $bridge); + return; + } + + my $plugin_config = get_plugin_config($vnet); + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + $plugin->tap_create($plugin_config, $vnet, $iface, $bridge); +} + +sub veth_create { + my ($veth, $vethpeer, $bridge, $hwaddr) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); + if (!$vnet) { # fallback for classic bridge + PVE::Network::veth_create($veth, $vethpeer, $bridge, $hwaddr); + return; + } + + my $plugin_config = get_plugin_config($vnet); + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + $plugin->veth_create($plugin_config, $vnet, $veth, $vethpeer, $bridge, $hwaddr); +} + +sub tap_plug { + my ($iface, $bridge, $tag, $firewall, $trunks, $rate) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); + if (!$vnet) { # fallback for classic bridge + my $interfaces_config = PVE::INotify::read_file('interfaces'); + my $opts = {}; + $opts->{learning} = 0 if $interfaces_config->{ifaces}->{$bridge} && $interfaces_config->{ifaces}->{$bridge}->{'bridge-disable-mac-learning'}; + PVE::Network::tap_plug($iface, $bridge, $tag, $firewall, $trunks, $rate, $opts); + return; + } + + my $plugin_config = get_plugin_config($vnet); + my $nodename = PVE::INotify::nodename(); + + die "vnet $bridge is not allowed on this node\n" + if $plugin_config->{nodes} && !defined($plugin_config->{nodes}->{$nodename}); + + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + $plugin->tap_plug($plugin_config, $vnet, $tag, $iface, $bridge, $firewall, $trunks, $rate); +} + +sub add_bridge_fdb { + my ($iface, $macaddr, $bridge, $firewall) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); + if (!$vnet) { # fallback for classic bridge + PVE::Network::add_bridge_fdb($iface, $macaddr, $firewall); + return; + } + + my $plugin_config = get_plugin_config($vnet); + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + PVE::Network::add_bridge_fdb($iface, $macaddr, $firewall) if $plugin_config->{'bridge-disable-mac-learning'}; +} + +sub del_bridge_fdb { + my ($iface, $macaddr, $bridge, $firewall) = @_; + + my $vnet = PVE::Network::SDN::Vnets::get_vnet($bridge, 1); + if (!$vnet) { # fallback for classic bridge + PVE::Network::del_bridge_fdb($iface, $macaddr, $firewall); + return; + } + + my $plugin_config = get_plugin_config($vnet); + my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type}); + PVE::Network::del_bridge_fdb($iface, $macaddr, $firewall) if $plugin_config->{'bridge-disable-mac-learning'}; +} + +1; + diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm new file mode 100644 index 0000000..a5a7539 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm @@ -0,0 +1,315 @@ +package PVE::Network::SDN::Zones::EvpnPlugin; + +use strict; +use warnings; +use PVE::Network::SDN::Zones::VxlanPlugin; +use PVE::Exception qw(raise raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); +use PVE::Tools qw($IPV4RE); +use PVE::INotify; +use PVE::Cluster; +use PVE::Tools; +use Net::IP; + +use PVE::Network::SDN::Controllers::EvpnPlugin; + +use base('PVE::Network::SDN::Zones::VxlanPlugin'); + +sub type { + return 'evpn'; +} + +PVE::JSONSchema::register_format('pve-sdn-bgp-rt', \&pve_verify_sdn_bgp_rt); +sub pve_verify_sdn_bgp_rt { + my ($rt) = @_; + + if ($rt =~ m/^(\d+):(\d+)$/) { + my $asn = $1; + my $id = $2; + + if ($asn < 0 || $asn > 4294967295) { + die "value does not look like a valid bgp route-target\n"; + } + if ($id < 0 || $id > 4294967295) { + die "value does not look like a valid bgp route-target\n"; + } + } else { + die "value does not look like a valid bgp route-target\n"; + } + return $rt; +} + +sub properties { + return { + 'vrf-vxlan' => { + type => 'integer', + description => "l3vni.", + }, + 'controller' => { + type => 'string', + description => "Frr router name", + }, + 'mac' => { + type => 'string', + description => "Anycast logical router mac address", + optional => 1, format => 'mac-addr' + }, + 'exitnodes' => get_standard_option('pve-node-list'), + 'exitnodes-local-routing' => { + type => 'boolean', + description => "Allow exitnodes to connect to evpn guests", + optional => 1 + }, + 'exitnodes-primary' => get_standard_option('pve-node', { + description => "Force traffic to this exitnode first."}), + 'advertise-subnets' => { + type => 'boolean', + description => "Advertise evpn subnets if you have silent hosts", + optional => 1 + }, + 'disable-arp-nd-suppression' => { + type => 'boolean', + description => "Disable ipv4 arp && ipv6 neighbour discovery suppression", + optional => 1 + }, + 'rt-import' => { + type => 'string', + description => "Route-Target import", + optional => 1, format => 'pve-sdn-bgp-rt-list' + } + }; +} + +sub options { + return { + nodes => { optional => 1}, + 'vrf-vxlan' => { optional => 0 }, + controller => { optional => 0 }, + exitnodes => { optional => 1 }, + 'exitnodes-local-routing' => { optional => 1 }, + 'exitnodes-primary' => { optional => 1 }, + 'advertise-subnets' => { optional => 1 }, + 'disable-arp-nd-suppression' => { optional => 1 }, + 'rt-import' => { optional => 1 }, + mtu => { optional => 1 }, + mac => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + my $tag = $vnet->{tag}; + my $alias = $vnet->{alias}; + my $mac = $plugin_config->{'mac'}; + + my $vrf_iface = "vrf_$zoneid"; + my $vrfvxlan = $plugin_config->{'vrf-vxlan'}; + my $local_node = PVE::INotify::nodename(); + + die "missing vxlan tag" if !$tag; + die "missing controller" if !$controller; + warn "vlan-aware vnet can't be enabled with evpn plugin" if $vnet->{vlanaware}; + + my @peers = PVE::Tools::split_list($controller->{'peers'}); + my $bgprouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_bgp_controller($local_node, $controller_cfg); + my $loopback = $bgprouter->{loopback} if $bgprouter->{loopback}; + my ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback); + my $is_evpn_gateway = $plugin_config->{'exitnodes'}->{$local_node}; + my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'}; + + + my $mtu = 1450; + $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu}; + $mtu = $plugin_config->{mtu} if $plugin_config->{mtu}; + + #vxlan interface + my $vxlan_iface = "vxlan_$vnetid"; + 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" if !$plugin_config->{'disable-arp-nd-suppression'}; + + push @iface_config, "mtu $mtu" if $mtu; + push(@{$config->{$vxlan_iface}}, @iface_config) if !$config->{$vxlan_iface}; + + #vnet bridge + @iface_config = (); + + my $address = {}; + my $ipv4 = undef; + my $ipv6 = undef; + my $enable_forward_v4 = undef; + my $enable_forward_v6 = undef; + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + foreach my $subnetid (sort keys %{$subnets}) { + my $subnet = $subnets->{$subnetid}; + my $cidr = $subnet->{cidr}; + my $mask = $subnet->{mask}; + + my $gateway = $subnet->{gateway}; + if ($gateway) { + push @iface_config, "address $gateway/$mask" if !defined($address->{$gateway}); + $address->{$gateway} = 1; + } + + my $iptables = undef; + my $checkrouteip = undef; + my $ipversion = Net::IP::ip_is_ipv6($gateway) ? 6 : 4; + + if ($ipversion == 6) { + $ipv6 = 1; + $iptables = "ip6tables"; + $checkrouteip = '2001:4860:4860::8888'; + $enable_forward_v6 = 1 if $gateway; + } else { + $ipv4 = 1; + $iptables = "iptables"; + $checkrouteip = '8.8.8.8'; + $enable_forward_v4 = 1 if $gateway; + } + + if ($subnet->{snat}) { + + #find outgoing interface + my ($outip, $outiface) = PVE::Network::SDN::Zones::Plugin::get_local_route_ip($checkrouteip); + if ($outip && $outiface && $is_evpn_gateway) { + #use snat, faster than masquerade + push @iface_config, "post-up $iptables -t nat -A POSTROUTING -s '$cidr' -o $outiface -j SNAT --to-source $outip"; + push @iface_config, "post-down $iptables -t nat -D POSTROUTING -s '$cidr' -o $outiface -j SNAT --to-source $outip"; + #add conntrack zone once on outgoing interface + push @iface_config, "post-up $iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1"; + push @iface_config, "post-down $iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1"; + } + } + } + + push @iface_config, "hwaddress $mac" if $mac; + push @iface_config, "bridge_ports $vxlan_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 @iface_config, "ip-forward on" if $enable_forward_v4; + push @iface_config, "ip6-forward on" if $enable_forward_v6; + push @iface_config, "arp-accept on" if $ipv4||$ipv6; + push @iface_config, "vrf $vrf_iface" if $vrf_iface; + push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid}; + + if ($vrf_iface) { + #vrf interface + @iface_config = (); + push @iface_config, "vrf-table auto"; + if(!$is_evpn_gateway) { + push @iface_config, "post-up ip route add vrf $vrf_iface unreachable default metric 4278198272"; + } else { + push @iface_config, "post-up ip route del vrf $vrf_iface unreachable default metric 4278198272"; + } + + push(@{$config->{$vrf_iface}}, @iface_config) if !$config->{$vrf_iface}; + + if ($vrfvxlan) { + #l3vni vxlan interface + my $iface_vrf_vxlan = "vrfvx_$zoneid"; + @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" if !$plugin_config->{'disable-arp-nd-suppression'}; + push @iface_config, "mtu $mtu" if $mtu; + push(@{$config->{$iface_vrf_vxlan}}, @iface_config) if !$config->{$iface_vrf_vxlan}; + + #l3vni bridge + my $brvrf = "vrfbr_$zoneid"; + @iface_config = (); + push @iface_config, "bridge-ports $iface_vrf_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_iface"; + push(@{$config->{$brvrf}}, @iface_config) if !$config->{$brvrf}; + } + + if ( $is_evpn_gateway && $exitnodes_local_routing ) { + #add a veth pair for local cross-vrf routing + my $iface_xvrf = "xvrf_$zoneid"; + my $iface_xvrfp = "xvrfp_$zoneid"; + + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "address 10.255.255.1/30"; + push @iface_config, "veth-peer-name $iface_xvrfp"; + push @iface_config, "mtu ".($mtu+50) if $mtu; + push(@{$config->{$iface_xvrf}}, @iface_config) if !$config->{$iface_xvrf}; + + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "address 10.255.255.2/30"; + push @iface_config, "veth-peer-name $iface_xvrf"; + push @iface_config, "vrf $vrf_iface"; + push @iface_config, "mtu ".($mtu+50) if $mtu; + push(@{$config->{$iface_xvrfp}}, @iface_config) if !$config->{$iface_xvrfp}; + } + } + return $config; +} + +sub on_update_hook { + my ($class, $zoneid, $zone_cfg, $controller_cfg) = @_; + + # verify that controller exist + my $controller = $zone_cfg->{ids}->{$zoneid}->{controller}; + if (!defined($controller_cfg->{ids}->{$controller})) { + die "controller $controller don't exist"; + } else { + die "$controller is not a evpn controller type" if $controller_cfg->{ids}->{$controller}->{type} ne 'evpn'; + } + + #vrf-vxlan need to be defined + + my $vrfvxlan = $zone_cfg->{ids}->{$zoneid}->{'vrf-vxlan'}; + # verify that vrf-vxlan is not already declared in another zone + foreach my $id (keys %{$zone_cfg->{ids}}) { + next if $id eq $zoneid; + die "vrf-vxlan $vrfvxlan is already declared in $id" + if (defined($zone_cfg->{ids}->{$id}->{'vrf-vxlan'}) && $zone_cfg->{ids}->{$id}->{'vrf-vxlan'} eq $vrfvxlan); + } + + if (!defined($zone_cfg->{ids}->{$zoneid}->{'mac'})) { + my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg'); + $zone_cfg->{ids}->{$zoneid}->{'mac'} = PVE::Tools::random_ether_addr($dc->{mac_prefix}); + } +} + + +sub vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + my $tag = $vnet->{tag}; + + raise_param_exc({ tag => "missing vxlan tag"}) if !defined($tag); + raise_param_exc({ tag => "vxlan tag max value is 16777216"}) if $tag > 16777216; + + # verify that tag is not already defined globally (vxlan-id are unique) + foreach my $id (keys %{$vnet_cfg->{ids}}) { + next if $id eq $vnetid; + my $othervnet = $vnet_cfg->{ids}->{$id}; + my $other_tag = $othervnet->{tag}; + my $other_zoneid = $othervnet->{zone}; + my $other_zone = $zone_cfg->{ids}->{$other_zoneid}; + next if $other_zone->{type} ne 'vxlan' && $other_zone->{type} ne 'evpn'; + raise_param_exc({ tag => "vxlan tag $tag already exist in vnet $id in zone $other_zoneid "}) if $other_tag && $tag eq $other_tag; + } +} + + +1; + + diff --git a/src/PVE/Network/SDN/Zones/FaucetPlugin.pm b/src/PVE/Network/SDN/Zones/FaucetPlugin.pm new file mode 100644 index 0000000..a237d17 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/FaucetPlugin.pm @@ -0,0 +1,74 @@ +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 properties { + return { + 'dp-id' => { + type => 'integer', + description => 'Faucet dataplane id', + }, + }; +} + +sub options { + + return { + nodes => { optional => 1}, + 'dp-id' => { optional => 0 }, +# 'uplink-id' => { optional => 0 }, + 'controller' => { optional => 0 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $uplinks, $controller, $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; + + diff --git a/src/PVE/Network/SDN/Zones/Makefile b/src/PVE/Network/SDN/Zones/Makefile new file mode 100644 index 0000000..8454388 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm VlanPlugin.pm VxlanPlugin.pm FaucetPlugin.pm EvpnPlugin.pm QinQPlugin.pm SimplePlugin.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 diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm b/src/PVE/Network/SDN/Zones/Plugin.pm new file mode 100644 index 0000000..2c707b3 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/Plugin.pm @@ -0,0 +1,340 @@ +package PVE::Network::SDN::Zones::Plugin; + +use strict; +use warnings; + +use PVE::Tools qw(run_command); +use PVE::JSONSchema; +use PVE::Cluster; +use PVE::Network; + +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file( + 'sdn/zones.cfg', + 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 "zone ID '$id' contains illegal characters\n"; + } + die "zone ID '$id' can't be more length than 8 characters\n" if length($id) > 8; + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + type => 'string', + }, + nodes => get_standard_option('pve-node-list', { optional => 1 }), + zone => get_standard_option('pve-sdn-zone-id', { + completion => \&PVE::Network::SDN::Zones::complete_sdn_zone, + }), + ipam => { + type => 'string', + description => "use a specific ipam", + optional => 1, + }, + }, +}; + +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 decode_value { + my ($class, $type, $key, $value) = @_; + + if ($key eq 'nodes' || $key eq 'exitnodes') { + my $res = {}; + + foreach my $node (PVE::Tools::split_list($value)) { + if (PVE::JSONSchema::pve_verify_node_name($node)) { + $res->{$node} = 1; + } + } + + return $res; + } + + return $value; +} + +sub encode_value { + my ($class, $type, $key, $value) = @_; + + if ($key eq 'nodes' || $key eq 'exitnodes') { + return join(',', keys(%$value)); + } + + return $value; +} + +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_config { + my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_; + + die "please implement inside plugin"; +} + +sub generate_controller_vnet_config { + my ($class, $plugin_config, $controller, $zoneid, $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, $zoneid, $vnet_cfg) = @_; + + # verify that no vnet are associated to this zone + foreach my $id (keys %{$vnet_cfg->{ids}}) { + my $vnet = $vnet_cfg->{ids}->{$id}; + die "zone $zoneid is used by vnet $id" + if ($vnet->{type} eq 'vnet' && defined($vnet->{zone}) && $vnet->{zone} eq $zoneid); + } +} + +sub on_update_hook { + my ($class, $zoneid, $zone_cfg, $controller_cfg) = @_; + + # do nothing by default +} + +sub vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + # 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); +} + +sub status { + my ($class, $plugin_config, $zone, $vnetid, $vnet, $status) = @_; + + my $err_msg = []; + + # ifaces to check + my $ifaces = [ $vnetid ]; + + foreach my $iface (@{$ifaces}) { + if (!$status->{$iface}->{status}) { + push @$err_msg, "missing $iface"; + } elsif ($status->{$iface}->{status} ne 'pass') { + push @$err_msg, "error $iface"; + } + } + return $err_msg; +} + + +sub tap_create { + my ($class, $plugin_config, $vnet, $iface, $vnetid) = @_; + + PVE::Network::tap_create($iface, $vnetid); +} + +sub veth_create { + my ($class, $plugin_config, $vnet, $veth, $vethpeer, $vnetid, $hwaddr) = @_; + + PVE::Network::veth_create($veth, $vethpeer, $vnetid, $hwaddr); +} + +sub tap_plug { + my ($class, $plugin_config, $vnet, $tag, $iface, $vnetid, $firewall, $trunks, $rate) = @_; + + my $vlan_aware = PVE::Tools::file_read_firstline("/sys/class/net/$vnetid/bridge/vlan_filtering"); + die "vm vlans are not allowed on vnet $vnetid" if !$vlan_aware && ($tag || $trunks); + + my $opts = {}; + $opts->{learning} = 0 if $plugin_config->{'bridge-disable-mac-learning'}; + PVE::Network::tap_plug($iface, $vnetid, $tag, $firewall, $trunks, $rate, $opts); +} + +#helper + +sub get_uplink_iface { + my ($interfaces_config, $uplink) = @_; + + my $iface = undef; + foreach my $id (keys %{$interfaces_config->{ifaces}}) { + my $interface = $interfaces_config->{ifaces}->{$id}; + if (my $iface_uplink = $interface->{'uplink-id'}) { + next if $iface_uplink ne $uplink; + if($interface->{type} ne 'eth' && $interface->{type} ne 'bond') { + warn "uplink $uplink is not a physical or bond interface"; + next; + } + $iface = $id; + } + } + + #create a dummy uplink interface if no uplink found + if(!$iface) { + warn "can't find uplink $uplink in physical interface"; + $iface = "uplink${uplink}"; + } + + return $iface; +} + +sub get_local_route_ip { + my ($targetip) = @_; + + my $ip = undef; + my $interface = undef; + + run_command(['/sbin/ip', 'route', 'get', $targetip], outfunc => sub { + if ($_[0] =~ m/src ($PVE::Tools::IPRE)/) { + $ip = $1; + } + if ($_[0] =~ m/dev (\S+)/) { + $interface = $1; + } + + }); + return ($ip, $interface); +} + + +sub find_local_ip_interface_peers { + my ($peers, $iface) = @_; + + my $network_config = PVE::INotify::read_file('interfaces'); + my $ifaces = $network_config->{ifaces}; + + #if iface is defined, return ip if exist (if not,try to find it on other ifaces) + if ($iface) { + my $ip = $ifaces->{$iface}->{address}; + return ($ip,$iface) if $ip; + } + + #is a local ip member of peers list ? + foreach my $address (@{$peers}) { + while (my $interface = each %$ifaces) { + my $ip = $ifaces->{$interface}->{address}; + if ($ip && $ip eq $address) { + return ($ip, $interface); + } + } + } + + #if peer is remote, find source with ip route + foreach my $address (@{$peers}) { + my ($ip, $interface) = get_local_route_ip($address); + return ($ip, $interface); + } +} + +sub find_bridge { + my ($bridge) = @_; + + die "can't find bridge $bridge" if !-d "/sys/class/net/$bridge"; +} + +sub is_vlanaware { + my ($bridge) = @_; + + return PVE::Tools::file_read_firstline("/sys/class/net/$bridge/bridge/vlan_filtering"); +} + +sub is_ovs { + my ($bridge) = @_; + + my $is_ovs = !-d "/sys/class/net/$bridge/brif"; + return $is_ovs; +} + +sub get_bridge_ifaces { + my ($bridge) = @_; + + my @bridge_ifaces = (); + my $dir = "/sys/class/net/$bridge/brif"; + PVE::Tools::dir_glob_foreach($dir, '(((eth|bond)\d+|en[^.]+)(\.\d+)?)', sub { + push @bridge_ifaces, $_[0]; + }); + + return @bridge_ifaces; +} +1; diff --git a/src/PVE/Network/SDN/Zones/QinQPlugin.pm b/src/PVE/Network/SDN/Zones/QinQPlugin.pm new file mode 100644 index 0000000..f4d12bc --- /dev/null +++ b/src/PVE/Network/SDN/Zones/QinQPlugin.pm @@ -0,0 +1,233 @@ +package PVE::Network::SDN::Zones::QinQPlugin; + +use strict; +use warnings; + +use PVE::Exception qw(raise raise_param_exc); + +use PVE::Network::SDN::Zones::Plugin; + +use base('PVE::Network::SDN::Zones::Plugin'); + +sub type { + return 'qinq'; +} + +sub properties { + return { + tag => { + type => 'integer', + minimum => 0, + description => "Service-VLAN Tag", + }, + mtu => { + type => 'integer', + description => "MTU", + optional => 1, + }, + 'vlan-protocol' => { + type => 'string', + enum => ['802.1q', '802.1ad'], + default => '802.1q', + optional => 1, + } + }; +} + +sub options { + return { + nodes => { optional => 1}, + 'tag' => { optional => 0 }, + 'bridge' => { optional => 0 }, + 'bridge-disable-mac-learning' => { optional => 1 }, + 'mtu' => { optional => 1 }, + 'vlan-protocol' => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + my ($bridge, $mtu, $stag) = $plugin_config->@{'bridge', 'mtu', 'tag'}; + my $vlanprotocol = $plugin_config->{'vlan-protocol'}; + + PVE::Network::SDN::Zones::Plugin::find_bridge($bridge); + + my $vlan_aware = PVE::Network::SDN::Zones::Plugin::is_vlanaware($bridge); + my $is_ovs = PVE::Network::SDN::Zones::Plugin::is_ovs($bridge); + + my @iface_config = (); + my $zone_notag_uplink = "ln_${zoneid}"; + my $zone_notag_uplinkpeer = "pr_${zoneid}"; + my $zone = "z_${zoneid}"; + + my $vnet_bridge_ports = ""; + if (my $ctag = $vnet->{tag}) { + $vnet_bridge_ports = "$zone.$ctag"; + } else { + $vnet_bridge_ports = $zone_notag_uplinkpeer; + } + + my $zone_bridge_ports = ""; + if ($is_ovs) { + # ovs--->ovsintport(dot1q-tunnel tag)------->vlanawarebrige-----(tag)--->vnet + + $vlanprotocol = "802.1q" if !$vlanprotocol; + my $svlan_iface = "sv_".$zoneid; + + # ovs dot1q-tunnel port + @iface_config = (); + push @iface_config, "ovs_type OVSIntPort"; + push @iface_config, "ovs_bridge $bridge"; + push @iface_config, "ovs_mtu $mtu" if $mtu; + push @iface_config, "ovs_options vlan_mode=dot1q-tunnel tag=$stag other_config:qinq-ethtype=$vlanprotocol"; + push(@{$config->{$svlan_iface}}, @iface_config) if !$config->{$svlan_iface}; + + # redefine main ovs bridge, ifupdown2 will merge ovs_ports + @{$config->{$bridge}}[0] = "ovs_ports" if !@{$config->{$bridge}}[0]; + my @ovs_ports = split / / , @{$config->{$bridge}}[0]; + @{$config->{$bridge}}[0] .= " $svlan_iface" if !grep( $_ eq $svlan_iface, @ovs_ports ); + + $zone_bridge_ports = $svlan_iface; + + } elsif ($vlan_aware) { + # VLAN_aware_brige-(tag)----->vlanwarebridge-(tag)----->vnet + + if ($vlanprotocol) { + @iface_config = (); + push @iface_config, "bridge-vlan-protocol $vlanprotocol"; + push(@{$config->{$bridge}}, @iface_config) if !$config->{$bridge}; + } + + $zone_bridge_ports = "$bridge.$stag"; + + } else { + # eth--->eth.x(svlan)----->vlanwarebridge-(tag)----->vnet---->vnet + + my @bridge_ifaces = PVE::Network::SDN::Zones::Plugin::get_bridge_ifaces($bridge); + + for my $bridge_iface (@bridge_ifaces) { + # use named vlan interface to avoid too long names + my $svlan_iface = "sv_$zoneid"; + + # svlan + @iface_config = (); + push @iface_config, "vlan-raw-device $bridge_iface"; + push @iface_config, "vlan-id $stag"; + push @iface_config, "vlan-protocol $vlanprotocol" if $vlanprotocol; + push(@{$config->{$svlan_iface}}, @iface_config) if !$config->{$svlan_iface}; + + $zone_bridge_ports = $svlan_iface; + last; + } + } + + # veth peer for notag vnet + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "veth-peer-name $zone_notag_uplinkpeer"; + push(@{$config->{$zone_notag_uplink}}, @iface_config) if !$config->{$zone_notag_uplink}; + + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "veth-peer-name $zone_notag_uplink"; + push(@{$config->{$zone_notag_uplinkpeer}}, @iface_config) if !$config->{$zone_notag_uplinkpeer}; + + # zone vlan aware bridge + @iface_config = (); + push @iface_config, "mtu $mtu" if $mtu; + push @iface_config, "bridge-stp off"; + push @iface_config, "bridge-ports $zone_bridge_ports $zone_notag_uplink"; + push @iface_config, "bridge-fd 0"; + push @iface_config, "bridge-vlan-aware yes"; + push @iface_config, "bridge-vids 2-4094"; + push(@{$config->{$zone}}, @iface_config) if !$config->{$zone}; + + # vnet bridge + @iface_config = (); + push @iface_config, "bridge_ports $vnet_bridge_ports"; + push @iface_config, "bridge_stp off"; + push @iface_config, "bridge_fd 0"; + if($vnet->{vlanaware}) { + push @iface_config, "bridge-vlan-aware yes"; + push @iface_config, "bridge-vids 2-4094"; + } + push @iface_config, "mtu $mtu" if $mtu; + push @iface_config, "alias $vnet->{alias}" if $vnet->{alias}; + push(@{$config->{$vnetid}}, @iface_config) if !$config->{$vnetid}; +} + +sub status { + my ($class, $plugin_config, $zone, $vnetid, $vnet, $status) = @_; + + my $bridge = $plugin_config->{bridge}; + my $err_msg = []; + + if (!-d "/sys/class/net/$bridge") { + push @$err_msg, "missing $bridge"; + return $err_msg; + } + + my $vlan_aware = PVE::Network::SDN::Zones::Plugin::is_vlanaware($bridge); + + my $tag = $vnet->{tag}; + my $vnet_uplink = "ln_".$vnetid; + my $vnet_uplinkpeer = "pr_".$vnetid; + my $zone_notag_uplink = "ln_".$zone; + my $zone_notag_uplinkpeer = "pr_".$zone; + my $zonebridge = "z_$zone"; + + # ifaces to check + my $ifaces = [ $vnetid, $bridge ]; + + push @$ifaces, $zonebridge; + push @$ifaces, $zone_notag_uplink; + push @$ifaces, $zone_notag_uplinkpeer; + + if (!$vlan_aware) { + my $svlan_iface = "sv_$zone"; + push @$ifaces, $svlan_iface; + } + + foreach my $iface (@{$ifaces}) { + if (!$status->{$iface}->{status}) { + push @$err_msg, "missing $iface"; + } elsif ($status->{$iface}->{status} ne 'pass') { + push @$err_msg, "error $iface"; + } + } + return $err_msg; +} + +sub vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + + my $tag = $vnet->{tag}; + raise_param_exc({ tag => "VLAN tag maximal value is 4096" }) if $tag && $tag > 4096; + + # verify that tag is not already defined in another vnet on same zone + for my $id (sort keys %{$vnet_cfg->{ids}}) { + next if $id eq $vnetid; + my $other_vnet = $vnet_cfg->{ids}->{$id}; + next if $vnet->{zone} ne $other_vnet->{zone}; + my $other_tag = $other_vnet->{tag}; + if ($tag) { + raise_param_exc({ tag => "tag $tag already exist in zone $vnet->{zone} vnet $id"}) + if $other_tag && $tag eq $other_tag; + } else { + raise_param_exc({ tag => "tag-less vnet already exists in zone $vnet->{zone} vnet $id"}) + if !$other_tag; + } + } +} + +1; + + diff --git a/src/PVE/Network/SDN/Zones/SimplePlugin.pm b/src/PVE/Network/SDN/Zones/SimplePlugin.pm new file mode 100644 index 0000000..7757747 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/SimplePlugin.pm @@ -0,0 +1,159 @@ +package PVE::Network::SDN::Zones::SimplePlugin; + +use strict; +use warnings; +use PVE::Network::SDN::Zones::Plugin; +use PVE::Exception qw(raise raise_param_exc); +use PVE::Cluster; +use PVE::Tools; + +use base('PVE::Network::SDN::Zones::Plugin'); + +sub type { + return 'simple'; +} + +sub properties { + return { + dns => { + type => 'string', + description => "dns api server", + }, + reversedns => { + type => 'string', + description => "reverse dns api server", + }, + dnszone => { + type => 'string', format => 'dns-name', + description => "dns domain zone ex: mydomain.com", + } + }; +} + +sub options { + return { + nodes => { optional => 1}, + mtu => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + return $config if$config->{$vnetid}; # nothing to do + + my $mac = $vnet->{mac}; + my $alias = $vnet->{alias}; + my $mtu = $plugin_config->{mtu} if $plugin_config->{mtu}; + + # vnet bridge + my @iface_config = (); + + my $address = {}; + my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1); + + my $ipv4 = undef; + my $ipv6 = undef; + my $enable_forward_v4 = undef; + my $enable_forward_v6 = undef; + + foreach my $subnetid (sort keys %{$subnets}) { + my $subnet = $subnets->{$subnetid}; + my $cidr = $subnet->{cidr}; + my $mask = $subnet->{mask}; + + my $gateway = $subnet->{gateway}; + if ($gateway) { + push @iface_config, "address $gateway/$mask" if !defined($address->{$gateway}); + $address->{$gateway} = 1; + } + + my $iptables = undef; + my $checkrouteip = undef; + my $ipversion = Net::IP::ip_is_ipv6($gateway) ? 6 : 4; + + if ( $ipversion == 6) { + $ipv6 = 1; + $iptables = "ip6tables"; + $checkrouteip = '2001:4860:4860::8888'; + $enable_forward_v6 = 1 if $gateway; + } else { + $ipv4 = 1; + $iptables = "iptables"; + $checkrouteip = '8.8.8.8'; + $enable_forward_v4 = 1 if $gateway; + } + + #add route for /32 pointtopoint + push @iface_config, "up ip route add $cidr dev $vnetid" if $mask == 32 && $ipversion == 4; + if ($subnet->{snat}) { + #find outgoing interface + my ($outip, $outiface) = PVE::Network::SDN::Zones::Plugin::get_local_route_ip($checkrouteip); + if ($outip && $outiface) { + #use snat, faster than masquerade + push @iface_config, "post-up $iptables -t nat -A POSTROUTING -s '$cidr' -o $outiface -j SNAT --to-source $outip"; + push @iface_config, "post-down $iptables -t nat -D POSTROUTING -s '$cidr' -o $outiface -j SNAT --to-source $outip"; + #add conntrack zone once on outgoing interface + push @iface_config, "post-up $iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1"; + push @iface_config, "post-down $iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1"; + } + } + } + + push @iface_config, "hwaddress $mac" if $mac; + push @iface_config, "bridge_ports none"; + push @iface_config, "bridge_stp off"; + push @iface_config, "bridge_fd 0"; + if ($vnet->{vlanaware}) { + push @iface_config, "bridge-vlan-aware yes"; + push @iface_config, "bridge-vids 2-4094"; + } + push @iface_config, "mtu $mtu" if $mtu; + push @iface_config, "alias $alias" if $alias; + push @iface_config, "ip-forward on" if $enable_forward_v4; + push @iface_config, "ip6-forward on" if $enable_forward_v6; + + push @{$config->{$vnetid}}, @iface_config; + + return $config; +} + +sub status { + my ($class, $plugin_config, $zone, $vnetid, $vnet, $status) = @_; + + # ifaces to check + my $ifaces = [ $vnetid ]; + my $err_msg = []; + foreach my $iface (@{$ifaces}) { + if (!$status->{$iface}->{status}) { + push @$err_msg, "missing $iface"; + } elsif ($status->{$iface}->{status} ne 'pass') { + push @$err_msg, "error iface $iface"; + } + } + return $err_msg; +} + + +sub vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + my $tag = $vnet->{tag}; + + raise_param_exc({ tag => "vlan tag is not allowed on simple zone"}) if defined($tag); + + if (!defined($vnet->{mac})) { + my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg'); + $vnet->{mac} = PVE::Tools::random_ether_addr($dc->{mac_prefix}); + } +} + +1; + + diff --git a/src/PVE/Network/SDN/Zones/VlanPlugin.pm b/src/PVE/Network/SDN/Zones/VlanPlugin.pm new file mode 100644 index 0000000..0bb6b8a --- /dev/null +++ b/src/PVE/Network/SDN/Zones/VlanPlugin.pm @@ -0,0 +1,199 @@ +package PVE::Network::SDN::Zones::VlanPlugin; + +use strict; +use warnings; +use PVE::Network::SDN::Zones::Plugin; +use PVE::Exception qw(raise raise_param_exc); + +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 { + 'bridge' => { + type => 'string', + }, + 'bridge-disable-mac-learning' => { + type => 'boolean', + description => "Disable auto mac learning.", + } + }; +} + +sub options { + + return { + nodes => { optional => 1}, + 'bridge' => { optional => 0 }, + 'bridge-disable-mac-learning' => { optional => 1 }, + mtu => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + my $bridge = $plugin_config->{bridge}; + PVE::Network::SDN::Zones::Plugin::find_bridge($bridge); + + my $vlan_aware = PVE::Network::SDN::Zones::Plugin::is_vlanaware($bridge); + my $is_ovs = PVE::Network::SDN::Zones::Plugin::is_ovs($bridge); + + my $tag = $vnet->{tag}; + my $alias = $vnet->{alias}; + my $mtu = $plugin_config->{mtu}; + + my $vnet_uplink = "ln_".$vnetid; + my $vnet_uplinkpeer = "pr_".$vnetid; + + my @iface_config = (); + + if($is_ovs) { + + # keep vmbrXvY for compatibility with existing network + # eth0----ovs vmbr0--(ovsintport tag)---->vnet---->vm + + @iface_config = (); + push @iface_config, "ovs_type OVSIntPort"; + push @iface_config, "ovs_bridge $bridge"; + push @iface_config, "ovs_mtu $mtu" if $mtu; + if($vnet->{vlanaware}) { + push @iface_config, "ovs_options vlan_mode=dot1q-tunnel other_config:qinq-ethtype=802.1q tag=$tag"; + } else { + push @iface_config, "ovs_options tag=$tag"; + } + push(@{$config->{$vnet_uplink}}, @iface_config) if !$config->{$vnet_uplink}; + + #redefine main ovs bridge, ifupdown2 will merge ovs_ports + @iface_config = (); + push @iface_config, "ovs_ports $vnet_uplink"; + push(@{$config->{$bridge}}, @iface_config); + + } elsif ($vlan_aware) { + # eth0----vlanaware bridge vmbr0--(vmbr0.X tag)---->vnet---->vm + $vnet_uplink = "$bridge.$tag"; + } else { + + # keep vmbrXvY for compatibility with existing network + # eth0<---->eth0.X----vmbr0v10------vnet---->vm + + my $bridgevlan = $bridge."v".$tag; + + my @bridge_ifaces = PVE::Network::SDN::Zones::Plugin::get_bridge_ifaces($bridge); + + my $bridge_ports = ""; + foreach my $bridge_iface (@bridge_ifaces) { + $bridge_ports .= " $bridge_iface.$tag"; + } + + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "veth-peer-name $vnet_uplinkpeer"; + push(@{$config->{$vnet_uplink}}, @iface_config) if !$config->{$vnet_uplink}; + + @iface_config = (); + push @iface_config, "link-type veth"; + push @iface_config, "veth-peer-name $vnet_uplink"; + push(@{$config->{$vnet_uplinkpeer}}, @iface_config) if !$config->{$vnet_uplinkpeer}; + + @iface_config = (); + push @iface_config, "bridge_ports $bridge_ports $vnet_uplinkpeer"; + push @iface_config, "bridge_stp off"; + push @iface_config, "bridge_fd 0"; + push(@{$config->{$bridgevlan}}, @iface_config) if !$config->{$bridgevlan}; + } + + #vnet bridge + @iface_config = (); + push @iface_config, "bridge_ports $vnet_uplink"; + push @iface_config, "bridge_stp off"; + push @iface_config, "bridge_fd 0"; + if($vnet->{vlanaware}) { + push @iface_config, "bridge-vlan-aware yes"; + push @iface_config, "bridge-vids 2-4094"; + } + 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 status { + my ($class, $plugin_config, $zone, $vnetid, $vnet, $status) = @_; + + my $bridge = $plugin_config->{bridge}; + + my $err_msg = []; + if (!-d "/sys/class/net/$bridge") { + push @$err_msg, "missing $bridge"; + return $err_msg; + } + + my $vlan_aware = PVE::Network::SDN::Zones::Plugin::is_vlanaware($bridge); + my $is_ovs = PVE::Network::SDN::Zones::Plugin::is_ovs($bridge); + + my $tag = $vnet->{tag}; + my $vnet_uplink = "ln_".$vnetid; + my $vnet_uplinkpeer = "pr_".$vnetid; + + # ifaces to check + my $ifaces = [ $vnetid, $bridge ]; + if($is_ovs) { + push @$ifaces, $vnet_uplink; + } elsif (!$vlan_aware) { + my $bridgevlan = $bridge."v".$tag; + push @$ifaces, $bridgevlan; + push @$ifaces, $vnet_uplink; + push @$ifaces, $vnet_uplinkpeer; + } + + foreach my $iface (@{$ifaces}) { + if (!$status->{$iface}->{status}) { + push @$err_msg, "missing $iface"; + } elsif ($status->{$iface}->{status} ne 'pass') { + push @$err_msg, "error iface $iface"; + } + } + return $err_msg; +} + +sub vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + my $tag = $vnet->{tag}; + + raise_param_exc({ tag => "missing vlan tag"}) if !defined($vnet->{tag}); + raise_param_exc({ tag => "vlan tag max value is 4096"}) if $vnet->{tag} > 4096; + + # verify that tag is not already defined in another vnet on same zone + foreach my $id (keys %{$vnet_cfg->{ids}}) { + next if $id eq $vnetid; + my $othervnet = $vnet_cfg->{ids}->{$id}; + my $other_tag = $othervnet->{tag}; + next if $vnet->{zone} ne $othervnet->{zone}; + raise_param_exc({ tag => "tag $tag already exist in vnet $id"}) if $other_tag && $tag eq $other_tag; + } +} + +1; + + diff --git a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm new file mode 100644 index 0000000..c523cf7 --- /dev/null +++ b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm @@ -0,0 +1,118 @@ +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 PVE::Network::SDN::Controllers::EvpnPlugin; +use PVE::Exception qw(raise raise_param_exc); + +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; +} + +sub type { + return 'vxlan'; +} + +sub properties { + return { + 'peers' => { + description => "peers address list.", + type => 'string', format => 'ip-list' + }, + }; +} + +sub options { + return { + nodes => { optional => 1}, + peers => { optional => 0 }, + mtu => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + ipam => { optional => 1 }, + }; +} + +# Plugin implementation +sub generate_sdn_config { + my ($class, $plugin_config, $zoneid, $vnetid, $vnet, $controller, $controller_cfg, $subnet_cfg, $interfaces_config, $config) = @_; + + my $tag = $vnet->{tag}; + my $alias = $vnet->{alias}; + my $multicastaddress = $plugin_config->{'multicast-address'}; + my @peers; + @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'}; + my $vxlan_iface = "vxlan_$vnetid"; + + die "missing vxlan tag" if !$tag; + + my ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers); + + my $mtu = 1450; + $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu}; + $mtu = $plugin_config->{mtu} if $plugin_config->{mtu}; + + #vxlan interface + my @iface_config = (); + push @iface_config, "vxlan-id $tag"; + + for my $address (@peers) { + next if $address eq $ifaceip; + push @iface_config, "vxlan_remoteip $address"; + } + + + push @iface_config, "mtu $mtu" if $mtu; + push(@{$config->{$vxlan_iface}}, @iface_config) if !$config->{$vxlan_iface}; + + #vnet bridge + @iface_config = (); + push @iface_config, "bridge_ports $vxlan_iface"; + push @iface_config, "bridge_stp off"; + push @iface_config, "bridge_fd 0"; + if ($vnet->{vlanaware}) { + push @iface_config, "bridge-vlan-aware yes"; + push @iface_config, "bridge-vids 2-4094"; + } + 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 vnet_update_hook { + my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_; + + my $vnet = $vnet_cfg->{ids}->{$vnetid}; + my $tag = $vnet->{tag}; + + raise_param_exc({ tag => "missing vxlan tag"}) if !defined($tag); + raise_param_exc({ tag => "vxlan tag max value is 16777216"}) if $tag > 16777216; + + # verify that tag is not already defined globally (vxlan-id are unique) + for my $id (sort keys %{$vnet_cfg->{ids}}) { + next if $id eq $vnetid; + my $othervnet = $vnet_cfg->{ids}->{$id}; + my $other_tag = $othervnet->{tag}; + my $other_zoneid = $othervnet->{zone}; + my $other_zone = $zone_cfg->{ids}->{$other_zoneid}; + next if $other_zone->{type} ne 'vxlan' && $other_zone->{type} ne 'evpn'; + raise_param_exc({ tag => "vxlan tag $tag already exist in vnet $id in zone $other_zoneid "}) if $other_tag && $tag eq $other_tag; + } +} + +1; + + |
