CloudFlare Dynamic DNS Update Script in Python 3












2














I have a computer that roams, but needs to have an FQDN attached to it for certain reasons such as email system integrations or testing email via a testing SMTP server that requires valid FQDNs and HELOs.



My domain(s) are all on CloudFlare, so I wrote an adapted version of another script I had to wrap around CloudFlare's API so I can update DNS entries and such.



This has a few requirements from PyPI:




  • ipaddress

  • CloudFlare Python wrapper (cloudflare on PyPI)


This script also has two other requirements to really function, but I can guarantee you that both of these components work:




  • WhatIsMyIP.com API key for IP lookup capabilities

  • CloudFlare Account with API key


Note that any sensitive information (such as login credentials or API keys) have been obfuscated in the below code. Additional bits can be provided as needed.



(There is a known limitation that this does not work for IPv6 addresses - I'm working on adding this, but this current iteration of the script does not have IPv6 in it.)



Critiques to improve the script are welcome, but keep in mind that I abide by PEP8 about linelengths <= 120 chars long because this is permissible if users on a team/development group agree on the longer length.



#!/usr/bin/python3

import CloudFlare
import ipaddress
import json
import shlex
import subprocess as sp
import syslog
import urllib.error
import urllib.request

from typing import AnyStr, Optional


# Used for `dig` queries because we are using CloudFlare, and we need actual DNS results, not CF results for checking
# the existence of an IP address currently. Therefore, we use Google DNS here.
DNS_NAMESERVER = "8.8.8.8"

# ZONE = root domain
# DOMAIN = hostname within root domain.
ZONE = "domain.tld"
DOMAIN = "subdomain"

# These next two are for WHATISMYIP - API Endpoints.
WHATISMYIP = "https://api.whatismyip.com/ip.php?key={key}&output=json"
API_KEYS = ['WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ']


# Wrapper function around syslog to allow default priority of INFO, but
# has the ability to change the priority if wished for a given message.
def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
syslog.syslog(priority, message)


# Horribly unnecessary wrapper function around `exit` which calls the
# "Process Ended" log message, and then actually exists with the given
# exit code (by default it exits on 0 - the "Success" exit code)
def _exit(code=0):
# type: (int) -> None
_syslog("DDNS Update Process Ended.")
exit(code)


# Singular Error handler for EmptyDNSResponse (could probably be a bare
# LookupError, but EmptyDNSResponse is nicer...)
class EmptyDNSResponse(LookupError):
pass # No changes from base LookupError


# Get current public IP address with WhatIsMyIP.com API
def _get_current_ip_address():
# type: () -> Optional[AnyStr]
for key in API_KEYS:
_syslog("Attempting lookup with API key {key}...".format(key=key))
try:
with urllib.request.urlopen(WHATISMYIP.format(key=key)) as req:
data = json.loads(req.read().decode("UTF-8"))
ipaddr = data['ip_address'][1]['result']
except (urllib.error.URLError, urllib.error.HTTPError):
_syslog("Could not look up public IP address, aborting update process.")
_exit(1)

try:
# noinspection PyUnboundLocalVariable
ipaddress.ip_address(ipaddr)
except ValueError:
if data is '0':
_syslog("API key was not entered for lookup, this is a programming error.", syslog.LOG_CRIT)
_exit(5)

if data in ['1', '2']:
_syslog("API Key Invalid or Inactive, attempting to try other keys...", syslog.LOG_WARNING)

if data is '3':
_syslog("API key lookup threshold reached, skipping to next API key...", syslog.LOG_WARNING)

if data in ['4', '5']:
_syslog("Query is bad, it needs 'input', which we can't do. This is a critical issue.",
syslog.LOG_CRIT)
_exit(6)

if data is '6':
_syslog("There was an unknown error with the WhatIsMyIP API, contact their support.",
syslog.LOG_CRIT)
_exit(7)

continue # Try next API key

return data


# Check if the DNS entry for a given hostname differs from current IP,
# and if it has no A record or it differs, return "True". Otherwise,
# return False, and assume the IP address doesn't differ.
def _dns_ip_address_status(host: AnyStr, curip: Optional[AnyStr] = None) -> AnyStr:

if not curip:
raise RuntimeError("Empty IP!")

dnsip = ""
try:
dnsip = sp.check_output(
shlex.split('dig +short @{nameserver} A {hostname}'.format(nameserver=DNS_NAMESERVER, hostname=host))
).decode('utf-8').strip()

if dnsip == '':
_syslog('Current IP record for '{hostname}': [NXDOMAIN]'.format(hostname=host), syslog.LOG_INFO)
raise EmptyDNSResponse
else:
_syslog('Current IP record for '{hostname}': {record}'.format(hostname=host, record=dnsip))
except sp.CalledProcessError as err:
syslog.syslog(syslog.LOG_CRIT, 'Subprocess error when calling `dig`, exiting.')
print("Subprocess error when calling dig: {}".format(err))
_exit(2) # Exit on code 10: Can't continue if subprocess isn't working...
except EmptyDNSResponse:
syslog.syslog(syslog.LOG_INFO, "Empty DNS response, assuming that entry doesn't exist.")
# Assume that the IP address differs or doesn't exist.
return "NXDOMAIN"

if dnsip == curip:
return "UPTODATE"
else:
return "NEEDSUPDATED"


# CloudFlare has different functions for Add and Change. Determine if we exist first.
def _update_cloudflare(cf: CloudFlare.CloudFlare, domain: AnyStr = ZONE, hostname: AnyStr = DOMAIN):
# Validate that zone exists first.
zone_id = None
try:
zone = cf.zones.get(params={'name': domain})
if len(zone) < 1:
raise LookupError
else:
zone_id = zone[0]['id']
except LookupError:
syslog.syslog(syslog.LOG_ERR, "No valid zone data on CloudFlare, root domain zone might not exist.")
_exit(3)

curip = _get_current_ip_address()
if not curip:
syslog.syslog(syslog.LOG_ERR, "Could not find valid current IP address, aborting update process.")
_exit(2)

fqdn = hostname + '.' + domain

ip_status = _dns_ip_address_status(host=fqdn, curip=curip)
if ip_status == "NXDOMAIN":
# Add new record: POST
cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
'proxiable': False, 'proxied': False})
elif ip_status == "NEEDSUPDATED":
dns_records = cf.zones.dns_records.get(zone_id, params={'name': fqdn})
if len(dns_records) != 1:
syslog.syslog(syslog.LOG_ERR,
"Invalid number of records returned, this might be a CF DNS records issue, check it.")
_exit(4)

dns_record_id = dns_records[0]['id']
cf.zones.dns_records.delete(zone_id, dns_record_id)

cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
'proxiable': False, 'proxied': False})

elif ip_status == "UPTODATE":
syslog.syslog(syslog.LOG_INFO, "DNS record for {} does not need adjusted.".format(fqdn))
pass


def execute():
syslog.openlog(ident='py-ddns-ipupdate', logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
_syslog("DDNS Update Process Started.")

# Test if Internet is up by reaching to Google.
try:
req = urllib.request.urlopen('https://google.com', timeout=5)
req.close()
except urllib.error.URLError:
_syslog("No Internet connection available, aborting update process.")
_exit(1)

# Get current public IP
ip = _get_current_ip_address()
if '.' not in ip and ':' not in ip:
_syslog("Unexpected response from WhatIsMyIP.com API: {response}".format(response=ip))
_exit(1)
else:
_syslog("Current Public IP: {ip}".format(ip=ip))

_update_cloudflare(CloudFlare.CloudFlare(email='valid@email.address',
token='CloudFlareAPITokenKey',
debug=False))

_exit(0)


if __name__ == "__main__":
execute()









share|improve this question





























    2














    I have a computer that roams, but needs to have an FQDN attached to it for certain reasons such as email system integrations or testing email via a testing SMTP server that requires valid FQDNs and HELOs.



    My domain(s) are all on CloudFlare, so I wrote an adapted version of another script I had to wrap around CloudFlare's API so I can update DNS entries and such.



    This has a few requirements from PyPI:




    • ipaddress

    • CloudFlare Python wrapper (cloudflare on PyPI)


    This script also has two other requirements to really function, but I can guarantee you that both of these components work:




    • WhatIsMyIP.com API key for IP lookup capabilities

    • CloudFlare Account with API key


    Note that any sensitive information (such as login credentials or API keys) have been obfuscated in the below code. Additional bits can be provided as needed.



    (There is a known limitation that this does not work for IPv6 addresses - I'm working on adding this, but this current iteration of the script does not have IPv6 in it.)



    Critiques to improve the script are welcome, but keep in mind that I abide by PEP8 about linelengths <= 120 chars long because this is permissible if users on a team/development group agree on the longer length.



    #!/usr/bin/python3

    import CloudFlare
    import ipaddress
    import json
    import shlex
    import subprocess as sp
    import syslog
    import urllib.error
    import urllib.request

    from typing import AnyStr, Optional


    # Used for `dig` queries because we are using CloudFlare, and we need actual DNS results, not CF results for checking
    # the existence of an IP address currently. Therefore, we use Google DNS here.
    DNS_NAMESERVER = "8.8.8.8"

    # ZONE = root domain
    # DOMAIN = hostname within root domain.
    ZONE = "domain.tld"
    DOMAIN = "subdomain"

    # These next two are for WHATISMYIP - API Endpoints.
    WHATISMYIP = "https://api.whatismyip.com/ip.php?key={key}&output=json"
    API_KEYS = ['WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
    'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ']


    # Wrapper function around syslog to allow default priority of INFO, but
    # has the ability to change the priority if wished for a given message.
    def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
    syslog.syslog(priority, message)


    # Horribly unnecessary wrapper function around `exit` which calls the
    # "Process Ended" log message, and then actually exists with the given
    # exit code (by default it exits on 0 - the "Success" exit code)
    def _exit(code=0):
    # type: (int) -> None
    _syslog("DDNS Update Process Ended.")
    exit(code)


    # Singular Error handler for EmptyDNSResponse (could probably be a bare
    # LookupError, but EmptyDNSResponse is nicer...)
    class EmptyDNSResponse(LookupError):
    pass # No changes from base LookupError


    # Get current public IP address with WhatIsMyIP.com API
    def _get_current_ip_address():
    # type: () -> Optional[AnyStr]
    for key in API_KEYS:
    _syslog("Attempting lookup with API key {key}...".format(key=key))
    try:
    with urllib.request.urlopen(WHATISMYIP.format(key=key)) as req:
    data = json.loads(req.read().decode("UTF-8"))
    ipaddr = data['ip_address'][1]['result']
    except (urllib.error.URLError, urllib.error.HTTPError):
    _syslog("Could not look up public IP address, aborting update process.")
    _exit(1)

    try:
    # noinspection PyUnboundLocalVariable
    ipaddress.ip_address(ipaddr)
    except ValueError:
    if data is '0':
    _syslog("API key was not entered for lookup, this is a programming error.", syslog.LOG_CRIT)
    _exit(5)

    if data in ['1', '2']:
    _syslog("API Key Invalid or Inactive, attempting to try other keys...", syslog.LOG_WARNING)

    if data is '3':
    _syslog("API key lookup threshold reached, skipping to next API key...", syslog.LOG_WARNING)

    if data in ['4', '5']:
    _syslog("Query is bad, it needs 'input', which we can't do. This is a critical issue.",
    syslog.LOG_CRIT)
    _exit(6)

    if data is '6':
    _syslog("There was an unknown error with the WhatIsMyIP API, contact their support.",
    syslog.LOG_CRIT)
    _exit(7)

    continue # Try next API key

    return data


    # Check if the DNS entry for a given hostname differs from current IP,
    # and if it has no A record or it differs, return "True". Otherwise,
    # return False, and assume the IP address doesn't differ.
    def _dns_ip_address_status(host: AnyStr, curip: Optional[AnyStr] = None) -> AnyStr:

    if not curip:
    raise RuntimeError("Empty IP!")

    dnsip = ""
    try:
    dnsip = sp.check_output(
    shlex.split('dig +short @{nameserver} A {hostname}'.format(nameserver=DNS_NAMESERVER, hostname=host))
    ).decode('utf-8').strip()

    if dnsip == '':
    _syslog('Current IP record for '{hostname}': [NXDOMAIN]'.format(hostname=host), syslog.LOG_INFO)
    raise EmptyDNSResponse
    else:
    _syslog('Current IP record for '{hostname}': {record}'.format(hostname=host, record=dnsip))
    except sp.CalledProcessError as err:
    syslog.syslog(syslog.LOG_CRIT, 'Subprocess error when calling `dig`, exiting.')
    print("Subprocess error when calling dig: {}".format(err))
    _exit(2) # Exit on code 10: Can't continue if subprocess isn't working...
    except EmptyDNSResponse:
    syslog.syslog(syslog.LOG_INFO, "Empty DNS response, assuming that entry doesn't exist.")
    # Assume that the IP address differs or doesn't exist.
    return "NXDOMAIN"

    if dnsip == curip:
    return "UPTODATE"
    else:
    return "NEEDSUPDATED"


    # CloudFlare has different functions for Add and Change. Determine if we exist first.
    def _update_cloudflare(cf: CloudFlare.CloudFlare, domain: AnyStr = ZONE, hostname: AnyStr = DOMAIN):
    # Validate that zone exists first.
    zone_id = None
    try:
    zone = cf.zones.get(params={'name': domain})
    if len(zone) < 1:
    raise LookupError
    else:
    zone_id = zone[0]['id']
    except LookupError:
    syslog.syslog(syslog.LOG_ERR, "No valid zone data on CloudFlare, root domain zone might not exist.")
    _exit(3)

    curip = _get_current_ip_address()
    if not curip:
    syslog.syslog(syslog.LOG_ERR, "Could not find valid current IP address, aborting update process.")
    _exit(2)

    fqdn = hostname + '.' + domain

    ip_status = _dns_ip_address_status(host=fqdn, curip=curip)
    if ip_status == "NXDOMAIN":
    # Add new record: POST
    cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
    'proxiable': False, 'proxied': False})
    elif ip_status == "NEEDSUPDATED":
    dns_records = cf.zones.dns_records.get(zone_id, params={'name': fqdn})
    if len(dns_records) != 1:
    syslog.syslog(syslog.LOG_ERR,
    "Invalid number of records returned, this might be a CF DNS records issue, check it.")
    _exit(4)

    dns_record_id = dns_records[0]['id']
    cf.zones.dns_records.delete(zone_id, dns_record_id)

    cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
    'proxiable': False, 'proxied': False})

    elif ip_status == "UPTODATE":
    syslog.syslog(syslog.LOG_INFO, "DNS record for {} does not need adjusted.".format(fqdn))
    pass


    def execute():
    syslog.openlog(ident='py-ddns-ipupdate', logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
    _syslog("DDNS Update Process Started.")

    # Test if Internet is up by reaching to Google.
    try:
    req = urllib.request.urlopen('https://google.com', timeout=5)
    req.close()
    except urllib.error.URLError:
    _syslog("No Internet connection available, aborting update process.")
    _exit(1)

    # Get current public IP
    ip = _get_current_ip_address()
    if '.' not in ip and ':' not in ip:
    _syslog("Unexpected response from WhatIsMyIP.com API: {response}".format(response=ip))
    _exit(1)
    else:
    _syslog("Current Public IP: {ip}".format(ip=ip))

    _update_cloudflare(CloudFlare.CloudFlare(email='valid@email.address',
    token='CloudFlareAPITokenKey',
    debug=False))

    _exit(0)


    if __name__ == "__main__":
    execute()









    share|improve this question



























      2












      2








      2







      I have a computer that roams, but needs to have an FQDN attached to it for certain reasons such as email system integrations or testing email via a testing SMTP server that requires valid FQDNs and HELOs.



      My domain(s) are all on CloudFlare, so I wrote an adapted version of another script I had to wrap around CloudFlare's API so I can update DNS entries and such.



      This has a few requirements from PyPI:




      • ipaddress

      • CloudFlare Python wrapper (cloudflare on PyPI)


      This script also has two other requirements to really function, but I can guarantee you that both of these components work:




      • WhatIsMyIP.com API key for IP lookup capabilities

      • CloudFlare Account with API key


      Note that any sensitive information (such as login credentials or API keys) have been obfuscated in the below code. Additional bits can be provided as needed.



      (There is a known limitation that this does not work for IPv6 addresses - I'm working on adding this, but this current iteration of the script does not have IPv6 in it.)



      Critiques to improve the script are welcome, but keep in mind that I abide by PEP8 about linelengths <= 120 chars long because this is permissible if users on a team/development group agree on the longer length.



      #!/usr/bin/python3

      import CloudFlare
      import ipaddress
      import json
      import shlex
      import subprocess as sp
      import syslog
      import urllib.error
      import urllib.request

      from typing import AnyStr, Optional


      # Used for `dig` queries because we are using CloudFlare, and we need actual DNS results, not CF results for checking
      # the existence of an IP address currently. Therefore, we use Google DNS here.
      DNS_NAMESERVER = "8.8.8.8"

      # ZONE = root domain
      # DOMAIN = hostname within root domain.
      ZONE = "domain.tld"
      DOMAIN = "subdomain"

      # These next two are for WHATISMYIP - API Endpoints.
      WHATISMYIP = "https://api.whatismyip.com/ip.php?key={key}&output=json"
      API_KEYS = ['WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
      'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ']


      # Wrapper function around syslog to allow default priority of INFO, but
      # has the ability to change the priority if wished for a given message.
      def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
      syslog.syslog(priority, message)


      # Horribly unnecessary wrapper function around `exit` which calls the
      # "Process Ended" log message, and then actually exists with the given
      # exit code (by default it exits on 0 - the "Success" exit code)
      def _exit(code=0):
      # type: (int) -> None
      _syslog("DDNS Update Process Ended.")
      exit(code)


      # Singular Error handler for EmptyDNSResponse (could probably be a bare
      # LookupError, but EmptyDNSResponse is nicer...)
      class EmptyDNSResponse(LookupError):
      pass # No changes from base LookupError


      # Get current public IP address with WhatIsMyIP.com API
      def _get_current_ip_address():
      # type: () -> Optional[AnyStr]
      for key in API_KEYS:
      _syslog("Attempting lookup with API key {key}...".format(key=key))
      try:
      with urllib.request.urlopen(WHATISMYIP.format(key=key)) as req:
      data = json.loads(req.read().decode("UTF-8"))
      ipaddr = data['ip_address'][1]['result']
      except (urllib.error.URLError, urllib.error.HTTPError):
      _syslog("Could not look up public IP address, aborting update process.")
      _exit(1)

      try:
      # noinspection PyUnboundLocalVariable
      ipaddress.ip_address(ipaddr)
      except ValueError:
      if data is '0':
      _syslog("API key was not entered for lookup, this is a programming error.", syslog.LOG_CRIT)
      _exit(5)

      if data in ['1', '2']:
      _syslog("API Key Invalid or Inactive, attempting to try other keys...", syslog.LOG_WARNING)

      if data is '3':
      _syslog("API key lookup threshold reached, skipping to next API key...", syslog.LOG_WARNING)

      if data in ['4', '5']:
      _syslog("Query is bad, it needs 'input', which we can't do. This is a critical issue.",
      syslog.LOG_CRIT)
      _exit(6)

      if data is '6':
      _syslog("There was an unknown error with the WhatIsMyIP API, contact their support.",
      syslog.LOG_CRIT)
      _exit(7)

      continue # Try next API key

      return data


      # Check if the DNS entry for a given hostname differs from current IP,
      # and if it has no A record or it differs, return "True". Otherwise,
      # return False, and assume the IP address doesn't differ.
      def _dns_ip_address_status(host: AnyStr, curip: Optional[AnyStr] = None) -> AnyStr:

      if not curip:
      raise RuntimeError("Empty IP!")

      dnsip = ""
      try:
      dnsip = sp.check_output(
      shlex.split('dig +short @{nameserver} A {hostname}'.format(nameserver=DNS_NAMESERVER, hostname=host))
      ).decode('utf-8').strip()

      if dnsip == '':
      _syslog('Current IP record for '{hostname}': [NXDOMAIN]'.format(hostname=host), syslog.LOG_INFO)
      raise EmptyDNSResponse
      else:
      _syslog('Current IP record for '{hostname}': {record}'.format(hostname=host, record=dnsip))
      except sp.CalledProcessError as err:
      syslog.syslog(syslog.LOG_CRIT, 'Subprocess error when calling `dig`, exiting.')
      print("Subprocess error when calling dig: {}".format(err))
      _exit(2) # Exit on code 10: Can't continue if subprocess isn't working...
      except EmptyDNSResponse:
      syslog.syslog(syslog.LOG_INFO, "Empty DNS response, assuming that entry doesn't exist.")
      # Assume that the IP address differs or doesn't exist.
      return "NXDOMAIN"

      if dnsip == curip:
      return "UPTODATE"
      else:
      return "NEEDSUPDATED"


      # CloudFlare has different functions for Add and Change. Determine if we exist first.
      def _update_cloudflare(cf: CloudFlare.CloudFlare, domain: AnyStr = ZONE, hostname: AnyStr = DOMAIN):
      # Validate that zone exists first.
      zone_id = None
      try:
      zone = cf.zones.get(params={'name': domain})
      if len(zone) < 1:
      raise LookupError
      else:
      zone_id = zone[0]['id']
      except LookupError:
      syslog.syslog(syslog.LOG_ERR, "No valid zone data on CloudFlare, root domain zone might not exist.")
      _exit(3)

      curip = _get_current_ip_address()
      if not curip:
      syslog.syslog(syslog.LOG_ERR, "Could not find valid current IP address, aborting update process.")
      _exit(2)

      fqdn = hostname + '.' + domain

      ip_status = _dns_ip_address_status(host=fqdn, curip=curip)
      if ip_status == "NXDOMAIN":
      # Add new record: POST
      cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
      'proxiable': False, 'proxied': False})
      elif ip_status == "NEEDSUPDATED":
      dns_records = cf.zones.dns_records.get(zone_id, params={'name': fqdn})
      if len(dns_records) != 1:
      syslog.syslog(syslog.LOG_ERR,
      "Invalid number of records returned, this might be a CF DNS records issue, check it.")
      _exit(4)

      dns_record_id = dns_records[0]['id']
      cf.zones.dns_records.delete(zone_id, dns_record_id)

      cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
      'proxiable': False, 'proxied': False})

      elif ip_status == "UPTODATE":
      syslog.syslog(syslog.LOG_INFO, "DNS record for {} does not need adjusted.".format(fqdn))
      pass


      def execute():
      syslog.openlog(ident='py-ddns-ipupdate', logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
      _syslog("DDNS Update Process Started.")

      # Test if Internet is up by reaching to Google.
      try:
      req = urllib.request.urlopen('https://google.com', timeout=5)
      req.close()
      except urllib.error.URLError:
      _syslog("No Internet connection available, aborting update process.")
      _exit(1)

      # Get current public IP
      ip = _get_current_ip_address()
      if '.' not in ip and ':' not in ip:
      _syslog("Unexpected response from WhatIsMyIP.com API: {response}".format(response=ip))
      _exit(1)
      else:
      _syslog("Current Public IP: {ip}".format(ip=ip))

      _update_cloudflare(CloudFlare.CloudFlare(email='valid@email.address',
      token='CloudFlareAPITokenKey',
      debug=False))

      _exit(0)


      if __name__ == "__main__":
      execute()









      share|improve this question















      I have a computer that roams, but needs to have an FQDN attached to it for certain reasons such as email system integrations or testing email via a testing SMTP server that requires valid FQDNs and HELOs.



      My domain(s) are all on CloudFlare, so I wrote an adapted version of another script I had to wrap around CloudFlare's API so I can update DNS entries and such.



      This has a few requirements from PyPI:




      • ipaddress

      • CloudFlare Python wrapper (cloudflare on PyPI)


      This script also has two other requirements to really function, but I can guarantee you that both of these components work:




      • WhatIsMyIP.com API key for IP lookup capabilities

      • CloudFlare Account with API key


      Note that any sensitive information (such as login credentials or API keys) have been obfuscated in the below code. Additional bits can be provided as needed.



      (There is a known limitation that this does not work for IPv6 addresses - I'm working on adding this, but this current iteration of the script does not have IPv6 in it.)



      Critiques to improve the script are welcome, but keep in mind that I abide by PEP8 about linelengths <= 120 chars long because this is permissible if users on a team/development group agree on the longer length.



      #!/usr/bin/python3

      import CloudFlare
      import ipaddress
      import json
      import shlex
      import subprocess as sp
      import syslog
      import urllib.error
      import urllib.request

      from typing import AnyStr, Optional


      # Used for `dig` queries because we are using CloudFlare, and we need actual DNS results, not CF results for checking
      # the existence of an IP address currently. Therefore, we use Google DNS here.
      DNS_NAMESERVER = "8.8.8.8"

      # ZONE = root domain
      # DOMAIN = hostname within root domain.
      ZONE = "domain.tld"
      DOMAIN = "subdomain"

      # These next two are for WHATISMYIP - API Endpoints.
      WHATISMYIP = "https://api.whatismyip.com/ip.php?key={key}&output=json"
      API_KEYS = ['WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
      'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ']


      # Wrapper function around syslog to allow default priority of INFO, but
      # has the ability to change the priority if wished for a given message.
      def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
      syslog.syslog(priority, message)


      # Horribly unnecessary wrapper function around `exit` which calls the
      # "Process Ended" log message, and then actually exists with the given
      # exit code (by default it exits on 0 - the "Success" exit code)
      def _exit(code=0):
      # type: (int) -> None
      _syslog("DDNS Update Process Ended.")
      exit(code)


      # Singular Error handler for EmptyDNSResponse (could probably be a bare
      # LookupError, but EmptyDNSResponse is nicer...)
      class EmptyDNSResponse(LookupError):
      pass # No changes from base LookupError


      # Get current public IP address with WhatIsMyIP.com API
      def _get_current_ip_address():
      # type: () -> Optional[AnyStr]
      for key in API_KEYS:
      _syslog("Attempting lookup with API key {key}...".format(key=key))
      try:
      with urllib.request.urlopen(WHATISMYIP.format(key=key)) as req:
      data = json.loads(req.read().decode("UTF-8"))
      ipaddr = data['ip_address'][1]['result']
      except (urllib.error.URLError, urllib.error.HTTPError):
      _syslog("Could not look up public IP address, aborting update process.")
      _exit(1)

      try:
      # noinspection PyUnboundLocalVariable
      ipaddress.ip_address(ipaddr)
      except ValueError:
      if data is '0':
      _syslog("API key was not entered for lookup, this is a programming error.", syslog.LOG_CRIT)
      _exit(5)

      if data in ['1', '2']:
      _syslog("API Key Invalid or Inactive, attempting to try other keys...", syslog.LOG_WARNING)

      if data is '3':
      _syslog("API key lookup threshold reached, skipping to next API key...", syslog.LOG_WARNING)

      if data in ['4', '5']:
      _syslog("Query is bad, it needs 'input', which we can't do. This is a critical issue.",
      syslog.LOG_CRIT)
      _exit(6)

      if data is '6':
      _syslog("There was an unknown error with the WhatIsMyIP API, contact their support.",
      syslog.LOG_CRIT)
      _exit(7)

      continue # Try next API key

      return data


      # Check if the DNS entry for a given hostname differs from current IP,
      # and if it has no A record or it differs, return "True". Otherwise,
      # return False, and assume the IP address doesn't differ.
      def _dns_ip_address_status(host: AnyStr, curip: Optional[AnyStr] = None) -> AnyStr:

      if not curip:
      raise RuntimeError("Empty IP!")

      dnsip = ""
      try:
      dnsip = sp.check_output(
      shlex.split('dig +short @{nameserver} A {hostname}'.format(nameserver=DNS_NAMESERVER, hostname=host))
      ).decode('utf-8').strip()

      if dnsip == '':
      _syslog('Current IP record for '{hostname}': [NXDOMAIN]'.format(hostname=host), syslog.LOG_INFO)
      raise EmptyDNSResponse
      else:
      _syslog('Current IP record for '{hostname}': {record}'.format(hostname=host, record=dnsip))
      except sp.CalledProcessError as err:
      syslog.syslog(syslog.LOG_CRIT, 'Subprocess error when calling `dig`, exiting.')
      print("Subprocess error when calling dig: {}".format(err))
      _exit(2) # Exit on code 10: Can't continue if subprocess isn't working...
      except EmptyDNSResponse:
      syslog.syslog(syslog.LOG_INFO, "Empty DNS response, assuming that entry doesn't exist.")
      # Assume that the IP address differs or doesn't exist.
      return "NXDOMAIN"

      if dnsip == curip:
      return "UPTODATE"
      else:
      return "NEEDSUPDATED"


      # CloudFlare has different functions for Add and Change. Determine if we exist first.
      def _update_cloudflare(cf: CloudFlare.CloudFlare, domain: AnyStr = ZONE, hostname: AnyStr = DOMAIN):
      # Validate that zone exists first.
      zone_id = None
      try:
      zone = cf.zones.get(params={'name': domain})
      if len(zone) < 1:
      raise LookupError
      else:
      zone_id = zone[0]['id']
      except LookupError:
      syslog.syslog(syslog.LOG_ERR, "No valid zone data on CloudFlare, root domain zone might not exist.")
      _exit(3)

      curip = _get_current_ip_address()
      if not curip:
      syslog.syslog(syslog.LOG_ERR, "Could not find valid current IP address, aborting update process.")
      _exit(2)

      fqdn = hostname + '.' + domain

      ip_status = _dns_ip_address_status(host=fqdn, curip=curip)
      if ip_status == "NXDOMAIN":
      # Add new record: POST
      cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
      'proxiable': False, 'proxied': False})
      elif ip_status == "NEEDSUPDATED":
      dns_records = cf.zones.dns_records.get(zone_id, params={'name': fqdn})
      if len(dns_records) != 1:
      syslog.syslog(syslog.LOG_ERR,
      "Invalid number of records returned, this might be a CF DNS records issue, check it.")
      _exit(4)

      dns_record_id = dns_records[0]['id']
      cf.zones.dns_records.delete(zone_id, dns_record_id)

      cf.zones.dns_records.post(zone_id, data={'name': hostname, 'type': 'A', 'content': curip,
      'proxiable': False, 'proxied': False})

      elif ip_status == "UPTODATE":
      syslog.syslog(syslog.LOG_INFO, "DNS record for {} does not need adjusted.".format(fqdn))
      pass


      def execute():
      syslog.openlog(ident='py-ddns-ipupdate', logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
      _syslog("DDNS Update Process Started.")

      # Test if Internet is up by reaching to Google.
      try:
      req = urllib.request.urlopen('https://google.com', timeout=5)
      req.close()
      except urllib.error.URLError:
      _syslog("No Internet connection available, aborting update process.")
      _exit(1)

      # Get current public IP
      ip = _get_current_ip_address()
      if '.' not in ip and ':' not in ip:
      _syslog("Unexpected response from WhatIsMyIP.com API: {response}".format(response=ip))
      _exit(1)
      else:
      _syslog("Current Public IP: {ip}".format(ip=ip))

      _update_cloudflare(CloudFlare.CloudFlare(email='valid@email.address',
      token='CloudFlareAPITokenKey',
      debug=False))

      _exit(0)


      if __name__ == "__main__":
      execute()






      python python-3.x






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited 5 mins ago









      Jamal

      30.2k11116226




      30.2k11116226










      asked 6 hours ago









      Thomas Ward

      1,783923




      1,783923






















          2 Answers
          2






          active

          oldest

          votes


















          2














          Docstrings



          You should really consider switching to Docstrings instead of using your current documentation method. (Note that using Docstrings will give the function a .__doc__ property which can be used for a variety including documentation generation). So for instance:



          # Wrapper function around syslog to allow default priority of INFO, but
          # has the ability to change the priority if wished for a given message.
          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          syslog.syslog(priority, message)


          Would (well, almost) become:



          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          """
          Wrapper function around syslog to allow default priority of INFO, but
          has the ability to change the priority if wished for a given message.
          """
          syslog.syslog(priority, message)


          However, there are couple remarks to make:




          1. The Docstring comment I have linked tends to have the summary on the same line as the """, but many conventions disregard this.

          2. You should probably mention the arguments and return values (when applicable of course.)


          Wrap it in a class?



          Admittedly, I don't know too much about the domain you are working with, and similarly, I don't really like promoting OOP since I find often too overused, but here is my rational:




          1. You have a lot of globals like DNS_NAMESERVER, ZONE, DOMAIN etc, these could be given default values in your class and made private variables. (On the other hand, you might actually want these constant, in which case ignore this.)

          2. A lot of your functions have default values which could be instead omitted and placed as class variables.


          On the other hand, I may not know enough about what you're doing. If you disagree with this assessment, just look at the first critique.






          share|improve this answer





















          • Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
            – Thomas Ward
            15 mins ago



















          1















          # Horribly unnecessary wrapper




          You're right. Don't write your own exit. Since exit itself generates an exception to terminate the program, simply put your _syslog call in a finally at the top level.



          with urllib.request.urlopen


          Unless you have a really good (and obscure) reason, never use urllib. Use requests. It's saner in every way.



          if data in ['1', '2']:


          Technically, since you're testing membership, make this a set:



          if data in {'1', '2'}:


          As for this function documentation:



          # Check if the DNS entry for a given hostname differs from current IP,
          # and if it has no A record or it differs, return "True". Otherwise,
          # return False, and assume the IP address doesn't differ.


          Fine... but this doesn't do what you say it does. It returns strings, not booleans. I'd offer that neither is appropriate, and that you should be returning an enum instead.



              if len(zone) < 1:
          raise LookupError
          else:
          zone_id = zone[0]['id']


          Get rid of the else; you've previously raised.






          share|improve this answer





















          • Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
            – Thomas Ward
            14 mins ago













          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210442%2fcloudflare-dynamic-dns-update-script-in-python-3%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          2 Answers
          2






          active

          oldest

          votes








          2 Answers
          2






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          2














          Docstrings



          You should really consider switching to Docstrings instead of using your current documentation method. (Note that using Docstrings will give the function a .__doc__ property which can be used for a variety including documentation generation). So for instance:



          # Wrapper function around syslog to allow default priority of INFO, but
          # has the ability to change the priority if wished for a given message.
          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          syslog.syslog(priority, message)


          Would (well, almost) become:



          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          """
          Wrapper function around syslog to allow default priority of INFO, but
          has the ability to change the priority if wished for a given message.
          """
          syslog.syslog(priority, message)


          However, there are couple remarks to make:




          1. The Docstring comment I have linked tends to have the summary on the same line as the """, but many conventions disregard this.

          2. You should probably mention the arguments and return values (when applicable of course.)


          Wrap it in a class?



          Admittedly, I don't know too much about the domain you are working with, and similarly, I don't really like promoting OOP since I find often too overused, but here is my rational:




          1. You have a lot of globals like DNS_NAMESERVER, ZONE, DOMAIN etc, these could be given default values in your class and made private variables. (On the other hand, you might actually want these constant, in which case ignore this.)

          2. A lot of your functions have default values which could be instead omitted and placed as class variables.


          On the other hand, I may not know enough about what you're doing. If you disagree with this assessment, just look at the first critique.






          share|improve this answer





















          • Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
            – Thomas Ward
            15 mins ago
















          2














          Docstrings



          You should really consider switching to Docstrings instead of using your current documentation method. (Note that using Docstrings will give the function a .__doc__ property which can be used for a variety including documentation generation). So for instance:



          # Wrapper function around syslog to allow default priority of INFO, but
          # has the ability to change the priority if wished for a given message.
          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          syslog.syslog(priority, message)


          Would (well, almost) become:



          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          """
          Wrapper function around syslog to allow default priority of INFO, but
          has the ability to change the priority if wished for a given message.
          """
          syslog.syslog(priority, message)


          However, there are couple remarks to make:




          1. The Docstring comment I have linked tends to have the summary on the same line as the """, but many conventions disregard this.

          2. You should probably mention the arguments and return values (when applicable of course.)


          Wrap it in a class?



          Admittedly, I don't know too much about the domain you are working with, and similarly, I don't really like promoting OOP since I find often too overused, but here is my rational:




          1. You have a lot of globals like DNS_NAMESERVER, ZONE, DOMAIN etc, these could be given default values in your class and made private variables. (On the other hand, you might actually want these constant, in which case ignore this.)

          2. A lot of your functions have default values which could be instead omitted and placed as class variables.


          On the other hand, I may not know enough about what you're doing. If you disagree with this assessment, just look at the first critique.






          share|improve this answer





















          • Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
            – Thomas Ward
            15 mins ago














          2












          2








          2






          Docstrings



          You should really consider switching to Docstrings instead of using your current documentation method. (Note that using Docstrings will give the function a .__doc__ property which can be used for a variety including documentation generation). So for instance:



          # Wrapper function around syslog to allow default priority of INFO, but
          # has the ability to change the priority if wished for a given message.
          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          syslog.syslog(priority, message)


          Would (well, almost) become:



          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          """
          Wrapper function around syslog to allow default priority of INFO, but
          has the ability to change the priority if wished for a given message.
          """
          syslog.syslog(priority, message)


          However, there are couple remarks to make:




          1. The Docstring comment I have linked tends to have the summary on the same line as the """, but many conventions disregard this.

          2. You should probably mention the arguments and return values (when applicable of course.)


          Wrap it in a class?



          Admittedly, I don't know too much about the domain you are working with, and similarly, I don't really like promoting OOP since I find often too overused, but here is my rational:




          1. You have a lot of globals like DNS_NAMESERVER, ZONE, DOMAIN etc, these could be given default values in your class and made private variables. (On the other hand, you might actually want these constant, in which case ignore this.)

          2. A lot of your functions have default values which could be instead omitted and placed as class variables.


          On the other hand, I may not know enough about what you're doing. If you disagree with this assessment, just look at the first critique.






          share|improve this answer












          Docstrings



          You should really consider switching to Docstrings instead of using your current documentation method. (Note that using Docstrings will give the function a .__doc__ property which can be used for a variety including documentation generation). So for instance:



          # Wrapper function around syslog to allow default priority of INFO, but
          # has the ability to change the priority if wished for a given message.
          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          syslog.syslog(priority, message)


          Would (well, almost) become:



          def _syslog(message: AnyStr, priority: int = syslog.LOG_INFO) -> None:
          """
          Wrapper function around syslog to allow default priority of INFO, but
          has the ability to change the priority if wished for a given message.
          """
          syslog.syslog(priority, message)


          However, there are couple remarks to make:




          1. The Docstring comment I have linked tends to have the summary on the same line as the """, but many conventions disregard this.

          2. You should probably mention the arguments and return values (when applicable of course.)


          Wrap it in a class?



          Admittedly, I don't know too much about the domain you are working with, and similarly, I don't really like promoting OOP since I find often too overused, but here is my rational:




          1. You have a lot of globals like DNS_NAMESERVER, ZONE, DOMAIN etc, these could be given default values in your class and made private variables. (On the other hand, you might actually want these constant, in which case ignore this.)

          2. A lot of your functions have default values which could be instead omitted and placed as class variables.


          On the other hand, I may not know enough about what you're doing. If you disagree with this assessment, just look at the first critique.







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered 4 hours ago









          Dair

          4,527729




          4,527729












          • Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
            – Thomas Ward
            15 mins ago


















          • Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
            – Thomas Ward
            15 mins ago
















          Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
          – Thomas Ward
          15 mins ago




          Class Wrap > 1: These are constants, not a class. This is just a script which I utilize for my dynamic DNS updates to CLoudFlare on a cronjob. Eventually there will be more than one hostname being updated, at which case #2 will have non-default inputs and I'll revise the script. I do agree with the docstring notes though, I'll work on that.
          – Thomas Ward
          15 mins ago













          1















          # Horribly unnecessary wrapper




          You're right. Don't write your own exit. Since exit itself generates an exception to terminate the program, simply put your _syslog call in a finally at the top level.



          with urllib.request.urlopen


          Unless you have a really good (and obscure) reason, never use urllib. Use requests. It's saner in every way.



          if data in ['1', '2']:


          Technically, since you're testing membership, make this a set:



          if data in {'1', '2'}:


          As for this function documentation:



          # Check if the DNS entry for a given hostname differs from current IP,
          # and if it has no A record or it differs, return "True". Otherwise,
          # return False, and assume the IP address doesn't differ.


          Fine... but this doesn't do what you say it does. It returns strings, not booleans. I'd offer that neither is appropriate, and that you should be returning an enum instead.



              if len(zone) < 1:
          raise LookupError
          else:
          zone_id = zone[0]['id']


          Get rid of the else; you've previously raised.






          share|improve this answer





















          • Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
            – Thomas Ward
            14 mins ago


















          1















          # Horribly unnecessary wrapper




          You're right. Don't write your own exit. Since exit itself generates an exception to terminate the program, simply put your _syslog call in a finally at the top level.



          with urllib.request.urlopen


          Unless you have a really good (and obscure) reason, never use urllib. Use requests. It's saner in every way.



          if data in ['1', '2']:


          Technically, since you're testing membership, make this a set:



          if data in {'1', '2'}:


          As for this function documentation:



          # Check if the DNS entry for a given hostname differs from current IP,
          # and if it has no A record or it differs, return "True". Otherwise,
          # return False, and assume the IP address doesn't differ.


          Fine... but this doesn't do what you say it does. It returns strings, not booleans. I'd offer that neither is appropriate, and that you should be returning an enum instead.



              if len(zone) < 1:
          raise LookupError
          else:
          zone_id = zone[0]['id']


          Get rid of the else; you've previously raised.






          share|improve this answer





















          • Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
            – Thomas Ward
            14 mins ago
















          1












          1








          1







          # Horribly unnecessary wrapper




          You're right. Don't write your own exit. Since exit itself generates an exception to terminate the program, simply put your _syslog call in a finally at the top level.



          with urllib.request.urlopen


          Unless you have a really good (and obscure) reason, never use urllib. Use requests. It's saner in every way.



          if data in ['1', '2']:


          Technically, since you're testing membership, make this a set:



          if data in {'1', '2'}:


          As for this function documentation:



          # Check if the DNS entry for a given hostname differs from current IP,
          # and if it has no A record or it differs, return "True". Otherwise,
          # return False, and assume the IP address doesn't differ.


          Fine... but this doesn't do what you say it does. It returns strings, not booleans. I'd offer that neither is appropriate, and that you should be returning an enum instead.



              if len(zone) < 1:
          raise LookupError
          else:
          zone_id = zone[0]['id']


          Get rid of the else; you've previously raised.






          share|improve this answer













          # Horribly unnecessary wrapper




          You're right. Don't write your own exit. Since exit itself generates an exception to terminate the program, simply put your _syslog call in a finally at the top level.



          with urllib.request.urlopen


          Unless you have a really good (and obscure) reason, never use urllib. Use requests. It's saner in every way.



          if data in ['1', '2']:


          Technically, since you're testing membership, make this a set:



          if data in {'1', '2'}:


          As for this function documentation:



          # Check if the DNS entry for a given hostname differs from current IP,
          # and if it has no A record or it differs, return "True". Otherwise,
          # return False, and assume the IP address doesn't differ.


          Fine... but this doesn't do what you say it does. It returns strings, not booleans. I'd offer that neither is appropriate, and that you should be returning an enum instead.



              if len(zone) < 1:
          raise LookupError
          else:
          zone_id = zone[0]['id']


          Get rid of the else; you've previously raised.







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered 4 hours ago









          Reinderien

          2,774719




          2,774719












          • Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
            – Thomas Ward
            14 mins ago




















          • Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
            – Thomas Ward
            14 mins ago


















          Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
          – Thomas Ward
          14 mins ago






          Agreed with these suggestions, and I'll work on improving. The headache I ran into with requests was this was originally written on an OLD Python3 which didn't have requests available. I haven't updated it to 3.7 standards yet... or at least, the urllib bits yet.
          – Thomas Ward
          14 mins ago




















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.





          Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


          Please pay close attention to the following guidance:


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210442%2fcloudflare-dynamic-dns-update-script-in-python-3%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Morgemoulin

          Scott Moir

          Souastre