File Coverage

lib/Net/ISC/DHCPd/Config/Role.pm
Criterion Covered Total %
statement 74 207 35.7
branch 19 88 21.5
condition 2 9 22.2
subroutine 8 109 7.3
pod 7 7 100.0
total 110 420 26.1


line stmt bran cond sub pod time code
1             package Net::ISC::DHCPd::Config::Role;
2              
3             =head1 NAME
4              
5             Net::ISC::DHCPd::Config::Role - Role with generic config methods and attributes
6              
7             =head1 DESCRIPTION
8              
9             This role contains common methods and attributes for each of the config
10             classes in the L<Net::ISC::DHCPd::Config> namespace.
11              
12             =head1 WARNINGS
13              
14             This module will warn when a line in the input config could not be parsed.
15             This can be turned off by adding the line below before calling L</parse>.
16              
17             no warnings 'net_isc_dhcpd_config_parse';
18              
19             =cut
20              
21 21     21   12352 use Class::Load;
  21         32  
  21         1024  
22 21     21   99 use Moose::Role;
  21         30  
  21         169  
23              
24             requires 'generate';
25              
26             =head1 ATTRIBUTES
27              
28             =head2 parent
29              
30             The parent node in the config tree. This must be an object which does
31             this role.
32              
33             =cut
34              
35             has parent => (
36             is => 'rw',
37             does => 'Net::ISC::DHCPd::Config::Role',
38             weak_ref => 1,
39             );
40              
41             =head2 root
42              
43             The root node in the config tree.
44              
45             =cut
46              
47             has root => (
48             is => 'ro',
49             isa => 'Object',
50             weak_ref => 1,
51             default => sub { $_[0] },
52             );
53              
54             =head2 depth
55              
56             Integer value that counts how far this node is from the root node.
57              
58             =cut
59              
60             has depth => (
61             is => 'ro',
62             isa => 'Int',
63             lazy => 1,
64             builder => '_build_depth',
65             );
66              
67             sub _build_depth {
68 0     0   0 my $self = shift;
69 0         0 my $obj = $self;
70 0         0 my $i = 0;
71              
72 0         0 while($obj = $obj->parent) {
73 0         0 $i++;
74 0 0       0 last if($obj == $self->root);
75             }
76              
77 0         0 return $i;
78             }
79              
80             =head2 children
81              
82             Holds a list of possible child objects as objects. This list is used
83             when L</parse> or L</generate_config_from_children> is called.
84             The child list has a default value set from L</create_children> in each
85             of the config modules. This is a static list, which reflects the actual
86             documentation from C<dhcpd.conf(5)>. Example:
87              
88             package Net::ISC::DHCPd::Config::Foo;
89             __PACKAGE__->create_children("Net::ISC::DHCPd::Config::Host");
90              
91             package main;
92             $config = Net::ISC::DHCPd::Config::Foo->new;
93             $config->add_host({ ... });
94             @host_objects = $config->find_hosts({ ... });
95             $config->remove_host({ ... });
96             @host_objects = $config->hosts;
97              
98             The L</create_children> method will autogenerate three methods and an
99             attribute. The name of the attribute and methods will be the last part
100             of the config class, with "s" at the end in some cases.
101              
102             =over 4
103              
104             =item foos
105              
106             C<foos> is the name the attribute as well as the accessor. The accessor
107             will auto-deref the array-ref to a list if called in list context. (yes:
108             be aware of this!).
109              
110             =item add_foo
111              
112             Instead of pushing values directly to the C<foos> list, an C<add_foo>
113             method is available. It can take either a hash, hash-ref or an object
114             to add/construct a new child.
115              
116             =item find_foos
117              
118             This method will return zero or more objects as a list. It takes
119             a hash-ref which will be matched against the object attributes of
120             the children.
121              
122             =item remove_foo
123              
124             This method will remove zero or more children from the C<foos> attribute.
125             The method takes a hash-ref which will be used to match against the
126             child list. It returns the number of child nodes actually matched and
127             removed.
128              
129             =back
130              
131             =cut
132              
133             # should be overridden by anything that has children
134 0     0 1 0 sub children { }
135              
136             # actual children
137             has _children => (
138             is => 'ro',
139             isa => 'ArrayRef',
140             default => sub { [] },
141             );
142              
143             =head2 comments
144              
145             @str = $self->comments;
146              
147             Will return all the comments before this element appeared in the config file.
148             The comments will not contain leading hash symbol spaces, nor trailing newline.
149              
150             =cut
151              
152             has _comments => (
153             is => 'ro',
154             traits => ['Array'],
155             init_arg => 'comments',
156             default => sub { [] },
157             handles => {
158             comments => 'elements',
159             },
160             );
161              
162             =head2 regex
163              
164             Regex used to scan a line of config text, which then spawns an
165             a new node to the config tree. This is used inside l</parse>.
166              
167             THIS IS A STATIC METHOD. SELF is not used.
168              
169             =cut
170              
171             has _filehandle => (
172             is => 'ro',
173             lazy_build => 1,
174             );
175              
176             sub _build__filehandle {
177 2     2   3 my $self = shift;
178 2         3 my $file;
179              
180             # get filehandle from parent to prevent seeking file from beginning
181 2 50       46 if(my $parent = $self->parent) {
182 0         0 return $parent->_filehandle;
183             }
184              
185 2 100       284 if ($self->fh) {
186 1         23 return $self->fh;
187             }
188              
189 1         28 $file = $self->file;
190              
191 1 50 33     16 if($file->is_relative and !-e $file) {
192 0         0 $file = Path::Class::File->new($self->root->file->dir, $file);
193             }
194              
195 1         105 return $file->openr;
196             }
197              
198             =head2 filename_callback
199              
200             Callback for changing file paths when include files are on different relative paths.
201              
202             # here is an example:
203             my $cb = sub {
204             my $file = shift;
205             print "We're in callback and file is $file\n";
206             if ($file =~ /catphotos/) {
207             return "/dog.conf";
208             }
209             };
210              
211             =cut
212              
213             has filename_callback => (
214             is => 'rw',
215             isa => 'CodeRef',
216             );
217              
218              
219             =head1 METHODS
220              
221             =head2 BUILD
222              
223             Used to convert input arguments to child nodes.
224              
225             =cut
226              
227             sub BUILD {
228 2     2 1 2956 my($self, $args) = @_;
229             # skip the expensive method check if called through the parser. The only
230             # time this code applies is with code like add_method( submethod => { } );
231 2 50       14 return if ($args->{'parse'});
232 2         10 my $meta = $self->meta;
233              
234 2         39 for my $key (sort keys %$args) {
235 3         6 my $list = $args->{$key};
236 3         5 my $method = "add_$key";
237 3         6 $method =~ s/s$//;
238 3 50 33     17 if(ref $list eq 'ARRAY' and $meta->has_method($method)) {
239 0         0 for my $element (@$list) {
240 0         0 $self->$method($element);
241             }
242             }
243             }
244             }
245              
246             =head2 parse
247              
248             Will read a line of the time from the current config
249             L<file|Net::ISC::DHCPd::Config::Root/file>. For each line, this method
250             will loop though each object in L</children> and try to match the line
251             against a given child and create a new node in the object graph if it
252             match the L</regex>. This method is called recursively for each child
253             when possible.
254              
255             =cut
256              
257             sub parse {
258 2     2 1 725 my $self = shift;
259 2         4 my $fh = $_[1];
260 2         3 my $linebuf = $_[2];
261 2         4 my($n, @comments);
262 0         0 my $lines;
263 2         3 my $line_from_array=0;
264             # if $fh is a File::Temp or IO::File object then comparing on assignment
265             # above makes it convert to a number (refaddr), which takes 4µs/call
266 2 50       9 if (!defined($fh)) {
267 2         59 $fh = $self->_filehandle;
268             }
269              
270             LINE:
271 2         3 while(1) {
272 2         3 my $line;
273 2 50       8 if (defined($linebuf->[0])) {
274 0         0 $line = pop(@{$linebuf});
  0         0  
275 0         0 $line_from_array=1;
276             } else {
277 2 50       24 defined($line = readline $fh) or last LINE;
278 2         4 $n++;
279 2         6 chomp $line;
280 2         2 $line_from_array=0;
281             # From here we need to preprocess the line to see if it can be broken
282             # into multiple lines. Something like group { option test; }
283             # lines with comments can't be handled by this so we do them first
284 2 50       9 if($line =~ /^\s*\#\s*(.*)/) {
285 0         0 push @comments, $1;
286 0         0 next LINE;
287             }
288              
289             # after semicolon or braces if there isn't a semicolon or return insert a newline
290 2 50       8 if ($line =~ s/([;\{\}])([^;\n\r])/$1\n$2/g) {
291 0         0 push(@{$linebuf}, reverse split(/\n/, $line));
  0         0  
292 0         0 next LINE;
293             }
294             }
295              
296              
297 2 50       17 if ($line =~ /^(?:\s*|\s*\{\s*)$/) {
    50          
298 0         0 next LINE;
299             }
300             elsif($line =~ /^\s*\}\s*$/) {
301 0 0       0 next LINE if($self->root == $self);
302 0         0 last LINE;
303             }
304              
305             # this is how we handle incomplete lines
306             # we need a space for lines like 'option\ndomain-name-servers'
307 2 50       8 if ($lines) {
308 0         0 $lines .= ' '.$line;
309             } else {
310 2         4 $lines = $line;
311             }
312              
313             CHILD:
314 2         19 for my $child ($self->children) {
315 21     21   104103 no strict 'refs';
  21         47  
  21         27896  
316 6         6 my $regex = ${"$child".'::regex'};
  6         25  
317 6 100       238 my @c = $lines =~ $regex or next CHILD;
318 2         19 my $add = 'add_' .lc +($child =~ /::(\w+)$/)[0];
319 2         32 my $method = $child->can('captured_to_args');
320 2         9 my $args = $method->(@c);
321 2         3 my $obj;
322              
323 2         6 $args->{'comments'} = [@comments];
324 2         4 $args->{'parse'} = 1;
325 2         3 @comments = ();
326 2         4 undef $lines;
327 2         141 $obj = $self->$add($args);
328 0 0       0 $n += $obj->_parse_slurp($fh, $linebuf) if ($obj->can('slurp'));
329              
330             # the recursive statement is used for Include.pm
331 0 0       0 $n += $obj->parse('recursive', $fh, $linebuf) if(@_ = $obj->children);
332              
333 0         0 next LINE;
334             }
335              
336             # if we get here that means our parse failed. If the incoming line
337             # doesn't have a semicolon then we can guess it's a partial line and
338             # append the next line to it.
339             # we could do this with Slurp but then everything would need to
340             # support slurp and odd semicolon handling. If we figure out a way to
341             # merge the lines then the normal parser should be able to cover it.
342 0 0       0 if ($lines !~ /;/) {
343 0         0 next LINE;
344             }
345              
346 0 0       0 if(warnings::enabled('net_isc_dhcpd_config_parse')) {
347 0         0 warn sprintf qq[Could not parse "%s" at %s line %s\n],
348             $line,
349             $self->root->file,
350             $fh->input_line_number
351             ;
352             }
353             }
354              
355 0 0       0 return $n ? $n : '0e0';
356             }
357              
358             =head2 _parse_slurp
359              
360             This is a simplified parser for the slurp method. It's only used when slurp
361             is available in a child method.
362              
363             =cut
364              
365              
366             sub _parse_slurp {
367 0     0   0 my $self = shift;
368 0         0 my $fh = shift;
369 0         0 my $linebuf = shift;
370 0         0 my($n, @comments);
371              
372             LINE:
373 0         0 while(1) {
374 0         0 my $line;
375 0 0       0 if (defined($linebuf->[0])) {
376 0         0 $line = pop(@{$linebuf});
  0         0  
377             } else {
378 0 0       0 defined($line = readline $fh) or last LINE;
379 0         0 $n++;
380 0         0 chomp $line;
381             }
382              
383              
384 0 0       0 if($self->can('slurp')) {
385 0         0 my $action = $self->slurp($line); # next or last
386 0 0       0 if($action eq 'next') {
    0          
    0          
387 0         0 next LINE;
388             }
389             elsif($action eq 'last') {
390 0         0 last LINE;
391             }
392             elsif($action eq 'backtrack') {
393 0         0 push(@{$linebuf}, $line);
  0         0  
394 0         0 last LINE;
395             }
396             }
397             }
398 0         0 return $n;
399             }
400              
401             =head2 captured_to_args
402              
403             $hash_ref = $self->captured_to_args(@list);
404              
405             Called when a L</regex> matches, with a list of captured strings.
406             This method then returns a hash-ref passed on to the constructor when
407             a new node in the object graph is constructed.
408              
409             THIS IS A STATIC METHOD. SELF is not used.
410              
411             =cut
412              
413             sub captured_to_args {
414 0     0 1 0 return {};
415             }
416              
417             =head2 create_children
418              
419             This method takes a list of classes, and creates builder method for
420             the L</children> attribute, an attribute and helper methods. See
421             L</children> for more details.
422              
423             =cut
424              
425             sub create_children {
426 87     87 1 199 my $self = shift;
427 87         431 my $meta = $self->meta;
428 87         1800 my @children = @_;
429              
430 87         224 for my $class (@children) {
431 245         6558 my $name = lc (($class =~ /::(\w+)$/)[0]);
432 245         591 my $attr = $name .'s';
433              
434             # hack so the child method for class is classes instead of classs
435 245 100       914 $attr = $name . 'es' if ($name =~ /s$/);
436              
437              
438 245         1094 Class::Load::load_class($class);
439              
440 202 50       7109 unless($meta->find_method_by_name($attr)) {
441 202     0   23613 $meta->add_method("add_${name}" => sub { shift->_add_child($class, @_) });
  0     0   0  
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
442 202     0   8229 $meta->add_method("find_${attr}" => sub { shift->_find_children($class, @_) });
  0     0   0  
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
443 202     0   6734 $meta->add_method("remove_${attr}" => sub { shift->_remove_children($class, @_) });
  0     0   0  
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
444             $meta->add_method($attr => sub {
445 0     0   0 my $self = shift;
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
        0      
446 0 0       0 return $self->_set_children($class, @_) if(@_);
447 0         0 return $self->_get_children_by_class($class);
448 202         6641 });
449             }
450             }
451              
452 44         1397 return \@children;
453             }
454              
455             sub _set_children {
456 0     0     my($self, $attr, $class, $children) = @_;
457              
458 0           for my $child (@$children) {
459 0 0         $child = $class->new(parent => $self, root => $self->root, %$child) if(ref $child eq 'HASH');
460             }
461              
462 0           @{ $self->_children } = @$children;
  0            
463             }
464              
465             sub _get_children_by_class {
466 0     0     my($self, $class) = @_;
467 0           my @children = grep { $class eq ref $_ } @{ $self->_children };
  0            
  0            
468              
469 0 0         return wantarray ? @children : \@children;
470             }
471              
472             sub _add_child {
473 0     0     my $self = shift;
474 0           my $class = shift;
475 0 0         my $child = @_ == 1 ? $_[0] : {@_};
476 0           my $children = $self->_children;
477              
478 0 0         if(ref $child eq 'HASH') {
479 0           $child = $class->new(parent => $self, root => $self->root, %$child);
480             }
481              
482 0           push @$children, $child;
483 0           return $child;
484             }
485              
486             sub _find_children {
487 0     0     my($self, $class, $query) = @_;
488 0           my @children;
489              
490 0 0         if(ref $query ne 'HASH') {
491 0           return;
492             }
493              
494             CHILD:
495 0           for my $child (@{ $self->_children }) {
  0            
496 0 0         if($class ne ref $child) {
497 0           next CHILD;
498             }
499 0           for my $key (keys %$query) {
500 0 0         next CHILD unless($child->$key eq $query->{$key});
501             }
502 0           push @children, $child;
503             }
504              
505 0           return @children;
506             }
507              
508             sub _remove_children {
509 0     0     my $self = shift;
510 0           my $class = shift;
511 0 0         my $query = shift or return;
512 0           my $children = $self->_children;
513 0           my $i = 0;
514 0           my @removed;
515              
516             CHILD:
517 0           while($i < @$children) {
518 0 0         if($class ne ref $children->[$i]) {
519 0           next CHILD;
520             }
521 0           for my $key (keys %$query) {
522 0 0         next CHILD unless($children->[$i]->$key eq $query->{$key});
523             }
524 0           push @removed, splice @$children, $i, 1;
525 0           $i--;
526             } continue {
527 0           $i++;
528             }
529              
530 0           return @removed;
531             }
532              
533              
534             =head2 find_all_children
535              
536             Loops through all child nodes with recursion looking for nodes of "class"
537             type. Returns an array of those nodes. You can use the full classname or
538             just the end part. For subclasses like Host::FixedAddress you would need to
539             use the whole name.
540              
541             my @subnet = $config->find_all_children('subnet');
542              
543             =cut
544              
545             sub find_all_children {
546 0     0 1   my $self = shift;
547 0           my $class = shift;
548 0           my @children;
549              
550 0 0         if ($class !~ /::/) {
551             # strip plural if they put it.
552 0           $class =~ s/s\z//;
553 0           $class =~ s/(class|address)e/$1/;
554 0           $class = 'Net::ISC::DHCPd::Config::' . ucfirst(lc($class));
555             }
556              
557 0           for my $child (@{ $self->_children }) {
  0            
558 0 0         if (ref($child) eq $class) {
559 0           push(@children, $child);
560             }
561              
562 0 0         if ($child->_children) {
563 0           push(@children, $child->find_all_children($class));
564             }
565             }
566 0           return @children;
567             }
568              
569             =head2 generate_config_from_children
570              
571             Loops all child nodes in reverse order and calls L</generate> on each
572             of them. Each L</generate> method must return a list of strings which
573             will be indented correctly and concatenated with newline inside this
574             method, before returned as one string.
575              
576             =cut
577              
578             sub generate_config_from_children {
579 0     0 1   return join "\n", shift->_generate_config_from_children;
580             }
581              
582             sub _generate_config_from_children {
583 0     0     my $self = shift;
584 0           my $indent = '';
585 0           my @text;
586              
587 0 0 0       if($self->parent and !$self->can('generate_with_include')) {
588 0           $indent = ' ' x 4;
589             }
590              
591 0           for my $child (@{ $self->_children }) {
  0            
592 0           push @text, map { "$indent# $_" } $child->comments;
  0            
593 0           push @text, map { "$indent$_" } $child->generate;
  0            
594             }
595              
596 0           return @text;
597             }
598              
599             =head2 generate
600              
601             A C<generate()> must be defined in the consuming class. This method
602             must return a list of lines (zero or more), which will be indented
603             and concatenated inside L</generate_config_from_children>.
604              
605             =head1 COPYRIGHT & LICENSE
606              
607             =head1 AUTHOR
608              
609             See L<Net::ISC::DHCPd>.
610              
611             =cut
612              
613             package # hack to register a new warnings category
614             net_isc_dhcpd_config_parse;
615 21     21   140 use warnings::register;
  21         31  
  21         2821  
616              
617             1;