From 6c80c47cc701cec60aec6eca6d55c9c5071b13f7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 29 Jul 2018 16:19:42 +0200 Subject: T758: XML/Python implementation for 'service dns dynamic' --- Makefile | 2 - interface-definitions/dynamic-dns.xml | 224 ++++++++++++++++++++++++++++++++++ src/conf_mode/dynamic_dns.py | 212 ++++++++++++++++++++++++++++++++ 3 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 interface-definitions/dynamic-dns.xml create mode 100755 src/conf_mode/dynamic_dns.py diff --git a/Makefile b/Makefile index 50710af47..d850036c5 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,6 @@ interface_definitions: # XXX: delete top level node.def's that now live in other packages rm -f $(TMPL_DIR)/system/node.def rm -f $(TMPL_DIR)/system/options/node.def - rm -f $(TMPL_DIR)/service/node.def - rm -f $(TMPL_DIR)/service/dns/node.def rm -f $(TMPL_DIR)/protocols/node.def .PHONY: op_mode_definitions diff --git a/interface-definitions/dynamic-dns.xml b/interface-definitions/dynamic-dns.xml new file mode 100644 index 000000000..e0b2cf764 --- /dev/null +++ b/interface-definitions/dynamic-dns.xml @@ -0,0 +1,224 @@ + + + + + + + + + + Dynamic DNS + 919 + + + + + Interface to send DDNS updates for [REQUIRED] + + + + + + + + RFC2136 Update name + + + + + File containing the secret key shared with remote DNS server [REQUIRED] + + file + File in /config/auth directory + + + + + + Record to be updated [REQUIRED] + + + + + + Server to be updated [REQUIRED] + + + + + Time To Live (default: 600) + + 1-86400 + DNS forwarding cache size + + + + + + + + + Zone to be updated [REQUIRED] + + + + + + + Service being used for Dynamic DNS [REQUIRED] + + custom + Custom or predefined service + + + afraid + + + + changeip + + + + cloudflare + + + + dnspark + + + + dslreports + + + + dyndns + + + + easydns + + + + namecheap + + + + noip + + + + sitelutions + + + + zoneedit + + + + + + + Hostname registered with DDNS service [REQUIRED] + + + + + + Login for DDNS service [REQUIRED] + + + + + Password for DDNS service [REQUIRED] + + + + + ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM] + + protocol + ddclient protocol + + + changeip + + + + cloudflare + + + + dnspark + + + + dslreports1 + + + + dyndns2 + + + + easydns + + + + namecheap + + + + noip + + + + sitelutions + + + + zoneedit1 + + + + + + + Server to send DDNS update to [REQUIRED FOR CUSTOM] + + IPv4 + IP address of DDNS server + + + FQDN + Hostname of DDNS server + + + + + + + + Web check used for obtaining the external IP address + + + + + Skip everything before this on the given URL + + + + + URL to obtain the current external IP address + + + + + + + + + + + + + diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py new file mode 100755 index 000000000..fa1102a90 --- /dev/null +++ b/src/conf_mode/dynamic_dns.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_dir = r'/etc/ddclient/' +config_file = config_dir + 'ddclient_{0}.conf' + +config_tmpl = """ +### Autogenerated by dynamic_dns.py ### +daemon=1m +syslog=yes +ssl=yes +pid=/var/run/ddclient/ddclient_{{ interface }}.pid +cache=/var/cache/ddclient/ddclient_{{ interface }}.cache +{% if web_url and web_skip -%} +use=web, web={{ web_url}}, web-skip={{ web_skip }} +{% else -%} +use=if, if={{ interface }} +{% endif -%} + +{% for rfc in rfc2136 -%} +{% for record in rfc.record %} +# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} +server={{ rfc.server }} +protocol=nsupdate +password={{ rfc.keyfile }} +ttl={{ rfc.ttl }} +zone={{ rfc.zone }} +{{ record }} +{% endfor %} +{% endfor -%} + +{% for srv in service %} +{% for host in srv.host %} +# DynDNS provider configuration for {{ host }} +protocol={{ srv.protocol }} +max-interval=28d +login={{ srv.login }} +password='{{ srv.password }}' +{{ host }} +{% endfor %} +{% endfor %} +""" + +# Mapping of service name to service protocol +default_service_protocol = { + 'afraid': 'freedns', + 'changeip': 'changeip', + 'cloudflare': 'cloudflare', + 'dnspark': 'dnspark', + 'dslreports': 'dslreports1', + 'dyndns': 'dyndns2', + 'easydns': 'easydns', + 'namecheap': 'namecheap', + 'noip': 'noip', + 'sitelutions': 'sitelutions', + 'zoneedit': 'zoneedit1' +} + +default_config_data = { + 'interfaces': [], +} + +def get_config(): + dyndns = default_config_data + conf = Config() + if not conf.exists('service dns dynamic'): + return None + else: + conf.set_level('service dns dynamic') + + for interface in conf.list_nodes('interface'): + node = { + 'interface': interface, + 'rfc2136': [], + 'service': [], + 'web_skip': '', + 'web_url': '' + } + + # set config level to e.g. "service dns dynamic interface eth0" + conf.set_level('service dns dynamic interface {0}'.format(interface)) + + # Handle RFC2136 - Dynamic Updates in the Domain Name System + for rfc2136 in conf.list_nodes('rfc2136'): + rfc = { + 'name': rfc2136, + 'keyfile': '', + 'record': [], + 'server': '', + 'ttl': '600', + 'zone': '' + } + + if conf.exists('rfc2136 {0} key'.format(rfc2136)): + rfc['keyfile'] = conf.return_value('rfc2136 {0} key'.format(rfc2136)) + + if conf.exists('rfc2136 {0} record'.format(rfc2136)): + rfc['record'] = conf.return_values('rfc2136 {0} record'.format(rfc2136)) + + if conf.exists('rfc2136 {0} server'.format(rfc2136)): + rfc['server'] = conf.return_value('rfc2136 {0} server'.format(rfc2136)) + + if conf.exists('rfc2136 {0} ttl'.format(rfc2136)): + rfc['ttl'] = conf.return_value('rfc2136 {0} ttl'.format(rfc2136)) + + if conf.exists('rfc2136 {0} zone'.format(rfc2136)): + rfc['zone'] = conf.return_value('rfc2136 {0} zone'.format(rfc2136)) + + node['rfc2136'].append(rfc) + + # Handle DynDNS service providers + for service in conf.list_nodes('service'): + srv = { + 'provider': service, + 'host': [], + 'login': '', + 'password': '', + 'protocol': '', + 'server': '' + } + + # preload protocol from default service mapping + if service in default_service_protocol.keys(): + srv['protocol'] = default_service_protocol[service] + + if conf.exists('service {0} login'.format(service)): + srv['login'] = conf.return_value('service {0} login'.format(service)) + + if conf.exists('service {0} host-name'.format(service)): + srv['host'] = conf.return_values('service {0} host-name'.format(service)) + + if conf.exists('service {0} protocol'.format(service)): + srv['protocol'] = conf.return_value('service {0} protocol'.format(service)) + + if conf.exists('service {0} password'.format(service)): + srv['password'] = conf.return_value('service {0} password'.format(service)) + + if conf.exists('service {0} server'.format(service)): + srv['server'] = conf.return_value('service {0} server'.format(service)) + + node['service'].append(srv) + + # Additional settings in CLI + if conf.exists('use-web skip'): + node['web_skip'] = conf.return_value('use-web skip') + + if conf.exists('use-web url'): + node['web_url'] = conf.return_value('use-web url') + + dyndns['interfaces'].append(node) + + return dyndns + +def verify(dyndns): + # bail out early - looks like removal from running config + if dyndns is None: + return None + + return None + +def generate(dyndns): + # bail out early - looks like removal from running config + if dyndns is None: + return None + + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + for node in dyndns['interfaces']: + tmpl = jinja2.Template(config_tmpl) + + config_text = tmpl.render(node) + with open(config_file.format(node['interface']), 'w') as f: + f.write(config_text) + + return None + +def apply(dns): + raise ConfigError("error") + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 3a02c31b5bc271f215faf5a7bdb0b4c6c21ef7d6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 29 Jul 2018 19:55:32 +0200 Subject: T758: add configuration validator --- src/conf_mode/dynamic_dns.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index fa1102a90..aabd2c499 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -138,12 +138,15 @@ def get_config(): 'login': '', 'password': '', 'protocol': '', - 'server': '' + 'server': '', + 'custom' : False } # preload protocol from default service mapping if service in default_service_protocol.keys(): srv['protocol'] = default_service_protocol[service] + else: + srv['custom'] = True if conf.exists('service {0} login'.format(service)): srv['login'] = conf.return_value('service {0} login'.format(service)) @@ -178,6 +181,44 @@ def verify(dyndns): if dyndns is None: return None + # A 'node' corresponds to an interface + for node in dyndns['interfaces']: + + # RFC2136 - configuration validation + for rfc2136 in node['rfc2136']: + if not rfc2136['record']: + raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['zone']: + raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['keyfile']: + raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + else: + if not os.path.isfile(rfc2136['keyfile']): + raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + # DynDNS service provider - configuration validation + for service in node['service']: + if not service['host']: + raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['login']: + raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['password']: + raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if service['custom'] is True: + if not service['protocol']: + raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + return None def generate(dyndns): -- cgit v1.2.3 From 078b328685493e27c2ea48206b3f72a3a0c42e20 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 29 Jul 2018 20:22:48 +0200 Subject: T758: refactor ddclient configuration file amd startup Since version 3.8.0 ddclient support the update of multiple ip's. The need for running multiple ddclient instances with different configuration files is thus no longer necessary. More information can be found on the ddclient forum: https://sourceforge.net/p/ddclient/mailman/message/20383414/ --- src/conf_mode/dynamic_dns.py | 47 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index aabd2c499..90d4ff567 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -23,23 +23,28 @@ import jinja2 from vyos.config import Config from vyos import ConfigError -config_dir = r'/etc/ddclient/' -config_file = config_dir + 'ddclient_{0}.conf' +config_file = r'/etc/ddclient.conf' config_tmpl = """ ### Autogenerated by dynamic_dns.py ### daemon=1m syslog=yes ssl=yes -pid=/var/run/ddclient/ddclient_{{ interface }}.pid -cache=/var/cache/ddclient/ddclient_{{ interface }}.cache -{% if web_url and web_skip -%} -use=web, web={{ web_url}}, web-skip={{ web_skip }} +pid=/var/run/ddclient/ddclient.pid +cache=/var/cache/ddclient/ddclient.cache + +{% for interface in interfaces -%} + +# +# ddclient configuration for interface "{{ interface.interface }}": +# +{% if interface.web_url and interface.web_skip -%} +use=web, web={{ interface.web_url}}, web-skip={{ interface.web_skip }} {% else -%} -use=if, if={{ interface }} +use=if, if={{ interface.interface }} {% endif -%} -{% for rfc in rfc2136 -%} +{% for rfc in interface.rfc2136 -%} {% for record in rfc.record %} # RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} server={{ rfc.server }} @@ -48,10 +53,10 @@ password={{ rfc.keyfile }} ttl={{ rfc.ttl }} zone={{ rfc.zone }} {{ record }} -{% endfor %} +{% endfor -%} {% endfor -%} -{% for srv in service %} +{% for srv in interface.service %} {% for host in srv.host %} # DynDNS provider configuration for {{ host }} protocol={{ srv.protocol }} @@ -60,6 +65,8 @@ login={{ srv.login }} password='{{ srv.password }}' {{ host }} {% endfor %} +{% endfor %} + {% endfor %} """ @@ -226,20 +233,20 @@ def generate(dyndns): if dyndns is None: return None - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - for node in dyndns['interfaces']: - tmpl = jinja2.Template(config_tmpl) + tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(node) - with open(config_file.format(node['interface']), 'w') as f: - f.write(config_text) + config_text = tmpl.render(dyndns) + with open(config_file, 'w') as f: + f.write(config_text) return None -def apply(dns): - raise ConfigError("error") +def apply(dyndns): + if dyndns is None: + os.system('/etc/init.d/ddclient stop') + else: + os.system('/etc/init.d/ddclient restart') + return None if __name__ == '__main__': -- cgit v1.2.3