update_infoblox.pl
#!/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;
}


    
Download this file
Jeremy Holland - Code Portfolio
Contact Me