summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/dns.py170
-rwxr-xr-xsrc/op_mode/dns_dynamic.py113
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py54
-rwxr-xr-xsrc/op_mode/dns_forwarding_restart.sh8
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py32
-rwxr-xr-xsrc/op_mode/firewall.py57
-rwxr-xr-xsrc/op_mode/image_installer.py12
-rwxr-xr-xsrc/op_mode/multicast.py72
-rwxr-xr-xsrc/op_mode/show_openvpn.py6
9 files changed, 266 insertions, 258 deletions
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index 2168aef89..16c462f23 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-2024 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
@@ -15,17 +15,35 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+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
-
-import vyos.opmode
-
-
-def _data_to_dict(data, sep="\t") -> dict:
+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:
"""
Return dictionary from plain text
separated by tab
@@ -51,37 +69,135 @@ def _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)
-def _get_raw_forwarding_statistics() -> dict:
- command = cmd('rec_control --socket-dir=/run/powerdns get-all')
- data = _data_to_dict(command)
- data['cache-size'] = "{0:.2f}".format( int(
- cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
return data
-
-def _get_formatted_forwarding_statistics(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")
+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} kbytes".format( int(
+ cmd('rec_control get cache-bytes')) / 1024 )
+ return data
-def show_forwarding_statistics(raw: bool):
+def _get_forwarding_statistics_formatted(data):
+ 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
- config = ConfigTreeQuery()
- if not config.exists('service dns forwarding'):
- raise vyos.opmode.UnconfiguredSubsystem('DNS forwarding is not configured')
+def _verify(target):
+ """Decorator checks if config for DNS related service exists"""
+ from functools import wraps
+
+ 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)
- dns_data = _get_raw_forwarding_statistics()
+@_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:
return dns_data
else:
- return _get_formatted_forwarding_statistics(dns_data)
+ return _get_forwarding_statistics_formatted(dns_data)
+
+@_verify('forwarding')
+def reset_forwarding(all: bool, domain: typing.Optional[str]):
+ """
+ Reset DNS Forwarding cache
+ :param all (bool): reset cache all domains
+ :param domain (str): reset cache for specified domain
+ """
+ if all:
+ rc, output = rc_cmd('rec_control wipe-cache ".$"')
+ if rc != 0:
+ print(output)
+ return None
+ print('DNS Forwarding cache reset for all domains!')
+ return output
+ elif domain:
+ rc, output = rc_cmd(f'rec_control wipe-cache "{domain}$"')
+ if rc != 0:
+ print(output)
+ return None
+ print(f'DNS Forwarding cache reset for domain "{domain}"!')
+ return output
if __name__ == '__main__':
try:
diff --git a/src/op_mode/dns_dynamic.py b/src/op_mode/dns_dynamic.py
deleted file mode 100755
index 12aa5494a..000000000
--- a/src/op_mode/dns_dynamic.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2023 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 <http://www.gnu.org/licenses/>.
-
-import os
-import argparse
-import sys
-import time
-from tabulate import tabulate
-
-from vyos.config import Config
-from vyos.template import is_ipv4, is_ipv6
-from vyos.utils.process import call
-
-cache_file = r'/run/ddclient/ddclient.cache'
-
-columns = {
- 'host': 'Hostname',
- 'ipv4': 'IPv4 address',
- 'status-ipv4': 'IPv4 status',
- 'ipv6': 'IPv6 address',
- 'status-ipv6': 'IPv6 status',
- 'mtime': 'Last update',
-}
-
-
-def _get_formatted_host_records(host_data):
- data_entries = []
- for entry in host_data:
- data_entries.append([entry.get(key) for key in columns.keys()])
-
- header = columns.values()
- output = tabulate(data_entries, header, numalign='left')
- return output
-
-
-def show_status():
- # A ddclient status file might not always exist
- if not os.path.exists(cache_file):
- sys.exit(0)
-
- data = []
-
- with open(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(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)
-
- print(_get_formatted_host_records(data))
-
-
-def update_ddns():
- call('systemctl stop ddclient.service')
- if os.path.exists(cache_file):
- os.remove(cache_file)
- call('systemctl start ddclient.service')
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- group = parser.add_mutually_exclusive_group()
- group.add_argument("--status", help="Show DDNS status", action="store_true")
- group.add_argument("--update", help="Update DDNS on a given interface", action="store_true")
- args = parser.parse_args()
-
- # Do nothing if service is not configured
- c = Config()
- if not c.exists_effective('service dns dynamic'):
- print("Dynamic DNS not configured")
- sys.exit(1)
-
- if args.status:
- show_status()
- elif args.update:
- update_ddns()
diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py
deleted file mode 100755
index 55e20918f..000000000
--- a/src/op_mode/dns_forwarding_reset.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/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 <http://www.gnu.org/licenses/>.
-#
-# File: vyos-show-version
-# Purpose:
-# Displays image version and system information.
-# Used by the "run show version" command.
-
-
-import os
-import argparse
-
-from sys import exit
-from vyos.config import Config
-from vyos.utils.process import call
-
-PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-a", "--all", action="store_true", help="Reset all cache")
-parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for")
-
-if __name__ == '__main__':
- args = parser.parse_args()
-
- # Do nothing if service is not configured
- c = Config()
- if not c.exists_effective(['service', 'dns', 'forwarding']):
- print("DNS forwarding is not configured")
- exit(0)
-
- if args.all:
- call(f"{PDNS_CMD} wipe-cache \'.$\'")
- exit(0)
-
- elif args.domain:
- call(f"{PDNS_CMD} wipe-cache \'{0}$\'".format(args.domain))
-
- else:
- parser.print_help()
- exit(1)
diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh
deleted file mode 100755
index 64cc92115..000000000
--- a/src/op_mode/dns_forwarding_restart.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-if cli-shell-api existsEffective service dns forwarding; then
- echo "Restarting the DNS forwarding service"
- systemctl restart pdns-recursor.service
-else
- echo "DNS forwarding is not configured"
-fi
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
deleted file mode 100755
index 32b5c76a7..000000000
--- a/src/op_mode/dns_forwarding_statistics.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-
-import jinja2
-from sys import exit
-
-from vyos.config import Config
-from vyos.utils.process import cmd
-
-PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
-
-OUT_TMPL_SRC = """
-DNS forwarding statistics:
-
-Cache entries: {{ cache_entries }}
-Cache size: {{ cache_size }} kbytes
-
-"""
-
-if __name__ == '__main__':
- # Do nothing if service is not configured
- c = Config()
- if not c.exists_effective('service dns forwarding'):
- print("DNS forwarding is not configured")
- exit(0)
-
- data = {}
-
- data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries')
- data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 )
-
- tmpl = jinja2.Template(OUT_TMPL_SRC)
- print(tmpl.render(data))
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 36bb013fe..4dcffc412 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -327,6 +327,8 @@ def show_firewall_group(name=None):
dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type)
in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group')
out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group')
+ dyn_group_source = dict_search_args(rule_conf, 'add_address_to_group', 'source_address', group_type)
+ dyn_group_dst = dict_search_args(rule_conf, 'add_address_to_group', 'destination_address', group_type)
if source_group:
if source_group[0] == "!":
source_group = source_group[1:]
@@ -348,6 +350,14 @@ def show_firewall_group(name=None):
if group_name == out_interface:
out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+ if dyn_group_source:
+ if group_name == dyn_group_source:
+ out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+ if dyn_group_dst:
+ if group_name == dyn_group_dst:
+ out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+
+
# Look references in route | route6
for name_type in ['route', 'route6']:
if name_type not in policy:
@@ -423,26 +433,37 @@ def show_firewall_group(name=None):
rows = []
for group_type, group_type_conf in firewall['group'].items():
- for group_name, group_conf in group_type_conf.items():
- if name and name != group_name:
- continue
+ ##
+ if group_type != 'dynamic_group':
- references = find_references(group_type, group_name)
- row = [group_name, group_type, '\n'.join(references) or 'N/D']
- if 'address' in group_conf:
- row.append("\n".join(sorted(group_conf['address'])))
- elif 'network' in group_conf:
- row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
- elif 'mac_address' in group_conf:
- row.append("\n".join(sorted(group_conf['mac_address'])))
- elif 'port' in group_conf:
- row.append("\n".join(sorted(group_conf['port'])))
- elif 'interface' in group_conf:
- row.append("\n".join(sorted(group_conf['interface'])))
- else:
- row.append('N/D')
- rows.append(row)
+ for group_name, group_conf in group_type_conf.items():
+ if name and name != group_name:
+ continue
+ references = find_references(group_type, group_name)
+ row = [group_name, group_type, '\n'.join(references) or 'N/D']
+ if 'address' in group_conf:
+ row.append("\n".join(sorted(group_conf['address'])))
+ elif 'network' in group_conf:
+ row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
+ elif 'mac_address' in group_conf:
+ row.append("\n".join(sorted(group_conf['mac_address'])))
+ elif 'port' in group_conf:
+ row.append("\n".join(sorted(group_conf['port'])))
+ elif 'interface' in group_conf:
+ row.append("\n".join(sorted(group_conf['interface'])))
+ else:
+ row.append('N/D')
+ rows.append(row)
+
+ else:
+ for dynamic_type in ['address_group', 'ipv6_address_group']:
+ if dynamic_type in firewall['group']['dynamic_group']:
+ for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items():
+ references = find_references(dynamic_type, dynamic_name)
+ row = [dynamic_name, dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D']
+ row.append('N/D')
+ rows.append(row)
if rows:
print('Firewall Groups\n')
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index fad6face7..501e9b804 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -69,8 +69,8 @@ MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continu
MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again'
-MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'
-'It must be between 1 and 32 characters long and contains only the next characters: .+-_ a-z A-Z 0-9'
+MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\
+'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9'
CONST_MIN_DISK_SIZE: int = 2147483648 # 2 GB
CONST_MIN_ROOT_SIZE: int = 1610612736 # 1.5 GB
# a reserved space: 2MB for header, 1 MB for BIOS partition, 256 MB for EFI
@@ -812,7 +812,11 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed')
if not no_prompt:
- image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
+ while True:
+ image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
+ if image.validate_name(image_name):
+ break
+ print(MSG_WARN_IMAGE_NAME_WRONG)
set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True)
else:
image_name: str = version_name
@@ -867,7 +871,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
except Exception as err:
# unmount an ISO and cleanup
cleanup([str(iso_path)])
- exit(f'Whooops: {err}')
+ exit(f'Error: {err}')
def parse_arguments() -> Namespace:
diff --git a/src/op_mode/multicast.py b/src/op_mode/multicast.py
new file mode 100755
index 000000000..0666f8af3
--- /dev/null
+++ b/src/op_mode/multicast.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
+
+import json
+import sys
+import typing
+
+from tabulate import tabulate
+from vyos.utils.process import cmd
+
+import vyos.opmode
+
+ArgFamily = typing.Literal['inet', 'inet6']
+
+def _get_raw_data(family, interface=None):
+ tmp = 'ip -4'
+ if family == 'inet6':
+ tmp = 'ip -6'
+ tmp = f'{tmp} -j maddr show'
+ if interface:
+ tmp = f'{tmp} dev {interface}'
+ output = cmd(tmp)
+ data = json.loads(output)
+ if not data:
+ return []
+ return data
+
+def _get_formatted_output(raw_data):
+ data_entries = []
+
+ # sort result by interface name
+ for interface in sorted(raw_data, key=lambda x: x['ifname']):
+ for address in interface['maddr']:
+ tmp = []
+ tmp.append(interface['ifname'])
+ tmp.append(address['family'])
+ tmp.append(address['address'])
+
+ data_entries.append(tmp)
+
+ headers = ["Interface", "Family", "Address"]
+ output = tabulate(data_entries, headers, numalign="left")
+ return output
+
+def show_group(raw: bool, family: ArgFamily, interface: typing.Optional[str]):
+ multicast_data = _get_raw_data(family=family, interface=interface)
+ if raw:
+ return multicast_data
+ else:
+ return _get_formatted_output(multicast_data)
+
+if __name__ == "__main__":
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index e29e594a5..6abafc8b6 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -63,9 +63,11 @@ def get_vpn_tunnel_address(peer, interface):
# filter out subnet entries
lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
- tunnel_ip = lst[0].split(',')[0]
+ if lst:
+ tunnel_ip = lst[0].split(',')[0]
+ return tunnel_ip
- return tunnel_ip
+ return 'n/a'
def get_status(mode, interface):
status_file = '/var/run/openvpn/{}.status'.format(interface)