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.

asked Feb 4, 2019 at 18:53
\$\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.
answered Feb 4, 2019 at 20:24
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the pointers! But what does the 'f' before the URL in records_url do? \$\endgroup\$ Commented Feb 4, 2019 at 20:30
  • \$\begingroup\$ @campegg Google "python f string" \$\endgroup\$ 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\$ Commented Feb 5, 2019 at 15:07

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.