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:
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
20
script/cibuild-setup-py
Executable 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 ####################################################################"
|
||||
@@ -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"
|
||||
|
||||
22
setup.py
22
setup.py
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user