1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Merge branch 'master' into log-version

This commit is contained in:
Ross McFarland
2022-03-27 11:56:33 -07:00
committed by GitHub
9 changed files with 210 additions and 66 deletions

View File

@@ -24,3 +24,15 @@ jobs:
- name: CI Build
run: |
./script/cibuild
setup-py:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup python
uses: actions/setup-python@v2
with:
python-version: '3.10'
architecture: x64
- name: CI setup.py
run: |
./script/cibuild-setup-py

View File

@@ -13,6 +13,8 @@
just standardizes what they are doing as many other providers appear to need
to do so, but weren't. There was an ordering before, but it was essentially
arbitrarily picked.
* Record.register_type added so that providers can register custom record
types, see [docs/records.md](docs/records.md) for more information
#### Stuff

View File

@@ -124,10 +124,10 @@ If you'd like to enable lenience for a whole zone you can do so with the followi
#### Restrict Record manipulations
OctoDNS currently provides the ability to limit the number of updates/deletes on
OctoDNS currently provides the ability to limit the number of updates/deletes on
DNS records by configuring a percentage of allowed operations as a threshold.
If left unconfigured, suitable defaults take over instead. In the below example,
the Dyn provider is configured with limits of 40% on both update and
If left unconfigured, suitable defaults take over instead. In the below example,
the Dyn provider is configured with limits of 40% on both update and
delete operations over all the records present.
````yaml
@@ -136,3 +136,42 @@ dyn:
update_pcent_threshold: 0.4
delete_pcent_threshold: 0.4
````
## Provider specific record types
### Creating and registering
octoDNS has support for provider specific record types through a dynamic type registration system. This functionality is powered by `Route.register_type` and can be used as follows.
```python
class _SpecificValue(object):
...
class SomeProviderSpecificRecord(ValuesMixin, Record):
_type = 'SomeProvider/SPECIFIC'
_value_type = _SpecificValue
Record.register_type(SomeProviderSpecificRecord)
```
Have a look in [octodns.record](/octodns/record/__init__.py) for examples of how records are implemented. `NsRecord` and `_NsValue` are fairly simple examples to start with. You can also take a look at [`Route53Provider`'s `Route53Provider/ALIAS` type](https://github.com/octodns/octodns-route53/blob/main/octodns_route53/record.py).
In general this support is intended for record types that only make sense for a single provider. If multiple providers have a similar record it may make sense to implement it in octoDNS core.
### Naming
By convention the record type should be prefix with the provider class, e.g. `Route53Provider` followed by a `/` and an all-caps record type name `ALIAS`, e.g. `Route53Provider/ALIAS`.
### YamlProvider support
Once the type is registered `YamlProvider` will automatically gain support for it and they can be included in your zone yaml files.
```yaml
alias:
type: Route53Provider/ALIAS
values:
- name: www
type: A
- name: www
type: AAAA
```

View File

@@ -64,7 +64,11 @@ class Delete(Change):
return f'Delete {self.existing}'
class ValidationError(Exception):
class RecordException(Exception):
pass
class ValidationError(RecordException):
@classmethod
def build_message(cls, fqdn, reasons):
@@ -80,6 +84,20 @@ class ValidationError(Exception):
class Record(EqualityTupleMixin):
log = getLogger('Record')
_CLASSES = {}
@classmethod
def register_type(cls, _class, _type=None):
if _type is None:
_type = _class._type
existing = cls._CLASSES.get(_type)
if existing:
module = existing.__module__
name = existing.__name__
msg = f'Type "{_type}" already registered by {module}.{name}'
raise RecordException(msg)
cls._CLASSES[_type] = _class
@classmethod
def new(cls, zone, name, data, source=None, lenient=False):
name = str(name)
@@ -89,24 +107,7 @@ class Record(EqualityTupleMixin):
except KeyError:
raise Exception(f'Invalid record {fqdn}, missing type')
try:
_class = {
'A': ARecord,
'AAAA': AaaaRecord,
'ALIAS': AliasRecord,
'CAA': CaaRecord,
'CNAME': CnameRecord,
'DNAME': DnameRecord,
'LOC': LocRecord,
'MX': MxRecord,
'NAPTR': NaptrRecord,
'NS': NsRecord,
'PTR': PtrRecord,
'SPF': SpfRecord,
'SRV': SrvRecord,
'SSHFP': SshfpRecord,
'TXT': TxtRecord,
'URLFWD': UrlfwdRecord,
}[_type]
_class = cls._CLASSES[_type]
except KeyError:
raise Exception(f'Unknown record type: "{_type}"')
reasons = _class.validate(name, fqdn, data)
@@ -284,11 +285,11 @@ class GeoValue(EqualityTupleMixin):
"{self.subdivision_code} {self.values}'"
class _ValuesMixin(object):
class ValuesMixin(object):
@classmethod
def validate(cls, name, fqdn, data):
reasons = super(_ValuesMixin, cls).validate(name, fqdn, data)
reasons = super(ValuesMixin, cls).validate(name, fqdn, data)
values = data.get('values', data.get('value', []))
@@ -297,7 +298,7 @@ class _ValuesMixin(object):
return reasons
def __init__(self, zone, name, data, source=None):
super(_ValuesMixin, self).__init__(zone, name, data, source=source)
super(ValuesMixin, self).__init__(zone, name, data, source=source)
try:
values = data['values']
except KeyError:
@@ -307,10 +308,10 @@ class _ValuesMixin(object):
def changes(self, other, target):
if self.values != other.values:
return Update(self, other)
return super(_ValuesMixin, self).changes(other, target)
return super(ValuesMixin, self).changes(other, target)
def _data(self):
ret = super(_ValuesMixin, self)._data()
ret = super(ValuesMixin, self)._data()
if len(self.values) > 1:
values = [getattr(v, 'data', v) for v in self.values if v]
if len(values) > 1:
@@ -330,7 +331,7 @@ class _ValuesMixin(object):
return f"<{klass} {self._type} {self.ttl}, {self.fqdn}, ['{values}']>"
class _GeoMixin(_ValuesMixin):
class _GeoMixin(ValuesMixin):
'''
Adds GeoDNS support to a record.
@@ -381,26 +382,26 @@ class _GeoMixin(_ValuesMixin):
return super(_GeoMixin, self).__repr__()
class _ValueMixin(object):
class ValueMixin(object):
@classmethod
def validate(cls, name, fqdn, data):
reasons = super(_ValueMixin, cls).validate(name, fqdn, data)
reasons = super(ValueMixin, cls).validate(name, fqdn, data)
reasons.extend(cls._value_type.validate(data.get('value', None),
cls._type))
return reasons
def __init__(self, zone, name, data, source=None):
super(_ValueMixin, self).__init__(zone, name, data, source=source)
super(ValueMixin, self).__init__(zone, name, data, source=source)
self.value = self._value_type.process(data['value'])
def changes(self, other, target):
if self.value != other.value:
return Update(self, other)
return super(_ValueMixin, self).changes(other, target)
return super(ValueMixin, self).changes(other, target)
def _data(self):
ret = super(_ValueMixin, self)._data()
ret = super(ValueMixin, self)._data()
if self.value:
ret['value'] = getattr(self.value, 'data', self.value)
return ret
@@ -804,16 +805,22 @@ class ARecord(_DynamicMixin, _GeoMixin, Record):
_value_type = Ipv4List
Record.register_type(ARecord)
class AaaaRecord(_DynamicMixin, _GeoMixin, Record):
_type = 'AAAA'
_value_type = Ipv6List
Record.register_type(AaaaRecord)
class AliasValue(_TargetValue):
pass
class AliasRecord(_ValueMixin, Record):
class AliasRecord(ValueMixin, Record):
_type = 'ALIAS'
_value_type = AliasValue
@@ -826,6 +833,9 @@ class AliasRecord(_ValueMixin, Record):
return reasons
Record.register_type(AliasRecord)
class CaaValue(EqualityTupleMixin):
# https://tools.ietf.org/html/rfc6844#page-5
@@ -872,12 +882,15 @@ class CaaValue(EqualityTupleMixin):
return f'{self.flags} {self.tag} "{self.value}"'
class CaaRecord(_ValuesMixin, Record):
class CaaRecord(ValuesMixin, Record):
_type = 'CAA'
_value_type = CaaValue
class CnameRecord(_DynamicMixin, _ValueMixin, Record):
Record.register_type(CaaRecord)
class CnameRecord(_DynamicMixin, ValueMixin, Record):
_type = 'CNAME'
_value_type = CnameValue
@@ -890,11 +903,17 @@ class CnameRecord(_DynamicMixin, _ValueMixin, Record):
return reasons
class DnameRecord(_DynamicMixin, _ValueMixin, Record):
Record.register_type(CnameRecord)
class DnameRecord(_DynamicMixin, ValueMixin, Record):
_type = 'DNAME'
_value_type = DnameValue
Record.register_type(DnameRecord)
class LocValue(EqualityTupleMixin):
# TODO: work out how to do defaults per RFC
@@ -1066,11 +1085,14 @@ class LocValue(EqualityTupleMixin):
f"{self.precision_horz:.2f}m {self.precision_vert:.2f}m'"
class LocRecord(_ValuesMixin, Record):
class LocRecord(ValuesMixin, Record):
_type = 'LOC'
_value_type = LocValue
Record.register_type(LocRecord)
class MxValue(EqualityTupleMixin):
@classmethod
@@ -1136,11 +1158,14 @@ class MxValue(EqualityTupleMixin):
return f"'{self.preference} {self.exchange}'"
class MxRecord(_ValuesMixin, Record):
class MxRecord(ValuesMixin, Record):
_type = 'MX'
_value_type = MxValue
Record.register_type(MxRecord)
class NaptrValue(EqualityTupleMixin):
VALID_FLAGS = ('S', 'A', 'U', 'P')
@@ -1214,11 +1239,14 @@ class NaptrValue(EqualityTupleMixin):
f"\"{regexp}\" {self.replacement}'"
class NaptrRecord(_ValuesMixin, Record):
class NaptrRecord(ValuesMixin, Record):
_type = 'NAPTR'
_value_type = NaptrValue
Record.register_type(NaptrRecord)
class _NsValue(object):
@classmethod
@@ -1241,11 +1269,14 @@ class _NsValue(object):
return values
class NsRecord(_ValuesMixin, Record):
class NsRecord(ValuesMixin, Record):
_type = 'NS'
_value_type = _NsValue
Record.register_type(NsRecord)
class PtrValue(_TargetValue):
@classmethod
@@ -1268,7 +1299,7 @@ class PtrValue(_TargetValue):
return [super(PtrValue, cls).process(v) for v in values]
class PtrRecord(_ValuesMixin, Record):
class PtrRecord(ValuesMixin, Record):
_type = 'PTR'
_value_type = PtrValue
@@ -1279,6 +1310,9 @@ class PtrRecord(_ValuesMixin, Record):
return self.values[0]
Record.register_type(PtrRecord)
class SshfpValue(EqualityTupleMixin):
VALID_ALGORITHMS = (1, 2, 3, 4)
VALID_FINGERPRINT_TYPES = (1, 2)
@@ -1338,12 +1372,15 @@ class SshfpValue(EqualityTupleMixin):
return f"'{self.algorithm} {self.fingerprint_type} {self.fingerprint}'"
class SshfpRecord(_ValuesMixin, Record):
class SshfpRecord(ValuesMixin, Record):
_type = 'SSHFP'
_value_type = SshfpValue
class _ChunkedValuesMixin(_ValuesMixin):
Record.register_type(SshfpRecord)
class _ChunkedValuesMixin(ValuesMixin):
CHUNK_SIZE = 255
_unescaped_semicolon_re = re.compile(r'\w;')
@@ -1392,6 +1429,9 @@ class SpfRecord(_ChunkedValuesMixin, Record):
_value_type = _ChunkedValue
Record.register_type(SpfRecord)
class SrvValue(EqualityTupleMixin):
@classmethod
@@ -1460,7 +1500,7 @@ class SrvValue(EqualityTupleMixin):
return f"'{self.priority} {self.weight} {self.port} {self.target}'"
class SrvRecord(_ValuesMixin, Record):
class SrvRecord(ValuesMixin, Record):
_type = 'SRV'
_value_type = SrvValue
_name_re = re.compile(r'^(\*|_[^\.]+)\.[^\.]+')
@@ -1474,6 +1514,9 @@ class SrvRecord(_ValuesMixin, Record):
return reasons
Record.register_type(SrvRecord)
class _TxtValue(_ChunkedValue):
pass
@@ -1483,6 +1526,9 @@ class TxtRecord(_ChunkedValuesMixin, Record):
_value_type = _TxtValue
Record.register_type(TxtRecord)
class UrlfwdValue(EqualityTupleMixin):
VALID_CODES = (301, 302)
VALID_MASKS = (0, 1, 2)
@@ -1555,6 +1601,9 @@ class UrlfwdValue(EqualityTupleMixin):
f'{self.masking} {self.query}'
class UrlfwdRecord(_ValuesMixin, Record):
class UrlfwdRecord(ValuesMixin, Record):
_type = 'URLFWD'
_value_type = UrlfwdValue
Record.register_type(UrlfwdRecord)

View File

@@ -26,16 +26,4 @@ echo "## lint ##################################################################
script/lint
echo "## tests/coverage ##############################################################"
script/coverage
echo "## validate setup.py build #####################################################"
python setup.py build
echo "## validate setup.py install ###################################################"
deactivate
TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX)
python3 -m venv $TMP_DIR
. "$TMP_DIR/bin/activate"
python setup.py install
octodns-sync --help
echo "## validate tests can run against installed code ###############################"
pip install pytest pytest-network
pytest --disable-network
echo "## complete ####################################################################"

20
script/cibuild-setup-py Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -e
cd "$(dirname "$0")/.."
echo "## create test venv ############################################################"
TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX)
python3 -m venv $TMP_DIR
. "$TMP_DIR/bin/activate"
echo "## environment & versions ######################################################"
python --version
pip --version
echo "## validate setup.py build #####################################################"
python setup.py build
echo "## validate setup.py install ###################################################"
python setup.py install
echo "## validate tests can run against installed code ###############################"
pip install pytest pytest-network
pytest --disable-network
echo "## complete ####################################################################"

View File

@@ -16,6 +16,9 @@ if [ ! -f "$ACTIVATE" ]; then
fi
. "$ACTIVATE"
# Set so that setup.py will create a public release style version number
export OCTODNS_RELEASE=1
VERSION="$(grep __VERSION__ "$ROOT/octodns/__init__.py" | sed -e "s/.* = '//" -e "s/'$//")"
git tag -s "v$VERSION" -m "Release $VERSION"

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env python
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from io import StringIO
from os import environ
from os.path import dirname, join
from subprocess import CalledProcessError, check_output
import octodns
try:
@@ -55,6 +54,19 @@ def long_description():
return buf.getvalue()
def version():
# pep440 style public & local version numbers
if environ.get('OCTODNS_RELEASE', False):
# public
return octodns.__VERSION__
try:
sha = check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')[:8]
except (CalledProcessError, FileNotFoundError):
sha = 'unknown'
# local
return f'{octodns.__VERSION__}+{sha}'
tests_require = (
'pytest>=6.2.5',
'pytest-cov>=3.0.0',
@@ -94,5 +106,5 @@ setup(
python_requires='>=3.6',
tests_require=tests_require,
url='https://github.com/octodns/octodns',
version=octodns.__VERSION__,
version=version(),
)

View File

@@ -10,9 +10,10 @@ from unittest import TestCase
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
CaaValue, CnameRecord, DnameRecord, Create, Delete, GeoValue, LocRecord, \
LocValue, MxRecord, MxValue, NaptrRecord, NaptrValue, NsRecord, \
PtrRecord, Record, SshfpRecord, SshfpValue, SpfRecord, SrvRecord, \
SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, ValidationError, \
_Dynamic, _DynamicPool, _DynamicRule
PtrRecord, Record, RecordException, SshfpRecord, SshfpValue, SpfRecord, \
SrvRecord, SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, \
ValidationError, _Dynamic, _DynamicPool, _DynamicRule, _NsValue, \
ValuesMixin
from octodns.zone import Zone
from helpers import DynamicProvider, GeoProvider, SimpleProvider
@@ -21,6 +22,24 @@ from helpers import DynamicProvider, GeoProvider, SimpleProvider
class TestRecord(TestCase):
zone = Zone('unit.tests.', [])
def test_registration(self):
with self.assertRaises(RecordException) as ctx:
Record.register_type(None, 'A')
self.assertEqual('Type "A" already registered by '
'octodns.record.ARecord', str(ctx.exception))
class AaRecord(ValuesMixin, Record):
_type = 'AA'
_value_type = _NsValue
Record.register_type(AaRecord)
aa = Record.new(self.zone, 'registered', {
'ttl': 360,
'type': 'AA',
'value': 'does.not.matter.',
})
self.assertEqual(AaRecord, aa.__class__)
def test_lowering(self):
record = ARecord(self.zone, 'MiXeDcAsE', {
'ttl': 30,