first working version
This commit is contained in:
commit
d363fc0b8a
2 changed files with 212 additions and 0 deletions
5
README.md
Normal file
5
README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
Various tools to manage Gandi DNS entries.
|
||||||
|
|
||||||
|
Currently only able to update a DNS entry, add and remove commands coming soon.
|
||||||
|
|
207
updater.py
Executable file
207
updater.py
Executable file
|
@ -0,0 +1,207 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from requests import get, post, exceptions, put
|
||||||
|
|
||||||
|
logger = logging.getLogger('updater')
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
GANDI_API_TOKEN = None
|
||||||
|
GANDI_DOMAINS = []
|
||||||
|
GANDI_RECORD_TYPES = ['A', 'AAAA', 'CAA', 'CDS', 'CNAME',
|
||||||
|
'DNAME', 'DS', 'LOC', 'MX', 'NS', 'PTR',
|
||||||
|
'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT', 'WKS']
|
||||||
|
DRY_RUN = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_gandi_api_token():
|
||||||
|
try:
|
||||||
|
return os.environ["GANDI_API_TOKEN"]
|
||||||
|
except KeyError:
|
||||||
|
raise Exception("The GANDI_API_TOKEN environment variable is not set.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_gandi_domains():
|
||||||
|
try:
|
||||||
|
return os.environ["GANDI_DOMAINS"].split(",")
|
||||||
|
except KeyError:
|
||||||
|
raise Exception("The GANDI_DOMAINS environment variable is not set.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_domain_records(domain):
|
||||||
|
global GANDI_API_TOKEN
|
||||||
|
try:
|
||||||
|
uri = "https://dns.api.gandi.net/api/v5/domains/{}/records".format(domain)
|
||||||
|
headers = {'X-Api-Key': GANDI_API_TOKEN}
|
||||||
|
r = get(uri, headers=headers)
|
||||||
|
logger.debug("Gandi API response: {}".format(r.json()))
|
||||||
|
if not r.status_code == 200:
|
||||||
|
raise exceptions.HTTPError("HTTP error: {}: {}".format(r.status_code, r.reason))
|
||||||
|
else:
|
||||||
|
return r.json()
|
||||||
|
except exceptions.RequestException as e:
|
||||||
|
raise Exception("Failed to retrieve the records for domain {}: {}".format(domain, e))
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception("Unhandled exception while trying to retrieve the records for domain {}: {}".format(domain, e))
|
||||||
|
|
||||||
|
|
||||||
|
def get_records_www_ip(records):
|
||||||
|
ip = None
|
||||||
|
try:
|
||||||
|
for record in records:
|
||||||
|
if record["rrset_name"] == "@":
|
||||||
|
ip = record["rrset_values"][0]
|
||||||
|
break
|
||||||
|
if ip is None:
|
||||||
|
raise Exception("No IP configured in the WWW record.")
|
||||||
|
else:
|
||||||
|
return ip
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
def update_gandi_domain_records(records, old_ip, new_ip):
|
||||||
|
global GANDI_RECORD_TYPES
|
||||||
|
try:
|
||||||
|
for record in records:
|
||||||
|
if record['rrset_type'] in GANDI_RECORD_TYPES:
|
||||||
|
for index, rrset_value in enumerate(record['rrset_values']):
|
||||||
|
if rrset_value == old_ip:
|
||||||
|
record['rrset_values'][index] = new_ip
|
||||||
|
else:
|
||||||
|
logger.debug("{}: {} ({})".format(record['rrset_type'], rrset_value, index))
|
||||||
|
else:
|
||||||
|
logger.debug("Discarding record type {} not in targeted list {}.".format(record['rrset_type'],
|
||||||
|
GANDI_RECORD_TYPES))
|
||||||
|
return records
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception("Failure while updating the IP address for domain records: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
def push_updated_domain_records(domain, updated_records):
|
||||||
|
global GANDI_API_TOKEN, DRY_RUN
|
||||||
|
try:
|
||||||
|
uri = "https://dns.api.gandi.net/api/v5/domains/{}/records".format(domain)
|
||||||
|
headers = {'X-Api-Key': GANDI_API_TOKEN, 'Content-Type': 'application/json'}
|
||||||
|
data = {'items': updated_records}
|
||||||
|
if not DRY_RUN:
|
||||||
|
r = put(uri, headers=headers, data=json.dumps(data))
|
||||||
|
r.raise_for_status()
|
||||||
|
logger.info("Records updated for domain {}".format(domain))
|
||||||
|
else:
|
||||||
|
logger.info("URI: {}".format(uri))
|
||||||
|
logger.info("headers: {}".format(headers))
|
||||||
|
logger.info("data: {}".format(data))
|
||||||
|
except exceptions.HTTPError:
|
||||||
|
raise exceptions.HTTPError("HTTP error: {}: {}".format(r.status_code, r.json()))
|
||||||
|
except exceptions.RequestException as e:
|
||||||
|
raise Exception("Failed to push the updated records for domain {}: {}".format(domain, e))
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(
|
||||||
|
"Unhandled exception while trying to push the updated records for domain {}: {}".format(domain, e))
|
||||||
|
|
||||||
|
|
||||||
|
def get_livebox_wan_ip():
|
||||||
|
try:
|
||||||
|
params = '''{"service":"sah.Device.Information","method":"createContext","parameters":{
|
||||||
|
"applicationName": "so_sdkut",
|
||||||
|
"username": "admin",
|
||||||
|
"password": "wXbS7vjX"}}'''
|
||||||
|
headers = {"Content-type": "application/x-sah-ws-4-call+json", "Authorization":"X-Sah-Login"}
|
||||||
|
r = post("http://livebox.local/ws", headers=headers, data=params).json()
|
||||||
|
context = r["data"]["contextID"]
|
||||||
|
logger.debug(context)
|
||||||
|
|
||||||
|
params = '''{"service":"NMC","method":"getWANStatus","parameters":{}}'''
|
||||||
|
headers = {"Content-type": "application/x-sah-ws-4-call+json", "X-Context":context}
|
||||||
|
r = post("http://livebox.local/ws", headers=headers, data=params).json()
|
||||||
|
logger.debug("Livebox WS response: {}".format(r))
|
||||||
|
livebox_wan_ip = r["data"]["IPAddress"]
|
||||||
|
logger.info("Livebox WAN IP: {}".format(livebox_wan_ip))
|
||||||
|
return livebox_wan_ip
|
||||||
|
except exceptions.RequestException as e:
|
||||||
|
raise Exception("Failed to query the livebox webservices: {}".format(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception("Unhandled exception while querying the livebox webservices: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
global GANDI_API_TOKEN, GANDI_DOMAINS, GANDI_RECORD_TYPES, DRY_RUN
|
||||||
|
parser = argparse.ArgumentParser(description='Update the DNS records for Gandi registered domains.')
|
||||||
|
parser.add_argument('-d', '--daemon', dest='daemon', action='store_true',
|
||||||
|
help='Run in background.')
|
||||||
|
parser.add_argument('-i', '--interval', dest='time', action='store',
|
||||||
|
help='Time interval between checks. Default is %(default)ssec.',
|
||||||
|
default=10800)
|
||||||
|
parser.add_argument('-t', '--api-token', dest='api_token', action='store',
|
||||||
|
help='The Gandi API token to use.')
|
||||||
|
parser.add_argument('-n', '--domains', dest='domains', action='store',
|
||||||
|
help='A comma separated list of domains to update.')
|
||||||
|
parser.add_argument('-r', '--records', dest='records', action='store', choices=GANDI_RECORD_TYPES,
|
||||||
|
help='The record type to update. Default is %(default)s.', default='all')
|
||||||
|
parser.add_argument('-l', '--log', dest='log_level', action='store',
|
||||||
|
help='The log level to display. Default is %(default)s.',
|
||||||
|
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||||
|
default='INFO')
|
||||||
|
parser.add_argument('--dry-run', dest='dry_run', action='store_true',
|
||||||
|
help='Do not push the updated domain records.')
|
||||||
|
parser.add_argument('--set-ip', dest='custom_ip', action='store',
|
||||||
|
help='Update the domain records using the specified IP. '
|
||||||
|
'Default is to extract the WAN address from the livebox on the LAN.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
logger.setLevel(getattr(logging, args.log_level, None))
|
||||||
|
logger.debug("Daemon mode: {}".format(args.daemon))
|
||||||
|
if args.daemon: logger.debug("Time interval set: {}".format(args.time))
|
||||||
|
GANDI_API_TOKEN = args.api_token if args.api_token else get_gandi_api_token()
|
||||||
|
GANDI_DOMAINS = args.domains.split(',') if args.domains else get_gandi_domains()
|
||||||
|
logger.debug("Targeted domains: {}".format(GANDI_DOMAINS))
|
||||||
|
if not args.records == "all": GANDI_RECORD_TYPES = list(args.records)
|
||||||
|
logger.debug("Targeted record types: {}".format(GANDI_RECORD_TYPES))
|
||||||
|
DRY_RUN = args.dry_run
|
||||||
|
logger.debug("Dry run mode: {}".format(DRY_RUN))
|
||||||
|
if args.custom_ip:
|
||||||
|
wan_ip = args.custom_ip
|
||||||
|
logger.debug("User defined WAN IP: {}".format(wan_ip))
|
||||||
|
else:
|
||||||
|
wan_ip = get_livebox_wan_ip()
|
||||||
|
logger.debug("Livebox WAN IP: {}".format(wan_ip))
|
||||||
|
return wan_ip, args.daemon, int(args.time)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global GANDI_DOMAINS
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
wan_ip, daemon_mode, time_interval = parse_args()
|
||||||
|
for domain in GANDI_DOMAINS:
|
||||||
|
logger.info("Checking domain: {}".format(domain))
|
||||||
|
records = get_domain_records(domain)
|
||||||
|
gandi_ip = get_records_www_ip(records)
|
||||||
|
if not gandi_ip == wan_ip:
|
||||||
|
logger.info("The IP address for domain {} must be updated ({}).".format(domain, gandi_ip))
|
||||||
|
updated_domain_records = update_gandi_domain_records(records, gandi_ip, wan_ip)
|
||||||
|
push_updated_domain_records(domain, updated_domain_records)
|
||||||
|
else:
|
||||||
|
logger.info("The IP address configured for domain {} is valid: {}.".format(domain, gandi_ip))
|
||||||
|
if not daemon_mode:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.debug("Sleeping for {}sec".format(time_interval))
|
||||||
|
time.sleep(time_interval)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in a new issue