4
\$\begingroup\$

Inspired by a shell script I saw on Github, I put together a Python version to update the dynamic IP address for a subdomain I have that uses DigitalOcean's nameservers. I added a check to see if the IP address actually needs to be updated, and not do the update if the IP address hasn't changed. All of the user-configurable variables - API token, domain, subdomain - are stored in a separate .env file alongside the script.

#!/usr/bin/env python3

# Import required modules
import dotenv
import json
import os
import requests

# Load user-configured variables from .env file
dotenv.load_dotenv()
token = os.environ.get('DO_API_TOKEN')
domain = os.environ.get('DO_DOMAIN')
subdomain = os.environ.get('DO_SUBDOMAIN')

# Other variables
check_ip_url = 'https://api.ipify.org'
do_api_url = 'https://api.digitalocean.com/v2/domains/'

# Get the current external IP
def get_current_ip():
    curr_ip = requests.get(check_ip_url).text.rstrip()
    return curr_ip

# Get the current subdomain IP
def get_sub_info():
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + token
    }
    response = requests.get(do_api_url + domain + '/records', headers=headers).text
    records = json.loads(response)
    for record in records['domain_records']:
        if record['name'] == subdomain:
            subdomain_info = {
                'ip': record['data'],
                'record_id': record['id']
            }
            return subdomain_info

# Update DNS records if required
def update_dns():
    current_ip_address = get_current_ip()
    subdomain_ip_address = get_sub_info()['ip']
    subdomain_record_id = get_sub_info()['record_id']
    if current_ip_address == subdomain_ip_address:
        print('Subdomain DNS record does not need updating.')
        return
    else:
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer $token',
        }
        data = '{"data":"' + current_ip_address + '"}'
        response = requests.put(do_api_url + domain + '/records/' + subdomain_record_id, headers=headers, data=data)
        if '200' in response:
            print('Subdomain IP address updated to ' + current_ip_address)
        else:
            print('IP address update failed with message: ' + response.text)
        return

if __name__ == '__main__':
    update_dns()

It works, but isn't the prettiest Python code out there, and I'm sure if could be made a bit more efficient. Are there any stylistic changes I should make to better adhere to best practices? I have run it through a PEP8 checker, and the only thing it brought up was about the line length in a few places, which I'm not super concerned about.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
#!/usr/bin/env python3

import os

import dotenv
import requests

dotenv.load_dotenv()
token = os.environ['DO_API_TOKEN']
domain = os.environ['DO_DOMAIN']
subdomain = os.environ['DO_SUBDOMAIN']

records_url = f'https://api.digitalocean.com/v2/domains/{domain}/records/'
session = requests.Session()
session.headers = {
    'Authorization': 'Bearer ' + token
}


def get_current_ip():
    return requests.get('https://api.ipify.org').text.rstrip()


def get_sub_info():
    records = session.get(records_url).json()
    for record in records['domain_records']:
        if record['name'] == subdomain:
            return record


def update_dns():
    current_ip_address = get_current_ip()
    sub_info = get_sub_info()
    subdomain_ip_address = sub_info['data']
    subdomain_record_id = sub_info['id']
    if current_ip_address == subdomain_ip_address:
        print('Subdomain DNS record does not need updating.')
    else:
        response = session.put(records_url + subdomain_record_id, json={'data': current_ip_address})
        if response.ok:
            print('Subdomain IP address updated to ' + current_ip_address)
        else:
            print('IP address update failed with message: ' + response.text)


if __name__ == '__main__':
    update_dns()
  1. The script will not work if any variables are missing, so use [...] instead of .get(...) to throw an error ASAP if needed.
  2. The DO URLs always start with /{domain}/records/ so I included that in the top level constant.
  3. A requests.Session makes multiple requests to the same domain faster as it keeps the connection open, and it lets you specify info like headers once.
  4. A few times you create a variable and immediately return it. You can just return the expression directly.
  5. I felt that the check_ip_url constant didn't add anything, so I inlined it. This is mostly a preference.
  6. Most of the comments do not help readers in any way, so I removed them. But if you want to describe what a function does, use a docstring.
  7. Calling .json() on a response parses the text as JSON for you.
  8. Moving the values of one dict to another dict before finally extracting them just adds another layer, so I just returned record directly.
  9. You called get_sub_info() twice which meant two identical requests. To speed things up, I extracted a variable.
  10. Again, requests makes JSON easy to use, now with the json= argument. This both converts the dictionary to a JSON string and sets the content type. But even without this, you really should have been using json.dumps rather than string concatenation.
  11. response.ok is typically how you check if a request succeeded, or by checking the value of response.status_code. I've never seen anyone using the in operator on a response.
  12. There's no reason to return when a function is ending anyway.
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the pointers! But what does the 'f' before the URL in records_url do? \$\endgroup\$
    – campegg
    Commented Feb 4, 2019 at 20:30
  • \$\begingroup\$ @campegg Google "python f string" \$\endgroup\$
    – Alex Hall
    Commented Feb 5, 2019 at 4:56
  • \$\begingroup\$ Got it - I was looking for "f variables" and things like that, but didn't turn anything up. Thanks again! \$\endgroup\$
    – campegg
    Commented Feb 5, 2019 at 15:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.