Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Healthcheck protocol ICMP support & check for unsupported protocols (UDP) #85

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading