diff --git a/salt/modules/infoblox.py b/salt/modules/infoblox.py new file mode 100644 index 0000000000..18322768b8 --- /dev/null +++ b/salt/modules/infoblox.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- +''' +Module for managing Infoblox + +Will look for pillar data infoblox:server, infoblox:user, infoblox:password if not passed to functions + +.. versionadded:: Boron + +:depends: + - requests +''' +from __future__ import absolute_import + +# Import salt libs +from salt.exceptions import CommandExecutionError +from salt.exceptions import SaltInvocationError +import logging + +log = logging.getLogger(__name__) + +try: + import json + import requests + HAS_IMPORTS = True +except ImportError: + HAS_IMPORTS = False + + +def __virtual__(): + if HAS_IMPORTS: + return True + return False + + +def _conn_info_check(infoblox_server=None, + infoblox_user=None, + infoblox_password=None): + ''' + get infoblox stuff from pillar if not passed + ''' + + if infoblox_server is None: + infoblox_server = __salt__['pillar.get']('infoblox:server', None) + if infoblox_user is None: + infoblox_user = __salt__['pillar.get']('infoblox:user', None) + log.debug('Infoblox username is "{0}"'.format(infoblox_user)) + if infoblox_password is None: + infoblox_password = __salt__['pillar.get']('infoblox:password', None) + + return infoblox_server, infoblox_user, infoblox_password + + +def _process_return_data(retData): + ''' + generic return processing + ''' + if retData.status_code == 200: + if retData.json(): + return retData + else: + log.debug('no data returned from infoblox') + return None + else: + msg = 'Unsuccessful error code {0} returned'.format(retData.status_code) + log.error(msg) + return None + + +def delete_record(name, + dns_view, + record_type, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + delete a record + + name + name of the record + + dns_view + the DNS view to remove the record from + + record_type + the record type (a, cname, host, etc) + + infoblox_server + the infoblox server hostname (can also use the infoblox:server pillar) + + infoblox_user + the infoblox user to connect with (can also use the infoblox:user pillar) + + infoblox_password + the infoblox user's password (can also use the infolblox:password pillar) + + infoblox_api_version + the infoblox api verison to use + + sslVerify + should ssl verification be done on the connection to the Infoblox REST API + + CLI Example: + + .. code-block:: bash + + salt my-minion infoblox.delete_record some.dns.record MyInfobloxView A sslVerify=False + ''' + infoblox_server, infoblox_user, infoblox_password = _conn_info_check(infoblox_server, + infoblox_user, + infoblox_password) + if infoblox_server is None and infoblox_user is None and infoblox_password is None: + _throw_no_creds() + return None + + currentRecords = get_record(name, + record_type, + infoblox_server, + infoblox_user, + infoblox_password, + dns_view, + infoblox_api_version, + sslVerify) + if currentRecords: + for currentRecord in currentRecords: + url = 'https://{0}/wapi/{1}/{2}'.format(infoblox_server, + infoblox_api_version, + currentRecord['Record ID']) + ret = requests.delete(url, + auth=(infoblox_user, infoblox_password), + headers={'Content-Type': 'application/json'}, + verify=sslVerify) + if ret.status_code == 200: + return True + else: + msg = 'Unsuccessful error code {0} returned -- full json dump {1}'.format(ret.status_code, ret.json()) + raise CommandExecutionError(msg) + return False + + +def update_record(name, + value, + dns_view, + record_type, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + update an entry to an infoblox dns view + + name + the dns name + + value + the value for the record + + record_type + the record type (a, cname, etc) + + dns_view + the DNS view to add the record to + + infoblox_server + the infoblox server hostname (can also use the infoblox:server pillar) + + infoblox_user + the infoblox user to connect with (can also use the infoblox:user pillar) + + infoblox_password + the infoblox user's password (can also use the infolblox:password pillar) + + infoblox_api_version + the infoblox api verison to use + + sslVerify + should ssl verification be done on the connection to the Infoblox REST API + + CLI Example: + + .. code-block:: bash + + salt '*' infoblox.update_record alias.network.name canonical.network.name MyInfobloxView cname sslVerify=False + ''' + + infoblox_server, infoblox_user, infoblox_password = _conn_info_check(infoblox_server, + infoblox_user, + infoblox_password) + if infoblox_server is None and infoblox_user is None and infoblox_password is None: + _throw_no_creds() + return None + + currentRecords = get_record(name, + record_type, + infoblox_server, + infoblox_user, + infoblox_password, + dns_view, + infoblox_api_version, + sslVerify) + if currentRecords: + for currentRecord in currentRecords: + url = 'https://{0}/wapi/{1}/{2}'.format( + infoblox_server, + infoblox_api_version, + currentRecord['Record ID']) + data = None + if record_type == 'cname': + data = json.dumps({'canonical': value}) + elif record_type == 'a': + data = {'ipv4addrs': []} + for i in value: + data['ipv4addrs'].append({'ipv4addr': i}) + data = json.dumps(data) + ret = requests.put(url, + data, + auth=(infoblox_user, infoblox_password), + headers={'Content-Type': 'application/json'}, + verify=sslVerify) + if ret.status_code == 200: + return True + else: + msg = 'Unsuccessful status code {0} returned.'.format(ret.status_code) + raise CommandExecutionError(msg) + else: + msg = 'Record {0} of type {1} was not found'.format(name, record_type) + log.error(msg) + return False + + +def add_record(name, + value, + record_type, + dns_view, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + add a record to an infoblox dns view + + name + the record name + + value + the value for the entry + can make use of infoblox functions for next available IP, like 'func:nextavailableip:10.1.0.0/24' + + record_type + the record type (cname, a, host, etc) + + dns_view + the DNS view to add the record to + + infoblox_server + the infoblox server hostname (can also use the infoblox:server pillar) + + infoblox_user + the infoblox user to connect with (can also use the infoblox:user pillar) + + infoblox_password + the infoblox user's password (can also use the infolblox:password pillar) + + infoblox_api_version + the infoblox api verison to use + + sslVerify + should ssl verification be done on the connection to the Infoblox REST API + + CLI Example: + + .. code-block:: bash + + salt 'myminion' infoblox.add_record alias.network.name canonical.network.name MyView + ''' + + infoblox_server, infoblox_user, infoblox_password = _conn_info_check(infoblox_server, + infoblox_user, + infoblox_password) + if infoblox_server is None and infoblox_user is None and infoblox_password is None: + _throw_no_creds() + return None + + record_type = record_type.lower() + + data = None + url = None + if record_type == 'cname': + data = json.dumps({'name': name, 'canonical': value, 'view': dns_view}) + if record_type == 'host' or record_type == 'a': + data = json.dumps({'name': name, 'ipv4addrs': [{'ipv4addr': value}], 'view': dns_view}) + #if record_type == 'alias': + # data = json.dumps({'name': name, 'aliases': [value], 'view': dns_view}) + # record_type = 'host' + # tRec = get_record(name, + # record_type, + # infoblox_server, + # infoblox_user, + # infoblox_password, + # dns_view, + # infoblox_api_version, + # sslVerify) + # if not tRec: + # log.error('A host record matching {0} was not found to add the alias to.'.format(name)) + # return False + # else: + # for _rec in tRec: + # url = 'https://{0}/wapi/{1}/{2}'.format( + # infoblox_server, + # infoblox_api_version, + # _rec['Record ID']) + + url = 'https://{0}/wapi/{1}/record:{2}'.format(infoblox_server, + infoblox_api_version, + record_type) + + ret = requests.post(url, + data, + auth=(infoblox_user, infoblox_password), + headers={'Content-Type': 'application/json'}, + verify=sslVerify) + if ret.status_code == 201: + return True + else: + msg = 'Unsuccessful error code {0} returned -- full json dump {1}'.format(ret.status_code, ret.json()) + raise CommandExecutionError(msg) + + +def _throw_no_creds(): + ''' + helper function to log no credentials found error + ''' + msg = 'An infoblox server, username, and password must be specified or configured via pillar' + raise SaltInvocationError(msg) + + +def get_network(network_name, + network_view=None, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + get a network from infoblox + + network_name + The name of the network in IPAM + + network_view + The name of the network view the network belongs to + + infoblox_server + the infoblox server hostname (can also use the infoblox:server pillar) + + infoblox_user + the infoblox user to connect with (can also use the infoblox:user pillar) + + infoblox_password + the infoblox user's password (can also use the infolblox:password pillar) + + infoblox_api_version + the infoblox api verison to use + + sslVerify + should ssl verification be done on the connection to the Infoblox REST API + + CLI Example: + + .. code-block:: bash + + salt myminion infoblox.get_network '10.0.0.0/8' + ''' + + records = [] + infoblox_server, infoblox_user, infoblox_password = _conn_info_check(infoblox_server, + infoblox_user, + infoblox_password) + if infoblox_server is None and infoblox_user is None and infoblox_password is None: + _throw_no_creds() + return None + + url = 'https://{0}/wapi/{1}/network?network={2}{3}'.format( + infoblox_server, + infoblox_api_version, + network_name, + ('' if network_view is None else '&network_view=' + network_view)) + log.debug('Requst url is "{0}"'.format(url)) + ret = _process_return_data(requests.get(url, + auth=(infoblox_user, infoblox_password), + verify=sslVerify)) + if ret: + for entry in ret.json(): + log.debug('Infoblox record returned: {0}'.format(entry)) + tEntry = {} + data = _parse_record_data(entry) + for key in data.keys(): + tEntry[key] = data[key] + records.append(tEntry) + return records + else: + return False + return False + + +def get_record(record_name, + record_type='host', + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + dns_view=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + get a record from infoblox + + record_name + name of the record to search for + + record_type + type of reacord to search for (host, cname, a, etc...defaults to host) + + infoblox_server + the infoblox server hostname (can also use the infoblox:server pillar) + + infoblox_user + the infoblox user to connect with (can also use the infoblox:user pillar) + + infoblox_password + the infoblox user's password (can also use the infolblox:password pillar) + + dns_view + the infoblox DNS view to search, if not specified all views are searched + + infoblox_api_version + the infoblox api verison to use + + sslVerify + should ssl verification be done on the connection to the Infoblox REST API + + CLI Example: + + .. code-block:: bash + + salt myminion infoblox.get_record some.host.com A sslVerify=False + ''' + + #TODO - verify record type (A, AAAA, CNAME< HOST, MX, PTR, SVR, TXT, host_ipv4addr, host_ipv6addr, naptr) + records = [] + + infoblox_server, infoblox_user, infoblox_password = _conn_info_check(infoblox_server, + infoblox_user, + infoblox_password) + + if infoblox_server is None and infoblox_user is None and infoblox_password is None: + _throw_no_creds() + return None + + url = 'https://{0}/wapi/{1}/record:{3}?name:={2}{4}{5}'.format( + infoblox_server, + infoblox_api_version, + record_name, + record_type, + ('' if dns_view is None else '&view=' + dns_view), + ('&_return_fields%2B=aliases' if record_type == 'host' else '') + ) + log.debug('Requst url is "{0}"'.format(url)) + ret = _process_return_data(requests.get(url, + auth=(infoblox_user, infoblox_password), + verify=sslVerify)) + if ret: + for entry in ret.json(): + log.debug('Infoblox record returned: {0}'.format(entry)) + tEntry = {} + data = _parse_record_data(entry) + for key in data.keys(): + tEntry[key] = data[key] + records.append(tEntry) + return records + else: + return False + return False + + +def _parse_record_data(entry_data): + ''' + returns the right value data we'd be interested in for the specified record type + ''' + + ret = {} + ipv4addrs = [] + aliases = [] + if 'canonical' in entry_data: + ret['Canonical Name'] = entry_data['canonical'] + if 'ipv4addrs' in entry_data: + for ipaddrs in entry_data['ipv4addrs']: + ipv4addrs.append(ipaddrs['ipv4addr']) + ret['IP Addresses'] = ipv4addrs + if 'aliases' in entry_data: + for alias in entry_data['aliases']: + aliases.append(alias) + ret['Aliases'] = aliases + if 'name' in entry_data: + ret['Name'] = entry_data['name'] + if 'view' in entry_data: + ret['DNS View'] = entry_data['view'] + if 'network_view' in entry_data: + ret['Network View'] = entry_data['network_view'] + if 'comment' in entry_data: + ret['Comment'] = entry_data['comment'] + if 'network' in entry_data: + ret['Network'] = entry_data['network'] + if '_ref' in entry_data: + ret['Record ID'] = entry_data['_ref'] + return ret diff --git a/salt/states/infoblox.py b/salt/states/infoblox.py new file mode 100644 index 0000000000..2c3db9771b --- /dev/null +++ b/salt/states/infoblox.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +''' +states for infoblox stuff + +ensures a record is either present or absent in an Infoblox DNS system + +.. versionadded:: Boron +''' +from __future__ import absolute_import + +# Import Python libs +import logging + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + make sure the infoblox module is available + ''' + return True if 'infoblox.get_record' in __salt__ else False + + +def present(name, + value, + record_type, + dns_view, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + Ensure a record exists + + name + Name of the record + + value + Value of the record + + record_type + record type (host, a, cname, etc) + + dns_view + DNS View + + infoblox_server + infoblox server to connect to (will try pillar if not specified) + + infoblox_user + username to use to connect to infoblox (will try pillar if not specified) + + infoblox_password + password to use to connect to infoblox (will try pillar if not specified) + + verify_ssl + verify SSL certificates + + Example: + + .. code-block:: yaml + + some-state: + infoblox.present: + - name: some.dns.record + - value: 10.1.1.3 + - record_type: host + - sslVerify: False + ''' + record_type = record_type.lower() + ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} + records = __salt__['infoblox.get_record'](name, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + dns_view=dns_view, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify) + if records: + #check records for updates + for record in records: + if record_type == 'cname': + if record['Canonical Name'] != value: + if __opts__['test']: + ret['result'] = None + ret['comment'] = ' '.join([ret['comment'], + 'DNS {0} record {1} in view {2} will be update'.format(record_type, + name, + dns_view)]) + else: + retval = __salt__['infoblox.update_record'](name, + value, + dns_view, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify) + if retval: + if 'old' not in ret['changes']: + ret['changes']['old'] = [] + if 'new' not in ret['changes']: + ret['changes']['new'] = [] + ret['changes']['old'].append(record) + ret['changes']['new'].append(__salt__['infoblox.get_record'](name, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + dns_view=dns_view, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify)) + else: + ret['result'] = False + return ret + else: + #no records + if __opts__['test']: + ret['result'] = None + ret['comment'] = ' '.join([ret['comment'], + 'DNS {0} record {1} set to be added to view {2}'.format(record_type, + name, + dns_view)]) + return ret + retval = __salt__['infoblox.add_record'](name, + value, + record_type, + dns_view, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=sslVerify) + if retval: + ret['result'] = True + ret['changes']['old'] = None + ret['changes']['new'] = __salt__['infoblox.get_record'](name, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + dns_view=dns_view, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify) + return ret + + +def absent(name, + record_type, + dns_view, + infoblox_server=None, + infoblox_user=None, + infoblox_password=None, + infoblox_api_version='v1.4.2', + sslVerify=True): + ''' + Ensure a record does not exists + + name + Name of the record + record_type + record type (host, a, cname, etc) + dns_view + DNS View + infoblox_server + infoblox server to connect to (will try pillar if not specified) + infoblox_user + username to use to connect to infoblox (will try pillar if not specified) + infoblox_password + password to use to connect to infoblox (will try pillar if not specified) + verify_ssl + verify SSL certificates + + Example: + + .. code-block:: yaml + + some-state: + infoblox.absent: + - name: some.dns.record + - record_type: host + - dns_view: MyView + - sslVerify: False + ''' + ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} + record = __salt__['infoblox.get_record'](name, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + dns_view=dns_view, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify) + if record: + if __opts__['test']: + ret['result'] = None + ret['comment'] = ' '.join([ret['comment'], + 'DNS {0} record {1} in view {2} will be removed'.format(record_type, + name, + dns_view)]) + else: + retval = __salt__['infoblox.delete_record'](name, + dns_view, + record_type, + infoblox_server=infoblox_server, + infoblox_user=infoblox_user, + infoblox_password=infoblox_password, + infoblox_api_version=infoblox_api_version, + sslVerify=sslVerify) + if retval: + if 'old' not in ret['changes']: + ret['changes']['old'] = [] + ret['changes']['new'] = None + ret['changes']['old'].append(record) + else: + ret['result'] = False + return ret + else: + #record not found + ret['result'] = True + ret['changes']['old'] = None + ret['changes']['new'] = None + ret['comment'] = 'DNS record does not exist' + + return ret