diff options
Diffstat (limited to 'src/op_mode/dns.py')
| -rwxr-xr-x | src/op_mode/dns.py | 128 | 
1 files changed, 108 insertions, 20 deletions
| 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 <http://www.gnu.org/licenses/>. -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 | 
