Skip to content

Commit

Permalink
Merge pull request #85 from octodns/healthcheck-protocol-supports
Browse files Browse the repository at this point in the history
Healthcheck protocol ICMP support & check for unsupported protocols (UDP)
  • Loading branch information
ross authored Jun 20, 2024
2 parents 7e215dc + ec48775 commit 61c6b02
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## v0.0.8 - 2024-??-?? -

* DNAME, DS, and TLSA record type support added.
* Validate that healthcheck protocol is supported (HTTP, HTTPS, ICMP, TCP)

## v0.0.7 - 2023-11-14 - Maintenance release

Expand Down
31 changes: 26 additions & 5 deletions octodns_ns1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ns1 import NS1
from ns1.rest.errors import RateLimitException, ResourceException

from octodns.provider import ProviderException
from octodns.provider import ProviderException, SupportsException
from octodns.provider.base import BaseProvider
from octodns.record import Create, Record, Update
from octodns.record.geo import GeoCodes
Expand Down Expand Up @@ -1029,6 +1029,17 @@ def populate(self, zone, target=False, lenient=False):
)
return exists

def _process_desired_zone(self, desired):
for record in desired.records:
if getattr(record, 'dynamic', False):
protocol = record.healthcheck_protocol
if protocol not in ('HTTP', 'HTTPS', 'ICMP', 'TCP'):
msg = f'healthcheck protocol "{protocol}" not supported'
# no workable fallbacks so straight error
raise SupportsException(f'{self.id}: {msg}')

return super()._process_desired_zone(desired)

def _params_for_geo_A(self, record):
# purposefully set non-geo answers to have an empty meta,
# so that we know we did this on purpose if/when troubleshooting
Expand Down Expand Up @@ -1235,18 +1246,28 @@ def _monitor_gen(self, record, value):
connect_timeout = self._healthcheck_connect_timeout(record)
response_timeout = self._healthcheck_response_timeout(record)

if record.healthcheck_protocol == 'TCP' or not self.use_http_monitors:
healthcheck_protocol = record.healthcheck_protocol
if healthcheck_protocol == 'ICMP':
ret['job_type'] = 'ping'
ret['config'] = {
'count': 4,
'host': value,
'interval': response_timeout * 250, # 1/4 response_timeout
'ipv6': _type == 'AAAA',
'timeout': response_timeout * 1000,
}
elif healthcheck_protocol == 'TCP' or not self.use_http_monitors:
ret['job_type'] = 'tcp'
ret['config'] = {
'host': value,
'port': record.healthcheck_port,
# TCP monitors use milliseconds, so convert from seconds to milliseconds
'connect_timeout': connect_timeout * 1000,
'response_timeout': response_timeout * 1000,
'ssl': record.healthcheck_protocol == 'HTTPS',
'ssl': healthcheck_protocol == 'HTTPS',
}

if record.healthcheck_protocol != 'TCP':
if healthcheck_protocol != 'TCP':
# legacy HTTP-emulating TCP monitor
# we need to send the HTTP request string
path = record.healthcheck_path
Expand All @@ -1268,7 +1289,7 @@ def _monitor_gen(self, record, value):
else:
# modern HTTP monitor
ret['job_type'] = 'http'
proto = record.healthcheck_protocol.lower()
proto = healthcheck_protocol.lower()
domain = f'[{value}]' if _type == 'AAAA' else value
port = record.healthcheck_port
path = record.healthcheck_path
Expand Down
63 changes: 63 additions & 0 deletions tests/test_provider_ns1.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from ns1.rest.errors import AuthException, RateLimitException, ResourceException

from octodns.provider import SupportsException
from octodns.provider.plan import Plan
from octodns.record import Delete, Record, Update
from octodns.zone import Zone
Expand Down Expand Up @@ -1254,6 +1255,33 @@ def test_monitor_gen_CNAME_http(self):
monitor = provider._monitor_gen(record, value)
self.assertTrue(value[:-1] in monitor['config']['url'])

def test_monitor_gen_ICMP(self):
provider = Ns1Provider('test', 'api-key', use_http_monitors=True)

value = '1.2.3.4'
record = self.record()
record._octodns['healthcheck']['protocol'] = 'ICMP'
monitor = provider._monitor_gen(record, value)
self.assertEqual('ping', monitor['job_type'])
self.assertEqual(
provider._healthcheck_response_timeout(record) * 1000,
monitor['config']['timeout'],
)
self.assertEqual(
provider._healthcheck_response_timeout(record) * 250,
monitor['config']['interval'],
)
self.assertFalse(monitor['config']['ipv6'])
self.assertTrue(value in monitor['config']['host'])

value = '::ffff:3.4.5.6'
record = self.aaaa_record()
record._octodns['healthcheck']['protocol'] = 'ICMP'
monitor = provider._monitor_gen(record, value)
self.assertEqual('ping', monitor['job_type'])
self.assertTrue(monitor['config']['ipv6'])
self.assertTrue(value in monitor['config']['host'])

def test_monitor_is_match(self):
provider = Ns1Provider('test', 'api-key')

Expand Down Expand Up @@ -1306,6 +1334,41 @@ def test_monitor_is_match(self):
)
)

def test_unsupported_healthcheck_protocol(self):
provider = Ns1Provider('test', 'api-key')
desired = Zone('unit.tests.', [])
record = Record.new(
desired,
'a',
{
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
'dynamic': {
'pools': {
'one': {'values': [{'value': '1.2.3.4'}]},
'two': {'values': [{'value': '2.2.3.4'}]},
},
'rules': [
{'geos': ['EU', 'NA-CA-NB', 'NA-US-OR'], 'pool': 'two'},
{'pool': 'one'},
],
},
'octodns': {'healthcheck': {'protocol': 'UDP'}},
},
lenient=True,
)
desired.add_record(record)
with self.assertRaises(SupportsException) as ctx:
provider._process_desired_zone(desired)
self.assertEqual(
'test: healthcheck protocol "UDP" not supported', str(ctx.exception)
)

record.octodns['healthcheck']['protocol'] = 'ICMP'
got = provider._process_desired_zone(desired)
self.assertEqual(got.records, desired.records)

@patch('octodns_ns1.Ns1Provider._feed_create')
@patch('octodns_ns1.Ns1Provider._monitor_delete')
@patch('octodns_ns1.Ns1Client.monitors_update')
Expand Down

0 comments on commit 61c6b02

Please sign in to comment.