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.
1 Answer 1
#!/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()
- The script will not work if any variables are missing, so use
[...]
instead of.get(...)
to throw an error ASAP if needed. - The DO URLs always start with
/{domain}/records/
so I included that in the top level constant. - 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. - A few times you create a variable and immediately return it. You can just return the expression directly.
- I felt that the
check_ip_url
constant didn't add anything, so I inlined it. This is mostly a preference. - 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.
- Calling
.json()
on a response parses the text as JSON for you. - Moving the values of one dict to another dict before finally extracting them just adds another layer, so I just returned
record
directly. - You called
get_sub_info()
twice which meant two identical requests. To speed things up, I extracted a variable. - Again,
requests
makes JSON easy to use, now with thejson=
argument. This both converts the dictionary to a JSON string and sets the content type. But even without this, you really should have been usingjson.dumps
rather than string concatenation. response.ok
is typically how you check if a request succeeded, or by checking the value ofresponse.status_code
. I've never seen anyone using thein
operator on a response.- There's no reason to
return
when a function is ending anyway.
-
\$\begingroup\$ Thanks for the pointers! But what does the 'f' before the URL in
records_url
do? \$\endgroup\$campegg– campegg2019年02月04日 20:30:46 +00:00Commented Feb 4, 2019 at 20:30 -
\$\begingroup\$ @campegg Google "python f string" \$\endgroup\$Alex Hall– Alex Hall2019年02月05日 04:56:38 +00:00Commented 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– campegg2019年02月05日 15:07:02 +00:00Commented Feb 5, 2019 at 15:07