From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 7CB976368B for ; Tue, 25 Aug 2020 07:21:56 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6D9922320E for ; Tue, 25 Aug 2020 07:21:26 +0200 (CEST) Received: from mailpro.odiso.net (mailpro.odiso.net [89.248.211.110]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 376AE22D18 for ; Tue, 25 Aug 2020 07:20:59 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by mailpro.odiso.net (Postfix) with ESMTP id 19AD715B9E32; Tue, 25 Aug 2020 07:20:59 +0200 (CEST) Received: from mailpro.odiso.net ([127.0.0.1]) by localhost (mailpro.odiso.net [127.0.0.1]) (amavisd-new, port 10032) with ESMTP id uTmZ2negcO2t; Tue, 25 Aug 2020 07:20:59 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by mailpro.odiso.net (Postfix) with ESMTP id 00C8015B9EAB; Tue, 25 Aug 2020 07:20:59 +0200 (CEST) X-Virus-Scanned: amavisd-new at mailpro.odiso.com Received: from mailpro.odiso.net ([127.0.0.1]) by localhost (mailpro.odiso.net [127.0.0.1]) (amavisd-new, port 10026) with ESMTP id v2bhQX5uEnOb; Tue, 25 Aug 2020 07:20:58 +0200 (CEST) Received: from pve.fritz.box (unknown [213.211.148.86]) by mailpro.odiso.net (Postfix) with ESMTPSA id 63E1815B9E32; Tue, 25 Aug 2020 07:20:58 +0200 (CEST) From: Alexandre Derumier To: pve-devel@lists.proxmox.com Date: Tue, 25 Aug 2020 07:02:19 +0200 Message-Id: <20200825050222.12447-16-aderumier@odiso.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200825050222.12447-1-aderumier@odiso.com> References: <20200825050222.12447-1-aderumier@odiso.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-SPAM-LEVEL: Spam detection results: 0 AWL 0.000 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [pveplugin.pm, in-addr.arpa, zones.pm, powerdnsplugin.pm, plugin.pm, mydomain.com, vnets.pm, 192.in-addr.arpa, vnetplugin.pm, dns.pm, sdn.pm, controllers.pm, ipams.pm, subnetplugin.pm] Subject: [pve-devel] [PATCH v6 pve-network 15/18] add dns plugin X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 25 Aug 2020 05:21:56 -0000 --- 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/PVEPlugin.pm | 1 - PVE/Network/SDN/Ipams/Plugin.pm | 2 +- PVE/Network/SDN/Makefile | 3 +- PVE/Network/SDN/SubnetPlugin.pm | 53 ++++-- PVE/Network/SDN/Subnets.pm | 156 +++++++++++++++-- PVE/Network/SDN/Vnets.pm | 12 +- 13 files changed, 814 insertions(+), 47 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 6055fe5..0a5fa33 100644 --- a/PVE/API2/Network/SDN.pm +++ b/PVE/API2/Network/SDN.pm @@ -17,6 +17,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; =20 use base qw(PVE::RESTHandler); =20 @@ -45,6 +46,11 @@ __PACKAGE__->register_method ({ path =3D> 'ipams', }); =20 +__PACKAGE__->register_method ({ + subclass =3D> "PVE::API2::Network::SDN::Dns", + path =3D> 'dns', +}); + __PACKAGE__->register_method({ name =3D> 'index', path =3D> '', @@ -76,6 +82,7 @@ __PACKAGE__->register_method({ { id =3D> 'controllers' }, { id =3D> 'subnets' }, { id =3D> 'ipams' }, + { id =3D> 'dns' }, ]; =20 return $res; diff --git a/PVE/API2/Network/SDN/Dns.pm b/PVE/API2/Network/SDN/Dns.pm new file mode 100644 index 0000000..ea26af3 --- /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 =3D PVE::Network::SDN::Dns::Plugin->lookup_types()= ; + +my $api_sdn_dns_config =3D sub { + my ($cfg, $id) =3D @_; + + my $scfg =3D dclone(PVE::Network::SDN::Dns::sdn_dns_config($cfg, $id= )); + $scfg->{dns} =3D $id; + $scfg->{digest} =3D $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name =3D> 'index', + path =3D> '', + method =3D> 'GET', + description =3D> "SDN dns index.", + permissions =3D> { + description =3D> "Only list entries where you have 'SDN.Audit' or 'SDN.= Allocate' permissions on '/sdn/dns/'", + user =3D> 'all', + }, + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + type =3D> { + description =3D> "Only list sdn dns of specific type", + type =3D> 'string', + enum =3D> $sdn_dns_type_enum, + optional =3D> 1, + }, + }, + }, + returns =3D> { + type =3D> 'array', + items =3D> { + type =3D> "object", + properties =3D> { dns =3D> { type =3D> 'string'}, + type =3D> { type =3D> 'string'}, + }, + }, + links =3D> [ { rel =3D> 'child', href =3D> "{dns}" } ], + }, + code =3D> sub { + my ($param) =3D @_; + + my $rpcenv =3D PVE::RPCEnvironment::get(); + my $authuser =3D $rpcenv->get_user(); + + + my $cfg =3D PVE::Network::SDN::Dns::config(); + + my @sids =3D PVE::Network::SDN::Dns::sdn_dns_ids($cfg); + my $res =3D []; + foreach my $id (@sids) { + my $privs =3D [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/dns/$id", $privs, 1); + + my $scfg =3D &$api_sdn_dns_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config =3D $cfg->{ids}->{$id}; + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($plugin_confi= g->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name =3D> 'read', + path =3D> '{dns}', + method =3D> 'GET', + description =3D> "Read sdn dns configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/dns/{dns}', ['SDN.Allocate']], + }, + + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + dns =3D> get_standard_option('pve-sdn-dns-id'), + }, + }, + returns =3D> { type =3D> 'object' }, + code =3D> sub { + my ($param) =3D @_; + + my $cfg =3D PVE::Network::SDN::Dns::config(); + + return &$api_sdn_dns_config($cfg, $param->{dns}); + }}); + +__PACKAGE__->register_method ({ + name =3D> 'create', + protected =3D> 1, + path =3D> '', + method =3D> 'POST', + description =3D> "Create a new sdn dns object.", + permissions =3D> { + check =3D> ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters =3D> PVE::Network::SDN::Dns::Plugin->createSchema(), + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $type =3D extract_param($param, 'type'); + my $id =3D extract_param($param, 'dns'); + + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($type); + my $opts =3D $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 =3D PVE::Network::SDN::Dns::config(); + + my $scfg =3D undef; + if ($scfg =3D PVE::Network::SDN::Dns::sdn_dns_config($dns_cfg, $id, 1)= ) { + die "sdn dns object ID '$id' already defined\n"; + } + + $dns_cfg->{ids}->{$id} =3D $opts; + + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($scfg->{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 =3D> 'update', + protected =3D> 1, + path =3D> '{dns}', + method =3D> 'PUT', + description =3D> "Update sdn dns object configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters =3D> PVE::Network::SDN::Dns::Plugin->updateSchema(), + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $id =3D extract_param($param, 'dns'); + my $digest =3D extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $dns_cfg =3D PVE::Network::SDN::Dns::config(); + + PVE::SectionConfig::assert_if_modified($dns_cfg, $digest); + + my $scfg =3D PVE::Network::SDN::Dns::sdn_dns_config($dns_cfg, $id); + + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($scfg->{type}= ); + my $opts =3D $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} =3D $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 =3D> 'delete', + protected =3D> 1, + path =3D> '{dns}', + method =3D> 'DELETE', + description =3D> "Delete sdn dns object configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/dns', ['SDN.Allocate']], + }, + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + dns =3D> get_standard_option('pve-sdn-dns-id', { + completion =3D> \&PVE::Network::SDN::Dns::complete_sdn_d= ns, + }), + }, + }, + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $id =3D extract_param($param, 'dns'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg =3D PVE::Network::SDN::Dns::config(); + + my $scfg =3D PVE::Network::SDN::Dns::sdn_dns_config($cfg, $id); + + my $plugin =3D 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/Makefil= e index 1117dfa..3683fa4 100644 --- a/PVE/API2/Network/SDN/Makefile +++ b/PVE/API2/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=3DVnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm +SOURCES=3DVnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm =20 =20 PERL5DIR=3D${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) =3D @_; + + die "no sdn dns ID specified\n" if !$id; + + my $scfg =3D $cfg->{ids}->{$id}; + die "sdn '$id' does not exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config =3D cfs_read_file("sdn/dns.cfg"); + return $config; +} + +sub write_config { + my ($cfg) =3D @_; + + cfs_write_file("sdn/dns.cfg", $cfg); +} + +sub sdn_dns_ids { + my ($cfg) =3D @_; + + return keys %{$cfg->{ids}}; +} + +sub complete_sdn_dns { + my ($cmdname, $pname, $cvalue) =3D @_; + + my $cfg =3D PVE::Network::SDN::Dns::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Dns::sdn_dns_i= ds($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=3DPlugin.pm PowerdnsPlugin.pm + + +PERL5DIR=3D${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.p= m 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 =3D> "The SDN dns object identifier.", + type =3D> 'string', format =3D> 'pve-sdn-dns-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-dns-id', \&parse_sdn_dns_id); +sub parse_sdn_dns_id { + my ($id, $noerr) =3D @_; + + 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 =3D { + + propertyList =3D> { + type =3D> { + description =3D> "Plugin type.", + type =3D> 'string', format =3D> 'pve-configid', + }, + ttl =3D> { type =3D> 'integer', optional =3D> 1 }, + dns =3D> get_standard_option('pve-sdn-dns-id', + { completion =3D> \&PVE::Network::SDN::Dns::complete_sdn_dns= }), + }, +}; + +sub private { + return $defaultData; +} + +sub parse_section_header { + my ($class, $line) =3D @_; + + if ($line =3D~ m/^(\S+):\s*(\S+)\s*$/) { + my ($type, $id) =3D (lc($1), $2); + my $errmsg =3D undef; # set if you want to skip whole section + eval { PVE::JSONSchema::pve_verify_configid($type); }; + $errmsg =3D $@ if $@; + my $config =3D {}; # to return additional attributes + return ($type, $id, $errmsg, $config); + } + return undef; +} + + +sub add_a_record { + my ($class, $plugin_config, $type, $zone, $reversezone, $hostname, $= ip) =3D @_; +} + +sub del_a_record { + my ($class, $plugin_config, $hostname, $ip) =3D @_; +} + +sub on_update_hook { + my ($class, $plugin_config) =3D @_; +} + +#helpers +sub api_request { + my ($method, $url, $headers, $data) =3D @_; + + my $encoded_data =3D to_json($data) if $data; + + my $req =3D HTTP::Request->new($method,$url, $headers, $encoded_data= ); + + my $ua =3D LWP::UserAgent->new(protocols_allowed =3D> ['http', 'http= s'], timeout =3D> 30); + my $proxy =3D undef; + + if ($proxy) { + $ua->proxy(['http', 'https'], $proxy); + } else { + $ua->env_proxy; + } + + $ua->ssl_opts(verify_hostname =3D> 0, SSL_verify_mode =3D> 0x00); + + my $response =3D $ua->request($req); + my $code =3D $response->code; + + if ($code !~ /^2(\d+)$/) { + my $msg =3D $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } + + my $raw =3D ''; + if (defined($response->decoded_content)) { + $raw =3D $response->decoded_content; + } else { + $raw =3D $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..8c5dd90 --- /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 =3D> { + type =3D> 'string', + }, + key =3D> { + type =3D> 'string', + }, + }; +} + +sub options { + + return { + url =3D> { optional =3D> 0}, + key =3D> { optional =3D> 0 }, + ttl =3D> { optional =3D> 1 }, + }; +} + +# Plugin implementation + +sub add_a_record { + my ($class, $plugin_config, $zone, $hostname, $ip) =3D @_; + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $ttl =3D $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $key]; + + my $type =3D Net::IP::ip_is_ipv6($ip) ? "AAAA" : "A"; + my $fqdn =3D $hostname.".".$zone."."; + + + my $record =3D { content =3D> $ip,=20 + disabled =3D> JSON::false,=20 + name =3D> $fqdn,=20 + type =3D> $type,=20 + priority =3D> 0 }; + + my $rrset =3D { name =3D> $fqdn,=20 + type =3D> $type,=20 + ttl =3D> $ttl,=20 + changetype =3D> "REPLACE", + records =3D> [ $record ] }; + + + my $params =3D { rrsets =3D> [ $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) =3D @_; + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $ttl =3D $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $key]; + + my $reverseip =3D join(".", reverse(split(/\./, $ip)))."in-addr.arpa= ."; + my $fqdn =3D $hostname.".".$zone."."; + my $type =3D "PTR"; + + my $record =3D { content =3D> $fqdn,=20 + disabled =3D> JSON::false,=20 + name =3D> $reverseip,=20 + type =3D> $type,=20 + priority =3D> 0 }; + + my $rrset =3D { name =3D> $reverseip,=20 + type =3D> $type,=20 + ttl =3D> $ttl,=20 + changetype =3D> "REPLACE", + records =3D> [ $record ] }; + + + my $params =3D { rrsets =3D> [ $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) =3D @_; + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $key]; + my $fqdn =3D $hostname.".".$zone."."; + my $type =3D "PTR"; + + my $rrset =3D { name =3D> $fqdn,=20 + type =3D> $type,=20 + changetype =3D> "DELETE", + records =3D> [] }; + + my $params =3D { rrsets =3D> [ $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) =3D @_; + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $key]; + + my $reverseip =3D join(".", reverse(split(/\./, $ip)))."in-addr.arpa= ."; + my $type =3D "PTR"; + + my $rrset =3D { name =3D> $reverseip,=20 + type =3D> $type,=20 + changetype =3D> "DELETE", + records =3D> [] }; + + my $params =3D { rrsets =3D> [ $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) =3D @_; + + #verify that api is working =20 + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $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) =3D @_; + + #verify that api is working + + my $url =3D $plugin_config->{url}; + my $key =3D $plugin_config->{key}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'X-API-Key' =3D> $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/PVEPlugin.pm b/PVE/Network/SDN/Ipams/P= VEPlugin.pm index 0dfc8a4..99af0ed 100644 --- a/PVE/Network/SDN/Ipams/PVEPlugin.pm +++ b/PVE/Network/SDN/Ipams/PVEPlugin.pm @@ -99,7 +99,6 @@ sub add_next_freeip { while(1) { my $ip =3D $iplist->ip(); ++$iplist; - print "nextip: $ip\n"; next if defined($s->{ips}->{$ip}); $freeip =3D $ip; last; diff --git a/PVE/Network/SDN/Ipams/Plugin.pm b/PVE/Network/SDN/Ipams/Plug= in.pm index fc736b8..683346c 100644 --- a/PVE/Network/SDN/Ipams/Plugin.pm +++ b/PVE/Network/SDN/Ipams/Plugin.pm @@ -110,7 +110,7 @@ sub api_request { my $response =3D $ua->request($req); my $code =3D $response->code; =20 - if ($code !~ /2(\d+)$/) { + if ($code !~ /^2(\d+)$/) { my $msg =3D $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=3DVnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm Subn= etPlugin.pm Ipams.pm +SOURCES=3DVnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm Subn= etPlugin.pm Ipams.pm Dns.pm =20 =20 PERL5DIR=3D${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 =20 diff --git a/PVE/Network/SDN/SubnetPlugin.pm b/PVE/Network/SDN/SubnetPlug= in.pm index 6224065..3769e04 100644 --- a/PVE/Network/SDN/SubnetPlugin.pm +++ b/PVE/Network/SDN/SubnetPlugin.pm @@ -65,22 +65,25 @@ sub properties { type =3D> 'string', description =3D> "static routes [network=3D:gateway= =3D,network=3D:gateway=3D,... ]", }, - #cloudinit, dhcp options - nameservers =3D> { - type =3D> 'string', format =3D> 'address-list', - description =3D> " dns nameserver", + dns =3D> { + type =3D> 'string', + description =3D> "dns api server", }, - #cloudinit, dhcp options - searchdomain =3D> { + reversedns =3D> { type =3D> 'string', + description =3D> "reverse dns api server", }, - dhcp =3D> { - type =3D> 'boolean', - description =3D> "enable dhcp for this subnet", + dnszone =3D> { + type =3D> 'string', + description =3D> "dns domain zone ex: mydomain.com", }, - dns_driver =3D> { + reversednszone =3D> { type =3D> 'string', - description =3D> "Develop some dns registrations plugins (po= werdns,...)", + description =3D> "reverse dns zone ex: 0.168.192.in-addr.arp= a", + }, + dnszoneprefix =3D> { + type =3D> 'string', + description =3D> "dns domain zone prefix ex: 'adm' -> .adm.mydomain.com", }, ipam =3D> { type =3D> 'string', @@ -93,11 +96,12 @@ sub options { return { gateway =3D> { optional =3D> 1 }, routes =3D> { optional =3D> 1 }, - nameservers =3D> { optional =3D> 1 }, - searchdomain =3D> { optional =3D> 1 }, snat =3D> { optional =3D> 1 }, - dhcp =3D> { optional =3D> 1 }, - dns_driver =3D> { optional =3D> 1 }, + dns =3D> { optional =3D> 1 }, + reversedns =3D> { optional =3D> 1 }, + dnszone =3D> { optional =3D> 1 }, + reversednszone =3D> { optional =3D> 1 }, + dnszoneprefix =3D> { optional =3D> 1 }, ipam =3D> { optional =3D> 1 }, }; } @@ -105,12 +109,25 @@ sub options { sub on_update_hook { my ($class, $subnetid, $subnet_cfg) =3D @_; =20 - my $subnet =3D $subnetid =3D~ s/-/\//r; - my $subnet_matcher =3D subnet_matcher($subnet); + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $subnet_matcher =3D subnet_matcher($cidr); + + my $subnet =3D $subnet_cfg->{ids}->{$subnetid}; =20 - my $gateway =3D $subnet_cfg->{ids}->{$subnetid}->{gateway}; + my $gateway =3D $subnet->{gateway}; + my $dns =3D $subnet->{dns}; + my $dnszone =3D $subnet->{dnszone}; + my $reversedns =3D $subnet->{reversedns}; + my $reversednszone =3D $subnet->{reversednszone}; + + #to: for /32 pointotoping, allow gateway outside the subnet raise_param_exc({ gateway =3D> "$gateway is not in subnet $subnet"})= if $gateway && !$subnet_matcher->($gateway); =20 + raise_param_exc({ dns =3D> "missing dns provider"}) if $dnszone && != $dns; + raise_param_exc({ dnszone =3D> "missing dns zone"}) if $dns && !$dns= zone; + raise_param_exc({ reversedns =3D> "missing dns provider"}) if $rever= sednszone && !$reversedns; + raise_param_exc({ reversednszone =3D> "missing dns zone"}) if $rever= sedns && !$reversednszone; + } =20 sub on_delete_hook { diff --git a/PVE/Network/SDN/Subnets.pm b/PVE/Network/SDN/Subnets.pm index 3ce2d44..4e8353e 100644 --- a/PVE/Network/SDN/Subnets.pm +++ b/PVE/Network/SDN/Subnets.pm @@ -5,8 +5,10 @@ use warnings; =20 use Net::Subnet qw(subnet_matcher); use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use Net::IP; =20 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,157 @@ sub find_ip_subnet { return ($subnetid, $subnet); } =20 +my $verify_dns_zone =3D sub { + my ($zone, $dns) =3D @_; + + return if !$zone || !$dns; + + my $dns_cfg =3D PVE::Network::SDN::Dns::config(); + my $plugin_config =3D $dns_cfg->{ids}->{$dns}; + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($plugin_config= ->{type}); + $plugin->verify_zone($plugin_config, $zone); +}; + +my $add_dns_record =3D sub { + my ($zone, $dns, $hostname, $dnszoneprefix, $ip, $reverse) =3D @_; + + return if !$zone || !$dns || !$hostname || !$ip; + + $hostname .=3D ".$dnszoneprefix" if $dnszoneprefix; + + my $dns_cfg =3D PVE::Network::SDN::Dns::config(); + my $plugin_config =3D $dns_cfg->{ids}->{$dns}; + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($plugin_config= ->{type}); + if($reverse) { + $plugin->add_ptr_record($plugin_config, $zone, $hostname, $ip); + } else { + $plugin->add_a_record($plugin_config, $zone, $hostname, $ip); + } +}; + +my $del_dns_record =3D sub { + my ($zone, $dns, $hostname, $dnszoneprefix, $ip, $reverse) =3D @_; + + return if !$zone || !$dns || !$hostname || !$ip; + + $hostname .=3D ".$dnszoneprefix" if $dnszoneprefix; + + my $dns_cfg =3D PVE::Network::SDN::Dns::config(); + my $plugin_config =3D $dns_cfg->{ids}->{$dns}; + my $plugin =3D PVE::Network::SDN::Dns::Plugin->lookup($plugin_config= ->{type}); + if($reverse) { + $plugin->del_ptr_record($plugin_config, $zone, $ip); + } else { + $plugin->del_a_record($plugin_config, $zone, $hostname); + } +}; + sub next_free_ip { - my ($subnetid, $subnet) =3D @_; + my ($subnetid, $subnet, $hostname) =3D @_; + + my $cidr =3D undef; + my $ip =3D undef; =20 my $ipamid =3D $subnet->{ipam}; - return if !$ipamid; + my $dns =3D $subnet->{dns}; + my $dnszone =3D $subnet->{dnszone}; + my $reversedns =3D $subnet->{reversedns}; + my $reversednszone =3D $subnet->{reversednszone}; + my $dnszoneprefix =3D $subnet->{dnszoneprefix}; + + #verify dns zones before ipam + &$verify_dns_zone($dnszone, $dns); + &$verify_dns_zone($reversednszone, $reversedns); + + if($ipamid) { + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config-= >{type}); + $cidr =3D $plugin->add_next_freeip($plugin_config, $subnetid, $subnet); + ($ip, undef) =3D split(/\//, $cidr); + } =20 - my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); - my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; - my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_conf= ig->{type}); - my $ip =3D $plugin->add_next_freeip($plugin_config, $subnetid, $subn= et); - return $ip; + eval { + #add dns + &$add_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + #add reverse dns + &$add_dns_record($reversednszone, $reversedns, $hostname, $dnszoneprefi= x, $ip, 1); + }; + if ($@) { + #rollback + my $err =3D $@; + eval { + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostna= me) + }; + die $err; + } + return $cidr; } =20 sub add_ip { - my ($subnetid, $subnet, $ip) =3D @_; + my ($subnetid, $subnet, $ip, $hostname) =3D @_; =20 my $ipamid =3D $subnet->{ipam}; - return if !$ipamid; + my $dns =3D $subnet->{dns}; + my $dnszone =3D $subnet->{dnszone}; + my $reversedns =3D $subnet->{reversedns}; + my $reversednszone =3D $subnet->{reversednszone}; + my $dnszoneprefix =3D $subnet->{dnszoneprefix}; + + #verify dns zones before ipam + &$verify_dns_zone($dnszone, $dns); + &$verify_dns_zone($reversednszone, $reversedns); + + if ($ipamid) { + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config-= >{type}); + $plugin->add_ip($plugin_config, $subnetid, $ip); + } =20 - my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); - my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; - my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_conf= ig->{type}); - $plugin->add_ip($plugin_config, $subnetid, $ip); + eval { + #add dns + &$add_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + #add reverse dns + &$add_dns_record($reversednszone, $reversedns, $hostname, $dnszoneprefi= x, $ip, 1); + }; + if ($@) { + #rollback + my $err =3D $@; + eval { + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostna= me) + }; + die $err; + } } =20 sub del_ip { - my ($subnetid, $subnet, $ip) =3D @_; + my ($subnetid, $subnet, $ip, $hostname) =3D @_; =20 my $ipamid =3D $subnet->{ipam}; - return if !$ipamid; + my $dns =3D $subnet->{dns}; + my $dnszone =3D $subnet->{dnszone}; + my $reversedns =3D $subnet->{reversedns}; + my $reversednszone =3D $subnet->{reversednszone}; + my $dnszoneprefix =3D $subnet->{dnszoneprefix}; + + &$verify_dns_zone($dnszone, $dns); + &$verify_dns_zone($reversednszone, $reversedns); + + if ($ipamid) { + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config-= >{type}); + $plugin->del_ip($plugin_config, $subnetid, $ip); + } =20 - my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); - my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; - my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_conf= ig->{type}); - $plugin->del_ip($plugin_config, $subnetid, $ip); + eval { + &$del_dns_record($dnszone, $dns, $hostname, $dnszoneprefix, $ip); + &$del_dns_record($reversednszone, $reversedns, $hostname, $dnszoneprefi= x, $ip, 1); + }; + if ($@) { + warn $@; + } } =20 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 { } =20 sub get_next_free_ip { - my ($vnet, $ipversion) =3D @_; + my ($vnet, $hostname, $ipversion) =3D @_; =20 $ipversion =3D 4 if !$ipversion; my $subnets_cfg =3D PVE::Network::SDN::Subnets::config(); @@ -71,7 +71,7 @@ sub get_next_free_ip { $subnet =3D $subnets_cfg->{ids}->{$subnetid}; if ($subnet && $subnet->{ipam}) { eval { - $ip =3D PVE::Network::SDN::Subnets::next_free_ip($subnetid, $subnet); + $ip =3D PVE::Network::SDN::Subnets::next_free_ip($subnetid, $subnet, $= hostname); }; warn $@ if $@; } @@ -83,23 +83,23 @@ sub get_next_free_ip { } =20 sub add_ip { - my ($vnet, $cidr, $name) =3D @_; + my ($vnet, $cidr, $hostname) =3D @_; =20 my ($ip, $mask) =3D split(/\//, $cidr); my ($subnetid, $subnet) =3D PVE::Network::SDN::Subnets::find_ip_subn= et($ip, $vnet->{subnets}); return if !$subnet->{ipam}; =20 - PVE::Network::SDN::Subnets::add_ip($subnetid, $subnet, $ip); + PVE::Network::SDN::Subnets::add_ip($subnetid, $subnet, $ip, $hostnam= e); } =20 sub del_ip { - my ($vnet, $cidr) =3D @_; + my ($vnet, $cidr, $hostname) =3D @_; =20 my ($ip, $mask) =3D split(/\//, $cidr); my ($subnetid, $subnet) =3D PVE::Network::SDN::Subnets::find_ip_subn= et($ip, $vnet->{subnets}); return if !$subnet->{ipam}; =20 - PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip); + PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip, $hostnam= e); } =20 1; --=20 2.20.1