source: trunk/dns-rpc.cgi@ 683

Last change on this file since 683 was 683, checked in by Kris Deugau, 9 years ago

/trunk

Fix semantic snafu in RPC handler's delByCIDR(); subsets of the passed
CIDR were being deleted when they shouldn't have been. Also separate
some code segments for reading clarity.

  • Property svn:executable set to *
  • Property svn:keywords set to Date Rev Author Id
File size: 33.0 KB
Line 
1#!/usr/bin/perl -w -T
2# XMLRPC interface to manipulate most DNS DB entities
3##
4# $Id: dns-rpc.cgi 683 2015-06-17 19:12:58Z kdeugau $
5# Copyright 2012,2013 Kris Deugau <kdeugau@deepnet.cx>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19##
20
21use strict;
22use warnings;
23
24# don't remove! required for GNU/FHS-ish install from tarball
25use lib '.'; ##uselib##
26use DNSDB;
27
28use FCGI;
29#use Frontier::RPC2;
30use Frontier::Responder;
31
32## We need to handle a couple of things globally, rather than pasting the same bit into *every* sub.
33## So, let's subclass Frontier::RPC2 + Frontier::Responder, so we can override the single sub in each
34## that needs kicking
35#### hmm. put this in a separate file?
36#package DNSDB::RPC;
37#our @ISA = ("Frontier::RPC2", "Frontier::Responder");
38#package main;
39
40my $dnsdb = DNSDB->new();
41
42my $methods = {
43#sub getPermissions {
44#sub changePermissions {
45#sub comparePermissions {
46#sub changeGroup {
47 'dnsdb.addDomain' => \&addDomain,
48 'dnsdb.delZone' => \&delZone,
49#sub domainName {
50#sub revName {
51 'dnsdb.domainID' => \&domainID,
52#sub revID {
53 'dnsdb.addRDNS' => \&addRDNS,
54#sub getZoneCount {
55#sub getZoneList {
56#sub getZoneLocation {
57 'dnsdb.addGroup' => \&addGroup,
58 'dnsdb.delGroup' => \&delGroup,
59#sub getChildren {
60#sub groupName {
61#sub getGroupCount {
62#sub getGroupList {
63#sub groupID {
64 'dnsdb.addUser' => \&addUser,
65#sub getUserCount {
66#sub getUserList {
67#sub getUserDropdown {
68 'dnsdb.updateUser' => \&updateUser,
69 'dnsdb.delUser' => \&delUser,
70#sub userFullName {
71#sub userStatus {
72#sub getUserData {
73#sub addLoc {
74#sub updateLoc {
75#sub delLoc {
76#sub getLoc {
77#sub getLocCount {
78#sub getLocList {
79 'dnsdb.getLocDropdown' => \&getLocDropdown,
80 'dnsdb.getSOA' => \&getSOA,
81#sub updateSOA {
82 'dnsdb.getRecLine' => \&getRecLine,
83 'dnsdb.getRecList' => \&getRecList,
84 'dnsdb.getRecCount' => \&getRecCount,
85 'dnsdb.addRec' => \&rpc_addRec,
86 'dnsdb.updateRec' => \&rpc_updateRec,
87#sub downconvert {
88 'dnsdb.addOrUpdateRevRec' => \&addOrUpdateRevRec,
89 'dnsdb.updateRevSet' => \&updateRevSet,
90 'dnsdb.splitTemplate' => \&splitTemplate,
91 'dnsdb.resizeTemplate' => \&resizeTemplate,
92 'dnsdb.templatesToRecords' => \&templatesToRecords,
93 'dnsdb.delRec' => \&delRec,
94 'dnsdb.delByCIDR' => \&delByCIDR,
95#sub getLogCount {}
96#sub getLogEntries {}
97 'dnsdb.getRevPattern' => \&getRevPattern,
98 'dnsdb.getRevSet' => \&getRevSet,
99 'dnsdb.getTypelist' => \&getTypelist,
100 'dnsdb.getTypemap' => \&getTypemap,
101 'dnsdb.getReverse_typemap' => \&getReverse_typemap,
102#sub parentID {
103#sub isParent {
104 'dnsdb.zoneStatus' => \&zoneStatus,
105 'dnsdb.getZonesByCIDR' => \&getZonesByCIDR,
106#sub importAXFR {
107#sub importBIND {
108#sub import_tinydns {
109#sub export {
110#sub mailNotify {
111
112 'dnsdb.getMethods' => \&get_method_list
113};
114
115my $reqcnt = 0;
116
117while (FCGI::accept >= 0) {
118 my $res = Frontier::Responder->new(
119 methods => $methods
120 );
121
122 # "Can't do that" errors
123 if (!$dnsdb) {
124 print "Content-type: text/xml\n\n".$res->{_decode}->encode_fault(5, $dnsdb->err);
125 } else {
126 print $res->answer;
127 }
128 last if $reqcnt++ > $dnsdb->{maxfcgi};
129} # while FCGI::accept
130
131
132exit;
133
134##
135## Subs below here
136##
137
138# Check RPC ACL
139sub _aclcheck {
140 my $subsys = shift;
141 return 1 if grep /$ENV{REMOTE_ADDR}/, @{$dnsdb->{rpcacl}{$subsys}};
142 warn "$subsys/$ENV{REMOTE_ADDR} not in ACL\n"; # a bit of logging
143 return 0;
144}
145
146# Let's see if we can factor these out of the RPC method subs
147sub _commoncheck {
148 my $argref = shift;
149 my $needslog = shift;
150
151 die "Missing remote system name\n" if !$argref->{rpcsystem};
152 die "Access denied\n" if !_aclcheck($argref->{rpcsystem});
153 if ($needslog) {
154 die "Missing remote username\n" if !$argref->{rpcuser};
155 die "Couldn't set userdata for logging\n"
156 unless $dnsdb->initRPC(username => $argref->{rpcuser}, rpcsys => $argref->{rpcsystem},
157 fullname => ($argref->{fullname} ? $argref->{fullname} : $argref->{rpcuser}) );
158 }
159}
160
161# check for defrec and revrec; only call on subs that deal with records
162sub _reccheck {
163 my $argref = shift;
164 die "Missing defrec and/or revrec flags\n" if !($argref->{defrec} || $argref->{revrec});
165}
166
167# set location to the zone's default location if none is specified
168sub _loccheck {
169 my $argref = shift;
170 if (!$argref->{location} && $argref->{defrec} eq 'n') {
171 $argref->{location} = $dnsdb->getZoneLocation($argref->{revrec}, $argref->{parent_id});
172 }
173}
174
175# set ttl to zone defailt minttl if none is specified
176sub _ttlcheck {
177 my $argref = shift;
178 if (!$argref->{ttl}) {
179 my $tmp = $dnsdb->getSOA($argref->{defrec}, $argref->{revrec}, $argref->{parent_id});
180 $argref->{ttl} = $tmp->{minttl};
181 }
182}
183
184#sub connectDB {
185#sub finish {
186#sub initGlobals {
187#sub initPermissions {
188#sub getPermissions {
189#sub changePermissions {
190#sub comparePermissions {
191#sub changeGroup {
192#sub _log {
193
194sub addDomain {
195 my %args = @_;
196
197 _commoncheck(\%args, 'y');
198
199 my ($code, $msg) = $dnsdb->addDomain($args{domain}, $args{group}, $args{state}, $args{location});
200 die "$msg\n" if $code eq 'FAIL';
201 return $msg; # domain ID
202}
203
204sub delZone {
205 my %args = @_;
206
207 _commoncheck(\%args, 'y');
208 die "Need forward/reverse zone flag\n" if !$args{revrec};
209
210 my ($code,$msg);
211 # Let's be nice; delete based on zone id OR zone name. Saves an RPC call round-trip, maybe.
212 if ($args{zone} =~ /^\d+$/) {
213 ($code,$msg) = $dnsdb->delZone($args{zone}, $args{revrec});
214 } else {
215 my $zoneid;
216 $zoneid = $dnsdb->domainID($args{zone}) if $args{revrec} eq 'n';
217 $zoneid = $dnsdb->revID($args{zone}) if $args{revrec} eq 'y';
218 die "Can't find zone: ".$dnsdb->errstr."\n" if !$zoneid;
219 ($code,$msg) = $dnsdb->delZone($zoneid, $args{revrec});
220 }
221 die "$msg\n" if $code eq 'FAIL';
222 return $msg;
223}
224
225#sub domainName {}
226#sub revName {}
227
228sub domainID {
229 my %args = @_;
230
231 _commoncheck(\%args, 'y');
232
233 my $domid = $dnsdb->domainID($args{domain});
234 die $dnsdb->errstr."\n" if !$domid;
235 return $domid;
236}
237
238#sub revID {}
239
240sub addRDNS {
241 my %args = @_;
242
243 _commoncheck(\%args, 'y');
244
245 my ($code, $msg) = $dnsdb->addRDNS($args{revzone}, $args{revpatt}, $args{group}, $args{state}, $args{defloc});
246 die "$msg\n" if $code eq 'FAIL';
247 return $msg; # domain ID
248}
249
250#sub getZoneCount {}
251#sub getZoneList {}
252#sub getZoneLocation {}
253
254sub addGroup {
255 my %args = @_;
256
257 _commoncheck(\%args, 'y');
258 die "Missing new group name\n" if !$args{groupname};
259 die "Missing parent group ID\n" if !$args{parent_id};
260
261# not sure how to usefully represent permissions via RPC. :/
262# not to mention, permissions are checked at the UI layer, not the DB layer.
263 my $perms = {domain_edit => 1, domain_create => 1, domain_delete => 1,
264 record_edit => 1, record_create => 1, record_delete => 1
265 };
266## optional $inhert arg?
267 my ($code,$msg) = $dnsdb->addGroup($args{groupname}, $args{parent_id}, $perms);
268 die "$msg\n" if $code eq 'FAIL';
269 return $msg;
270}
271
272sub delGroup {
273 my %args = @_;
274
275 _commoncheck(\%args, 'y');
276 die "Missing group ID or name to remove\n" if !$args{group};
277
278 my ($code,$msg);
279 # Let's be nice; delete based on groupid OR group name. Saves an RPC call round-trip, maybe.
280 if ($args{group} =~ /^\d+$/) {
281 ($code,$msg) = $dnsdb->delGroup($args{group});
282 } else {
283 my $grpid = $dnsdb->groupID($args{group});
284 die "Can't find group\n" if !$grpid;
285 ($code,$msg) = $dnsdb->delGroup($grpid);
286 }
287 die "$msg\n" if $code eq 'FAIL';
288 return $msg;
289}
290
291#sub getChildren {}
292#sub groupName {}
293#sub getGroupCount {}
294#sub getGroupList {}
295#sub groupID {}
296
297sub addUser {
298 my %args = @_;
299
300 _commoncheck(\%args, 'y');
301
302# not sure how to usefully represent permissions via RPC. :/
303# not to mention, permissions are checked at the UI layer, not the DB layer.
304 # bend and twist; get those arguments in in the right order!
305 $args{type} = 'u' if !$args{type};
306 $args{permstring} = 'i' if !defined($args{permstring});
307 my @userargs = ($args{username}, $args{group}, $args{pass}, $args{state}, $args{type}, $args{permstring});
308 for my $argname ('fname','lname','phone') {
309 last if !$args{$argname};
310 push @userargs, $args{$argname};
311 }
312 my ($code,$msg) = $dnsdb->addUser(@userargs);
313 die "$msg\n" if $code eq 'FAIL';
314 return $msg;
315}
316
317#sub getUserCount {}
318#sub getUserList {}
319#sub getUserDropdown {}
320#sub checkUser {}
321
322sub updateUser {
323 my %args = @_;
324
325 _commoncheck(\%args, 'y');
326
327 die "Missing UID\n" if !$args{uid};
328
329 # bend and twist; get those arguments in in the right order!
330 $args{type} = 'u' if !$args{type};
331 my @userargs = ($args{uid}, $args{username}, $args{group}, $args{pass}, $args{state}, $args{type});
332 for my $argname ('fname','lname','phone') {
333 last if !$args{$argname};
334 push @userargs, $args{$argname};
335 }
336##fixme: also underlying in DNSDB::updateUser(): no way to just update this or that attribute;
337# have to pass them all in to be overwritten
338 my ($code,$msg) = $dnsdb->updateUser(@userargs);
339 die "$msg\n" if $code eq 'FAIL';
340 return $msg;
341}
342
343sub delUser {
344 my %args = @_;
345
346 _commoncheck(\%args, 'y');
347
348 die "Missing UID\n" if !$args{uid};
349 my ($code,$msg) = $dnsdb->delUser($args{uid});
350 die "$msg\n" if $code eq 'FAIL';
351 return $msg;
352}
353
354#sub userFullName {}
355#sub userStatus {}
356#sub getUserData {}
357
358#sub addLoc {}
359#sub updateLoc {}
360#sub delLoc {}
361#sub getLoc {}
362#sub getLocCount {}
363#sub getLocList {}
364
365sub getLocDropdown {
366 my %args = @_;
367
368 _commoncheck(\%args);
369 $args{defloc} = '' if !$args{defloc};
370
371 my $ret = $dnsdb->getLocDropdown($args{group}, $args{defloc});
372 return $ret;
373}
374
375sub getSOA {
376 my %args = @_;
377
378 _commoncheck(\%args);
379
380 _reccheck(\%args);
381
382 my $ret = $dnsdb->getSOA($args{defrec}, $args{revrec}, $args{id});
383 if (!$ret) {
384 if ($args{defrec} eq 'y') {
385 die "No default SOA record in group\n";
386 } else {
387 die "No SOA record in zone\n";
388 }
389 }
390 return $ret;
391}
392
393#sub updateSOA {}
394
395sub getRecLine {
396 my %args = @_;
397
398 _commoncheck(\%args);
399
400 _reccheck(\%args);
401
402 my $ret = $dnsdb->getRecLine($args{defrec}, $args{revrec}, $args{id});
403
404 die $dnsdb->errstr."\n" if !$ret;
405
406 return $ret;
407}
408
409sub getRecList {
410 my %args = @_;
411
412 _commoncheck(\%args);
413
414 # deal gracefully with alternate calling convention for args{id}
415 $args{id} = $args{ID} if !$args{id} && $args{ID};
416 # ... and fail if we don't have one
417 die "Missing zone ID\n" if !$args{id};
418
419 # set some optional args
420 $args{offset} = 0 if !$args{offset};
421## for order, need to map input to column names
422 $args{order} = 'host' if !$args{order};
423 $args{direction} = 'ASC' if !$args{direction};
424 $args{defrec} = 'n' if !$args{defrec};
425 $args{revrec} = 'n' if !$args{revrec};
426
427 # convert zone name to zone ID, if needed
428 if ($args{defrec} eq 'n') {
429 if ($args{revrec} eq 'n') {
430 $args{id} = $dnsdb->domainID($args{id}) if $args{id} !~ /^\d+$/;
431 } else {
432 $args{id} = $dnsdb->revID($args{id}) if $args{id} !~ /^\d+$/
433 }
434 }
435
436 # fail if we *still* don't have a valid zone ID
437 die $dnsdb->errstr."\n" if !$args{id};
438
439 # and finally retrieve the records.
440 my $ret = $dnsdb->getRecList(defrec => $args{defrec}, revrec => $args{revrec}, id => $args{id},
441 offset => $args{offset}, nrecs => $args{nrecs}, sortby => $args{sortby},
442 sortorder => $args{sortorder}, filter => $args{filter});
443 die $dnsdb->errstr."\n" if !$ret;
444
445 return $ret;
446}
447
448sub getRecCount {
449 my %args = @_;
450
451 _commoncheck(\%args);
452
453 _reccheck(\%args);
454
455 # set some optional args
456 $args{nrecs} = 'all' if !$args{nrecs};
457 $args{nstart} = 0 if !$args{nstart};
458## for order, need to map input to column names
459 $args{order} = 'host' if !$args{order};
460 $args{direction} = 'ASC' if !$args{direction};
461
462 my $ret = $dnsdb->getRecCount(defrec => $args{defrec}, revrec => $args{revrec},
463 id => $args{id}, filter => $args{filter});
464
465 die $dnsdb->errstr."\n" if !$ret;
466
467 return $ret;
468}
469
470sub rpc_addRec {
471 my %args = @_;
472
473 _commoncheck(\%args, 'y');
474
475 _reccheck(\%args);
476 _loccheck(\%args);
477 _ttlcheck(\%args);
478
479 # allow passing text types rather than DNS integer IDs
480 $args{type} = $DNSDB::reverse_typemap{$args{type}} if $args{type} !~ /^\d+$/;
481
482 my @recargs = ($args{defrec}, $args{revrec}, $args{parent_id},
483 \$args{name}, \$args{type}, \$args{address}, $args{ttl}, $args{location},
484 $args{expires}, $args{stamp});
485 if ($args{type} == $DNSDB::reverse_typemap{MX} or $args{type} == $DNSDB::reverse_typemap{SRV}) {
486 push @recargs, $args{distance};
487 if ($args{type} == $DNSDB::reverse_typemap{SRV}) {
488 push @recargs, $args{weight};
489 push @recargs, $args{port};
490 }
491 }
492
493 my ($code, $msg) = $dnsdb->addRec(@recargs);
494
495 die "$msg\n" if $code eq 'FAIL';
496 return $msg;
497} # rpc_addRec
498
499sub rpc_updateRec {
500 my %args = @_;
501
502 _commoncheck(\%args, 'y');
503
504 _reccheck(\%args);
505
506 # put some caller-friendly names in their rightful DB column places
507 $args{val} = $args{address} if !$args{val};
508 $args{host} = $args{name} if !$args{host};
509
510 # get old line, so we can update only the bits that the caller passed to change
511 my $oldrec = $dnsdb->getRecLine($args{defrec}, $args{revrec}, $args{id});
512 foreach my $field (qw(host type val ttl location expires distance weight port)) {
513 $args{$field} = $oldrec->{$field} if !$args{$field} && defined($oldrec->{$field});
514 }
515 # stamp has special handling when blank or 0. "undefined" from the caller should mean "don't change"
516 $args{stamp} = $oldrec->{stamp} if !defined($args{stamp}) && $oldrec->{stampactive};
517
518 # allow passing text types rather than DNS integer IDs
519 $args{type} = $DNSDB::reverse_typemap{$args{type}} if $args{type} !~ /^\d+$/;
520
521 # note dist, weight, port are not required on all types; will be ignored if not needed.
522 # parent_id is the "primary" zone we're updating; necessary for forward/reverse voodoo
523 my ($code, $msg) = $dnsdb->updateRec($args{defrec}, $args{revrec}, $args{id}, $args{parent_id},
524 \$args{host}, \$args{type}, \$args{val}, $args{ttl}, $args{location},
525 $args{expires}, $args{stamp},
526 $args{distance}, $args{weight}, $args{port});
527
528 die "$msg\n" if $code eq 'FAIL';
529 return $msg;
530} # rpc_updateRec
531
532# Takes a passed CIDR block and DNS pattern; adds a new record or updates the record(s) affected
533sub addOrUpdateRevRec {
534 my %args = @_;
535
536 _commoncheck(\%args, 'y');
537 my $cidr = new NetAddr::IP $args{cidr};
538
539 my $zonelist = $dnsdb->getZonesByCIDR(%args);
540 if (scalar(@$zonelist) == 0) {
541 # enhh.... WTF?
542 } elsif (scalar(@$zonelist) == 1) {
543 # check if the single zone returned is bigger than the CIDR. if so, we can just add a record
544 my $zone = new NetAddr::IP $zonelist->[0]->{revnet};
545 if ($zone->contains($cidr)) {
546 # We need to strip the CIDR mask on IPv4 /32 assignments, or we just add a new record all the time.
547 my $filt = ($cidr->{isv6} || $cidr->masklen != 32 ? "$cidr" : $cidr->addr);
548 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y',
549 id => $zonelist->[0]->{rdns_id}, filter => $filt);
550##fixme: Figure some new magic to automerge new incoming A(AAA)+PTR requests
551# with existing A records to prevent duplication of A(AAA) records
552 if (scalar(@$reclist) == 0) {
553 # Aren't Magic Numbers Fun? See pseudotype list in dnsadmin.
554 my $type = ($cidr->{isv6} ? ($cidr->masklen == 128 ? 65281 : 65284) : ($cidr->masklen == 32 ? 65280 : 65283) );
555 rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zonelist->[0]->{rdns_id}, type => $type,
556 address => "$cidr", %args);
557 } else {
558 my $flag = 0;
559 foreach my $rec (@$reclist) {
560 # pure PTR plus composite types
561 next unless $rec->{type} == 12 || $rec->{type} == 65280 || $rec->{type} == 65281
562 || $rec->{type} == 65282 || $rec->{type} == 65283 || $rec->{type} == 65284;
563 next unless $rec->{val} eq $filt; # make sure we really update the record we want to update.
564 rpc_updateRec(defrec =>'n', revrec => 'y', id => $rec->{record_id},
565 parent_id => $zonelist->[0]->{rdns_id}, address => "$cidr", %args);
566 $flag = 1;
567 last; # only do one record.
568 }
569 unless ($flag) {
570 # Nothing was updated, so we didn't really have a match. Add as per @$reclist==0
571 # Aren't Magic Numbers Fun? See pseudotype list in dnsadmin.
572 my $type = ($cidr->{isv6} ? 65282 : ($cidr->masklen == 32 ? 65280 : 65283) );
573 rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zonelist->[0]->{rdns_id}, type => $type,
574 address => "$cidr", %args);
575 }
576 }
577 } else {
578 # ebbeh? CIDR is only partly represented in DNS. This needs manual intervention.
579 } # done single-zone-contains-$cidr
580 } else {
581 # Overlapping reverse zones shouldn't be possible, so if we're here we've got a CIDR
582 # that spans multiple reverse zones (eg, /23 CIDR -> 2 /24 rzones)
583 foreach my $zdata (@$zonelist) {
584 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y',
585 id => $zdata->{rdns_id}, filter => $zdata->{revnet});
586 if (scalar(@$reclist) == 0) {
587 my $type = ($args{cidr}->{isv6} ? 65282 : ($args{cidr}->masklen == 32 ? 65280 : 65283) );
588 rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zdata->{rdns_id}, type => $type,
589 address => "$args{cidr}", %args);
590 } else {
591 foreach my $rec (@$reclist) {
592 # only the composite and/or template types; pure PTR or nontemplate composite
593 # types are nominally impossible here.
594 next unless $rec->{type} == 65282 || $rec->{type} == 65283 || $rec->{type} == 65284;
595 rpc_updateRec(defrec => 'n', revrec => 'y', id => $rec->{record_id},
596 parent_id => $zdata->{rdns_id}, %args);
597 last; # only do one record.
598 }
599 }
600 } # iterate zones within $cidr
601 } # done $cidr-contains-zones
602##fixme: what about errors? what about warnings?
603} # done addOrUpdateRevRec()
604
605# Update rDNS on a whole batch of IP addresses. Presented as a separate sub via RPC
606# since RPC calls can be s...l...o...w....
607sub updateRevSet {
608 my %args = @_;
609
610 _commoncheck(\%args, 'y');
611
612 my @ret;
613 # loop over passed IP/hostname pairs
614 foreach my $key (keys %args) {
615 next unless $key =~ m{^host_((?:[\d.]+|[\da-f:]+)(?:/\d+)?)$};
616 my $ip = $1;
617 push @ret, addOrUpdateRevRec(cidr => $ip, name => $args{$key}, %args);
618 }
619##fixme: what about errors? what about warnings?
620 return \@ret;
621} # done updateRevSet()
622
623# Split a template record as per a passed CIDR.
624# Requires the CIDR and the new mask length
625sub splitTemplate {
626 my %args = @_;
627
628 _commoncheck(\%args, 'y');
629
630 my $cidr = new NetAddr::IP $args{cidr};
631
632 my $zonelist = $dnsdb->getZonesByCIDR(%args);
633
634 if (scalar(@$zonelist) == 0) {
635 # enhh.... WTF?
636
637 } elsif (scalar(@$zonelist) == 1) {
638 my $zone = new NetAddr::IP $zonelist->[0]->{revnet};
639 if ($zone->contains($cidr)) {
640 # Find the first record in the reverse zone that matches the CIDR we're splitting...
641 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y',
642 id => $zonelist->[0]->{rdns_id}, filter => $cidr, sortby => 'val', sortorder => 'DESC');
643 my $oldrec;
644 foreach my $rec (@$reclist) {
645 my $reccidr = new NetAddr::IP $rec->{val};
646 next unless $cidr->contains($reccidr); # not sure this is needed here
647 # ... and is a reverse-template type.
648 # Could arguably trim the list below to just 65282, 65283, 65284
649 next unless $rec->{type} == 12 || $rec->{type} == 65280 || $rec->{type} == 65281 ||
650 $rec->{type} == 65282 || $rec->{type} == 65283 ||$rec->{type} == 65284;
651 # snag old record so we can copy its data
652 $oldrec = $dnsdb->getRecLine('n', 'y', $rec->{record_id});
653 last; # we've found one record that meets our criteria; Extras Are Irrelevant
654 }
655
656 my @newblocks = $cidr->split($args{newmask});
657 # Change the existing record with the new CIDR
658 my $up_res = rpc_updateRec(%args, val => $newblocks[0], id => $oldrec->{record_id}, defrec => 'n', revrec => 'y');
659 my @ret;
660 # the update is assumed to have succeeded if it didn't fail.
661##fixme: find a way to save and return "warning" states?
662 push @ret, {block => "$newblocks[0]", code => "OK", msg => $up_res};
663 # And now add new record(s) for each of the new CIDR entries, reusing the old data
664 for (my $i = 1; $i <= $#newblocks; $i++) {
665 my $newval = "$newblocks[$i]";
666 my @recargs = ('n', 'y', $oldrec->{rdns_id}, \$oldrec->{host}, \$oldrec->{type}, \$newval,
667 $oldrec->{ttl}, $oldrec->{location}, 0, '');
668 my ($code, $msg) = $dnsdb->addRec(@recargs);
669 # Note failures here are not fatal; this should typically only ever be called by IPDB
670 push @ret, {block => "$newblocks[$i]", code => $code, msg => $up_res};
671 }
672 # return an info hash in case of warnings doing the update or add(s)
673 return \@ret;
674
675 } else { # $cidr > $zone but we only have one zone
676 # ebbeh? CIDR is only partly represented in DNS. This needs manual intervention.
677 return "Warning: $args{cidr} is only partly represented in DNS. Check and update DNS records manually.";
678 } # done single-zone-contains-$cidr
679
680 } else {
681 # multiple zones nominally "contain" $cidr
682 } # done $cidr-contains-zones
683
684} # done splitTemplate()
685
686# Resize a template according to an old/new CIDR pair
687# Takes the old cidr in $args{oldcidr} and the new in $args{newcidr}
688sub resizeTemplate {
689 my %args = @_;
690
691 _commoncheck(\%args, 'y');
692
693 my $oldcidr = new NetAddr::IP $args{oldcidr};
694 my $newcidr = new NetAddr::IP $args{newcidr};
695 die "$oldcidr and $newcidr do not overlap"
696 unless $oldcidr->contains($newcidr) || $newcidr->contains($oldcidr);
697 $args{cidr} = $args{oldcidr};
698
699 my $up_res;
700
701 my $zonelist = $dnsdb->getZonesByCIDR(%args);
702 if (scalar(@$zonelist) == 0) {
703 # enhh.... WTF?
704
705 } elsif (scalar(@$zonelist) == 1) {
706 my $zone = new NetAddr::IP $zonelist->[0]->{revnet};
707 if ($zone->contains($oldcidr)) {
708 # Find record(s) matching the old and new CIDR
709
710 my $sql = q(
711 SELECT record_id,host,val
712 FROM records
713 WHERE rdns_id = ?
714 AND type IN (12, 65280, 65281, 65282, 65283, 65284)
715 AND (val = ? OR val = ?)
716 ORDER BY masklen(inetlazy(val)) ASC
717 );
718 my $sth = $dnsdb->{dbh}->prepare($sql);
719 $sth->execute($zonelist->[0]->{rdns_id}, "$oldcidr", "$newcidr");
720 my $upd_id;
721 my $oldhost;
722 while (my ($recid, $host, $val) = $sth->fetchrow_array) {
723 my $tcidr = NetAddr::IP->new($val);
724 if ($tcidr == $newcidr) {
725 # Match found for new CIDR. Delete this record.
726 $up_res = $dnsdb->delRec('n', 'y', $recid);
727 } else {
728 # Update this record, then exit the loop
729 $up_res = rpc_updateRec(%args, val => $newcidr, id => $recid, defrec => 'n', revrec => 'y');
730 last;
731 }
732 # Your llama is on fire
733 }
734 $sth->finish;
735
736 return "Template record for $oldcidr changed to $newcidr.";
737
738 } else { # $cidr > $zone but we only have one zone
739 # ebbeh? CIDR is only partly represented in DNS. This needs manual intervention.
740 return "Warning: $args{cidr} is only partly represented in DNS. Check and update DNS records manually.";
741 } # done single-zone-contains-$cidr
742
743 } else {
744 # multiple zones nominally "contain" $cidr
745 }
746
747 return $up_res;
748} # done resizeTemplate()
749
750# Convert one or more template records to a set of individual IP records. Expands the template.
751# Handle the case of nested templates, although the primary caller (IPDB) should not be
752# able to generate records that would trigger that case.
753# Accounts for existing PTR or A+PTR records same as on-export template expansion.
754# Takes a list of templates and a bounding CIDR?
755sub templatesToRecords {
756 my %args = @_;
757
758 _commoncheck(\%args, 'y');
759
760 my %iplist;
761 my @retlist;
762
763 my $zsth = $dnsdb->{dbh}->prepare("SELECT rdns_id,group_id FROM revzones WHERE revnet >>= ?");
764 # Going to assume template records with no expiry
765 # Also note IPv6 template records don't expand sanely the way v4 records do
766 my $recsth = $dnsdb->{dbh}->prepare(q(
767 SELECT record_id, domain_id, host, type, val, ttl, location
768 FROM records
769 WHERE rdns_id = ?
770 AND type IN (12, 65280, 65282, 65283)
771 AND inetlazy(val) <<= ?
772 ORDER BY masklen(inetlazy(val)) DESC
773 ));
774 my $insth = $dnsdb->{dbh}->prepare("INSERT INTO records (domain_id, rdns_id, host, type, val, ttl, location)".
775 " VALUES (?,?,?,?,?,?,?)");
776 my $delsth = $dnsdb->{dbh}->prepare("DELETE FROM records WHERE record_id = ?");
777 my %typedown = (12 => 12, 65280 => 65280, 65281 => 65281, 65282 => 12, 65283 => 65280, 65284 => 65281);
778
779 my @checkrange;
780
781 local $dnsdb->{dbh}->{AutoCommit} = 0;
782 local $dnsdb->{dbh}->{RaiseError} = 1;
783
784 eval {
785 foreach my $template (@{$args{templates}}) {
786 $zsth->execute($template);
787 my ($zid,$zgrp) = $zsth->fetchrow_array;
788 if (!$zid) {
789 push @retlist, {$template, "Zone not found"};
790 next;
791 }
792 $recsth->execute($zid, $template);
793 while (my ($recid, $domid, $host, $type, $val, $ttl, $loc) = $recsth->fetchrow_array) {
794 # Skip single IPs with PTR or A+PTR records
795 if ($type == 12 || $type == 65280) {
796 $iplist{"$val/32"}++;
797 next;
798 }
799 my @newips = NetAddr::IP->new($template)->split(32);
800 $type = $typedown{$type};
801 foreach my $ip (@newips) {
802 next if $iplist{$ip};
803 my $newhost = $host;
804 DNSDB::_template4_expand(\$newhost, $ip->addr);
805 $insth->execute($domid, $zid, $newhost, $type, $ip->addr, $ttl, $loc);
806 $iplist{$ip}++;
807 }
808 $delsth->execute($recid);
809 $dnsdb->_log(group_id => $zgrp, domain_id => $domid, rdns_id => $zid,
810 entry => "$template converted to individual $typemap{$type} records");
811 push @retlist, "$template converted to individual records";
812 } # record fetch
813 } # foreach passed template CIDR
814
815 $dnsdb->{dbh}->commit;
816 };
817 if ($@) {
818 die "Error converting a template record to individual records: $@";
819 }
820
821 return \@retlist;
822
823} # done templatesToRecords()
824
825sub delRec {
826 my %args = @_;
827
828 _commoncheck(\%args, 'y');
829
830 _reccheck(\%args);
831
832 my ($code, $msg) = $dnsdb->delRec($args{defrec}, $args{recrev}, $args{id});
833
834 die "$msg\n" if $code eq 'FAIL';
835 return $msg;
836}
837
838sub delByCIDR {
839 my %args = @_;
840
841 _commoncheck(\%args, 'y');
842
843 # Caller may pass 'n' in delsubs. Assume it should be false/undefined
844 # unless the caller explicitly requested 'yes'
845 $args{delsubs} = 0 if $args{delsubs} ne 'y';
846
847 # much like addOrUpdateRevRec()
848 my $zonelist = $dnsdb->getZonesByCIDR(%args);
849 my $cidr = new NetAddr::IP $args{cidr};
850
851 if (scalar(@$zonelist) == 0) {
852 # enhh.... WTF?
853 } elsif (scalar(@$zonelist) == 1) {
854
855 # check if the single zone returned is bigger than the CIDR
856 my $zone = new NetAddr::IP $zonelist->[0]->{revnet};
857 if ($zone->contains($cidr)) {
858 if ($args{delsubs}) {
859 # Delete ALL EVARYTHING!!one11!! in $args{cidr}
860 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y', id => $zonelist->[0]->{rdns_id});
861 foreach my $rec (@$reclist) {
862 my $reccidr = new NetAddr::IP $rec->{val};
863 next unless $cidr->contains($reccidr);
864 next unless $rec->{type} == 12 || $rec->{type} == 65280 || $rec->{type} == 65281 ||
865 $rec->{type} == 65282 || $rec->{type} == 65283 ||$rec->{type} == 65284;
866 ##fixme: multiple records, wanna wax'em all, how to report errors?
867 if ($args{delforward} ||
868 $rec->{type} == 12 || $rec->{type} == 65282 ||
869 $rec->{type} == 65283 || $rec->{type} == 65284) {
870 my ($code,$msg) = $dnsdb->delRec('n', 'y', $rec->{record_id});
871 } else {
872 my $ret = $dnsdb->downconvert($rec->{record_id}, $DNSDB::reverse_typemap{A});
873 }
874 }
875 if ($args{parpatt} && $zone == $cidr) {
876 # Edge case; we've just gone and axed all the records in the reverse zone.
877 # Re-add one to match the parent if we've been given a pattern to use.
878 rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zonelist->[0]->{rdns_id},
879 type => ($zone->{isv6} ? 65284 : 65283), address => "$cidr", name => $args{parpatt}, %args);
880 }
881
882 } else {
883 # Selectively delete only exact matches on $args{cidr}
884 # We need to strip the CIDR mask on IPv4 /32 assignments, or we can't find single-IP records
885 my $filt = ($cidr->{isv6} || $cidr->masklen != 32 ? "$cidr" : $cidr->addr);
886 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y',
887 id => $zonelist->[0]->{rdns_id}, filter => $filt, sortby => 'val', sortorder => 'DESC');
888 foreach my $rec (@$reclist) {
889 my $reccidr = new NetAddr::IP $rec->{val};
890 next unless $cidr == $reccidr;
891 next unless $rec->{type} == 12 || $rec->{type} == 65280 || $rec->{type} == 65281 ||
892 $rec->{type} == 65282 || $rec->{type} == 65283 ||$rec->{type} == 65284;
893 if ($args{delforward} || $rec->{type} == 12) {
894 my ($code,$msg) = $dnsdb->delRec('n', 'y', $rec->{record_id});
895 die "$msg\n" if $code eq 'FAIL';
896 return $msg;
897 } else {
898 my $ret = $dnsdb->downconvert($rec->{record_id}, $DNSDB::reverse_typemap{A});
899 die $dnsdb->errstr."\n" if !$ret;
900 return "A+PTR for $args{cidr} split and PTR removed";
901 }
902 } # foreach @$reclist
903 }
904
905 } else { # $cidr > $zone but we only have one zone
906 # ebbeh? CIDR is only partly represented in DNS. This needs manual intervention.
907 return "Warning: $args{cidr} is only partly represented in DNS. Check and remove DNS records manually.";
908 } # done single-zone-contains-$cidr
909
910 } else { # multiple zones nominally "contain" $cidr
911 # Overlapping reverse zones shouldn't be possible, so if we're here we've got a CIDR
912 # that spans multiple reverse zones (eg, /23 CIDR -> 2 /24 rzones)
913 foreach my $zdata (@$zonelist) {
914 my $reclist = $dnsdb->getRecList(defrec => 'n', revrec => 'y', id => $zdata->{rdns_id});
915 if (scalar(@$reclist) == 0) {
916# nothing to do? or do we (re)add a record based on the parent?
917# yes, yes we do, past the close of the else
918# my $type = ($args{cidr}->{isv6} ? 65282 : ($args{cidr}->masklen == 32 ? 65280 : 65283) );
919# rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zdata->{rdns_id}, type => $type,
920# address => "$args{cidr}", %args);
921 } else {
922 foreach my $rec (@$reclist) {
923 next unless $rec->{type} == 12 || $rec->{type} == 65280 || $rec->{type} == 65281 ||
924 $rec->{type} == 65282 || $rec->{type} == 65283 || $rec->{type} == 65284;
925 # Template types are only useful when attached to a reverse zone.
926##fixme ..... or ARE THEY?
927 if ($args{delforward} ||
928 $rec->{type} == 12 || $rec->{type} == 65282 ||
929 $rec->{type} == 65283 || $rec->{type} == 65284) {
930 my ($code,$msg) = $dnsdb->delRec('n', 'y', $rec->{record_id});
931 } else {
932 my $ret = $dnsdb->downconvert($rec->{record_id}, $DNSDB::reverse_typemap{A});
933 }
934 } # foreach @$reclist
935 } # nrecs != 0
936 if ($args{parpatt}) {
937 # We've just gone and axed all the records in the reverse zone.
938 # Re-add one to match the parent if we've been given a pattern to use.
939 rpc_addRec(defrec => 'n', revrec => 'y', parent_id => $zdata->{rdns_id},
940 type => ($cidr->{isv6} ? 65284 : 65283),
941 address => $zdata->{revnet}, name => $args{parpatt}, %args);
942 }
943 } # iterate zones within $cidr
944 } # done $cidr-contains-zones
945
946} # end delByCIDR()
947
948#sub getLogCount {}
949#sub getLogEntries {}
950
951sub getRevPattern {
952 my %args = @_;
953
954 _commoncheck(\%args, 'y');
955
956 return $dnsdb->getRevPattern($args{cidr}, $args{group});
957}
958
959sub getRevSet {
960 my %args = @_;
961
962 _commoncheck(\%args, 'y');
963
964 return $dnsdb->getRevSet($args{cidr}, $args{group});
965}
966
967sub getTypelist {
968 my %args = @_;
969 _commoncheck(\%args, 'y');
970
971 $args{selected} = $reverse_typemap{A} if !$args{selected};
972
973 return $dnsdb->getTypelist($args{recgroup}, $args{selected});
974}
975
976sub getTypemap {
977 my %args = @_;
978 _commoncheck(\%args, 'y');
979 return \%typemap;
980}
981
982sub getReverse_typemap {
983 my %args = @_;
984 _commoncheck(\%args, 'y');
985 return \%reverse_typemap;
986}
987
988#sub parentID {}
989#sub isParent {}
990
991sub zoneStatus {
992 my %args = @_;
993
994 _commoncheck(\%args, 'y');
995
996 my @arglist = ($args{zoneid});
997 push @arglist, $args{status} if defined($args{status});
998
999 my $status = $dnsdb->zoneStatus(@arglist);
1000}
1001
1002# Get a list of hashes referencing the reverse zone(s) for a passed CIDR block
1003sub getZonesByCIDR {
1004 my %args = @_;
1005
1006 _commoncheck(\%args, 'y');
1007
1008 return $dnsdb->getZonesByCIDR(%args);
1009}
1010
1011#sub importAXFR {}
1012#sub importBIND {}
1013#sub import_tinydns {}
1014#sub export {}
1015#sub __export_tiny {}
1016#sub _printrec_tiny {}
1017#sub mailNotify {}
1018
1019sub get_method_list {
1020 my @methods = keys %{$methods};
1021 return \@methods;
1022}
Note: See TracBrowser for help on using the repository browser.