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.