]> git.proxmox.com Git - pve-network.git/blob - PVE/Network/SDN/Dns/PowerdnsPlugin.pm
dns: add noerr param
[pve-network.git] / PVE / Network / SDN / Dns / PowerdnsPlugin.pm
1 package PVE::Network::SDN::Dns::PowerdnsPlugin;
2
3 use strict;
4 use warnings;
5 use PVE::INotify;
6 use PVE::Cluster;
7 use PVE::Tools;
8 use JSON;
9 use Net::IP;
10 use NetAddr::IP qw(:lower);
11 use base('PVE::Network::SDN::Dns::Plugin');
12
13 sub type {
14 return 'powerdns';
15 }
16
17 sub properties {
18 return {
19 url => {
20 type => 'string',
21 },
22 key => {
23 type => 'string',
24 },
25 reversemaskv6 => {
26 type => 'integer'
27 },
28 };
29 }
30
31 sub options {
32
33 return {
34 url => { optional => 0},
35 key => { optional => 0 },
36 ttl => { optional => 1 },
37 reversemaskv6 => { optional => 1, description => "force a different netmask for the ipv6 reverse zone name." },
38
39 };
40 }
41
42 # Plugin implementation
43
44 sub add_a_record {
45 my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_;
46
47 my $url = $plugin_config->{url};
48 my $key = $plugin_config->{key};
49 my $ttl = $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400;
50 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
51
52 my $type = Net::IP::ip_is_ipv6($ip) ? "AAAA" : "A";
53 my $fqdn = $hostname.".".$zone.".";
54
55 my $zonecontent = get_zone_content($plugin_config, $zone);
56 my $existing_rrset = get_zone_rrset($zonecontent, $fqdn);
57
58 my $final_records = [];
59 my $foundrecord = undef;
60 foreach my $record (@{$existing_rrset->{records}}) {
61 if($record->{content} eq $ip) {
62 $foundrecord = 1;
63 next;
64 }
65 push @$final_records, $record;
66 }
67 return if $foundrecord;
68
69 my $record = { content => $ip,
70 disabled => JSON::false,
71 name => $fqdn,
72 type => $type,
73 priority => 0 };
74
75 push @$final_records, $record;
76
77 my $rrset = { name => $fqdn,
78 type => $type,
79 ttl => $ttl,
80 changetype => "REPLACE",
81 records => $final_records };
82
83
84 my $params = { rrsets => [ $rrset ] };
85
86 eval {
87 PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params);
88 };
89
90 if ($@) {
91 die "error add $fqdn to zone $zone: $@" if !$noerr;
92 }
93 }
94
95 sub add_ptr_record {
96 my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_;
97
98 my $url = $plugin_config->{url};
99 my $key = $plugin_config->{key};
100 my $ttl = $plugin_config->{ttl} ? $plugin_config->{ttl} : 14400;
101 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
102 $hostname .= ".";
103
104 my $reverseip = Net::IP->new($ip)->reverse_ip();
105
106 my $type = "PTR";
107
108 my $record = { content => $hostname,
109 disabled => JSON::false,
110 name => $reverseip,
111 type => $type,
112 priority => 0 };
113
114 my $rrset = { name => $reverseip,
115 type => $type,
116 ttl => $ttl,
117 changetype => "REPLACE",
118 records => [ $record ] };
119
120
121 my $params = { rrsets => [ $rrset ] };
122
123 eval {
124 PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params);
125 };
126
127 if ($@) {
128 die "error add $reverseip to zone $zone: $@" if !$noerr;
129 }
130 }
131
132 sub del_a_record {
133 my ($class, $plugin_config, $zone, $hostname, $ip, $noerr) = @_;
134
135 my $url = $plugin_config->{url};
136 my $key = $plugin_config->{key};
137 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
138 my $fqdn = $hostname.".".$zone.".";
139 my $type = Net::IP::ip_is_ipv6($ip) ? "AAAA" : "A";
140
141 my $zonecontent = get_zone_content($plugin_config, $zone);
142 my $existing_rrset = get_zone_rrset($zonecontent, $fqdn);
143
144 my $final_records = [];
145 my $foundrecord = undef;
146 foreach my $record (@{$existing_rrset->{records}}) {
147 if ($record->{content} eq $ip) {
148 $foundrecord = 1;
149 next;
150 }
151 push @$final_records, $record;
152 }
153 return if !$foundrecord;
154
155 my $rrset = {};
156
157 if (scalar (@{$final_records}) > 0) {
158 #if we still have other records, we rewrite them without removed ip
159 $rrset = { name => $fqdn,
160 type => $type,
161 ttl => $existing_rrset->{ttl},
162 changetype => "REPLACE",
163 records => $final_records };
164
165 } else {
166
167 $rrset = { name => $fqdn,
168 type => $type,
169 changetype => "DELETE",
170 records => [] };
171 }
172
173 my $params = { rrsets => [ $rrset ] };
174
175 eval {
176 PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params);
177 };
178
179 if ($@) {
180 die "error delete $fqdn from zone $zone: $@" if !$noerr;
181 }
182 }
183
184 sub del_ptr_record {
185 my ($class, $plugin_config, $zone, $ip, $noerr) = @_;
186
187 my $url = $plugin_config->{url};
188 my $key = $plugin_config->{key};
189 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
190
191 my $reverseip = Net::IP->new($ip)->reverse_ip();
192
193 my $type = "PTR";
194
195 my $rrset = { name => $reverseip,
196 type => $type,
197 changetype => "DELETE",
198 records => [] };
199
200 my $params = { rrsets => [ $rrset ] };
201
202 eval {
203 PVE::Network::SDN::api_request("PATCH", "$url/zones/$zone", $headers, $params);
204 };
205
206 if ($@) {
207 die "error delete $reverseip from zone $zone: $@" if !$noerr;
208 }
209 }
210
211 sub verify_zone {
212 my ($class, $plugin_config, $zone, $noerr) = @_;
213
214 #verify that api is working
215
216 my $url = $plugin_config->{url};
217 my $key = $plugin_config->{key};
218 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
219
220 eval {
221 PVE::Network::SDN::api_request("GET", "$url/zones/$zone?rrsets=false", $headers);
222 };
223
224 if ($@) {
225 die "can't read zone $zone: $@" if !$noerr;
226 }
227 }
228
229 sub get_reversedns_zone {
230 my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_;
231
232 my $cidr = $subnet->{cidr};
233 my $mask = $subnet->{mask};
234
235 my $zone = "";
236
237 if (Net::IP::ip_is_ipv4($ip)) {
238 my ($ipblock1, $ipblock2, $ipblock3, $ipblock4) = split(/\./, $ip);
239
240 my $ipv4 = new NetAddr::IP($cidr);
241 #private addresse #powerdns built-in private zone : serve-rfc1918
242 if($ipv4->is_rfc1918()) {
243 if ($ipblock1 == 192) {
244 $zone = "168.192.in-addr.arpa.";
245 } elsif ($ipblock1 == 172) {
246 $zone = "16-31.172.in-addr.arpa.";
247 } elsif ($ipblock1 == 10) {
248 $zone = "10.in-addr.arpa.";
249 }
250
251 } else {
252 #public ipv4 : RIPE,ARIN,AFRNIC
253 #. 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)
254 #One or more /24 type zones need to be created if your address space has a prefix length between /17 and /24.
255 # If your prefix length is between /16 and /9 you will have to request one or more delegations for /16 type zones.
256
257 if ($mask <= 24) {
258 $zone = "$ipblock3.$ipblock2.$ipblock1.in-addr.arpa.";
259 } elsif ($mask <= 16) {
260 $zone = "$ipblock2.$ipblock1.in-addr.arpa.";
261 } elsif ($mask <= 8) {
262 $zone = "$ipblock1.in-addr.arpa.";
263 }
264 }
265 } else {
266 $mask = $plugin_config->{reversemaskv6} if $plugin_config->{reversemaskv6};
267 die "reverse dns zone mask need to be a multiple of 4" if ($mask % 4);
268 my $networkv6 = NetAddr::IP->new($cidr)->network();
269 $zone = Net::IP->new($networkv6)->reverse_ip();
270 }
271
272 return $zone;
273 }
274
275
276 sub on_update_hook {
277 my ($class, $plugin_config) = @_;
278
279 #verify that api is working
280
281 my $url = $plugin_config->{url};
282 my $key = $plugin_config->{key};
283 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
284
285 eval {
286 PVE::Network::SDN::api_request("GET", "$url", $headers);
287 };
288
289 if ($@) {
290 die "dns api error: $@";
291 }
292 }
293
294
295 sub get_zone_content {
296 my ($plugin_config, $zone) = @_;
297
298 #verify that api is working
299
300 my $url = $plugin_config->{url};
301 my $key = $plugin_config->{key};
302 my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'X-API-Key' => $key];
303
304 my $result = undef;
305 eval {
306 $result = PVE::Network::SDN::api_request("GET", "$url/zones/$zone", $headers);
307 };
308
309 if ($@) {
310 die "can't read zone $zone: $@";
311 }
312 return $result;
313 }
314
315 sub get_zone_rrset {
316 my ($zonecontent, $name) = @_;
317
318 my $rrsetresult = undef;
319 foreach my $rrset (@{$zonecontent->{rrsets}}) {
320 next if $rrset->{name} ne $name;
321 $rrsetresult = $rrset;
322 last;
323 }
324 return $rrsetresult;
325 }
326
327 1;
328
329