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