mirror of
				https://github.com/github/octodns.git
				synced 2024-05-11 05:55:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			187 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #
 | |
| #
 | |
| #
 | |
| 
 | |
| from __future__ import absolute_import, division, print_function, \
 | |
|     unicode_literals
 | |
| 
 | |
| from collections import defaultdict
 | |
| from logging import getLogger
 | |
| import re
 | |
| 
 | |
| from .record import Create, Delete
 | |
| 
 | |
| 
 | |
| class SubzoneRecordException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class DuplicateRecordException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class InvalidNodeException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| def _is_eligible(record):
 | |
|     # Should this record be considered when computing changes
 | |
|     # We ignore all top-level NS records
 | |
|     return record._type != 'NS' or record.name != ''
 | |
| 
 | |
| 
 | |
| class Zone(object):
 | |
|     log = getLogger('Zone')
 | |
| 
 | |
|     def __init__(self, name, sub_zones):
 | |
|         if not name[-1] == '.':
 | |
|             raise Exception('Invalid zone name {}, missing ending dot'
 | |
|                             .format(name))
 | |
|         # Force everything to lowercase just to be safe
 | |
|         self.name = unicode(name).lower() if name else name
 | |
|         self.sub_zones = sub_zones
 | |
|         # We're grouping by node, it allows us to efficiently search for
 | |
|         # duplicates and detect when CNAMEs co-exist with other records
 | |
|         self._records = defaultdict(set)
 | |
|         # optional leading . to match empty hostname
 | |
|         # optional trailing . b/c some sources don't have it on their fqdn
 | |
|         self._name_re = re.compile(r'\.?{}?$'.format(name))
 | |
| 
 | |
|         self.log.debug('__init__: zone=%s, sub_zones=%s', self, sub_zones)
 | |
| 
 | |
|     @property
 | |
|     def records(self):
 | |
|         return set([r for _, node in self._records.items() for r in node])
 | |
| 
 | |
|     def hostname_from_fqdn(self, fqdn):
 | |
|         return self._name_re.sub('', fqdn)
 | |
| 
 | |
|     def add_record(self, record, replace=False, lenient=False):
 | |
|         name = record.name
 | |
|         last = name.split('.')[-1]
 | |
| 
 | |
|         if not lenient and last in self.sub_zones:
 | |
|             if name != last:
 | |
|                 # it's a record for something under a sub-zone
 | |
|                 raise SubzoneRecordException('Record {} is under a '
 | |
|                                              'managed subzone'
 | |
|                                              .format(record.fqdn))
 | |
|             elif record._type != 'NS':
 | |
|                 # It's a non NS record for exactly a sub-zone
 | |
|                 raise SubzoneRecordException('Record {} a managed sub-zone '
 | |
|                                              'and not of type NS'
 | |
|                                              .format(record.fqdn))
 | |
| 
 | |
|         if replace:
 | |
|             # will remove it if it exists
 | |
|             self._records[name].discard(record)
 | |
| 
 | |
|         node = self._records[name]
 | |
|         if record in node:
 | |
|             # We already have a record at this node of this type
 | |
|             raise DuplicateRecordException('Duplicate record {}, type {}'
 | |
|                                            .format(record.fqdn,
 | |
|                                                    record._type))
 | |
|         elif not lenient and (((record._type == 'CNAME' and len(node) > 0) or
 | |
|                                ('CNAME' in map(lambda r: r._type, node)))):
 | |
|             # We're adding a CNAME to existing records or adding to an existing
 | |
|             # CNAME
 | |
|             raise InvalidNodeException('Invalid state, CNAME at {} cannot '
 | |
|                                        'coexist with other records'
 | |
|                                        .format(record.fqdn))
 | |
| 
 | |
|         node.add(record)
 | |
| 
 | |
|     def _remove_record(self, record):
 | |
|         'Only for use in tests'
 | |
|         self._records[record.name].discard(record)
 | |
| 
 | |
|     def changes(self, desired, target):
 | |
|         self.log.debug('changes: zone=%s, target=%s', self, target)
 | |
| 
 | |
|         # Build up a hash of the desired records, thanks to our special
 | |
|         # __hash__ and __cmp__ on Record we'll be able to look up records that
 | |
|         # match name and _type with it
 | |
|         desired_records = {r: r for r in desired.records}
 | |
| 
 | |
|         changes = []
 | |
| 
 | |
|         # Find diffs & removes
 | |
|         for record in filter(_is_eligible, self.records):
 | |
|             if record.ignored:
 | |
|                 continue
 | |
|             elif len(record.included) > 0 and \
 | |
|                     target.id not in record.included:
 | |
|                 self.log.debug('changes:  skipping record=%s %s - %s not'
 | |
|                                ' included ', record.fqdn, record._type,
 | |
|                                target.id)
 | |
|                 continue
 | |
|             elif target.id in record.excluded:
 | |
|                 self.log.debug('changes:  skipping record=%s %s - %s '
 | |
|                                'excluded ', record.fqdn, record._type,
 | |
|                                target.id)
 | |
|                 continue
 | |
|             try:
 | |
|                 desired_record = desired_records[record]
 | |
|                 if desired_record.ignored:
 | |
|                     continue
 | |
|                 elif len(desired_record.included) > 0 and \
 | |
|                         target.id not in desired_record.included:
 | |
|                     self.log.debug('changes:  skipping record=%s %s - %s'
 | |
|                                    'not included ', record.fqdn, record._type,
 | |
|                                    target.id)
 | |
|                     continue
 | |
|                 elif target.id in desired_record.excluded:
 | |
|                     continue
 | |
|             except KeyError:
 | |
|                 if not target.supports(record):
 | |
|                     self.log.debug('changes:  skipping record=%s %s - %s does '
 | |
|                                    'not support it', record.fqdn, record._type,
 | |
|                                    target.id)
 | |
|                     continue
 | |
|                 # record has been removed
 | |
|                 self.log.debug('changes: zone=%s, removed record=%s', self,
 | |
|                                record)
 | |
|                 changes.append(Delete(record))
 | |
|             else:
 | |
|                 change = record.changes(desired_record, target)
 | |
|                 if change:
 | |
|                     self.log.debug('changes: zone=%s, modified\n'
 | |
|                                    '    existing=%s,\n     desired=%s', self,
 | |
|                                    record, desired_record)
 | |
|                     changes.append(change)
 | |
|                 else:
 | |
|                     self.log.debug('changes: zone=%s, n.c. record=%s', self,
 | |
|                                    record)
 | |
| 
 | |
|         # Find additions, things that are in desired, but missing in ourselves.
 | |
|         # This uses set math and our special __hash__ and __cmp__ functions as
 | |
|         # well
 | |
|         for record in filter(_is_eligible, desired.records - self.records):
 | |
|             if record.ignored:
 | |
|                 continue
 | |
|             elif len(record.included) > 0 and \
 | |
|                     target.id not in record.included:
 | |
|                 self.log.debug('changes:  skipping record=%s %s - %s not'
 | |
|                                ' included ', record.fqdn, record._type,
 | |
|                                target.id)
 | |
|                 continue
 | |
|             elif target.id in record.excluded:
 | |
|                 self.log.debug('changes:  skipping record=%s %s - %s '
 | |
|                                'excluded ', record.fqdn, record._type,
 | |
|                                target.id)
 | |
|                 continue
 | |
| 
 | |
|             if not target.supports(record):
 | |
|                 self.log.debug('changes:  skipping record=%s %s - %s does not '
 | |
|                                'support it', record.fqdn, record._type,
 | |
|                                target.id)
 | |
|                 continue
 | |
|             self.log.debug('changes: zone=%s, create record=%s', self, record)
 | |
|             changes.append(Create(record))
 | |
| 
 | |
|         return changes
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return 'Zone<{}>'.format(self.name)
 |