#!/usr/bin/perl -w use strict; # ============================================================================== # update_infoblox.pl # ============================================================================== # A script that updates the Infoblox DNS appliance to match our NIS hosts # ----------------------------------------------------------------------------- # Author: Jeremy Holland # ----------------------------------------------------------------------------- # Usage: update_infoblox.pl [--dry-run] # ----------------------------------------------------------------------------- # Rev History # 2011-04-13 - J. Holland - Initial release # 2011-04-14 - J. Holland - Added dry-run mode # # Known Bugs # - Does not detect a reverse zone known only to DNS; only uses zones known # to NIS. Therefore, if you remove the last member of a reverse zone from # NIS, it will not be removed from the Infoblox. However, the solution to # this (generate a union of all known reverse zones from NIS and DNS) is # deemed to complicated to be worth implementing, as this scenario has not # actually ever occurred to anyone's knowledge. # ----------------------------------------------------------------------------- use Net::Domain qw(hostdomain); use List::Util qw(sum); $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0; # for Infoblox module use lib "/export/infoblox/lib"; use Infoblox; # NOTE: For this demonstration, for clarity and brevity, extensive environment # and command-line checks have been elided. Also, the following Infoblox # login information is normally stored in a configuration file but has # been moved into this script. my $infoblox_host = "10.0.0.1"; my $infoblox_user = "infoblox_user"; my $infoblox_pass = "abc123"; # Ignore the following hostnames if they are returned by ypcat my %skip_host = map { $_ => 1 } qw(localhost loghost localhost.localdomain); # Generate the comment string that will be included in all DNS records my @wday_str = qw(Sun Mon Tue Wed Thu Fri Sat); my @month_str = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my ($sec,$min,$hour,$day,$mon,$year,$wday) = localtime; my $timestamp = sprintf("%3.3s %2.2d-%3.3s-%4.4d %2.2d:%2.2d", $wday_str[$wday], $day, $month_str[$mon], 1900+$year, $hour, $min, $sec); my $record_comment = "Set by update_infoblox.pl $timestamp"; # Determine your NIS domain (fully-qualified and simplified) my $fulldomain = hostdomain(); my $domain = chopdots($fulldomain); # Process command-line (the non-demonstration version of this script uses # Getopt::Long to check for several additional operation flags, including # verbosity, debug mode, test data, etc. These have been elided from the # demonstration version for brevity.) my $dry_run = 0; if (defined($ARGV[0]) and $ARGV[0] eq "--dry-run") { shift @ARGV; $dry_run = 1; } if (@ARGV) { die "Usage: update_infoblox.pl [--dry-run]\n"; } if ($dry_run) { print "\n"; print "***** NOTICE **************************************\n"; print "Executing in dry-run mode. No changes will be made.\n"; print "***************************************************\n"; print "\n"; } # Create Infoblox object and attach to Infoblox print "Opening connection to Infoblox...\n"; my $INFOBLOX = Infoblox::Session->new( master => $infoblox_host, username => $infoblox_user, password => $infoblox_pass, ); if (not $INFOBLOX) { die "Could not create Infoblox::Session object\n"; } # Retrieve HOST records on the Infoblox. print " Downloading HOST Records...\n"; my %infoblox_host_records = ( ); my @infoblox_HOST_records = $INFOBLOX->search( object => "Infoblox::DNS::Host", name => ".*\Q.$fulldomain\E", ); if (@infoblox_HOST_records) { foreach my $infoblox_HOST_record (@infoblox_HOST_records) { # $infoblox_HOST_record = { # name => "servername.subdomain.mycompany.com", # zone => "subdomain.mycompany.com", # ipv4addrs => [ "10.66.41.88", ... ], # aliases => [ "serveralias.subdomain.mycompany.com", ... ], # comment => "Set by update_infoblox Mon 01-Jan-2012 12:00" # } my $fqdn = $infoblox_HOST_record->{name}; my $name = chopdots($fqdn); $infoblox_host_records{$name} = 1; } } else { if ($INFOBLOX->status_code() == 1003) { # no records found print " No HOST records were retrieved from Infoblox\n"; } else { infoblox_error($INFOBLOX, "Could not get HOST records from Infoblox"); } } # Generate the DNS records we will need from the NIS hosts map. Call ypcat # to get the information as gethostent() returns corrupted data on our # production server. my $A_records_NIS = { }; my $CNAME_records_NIS = { }; my $PTR_records_NIS = { }; print "Building records from NIS...\n"; open YPCAT_CMD, "-|", "/usr/bin/ypcat", "hosts" or die "Could not execute ypcat command: $!\n"; my @host_lines = <YPCAT_CMD>; close YPCAT_CMD; my $retval = $? >> 8; if ($retval != 0) { die "ypcat returned nonzero exit code ($retval)\n"; } my %seen; foreach my $host_line (@host_lines) { # skip dups (ypcat returns dups for hosts with multiple aliases) next if $seen{$host_line}++; my ($ip_addr, $hostname, $aliases, @aliases, $rev_zone, $ptr_key); if ($host_line =~ m/^(\d+\.\d+\.\d+\.\d+)\s+(\S+)\s*(.*)$/) { ($ip_addr, $hostname, $aliases) = ($1, $2, $3); $hostname = lc $hostname; if ($aliases) { @aliases = split /\s+/, lc $aliases; } my @octets = (split /\./, $ip_addr); $rev_zone = join ".", @octets[2, 1, 0]; $ptr_key = $octets[3]; } else { chomp $host_line; die "invalid hosts entry \"$host_line\"\n"; } # skip records that conflist with a HOST record, are on the manual # "skip list", or that contain illegal characters next if ($infoblox_host_records{$hostname}); next if ($skip_host{$hostname}); next if ($hostname =~ m/[^a-z0-9_-]/); # The Infoblox doesn't like A names with underbars, but CNAMEs with underbars # are OK. Therefore systems with underbars get a modified A with no underbars # and a CNAME record with true name aliased to modified name if ($hostname =~ m/_/) { push @aliases, $hostname; $hostname =~ s/_//g; } # record A record, PTR record, and CNAME records (if any) $A_records_NIS->{$hostname} = $ip_addr; $PTR_records_NIS->{$rev_zone}{$ptr_key} = $hostname; foreach my $alias (@aliases) { # skip aliases that are the same as the hostname, that conflict with # HOST records, or that contain illegal characters next if ($alias eq $hostname); next if ($infoblox_host_records{$alias}); next if ($alias =~ m/[^a-z0-9_-]/); $CNAME_records_NIS->{$alias} = $hostname; } } # foreach host line print " Generated " . (keys %{$A_records_NIS}) . " A records.\n"; print " " . (keys %{$CNAME_records_NIS}) . " CNAME records.\n"; print " " . (sum map { scalar keys %{$_} } values %{$PTR_records_NIS}) . " PTR records " . "in " . (scalar keys %{$PTR_records_NIS}) . " zones.\n"; # ----------------------------------------------------------------------------- # For each record type (A, CNAME, and each PTR zone) check Infoblox records # against NIS records; delete Infoblox records that don't exist in NIS or don't # match and forget NIS records that match Infoblox (i.e. don't need to be # recreated); leftover NIS records are new or changed and need to be added # to Infoblox. # ----------------------------------------------------------------------------- # ===== PROCESS A RECORDS ===================================================== print "Processing A Records...\n"; my $any_A_record_changes; my @infoblox_A_records = $INFOBLOX->search( object => "Infoblox::DNS::Record::A", name => ".*\Q.$fulldomain\E", ); if (@infoblox_A_records) { print " Analyzing Infoblox A records\n"; foreach my $infoblox_A_record (@infoblox_A_records) { # $infoblox_A_record = { # zone => "subdomain.mycompany.com", # name => "servername.subdomain.mycompany.com", # ipv4addr => "10.66.41.88", # comment => "Set by update_infoblox Fri 27-Jan-2012 14:50", # ... # } my $infoblox_name_fqdn = lc $infoblox_A_record->{name}; my $infoblox_name = chopdots($infoblox_name_fqdn); # "servername.subdomain.mycompany.com" --> "servername" my $infoblox_ip = $infoblox_A_record->{ipv4addr}; if (defined_and_matches($A_records_NIS->{$infoblox_name}, $infoblox_ip)) { delete $A_records_NIS->{$infoblox_name}; } else { $any_A_record_changes = 1; printf " - deleting A from $fulldomain: %16s => $infoblox_ip\n", $infoblox_name; if (not $dry_run) { $INFOBLOX->remove($infoblox_A_record); } } } } else { if ($INFOBLOX->status_code() == 1003) { # no records found print " No A records were retrieved from Infoblox\n"; } else { infoblox_error($INFOBLOX, "Could not get A records from Infoblox"); } } print " Analyzing NIS A records\n"; foreach my $NIS_host (keys %{$A_records_NIS}) { $any_A_record_changes = 1; my $NIS_ip = $A_records_NIS->{$NIS_host}; my $NIS_host_fqdn = "$NIS_host.$fulldomain"; printf " - adding A to $fulldomain: %16s => $NIS_ip\n", $NIS_host; my $A_record = Infoblox::DNS::Record::A->new( name => $NIS_host_fqdn, ipv4addr => $NIS_ip, comment => $record_comment, ); if (not $dry_run) { $INFOBLOX->add($A_record); } } if (not $any_A_record_changes) { print "No A records were changed.\n"; } # ===== PROCESS CNAME RECORDS ================================================= print "Processing CNAME Records...\n"; my $any_CNAME_record_changes; my @infoblox_CNAME_records = $INFOBLOX->search( object => "Infoblox::DNS::Record::CNAME", name => ".*\Q.$fulldomain\E", ); if (@infoblox_CNAME_records) { print " Analyzing Infoblox CNAME records\n"; foreach my $infoblox_CNAME_record (@infoblox_CNAME_records) { # $infoblox_CNAME_record = { # zone => "subdomain.mycompany.com", # name => "serveralias.subdomain.mycompany.com", # canonical => "servername.testwilm.mycompany.com", # comment => "Set by update_infoblox Mon 30-Jan-2012 09:45" # } my $infoblox_name_fqdn = lc $infoblox_CNAME_record->{name}; my $infoblox_name = chopdots($infoblox_name_fqdn); # "serveralias.subdomain.mycompany.com" --> "serveralias" my $infoblox_canonical_fqdn = lc $infoblox_CNAME_record->{canonical}; my $infoblox_canonical = chopdots($infoblox_canonical_fqdn); # "servername.subdomain.mycompany.com" --> "servername" if (defined_and_matches($CNAME_records_NIS->{$infoblox_name}, $infoblox_canonical)) { delete $CNAME_records_NIS->{$infoblox_name}; } else { $any_CNAME_record_changes = 1; printf " - deleting CNAME from $fulldomain: %16s => $infoblox_canonical\n", $infoblox_name; if (not $dry_run) { $INFOBLOX->remove($infoblox_CNAME_record); } } } } else { if ($INFOBLOX->status_code() == 1003) { # no records found print " No CNAME records were retrieved from Infoblox\n"; } else { infoblox_error($INFOBLOX, "Could not get CNAME records from Infoblox"); } } print " Analyzing NIS CNAME records\n"; foreach my $NIS_alias (keys %{$CNAME_records_NIS}) { $any_CNAME_record_changes = 1; my $NIS_alias_fqdn = "$NIS_alias.$fulldomain"; my $NIS_canonical = $CNAME_records_NIS->{$NIS_alias}; my $NIS_canonical_fqdn = "$NIS_canonical.$fulldomain"; printf " - adding CNAME to $fulldomain: %16s => $NIS_canonical_fqdn\n", $NIS_alias; my $CNAME_record = Infoblox::DNS::Record::CNAME->new( name => $NIS_alias_fqdn, canonical => $NIS_canonical_fqdn, comment => $record_comment, ); if (not $dry_run) { $INFOBLOX->add($CNAME_record); } } if (not $any_CNAME_record_changes) { print "No CNAME records were changed.\n"; } # ===== PROCESS PTR RECORDS =================================================== print "Processing PTR Records...\n"; my $any_PTR_record_changes; foreach my $revzone (keys %{$PTR_records_NIS}) { my $subnet = join ".", reverse split /\./, $revzone; my $NIS_REVZONE_RECORDS = $PTR_records_NIS->{$revzone}; my @infoblox_PTR_records = $INFOBLOX->search( object => "Infoblox::DNS::Record::PTR", zone => "$revzone.in-addr.arpa", ); if (@infoblox_PTR_records) { print " Analyzing Infoblox PTR records for $revzone\n"; foreach my $infoblox_PTR_record (@infoblox_PTR_records) { # $infoblox_PTR_record = { # zone => "41.66.10.in-addr.arpa", # name => "88.41.66.10.in-addr.arpa", # ipv4addr => "10.66.41.88", # ptrdname => "servername.subdomain.mycompany.com", # comment => "Set by update_infoblox Fri 27-Jan-2012 14:50", # } my $infoblox_name_fqdn = $infoblox_PTR_record->{name}; my $infoblox_name = chopdots($infoblox_name_fqdn); # "88.41.66.10.in-addr.arpa" --> "88" my $infoblox_ptrdname_fqdn = lc $infoblox_PTR_record->{ptrdname}; my $infoblox_ptrdname = chopdots($infoblox_ptrdname_fqdn); # "servername.subdomain.mycompany.com" --> "servername" if (defined_and_matches($NIS_REVZONE_RECORDS->{$infoblox_name}, $infoblox_ptrdname)) { delete $NIS_REVZONE_RECORDS->{$infoblox_name}; } else { $any_PTR_record_changes = 1; printf " - deleting PTR from $revzone: %4s => $infoblox_ptrdname\n", $infoblox_name; if (not $dry_run) { $INFOBLOX->remove($infoblox_PTR_record); } } } } else { if ($INFOBLOX->status_code() == 1003) { # no records found print " No PTR records were retrieved from Infoblox for $revzone\n"; } else { infoblox_error($INFOBLOX, "Could not get PTR records from Infoblox for $revzone"); } } print " Analyzing NIS PTR records for $revzone\n"; foreach my $NIS_ipnum (keys %{$NIS_REVZONE_RECORDS}) { $any_PTR_record_changes = 1; my $NIS_ip_addr = "$subnet.$NIS_ipnum"; my $NIS_host = $NIS_REVZONE_RECORDS->{$NIS_ipnum}; my $NIS_host_fqdn = "$NIS_host.$fulldomain"; printf " - adding PTR to $revzone: %4s => $NIS_host_fqdn\n", $NIS_ipnum; my $PTR_record = Infoblox::DNS::Record::PTR->new( ptrdname => $NIS_host_fqdn, ipv4addr => $NIS_ip_addr, comment => $record_comment, ); if (not $dry_run) { $INFOBLOX->add($PTR_record); } } } if (not $any_PTR_record_changes) { print "No PTR records were changed.\n"; } # ===== PROCESS SOA AND MX RECORDS ============================================ # The SOA record is automatically updated by Infoblox when other records change # Our DNS and mail systems do not use MX records. # ===== SUBROUTINES =========================================================== # ----------------------------------------------------------------------------- sub infoblox_error # ----------------------------------------------------------------------------- # abort the program with more details after the Infoblox has returned failure # ----------------------------------------------------------------------------- # ARGS: An Infoblox object and a message string # RVAL: # DIES: ALWAYS # ----------------------------------------------------------------------------- { my ($infoblox, $message) = @_; my $err_code = $infoblox->status_code(); my $err_detail = $infoblox->status_detail(); chomp $message; chomp $err_detail; my $err_str = "$message: Error $err_code ($err_detail)\n"; die $err_str; } # ----------------------------------------------------------------------------- sub chopdots # ----------------------------------------------------------------------------- # remove second and subsequent period-delimited fields from a string (for # example, when passed a FQDN, returns a simple hostname) # ----------------------------------------------------------------------------- # ARGS: a string # RVAL: the altered string # DIES: n/a # ----------------------------------------------------------------------------- { my $fq = $_[0]; (my $non_fq = $fq) =~ s/\..*$//; return $non_fq; } # ----------------------------------------------------------------------------- sub defined_and_matches # ----------------------------------------------------------------------------- # determines whether two strings are both defined and if so, equal # ----------------------------------------------------------------------------- # ARGS: two strings. # RVAL: Boolean if both strings are defined and equal # DIES: n/a # ----------------------------------------------------------------------------- { my ($str1, $str2) = @_; return if not defined $str1; return if not defined $str2; return $str1 eq $str2; }