File Coverage

lib/Provision/Unix/DNS/tinydns.pm
Criterion Covered Total %
statement 37 188 19.6
branch 4 44 9.0
condition 0 53 0.0
subroutine 10 29 34.4
pod 0 20 0.0
total 51 334 15.2


line stmt bran cond sub pod time code
1             package Provision::Unix::DNS::tinydns;
2             # ABSTRACT: Provision tinydns DNS entries
3              
4 1     1   7 use strict;
  1         2  
  1         124  
5 1     1   7 use warnings;
  1         2  
  1         95  
6              
7             our $VERSION = '0.53';
8              
9 1     1   6 use Cwd;
  1         1  
  1         151  
10 1     1   8 use English qw( -no_match_vars );
  1         2  
  1         26  
11 1     1   1579 use Params::Validate qw(:all);
  1         3  
  1         426  
12              
13 1     1   6 use lib 'lib';
  1         2  
  1         97  
14 1     1   1584 use Provision::Unix::Utility;
  1         2  
  1         8089  
15              
16             my ( $prov, $util );
17              
18             sub new {
19 1     1 0 7 my $class = shift;
20              
21 1         36 my %p = validate( @_, { 'prov' => { type => OBJECT }, } );
22              
23 1         7 my $self = { prov => $p{prov}, };
24 1         3 bless( $self, $class );
25              
26 1         4 $prov = $p{prov};
27 1         6 $prov->audit("loaded DNS/tinydns");
28 1         94 $util = $prov->get_util;
29              
30             #$self->{server} = $self->_load_DNS_TinyDNS();
31 1         4 $self->{special} = $self->_special_chars();
32 1         11 return $self;
33             }
34              
35             sub create_zone {
36 0     0 0 0 my $self = shift;
37              
38 0         0 my %p = validate(
39             @_,
40             { 'zone' => { type => SCALAR },
41             'contact' => { type => SCALAR | UNDEF, optional => 1 },
42             'serial' => { type => SCALAR | UNDEF, optional => 1 },
43             'ttl' => { type => SCALAR | UNDEF, optional => 1 },
44             'refresh' => { type => SCALAR | UNDEF, optional => 1 },
45             'retry' => { type => SCALAR | UNDEF, optional => 1 },
46             'expire' => { type => SCALAR | UNDEF, optional => 1 },
47             'minimum' => { type => SCALAR | UNDEF, optional => 1 },
48             'nameserver' => { type => SCALAR | UNDEF, optional => 1, },
49             'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
50             'debug' => { type => BOOLEAN, optional => 1, default => 1 },
51             }
52             );
53              
54 0         0 my $zone = $p{zone};
55              
56 0         0 $prov->audit("creating zone $zone");
57              
58 0         0 my $service_dir = $prov->{config}{tinydns}{service_dir};
59 0 0       0 if ( $self->get_zone( zone => $zone, fatal => 0 ) ) {
60 0         0 return $prov->error( "zone $zone already exists",
61             fatal => $p{fatal},
62             debug => $p{debug},
63             );
64             }
65              
66             # publishing an explicit SOA record for every zone managed is
67             # a reliable way to determine if a zone is provisioned
68             #
69             # SOA, Zfqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo
70             #Ztesting.com:x2.nictool.com.:hostmaster.testing.com::16384:2048:1048576:2560:86400::
71              
72 0   0     0 my $nameserver = $self->qualify( $zone, $p{nameserver} || "a.ns" );
73              
74 0         0 my $soa = $self->{special}{SOA};
75              
76 0   0     0 $soa .= join(
      0        
      0        
      0        
      0        
      0        
77             ":",
78             $p{zone},
79             $nameserver, # mname
80             $p{contact} || "hostmaster.$p{zone}", # rname
81             '', # serial, blank lets tinydns autogenerate
82             $p{refresh} || $prov->{config}{DNS}{zone_refresh},
83             $p{retry} || $prov->{config}{DNS}{zone_retry},
84             $p{expire} || $prov->{config}{DNS}{zone_expire},
85             $p{minimum} || $prov->{config}{DNS}{zone_minimum},
86             $p{ttl} || $prov->{config}{DNS}{zone_ttl},
87             );
88 0         0 $soa .= ":"; # timestamp
89 0         0 $soa .= ":"; # location (ala, split horizon)
90              
91             # append the record to $data
92 0         0 $util->file_write( "$service_dir/root/data",
93             lines => [$soa],
94             append => 1,
95             debug => $p{debug},
96             );
97              
98 0         0 $self->compile_data_cdb();
99              
100 0         0 return 1;
101             }
102              
103             sub create_zone_record {
104 0     0 0 0 my $self = shift;
105 0         0 my %p = validate(
106             @_,
107             { 'zone' => { type => SCALAR },
108             'zone_id' => { type => SCALAR, optional => 1 },
109             'type' => { type => SCALAR },
110             'name' => { type => SCALAR },
111             'address' => { type => SCALAR },
112             'weight' => { type => SCALAR, optional => 1 },
113             'ttl' => { type => SCALAR, optional => 1 },
114             'priority' => { type => SCALAR, optional => 1 },
115             'port' => { type => SCALAR, optional => 1 },
116             'debug' => { type => SCALAR, optional => 1, default => 1 },
117             'fatal' => { type => SCALAR, optional => 1, default => 1 },
118             }
119             );
120              
121 0         0 my $type = uc( $p{type} );
122              
123 0         0 $prov->audit("creating $type record in $p{zone}");
124              
125 0 0       0 if ( !$self->get_zone( zone => $p{zone} ) ) {
126 0         0 $prov->error( "zone $p{zone} does not exist!" );
127             }
128              
129 0 0       0 my $record
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
130             = $type eq 'A' ? $self->build_a( \%p )
131             : $type eq 'MX' ? $self->build_mx( \%p )
132             : $type eq 'NS' ? $self->build_ns( \%p )
133             : $type eq 'PTR' ? $self->build_ptr( \%p )
134             : $type eq 'TXT' ? $self->build_txt( \%p )
135             : $type eq 'CNAME' ? $self->build_cname( \%p )
136             : $type eq 'SRV' ? $self->build_srv( \%p )
137             : $type eq 'NAPTR' ? $self->build_naptr( \%p )
138             : $type eq 'AAAA' ? $self->build_aaaa( \%p )
139             : $prov->error( 'invalid record type', fatal => $p{fatal} );
140              
141 0         0 my $service_dir = $prov->{config}{tinydns}{service_dir};
142              
143             # append the record to $data
144 0         0 $util->file_write( "$service_dir/root/data",
145             lines => [$record],
146             append => 1,
147             debug => $p{debug},
148             );
149              
150 0         0 $self->compile_data_cdb();
151              
152 0         0 return $record;
153             }
154              
155             sub build_a {
156 0     0 0 0 my ( $self, $p ) = @_;
157              
158             # +fqdn:ip:ttl:timestamp:lo
159              
160 0   0     0 return $self->{special}{A}
161             . $self->qualify( $p->{zone}, $p->{name} )
162             . ':' . $p->{address}
163             . ':' . $p->{ttl} || $prov->{config}{DNS}{ttl}
164             . '::';
165             }
166              
167             sub build_mx {
168 0     0 0 0 my ( $self, $p ) = @_;
169              
170             # @fqdn:ip:x:dist:ttl:timestamp:lo
171 0         0 my $r = $self->{special}{MX};
172 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ":";
173 0         0 $r .= ":"; # ip leave blank, defined with an A record
174 0         0 $r .= $self->qualify( $p->{zone}, $p->{address} ) . ".:";
175 0         0 $r .= $p->{weight} . ":";
176 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
177              
178 0         0 return $r;
179             }
180              
181             sub build_ns {
182 0     0 0 0 my ( $self, $p ) = @_;
183              
184             # &fqdn:ip:x:ttl:timestamp:lo (NS + A)
185 0         0 my $r = $self->{special}{NS};
186 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ":";
187 0         0 $r .= ":"; # ip leave blank, defined with an A record
188 0         0 $r .= $self->qualify( $p->{zone}, $p->{address} ) . ".:";
189 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
190              
191 0         0 return $r;
192             }
193              
194             sub build_cname {
195 0     0 0 0 my ( $self, $p ) = @_;
196              
197             # Cfqdn:p:ttl:timestamp:lo
198 0         0 my $r = $self->{special}{CNAME};
199 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ":";
200 0         0 $r .= $self->qualify( $p->{zone}, $p->{address} ) . ".:";
201 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
202              
203 0         0 return $r;
204             }
205              
206             sub build_txt {
207 0     0 0 0 my ( $self, $p ) = @_;
208              
209             # 'fqdn:s:ttl:timestamp:lo
210 0         0 my $r = $self->{special}{TXT};
211 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ":";
212 0         0 $r .= $self->escape( $p->{address} ) . ":";
213 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
214              
215 0         0 return $r;
216             }
217              
218             sub build_ptr {
219 0     0 0 0 my ( $self, $p ) = @_;
220              
221             # ^fqdn:p:ttl:timestamp:lo
222 0         0 my $r = $self->{special}{PTR};
223             ## TODO
224             # check that our zone matches NN.in-addr.arpa and/or a pattern
225             # the can be automatically expanded as such
226              
227 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ":";
228 0         0 $r .= $p->{address} . ":";
229 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
230              
231 0         0 return $r;
232             }
233              
234             sub build_soa {
235 0     0 0 0 my $self = shift;
236             # Zfqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo
237             };
238              
239             sub build_srv {
240 0     0 0 0 my ( $self, $p ) = @_;
241              
242             # :fqdn:n:rdata:ttl:timestamp:lo (Generic record)
243 0         0 my $priority = $p->{priority};
244 0         0 my $weight = $p->{weight};
245 0         0 my $port = $p->{port};
246              
247             # SRV
248             # :sip.tcp.example.com:33:\000\001\000\002\023\304\003pbx\007example\003com\000
249 0 0 0     0 if ( $priority < 0 || $priority > 65535 ) {
250 0         0 $prov->error( "priority $priority not within 0 - 65535" );
251             }
252 0 0 0     0 if ( $weight < 0 || $weight > 65535 ) {
253 0         0 $prov->error( "weight $weight not within 0 - 65535" );
254             }
255 0 0 0     0 if ( $port < 0 || $port > 65535 ) {
256 0         0 $prov->error( "port $port not within 0 - 65535" );
257             }
258              
259 0         0 $priority = escapeNumber($priority);
260 0         0 $weight = escapeNumber($weight);
261 0         0 $port = escapeNumber($port);
262              
263 0         0 my $target = "";
264 0         0 my @chunks = split /\./,
265             $self->qualify( $p->{zone}, $p->{address} );
266 0         0 foreach my $chunk (@chunks) {
267 0         0 $target .= characterCount($chunk) . $chunk;
268             }
269              
270 0         0 my $service = $self->qualify( $p->{zone}, $p->{name} );
271 0         0 $service = escape($service);
272              
273 0         0 my $r = ":";
274 0         0 $r .= "$service:33:" . $priority . $weight . $port;
275 0         0 $r .= $target . "\\000:";
276 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
277              
278 0         0 return $r;
279             }
280              
281             sub build_aaaa {
282 0     0 0 0 my ( $self, $p ) = @_;
283              
284             # :fqdn:n:rdata:ttl:timestamp:lo (generic record format)
285             # ffff:1234:5678:9abc:def0:1234:0:0
286             # :example.com:28:\377\377\022\064\126\170\232\274\336\360\022\064\000\000\000\000
287              
288 0         0 my ( $a, $b, $c, $d, $e, $f, $g, $h ) = split /:/, $p->{address};
289 0 0       0 if ( !defined $h ) {
290 0         0 die "Didn't get a valid-looking IPv6 address\n";
291             }
292              
293 0         0 $a = escapeHex( sprintf "%04s", $a );
294 0         0 $b = escapeHex( sprintf "%04s", $b );
295 0         0 $c = escapeHex( sprintf "%04s", $c );
296 0         0 $d = escapeHex( sprintf "%04s", $d );
297 0         0 $e = escapeHex( sprintf "%04s", $e );
298 0         0 $f = escapeHex( sprintf "%04s", $f );
299 0         0 $g = escapeHex( sprintf "%04s", $g );
300 0         0 $h = escapeHex( sprintf "%04s", $h );
301              
302 0         0 my $r = ':';
303 0         0 $r .= $self->qualify( $p->{zone}, $p->{name} ) . ':';
304 0         0 $r .= '28:' . "$a$b$c$d$e$f$g$h" . ':';
305 0   0     0 $r .= $p->{ttl} || $prov->{config}{DNS}{ttl};
306              
307 0         0 return $r;
308             }
309              
310             sub qualify {
311 3     3 0 6 my $self = shift;
312 3         15 my ( $zone, $record ) = @_;
313              
314 3 100       16 return $record if $record =~ /\.$/; # already ends in .
315              
316             # append the zone name if needed
317 2 100       46 return "$record.$zone" if $record !~ /$zone$/;
318              
319 1         6 return $record;
320             }
321              
322             sub compile_data_cdb {
323              
324 0     0 0 0 my $self = shift;
325              
326 0         0 my $service_dir = $prov->{config}{tinydns}{service_dir};
327 0         0 my $data_dir = "$service_dir/root";
328              
329 0         0 my $tdata = $util->find_bin( 'tinydns-data', debug => 0 );
330              
331             # compile the data.cdb file
332 0         0 my $original_wd = getcwd;
333 0 0       0 chdir($data_dir)
334             or $prov->error( "unable to chdir to $data_dir" );
335 0 0       0 system $tdata and $prov->error( "could not compile data" );
336 0         0 chdir $original_wd;
337              
338 0         0 return 1;
339             }
340              
341             sub get_zone {
342              
343 0     0 0 0 my $self = shift;
344              
345 0         0 my %p = validate(
346             @_,
347             { 'zone' => { type => SCALAR },
348             'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
349             'debug' => { type => BOOLEAN, optional => 1, default => 1 },
350             }
351             );
352              
353 0         0 my $zone = $p{zone};
354 0         0 $prov->audit("getting zone $zone");
355              
356 0         0 my $service_dir = $prov->{config}{tinydns}{service_dir};
357 0         0 my @lines = $util->file_read( "$service_dir/root/data" );
358              
359 0         0 @lines = grep ( /^Z$zone:/, @lines );
360              
361             #warn "matching zones:\n", join ("\n", @lines), "\n";
362              
363 0 0       0 if ( scalar @lines > 0 ) {
364 0         0 $prov->audit( "\tfound " . substr( $lines[0], 0, 35 ) . '...' );
365 0         0 return 1;
366             }
367 0         0 return;
368             }
369              
370             sub delete_zone {
371              
372 0     0 0 0 my $self = shift;
373              
374 0         0 my %p = validate(
375             @_,
376             { 'id' => { type => SCALAR, optional => 1 },
377             'zone' => { type => SCALAR },
378             'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
379             'debug' => { type => BOOLEAN, optional => 1, default => 1 },
380             }
381             );
382              
383 0         0 $prov->audit("getting zone $p{zone}");
384              
385             }
386              
387 0     0   0 sub _load_DNS_TinyDNS {
388              
389             # my $self = shift;
390              
391             # eval { require DNS::TinyDNS; };
392              
393             # if ($EVAL_ERROR) {
394             # $prov->error( "could not load DNS::TinyDNS. Is it installed?" );
395             # }
396              
397             # my $service_dir = $prov->{config}{tinydns}{service_dir};
398              
399             # $prov->audit("loaded DNS::TinyDNS");
400             # return DNS::TinyDNS->new(
401             # type => 'dnsserver',
402             # dir => $service_dir
403             # );
404             }
405              
406             sub _special_chars {
407 1     1   9 my %special = (
408             A => '+', # fqdn : ip : ttl:timestamp:lo
409             MX => '@', # fqdn : ip : x:dist:ttl:timestamp:lo
410             NS => '&', # fqdn : ip : x:ttl:timestamp:lo
411             CNAME => 'C', # fqdn : p : ttl:timestamp:lo
412             PTR => '^', # fqdn : p : ttl:timestamp:lo
413             TXT => "'", # fqdn : s : ttl:timestamp:lo
414             SOA => 'Z', # fqdn:mname:rname:ser:ref:ret:exp:min:ttl:time:lo
415             IGNORE => '-', # fqdn : ip : ttl:timestamp:lo
416             'A,PTR' => '=', # fqdn : ip : ttl:timestamp:lo
417             'SOA,NS,A' => '.', # fqdn : ip : x:ttl:timestamp:lo
418             GENERIC => ':', # fqdn : n : rdata:ttl:timestamp:lo
419             );
420 1         10 return \%special;
421             }
422              
423              
424             my $stuff = <<'IGNORE'
425             # SPF
426             # ":$domain:16:" . characterCount( $text ) . escape( $text ) . ":" . $ttl;
427             NAPTR
428             # :comunip.com:35:\000\012\000\144\001u\007E2U+sip\036!^.*$!sip\072info@comunip.com.br!\000:300
429             # |-order-|-pref--|flag|-services-|---------------regexp---------------|re-|
430             if ( ( $order >= 0 && $order <= 65535 ) &&
431             ( $prefrence >= 0 && $prefrence <= 65535 ) &&
432             ( $flag eq "u" ) ) {
433             $result = ":" . escape( $domain ) . ":35:" . escapeNumber( $order ) .
434             escapeNumber( $prefrence ) . characterCount( $flag ) . $flag .
435             characterCount( $services ) . escape( $services ) .
436             characterCount( $regexp ) . escape( $regexp );
437              
438             if ( $replacement ne "" ) {
439             $result = $result . characterCount( $replacement ) . escape( $replacement );
440             }
441             $result = $result . "\\000:" . $ttl;
442              
443             print $result;
444             }
445             else {
446             print "priority, weight or port not within 0 - 65535\n";
447             }
448             }
449             domainKeys
450             # :joe._domainkey.anders.com:16:\341k=rsa; p=MIGfMA0GCSqGSIb3DQ ... E2hHCvoVwXqyZ/MbQIDAQAB
451             # |lt| |typ| |-key----------------------------------------|
452             if ( $key ne "" ) {
453             $key = $key;
454             $key =~ s/\r//g;
455             $key =~ s/\n//g;
456             $line = "k=" . $encryptionType . "; p=" . $key;
457             $result = ":" . escape( $domain ) . ":16:" . characterCount( $line ) .
458             escape( $line ) . ":" . $ttl;
459             print $result;
460             }
461             else {
462             print "didn't get a valid key for the key field\n";
463             }
464             }
465              
466             IGNORE
467             ;
468              
469             # based on http://www.anders.com/projects/sysadmin/djbdnsRecordBuilder/
470             sub escape {
471 0     0 0   my $line = pop @_;
472 0           my $out;
473              
474 0           foreach my $char ( split //, $line ) {
475 0 0         if ( $char =~ /[\r\n\t: \\\/]/ ) {
476 0           $out .= sprintf "\\%.3lo", ord $char;
477             }
478             else {
479 0           $out .= $char;
480             }
481             }
482 0           return $out;
483             }
484              
485             sub escapeNumber {
486 0     0 0   my $number = pop @_;
487 0           my $highNumber = 0;
488              
489 0 0         if ( $number - 256 >= 0 ) {
490 0           $highNumber = int( $number / 256 );
491 0           $number = $number - ( $highNumber * 256 );
492             }
493 0           my $out = sprintf "\\%.3lo", $highNumber;
494 0           $out .= sprintf "\\%.3lo", $number;
495              
496 0           return $out;
497             }
498              
499             sub escapeHex {
500              
501             # takes a 4 character hex value and converts it to two escaped numbers
502 0     0 0   my $line = pop @_;
503 0           my @chars = split //, $line;
504              
505 0           my $out = sprintf "\\%.3lo", hex "$chars[0]$chars[1]";
506 0           $out .= sprintf "\\%.3lo", hex "$chars[2]$chars[3]";
507              
508 0           return ($out);
509             }
510              
511             sub characterCount {
512 0     0 0   my $line = pop @_;
513 0           my @chars = split //, $line;
514 0           my $count = @chars;
515              
516 0           return ( sprintf "\\%.3lo", $count );
517             }
518              
519             1;
520              
521              
522              
523              
524             __END__
525             =pod
526              
527             =head1 NAME
528              
529             Provision::Unix::DNS::tinydns - Provision tinydns DNS entries
530              
531             =head1 VERSION
532              
533             version 1.06
534              
535             =head1 SYNOPSIS
536              
537             Provision DNS entries into a tinydns DNS management system using the tinydns native API.
538              
539             use Provision::Unix::DNS::tinydns;
540              
541             my $dns = Provision::Unix::DNS::tinydns->new();
542             ...
543              
544             =head1 FUNCTIONS
545              
546             =head1 BUGS
547              
548             Please report any bugs or feature requests to C<bug-unix-provision-dns at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Provision-Unix>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
549              
550             =head1 SUPPORT
551              
552             You can find documentation for this module with the perldoc command.
553              
554             perldoc Provision::Unix::DNS::tinydns
555              
556             You can also look for information at:
557              
558             =over 4
559              
560             =item * RT: CPAN's request tracker
561              
562             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Provision-Unix>
563              
564             =item * AnnoCPAN: Annotated CPAN documentation
565              
566             L<http://annocpan.org/dist/Provision-Unix>
567              
568             =item * CPAN Ratings
569              
570             L<http://cpanratings.perl.org/d/Provision-Unix>
571              
572             =item * Search CPAN
573              
574             L<http://search.cpan.org/dist/Provision-Unix>
575              
576             =back
577              
578             =head1 ACKNOWLEDGEMENTS
579              
580             some of the record generation logic was lifted from http://www.anders.com/projects/sysadmin/djbdnsRecordBuilder/
581              
582             =head1 AUTHOR
583              
584             Matt Simerson <msimerson@cpan.org>
585              
586             =head1 COPYRIGHT AND LICENSE
587              
588             This software is copyright (c) 2013 by The Network People, Inc..
589              
590             This is free software; you can redistribute it and/or modify it under
591             the same terms as the Perl 5 programming language system itself.
592              
593             =cut
594