Initial commit.
This commit is contained in:
commit
8c1dc8f9ec
2 changed files with 162 additions and 0 deletions
157
main.py
Executable file
157
main.py
Executable file
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
import yaml
|
||||
import schema
|
||||
import click
|
||||
import ovh
|
||||
import dns.resolver
|
||||
import dns.rdatatype
|
||||
|
||||
from schema import And, Optional, Use, Schema
|
||||
|
||||
logger = logging.getLogger('ovhdns')
|
||||
|
||||
config_schema = Schema({
|
||||
'ovh': {
|
||||
'application_key': Use(str),
|
||||
'application_secret': Use(str),
|
||||
'consumer_key': Use(str),
|
||||
'endpoint': Use(str),
|
||||
},
|
||||
'zone': Use(str),
|
||||
'subdomain': Use(str),
|
||||
Optional('field_type', default='A'): And(Use(str), Use(str.upper), dns.rdatatype.RdataType.make),
|
||||
Optional('logging', default={'level': ':WARNING,ovhdns:INFO'}): {
|
||||
Optional('level', default=':WARNING,ovhdns:INFO'): Use(str),
|
||||
},
|
||||
Optional('ttl', default=None): Use(lambda i: int(i) if i is not None else None),
|
||||
})
|
||||
|
||||
def get_opendns_ips():
|
||||
ips = []
|
||||
for addr, typ in itertools.product(
|
||||
['resolver1.opendns.com', 'resolver2.opendns.com'],
|
||||
['A', 'AAAA'],
|
||||
):
|
||||
ips += [r.to_text() for r in dns.resolver.resolve(addr, typ)]
|
||||
return ips
|
||||
|
||||
def get_my_ips():
|
||||
resolver = dns.resolver.Resolver(configure=False)
|
||||
resolver.nameservers = get_opendns_ips()
|
||||
return [r.to_text() for r in resolver.resolve('myip.opendns.com')]
|
||||
|
||||
|
||||
def get_records(client, config):
|
||||
zone = config['zone']
|
||||
records = client.get(f'/domain/zone/{zone}/record')
|
||||
records = [
|
||||
client.get(f'/domain/zone/{zone}/record/{id}')
|
||||
for id in records
|
||||
]
|
||||
return records
|
||||
|
||||
def init_logging():
|
||||
logging.getLogger().addHandler(
|
||||
logging.StreamHandler(),
|
||||
)
|
||||
|
||||
def set_logging_level(level: str):
|
||||
levels = {
|
||||
'NOTSET': logging.NOTSET,
|
||||
'DEBUG': logging.DEBUG,
|
||||
'INFO': logging.INFO,
|
||||
'WARNING': logging.WARNING,
|
||||
'WARN': logging.WARNING,
|
||||
'ERROR': logging.ERROR,
|
||||
'CRITICAL': logging.CRITICAL,
|
||||
}
|
||||
|
||||
log_levels = [x.split(':') for x in level.split(',')]
|
||||
for module, level in log_levels:
|
||||
module = module.strip() if module else None
|
||||
level = level.strip().upper()
|
||||
if level not in levels:
|
||||
raise ValueError(f'invalid log level, allowed values: {repr(set(levels.keys()))}')
|
||||
logging.getLogger(module).setLevel(levels[level])
|
||||
|
||||
def load_config(file):
|
||||
if file == '-':
|
||||
config = yaml.safe_load(sys.stdin)
|
||||
else:
|
||||
if os.stat(file).st_mode & 0o777 & ~0o600:
|
||||
raise Exception('refusing to load insecure configuration file, file must have permission 0o600')
|
||||
with open(file) as fp:
|
||||
config = yaml.safe_load(fp)
|
||||
return config_schema.validate(config)
|
||||
|
||||
@click.command()
|
||||
@click.option('--config', '-c', envvar='OVHDNS_CONFIG', default='-', help='The configuration file')
|
||||
@click.option('--level', '-l', envvar='OVHDNS_LEVEL', default=None, help='The log level for the application')
|
||||
@click.option('--force-refresh', '-f', envvar='OVHDNS_FORCE_REFRESH', is_flag=True, help='Force zone refresh even if deemed not needed')
|
||||
def main(config, level, force_refresh):
|
||||
init_logging()
|
||||
config = load_config(config)
|
||||
if level:
|
||||
config['logging']['level'] = level
|
||||
config = config_schema.validate(config)
|
||||
set_logging_level(config['logging']['level'])
|
||||
|
||||
ovh_client = ovh.Client(
|
||||
**config['ovh'],
|
||||
)
|
||||
records = [
|
||||
r
|
||||
for r in get_records(ovh_client, {'zone': 'redxef.at'})
|
||||
if r['fieldType'] == config['field_type']
|
||||
and r['zone'] == config['zone']
|
||||
and r['subDomain'] == config['subdomain']
|
||||
]
|
||||
logger.info('found matching records: %s', records)
|
||||
if len(records) > 1:
|
||||
logger.error('found more than one record, don\'t know which to pick')
|
||||
sys.exit(1)
|
||||
|
||||
my_ips = get_my_ips()
|
||||
need_refresh = False
|
||||
if len(records) == 1:
|
||||
logger.info('record already present, updating if needed')
|
||||
if my_ips[0] == records[0]['target']:
|
||||
logger.info('ip is up-to-date, skipping update')
|
||||
else:
|
||||
need_refresh = True
|
||||
ovh_client.put(
|
||||
f'/domain/zone/{config["zone"]}/record/{records[0]["id"]}',
|
||||
target=my_ips[0],
|
||||
ttl=config['ttl'],
|
||||
)
|
||||
logger.info('updated record, new ip is %s', my_ips[0])
|
||||
|
||||
else:
|
||||
logger.info('record not already present, creating new one')
|
||||
need_refresh = True
|
||||
ovh_client.post(
|
||||
f'/domain/zone/{config["zone"]}/record/',
|
||||
subDomain=config['subdomain'],
|
||||
target=my_ips[0],
|
||||
fieldType=config['field_type'],
|
||||
ttl=config['ttl'],
|
||||
)
|
||||
logger.info('created new record, ip is %s', my_ips[0])
|
||||
|
||||
if need_refresh or force_refresh:
|
||||
ovh_client.post(f'/domain/zone/{config["zone"]}/refresh')
|
||||
logger.info('refreshed zone')
|
||||
else:
|
||||
logger.info('zone refresh skipped')
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
pyyaml
|
||||
schema
|
||||
click
|
||||
ovh
|
||||
dnspython
|
Loading…
Reference in a new issue