Moving Zones Between Views in Infoblox

It turns out that moving zones between views in Infoblox is a surprisingly hard thing to do. The challenge ended up on my desk after we made the decision to operate a separate external view for a selection of our domains. At that point, we need to move a selection of the domains from the internal view to the new external view.

One way of doing this is to export each zone, one at a time as a CSV file and import it back in. Unfortunately, we found it tended to run into errors doing this and would have been incredibly tedious to do for the number of zones involved.

The approach I took in the end was to use the web API to get all the records we need out of the zones, create new zones and duplicate the records in the new view. This isn’t my first time making use of the web API in Infoblox but it did (thankfully) prove powerful enough to do the job.

This is the result of all that work:

'''
	Moves zones between views.
	Author: Marc Steele
	Date: May 2017
'''

# Settings

SERVER = '192.168.207.10'
USERNAME = 'apiusername'
PASSWORD = [email protected]'
SRC_VIEW = 'default'
DST_VIEW = 'External'
PAGE_SIZE = 10000
HTTP_SUCCESS = 200
HTTP_CREATED = 201
SSL_CHECK = False
DNS_SERVER_GROUP = 'DMZ Public Facing'
RECORD_TYPES = ['record:ptr', 'record:a', 'record:cname', 'record:mx', 'record:txt']
DELETE_OLD_ZONES = False

# Imports

import requests
import pprint
import json
from netmiko import ConnectHandler
import re
from netaddr import IPNetwork, IPAddress
import ldap
import dns.resolver
import dns.reversename
import socket

### HELPER FUNCTIONS ###

def delete_record(record):

	# Sanity check

	if not record:
		return

	# Perform the deletion

	delete_url = 'https://{}/wapi/v1.7.5/{}'.format(SERVER, record['_ref'])
	delete_request = requests.delete(delete_url, auth=(USERNAME, PASSWORD), verify=SSL_CHECK)
	if (delete_request.status_code != HTTP_SUCCESS):
		print('Failed to delete record {}. Reason: {}.'.format(record['_ref'], delete_request.text))

def get_records(url, payload):

	# Sanity check

	if (not url) or (not payload):
		print('No URL or playload supplied for retireving records.')

	# Cycle through the pages of responses we will get

	results = []
	more_pages = True

	while (more_pages):

		# Check we got a valid response from the server

		request = requests.get(url, params=payload, auth=(USERNAME, PASSWORD), verify=SSL_CHECK)
		if (request.status_code != HTTP_SUCCESS):
			print('Failed to retrieve data from {}. Response: {}.'.format(url, request.text))
			return None

		# Inflate out the results

		json = request.json()
		for result in json['result']:
			results.append(result)

		# Cycle onto the next page

		if ('next_page_id' in json):
			payload['_page_id'] = json['next_page_id']
		else:
			more_pages = False

	return results

def zone_exists(fqdn, view):

	# Sanity check

	if (not fqdn) or (not view):
		print('You must supply both an FQDN and view to check if the zone exists.')
		return False

	# Look for the zone

	zones_url = 'https://{}/wapi/v1.7.5/zone_auth'.format(SERVER)
	zones_payload = {
		'_paging': 1,
		'_return_as_object': 1,
		'_max_results': PAGE_SIZE,
		'view': view,
		'fqdn': fqdn
	}

	zones = get_records(zones_url, zones_payload)
	return len(zones) > 0

### END OF HELPER FUNCTIONS ###


# Find all zones in the source view

print('Retrieving zones from {} for the {} view...'.format(SERVER, SRC_VIEW))

zones_url = 'https://{}/wapi/v1.7.5/zone_auth'.format(SERVER)
zones_payload = {
	'_paging': 1,
	'_return_as_object': 1,
	'_max_results': PAGE_SIZE,
	'view': SRC_VIEW
}


zones = get_records(zones_url, zones_payload)

for zone in zones:

	if not zone_exists(zone['fqdn'], DST_VIEW):

		print('Moving the {} zone.'.format(zone['fqdn']))

		# Create the new zone

		new_zone = {
			'fqdn': zone['fqdn'],
			'view': DST_VIEW,
			'ns_group': DNS_SERVER_GROUP
		}
		
		zone_url = 'https://{}/wapi/v1.7.5/zone_auth'.format(SERVER)
		zone_request = requests.post(zone_url, data=json.dumps(new_zone), auth=(USERNAME, PASSWORD), verify=SSL_CHECK)
		if (zone_request.status_code < 200 or zone_request.status_code > 299):
			print('Failed to move zone {}. Reason: {}.'.format(new_zone['fqdn'], zone_request.text))
		else:
			print('Successfully moved zone {} in the {} view.'.format(new_zone['fqdn'], DST_VIEW))

		# Move all the records

		for record_type in RECORD_TYPES:

			# Request the entries

			entry_url = 'https://{}/wapi/v1.7.5/{}'.format(SERVER, record_type)
			entry_payload = {
				'_paging': 1,
				'_return_as_object': 1,
				'_max_results': PAGE_SIZE,
				'view': SRC_VIEW,
				'zone': zone['fqdn']
			}

			if record_type == '':
				entry_payload['_return_fields+'] = 'ipv4addr'

			entries = get_records(entry_url, entry_payload)

			if entries:
				for entry in entries:

					if record_type == 'record:ptr':

						new_entry = {
							'ipv4addr': entry['ipv4addr'],
							'ptrdname': entry['ptrdname']
						}

					elif record_type == 'record:a':

						new_entry = {
							'name': entry['name'],
							'ipv4addr': entry['ipv4addr']
						}

					elif record_type == 'record:cname':

						new_entry = {
							'canonical': entry['canonical'],
							'name': entry['name']
						}

					elif record_type == 'record:mx':

						new_entry = {
							'mail_exchanger': entry['mail_exchanger'],
							'name': entry['name'],
							'preference': entry['preference']
						}

					elif record_type == 'record:txt':

						new_entry = {
							'name': entry['name'],
							'text': entry['text']
						}

					else:
						continue

					# Set the view and create the new object

					new_entry['view'] = DST_VIEW
					update_url = 'https://{}/wapi/v1.7.5/{}'.format(SERVER, record_type)

					update_request = requests.post(update_url, data=json.dumps(new_entry), auth=(USERNAME, PASSWORD), verify=SSL_CHECK)
					if (update_request.status_code != HTTP_CREATED):
						print('Failed to move entry {}. Reason: {}.'.format(entry['name'], update_request.text))
			

		# Delete the zone from the old view

		if DELETE_OLD_ZONES:
			print('Removing the {} zone from the {} view...'.format(zone['fqdn'], SRC_VIEW))
			delete_record(zone)

You will need to adjust a few things for your own use. In most cases it’s just a matter of adjusting the settings at the top of the script.

However, the logic we’ve used here is to move zones that don’t already exist in the destination view. The idea behind this we’d already created the external views that needed to be split.

Either way, hopefully this script is of some use to you and saves a bit of pain hand cranking the move.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *