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 = 'P@55w0rd'
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...