From 2080f8edd69cdf7eed4bcfb739e68300faf0681f Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 19 Jan 2024 23:47:46 -0600 Subject: ddclient: T5966: Streamline dynamic dns op-mode configuration Update op-mode for dynamic dns to standardize on `vyos.opmode`. All methods of `op_mode/dns_dynamic.py` are now available in standardized `op_mode/dns.py`. Move op-mode command `update dns dynamic` to `reset dns dynamic` to reflect that it is not an update but a reset of the dynamic dns service. Also, make the help texts more consistent for all op-mode commands for `dns dynamic` and `dns forwarding`. --- src/op_mode/dns.py | 128 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 20 deletions(-) (limited to 'src/op_mode/dns.py') diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index 309bef3b9..16c462f23 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -15,14 +15,33 @@ # along with this program. If not, see . -import typing +import os import sys +import time +import typing import vyos.opmode from tabulate import tabulate from vyos.configquery import ConfigTreeQuery from vyos.utils.process import cmd, rc_cmd +from vyos.template import is_ipv4, is_ipv6 + +_dynamic_cache_file = r'/run/ddclient/ddclient.cache' + +_dynamic_status_columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} +_forwarding_statistics_columns = { + 'cache-entries': 'Cache entries', + 'max-cache-entries': 'Max cache entries', + 'cache-size': 'Cache size', +} def _forwarding_data_to_dict(data, sep="\t") -> dict: """ @@ -50,37 +69,106 @@ def _forwarding_data_to_dict(data, sep="\t") -> dict: dictionary[key] = value return dictionary +def _get_dynamic_host_records_raw() -> dict: + + data = [] + + if os.path.isfile(_dynamic_cache_file): # A ddclient status file might not always exist + with open(_dynamic_cache_file, 'r') as f: + for line in f: + if line.startswith('#'): + continue + + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in list(_dynamic_status_columns.keys()) + ['ip', 'status']: # ip and status are legacy keys + props[k] = v + + # Extract IPv4 and IPv6 address and status from legacy keys + # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 + if 'ip' in props: + if is_ipv4(props['ip']): + props['ipv4'] = props['ip'] + props['status-ipv4'] = props['status'] + elif is_ipv6(props['ip']): + props['ipv6'] = props['ip'] + props['status-ipv6'] = props['status'] + del props['ip'] + + # Convert mtime to human readable format + if 'mtime' in props: + props['mtime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) + + data.append(props) + + return data + +def _get_dynamic_host_records_formatted(data): + data_entries = [] + for entry in data: + data_entries.append([entry.get(key) for key in _dynamic_status_columns.keys()]) + header = _dynamic_status_columns.values() + output = tabulate(data_entries, header, numalign='left') + return output def _get_forwarding_statistics_raw() -> dict: command = cmd('rec_control get-all') data = _forwarding_data_to_dict(command) - data['cache-size'] = "{0:.2f}".format( int( + data['cache-size'] = "{0:.2f} kbytes".format( int( cmd('rec_control get cache-bytes')) / 1024 ) return data - def _get_forwarding_statistics_formatted(data): - cache_entries = data.get('cache-entries') - max_cache_entries = data.get('max-cache-entries') - cache_size = data.get('cache-size') - data_entries = [[cache_entries, max_cache_entries, f'{cache_size} kbytes']] - headers = ["Cache entries", "Max cache entries" , "Cache size"] - output = tabulate(data_entries, headers, numalign="left") + data_entries = [] + data_entries.append([data.get(key) for key in _forwarding_statistics_columns.keys()]) + header = _forwarding_statistics_columns.values() + output = tabulate(data_entries, header, numalign='left') return output -def _verify_forwarding(func): - """Decorator checks if DNS Forwarding config exists""" +def _verify(target): + """Decorator checks if config for DNS related service exists""" from functools import wraps - @wraps(func) - def _wrapper(*args, **kwargs): - config = ConfigTreeQuery() - if not config.exists('service dns forwarding'): - raise vyos.opmode.UnconfiguredSubsystem('DNS Forwarding is not configured') - return func(*args, **kwargs) - return _wrapper + if target not in ['dynamic', 'forwarding']: + raise ValueError('Invalid target') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + if not config.exists(f'service dns {target}'): + _prefix = f'Dynamic DNS' if target == 'dynamic' else 'DNS Forwarding' + raise vyos.opmode.UnconfiguredSubsystem(f'{_prefix} is not configured') + return func(*args, **kwargs) + return _wrapper + return _verify_target + +@_verify('dynamic') +def show_dynamic_status(raw: bool): + host_data = _get_dynamic_host_records_raw() + if raw: + return host_data + else: + return _get_dynamic_host_records_formatted(host_data) -@_verify_forwarding +@_verify('dynamic') +def reset_dynamic(): + """ + Reset Dynamic DNS cache + """ + if os.path.exists(_dynamic_cache_file): + os.remove(_dynamic_cache_file) + rc, output = rc_cmd('systemctl restart ddclient.service') + if rc != 0: + print(output) + return None + print(f'Dynamic DNS state reset!') + +@_verify('forwarding') def show_forwarding_statistics(raw: bool): dns_data = _get_forwarding_statistics_raw() if raw: @@ -88,7 +176,7 @@ def show_forwarding_statistics(raw: bool): else: return _get_forwarding_statistics_formatted(dns_data) -@_verify_forwarding +@_verify('forwarding') def reset_forwarding(all: bool, domain: typing.Optional[str]): """ Reset DNS Forwarding cache -- cgit v1.2.3