From 57d53b936c7b10cf92d8e02e1a9078a25ad99196 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Tue, 5 Mar 2019 11:16:45 -0800 Subject: [PATCH] Working commit --- .dockerignore | 2 + Dockerfile | 14 +++++ Makefile | 18 ++++++ README.md | 22 +++++++- requirements.txt | 1 + start.sh | 3 + update_ddns.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 requirements.txt create mode 100755 start.sh create mode 100755 update_ddns.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..69ab732 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +README.md +Makefile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1325445 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3 + +RUN mkdir -p /src +WORKDIR /src + +COPY ./requirements.txt ./ +RUN pip install -r ./requirements.txt + +COPY ./start.sh ./ +COPY ./update_ddns.py ./update_ddns.py + +ENV DOMAIN="" + +CMD [ "/src/start.sh" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..870c3b1 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +DOCKER_TAG ?= cloudflare-ddns-dev-${USER} +default: test + +test: + @echo ok + +update: + curl -o update_ddns.py https://raw.githubusercontent.com/cloudflare/python-cloudflare/master/examples/example_update_dynamic_dns.py + chmod +x update_ddns.py + +build: + docker build . -t ${DOCKER_TAG} + +run: build + docker run --rm -e DOMAIN=${DOMAIN} \ + -e CF_API_EMAIL=${CF_API_EMAIL} \ + -e CF_API_KEY=${CF_API_KEY} \ + ${DOCKER_TAG} diff --git a/README.md b/README.md index 8e7a3f3..04591a1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ -# docker-cloudflare-ddns +# Docker Cloudfare DDNS -Docker build of a Cloudflare DDNS client using their example code \ No newline at end of file +Simple Docker image to dynamically update a Cloudflare DNS record. + +## Usage + +All parameters are passed to the script using env variables, so export the following: + + DOMAIN=sub.example.com + CF_API_EMAIL=admin@example.com + CF_API_KEY=00000000000000000000 + +Then run. To execute from this directory, you can use the convenient Make target. + + make run + +## Development + +The script is straight from the examples provided by Cloudflare on their Github. The latest version can be downloaded using: + + make update diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0a9630a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +cloudflare diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..253d218 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +exec /src/update_ddns.py $DOMAIN diff --git a/update_ddns.py b/update_ddns.py new file mode 100755 index 0000000..8f0239c --- /dev/null +++ b/update_ddns.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +"""Cloudflare API code - example""" + +from __future__ import print_function + +import os +import sys +import re +import json +import requests + +sys.path.insert(0, os.path.abspath('..')) +import CloudFlare + +def my_ip_address(): + """Cloudflare API code - example""" + + # This list is adjustable - plus some v6 enabled services are needed + # url = 'http://myip.dnsomatic.com' + # url = 'http://www.trackip.net/ip' + # url = 'http://myexternalip.com/raw' + url = 'https://api.ipify.org' + try: + ip_address = requests.get(url).text + except: + exit('%s: failed' % (url)) + if ip_address == '': + exit('%s: failed' % (url)) + + if ':' in ip_address: + ip_address_type = 'AAAA' + else: + ip_address_type = 'A' + + return ip_address, ip_address_type + +def do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type): + """Cloudflare API code - example""" + + try: + params = {'name':dns_name, 'match':'all', 'type':ip_address_type} + dns_records = cf.zones.dns_records.get(zone_id, params=params) + except CloudFlare.exceptions.CloudFlareAPIError as e: + exit('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e)) + + updated = False + + # update the record - unless it's already correct + for dns_record in dns_records: + old_ip_address = dns_record['content'] + old_ip_address_type = dns_record['type'] + + if ip_address_type not in ['A', 'AAAA']: + # we only deal with A / AAAA records + continue + + if ip_address_type != old_ip_address_type: + # only update the correct address type (A or AAAA) + # we don't see this becuase of the search params above + print('IGNORED: %s %s ; wrong address family' % (dns_name, old_ip_address)) + continue + + if ip_address == old_ip_address: + print('UNCHANGED: %s %s' % (dns_name, ip_address)) + updated = True + continue + + # Yes, we need to update this record - we know it's the same address type + + dns_record_id = dns_record['id'] + dns_record = { + 'name':dns_name, + 'type':ip_address_type, + 'content':ip_address + } + try: + dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=dns_record) + except CloudFlare.exceptions.CloudFlareAPIError as e: + exit('/zones.dns_records.put %s - %d %s - api call failed' % (dns_name, e, e)) + print('UPDATED: %s %s -> %s' % (dns_name, old_ip_address, ip_address)) + updated = True + + if updated: + return + + # no exsiting dns record to update - so create dns record + dns_record = { + 'name':dns_name, + 'type':ip_address_type, + 'content':ip_address + } + try: + dns_record = cf.zones.dns_records.post(zone_id, data=dns_record) + except CloudFlare.exceptions.CloudFlareAPIError as e: + exit('/zones.dns_records.post %s - %d %s - api call failed' % (dns_name, e, e)) + print('CREATED: %s %s' % (dns_name, ip_address)) + +def main(): + """Cloudflare API code - example""" + + try: + dns_name = sys.argv[1] + except IndexError: + exit('usage: example-update-dynamic-dns.py fqdn-hostname') + + host_name, zone_name = dns_name.split('.', 1) + + ip_address, ip_address_type = my_ip_address() + + print('MY IP: %s %s' % (dns_name, ip_address)) + + cf = CloudFlare.CloudFlare() + + # grab the zone identifier + try: + params = {'name':zone_name} + zones = cf.zones.get(params=params) + except CloudFlare.exceptions.CloudFlareAPIError as e: + exit('/zones %d %s - api call failed' % (e, e)) + except Exception as e: + exit('/zones.get - %s - api call failed' % (e)) + + if len(zones) == 0: + exit('/zones.get - %s - zone not found' % (zone_name)) + + if len(zones) != 1: + exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones))) + + zone = zones[0] + + zone_name = zone['name'] + zone_id = zone['id'] + + do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type) + exit(0) + +if __name__ == '__main__': + main() +