From ee4f339e8026c7dbe793e112bdcb5b1981c6a66e Mon Sep 17 00:00:00 2001 From: Alexandre Derumier Date: Mon, 5 Oct 2020 17:08:52 +0200 Subject: [PATCH] add DNS plugin Signed-off-by: Alexandre Derumier --- PVE/API2/Network/SDN.pm | 7 + PVE/API2/Network/SDN/Dns.pm | 242 ++++++++++++++++++++++++++ PVE/API2/Network/SDN/Makefile | 2 +- PVE/Network/SDN/Dns.pm | 57 ++++++ PVE/Network/SDN/Dns/Makefile | 8 + PVE/Network/SDN/Dns/Plugin.pm | 117 +++++++++++++ PVE/Network/SDN/Dns/PowerdnsPlugin.pm | 201 +++++++++++++++++++++ PVE/Network/SDN/Ipams/Plugin.pm | 2 +- PVE/Network/SDN/Makefile | 3 +- PVE/Network/SDN/SubnetPlugin.pm | 54 ++++-- PVE/Network/SDN/Subnets.pm | 172 ++++++++++++++++-- PVE/Network/SDN/Vnets.pm | 12 +- 12 files changed, 831 insertions(+), 46 deletions(-) create mode 100644 PVE/API2/Network/SDN/Dns.pm create mode 100644 PVE/Network/SDN/Dns.pm create mode 100644 PVE/Network/SDN/Dns/Makefile create mode 100644 PVE/Network/SDN/Dns/Plugin.pm create mode 100644 PVE/Network/SDN/Dns/PowerdnsPlugin.pm diff --git a/PVE/API2/Network/SDN.pm b/PVE/API2/Network/SDN.pm index 736204f..4aef654 100644 --- a/PVE/API2/Network/SDN.pm +++ b/PVE/API2/Network/SDN.pm @@ -16,6 +16,7 @@ use PVE::API2::Network::SDN::Vnets; use PVE::API2::Network::SDN::Zones; use PVE::API2::Network::SDN::Subnets; use PVE::API2::Network::SDN::Ipams; +use PVE::API2::Network::SDN::Dns; use base qw(PVE::RESTHandler); @@ -44,6 +45,11 @@ __PACKAGE__->register_method ({ path => 'ipams', }); +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Dns", + path => 'dns', +}); + __PACKAGE__->register_method({ name => 'index', path => '', @@ -75,6 +81,7 @@ __PACKAGE__->register_method({ { id => 'controllers' }, { id => 'subnets' }, { id => 'ipams' }, + { id => 'dns' }, ]; return $res; diff --git a/PVE/API2/Network/SDN/Dns.pm b/PVE/API2/Network/SDN/Dns.pm new file mode 100644 index 0000000..3d08552 --- /dev/null +++ b/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/'", + 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/PVE/API2/Network/SDN/Makefile b/PVE/API2/Network/SDN/Makefile index 1117dfa..3683fa4 100644 --- a/PVE/API2/Network/SDN/Makefile +++ b/PVE/API2/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm +SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm PERL5DIR=${DESTDIR}/usr/share/perl5 diff --git a/PVE/Network/SDN/Dns.pm b/PVE/Network/SDN/Dns.pm new file mode 100644 index 0000000..c2e153a --- /dev/null +++ b/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/PVE/Network/SDN/Dns/Makefile b/PVE/Network/SDN/Dns/Makefile new file mode 100644 index 0000000..81cd2a1 --- /dev/null +++ b/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/PVE/Network/SDN/Dns/Plugin.pm b/PVE/Network/SDN/Dns/Plugin.pm new file mode 100644 index 0000000..baa9316 --- /dev/null +++ b/PVE/Network/SDN/Dns/Plugin.pm @@ -0,0 +1,117 @@ +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 JSON; + +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 }, + 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, $type, $zone, $reversezone, $hostname, $ip) = @_; +} + +sub del_a_record { + my ($class, $plugin_config, $hostname, $ip) = @_; +} + +sub on_update_hook { + my ($class, $plugin_config) = @_; +} + +#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 from_json($raw) if $raw ne ''; +} + +1; diff --git a/PVE/Network/SDN/Dns/PowerdnsPlugin.pm b/PVE/Network/SDN/Dns/PowerdnsPlugin.pm new file mode 100644 index 0000000..f02c2f1 --- /dev/null +++ b/PVE/Network/SDN/Dns/PowerdnsPlugin.pm @@ -0,0 +1,201 @@ +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 base('PVE::Network::SDN::Dns::Plugin'); + +sub type { + return 'powerdns'; +} + +sub properties { + return { + url => { + type => 'string', + }, + key => { + type => 'string', + }, + }; +} + +sub options { + + return { + url => { optional => 0}, + key => { optional => 0 }, + ttl => { optional => 1 }, + }; +} + +# Plugin implementation + +sub add_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip) = @_; + + 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 $record = { content => $ip, + disabled => JSON::false, + name => $fqdn, + type => $type, + priority => 0 }; + + my $rrset = { name => $fqdn, + type => $type, + ttl => $ttl, + changetype => "REPLACE", + records => [ $record ] }; + + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::Dns::Plugin::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error add $fqdn to zone $zone: $@"; + } +} + +sub add_ptr_record { + my ($class, $plugin_config, $zone, $hostname, $ip) = @_; + + 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 = join(".", reverse(split(/\./, $ip))).".in-addr.arpa."; + 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::Dns::Plugin::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error add $reverseip to zone $zone: $@"; + } +} + +sub del_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip) = @_; + + 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 $rrset = { name => $fqdn, + type => $type, + changetype => "DELETE", + records => [] }; + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::Dns::Plugin::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error delete $fqdn from zone $zone: $@"; + } +} + +sub del_ptr_record { + my ($class, $plugin_config, $zone, $ip) = @_; + + 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 = join(".", reverse(split(/\./, $ip))).".in-addr.arpa."; + my $type = "PTR"; + + my $rrset = { name => $reverseip, + type => $type, + changetype => "DELETE", + records => [] }; + + my $params = { rrsets => [ $rrset ] }; + + eval { + PVE::Network::SDN::Dns::Plugin::api_request("PATCH", "$url/zones/$zone", $headers, $params); + }; + + if ($@) { + die "error delete $reverseip from zone $zone: $@"; + } +} + +sub verify_zone { + my ($class, $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]; + + eval { + PVE::Network::SDN::Dns::Plugin::api_request("GET", "$url/zones/$zone", $headers); + }; + + if ($@) { + die "can't read zone $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::Dns::Plugin::api_request("GET", "$url", $headers); + }; + + if ($@) { + die "dns api error: $@"; + } +} + +1; + + diff --git a/PVE/Network/SDN/Ipams/Plugin.pm b/PVE/Network/SDN/Ipams/Plugin.pm index e31fb05..9330ba9 100644 --- a/PVE/Network/SDN/Ipams/Plugin.pm +++ b/PVE/Network/SDN/Ipams/Plugin.pm @@ -114,7 +114,7 @@ sub api_request { my $response = $ua->request($req); my $code = $response->code; - if ($code !~ /2(\d+)$/) { + if ($code !~ /^2(\d+)$/) { my $msg = $response->message || 'unknown'; die "Invalid response from server: $code $msg\n"; } diff --git a/PVE/Network/SDN/Makefile b/PVE/Network/SDN/Makefile index fb68856..92cfcd0 100644 --- a/PVE/Network/SDN/Makefile +++ b/PVE/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm +SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm PERL5DIR=${DESTDIR}/usr/share/perl5 @@ -9,4 +9,5 @@ install: make -C Controllers install make -C Zones install make -C Ipams install + make -C Dns install diff --git a/PVE/Network/SDN/SubnetPlugin.pm b/PVE/Network/SDN/SubnetPlugin.pm index 6224065..32a9d78 100644 --- a/PVE/Network/SDN/SubnetPlugin.pm +++ b/PVE/Network/SDN/SubnetPlugin.pm @@ -65,22 +65,25 @@ sub properties { type => 'string', description => "static routes [network=:gateway=,network=:gateway=,... ]", }, - #cloudinit, dhcp options - nameservers => { - type => 'string', format => 'address-list', - description => " dns nameserver", + dns => { + type => 'string', + description => "dns api server", }, - #cloudinit, dhcp options - searchdomain => { + reversedns => { type => 'string', + description => "reverse dns api server", }, - dhcp => { - type => 'boolean', - description => "enable dhcp for this subnet", + dnszone => { + type => 'string', + description => "dns domain zone ex: mydomain.com", }, - dns_driver => { + reversednszone => { type => 'string', - description => "Develop some dns registrations plugins (powerdns,...)", + description => "reverse dns zone ex: 0.168.192.in-addr.arpa", + }, + dnszoneprefix => { + type => 'string', + description => "dns domain zone prefix ex: 'adm' -> .adm.mydomain.com", }, ipam => { type => 'string', @@ -93,11 +96,12 @@ sub options { return { gateway => { optional => 1 }, routes => { optional => 1 }, - nameservers => { optional => 1 }, - searchdomain => { optional => 1 }, snat => { optional => 1 }, - dhcp => { optional => 1 }, - dns_driver => { optional => 1 }, + dns => { optional => 1 }, + reversedns => { optional => 1 }, + dnszone => { optional => 1 }, + reversednszone => { optional => 1 }, + dnszoneprefix => { optional => 1 }, ipam => { optional => 1 }, }; } @@ -105,12 +109,26 @@ sub options { sub on_update_hook { my ($class, $subnetid, $subnet_cfg) = @_; - my $subnet = $subnetid =~ s/-/\//r; - my $subnet_matcher = subnet_matcher($subnet); + my $cidr = $subnetid =~ s/-/\//r; + my $subnet_matcher = subnet_matcher($cidr); + + my $subnet = $subnet_cfg->{ids}->{$subnetid}; - my $gateway = $subnet_cfg->{ids}->{$subnetid}->{gateway}; + my $gateway = $subnet->{gateway}; + my $dns = $subnet->{dns}; + my $dnszone = $subnet->{dnszone}; + my $reversedns = $subnet->{reversedns}; + my $reversednszone = $subnet->{reversednszone}; + + #to: for /32 pointotoping, allow gateway outside the subnet raise_param_exc({ gateway => "$gateway is not in subnet $subnet"}) if $gateway && !$subnet_matcher->($gateway); + raise_param_exc({ dns => "missing dns provider"}) if $dnszone && !$dns; + raise_param_exc({ dnszone => "missing dns zone"}) if $dns && !$dnszone; + raise_param_exc({ reversedns => "missing dns provider"}) if $reversednszone && !$reversedns; + raise_param_exc({ reversednszone => "missing dns zone"}) if $reversedns && !$reversednszone; + raise_param_exc({ reversedns => "missing forward dns zone"}) if $reversednszone && !$dnszone; + } sub on_delete_hook { diff --git a/PVE/Network/SDN/Subnets.pm b/PVE/Network/SDN/Subnets.pm index 3ce2d44..07ef688 100644 --- a/PVE/Network/SDN/Subnets.pm +++ b/PVE/Network/SDN/Subnets.pm @@ -5,8 +5,10 @@ use warnings; use Net::Subnet qw(subnet_matcher); use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use Net::IP; use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Dns; use PVE::Network::SDN::SubnetPlugin; PVE::Network::SDN::SubnetPlugin->register(); PVE::Network::SDN::SubnetPlugin->init(); @@ -75,41 +77,173 @@ sub find_ip_subnet { return ($subnetid, $subnet); } +my $verify_dns_zone = sub { + 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); +}; + +my $add_dns_record = sub { + my ($zone, $dns, $hostname, $dnszoneprefix, $ip) = @_; + return if !$zone || !$dns || !$hostname || !$ip; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + 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); + +}; + +my $add_dns_ptr_record = sub { + my ($reversezone, $zone, $dns, $hostname, $dnszoneprefix, $ip) = @_; + + return if !$zone || !$reversezone || !$dns || !$hostname || !$ip; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + $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); +}; + +my $del_dns_record = sub { + my ($zone, $dns, $hostname, $dnszoneprefix, $ip) = @_; + + return if !$zone || !$dns || !$hostname || !$ip; + + $hostname .= ".$dnszoneprefix" if $dnszoneprefix; + + 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); +}; + +my $del_dns_ptr_record = sub { + 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 next_free_ip { - my ($subnetid, $subnet) = @_; + my ($subnetid, $subnet, $hostname) = @_; + + my $cidr = undef; + my $ip = undef; my $ipamid = $subnet->{ipam}; - return if !$ipamid; + my $dns = $subnet->{dns}; + my $dnszone = $subnet->{dnszone}; + my $reversedns = $subnet->{reversedns}; + my $reversednszone = $subnet->{reversednszone}; + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + #verify dns zones before ipam + &$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}); + $cidr = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet); + ($ip, undef) = split(/\//, $cidr); + } - 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}); - my $ip = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet); - return $ip; + eval { + #add dns + &$add_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + #add reverse dns + &$add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $dnszoneprefix, $ip); + }; + if ($@) { + #rollback + my $err = $@; + eval { + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostname) + }; + die $err; + } + return $cidr; } sub add_ip { - my ($subnetid, $subnet, $ip) = @_; + my ($subnetid, $subnet, $ip, $hostname) = @_; my $ipamid = $subnet->{ipam}; - return if !$ipamid; + my $dns = $subnet->{dns}; + my $dnszone = $subnet->{dnszone}; + my $reversedns = $subnet->{reversedns}; + my $reversednszone = $subnet->{reversednszone}; + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + #verify dns zones before ipam + &$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->add_ip($plugin_config, $subnetid, $ip); + } - 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->add_ip($plugin_config, $subnetid, $ip); + eval { + #add dns + &$add_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + #add reverse dns + &$add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $dnszoneprefix, $ip); + }; + if ($@) { + #rollback + my $err = $@; + eval { + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostname) + }; + die $err; + } } sub del_ip { - my ($subnetid, $subnet, $ip) = @_; + my ($subnetid, $subnet, $ip, $hostname) = @_; my $ipamid = $subnet->{ipam}; - return if !$ipamid; + my $dns = $subnet->{dns}; + my $dnszone = $subnet->{dnszone}; + my $reversedns = $subnet->{reversedns}; + my $reversednszone = $subnet->{reversednszone}; + my $dnszoneprefix = $subnet->{dnszoneprefix}; + + &$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, $ip); + } - 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, $ip); + eval { + &$del_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + &$del_dns_ptr_record($reversednszone, $reversedns, $ip); + }; + if ($@) { + warn $@; + } } 1; diff --git a/PVE/Network/SDN/Vnets.pm b/PVE/Network/SDN/Vnets.pm index 6ea3a9a..c9916b1 100644 --- a/PVE/Network/SDN/Vnets.pm +++ b/PVE/Network/SDN/Vnets.pm @@ -55,7 +55,7 @@ sub get_vnet { } sub get_next_free_ip { - my ($vnet, $ipversion) = @_; + my ($vnet, $hostname, $ipversion) = @_; $ipversion = 4 if !$ipversion; my $subnets_cfg = PVE::Network::SDN::Subnets::config(); @@ -71,7 +71,7 @@ sub get_next_free_ip { $subnet = $subnets_cfg->{ids}->{$subnetid}; if ($subnet && $subnet->{ipam}) { eval { - $ip = PVE::Network::SDN::Subnets::next_free_ip($subnetid, $subnet); + $ip = PVE::Network::SDN::Subnets::next_free_ip($subnetid, $subnet, $hostname); }; warn $@ if $@; } @@ -83,23 +83,23 @@ sub get_next_free_ip { } sub add_ip { - my ($vnet, $cidr, $name) = @_; + my ($vnet, $cidr, $hostname) = @_; my ($ip, $mask) = split(/\//, $cidr); my ($subnetid, $subnet) = PVE::Network::SDN::Subnets::find_ip_subnet($ip, $vnet->{subnets}); return if !$subnet->{ipam}; - PVE::Network::SDN::Subnets::add_ip($subnetid, $subnet, $ip); + PVE::Network::SDN::Subnets::add_ip($subnetid, $subnet, $ip, $hostname); } sub del_ip { - my ($vnet, $cidr) = @_; + my ($vnet, $cidr, $hostname) = @_; my ($ip, $mask) = split(/\//, $cidr); my ($subnetid, $subnet) = PVE::Network::SDN::Subnets::find_ip_subnet($ip, $vnet->{subnets}); return if !$subnet->{ipam}; - PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip); + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostname); } 1; -- 2.39.2