Skip to content

Commit

Permalink
Adding automatic record ID retrieval and dynamic YAML use.
Browse files Browse the repository at this point in the history
  • Loading branch information
root committed May 21, 2014
1 parent 281c93d commit 8cdef02
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
*.pyc
*.swp
config.yaml
*.yaml
118 changes: 28 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,99 +24,39 @@ Usage
First, a few assumptions:

- You have a CloudFlare account.
- You're using CF to host DNS for a domain you own.
- You have an A record in CF you intend to dynamically update.
- You're using CloudFlare to host DNS for a domain you own.
- You have an A record in CloudFlare you intend to dynamically update.

Now, to use this script you'll want to fill out the configuration file. Make a
copy of `config.yaml.template` named `config.yaml`, then fill out the options in
`config.yaml`. The comments in the file make it fairly self explanatory.
To use this utility, create a copy of the `config.yaml.template` file (and remove .template
from the filename). Create one template per each record / domain pair you intend to update.
For example, I might have two configuration files: `mattslifebytes_naked.yaml` that
updates the A record for the naked (no www prefix) domain mattslifebytes.com,
and a second config, `mattslifebytes_www.yaml` that updates the A record
for www.mattslifebytes.com

Now to do a one-off update of your DNS record, simply run `cloudflare_ddns.py`
from your terminal. It'll get your public IP address, then update the
CloudFlare DNS record with it. Easy! (In fact, you should probably do this once
to test that your config file is correct.) You should see something like this:
To do a one-off update of your DNS record, simply run
`python cloudflare_ddns.py config_file_name.yaml` from your terminal.
The script will determine your public IP address and automatically update the CloudFlare
DNS record along with it.

jpk@truth:~/code/cloudflare-ddns$ ./cloudflare_ddns.py
Tue Oct 8 22:09:00 2013, success, 123.45.67.89
jpk@truth:~/code/cloudflare-ddns$
If the program encounters an issue while attempting to update CloudFlare's records,
it will print the failure response CloudFlare returns. Check your configuration
file for accurate information and try again.

If it fails, it'll print the failure response CloudFlare returns. Check your
`config.yaml` and try again.

Because dynamic IPs can change regularly, it's recommended that you run this
utility periodically in the background to keep the CloudFlare record up-to-date.

If you're like me, though, you probably want it to run periodically in the
background to keep the record up-to-date as your public IP address changes.
Just add a line to your [crontab](http://en.wikipedia.org/wiki/Cron) and let
cron run it for you. My crontab has a line in it like this:

# Every 15 minutes, check public ip, and update a record on cloudflare.
*/15 * * * * ~/code/cloudflare-ddns/cloudflare-ddns.py >> ~/code/cloudflare-ddns/logs/log.txt

That will update the record every 15 minutes. You'll want the paths there to
match up with wherever you checked this repo out. The redirection to append to
a log file is optional, but handy for debugging if you notice the DNS record
isn't staying up-to-date or something.

Getting the CloudFlare Record ID
--------------------------------

The only tricky thing about the configuration is `cf_record_id`. This is
CloudFlare's id for the DNS record you want to update. You'll need to make a
call to their API to find out what this is. You can use this command
to make that call (fill in with your information):

curl https://www.cloudflare.com/api_json.html \
-d 'a=rec_load_all' \
-d 'tkn=YOUR_TOKEN_HERE' \
-d 'email=YOUR_EMAIL_HERE' \
-d 'z=YOUR_DOMAIN_HERE' | python -mjson.tool

This should pretty-print a bunch of JSON, part of which will be a list of
objects representing DNS records in your zone. They look like this:

...
{
"auto_ttl": 0,
"content": "123.45.67.89",
"display_content": "123.45.67.89",
"display_name": "ddns",
"name": "ddns.domain.com",
"prio": null,
"props": {
"cf_open": 1,
"cloud_on": 0,
"expired_ssl": 0,
"expiring_ssl": 0,
"pending_ssl": 0,
"proxiable": 1,
"ssl": 0,
"vanity_lock": 0
},
"rec_id": "12345678",
"rec_tag": "[some long hex value]",
"service_mode": "0",
"ssl_expires_on": null,
"ssl_id": null,
"ssl_status": null,
"ttl": "120",
"ttl_ceil": 86400,
"type": "A",
"zone_name": "domain.com"
},
...

Find the one with a `name` field that matches the host you're wanting to update,
and the `rec_id` field is the record id you want to put in your config.
(In this case "ddns.domain.com" is the one we're looking for, and it has a
record id of "12345678".)

Note that if you have a lot of records the response will be paginated, and
the one you're looking for might be in a later page. If you can't find your
record in the first response and the `has_more` field near the start of the
response is `true`, then that's probably what happened. Do the request again,
but add a `-d 'o=N'` parameter to curl where `N` is whatever `count` was in
the last response. That'll get the next page of records. Repeat until you
find the one you need.
cron run it for you at a regular interval.

# Every 15 minutes, check the current public IP, and update the A record on CloudFlare.
*/15 * * * * /path/to/code/cloudflare-ddns.py /path/to/code/mattslifebytes_www.yaml >> /var/log/cloudflare_ddns.log

This example will update the record every 15 minutes. You'll want to be sure
that you insert the correct paths to reflect were the codebase is located.
The redirection (`>>`) to append to a log file is optional, but handy for debugging if you notice the DNS
record is not staying up-to-date.

If you want to learn more about the CloudFlare API, you can read on
[here](http://www.cloudflare.com/docs/client-api.html).
Expand All @@ -126,7 +66,5 @@ Credits and Thanks

- [CloudFlare](https://www.cloudflare.com/) for having an API and otherwise
generally being cool.
- [jsonip.com](http://jsonip.com/) for making grabbing your public IP from a
script super easy. Put together by [@geuis](https://twitter.com/geuis). Go
to his twitter and shower him with praise.

- [icanhazip.com](http://icanhazip.com/) for making grabbing your public IP from a
script super easy.
76 changes: 51 additions & 25 deletions cloudflare_ddns.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
#!/usr/bin/env python
#
# Ghetto dynamic dns using cloudflare's api.
#
import requests
import json
import time
import yaml
import os
import sys

# CloudFlare api url.
CLOUDFLARE_URL = 'https://www.cloudflare.com/api_json.html'
# jsonip api url.
JSONIP_URL = 'http://jsonip.com'

# Time-to-live for your A record. This should be as small as possible to ensure
# changes aren't cached for too long and are propogated quickly. CloudFlare's
# api docs set a minimum of 120 seconds.
TTL = '120'
# CloudFlare service mode. This enables/disables CF's traffic acceleration.
# Enabled (orange cloud) is 1. Disabled (grey cloud) is 0.
SERVICE_MODE = 0

# DNS record type for your DDNS host. Probably an A record.
RECORD_TYPE = 'A'

# Location of this script.
SCRIPT_ROOT = os.path.dirname(os.path.realpath(__file__))

# Location of the config file.
CONFIG_FILE = os.path.join(SCRIPT_ROOT, 'config.yaml')
CONFIG_FILE = os.path.join(SCRIPT_ROOT, sys.argv[1])


def main():
Expand All @@ -35,38 +31,68 @@ def main():
cf_email = config.get('cf_email')
cf_domain = config.get('cf_domain')
cf_subdomain = config.get('cf_subdomain')
cf_record_id = config.get('cf_record_id')
cf_service_mode = config.get('cf_service_mode')

# Discover your public IP address.
public_ip = requests.get(JSONIP_URL).json['ip']
public_ip = requests.get("http://icanhazip.com/").text.strip()

# Prepare request to cloudflare api, updating your A record.
# Discover the record_id for the record we intend to update.
cf_params = {
'a': 'rec_load_all',
'tkn': cf_key,
'email': cf_email,
'z': cf_domain,
'o': 0
}

record_id = None
while not record_id: # Getting all records can return a paginated result, so we do a while.
cf_response = requests.get(CLOUDFLARE_URL, params=cf_params)
if cf_response.status_code < 200 or cf_response.status_code > 299:
raise Exception("CloudFlare returned an unexpected status code: %s" % response.status_code)

response = json.loads(cf_response.text)
for record in response["response"]["recs"]["objs"]:
if record["type"] == RECORD_TYPE and \
(record["name"] == cf_subdomain or record["name"] == str(cf_subdomain + "." + cf_domain)):

# If this record already has the correct IP, we return early and don't do anything.
if record["content"] == public_ip:
return

record_id = record["rec_id"]

# We didn't see a result. Check if the response was paginated and if so, call another page.
if not record_id:
if response["response"]["recs"]["has_more"]:
cf_params["o"] = response["response"]["recs"]["count"] # Set a new start point
else:
raise Exception("Can't find an existing %s record matching the name '%s'" % (RECORD_TYPE, cf_subdomain))


# Now we've got a record_id and all the good stuff to actually update the record, so let's do it.
cf_params = {
'a': 'rec_edit',
'tkn': cf_key,
'id': cf_record_id,
'id': record_id,
'email': cf_email,
'z': cf_domain,
'type': RECORD_TYPE,
'ttl': TTL,
'name': cf_subdomain,
'content': public_ip,
'service_mode': SERVICE_MODE
'service_mode': cf_service_mode
}

# Make request, collect response.
cf_response = requests.get(CLOUDFLARE_URL, params=cf_params)
if cf_response.status_code < 200 or cf_response.status_code > 299:
raise Exception("CloudFlare returned an unexpected status code: %s" % response.status_code)
response = json.loads(cf_response.text)

# Print success/fail log message using response.
result = cf_response.json['result']
date = time.ctime()
if result == 'success':
print '{date}, success, {ip}'.format(date=date, ip=public_ip)
if response["result"] == "success":
print "%s has been successfully updated to point to the IP %s" % (cf_subdomain, public_ip)
else:
response = json.dumps(cf_response.json)
print '{date}, fail, {response}'.format(date=date, response=response)

return
raise Exception("Updating record failed with the result '%s'" % response["result"])


if __name__ == '__main__':
Expand Down
8 changes: 3 additions & 5 deletions config.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ cf_domain: 'YOUR DOMAIN HERE'
# If the host name you're updating is "ddns.domain.com", make this "ddns".
cf_subdomain: 'YOUR SUBDOMAIN HERE'

# The record id of the dns record we're updating.
# See README for instructions on how to find this.
cf_record_id: 'YOUR RECORD HERE'


# CloudFlare service mode. This enables/disables CF's traffic acceleration.
# Enabled (orange cloud) is 1. Disabled (grey cloud) is 0.
cf_service_mode: 1
2 changes: 0 additions & 2 deletions logs/.gitignore

This file was deleted.

0 comments on commit 8cdef02

Please sign in to comment.