diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2025e1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +env +settings.py +*.orig +*.csv diff --git a/DynResource/__init__.py b/DynResource/__init__.py new file mode 100644 index 0000000..a00e722 --- /dev/null +++ b/DynResource/__init__.py @@ -0,0 +1,5 @@ +from dynrestresource import DynRestResource +from formatter import Formatter +import dynhelpers + +from dynresourcemap import DYN_API_MAP, SUPPORTED_RECORD_TYPES \ No newline at end of file diff --git a/DynResource/dynhelpers/__init__.py b/DynResource/dynhelpers/__init__.py new file mode 100644 index 0000000..270ec40 --- /dev/null +++ b/DynResource/dynhelpers/__init__.py @@ -0,0 +1,6 @@ +from dynhelper import DynHelper +from add import Add +from update import Update +from delete import Delete +from search import Search +from dynhelpermap import DYN_HELPERS, SUPPORTED_RECORD_TYPES \ No newline at end of file diff --git a/DynResource/dynhelpers/add.py b/DynResource/dynhelpers/add.py new file mode 100644 index 0000000..77f8240 --- /dev/null +++ b/DynResource/dynhelpers/add.py @@ -0,0 +1,41 @@ +import logging +from pprint import pprint, pformat + +from dynhelper import DynHelper +from .. import DynRestResource + + +class Add(DynHelper): + + #inherit DynectRest init + def __init__(self, user, password, account, logger=None): + super(Add, self).__init__(user, password, account, logger) + + #if logger name is specified, log commands will log to it + if logger is not None: + parent_logger = logger + global log + log = logging.getLogger('{0}.{1}'.format(parent_logger,__name__)) + #if not, set up a NullHandler so log commands don't error + else: + log = logging.getLogger('{0}'.format(__name__)) + null_handler = logging.NullHandler() + self.log.addHandler(null_handler) + + def do(self, resource, arg_dict, auto_publish=True): + + log.debug('Add request started') + + #Sort out fqdn, zone, and hostname provided + arg_dict = self._get_zone_from_fqdn(arg_dict) + + self._log_request_info(resource, arg_dict) + + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + result = dyn.do(resource, 'add', arg_dict, auto_publish) + + log.info("Request completed") + output = result + log.debug(pformat(output)) + + return output \ No newline at end of file diff --git a/DynResource/dynhelpers/delete.py b/DynResource/dynhelpers/delete.py new file mode 100644 index 0000000..a0fdd3c --- /dev/null +++ b/DynResource/dynhelpers/delete.py @@ -0,0 +1,66 @@ +import logging +from pprint import pprint, pformat + +from dynhelper import DynHelper +from .. import DynRestResource + + +class Delete(DynHelper): + + #inherit DynectRest init + def __init__(self, user, password, account, logger=None): + super(Delete, self).__init__(user, password, account, logger) + + #if logger name is specified, log commands will log to it + if logger is not None: + parent_logger = logger + global log + log = logging.getLogger('{0}.{1}'.format(parent_logger,__name__)) + #if not, set up a NullHandler so log commands don't error + else: + log = logging.getLogger('{0}'.format(__name__)) + null_handler = logging.NullHandler() + log.addHandler(null_handler) + + + def do(self, resource, arg_dict, auto_publish=True): + + log.debug('Delete request started') + + #Sort out fqdn, zone, and hostname provided + arg_dict = self._get_zone_from_fqdn(arg_dict) + + self._log_request_info(resource, arg_dict) + + log.debug("Finding records that match fqdn {0}".format(arg_dict['fqdn'])) + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + result_list = dyn.do(resource, 'list', arg_dict, auto_publish) + + if 'delete_all' in arg_dict.keys(): + log.info('delete_all flag detected') + update_all = arg_dict['delete_all'] + else: + update_all = False + + if len(result_list['data']) > 1 and delete_all is False: + log.warning('Multiple records found, please specify the ID with the -i flag or use the --update_all flag cautiously') + output = result_list + + elif len(result_list['data']) == 1 or delete_all is True: + output = [] + log.debug('Either one record was found, or delete_all flag was used') + + for record in result_list['data']: + + arg_dict['record_id'] = record.rsplit('/')[-1] + log.debug('Deleting record {0}'.format(arg_dict['record_id'])) + result_delete = dyn.do(resource, 'delete', arg_dict, auto_publish) + + output.append({ + 'result': result_delete, + }) + + log.info("Request completed") + log.debug(pformat(output)) + + return output \ No newline at end of file diff --git a/DynResource/dynhelpers/dynhelper.py b/DynResource/dynhelpers/dynhelper.py new file mode 100644 index 0000000..cd77b6f --- /dev/null +++ b/DynResource/dynhelpers/dynhelper.py @@ -0,0 +1,123 @@ +import logging +import re + +from dynhelpermap import DYN_HELPERS, SUPPORTED_RECORD_TYPES + +class DynHelper(object): + + def __init__(self,user,password,account,logger=None): + + self.user = user + self.password = password + self.account = account + self.logger = logger + + #if logger name is specified, log commands will log to it + if logger is not None: + parent_logger = logger + global log + log = logging.getLogger('{0}.{1}'.format(parent_logger,__name__)) + #if not, set up a NullHandler so log commands don't error + else: + log = logging.getLogger('{0}'.format(__name__)) + null_handler = logging.NullHandler() + self.log.addHandler(null_handler) + + def _get_zone_from_fqdn(self,arg_dict): + + log.debug('Getting Zone from FQDN') + + if 'zone' not in arg_dict.keys(): + log.debug('No zone provided, getting zone from FQDN') + arg_dict['zone'] = "{0}.{1}".format(arg_dict['fqdn'].rsplit('.')[-2],arg_dict['fqdn'].rsplit('.')[-1]) + log.info('Zone {0} assumed from FQDN {1}'.format(arg_dict['zone'], arg_dict['fqdn'])) + + return arg_dict + + def _get_zone_from_zonelink(self, zonelinks, arg_dict): + + output = [] + log.debug('Getting zone name from zonelinks list') + for zonelink in zonelinks: + zone = "{0}".format(zonelink.rsplit('/')[-2]) + output.append(zone) + log.debug('Zone {0} found in zonelink {1}'.format(zone, zonelink)) + + return output + + def _redact_password(self, arg_dict): + + log.debug('Redacting password from arg_dict for logging') + new_arg_dict = arg_dict + + if 'password' in arg_dict.keys(): + new_arg_dict['password'] = "********" + log.debug('Password redacted') + + return new_arg_dict + + def _log_request_info(self, request, arg_dict): + + log.debug("Get request info") + arg_list = arg_dict.keys() + log_data = 'New {0} Request: '.format(request) + + for arg_item in arg_list: + if arg_item not in ['user', 'password', 'account', 'helper_action', 'logger_name' ]: + log_data = "{0}{1}={2}, ".format(log_data, arg_item, arg_dict[arg_item]) + + log.info(log_data) + + def get_record_type(self, original, new_format): + + log.debug('Getting record type') + format_choices = ['short', 'dataset', 'resource', ] + if new_format not in format_choices: + log.critical("Invalid new_format choice. Must be 'short', 'dataset', or 'resource'") + + # long record_type for datasets + long_name_dataset = ("{0}_record".format(x.lower()) for x in SUPPORTED_RECORD_TYPES) + # long resource_type for resource calls + long_name_resource = ("{0}Record".format(x.upper()) for x in SUPPORTED_RECORD_TYPES) + + if original in SUPPORTED_RECORD_TYPES: + original_format = 'short' + log.info('Short record type detected') + elif original in long_name_dataset: + log.info('Long record type detected type') + original_format = 'dataset' + elif original in long_name_resource: + log.info('converted to short long range type') + original_format = 'resource' + + if original_format == new_format: + log.debug('Requested record_type is already in desired format') + return original + + else: + if new_format == 'short': + if original_format == 'dataset': + output = original.split('_')[0].upper() + log.debug("Converted to short format, {0}".format(output)) + elif original_format == 'resource': + output = re.search('^[A-Z]*', original).group(0) + output = re.sub('R$','$', output) + log.debug("Converted to short format, {0}".format(output)) + elif new_format == 'dataset': + if original_format =='short': + output = "{0}_records".format(original.lower()) + log.debug("Converted to dataset format, {0}".format(output)) + elif original_format == 'resource': + short = re.sub('Record','_records', original) + output = short.lower() + log.debug("Converted to dataset format, {0}".format(output)) + elif new_format == 'resource': + if original_format == 'short': + output = '{0}Record'.format(original) + log.debug("Converted to resource format, {0}".format(output)) + elif output_format == 'dataset': + short = re.match('^[A-Z]*_', original).group(0).upper() + output = '{0}Record'.format(original) + log.debug("Converted to resource format, {0}".format(output)) + + return output diff --git a/DynResource/dynhelpers/dynhelpermap.py b/DynResource/dynhelpers/dynhelpermap.py new file mode 100644 index 0000000..1ce0459 --- /dev/null +++ b/DynResource/dynhelpers/dynhelpermap.py @@ -0,0 +1,486 @@ +SUPPORTED_RECORD_TYPES = [ + 'A', + 'CNAME', + 'NS', + 'PTR', + 'SOA', + 'TXT', + 'SPF', + 'SRV', + 'MX', +] + +#### Common options #### +OPT_RECORD_TYPES = { + 'flag': 'r', + 'nargs': "*", + 'choices': SUPPORTED_RECORD_TYPES, + 'help': 'Record types', +} +OPT_ZONES = { + 'flag': 'z', + 'nargs': "*", + 'help': 'Zones to search', +} +OPT_ZONE = { + 'flag': 'z', + 'help': 'Zones to search', +} +OPT_FQDN = { + 'flag': 1, + 'help': 'FQDN of record', +} +OPT_TTL = { + 'flag': 't', + 'help': 'TTL for record', +} +OPT_DATAFIELDS = { + 'flag': 'D', + 'nargs': "*", + 'help': 'Datafields you would like to receive in what order', +} +OPT_RECORD_ID = { + 'flag': 'i', + 'help': 'ID for specific record. Required if more than one record exists for FQDN', +} +OPT_DETAIL = { 'constant': 'Y', 'help': 'Detailed output', } +OPT_UPDATE_ALL = { 'help': 'Update all records that match', } +OPT_NOHEADERS = { 'help': 'Do not show headers at top of CSV', } +OPT_CSV = { 'help': 'Show output as CSV', } +OPT_YES = { 'help': 'Answer yes to all prompts', } +OPT_DELETE_ALL = { 'help': 'Delete all matching records', } +OPT_AS_LIST = { 'help': 'Display data as a Bash-style list, space separated if multiple datafields are requested.', } + +#### Not-so-common options #### +OPT_SERVICE_TYPE = { + 'flag': 'S', + 'help': 'Service type to search', + 'choices': [ + 'HTTPRedirect', + ] +} +OPT_PORT = { + 'flag': 'P', + 'help': 'Indicates the port where the service is running' +} +OPT_PRIORITY = { + 'flag': 'N', + 'help': 'Numeric value for priority usage. Lower value takes precedence over higher value where two records of the same type exist on the zone/node.' +} +OPT_WEIGHT = { + 'flag': 'W', + 'help': 'Secondary prioritizing of records to serve. Records of equal priority should be served based on their weight. Higher values are served more often.', +} +OPT_CODE = { + 'flag': 'C', + 'help': 'HTTP response code to return for redirection', + 'choices': [ + '301', + '302', + ] +} +OPT_KEEP_URI = { + 'flag': 'K', + 'help': """A flag indicating whether the redirection + should include the originally requested URI""", + 'choices': [ + 'Y', + 'N', + ] +} +OPT_SERIAL_STYLE = { + 'flag': 'S', + 'help': 'The style in which serial numbers will be generated for the zone', + 'choices': [ + 'increment', + 'epoch', + 'day', + 'minute', + ], +} + +DYN_HELPERS = {} +DYN_HELPERS['search'] = { + 'actions': { + 'records': { + 'options': { + 'record_types': OPT_RECORD_TYPES, + 'zones': OPT_ZONES, + 'datafields': OPT_DATAFIELDS, + 'noheaders': OPT_NOHEADERS, + 'as_list': OPT_AS_LIST, + 'ttl_min': { + 'flag': 'm', + 'help': 'Minimum TTL to search for', + }, + 'ttl_max': { + 'flag': 'M', + 'help': 'Maximum TTL to search for', + }, + 'csv': { + 'flag': False, + 'help': "Output as CSV", + }, + }, + }, + 'changelog': { + 'options': { + 'zones': OPT_ZONES, + 'csv': OPT_CSV, + 'limit': { + 'flag': 'l', + 'help': 'Limit the record of numbers to be returned', + }, + 'age': { + 'flag': 'A', + 'help': 'Minimum age of note in hours', + }, + }, + }, + 'zones': { + 'options': { + 'as_list': OPT_AS_LIST, + }, + }, + 'services': { + 'options': { + 'service_type': OPT_SERVICE_TYPE, + 'zone': OPT_ZONE, + 'detail': OPT_DETAIL, + }, + } + }, + 'help': 'Search Dyn Data', +} + +DYN_HELPERS['add'] = { + 'actions': { + 'ARecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'address': { + 'flag': 'd', + 'help': 'Data - IP Address', + 'required': True, + }, + }, + 'help': 'Add an A Record', + }, + 'CNAMERecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'cname': { + 'flag': 'd', + 'help': 'Data - CNAME Address to point to', + }, + }, + 'help': 'Add a CNAME Record', + }, + 'NSRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'nsdname': { + 'flag': 'd', + 'help': 'Data - FQDN of Name Server', + }, + }, + 'help': 'Add an NS Record', + }, + 'PTRRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'ptrdname': { + 'flag': 'd', + 'help': 'Data - FQDN Address to point to', + }, + }, + 'help': 'Add a PTR Record', + }, + 'TXTRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'txtdata': { + 'flag': 'd', + 'help': 'Data - Free form text', + }, + }, + 'help': 'Add a TXT Record', + }, + 'SRVRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'port': OPT_PORT, + 'priority': OPT_PRIORITY, + 'weight': OPT_WEIGHT, + 'target': { + 'flag': 'd', + 'help': 'Data - The domain name of a host where the service is running on the specified port', + }, + }, + 'help': 'Add an SRV Record', + }, + 'MXRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'preference': OPT_PRIORITY, + 'ttl': OPT_TTL, + 'exchange': { + 'flag': 'd', + 'help': 'Hostname of the server responsible for accepting mail messages in the zone', + }, + }, + 'help': 'Add an MX Record', + }, + 'HTTPRedirect': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'code': OPT_CODE, + 'keep_uri': OPT_KEEP_URI, + 'url': { + 'flag': 'd', + 'help': 'Data - The URL of the redirect destination' + }, + }, + 'help': 'Add an HTTPRedirect service', + }, + }, + 'help': 'Add stuff', +} + +DYN_HELPERS['update'] = { + 'actions': { + 'ARecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'record_id': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'address': { + 'flag': 'd', + 'help': 'IP Address', + }, + }, + 'help': 'Update an A Record', + }, + 'CNAMERecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'record_id': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'cname': { + 'flag': 'd', + 'help': 'Data - CNAME Address to point to', + }, + }, + 'help': 'Update a CNAME Record', + }, + 'NSRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'recordid': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'nsdname': { + 'flag': 'd', + 'help': 'Data - FQDN of Name Server', + }, + }, + 'help': 'Update an NS Record', + }, + 'PTRRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'record_id': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'ptrdname': { + 'flag': 'd', + 'help': 'Data - FQDN Address to point to', + }, + }, + 'help': 'Update a PTR Record', + }, + 'TXTRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'id': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'txtdata': { + 'flag': 'd', + 'help': 'Data - Free form text', + }, + }, + 'help': 'Update a TXT Record', + }, + 'SRVRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'port': OPT_PORT, + 'priority': OPT_PRIORITY, + 'weight': OPT_WEIGHT, + 'id': OPT_RECORD_ID, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'target': { + 'flag': 'd', + 'help': 'Data - The domain name of a host where the service is running on the specified port' + }, + }, + 'help': 'Update an SRV Record', + }, + 'MXRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'preference': OPT_PRIORITY, + 'ttl': OPT_TTL, + 'id': OPT_RECORD_ID, + 'yes': OPT_YES, + 'exchange': { + 'flag': 'd', + 'help': 'Data - Hostname of the server responsible for accepting mail messages in the zone', + }, + }, + 'help': 'Update an MX Record', + }, + 'SOARecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'ttl': OPT_TTL, + 'serial_style': OPT_SERIAL_STYLE, + 'update_all': OPT_UPDATE_ALL, + 'yes': OPT_YES, + 'rname': { + 'flag': 'd', + 'help': 'Data - Domain name which specifies the mailbox of the person responsible for this zone', + }, + }, + 'help': 'Update an SOA record', + }, + 'HTTPRedirect': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'code': OPT_CODE, + 'keep_uri': OPT_KEEP_URI, + 'url': { + 'flag': 'd', + 'help': 'Data - The URL of the redirect destination' + }, + }, + 'help': 'Update HTTP Redirect service', + } + }, + 'help': 'Update stuff', +} + +DYN_HELPERS['delete'] = { + 'actions': { + 'ARecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete an A Record', + }, + 'CNAMERecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete a CNAME Record', + }, + 'NSRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete an NS Record', + }, + 'PTRRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete a PTR Record', + }, + 'TXTRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete a TXT Record', + }, + 'SRVRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete an SRV Record', + }, + 'MXRecord': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE, + 'record_id': OPT_RECORD_ID, + 'delete_all': OPT_DELETE_ALL, + 'yes': OPT_YES, + }, + 'help': 'Delete an MX Record', + }, + 'HTTPRedirect': { + 'options': { + 'fqdn': OPT_FQDN, + 'zone': OPT_ZONE + }, + 'help': 'Delete HTTPRedirect service', + }, + }, + 'help': 'Delete stuff', +} \ No newline at end of file diff --git a/DynResource/dynhelpers/search.py b/DynResource/dynhelpers/search.py new file mode 100644 index 0000000..60ae835 --- /dev/null +++ b/DynResource/dynhelpers/search.py @@ -0,0 +1,203 @@ +import logging +import re +import time +from pprint import pprint, pformat + +from dynhelper import DynHelper +from .. import DynRestResource +from ..dynresourcemap import DYN_API_MAP + +class Search(DynHelper): + + #inherit DynectRest init + def __init__(self, user, password, account, logger=None): + super(Search, self).__init__(user, password, account, logger) + + #if logger name is specified, log commands will log to it + if logger is not None: + parent_logger = logger + global log + log = logging.getLogger('{0}.{1}'.format(parent_logger,__name__)) + #if not, set up a NullHandler so log commands don't error + else: + log = logging.getLogger('{0}'.format(__name__)) + null_handler = logging.NullHandler() + log.addHandler(null_handler) + + + def do(self, resource, arg_dict, auto_publish=True): + + log.debug('Search request started') + + self._log_request_info(resource, arg_dict) + + if arg_dict['helper_action'] == 'records': + log.debug('Searching Records') + + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + + # get zone records + zone_dict = {} + ('Getting zone data') + + zone_query_dict = arg_dict + zone_query_dict['detail'] = True + zone_query_dict['resource'] = 'AllRecord' + + output = {} + + # Apply any specified filter args to output + filter_args = ['record_types', 'datafields', 'ttl_min', 'ttl_max', ] + + for zone in arg_dict['zones']: + output[zone] = {} + zone_query_dict['zone'] = zone + zone_data = dyn.do('AllRecord', 'list', zone_query_dict, auto_publish) + output[zone]['status'] = zone_data['status'] + output[zone]['job_id'] = zone_data['job_id'] + output[zone]['msgs'] = zone_data['msgs'] + output[zone]['data'] = {} + + if any(x in arg_dict.keys() for x in filter_args): + + if 'record_types' in arg_dict.keys(): + log.debug("record_types arg detected") + + record_types = [] + for record_type_arg in arg_dict['record_types']: + record_type_arg = self.get_record_type(record_type_arg, 'dataset') + record_types.append(record_type_arg) + + else: + record_types = zone_data['data'].keys() + + for record_type in record_types: + + output[zone]['data'][record_type] = [] + + try: + + for record in zone_data['data'][record_type]: + + if ('ttl_min' in arg_dict.keys()) and ('ttl_max' in arg_dict.keys()): + if (int(record['ttl']) > int(arg_dict['ttl_min'])) and (int(record['ttl']) < int(arg_dict['ttl_max'])): + output[zone]['data'][record_type].append(record) + + elif 'ttl_min' in arg_dict.keys(): + if int(record['ttl']) > int(arg_dict['ttl_min']): + log.debug("ttl for {0}, ttl:{1} is less than {2}, filtering".format( + record['fqdn'], record['ttl'], arg_dict['ttl_min'])) + output[zone]['data'][record_type].append(record) + else: + log.debug("ttl for {0}, ttl:{1} is greater than {2}, keeping".format( + record['fqdn'], record['ttl'], arg_dict['ttl_min'])) + + elif 'ttl_max' in arg_dict.keys(): + if int(record['ttl']) < int(arg_dict['ttl_max']): + log.debug("ttl for {0}, ttl:{1} is greater than {2}, filtering".format( + record['fqdn'], record['ttl'], arg_dict['ttl_max'])) + output[zone]['data'][record_type].append(record) + else: + log.debug("ttl for {0}, ttl:{1} is less than {2}, keeping".format( + record['fqdn'], record['ttl'], arg_dict['ttl_max'])) + + else: + output[zone]['data'][record_type].append(record) + + except KeyError: + log.info('No {0} found'.format(record_type)) + except: + raise + + else: + output[zone]['data'] = zone_data['data'] + + return output + + elif arg_dict['helper_action'] == 'changelog': + + log.info('changelog action specified') + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + + if 'zones' not in arg_dict.keys(): + log.info('Zones not specified in command, using all zones') + zones = dyn.do('Zone', 'list', arg_dict, auto_publish) + zones_list = [] + for zone_link in zones['data']: + zones_list.append(zone_link.rsplit('/')[-2]) + else: + log.debug('Zones specified in command') + + if 'age' not in arg_dict.keys(): + log.info('No log age defined') + arg_dict['age'] = 0 + + if 'limit' not in arg_dict.keys(): + log.info('No log entry limit defined, using last 50 entries') + arg_dict['limit'] = 50 + else: + log.debug('Limit of log entries specified at {0}'.format(arg_dict['limit'])) + + log.debug('Getting zone notes') + + zone_notes = {} + for zone in arg_dict['zones']: + arg_dict['zone'] = zone + arg_dict['resource'] = 'ZoneNoteReport' + zone_notes[zone] = dyn.do('ZoneNoteReport', 'get', arg_dict, auto_publish) + log.info('Zone notes retrieved successfully') + + if arg_dict['age'] == 0: + cutoff = 0 + log.debug('Cutoff age is 0') + else: + epoch_age = int(arg_dict['age']) * 60 * 60 + cutoff = int(time.time()) - epoch_age + log.debug('Cutoff age is {0}'.format(cutoff)) + + output = {} + for zone in arg_dict['zones']: + + output[zone] = {} + output[zone]['data'] = [] + output[zone]['msgs'] = zone_notes[zone]['msgs'] + output[zone]['status'] = zone_notes[zone]['status'] + output[zone]['job_id'] = zone_notes[zone]['job_id'] + + if zone_notes[zone]['data'] is not None: + log.debug('Data was found') + for note in zone_notes[zone]['data']: + log.debug("Analyzing zone note {0}".format(note['timestamp'])) + if int(note['timestamp']) > int(cutoff): + log.debug("Note with timestamp {0} was newer than cutoff {1}, appending output".format(note['timestamp'], cutoff)) + log.debug("log items for output: {0}".format(len(output[zone]['data']))) + output[zone]['data'].append(note) + else: + log.debug("Note with timestamp {0} older than cuttoff {1}, skipping".format(note['timestamp'], cutoff)) + + return output + + elif arg_dict['helper_action'] == 'zones': + log.info('Search zones request received') + log.debug('Getting zone list') + arg_dict['resource_action'] = 'Zone' + arg_dict['resource'] = 'Zone' + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + output = dyn.do('Zone', 'list', arg_dict, self.logger) + + log.debug('Zone list received') + + return output + + elif arg_dict['helper_action'] == 'services': + + log.info('Search services request received') + arg_dict['resource'] = arg_dict['service_type'] + + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + + log.debug('Making query to dyn') + output = dyn.do(arg_dict['service_type'], 'list', arg_dict, self.logger) + log.info('Dyn Search Query result received') + + return output \ No newline at end of file diff --git a/DynResource/dynhelpers/update.py b/DynResource/dynhelpers/update.py new file mode 100644 index 0000000..6248b92 --- /dev/null +++ b/DynResource/dynhelpers/update.py @@ -0,0 +1,79 @@ +import logging +from pprint import pprint, pformat + +from dynhelper import DynHelper +from .. import DynRestResource + + +class Update(DynHelper): + + + #inherit DynectRest init + def __init__(self, user, password, account, logger=None): + super(Update, self).__init__(user, password, account, logger) + + #if logger name is specified, log commands will log to it + if logger is not None: + parent_logger = logger + global log + log = logging.getLogger('{0}.{1}'.format(parent_logger,__name__)) + #if not, set up a NullHandler so log commands don't error + else: + log = logging.getLogger('{0}'.format(__name__)) + null_handler = logging.NullHandler() + log.addHandler(null_handler) + + def do(self, resource, arg_dict, auto_publish=True): + + log.debug('Update request started') + + #Sort out fqdn, zone, and hostname provided + arg_dict = self._get_zone_from_fqdn(arg_dict) + + self._log_request_info(resource, arg_dict) + + log.debug("Finding records that match fqdn {0}".format(arg_dict['fqdn'])) + dyn = DynRestResource(self.user, self.password, self.account, self.logger) + result_list = dyn.do(resource, 'list', arg_dict, auto_publish) + + if 'update_all' in arg_dict.keys(): + log.info('update_all flag detected') + update_all = arg_dict['update_all'] + else: + update_all = False + + if len(result_list['data']) > 1 and update_all is False: + log.warning('Multiple records found, please specify the ID with the -i flag or use the --update_all flag cautiously') + output = result_list + + elif len(result_list['data']) == 1 or update_all is True: + log.debug('Either one record was found, or update_all flag was used') + + output = {} + + for record in result_list['data']: + output[record] = {} + + get_arg_dict = arg_dict.copy() + + get_arg_dict['record_id'] = record.rsplit('/')[-1] + log.debug('Requesting detailed data for existing record') + result_get = dyn.do(resource, 'get', get_arg_dict, auto_publish) + orig_dict = result_get['data'].copy() + + update_arg_dict = get_arg_dict.copy() + update_arg_dict['resource'] = resource + + log.debug('Sending update now') + result_update = dyn.do(resource, 'update', update_arg_dict, auto_publish) + + output[record]['status'] = result_update['status'] + output[record]['data'] = { + 'original_record': orig_dict, + 'result': result_update, + } + + log.info("Request completed") + log.info(pformat(output)) + + return output diff --git a/DynResource/dynresourcemap.py b/DynResource/dynresourcemap.py new file mode 100644 index 0000000..fa5d074 --- /dev/null +++ b/DynResource/dynresourcemap.py @@ -0,0 +1,892 @@ +#This dictionary serves to map out DynectAPI resources as per their documentation https://help.dyn.com/rest-resources/ +# This map is meant to be used by the CLI and the library, so some dictionary items may pertain to the CLI only. +# The win here is that we don't need to add a new function every time we want to wrap a new API resource. Instead we just add a new dictionary item here, +# and things shoudl just work. +# +# The structure of the dictionary works as follows: +# [RESOURCE] +# --[vars] #vars are constants related to the Resource. Common use cases are GET params like 'detail=y' or Booleans +# ----[] #the var and it's value. one line for each var +# --[actions] #Actions performed with this resource. +# ----[] #Action choice - one block of these per action +# ------[url_vars] #URL vars for the resource. These are mandatory values that are used in the URI part of the REST call +# ------[method] #The method used for the REST API Call (GET,PUT,POST,DELETE) +# ------[rdata] #Rdata is the DNS data to use in the call. Set to False if there is no rdata for the resource to send +# --------[rdata_fields] #Rdata field and value pairs, one per line +# ------[options] #Options are optional items. There are also some not-so-obvious things that go on here for the CLI +# --------[