Skip to content

Commit

Permalink
implement retries for ipify with backoff
Browse files Browse the repository at this point in the history
add tests
  • Loading branch information
crazystick committed Mar 22, 2020
1 parent 6b71483 commit 4eedf2d
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 29 deletions.
82 changes: 53 additions & 29 deletions dynamic_cloud_dns_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
from time import sleep
import logging
import os
import backoff

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

IPIFY4 = 'https://api.ipify.org?format=json'
IPIFY6 = 'https://api6.ipify.org?format=json'
IPIFY4 = 'https://api.ipify.org/?format=json'
IPIFY6 = 'https://api6.ipify.org/?format=json'

RETRIES = 5

@backoff.on_exception(backoff.expo,
(requests.exceptions.Timeout,
requests.exceptions.ConnectionError),
max_tries=RETRIES)
def get_ipv4():
logger.debug("call ipv4")
r = requests.get(IPIFY4)
r.raise_for_status()
return r.json()['ip']

@backoff.on_exception(backoff.expo,
(requests.exceptions.Timeout,
requests.exceptions.ConnectionError),
max_tries=RETRIES)
def get_ipv6():
r = requests.get(IPIFY6)
r.raise_for_status()
Expand Down Expand Up @@ -41,39 +53,51 @@ def update_cloud_dns(ipv4=None, ipv6=None):
logger.info("IP addresses updated: IPv4={} IPv6={}".format(ipv4, ipv6))


if __name__ == "__main__":
current_ipv4 = None
current_ipv6 = None
def do_update(current_ipv4, current_ipv6):
ipv4 = None
ipv6 = None

while True:
if os.environ['DCDNS_IPV4'] == 'YES':
try:
ipv4 = get_ipv4()
if ipv4 == current_ipv4:
ipv4 = None
else:
current_ipv4 = ipv4
except requests.exceptions.HTTPError:
retries = 1

if os.environ['DCDNS_IPV4'] == 'YES':
try:
ipv4 = get_ipv4()
if ipv4 == current_ipv4:
ipv4 = None
else:
current_ipv4 = ipv4
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError):
ipv4 = None
except requests.exceptions.HTTPError as err:
logger.exception(err)

if os.environ['DCDNS_IPV6'] == 'YES':
try:
ipv6 = get_ipv6()
if ipv6 == current_ipv6:
ipv6 = None
else:
current_ipv6 = ipv6
except requests.exceptions.HTTPError:
ipv6 = None

if os.environ['DCDNS_IPV6'] == 'YES':
try:
update_cloud_dns(ipv4=ipv4, ipv6=ipv6)
except ValueError:
logger.info("No IP address updates")
ipv6 = get_ipv6()
if ipv6 == current_ipv6:
ipv6 = None
else:
current_ipv6 = ipv6
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError):
ipv6 = None
except requests.exceptions.HTTPError as err:
logger.exception(err)

sleep(int(os.environ['DCDNS_FREQUENCY']))
try:
update_cloud_dns(ipv4=ipv4, ipv6=ipv6)
except ValueError:
logger.info("No IP address updates")
except requests.exceptions.HTTPError as err:
logger.exception(err)

return current_ipv4, current_ipv6


if __name__ == "__main__":
current_ipv4 = None
current_ipv6 = None

while True:
current_ipv4, current_ipv6 = do_update(current_ipv4, current_ipv6)
sleep(int(os.environ['DCDNS_FREQUENCY']))
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
requests
backoff==1.10.0
pytest
requests-mock
131 changes: 131 additions & 0 deletions test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@

from dynamic_cloud_dns_client import *

def test_get_ipv4(requests_mock):
# given
requests_mock.get(IPIFY4, json={'ip': '0.0.0.0'})

# when
ip = get_ipv4()

# then
assert ip == '0.0.0.0'

def test_get_ipv6(requests_mock):
# given
requests_mock.get(IPIFY6, json={'ip': '2001:0db8:0000:0000:0000:8a2e:0370:7334'})

# when
ip = get_ipv6()

# then
assert ip == '2001:0db8:0000:0000:0000:8a2e:0370:7334'

def test_main_no_update(requests_mock, monkeypatch):
# given
monkeypatch.setenv("DCDNS_IPV4", "YES")
monkeypatch.setenv("DCDNS_IPV6", "YES")
monkeypatch.setenv("DCDNS_TOKEN", "TEST_TOKEN")
monkeypatch.setenv("DCDNS_HOST", "TEST_HOST")
monkeypatch.setenv("DCDNS_ZONE", "TEST_ZONE")
monkeypatch.setenv("DCDNS_FUNCTION_URL", "https://cloudfunctions.net.mock/updateHost")
requests_mock.get(IPIFY4, json={'ip': '0.0.0.0'})
requests_mock.get(IPIFY6, json={'ip': '2001:0db8:0000:0000:0000:8a2e:0370:7334'})
requests_mock.post("https://cloudfunctions.net.mock/updateHost")

# when
ipv4, ipv6 = do_update('0.0.0.0', '2001:0db8:0000:0000:0000:8a2e:0370:7334')

# then
assert ipv4 == '0.0.0.0'
assert ipv6 == '2001:0db8:0000:0000:0000:8a2e:0370:7334'
assert requests_mock.call_count == 2
assert requests_mock.request_history[0].url == IPIFY4
assert requests_mock.request_history[1].url == IPIFY6

def test_main_does_update(requests_mock, monkeypatch):
# given
monkeypatch.setenv("DCDNS_IPV4", "YES")
monkeypatch.setenv("DCDNS_IPV6", "YES")
monkeypatch.setenv("DCDNS_TOKEN", "TEST_TOKEN")
monkeypatch.setenv("DCDNS_HOST", "TEST_HOST")
monkeypatch.setenv("DCDNS_ZONE", "TEST_ZONE")
monkeypatch.setenv("DCDNS_FUNCTION_URL", "https://cloudfunctions.net.mock/updateHost")
requests_mock.get(IPIFY4, json={'ip': '1.2.3.4'})
requests_mock.get(IPIFY6, json={'ip': '2001:0db8:9999:0000:0000:8a2e:0370:7334'})
requests_mock.post("https://cloudfunctions.net.mock/updateHost")

# when
ipv4, ipv6 = do_update('0.0.0.0', '2001:0db8:0000:0000:0000:8a2e:0370:7334')

# then
assert ipv4 == '1.2.3.4'
assert ipv6 == '2001:0db8:9999:0000:0000:8a2e:0370:7334'
assert requests_mock.call_count == 3
assert requests_mock.request_history[0].url == IPIFY4
assert requests_mock.request_history[1].url == IPIFY6
assert requests_mock.request_history[2].url == "https://cloudfunctions.net.mock/updateHost"

def test_main_single_retry(requests_mock, monkeypatch):
# given
monkeypatch.setenv("DCDNS_IPV4", "YES")
monkeypatch.setenv("DCDNS_IPV6", "YES")
monkeypatch.setenv("DCDNS_TOKEN", "TEST_TOKEN")
monkeypatch.setenv("DCDNS_HOST", "TEST_HOST")
monkeypatch.setenv("DCDNS_ZONE", "TEST_ZONE")
monkeypatch.setenv("DCDNS_FUNCTION_URL", "https://cloudfunctions.net.mock/updateHost")
requests_mock.get(IPIFY4, [{'exc': requests.exceptions.ConnectTimeout},{'json': {'ip': '1.2.3.4'}}])
requests_mock.get(IPIFY6, json={'ip': '2001:0db8:9999:0000:0000:8a2e:0370:7334'})
requests_mock.post("https://cloudfunctions.net.mock/updateHost")

# when
ipv4, ipv6 = do_update('0.0.0.0', '2001:0db8:0000:0000:0000:8a2e:0370:7334')

# then
assert ipv4 == '1.2.3.4'
assert ipv6 == '2001:0db8:9999:0000:0000:8a2e:0370:7334'
assert requests_mock.call_count == 4
assert requests_mock.request_history[0].url == IPIFY4
assert requests_mock.request_history[1].url == IPIFY4
assert requests_mock.request_history[2].url == IPIFY6
assert requests_mock.request_history[3].url == "https://cloudfunctions.net.mock/updateHost"

def test_main_max_retry(requests_mock, monkeypatch):
# given
monkeypatch.setenv("DCDNS_IPV4", "YES")
monkeypatch.setenv("DCDNS_IPV6", "YES")
monkeypatch.setenv("DCDNS_TOKEN", "TEST_TOKEN")
monkeypatch.setenv("DCDNS_HOST", "TEST_HOST")
monkeypatch.setenv("DCDNS_ZONE", "TEST_ZONE")
monkeypatch.setenv("DCDNS_FUNCTION_URL", "https://cloudfunctions.net.mock/updateHost")
requests_mock.get(IPIFY4, [
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout}])
requests_mock.get(IPIFY6, [
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout},
{'exc': requests.exceptions.ConnectTimeout}])
requests_mock.post("https://cloudfunctions.net.mock/updateHost")

# when
ipv4, ipv6 = do_update('0.0.0.0', '2001:0db8:0000:0000:0000:8a2e:0370:7334')

# then
assert ipv4 == '0.0.0.0'
assert ipv6 == '2001:0db8:0000:0000:0000:8a2e:0370:7334'
assert requests_mock.call_count == 10
assert requests_mock.request_history[0].url == IPIFY4
assert requests_mock.request_history[1].url == IPIFY4
assert requests_mock.request_history[2].url == IPIFY4
assert requests_mock.request_history[3].url == IPIFY4
assert requests_mock.request_history[4].url == IPIFY4
assert requests_mock.request_history[5].url == IPIFY6
assert requests_mock.request_history[6].url == IPIFY6
assert requests_mock.request_history[7].url == IPIFY6
assert requests_mock.request_history[8].url == IPIFY6
assert requests_mock.request_history[9].url == IPIFY6

0 comments on commit 4eedf2d

Please sign in to comment.