1package Netdot::Model::DhcpScope;
2
3use base 'Netdot::Model';
4use warnings;
5use strict;
6
7my $logger = Netdot->log->get_logger('Netdot::Model::DHCP');
8
9=head1 NAME
10
11Netdot::Model::DhcpScope - DHCP scope Class
12
13=head1 CLASS METHODS
14=cut
15
16############################################################################
17
18=head2 search
19
20  Argsuments:
21    Hash with search criteria
22  Returns:
23    Array of DhcpScope objects or iterator (See Class::DBI)
24  Examples:
25    DhcpScope->search(key=>"value");
26=cut
27
28sub search {
29    my ($class, @args) = @_;
30    $class->isa_class_method('search');
31
32    # Class::DBI::search() might include an extra 'options' hash ref
33    # at the end.  In that case, we want to extract the
34    # field/value hash first.
35    my $opts = @args % 2 ? pop @args : {};
36    my %args = @args;
37
38    if ( defined $args{type} ){
39	if ( $args{type} =~ /\w+/ ){
40	    if ( my $type = DhcpScopeType->search(name=>$args{type})->first ){
41		$args{type} = $type->id;
42	    }
43	}
44    }
45    return $class->SUPER::search(%args, $opts);
46}
47
48############################################################################
49
50=head2 insert - Insert new Scope
51
52    Override base method to:
53      - Objectify some arguments
54      - Validate arguments
55      - Assign name based on arguments
56      - Inherit failover properties from global scope if inserting subnet scope
57      - Insert given attributes
58      - Assign default version on global scopes
59      - Assign contained subnets to shared-network
60 Argsuments:
61    Hashref with following keys (in addition to DhcpScope fields):
62      active      - Whether the scope should be exported or not
63      type        - DhcpScopeType name, id or object
64      attributes  - A hash ref with attribute key/value pairs
65      subnets     - Arrayref of Ipblock objects
66  Returns:
67    DhcpScope object
68  Examples:
69    my $host = DhcpScope->insert({type      => 'host',
70                                  ipblock   => $ip,
71                                  physaddr  => $mac,
72				  });
73=cut
74
75sub insert {
76    my ($class, $argv) = @_;
77    $class->isa_class_method('insert');
78    $class->throw_fatal('DhcpScope::insert: Missing required parameters')
79	unless ( defined $argv->{type} );
80
81    # Make it active unless told otherwise
82    $argv->{active} = 1 unless exists $argv->{active};
83
84    my @shared_subnets = @{$argv->{subnets}} if $argv->{subnets};
85
86    $class->_objectify_args($argv);
87    $class->_assign_name($argv) unless $argv->{name};
88    $class->_validate_args($argv);
89
90    my $attributes = delete $argv->{attributes} if exists $argv->{attributes};
91
92    my $scope;
93    if ( $scope = $class->search(name=>$argv->{name})->first ){
94	$class->throw_user("DHCP scope ".$argv->{name}." already exists!");
95    }else{
96	$scope = $class->SUPER::insert($argv);
97    }
98
99    if ( $scope->type->name eq 'subnet' ){
100	if ( $scope->container->version == 4 ){
101	    # Add standard attributes
102	    $attributes->{'option broadcast-address'} = $argv->{ipblock}->netaddr->broadcast->addr();
103	    $attributes->{'option subnet-mask'}       = $argv->{ipblock}->netaddr->mask;
104
105	    if ( $scope->container->enable_failover ){
106		my $failover_peer = $scope->container->failover_peer || 'dhcp-peer';
107		$scope->SUPER::update({enable_failover=>1, failover_peer=> $failover_peer});
108	    }
109	}
110	if ( my $zone = $argv->{ipblock}->forward_zone ){
111	    # Add the domain-name attribute
112	    $attributes->{'option domain-name'} = $zone->name;
113	}
114    }elsif ( $scope->type->name eq 'shared-network' ){
115	# Shared subnets need to point to the new shared-network scope
116	my $failoverstatus = 0;
117	foreach my $s ( @shared_subnets ){
118	    my $subnet_scope = $s->dhcp_scopes->first;
119	    $subnet_scope->update({container=>$scope,
120				   ipblock=>$s});
121	    if ( $subnet_scope->enable_failover == 1 ) {
122		$failoverstatus = 1;
123	    }
124	}
125	if ( $failoverstatus ){
126	    my $failover_peer = $scope->container->failover_peer || 'dhcp-peer';
127	    $scope->SUPER::update({enable_failover => 1,
128				   failover_peer   => $failover_peer});
129	}
130    }
131    $scope->_update_attributes($attributes) if $attributes;
132    return $scope;
133}
134
135
136############################################################################
137
138=head2 get_containers
139
140    This method returns all scopes which can contain other scopes.
141    Scope types that can contain other scopes are:
142      global
143      group
144      pool
145      shared-network
146
147  Arguments:
148    None
149  Returns:
150    Array of DhcpScope objects
151  Example:
152    DhcpScope->get_containers();
153
154=cut
155
156sub get_containers {
157    my ($class) = @_;
158    $class->isa_class_method('get_containers');
159
160    my @res;
161
162    my $q = "SELECT  dhcpscope.id
163             FROM    dhcpscopetype, dhcpscope
164             WHERE   dhcpscopetype.id=dhcpscope.type
165                AND  (dhcpscopetype.name='global' OR
166                     dhcpscopetype.name='group' OR
167                     dhcpscopetype.name='pool' OR
168                     dhcpscopetype.name='shared-network')
169       ";
170
171    my $dbh  = $class->db_Main();
172    my $rows = $dbh->selectall_arrayref($q);
173
174    foreach my $r ( @$rows ){
175	my $id = $r->[0];
176	if ( my $obj = DhcpScope->retrieve($id) ){
177	    push @res, $obj;
178	}
179    }
180    return @res;
181}
182
183=head1 INSTANCE METHODS
184=cut
185
186############################################################################
187
188=head2 update
189
190    Override parent method to:
191    - Objectify some arguments
192    - Validate arguments
193    - Deal with attributes
194
195  Args:
196    Hashref
197  Returns:
198    See Class::DBI
199  Examples:
200    $dhcp_scope->update(\%args);
201=cut
202
203sub update{
204    my ($self, $argv) = @_;
205
206    $self->_objectify_args($argv);
207    $self->_assign_name($argv) unless $argv->{name};
208    $self->_validate_args($argv);
209
210    my $attributes = delete $argv->{attributes} if defined $argv->{attributes};
211
212    if ( $self->type->name eq 'subnet' ){
213	if ( $self->version == 4 ){
214	    # Add standard attributes
215	    $attributes->{'option broadcast-address'} = $argv->{ipblock}->netaddr->broadcast->addr();
216	    $attributes->{'option subnet-mask'}       = $argv->{ipblock}->netaddr->mask;
217	    if ( my $zone = $argv->{ipblock}->forward_zone ){
218		# Add the domain-name attribute
219		$attributes->{'option domain-name'} = $zone->name;
220	    }
221	}
222    }
223
224    my @res = $self->SUPER::update($argv);
225
226    $self->_update_attributes($attributes) if $attributes;
227
228    return @res;
229}
230
231############################################################################
232
233=head2 delete
234
235    Override parent method to:
236
237    - Remove shared-network scope when deleting its last subnet
238
239  Arguments:
240    None
241  Returns:
242    True if successful
243  Examples:
244    $dhcp_scope->delete();
245
246=cut
247
248sub delete{
249    my ($self, $argv) = @_;
250    $self->isa_object_method('delete');
251    my $class = ref($self);
252
253    my $type = $self->type;
254    my $shared_network;
255    if ( $type && $type->name eq 'subnet' ){
256	if ( my $container = $self->container ){
257	    if ( $container->type &&
258		 $container->type->name eq 'shared-network' ){
259		$shared_network = $container;
260	    }
261	}
262    }
263    my @ret = $self->SUPER::delete();
264    if ( $shared_network ){
265	if ( scalar($shared_network->contained_scopes) == 0 ){
266	    $shared_network->delete();
267	}
268    }
269
270    return @ret;
271}
272
273############################################################################
274
275=head2 print_to_file -  Print the config file as text (ISC DHCPD format)
276
277  Args:
278    Hash with following keys:
279    filename - (Optional)
280  Returns:
281    True
282  Examples:
283    $scope->print_to_file();
284
285=cut
286
287sub print_to_file{
288    my ($self, %argv) = @_;
289    $self->isa_object_method('print_to_file');
290    my $class = ref($self);
291    my $filename;
292
293    unless ( $self->active ){
294	$logger->info(sprintf("DhcpScope::print_to_file: Scope %s is marked ".
295			      "as not active. Aborting", $self->get_label));
296	return;
297    }
298
299    my $start = time;
300    my $dir = Netdot->config->get('DHCPD_EXPORT_DIR')
301	|| $self->throw_user('DHCPD_EXPORT_DIR not defined in config file!');
302
303    unless ( $filename = $argv{filename} ){
304	$filename = $self->export_file;
305	unless ( $filename ){
306	    $logger->warn('Export filename not defined for this global scope: '. $self->name.' Using scope name.');
307	    $filename = $self->name;
308	}
309    }
310    my $path = "$dir/$filename";
311    my $fh = Netdot::Exporter->open_and_lock($path);
312
313    my $data = $class->_get_all_data();
314
315    if ( !exists $data->{$self->id} ){
316	$self->throw_fatal("DHCPScope::print_to_file:: Scope id". $self->id. " not found!");
317    }
318
319    print $fh "###############################################################\n";
320    print $fh "# Generated by Netdot (http://netdot.uoregon.edu)\n";
321    print $fh "###############################################################\n\n";
322
323    $class->_print($fh, $self->id, $data);
324
325    print $fh "\n#### EOF ####\n";
326    close($fh);
327
328    my $end = time;
329    $logger->info(sprintf("DHCPD Scope %s exported to %s, in %s",
330			  $self->name, $path, $class->sec2dhms($end-$start) ));
331
332}
333
334
335############################################################################
336
337=head2 import_hosts
338
339  Args:
340    text
341    overwrite
342  Returns:
343    Nothing
344  Examples:
345    $dhcp_scope->import_hosts(text=>$data);
346=cut
347
348sub import_hosts{
349    my ($self, %argv) = @_;
350    $self->isa_object_method('import_hosts');
351
352    $self->throw_fatal("Missing required argument: text")
353	unless $argv{text};
354
355    my @lines = split $/, $argv{text};
356
357    foreach my $line ( @lines ){
358	my ($mac, $ip) = split /\s+/, $line;
359	$mac =~ s/\s+//g;
360	$ip  =~ s/\s+//g;
361	$self->throw_user("Invalid line: $line")
362	    unless ($mac && $ip);
363
364	$self->throw_user("Invalid mac: $mac")
365	    unless ( PhysAddr->validate($mac) );
366
367	$self->throw_user("Invalid IP: $ip")
368	    unless ( Ipblock->matches_ip($ip) );
369
370	if ( $argv{overwrite} ){
371	    if ( my $phys = PhysAddr->search(address=>$mac)->first ){
372		foreach my $scope ( $phys->dhcp_hosts ){
373		    $scope->delete();
374		}
375	    }
376	    if ( my $ipb = Ipblock->search(address=>$ip)->first ){
377		foreach my $scope ( $ipb->dhcp_scopes ){
378		    $scope->delete();
379		}
380	    }
381	}
382    	DhcpScope->insert({
383	    type      => 'host',
384	    ipblock   => $ip,
385	    physaddr  => $mac,
386	    container => $self,
387			  });
388    }
389}
390
391############################################################################
392
393=head2 get_global - Return the global scope where this scope belongs
394
395  Args:
396    None
397  Returns:
398    DhcpScope object
399  Examples:
400    $dhcp_scope->get_global();
401=cut
402
403sub get_global {
404    my ($self, %argv) = @_;
405    $self->isa_object_method('get_global');
406
407    my $container = $self->container;
408
409    # This does not guarantee that the result is of type "global"
410    # but it's fast
411    if ( !defined($container) ){
412	return $self;
413    }elsif ( ref($container) ){
414	# Go recursive
415	return $container->get_global();
416    }elsif ( $container = DhcpScope->retrieve($container) ){
417	# Why is this happening?
418	$logger->debug("DhcpScope::get_global: Scope ".$self->get_label. " had to objectify container: ".$container);
419	return $container->get_global();
420    }else{
421	$self->throw_fatal("DhcpScope::get_global: Scope ".$self->get_label. " has invalid container: ".$container);
422    }
423}
424
425############################################################################
426# Private methods
427############################################################################
428
429############################################################################
430
431=head2 _objectify_args
432
433    Convert following arguments into objects:
434    - type
435    - physaddr
436    - ipblock
437
438  Args:
439    hashref
440  Returns:
441    True
442  Examples:
443    $class->_objectify_args($argv);
444
445=cut
446
447sub _objectify_args {
448    my ($self, $argv) = @_;
449
450    if ( $argv->{type} && !ref($argv->{type}) ){
451	if ( $argv->{type} =~ /\D+/ ){
452	    my $type = DhcpScopeType->search(name=>$argv->{type})->first;
453	    $self->throw_user("DhcpScope::objectify_args: Unknown type: ".$argv->{type})
454		unless $type;
455	    $argv->{type} = $type;
456	}elsif ( my $type = DhcpScopeType->retrieve($argv->{type}) ){
457	    $argv->{type} = $type;
458	}else{
459	    $self->throw_user("Invalid type argument ".$argv->{type});
460	}
461    }
462
463    if ( $argv->{physaddr} && !ref($argv->{physaddr}) ){
464	# Could be an ID or an actual address
465	my $phys;
466	if ( PhysAddr->validate($argv->{physaddr}) ){
467	    # It looks like an address
468	    $phys = PhysAddr->find_or_create({address=>$argv->{physaddr}});
469	}elsif ( $argv->{physaddr} !~ /\D/ ){
470	    # Does not contain non-digits, so it must be an ID
471	    $phys = PhysAddr->retrieve($argv->{physaddr});
472	}
473	if ( $phys ){
474	    $argv->{physaddr} = $phys;
475	}else{
476	    $self->throw_user("Could not find or create physaddr");
477	}
478    }
479
480    if ( $argv->{container} && !ref($argv->{container}) ){
481	my $container;
482	if ( $argv->{container} =~ /\D+/ ){
483	    if ( $container = DhcpScope->search(name=>$argv->{container})->first ){
484		$argv->{container} = $container;
485	    }
486	}elsif ( $container = DhcpScope->retrieve($argv->{container}) ){
487	    $argv->{container} = $container;
488	}else{
489	    $self->throw_user("Invalid container argument ".$argv->{container});
490	}
491    }
492
493    if ( $argv->{ipblock} && !ref($argv->{ipblock}) ){
494	if ( $argv->{ipblock} =~ /\D+/ ){
495	    my $ipblock;
496	    unless ( $ipblock = Ipblock->search(address=>$argv->{ipblock})->first ){
497		$ipblock = Ipblock->insert({address=>$argv->{ipblock}});
498		if ( $ipblock->is_address ){
499		    $ipblock->update({status=>'Static'});
500		}else{
501		    $ipblock->update({status=>'Subnet'});
502		}
503	    }
504	    $argv->{ipblock} = $ipblock;
505	}elsif ( my $ipb = Ipblock->retrieve($argv->{ipblock}) ){
506		$argv->{ipblock} = $ipb;
507	}else{
508	    $self->throw_user("Invalid ipblock argument ".$argv->{ipblock});
509	}
510    }
511    1;
512}
513
514############################################################################
515
516=head2 _validate_args
517
518  Args:
519    hashref
520  Returns:
521    True, or throws exception if validation fails
522  Examples:
523    $class->_validate_args($argv);
524
525=cut
526
527sub _validate_args {
528    my ($self, $argv) = @_;
529
530    my %fields;
531    foreach my $field ( qw(name type version physaddr duid ipblock container) ){
532	if ( ref($self) ){
533	    $fields{$field} = $self->$field if $self->$field;
534	}
535	# Overrides current value with given argument
536	$fields{$field} = $argv->{$field} if exists $argv->{$field};
537    }
538
539    my $name = $fields{name} || $self->throw_user("A scope name is required");
540
541    $self->throw_user("$name: A scope type is required") unless $fields{type};
542    my $type = $fields{type}->name;
543
544    $self->throw_user("$name: Version field only applies to global scopes")
545	if ( $fields{version} && $type ne 'global' );
546
547    if ( $fields{physaddr} && $fields{duid} ){
548	    $self->throw_user("$name: Cannot use both physaddr and duid");
549    }
550    if ( $fields{physaddr} && $type ne 'host' ){
551	$self->throw_user("$name: Cannot assign physical address ($fields{physaddr}) to a non-host scope");
552    }
553    if ( my $duid = $fields{duid} ){
554	if ( $type ne 'host' ){
555	    $self->throw_user("$name: Cannot assign DUID ($fields{duid}) to a non-host scope");
556	}
557	if ( $duid =~ /[^A-Fa-f0-9:]/ ){
558	    $self->throw_user("$name: DUID should only contain hexadecimal digits and colons: $duid");
559	}
560	my $hexonly = $duid;
561	$hexonly =~ s/://g; # Remove colons
562	if ( length($hexonly) < 1 ){
563	    $self->throw_user("$name: Invalid DUID (too short): '$duid'\n");
564	}
565	if ( length($hexonly) > 255 ){
566	    $self->throw_user("$name: Invalid DUID (too long): '$duid'\n");
567	}
568    }
569    if ( $fields{ipblock} ){
570	my $ip_status = $fields{ipblock}->status->name;
571	if ( ($ip_status eq 'Subnet') && $type ne 'subnet' ){
572	    $self->throw_user("$name: Cannot assign a subnet to a non-subnet scope");
573	}elsif ( ($ip_status eq 'Static') && $type ne 'host'  ){
574	    $self->throw_user("$name: Cannot assign an IP address to a non-host scope");
575	}
576	if ( $type eq 'host' && $ip_status ne 'Static' ){
577	    $self->throw_user("$name: IP in host declaration can only be Static");
578	}
579    }
580    if ( $type eq 'host' ){
581	if ( $fields{ipblock} ){
582	    if ( $fields{ipblock}->version == 4 && !$fields{physaddr} ){
583		$self->throw_user("$name: an IPv4 host scope requires an ethernet address");
584	    }
585	    if ( $fields{ipblock}->version == 6 && !$fields{duid} && !$fields{physaddr} ){
586		$self->throw_user("$name: an IPv6 host scope requires a DUID or ethernet address");
587	    }
588	    # Is Subnet scope defined?
589	    my $subnet = $fields{ipblock}->parent ||
590		$self->throw_user("$name: $fields{ipblock} not within subnet");
591	    my $subnet_scope;
592	    unless ( $subnet_scope = ($subnet->dhcp_scopes)[0] ){
593		$self->throw_user("$name: Subnet ".$subnet->get_label." not dhcp-enabled.");
594	    }
595	    # Make sure we assign to the correct global container if none passed
596	    $argv->{container} = $subnet_scope->get_global
597		unless defined $argv->{container};
598	    $fields{container} = $argv->{container};
599
600	    # Check for mismatched versions
601	    if ( $fields{container}->type eq 'global' &&
602		 $fields{ipblock}->version != $fields{container}->version ){
603		$self->throw_user("$name: IP version in host scope does not match version in global scope");
604	    }
605	}
606
607	if ( $fields{physaddr} ){
608	    if ( my @scopes = DhcpScope->search(physaddr=>$fields{physaddr}) ){
609		if ( my $subnet = $fields{ipblock}->parent ){
610		    foreach my $s ( @scopes ){
611			next if ( ref($self) && $s->id == $self->id );
612			if ( $s->ipblock && (my $osubnet = $s->ipblock->parent) ){
613			    if ( $osubnet->id == $subnet->id ){
614				$self->throw_user("$name: Duplicate MAC address in this subnet: ".
615						  $fields{physaddr}->address);
616			    }
617			}
618		    }
619		}
620	    }
621	}
622	if ( $fields{duid} ){
623	    if ( my @scopes = DhcpScope->search(duid=>$fields{duid}) ){
624		if ( my $subnet = $fields{ipblock}->parent ){
625		    foreach my $s ( @scopes ){
626			next if ( ref($self) && $s->id == $self->id );
627			if ( $s->ipblock && (my $osubnet = $s->ipblock->parent) ){
628			    if ( $osubnet->id == $subnet->id ){
629				$self->throw_user("$name: Duplicate DUID in this subnet: ".
630						  $fields{duid});
631			    }
632			}
633		    }
634		}
635	    }
636	}
637
638    }elsif ( $type eq 'subnet' ){
639
640	$self->throw_user("$name: Subnet IP block not defined")
641	    unless $fields{ipblock};
642
643	$self->throw_user("$name: Subnet scopes require a container")
644	    unless $fields{container};
645
646	if ( $fields{container}->type->name eq 'global' &&
647	     $fields{ipblock}->version != $fields{container}->version ){
648	    $self->throw_user("$name: IP version in subnet scope does not match IP version in container");
649	}
650    }elsif ( $type eq 'global' ){
651	$argv->{version} = $fields{version} || 4;
652	if ( $argv->{version} != 4 && $argv->{version} != 6 ){
653	    $self->throw_user("$name: Invalid IP version: $fields{version}");
654	}
655    }
656    if ( $fields{container} ){
657	my $ctype = $fields{container}->type->name;
658	$self->throw_user("$name: container scope type not defined")
659	    unless defined $ctype;
660
661	if ( $type eq 'global' ){
662	    $self->throw_user("$name: a global scope cannot exist within another scope");
663	}
664	if ( $type eq 'host' && !($ctype eq 'global' || $ctype eq 'group') ){
665	    $self->throw_user("$name: a host scope can only exist within a global or group scope");
666	}
667	if ( $type eq 'group' && $ctype ne 'global' ){
668	    $self->throw_user("$name: a group scope can only exist within a global scope");
669	}
670	if ( $type eq 'subnet' && !($ctype eq 'global' || $ctype eq 'shared-network') ){
671	    $self->throw_user("$name: a subnet scope can only exist within a global or shared-network scope");
672	}
673	if ( $type eq 'shared-network' && $ctype ne 'global' ){
674	    $self->throw_user("$name: a shared-network scope can only exist within a global scope");
675	}
676	if ( $type eq 'pool' && !($ctype eq 'subnet' || $ctype eq 'shared-network') ){
677	    $self->throw_user("$name: a pool scope can only exist within a subnet or shared-network scope");
678	}
679	if ( ($type eq 'class' || $type eq 'subclass') && $ctype ne 'global' ){
680	    $self->throw_user("$name: a class or subclass scope can only exist within a global scope");
681	}
682    }elsif ( $type ne 'global' && $type ne 'template' ){
683	$self->throw_user("$name: A container scope is required except for global and template scopes");
684    }
685
686    1;
687}
688
689############################################################################
690# _print - Generate text file with scope definitions
691#
692# Arguments:
693#   fh     - File handle
694#   id     - Scope id
695#   data   - Data hash from get_all_data method
696#   indent - Indent space
697#
698sub _print {
699    my ($class, $fh, $id, $data, $indent) = @_;
700
701    $indent ||= "";
702    my $pindent = $indent;
703
704    if ( !defined $fh ){
705	$class->throw_fatal("Missing file handle");
706    }
707
708    if ( !defined $id ){
709	$class->throw_fatal("Scope id missing");
710    }
711
712    if ( !defined $data || ref($data) ne 'HASH' ){
713	$class->throw_fatal("Data missing or invalid");
714    }
715
716    unless ( $data->{$id}->{active} ){
717	$logger->debug(sprintf("DhcpScope::print_to_file: Scope %d is marked ".
718                               "as not active. Aborting", $id));
719	return;
720    }
721
722    my $type;
723    unless ( $type = $data->{$id}->{type} ){
724	$class->throw_fatal("Scope id $id missing type");
725    }
726
727    if ( $type ne 'global' && $type ne 'template' ){
728	my $st   = $data->{$id}->{statement};
729	my $name = $data->{$id}->{name};
730	print $fh $indent."$st $name {\n";
731	$indent .= " " x 4;
732    }
733
734    # Print free-form text
735    if ( $data->{$id}->{text} ){
736 	chomp (my $text = $data->{$id}->{text});
737 	$text =~ s/\n/\n$indent/g  ;
738 	print $fh $indent.$text, "\n" ;
739    }
740
741    # Print attributes
742    foreach my $attr_id ( sort { $data->{$id}->{attrs}->{$a}->{name} cmp
743				     $data->{$id}->{attrs}->{$b}->{name} }
744			  keys %{$data->{$id}->{attrs}} ){
745
746	my $name   = $data->{$id}->{attrs}->{$attr_id}->{name};
747	my $code   = $data->{$id}->{attrs}->{$attr_id}->{code};
748	my $format = $data->{$id}->{attrs}->{$attr_id}->{format};
749	my $value  = $data->{$id}->{attrs}->{$attr_id}->{value};
750	print $fh $indent.$name;
751	if ( defined $value ) {
752	    if ( defined $format && ($format eq 'text' || $format eq 'string') ){
753		# DHCPD requires double quotes
754		if ( $value !~ /^"(.*)"$/ ){
755		    $value = "\"$value\"";
756		}
757	    }
758	    print $fh " $value";
759	}
760	elsif ( $type eq 'global' &&
761		defined $code && defined $format ){
762	    # Assume that user is trying to define a new option
763	    print $fh " code $code = $format";
764	}
765	print $fh ";\n";
766    }
767    # Print "inherited" attributes from used templates
768    if ( defined $data->{$id}->{templates} ){
769	foreach my $template_id ( @{$data->{$id}->{templates}} ){
770	    $class->_print($fh, $template_id, $data, $indent);
771	}
772    }
773
774    # Create pools for subnets with dynamic addresses
775    if ( $type eq 'subnet' ){
776	my $s   = DhcpScope->retrieve($id);
777
778	my $failover_enabled = ($s->enable_failover &&
779				$s->container->enable_failover)? 1 : 0;
780	my $failover_peer = $s->failover_peer ||
781	    $s->container->failover_peer;
782
783	my $ipb = $s->ipblock;
784	my @ranges = $ipb->get_dynamic_ranges();
785
786	if ( @ranges ){
787	    if ( $failover_enabled && $failover_peer ne ""){
788		print $fh $indent."pool {\n";
789		my $nindent = $indent . " " x 4;
790		# This is a requirement of ISC DHCPD:
791		print $fh $nindent."deny dynamic bootp clients;\n";
792		print $fh $nindent."failover peer \"$failover_peer\";\n";
793		foreach my $range ( @ranges ){
794		    print $fh $nindent."range $range;\n";
795		}
796		print $fh $indent."}\n";
797	    }else{
798		foreach my $range ( @ranges ){
799		    my $st = ( $ipb->version == 6 )? 'range6' : 'range';
800		    print $fh $indent."$st $range;\n";
801		}
802	    }
803	}
804    }
805
806    # Recurse for each child scope
807    if ( defined $data->{$id}->{children} ){
808	foreach my $child_id ( sort { $data->{$a}->{type} cmp $data->{$b}->{type}
809				      ||
810				      $data->{$a}->{name} cmp $data->{$b}->{name} }
811			       @{$data->{$id}->{children}} ){
812	    next if $data->{$child_id}->{type} eq 'template';
813	    $class->_print($fh, $child_id, $data, $indent);
814	}
815    }
816
817    # Close scope definition
818    if ( $type ne 'global' && $type ne 'template' ){
819	$indent = $pindent;
820	print $fh $indent."}\n";
821    }
822
823}
824
825############################################################################
826# _get_all_data - Build a hash with all necessary information to build DHCPD config
827#
828# Arguments:
829#   None
830# Returns:
831#   Hash ref
832# Example:
833#   DhcpScope->_get_all_data();
834#
835sub _get_all_data {
836    my ($class) = @_;
837
838    my %data;
839
840    $logger->debug("DhcpScope::_get_all_data: Querying database");
841
842    my $q = "SELECT          dhcpscope.id, dhcpscope.name, dhcpscope.active, dhcpscope.text,
843                             dhcpscopetype.name, dhcpscope.container,
844                             dhcpattr.id, dhcpattrname.name, dhcpattr.value, dhcpattrname.code, dhcpattrname.format,
845                             physaddr.address, ipblock.address, ipblock.version, dhcpscope.duid, dhcpscope.version
846             FROM            dhcpscopetype, dhcpscope
847             LEFT OUTER JOIN physaddr ON dhcpscope.physaddr=physaddr.id
848             LEFT OUTER JOIN ipblock  ON dhcpscope.ipblock=ipblock.id
849             LEFT OUTER JOIN (dhcpattr CROSS JOIN dhcpattrname) ON
850                             dhcpattr.scope=dhcpscope.id AND dhcpattr.name=dhcpattrname.id
851             WHERE           dhcpscopetype.id=dhcpscope.type
852       ";
853
854    my $dbh  = $class->db_Main();
855    my $rows = $dbh->selectall_arrayref($q);
856
857    $logger->debug("DhcpScope::_get_all_data: Building data structure");
858
859    foreach my $r ( @$rows ){
860	my ($scope_id, $scope_name, $scope_active, $scope_text,
861	    $scope_type, $scope_container,
862	    $attr_id, $attr_name, $attr_value, $attr_code, $attr_format,
863	    $mac, $ip, $ipversion, $scope_duid, $scope_version) = @$r;
864	$data{$scope_id}{name}      = $scope_name;
865	$data{$scope_id}{type}      = $scope_type;
866	$data{$scope_id}{active}    = $scope_active;
867	$data{$scope_id}{container} = $scope_container;
868	$data{$scope_id}{text}      = $scope_text;
869	$data{$scope_id}{duid}      = $scope_duid;
870	$data{$scope_id}{version}   = $scope_version;
871	if ( $scope_type eq 'subnet' && $ipversion == 6 ){
872	    $data{$scope_id}{statement} = 'subnet6';
873	}else{
874	    $data{$scope_id}{statement} = $scope_type;
875	}
876	if ( $attr_id ){
877	    $data{$scope_id}{attrs}{$attr_id}{name}   = $attr_name;
878	    $data{$scope_id}{attrs}{$attr_id}{code}   = $attr_code   if $attr_code;
879	    $data{$scope_id}{attrs}{$attr_id}{format} = $attr_format if $attr_format;
880	    $data{$scope_id}{attrs}{$attr_id}{value}  = $attr_value  if $attr_value;
881	}
882	if ( $scope_type eq 'host' ){
883	    if ( $scope_duid ){
884		$data{$scope_id}{attrs}{'client-id'}{name}  = 'host-identifier option dhcp6.client-id';
885		$data{$scope_id}{attrs}{'client-id'}{value} = $scope_duid;
886	    }elsif ( $mac ){
887		$data{$scope_id}{attrs}{'hardware ethernet'}{name}  = 'hardware ethernet';
888		$data{$scope_id}{attrs}{'hardware ethernet'}{value} = PhysAddr->colon_address($mac);
889	    }else{
890		# Without DUID or MAC, this would be invalid
891		next;
892	    }
893	    if ( $ip ){
894		if ( $ipversion == 6 ){
895		    my $addr = Ipblock->int2ip($ip, $ipversion);
896		    $data{$scope_id}{attrs}{'fixed-address6'}{name}  = 'fixed-address6';
897		    $data{$scope_id}{attrs}{'fixed-address6'}{value} = $addr;
898		    my $addr_full = NetAddr::IP->new6($addr)->full();
899		    $addr_full =~ s/\:+/-/g;
900		    $data{$scope_id}{name} = $addr_full;
901		}else{
902		    $data{$scope_id}{attrs}{'fixed-address'}{name}  = 'fixed-address';
903		    $data{$scope_id}{attrs}{'fixed-address'}{value} = Ipblock->int2ip($ip, $ipversion);
904		}
905	    }
906	}
907    }
908
909    # Make children lists
910    foreach my $id ( keys %data ){
911	if ( my $parent = $data{$id}{container} ){
912	    push @{$data{$parent}{children}}, $id if defined $data{$parent} ;
913	}
914    }
915
916    # add  templates
917    my $q2 = "SELECT scope, template FROM dhcpscopeuse";
918    my $rows2  = $dbh->selectall_arrayref($q2);
919
920    foreach my $r2 ( @$rows2 ){
921	my ($id, $template) = @$r2;
922	push @{$data{$id}{templates}}, $template if defined $data{$template};
923    }
924
925    return \%data;
926}
927
928############################################################################
929# Assign scope name based on type and other values
930sub _assign_name {
931    my ($self, $argv) = @_;
932
933    # Get these values from object if not passed
934    if ( ref($self) ){
935	foreach my $key (qw(type ipblock physaddr duid)){
936	    $argv->{$key} = $self->$key unless exists $argv->{$key}
937	}
938    }
939
940    unless ( $argv->{type} ){
941	$self->throw_fatal("DhcpScope::_assign_name: Missing required argument: type")
942    }
943
944    my $name;
945    if ( $argv->{type}->name eq 'host' ){
946	# Try to find a unique name for this scope
947	if ( $argv->{ipblock} ){
948	    $name = $argv->{ipblock}->full_address;
949	}elsif ( $argv->{physaddr} ){
950	    $name = $argv->{physaddr}->address;
951	}elsif ( $argv->{duid} ){
952	    $name = $argv->{duid};
953	}
954	$name =~ s/:/-/g;
955
956    }elsif ( $argv->{type}->name eq 'subnet' ){
957	$self->throw_fatal("DhcpScope::_assign_name: Missing ipblock object")
958	    unless $argv->{ipblock};
959	if ( $argv->{ipblock}->version == 6 ){
960	    $name = $argv->{ipblock}->cidr;
961	}else{
962	    $name = $argv->{ipblock}->address." netmask ".$argv->{ipblock}->netaddr->mask;
963	}
964
965    }elsif ( $argv->{type}->name eq 'shared-network' ){
966	$self->throw_fatal("DhcpScope::_assign_name: Missing subnet list")
967	    unless $argv->{subnets};
968	my $subnets = delete $argv->{subnets};
969	$name = join('_', (map { $_->address } sort { $a->address_numeric <=> $b->address_numeric } @$subnets));
970
971    }else{
972	$self->throw_fatal("DhcpScope::_assign_name: Don't know how to assign name for type: ".
973			   $argv->{type}->name);
974    }
975    $argv->{name} = $name;
976}
977
978############################################################################
979# Insert or update attributes
980sub _update_attributes {
981    my ($self, $attributes) = @_;
982    while ( my($key, $val) = each %$attributes ){
983	my $attr;
984	my %args = (name=>$key, scope=>$self->id);
985	my $str = $key;
986	$str .= ": $val" if $val;
987	if ( $attr = DhcpAttr->search(%args)->first ){
988	    $logger->debug("DhcpScope::_update_attributes: ".$self->get_label.": Updating DhcpAttr $str");
989	    $args{value} = $val;
990	    $attr->update(\%args);
991	}else{
992	    $logger->debug("DhcpScope::_update_attributes: ".$self->get_label.": Inserting DhcpAttr $str");
993	    $args{value} = $val;
994	    DhcpAttr->insert(\%args);
995	}
996    }
997    1;
998}
999
1000=head1 AUTHOR
1001
1002Carlos Vicente, C<< <cvicente at ns.uoregon.edu> >>
1003
1004=head1 COPYRIGHT & LICENSE
1005
1006Copyright 2012 University of Oregon, all rights reserved.
1007
1008This program is free software; you can redistribute it and/or modify
1009it under the terms of the GNU General Public License as published by
1010the Free Software Foundation; either version 2 of the License, or
1011(at your option) any later version.
1012
1013This program is distributed in the hope that it will be useful, but
1014WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
1015or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
1016License for more details.
1017
1018You should have received a copy of the GNU General Public License
1019along with this program; if not, write to the Free Software Foundation,
1020Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
1021
1022=cut
1023
1024#Be sure to return 1
10251;
1026
1027