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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
'''
    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...