#!/usr/bin/env python3 __author__ = 'xtof' import argparse import re import logging import sys from subprocess import Popen, PIPE from docker import Client from docker.utils import kwargs_from_env from dns.resolver import Resolver from dns.exception import DNSException # Templates for nsupdate zone_update_start_template = """server {0} zone {1}. """ zone_update_template = """update delete {0}.{1} update add {0}.{1} 60 A {2} """ zone_update_add_alias_template = """update delete {0}.{1} update add {0}.{1} 600 CNAME {2}.{1}. update add {2}.{1} 600 TXT dockerDDNS-alias:{0}: """ zone_update_delete_record_template = """update delete {0}.{1} """ def register_container(container_id): detail = c.inspect_container(container_id) container_hostname = detail["Config"]["Hostname"] container_name = detail["Name"].split('/', 1)[1] network_mode = detail["HostConfig"]["NetworkMode"] if network_mode == "default": container_ip = detail["NetworkSettings"]["IPAddress"] else : container_ip = detail["NetworkSettings"]["Networks"][network_mode]["IPAddress"] logging.info("Updating %s to ip (%s|%s) -> %s", container_id, container_hostname, container_name, container_ip) if not args.dry_run: nsupdate = Popen(['nsupdate', '-k', args.key], stdin=PIPE) nsupdate.stdin.write(bytes(zone_update_start_template.format(args.server, args.zone), "UTF-8")) nsupdate.stdin.write(bytes(zone_update_template.format(container_hostname, args.domain, container_ip), "UTF-8")) if container_name != container_hostname: nsupdate.stdin.write(bytes(zone_update_add_alias_template.format(container_name, args.domain, container_hostname), "UTF-8")) if re.search("_", container_name): alternate_name = re.sub('_','-',container_name) logging.info("Adding alternate name %s to %s", alternate_name, container_name) nsupdate.stdin.write(bytes(zone_update_add_alias_template.format(alternate_name, args.domain, container_hostname), "UTF-8")) nsupdate.stdin.write(bytes("send\n", "UTF-8")) nsupdate.stdin.close() def remove_container(container_id): logging.info("Destroying %s", container_id) short_id = container_id[:12] record_to_delete = [short_id] logging.debug("Looking for alias to %s.%s", short_id, args.domain) try: answers = resolver.query("{0}.{1}.".format(short_id, args.domain), "TXT", raise_on_no_answer=False).rrset if answers: for answer in answers: logging.debug("Checking TXT record %s for alias", answer) match = re.search(r"dockerDDNS-alias:([^:]+):", answer.to_text()) if match: record_to_delete.append(match.group(1)) except DNSException as e: logging.error("Cannot get TXT record for %s: %s", short_id, e) except: logging.error("Unexpected error: %s", sys.exc_info()[0]) raise if not args.dry_run: nsupdate = Popen(['nsupdate', '-k', args.key], stdin=PIPE) nsupdate.stdin.write(bytes(zone_update_start_template.format(args.server, args.zone), "UTF-8")) for record in record_to_delete: logging.info("Removing record for %s", record) nsupdate.stdin.write(bytes(zone_update_delete_record_template.format(record, args.domain), "UTF-8")) nsupdate.stdin.write(bytes("send\n", "UTF-8")) nsupdate.stdin.close() parser = argparse.ArgumentParser() parser.add_argument("--key", required=True, help="Path to the dynamic dns key") parser.add_argument("--server", help="IP/Hostname of the server to update", default="127.0.0.1") parser.add_argument("--domain", help="The domain to be updated", required=True) parser.add_argument("--zone", help="The zone to be updated (default to the domain)") parser.add_argument("--dry-run", help="Run in dry run mode without doing any update", default=False, action="store_true") parser.add_argument("--catchup", help="Register the running containers on startup", default=False, action="store_true") parser.add_argument("--log-level", help="Log level to display", default="INFO") parser.add_argument("--log-file", help="Where to put the logs", default="/var/log/docker-ddns.log") args = parser.parse_args() logging.basicConfig(level=getattr(logging,args.log_level.upper()), format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', filename=(args.log_file if args.log_file != '-' else None)) if args.zone is None: args.zone = args.domain logging.info("Starting with arguments %s", args) c = Client(**(kwargs_from_env())) resolver = Resolver() resolver.nameservers = [args.server] if args.catchup: logging.info("Registering existing containers") containers = c.containers() for container in containers: register_container(container["Id"]) # TODO use docker-py streaming API events_pipe = Popen(['docker', 'events'], stdout=PIPE) while True: line = events_pipe.stdout.readline() if line != '': text_line = line.decode().rstrip() logging.debug("Read line %s", text_line) m = re.search(r"\s+([0-9a-f]{64}):.*\s+([a-z]+)\s*$", text_line) if m: event = m.group(2) container_id = m.group(1) logging.debug("Got event %s for container %s", event, container_id) if event == "start": register_container(container_id) elif event == "destroy": remove_container(container_id) else: print("Done return code: ", events_pipe.returncode) break # 2014-11-28T15:32:04.000000000+01:00 a3d66b00acc9adbdbdbc91cc664d2d94b6a07cc4295c5cf54fcc595e2aa92a43: (from mongo:latest) restart # 2015-03-05T08:36:14.000000000+01:00 eb75c1a5ad836d008b0fd66bf6b1ea353510175e8caa619e59d9851029b1ceca: (from ggtools/zabbix-server:latest) exec_start: ifconfig eth0