diff options
| author | Thomas Lamprecht <t.lamprecht@proxmox.com> | 2023-05-25 18:10:14 +0200 |
|---|---|---|
| committer | Thomas Lamprecht <t.lamprecht@proxmox.com> | 2023-05-25 18:18:57 +0200 |
| commit | 6029cbb071c3722c717eebbafaf1b373f3edaadc (patch) | |
| tree | 456d7aff44d2ae220d1671f77da7528174d53fe6 /src/PVE | |
| parent | cead0f28af4aceee83af6636d4f5ffb2d2f6c6b1 (diff) | |
separate packaging and source build system
like almost all of our repos do nowadays, modern git can detect such
things on rebase so in development stuff should be hopefully not too
much affected by this.
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Diffstat (limited to 'src/PVE')
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; + + |
