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