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