summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/completion/list_ipsec_profile_tunnels.py48
-rwxr-xr-xsrc/completion/list_ntp_servers.sh4
-rwxr-xr-xsrc/conf_mode/container.py142
-rwxr-xr-xsrc/conf_mode/dhcp_server.py2
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py38
-rwxr-xr-xsrc/conf_mode/firewall.py10
-rwxr-xr-xsrc/conf_mode/https.py2
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py5
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py4
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py97
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py2
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook (renamed from src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook)0
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks (renamed from src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks)0
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook (renamed from src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook)0
-rw-r--r--src/etc/systemd/system/frr.service.d/override.conf1
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/3-to-449
-rwxr-xr-xsrc/migration-scripts/firewall/9-to-1080
-rwxr-xr-xsrc/op_mode/accelppp.py6
-rwxr-xr-xsrc/op_mode/dns.py4
-rwxr-xr-xsrc/op_mode/interfaces.py18
-rwxr-xr-xsrc/op_mode/ipsec.py141
-rwxr-xr-xsrc/op_mode/openvpn.py26
-rwxr-xr-xsrc/op_mode/reset_vpn.py75
-rwxr-xr-xsrc/op_mode/sflow.py108
-rwxr-xr-xsrc/op_mode/show_interfaces.py310
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py61
-rw-r--r--src/services/api/graphql/generate/config_session_function.py2
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_composite.py72
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_config_session.py72
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_op_mode.py3
-rw-r--r--src/services/api/graphql/graphql/client_op/auth_token.graphql10
-rwxr-xr-xsrc/services/vyos-hostsd2
-rw-r--r--src/tests/test_config_diff.py70
-rw-r--r--src/tests/test_config_parser.py4
34 files changed, 939 insertions, 529 deletions
diff --git a/src/completion/list_ipsec_profile_tunnels.py b/src/completion/list_ipsec_profile_tunnels.py
new file mode 100644
index 000000000..df6c52f6d
--- /dev/null
+++ b/src/completion/list_ipsec_profile_tunnels.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 sys
+import argparse
+
+from vyos.config import Config
+from vyos.util import dict_search
+
+def get_tunnels_from_ipsecprofile(profile):
+ config = Config()
+ base = ['vpn', 'ipsec', 'profile', profile, 'bind']
+ profile_conf = config.get_config_dict(base, effective=True, key_mangling=('-', '_'))
+ tunnels = []
+
+ try:
+ for tunnel in (dict_search('bind.tunnel', profile_conf) or []):
+ tunnels.append(tunnel)
+ except:
+ pass
+
+ return tunnels
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-p", "--profile", type=str, help="List tunnels per profile")
+ args = parser.parse_args()
+
+ tunnels = []
+
+ tunnels = get_tunnels_from_ipsecprofile(args.profile)
+
+ print(" ".join(tunnels))
+
diff --git a/src/completion/list_ntp_servers.sh b/src/completion/list_ntp_servers.sh
deleted file mode 100755
index d0977fbd6..000000000
--- a/src/completion/list_ntp_servers.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-# Completion script used to select specific NTP server
-/bin/cli-shell-api -- listEffectiveNodes system ntp server | sed "s/'//g"
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 68070ea5b..05595f86f 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -16,6 +16,7 @@
import os
+from hashlib import sha256
from ipaddress import ip_address
from ipaddress import ip_network
from json import dumps as json_write
@@ -24,6 +25,9 @@ from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import Interface
from vyos.util import call
from vyos.util import cmd
from vyos.util import run
@@ -38,8 +42,9 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_containers_registry = '/etc/containers/registries.conf'
-config_containers_storage = '/etc/containers/storage.conf'
+config_containers = '/etc/containers/containers.conf'
+config_registry = '/etc/containers/registries.conf'
+config_storage = '/etc/containers/storage.conf'
systemd_unit_path = '/run/systemd/system'
def _cmd(command):
@@ -83,6 +88,15 @@ def get_config(config=None):
for name in container['name']:
container['name'][name] = dict_merge(default_values, container['name'][name])
+ # T5047: Any container related configuration changed? We only
+ # wan't to restart the required containers and not all of them ...
+ tmp = is_node_changed(conf, base + ['name', name])
+ if tmp:
+ if 'container_restart' not in container:
+ container['container_restart'] = [name]
+ else:
+ container['container_restart'].append(name)
+
# XXX: T2665: we can not safely rely on the defaults() when there are
# tagNodes in place, it is better to blend in the defaults manually.
if 'port' in container['name'][name]:
@@ -154,21 +168,29 @@ def verify(container):
raise ConfigError(f'Container network "{network_name}" does not exist!')
if 'address' in container_config['network'][network_name]:
- address = container_config['network'][network_name]['address']
- network = None
- if is_ipv4(address):
- network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0]
- elif is_ipv6(address):
- network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0]
-
- # Specified container IP address must belong to network prefix
- if ip_address(address) not in ip_network(network):
- raise ConfigError(f'Used container address "{address}" not in network "{network}"!')
-
- # We can not use the first IP address of a network prefix as this is used by podman
- if ip_address(address) == ip_network(network)[1]:
- raise ConfigError(f'IP address "{address}" can not be used for a container, '\
- 'reserved for the container engine!')
+ cnt_ipv4 = 0
+ cnt_ipv6 = 0
+ for address in container_config['network'][network_name]['address']:
+ network = None
+ if is_ipv4(address):
+ network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0]
+ cnt_ipv4 += 1
+ elif is_ipv6(address):
+ network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0]
+ cnt_ipv6 += 1
+
+ # Specified container IP address must belong to network prefix
+ if ip_address(address) not in ip_network(network):
+ raise ConfigError(f'Used container address "{address}" not in network "{network}"!')
+
+ # We can not use the first IP address of a network prefix as this is used by podman
+ if ip_address(address) == ip_network(network)[1]:
+ raise ConfigError(f'IP address "{address}" can not be used for a container, '\
+ 'reserved for the container engine!')
+
+ if cnt_ipv4 > 1 or cnt_ipv6 > 1:
+ raise ConfigError(f'Only one IP address per address family can be used for '\
+ f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!')
if 'device' in container_config:
for dev, dev_config in container_config['device'].items():
@@ -230,6 +252,8 @@ def verify(container):
if v6_prefix > 1:
raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!')
+ # Verify VRF exists
+ verify_vrf(network_config)
# A network attached to a container can not be deleted
if {'network_remove', 'name'} <= set(container):
@@ -238,9 +262,11 @@ def verify(container):
if 'network' in container_config and network in container_config['network']:
raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!')
- if 'registry' in container and 'authentication' in container['registry']:
- for registry, registry_config in container['registry']['authentication'].items():
- if not {'username', 'password'} <= set(registry_config):
+ if 'registry' in container:
+ for registry, registry_config in container['registry'].items():
+ if 'authentication' not in registry_config:
+ continue
+ if not {'username', 'password'} <= set(registry_config['authentication']):
raise ConfigError('If registry username or or password is defined, so must be the other!')
return None
@@ -326,51 +352,47 @@ def generate_run_arguments(name, container_config):
ip_param = ''
networks = ",".join(container_config['network'])
for network in container_config['network']:
- if 'address' in container_config['network'][network]:
- address = container_config['network'][network]['address']
- ip_param = f'--ip {address}'
+ if 'address' not in container_config['network'][network]:
+ continue
+ for address in container_config['network'][network]['address']:
+ if is_ipv6(address):
+ ip_param += f' --ip6 {address}'
+ else:
+ ip_param += f' --ip {address}'
return f'{container_base_cmd} --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip()
def generate(container):
# bail out early - looks like removal from running config
if not container:
- if os.path.exists(config_containers_registry):
- os.unlink(config_containers_registry)
- if os.path.exists(config_containers_storage):
- os.unlink(config_containers_storage)
+ for file in [config_containers, config_registry, config_storage]:
+ if os.path.exists(file):
+ os.unlink(file)
return None
if 'network' in container:
for network, network_config in container['network'].items():
tmp = {
- 'cniVersion' : '0.4.0',
- 'name' : network,
- 'plugins' : [{
- 'type': 'bridge',
- 'bridge': f'cni-{network}',
- 'isGateway': True,
- 'ipMasq': False,
- 'hairpinMode': False,
- 'ipam' : {
- 'type': 'host-local',
- 'routes': [],
- 'ranges' : [],
- },
- }]
+ 'name': network,
+ 'id' : sha256(f'{network}'.encode()).hexdigest(),
+ 'driver': 'bridge',
+ 'network_interface': f'podman-{network}',
+ 'subnets': [],
+ 'ipv6_enabled': False,
+ 'internal': False,
+ 'dns_enabled': False,
+ 'ipam_options': {
+ 'driver': 'host-local'
+ }
}
-
for prefix in network_config['prefix']:
- net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}]
- tmp['plugins'][0]['ipam']['ranges'].append(net)
+ net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)}
+ tmp['subnets'].append(net)
- # install per address-family default orutes
- default_route = '0.0.0.0/0'
if is_ipv6(prefix):
- default_route = '::/0'
- tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route})
+ tmp['ipv6_enabled'] = True
- write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2))
+ write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2))
if 'registry' in container:
cmd = f'podman logout --all'
@@ -390,8 +412,9 @@ def generate(container):
if rc != 0:
raise ConfigError(out)
- render(config_containers_registry, 'container/registries.conf.j2', container)
- render(config_containers_storage, 'container/storage.conf.j2', container)
+ render(config_containers, 'container/containers.conf.j2', container)
+ render(config_registry, 'container/registries.conf.j2', container)
+ render(config_storage, 'container/storage.conf.j2', container)
if 'name' in container:
for name, container_config in container['name'].items():
@@ -420,10 +443,7 @@ def apply(container):
# Delete old networks if needed
if 'network_remove' in container:
for network in container['network_remove']:
- call(f'podman network rm {network}')
- tmp = f'/etc/cni/net.d/{network}.conflist'
- if os.path.exists(tmp):
- os.unlink(tmp)
+ call(f'podman network rm {network} >/dev/null 2>&1')
# Add container
disabled_new = False
@@ -447,11 +467,21 @@ def apply(container):
os.unlink(file_path)
continue
- cmd(f'systemctl restart vyos-container-{name}.service')
+ if 'container_restart' in container and name in container['container_restart']:
+ cmd(f'systemctl restart vyos-container-{name}.service')
if disabled_new:
call('systemctl daemon-reload')
+ # Start network and assign it to given VRF if requested. this can only be done
+ # after the containers got started as the podman network interface will
+ # only be enabled by the first container and yet I do not know how to enable
+ # the network interface in advance
+ if 'network' in container:
+ for network, network_config in container['network'].items():
+ tmp = Interface(f'podman-{network}')
+ tmp.set_vrf(network_config.get('vrf', ''))
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 39c87478f..2b2af252d 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -247,7 +247,7 @@ def verify(dhcp):
net2 = ip_network(n)
if (net != net2):
if net.overlaps(net2):
- raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index d0d87d73e..36c1098fe 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.hostsd_client import Client as hostsd_client
from vyos.template import render
-from vyos.template import is_ipv6
+from vyos.template import bracketize_ipv6
from vyos.util import call
from vyos.util import chown
from vyos.util import dict_search
@@ -58,8 +58,26 @@ def get_config(config=None):
default_values = defaults(base)
# T2665 due to how defaults under tag nodes work, we must clear these out before we merge
del default_values['authoritative_domain']
+ del default_values['name_server']
+ del default_values['domain']['name_server']
dns = dict_merge(default_values, dns)
+ # T2665: we cleared default values for tag node 'name_server' above.
+ # We now need to add them back back in a granular way.
+ if 'name_server' in dns:
+ default_values = defaults(base + ['name-server'])
+ for server in dns['name_server']:
+ dns['name_server'][server] = dict_merge(default_values, dns['name_server'][server])
+
+ # T2665: we cleared default values for tag node 'domain' above.
+ # We now need to add them back back in a granular way.
+ if 'domain' in dns:
+ default_values = defaults(base + ['domain', 'name-server'])
+ for domain in dns['domain'].keys():
+ for server in dns['domain'][domain]['name_server']:
+ dns['domain'][domain]['name_server'][server] = dict_merge(
+ default_values, dns['domain'][domain]['name_server'][server])
+
# some additions to the default dictionary
if 'system' in dns:
base_nameservers = ['system', 'name-server']
@@ -263,7 +281,7 @@ def verify(dns):
# as a domain will contains dot's which is out dictionary delimiter.
if 'domain' in dns:
for domain in dns['domain']:
- if 'server' not in dns['domain'][domain]:
+ if 'name_server' not in dns['domain'][domain]:
raise ConfigError(f'No server configured for domain {domain}!')
if 'dns64_prefix' in dns:
@@ -329,7 +347,12 @@ def apply(dns):
# sources
hc.delete_name_servers([hostsd_tag])
if 'name_server' in dns:
- hc.add_name_servers({hostsd_tag: dns['name_server']})
+ # 'name_server' is of the form
+ # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+ # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+ nslist = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+ for (h, p) in dns['name_server'].items()]
+ hc.add_name_servers({hostsd_tag: nslist})
# delete all nameserver tags
hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor())
@@ -358,7 +381,14 @@ def apply(dns):
# the list and keys() are required as get returns a dict, not list
hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
if 'domain' in dns:
- hc.add_forward_zones(dns['domain'])
+ zones = dns['domain']
+ for domain in zones.keys():
+ # 'name_server' is of the form
+ # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+ # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+ zones[domain]['name_server'] = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+ for (h, p) in zones[domain]['name_server'].items()]
+ hc.add_forward_zones(zones)
# hostsd generates NTAs for the authoritative zones
# the list and keys() are required as get returns a dict, not list
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index b63ed4eb9..c41a442df 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -282,6 +282,16 @@ def verify_rule(firewall, rule_conf, ipv6):
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
+ if 'log_options' in rule_conf:
+ if 'log' not in rule_conf or 'enable' not in rule_conf['log']:
+ raise ConfigError('log-options defined, but log is not enable')
+
+ if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options snapshot-length defined, but log group is not define')
+
+ if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options queue-threshold defined, but log group is not define')
+
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
return
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index ce5e63928..b0c38e8d3 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -159,6 +159,8 @@ def generate(https):
server_block['port'] = data.get('listen-port', '443')
name = data.get('server-name', ['_'])
server_block['name'] = name
+ allow_client = data.get('allow-client', {})
+ server_block['allow_client'] = allow_client.get('address', [])
server_block_list.append(server_block)
# get certificate data
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 4f05957fa..cf553f0e8 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -412,6 +412,11 @@ def verify(bgp):
raise ConfigError('Missing mandatory configuration option for '\
f'global administrative distance {key}!')
+ # TCP keepalive requires all three parameters to be set
+ if dict_search('parameters.tcp_keepalive', bgp) != None:
+ if not {'idle', 'interval', 'probes'} <= set(bgp['parameters']['tcp_keepalive']):
+ raise ConfigError('TCP keepalive incomplete - idle, keepalive and probes must be set')
+
# Address Family specific validation
if 'address_family' in bgp:
for afi, afi_config in bgp['address_family'].items():
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 0582d32be..eb64afa0c 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -89,7 +89,7 @@ def get_config(config=None):
if 'mpls_te' not in ospf:
del default_values['mpls_te']
- for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:
# table is a tagNode thus we need to clean out all occurances for the
# default values and load them in later individually
if protocol == 'table':
@@ -234,7 +234,7 @@ def verify(ospf):
if list(set(global_range) & set(local_range)):
raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\
f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')
-
+
# Check for a blank or invalid value per prefix
if dict_search('segment_routing.prefix', ospf):
for prefix, prefix_config in ospf['segment_routing']['prefix'].items():
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 4fabe170f..95c72df47 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import jmespath
from sys import exit
@@ -29,9 +30,92 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
+
ipoe_conf = '/run/accel-pppd/ipoe.conf'
ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets'
+
+def get_pools_in_order(data: dict) -> list:
+ """Return a list of dictionaries representing pool data in the order
+ in which they should be allocated. Pool must be defined before we can
+ use it with 'next-pool' option.
+
+ Args:
+ data: A dictionary of pool data, where the keys are pool names and the
+ values are dictionaries containing the 'subnet' key and the optional
+ 'next_pool' key.
+
+ Returns:
+ list: A list of dictionaries
+
+ Raises:
+ ValueError: If a 'next_pool' key references a pool name that
+ has not been defined.
+ ValueError: If a circular reference is found in the 'next_pool' keys.
+
+ Example:
+ config_data = {
+ ... 'first-pool': {
+ ... 'next_pool': 'second-pool',
+ ... 'subnet': '192.0.2.0/25'
+ ... },
+ ... 'second-pool': {
+ ... 'next_pool': 'third-pool',
+ ... 'subnet': '203.0.113.0/25'
+ ... },
+ ... 'third-pool': {
+ ... 'subnet': '198.51.100.0/24'
+ ... },
+ ... 'foo': {
+ ... 'subnet': '100.64.0.0/24',
+ ... 'next_pool': 'second-pool'
+ ... }
+ ... }
+
+ % get_pools_in_order(config_data)
+ [{'third-pool': {'subnet': '198.51.100.0/24'}},
+ {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}},
+ {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}},
+ {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}]
+ """
+ pools = []
+ unresolved_pools = {}
+
+ for pool, pool_config in data.items():
+ if 'next_pool' not in pool_config:
+ pools.insert(0, {pool: pool_config})
+ else:
+ unresolved_pools[pool] = pool_config
+
+ while unresolved_pools:
+ resolved_pools = []
+
+ for pool, pool_config in unresolved_pools.items():
+ next_pool_name = pool_config['next_pool']
+
+ if any(p for p in pools if next_pool_name in p):
+ index = next(
+ (i for i, p in enumerate(pools) if next_pool_name in p),
+ None)
+ pools.insert(index + 1, {pool: pool_config})
+ resolved_pools.append(pool)
+ elif next_pool_name in unresolved_pools:
+ # next pool not yet resolved
+ pass
+ else:
+ raise ValueError(
+ f"Pool '{next_pool_name}' not defined in configuration data"
+ )
+
+ if not resolved_pools:
+ raise ValueError("Circular reference in configuration data")
+
+ for pool in resolved_pools:
+ unresolved_pools.pop(pool)
+
+ return pools
+
+
def get_config(config=None):
if config:
conf = config
@@ -43,6 +127,19 @@ def get_config(config=None):
# retrieve common dictionary keys
ipoe = get_accel_dict(conf, base, ipoe_chap_secrets)
+
+ if jmespath.search('client_ip_pool.name', ipoe):
+ dict_named_pools = jmespath.search('client_ip_pool.name', ipoe)
+ # Multiple named pools require ordered values T5099
+ ipoe['ordered_named_pools'] = get_pools_in_order(dict_named_pools)
+ # T5099 'next-pool' option
+ if jmespath.search('client_ip_pool.name.*.next_pool', ipoe):
+ for pool, pool_config in ipoe['client_ip_pool']['name'].items():
+ if 'next_pool' in pool_config:
+ ipoe['first_named_pool'] = pool
+ ipoe['first_named_pool_subnet'] = pool_config
+ break
+
return ipoe
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index d207c63df..63887b278 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -549,6 +549,8 @@ def generate(ipsec):
if ipsec['dhcp_no_address']:
with open(DHCP_HOOK_IFLIST, 'w') as f:
f.write(" ".join(ipsec['dhcp_no_address'].values()))
+ elif os.path.exists(DHCP_HOOK_IFLIST):
+ os.unlink(DHCP_HOOK_IFLIST)
for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_PATH]:
if not os.path.exists(path):
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook
index 49bb18372..49bb18372 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks
index 442419d79..442419d79 100755
--- a/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
index 1f1926e17..1f1926e17 100755
--- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf
index 2e2f67f70..2e4b6e295 100644
--- a/src/etc/systemd/system/frr.service.d/override.conf
+++ b/src/etc/systemd/system/frr.service.d/override.conf
@@ -4,7 +4,6 @@ Before=vyos-router.service
[Service]
LimitNOFILE=4096
-LimitNOFILESoft=4096
ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \
echo "log syslog" > /run/frr/config/frr.conf; \
echo "log facility local7" >> /run/frr/config/frr.conf; \
diff --git a/src/migration-scripts/dns-forwarding/3-to-4 b/src/migration-scripts/dns-forwarding/3-to-4
new file mode 100755
index 000000000..55165c2c5
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/3-to-4
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 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/>.
+
+# T5115: migrate "service dns forwarding domain example.com server" to
+# "service dns forwarding domain example.com name-server"
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'dns', 'forwarding', 'domain']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+for domain in config.list_nodes(base):
+ if config.exists(base + [domain, 'server']):
+ config.copy(base + [domain, 'server'], base + [domain, 'name-server'])
+ config.delete(base + [domain, 'server'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/firewall/9-to-10 b/src/migration-scripts/firewall/9-to-10
new file mode 100755
index 000000000..6f67cc512
--- /dev/null
+++ b/src/migration-scripts/firewall/9-to-10
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# T5050: Log options
+# cli changes from:
+# set firewall [name | ipv6-name] <name> rule <number> log-level <log_level>
+# To
+# set firewall [name | ipv6-name] <name> rule <number> log-options level <log_level>
+
+import re
+
+from sys import argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+from vyos.ifconfig import Section
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['firewall']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+if config.exists(base + ['name']):
+ for name in config.list_nodes(base + ['name']):
+ if not config.exists(base + ['name', name, 'rule']):
+ continue
+
+ for rule in config.list_nodes(base + ['name', name, 'rule']):
+ log_options_base = base + ['name', name, 'rule', rule, 'log-options']
+ rule_log_level = base + ['name', name, 'rule', rule, 'log-level']
+
+ if config.exists(rule_log_level):
+ tmp = config.return_value(rule_log_level)
+ config.delete(rule_log_level)
+ config.set(log_options_base + ['level'], value=tmp)
+
+if config.exists(base + ['ipv6-name']):
+ for name in config.list_nodes(base + ['ipv6-name']):
+ if not config.exists(base + ['ipv6-name', name, 'rule']):
+ continue
+
+ for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']):
+ log_options_base = base + ['ipv6-name', name, 'rule', rule, 'log-options']
+ rule_log_level = base + ['ipv6-name', name, 'rule', rule, 'log-level']
+
+ if config.exists(rule_log_level):
+ tmp = config.return_value(rule_log_level)
+ config.delete(rule_log_level)
+ config.set(log_options_base + ['level'], value=tmp)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1) \ No newline at end of file
diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py
index 87a25bb96..00de45fc8 100755
--- a/src/op_mode/accelppp.py
+++ b/src/op_mode/accelppp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -75,8 +75,8 @@ def _get_raw_statistics(accel_output, pattern, protocol):
def _get_raw_sessions(port):
- cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \
- 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \
+ cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,rate-limit,' \
+ 'state,uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \
'tx-bytes-raw,rx-pkts,tx-pkts'
output = vyos.accel_ppp.accel_cmd(port, cmd_options)
parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse(
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index a0e47d7ad..f8863c530 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -17,7 +17,6 @@
import sys
-from sys import exit
from tabulate import tabulate
from vyos.configquery import ConfigTreeQuery
@@ -75,8 +74,7 @@ def show_forwarding_statistics(raw: bool):
config = ConfigTreeQuery()
if not config.exists('service dns forwarding'):
- print("DNS forwarding is not configured")
- exit(0)
+ raise vyos.opmode.UnconfiguredSubsystem('DNS forwarding is not configured')
dns_data = _get_raw_forwarding_statistics()
if raw:
diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py
index 678c74980..dd87b5901 100755
--- a/src/op_mode/interfaces.py
+++ b/src/op_mode/interfaces.py
@@ -207,7 +207,11 @@ def _get_raw_data(ifname: typing.Optional[str],
res_intf['description'] = interface.get_alias()
- res_intf['stats'] = interface.operational.get_stats()
+ stats = interface.operational.get_stats()
+ for k in list(stats):
+ stats[k] = _get_counter_val(cache[k], stats[k])
+
+ res_intf['stats'] = stats
ret.append(res_intf)
@@ -402,6 +406,18 @@ def show_counters(raw: bool, intf_name: typing.Optional[str],
return data
return _format_show_counters(data)
+def clear_counters(intf_name: typing.Optional[str],
+ intf_type: typing.Optional[str],
+ vif: bool, vrrp: bool):
+ for interface in filtered_interfaces(intf_name, intf_type, vif, vrrp):
+ interface.operational.clear_counters()
+
+def reset_counters(intf_name: typing.Optional[str],
+ intf_type: typing.Optional[str],
+ vif: bool, vrrp: bool):
+ for interface in filtered_interfaces(intf_name, intf_type, vif, vrrp):
+ interface.operational.reset_counters()
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 8e76f4cc0..7f4fb72e5 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -24,6 +24,7 @@ from tabulate import tabulate
from vyos.util import convert_data
from vyos.util import seconds_to_human
+from vyos.configquery import ConfigTreeQuery
import vyos.opmode
import vyos.ipsec
@@ -401,30 +402,152 @@ def _get_childsa_id_list(ike_sas: list) -> list:
return list_childsa_id
+def _get_all_sitetosite_peers_name_list() -> list:
+ """
+ Return site-to-site peers configuration
+ :return: site-to-site peers configuration
+ :rtype: list
+ """
+ conf: ConfigTreeQuery = ConfigTreeQuery()
+ config_path = ['vpn', 'ipsec', 'site-to-site', 'peer']
+ peers_config = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ peers_list: list = []
+ for name in peers_config:
+ peers_list.append(name)
+ return peers_list
+
+
def reset_peer(peer: str, tunnel: typing.Optional[str] = None):
# Convert tunnel to Strongwan format of CHILD_SA
+ tunnel_sw = None
if tunnel:
if tunnel.isnumeric():
- tunnel = f'{peer}-tunnel-{tunnel}'
+ tunnel_sw = f'{peer}-tunnel-{tunnel}'
elif tunnel == 'vti':
- tunnel = f'{peer}-vti'
+ tunnel_sw = f'{peer}-vti'
try:
- sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel)
-
+ sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw)
if not sa_list:
- raise vyos.opmode.IncorrectValue('Peer not found, aborting')
+ raise vyos.opmode.IncorrectValue(
+ f'Peer\'s {peer} SA(s) not found, aborting')
if tunnel and sa_list:
childsa_id_list: list = _get_childsa_id_list(sa_list)
if not childsa_id_list:
raise vyos.opmode.IncorrectValue(
- 'Peer or tunnel(s) not found, aborting')
- vyos.ipsec.terminate_vici_by_name(peer, tunnel)
- print('Peer reset result: success')
+ f'Peer {peer} tunnel {tunnel} SA(s) not found, aborting')
+ vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
+ print(f'Peer {peer} reset result: success')
except (vyos.ipsec.ViciInitiateError) as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciInitiateError) as err:
+ except (vyos.ipsec.ViciCommandError) as err:
raise vyos.opmode.IncorrectValue(err)
+def reset_all_peers():
+ sitetosite_list = _get_all_sitetosite_peers_name_list()
+ if sitetosite_list:
+ for peer_name in sitetosite_list:
+ try:
+ reset_peer(peer_name)
+ except (vyos.opmode.IncorrectValue) as err:
+ print(err)
+ print('Peers reset result: success')
+ else:
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'VPN IPSec site-to-site is not configured, aborting')
+
+def _get_ra_session_list_by_username(username: typing.Optional[str] = None):
+ """
+ Return list of remote-access IKE_SAs uniqueids
+ :param username:
+ :type username:
+ :return:
+ :rtype:
+ """
+ list_sa_id = []
+ sa_list = vyos.ipsec.get_vici_sas()
+ for sa_val in sa_list:
+ for sa in sa_val.values():
+ if 'remote-eap-id' in sa:
+ if username:
+ if username == sa['remote-eap-id'].decode():
+ list_sa_id.append(sa['uniqueid'].decode())
+ else:
+ list_sa_id.append(sa['uniqueid'].decode())
+ return list_sa_id
+
+
+def reset_ra(username: typing.Optional[str] = None):
+ #Reset remote-access ipsec sessions
+ if username:
+ list_sa_id = _get_ra_session_list_by_username(username)
+ else:
+ list_sa_id = _get_ra_session_list_by_username()
+ if list_sa_id:
+ vyos.ipsec.terminate_vici_ikeid_list(list_sa_id)
+
+
+def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str):
+ if profile and tunnel and nbma_dst:
+ ike_sa_name = f'dmvpn-{profile}-{tunnel}'
+ try:
+ # Get IKE SAs
+ sa_list = convert_data(
+ vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ if not sa_list:
+ raise vyos.opmode.IncorrectValue(
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
+ sa_nbma_list = list([x for x in sa_list if
+ ike_sa_name in x and x[ike_sa_name][
+ 'remote-host'] == nbma_dst])
+ if not sa_nbma_list:
+ raise vyos.opmode.IncorrectValue(
+ f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting')
+ # terminate IKE SAs
+ vyos.ipsec.terminate_vici_ikeid_list(list(
+ [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if
+ ike_sa_name in x]))
+ # initiate IKE SAs
+ for ike in sa_nbma_list:
+ if ike_sa_name in ike:
+ vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'])
+ print(
+ f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success')
+ except (vyos.ipsec.ViciInitiateError) as err:
+ raise vyos.opmode.UnconfiguredSubsystem(err)
+ except (vyos.ipsec.ViciCommandError) as err:
+ raise vyos.opmode.IncorrectValue(err)
+
+
+def reset_profile_all(profile: str, tunnel: str):
+ if profile and tunnel:
+ ike_sa_name = f'dmvpn-{profile}-{tunnel}'
+ try:
+ # Get IKE SAs
+ sa_list: list = convert_data(
+ vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ if not sa_list:
+ raise vyos.opmode.IncorrectValue(
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
+ # terminate IKE SAs
+ vyos.ipsec.terminate_vici_by_name(ike_sa_name, None)
+ # initiate IKE SAs
+ for ike in sa_list:
+ if ike_sa_name in ike:
+ vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'])
+ print(
+ f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success')
+ print(f'Profile {profile} tunnel {tunnel} reset result: success')
+ except (vyos.ipsec.ViciInitiateError) as err:
+ raise vyos.opmode.UnconfiguredSubsystem(err)
+ except (vyos.ipsec.ViciCommandError) as err:
+ raise vyos.opmode.IncorrectValue(err)
+
def show_sa(raw: bool):
sa_data = _get_raw_data_sas()
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
index 8f88ab422..37fdbcbeb 100755
--- a/src/op_mode/openvpn.py
+++ b/src/op_mode/openvpn.py
@@ -53,7 +53,7 @@ def _get_tunnel_address(peer_host, peer_port, status_file):
def _get_interface_status(mode: str, interface: str) -> dict:
status_file = f'/run/openvpn/{interface}.status'
- data = {
+ data: dict = {
'mode': mode,
'intf': interface,
'local_host': '',
@@ -142,18 +142,18 @@ def _get_interface_status(mode: str, interface: str) -> dict:
return data
-def _get_raw_data(mode: str) -> dict:
- data = {}
+def _get_raw_data(mode: str) -> list:
+ data: list = []
conf = Config()
conf_dict = conf.get_config_dict(['interfaces', 'openvpn'],
get_first_key=True)
if not conf_dict:
return data
- interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode]
+ interfaces = [x for x in list(conf_dict) if
+ conf_dict[x]['mode'].replace('-', '_') == mode]
for intf in interfaces:
- data[intf] = _get_interface_status(mode, intf)
- d = data[intf]
+ d = _get_interface_status(mode, intf)
d['local_host'] = conf_dict[intf].get('local-host', '')
d['local_port'] = conf_dict[intf].get('local-port', '')
if conf.exists(f'interfaces openvpn {intf} server client'):
@@ -164,10 +164,11 @@ def _get_raw_data(mode: str) -> dict:
client['name'] = 'None (PSK)'
client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0]
client['remote_port'] = conf_dict[intf].get('remote-port', '1194')
+ data.append(d)
return data
-def _format_openvpn(data: dict) -> str:
+def _format_openvpn(data: list) -> str:
if not data:
out = 'No OpenVPN interfaces configured'
return out
@@ -176,11 +177,12 @@ def _format_openvpn(data: dict) -> str:
'TX bytes', 'RX bytes', 'Connected Since']
out = ''
- for intf in list(data):
+ for d in data:
data_out = []
- l_host = data[intf]['local_host']
- l_port = data[intf]['local_port']
- for client in list(data[intf]['clients']):
+ intf = d['intf']
+ l_host = d['local_host']
+ l_port = d['local_port']
+ for client in d['clients']:
r_host = client['remote_host']
r_port = client['remote_port']
@@ -201,7 +203,7 @@ def _format_openvpn(data: dict) -> str:
return out
-def show(raw: bool, mode: ArgMode) -> str:
+def show(raw: bool, mode: ArgMode) -> typing.Union[list,str]:
openvpn_data = _get_raw_data(mode)
if raw:
diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py
index 3a0ad941c..46195d6cd 100755
--- a/src/op_mode/reset_vpn.py
+++ b/src/op_mode/reset_vpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -13,60 +13,49 @@
#
# 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 sys
-import argparse
+import typing
from vyos.util import run
+import vyos.opmode
+
cmd_dict = {
- 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}',
- 'vpn_types' : {
- 'pptp' : 2003,
- 'l2tp' : 2004,
- 'sstp' : 2005
+ 'cmd_base': '/usr/bin/accel-cmd -p {} terminate {} {}',
+ 'vpn_types': {
+ 'pptp': 2003,
+ 'l2tp': 2004,
+ 'sstp': 2005
}
}
-def terminate_sessions(username='', interface='', protocol=''):
- # Reset vpn connections by username
+def reset_conn(protocol: str, username: typing.Optional[str] = None,
+ interface: typing.Optional[str] = None):
if protocol in cmd_dict['vpn_types']:
- if username == "all_users":
- run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', ''))
- else:
- run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username))
-
- # Reset vpn connections by ifname
- elif interface:
- for proto in cmd_dict['vpn_types']:
- run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface))
-
- elif username:
- # Reset all vpn connections
- if username == "all_users":
- for proto in cmd_dict['vpn_types']:
- run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', ''))
+ # Reset by Interface
+ if interface:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol],
+ 'if', interface))
+ return
+ # Reset by username
+ if username:
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol],
+ 'username', username))
+ # Reset all
else:
- for proto in cmd_dict['vpn_types']:
- run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username))
-
-def main():
- #parese args
- parser = argparse.ArgumentParser()
- parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False)
- parser.add_argument('--interface', help='Terminate by interface', required=False)
- parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False)
- args = parser.parse_args()
-
- if args.username or args.interface:
- terminate_sessions(username=args.username, interface=args.interface, protocol=args.protocol)
+ run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol],
+ 'all',
+ ''))
else:
- print("Param --username or --interface required")
- sys.exit(1)
-
- terminate_sessions()
+ vyos.opmode.IncorrectValue('Unknown VPN Protocol, aborting')
if __name__ == '__main__':
- 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/sflow.py b/src/op_mode/sflow.py
new file mode 100755
index 000000000..88f70d6bd
--- /dev/null
+++ b/src/op_mode/sflow.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 dbus
+import sys
+
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import cmd
+
+import vyos.opmode
+
+
+def _get_raw_sflow():
+ bus = dbus.SystemBus()
+ config = ConfigTreeQuery()
+
+ interfaces = config.values('system sflow interface')
+ servers = config.list_nodes('system sflow server')
+
+ sflow = bus.get_object('net.sflow.hsflowd', '/net/sflow/hsflowd')
+ sflow_telemetry = dbus.Interface(
+ sflow, dbus_interface='net.sflow.hsflowd.telemetry')
+ agent_address = sflow_telemetry.GetAgent()
+ samples_dropped = int(sflow_telemetry.Get('dropped_samples'))
+ packet_drop_sent = int(sflow_telemetry.Get('event_samples'))
+ samples_packet_sent = int(sflow_telemetry.Get('flow_samples'))
+ samples_counter_sent = int(sflow_telemetry.Get('counter_samples'))
+ datagrams_sent = int(sflow_telemetry.Get('datagrams'))
+ rtmetric_samples = int(sflow_telemetry.Get('rtmetric_samples'))
+ event_samples_suppressed = int(sflow_telemetry.Get('event_samples_suppressed'))
+ samples_suppressed = int(sflow_telemetry.Get('flow_samples_suppressed'))
+ counter_samples_suppressed = int(
+ sflow_telemetry.Get("counter_samples_suppressed"))
+ version = sflow_telemetry.GetVersion()
+
+ sflow_dict = {
+ 'agent_address': agent_address,
+ 'sflow_interfaces': interfaces,
+ 'sflow_servers': servers,
+ 'counter_samples_sent': samples_counter_sent,
+ 'datagrams_sent': datagrams_sent,
+ 'packet_drop_sent': packet_drop_sent,
+ 'packet_samples_dropped': samples_dropped,
+ 'packet_samples_sent': samples_packet_sent,
+ 'rtmetric_samples': rtmetric_samples,
+ 'event_samples_suppressed': event_samples_suppressed,
+ 'flow_samples_suppressed': samples_suppressed,
+ 'counter_samples_suppressed': counter_samples_suppressed,
+ 'hsflowd_version': version
+ }
+ return sflow_dict
+
+
+def _get_formatted_sflow(data):
+ table = [
+ ['Agent address', f'{data.get("agent_address")}'],
+ ['sFlow interfaces', f'{data.get("sflow_interfaces", "n/a")}'],
+ ['sFlow servers', f'{data.get("sflow_servers", "n/a")}'],
+ ['Counter samples sent', f'{data.get("counter_samples_sent")}'],
+ ['Datagrams sent', f'{data.get("datagrams_sent")}'],
+ ['Packet samples sent', f'{data.get("packet_samples_sent")}'],
+ ['Packet samples dropped', f'{data.get("packet_samples_dropped")}'],
+ ['Packet drops sent', f'{data.get("packet_drop_sent")}'],
+ ['Packet drops suppressed', f'{data.get("event_samples_suppressed")}'],
+ ['Flow samples suppressed', f'{data.get("flow_samples_suppressed")}'],
+ ['Counter samples suppressed', f'{data.get("counter_samples_suppressed")}']
+ ]
+
+ return tabulate(table)
+
+
+def show(raw: bool):
+
+ config = ConfigTreeQuery()
+ if not config.exists('system sflow'):
+ raise vyos.opmode.UnconfiguredSubsystem(
+ '"system sflow" is not configured!')
+
+ sflow_data = _get_raw_sflow()
+ if raw:
+ return sflow_data
+ else:
+ return _get_formatted_sflow(sflow_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_interfaces.py b/src/op_mode/show_interfaces.py
deleted file mode 100755
index eac068274..000000000
--- a/src/op_mode/show_interfaces.py
+++ /dev/null
@@ -1,310 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright 2017-2021 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library 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
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-import sys
-import glob
-import argparse
-
-from vyos.ifconfig import Section
-from vyos.ifconfig import Interface
-from vyos.ifconfig import VRRP
-from vyos.util import cmd, call
-
-
-# interfaces = Sections.reserved()
-interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe']
-glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces))
-
-
-actions = {}
-def register(name):
- """
- Decorator to register a function into actions with a name.
- `actions[name]' can be used to call the registered functions.
- We wrap each function in a SIGPIPE handler as all registered functions
- can be subject to a broken pipe if there are a lot of interfaces.
- """
- def _register(function):
- def handled_function(*args, **kwargs):
- try:
- function(*args, **kwargs)
- except BrokenPipeError:
- # Flush output to /dev/null and bail out.
- os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
- sys.exit(1)
- actions[name] = handled_function
- return handled_function
- return _register
-
-
-def filtered_interfaces(ifnames, iftypes, vif, vrrp):
- """
- get all the interfaces from the OS and returns them
- ifnames can be used to filter which interfaces should be considered
-
- ifnames: a list of interfaces names to consider, empty do not filter
- return an instance of the interface class
- """
- if isinstance(iftypes, list):
- for iftype in iftypes:
- yield from filtered_interfaces(ifnames, iftype, vif, vrrp)
-
- for ifname in Section.interfaces(iftypes):
- # Bail out early if interface name not part of our search list
- if ifnames and ifname not in ifnames:
- continue
-
- # As we are only "reading" from the interface - we must use the
- # generic base class which exposes all the data via a common API
- interface = Interface(ifname, create=False, debug=False)
-
- # VLAN interfaces have a '.' in their name by convention
- if vif and not '.' in ifname:
- continue
-
- if vrrp:
- vrrp_interfaces = VRRP.active_interfaces()
- if ifname not in vrrp_interfaces:
- continue
-
- yield interface
-
-
-def split_text(text, used=0):
- """
- take a string and attempt to split it to fit with the width of the screen
-
- text: the string to split
- used: number of characted already used in the screen
- """
- no_tty = call('tty -s')
-
- returned = cmd('stty size') if not no_tty else ''
- if len(returned) == 2:
- rows, columns = [int(_) for _ in returned]
- else:
- rows, columns = (40, 80)
-
- desc_len = columns - used
-
- line = ''
- for word in text.split():
- if len(line) + len(word) < desc_len:
- line = f'{line} {word}'
- continue
- if line:
- yield line[1:]
- else:
- line = f'{line} {word}'
-
- yield line[1:]
-
-
-def get_counter_val(clear, now):
- """
- attempt to correct a counter if it wrapped, copied from perl
-
- clear: previous counter
- now: the current counter
- """
- # This function has to deal with both 32 and 64 bit counters
- if clear == 0:
- return now
-
- # device is using 64 bit values assume they never wrap
- value = now - clear
- if (now >> 32) != 0:
- return value
-
- # The counter has rolled. If the counter has rolled
- # multiple times since the clear value, then this math
- # is meaningless.
- if (value < 0):
- value = (4294967296 - clear) + now
-
- return value
-
-
-@register('help')
-def usage(*args):
- print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION")
- print(f" NAME = " + ' | '.join(Section.interfaces()))
- print(f" TYPE = " + ' | '.join(Section.sections()))
- print(f" ACTION = " + ' | '.join(actions))
- sys.exit(1)
-
-
-@register('allowed')
-def run_allowed(**kwarg):
- sys.stdout.write(' '.join(Section.interfaces()))
-
-
-def pppoe(ifname):
- out = cmd(f'ps -C pppd -f')
- if ifname in out:
- return 'C'
- elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]:
- return 'D'
- return ''
-
-
-@register('show')
-def run_show_intf(ifnames, iftypes, vif, vrrp):
- handled = []
- for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- handled.append(interface.ifname)
- cache = interface.operational.load_counters()
-
- out = cmd(f'ip addr show {interface.ifname}')
- out = re.sub(f'^\d+:\s+','',out)
- if re.search('link/tunnel6', out):
- tunnel = cmd(f'ip -6 tun show {interface.ifname}')
- # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000)
- tunnel = re.sub('.*encap', 'encap', tunnel)
- out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out)
-
- print(out)
-
- timestamp = int(cache.get('timestamp', 0))
- if timestamp:
- when = interface.operational.strtime(timestamp)
- print(f' Last clear: {when}')
-
- description = interface.get_alias()
- if description:
- print(f' Description: {description}')
-
- print()
- print(interface.operational.formated_stats())
-
- for ifname in ifnames:
- if ifname not in handled and ifname.startswith('pppoe'):
- state = pppoe(ifname)
- if not state:
- continue
- string = {
- 'C': 'Coming up',
- 'D': 'Link down',
- }[state]
- print('{}: {}'.format(ifname, string))
-
-
-@register('show-brief')
-def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
- format1 = '%-16s %-33s %-4s %s'
- format2 = '%-16s %s'
-
- print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down')
- print(format1 % ("Interface", "IP Address", "S/L", "Description"))
- print(format1 % ("---------", "----------", "---", "-----------"))
-
- handled = []
- for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- handled.append(interface.ifname)
-
- oper_state = interface.operational.get_state()
- admin_state = interface.get_admin_state()
-
- intf = [interface.ifname,]
-
- oper = ['u', ] if oper_state in ('up', 'unknown') else ['D', ]
- admin = ['u', ] if admin_state in ('up', 'unknown') else ['A', ]
- addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ]
- descs = list(split_text(interface.get_alias(),0))
-
- while intf or oper or admin or addrs or descs:
- i = intf.pop(0) if intf else ''
- a = addrs.pop(0) if addrs else ''
- d = descs.pop(0) if descs else ''
- s = [admin.pop(0)] if admin else []
- l = [oper.pop(0)] if oper else []
- if len(a) < 33:
- print(format1 % (i, a, '/'.join(s+l), d))
- else:
- print(format2 % (i, a))
- print(format1 % ('', '', '/'.join(s+l), d))
-
- for ifname in ifnames:
- if ifname not in handled and ifname.startswith('pppoe'):
- state = pppoe(ifname)
- if not state:
- continue
- string = {
- 'C': 'u/D',
- 'D': 'A/D',
- }[state]
- print(format1 % (ifname, '', string, ''))
-
-
-@register('show-count')
-def run_show_counters(ifnames, iftypes, vif, vrrp):
- formating = '%-12s %10s %10s %10s %10s'
- print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes'))
-
- for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- oper = interface.operational.get_state()
-
- if oper not in ('up','unknown'):
- continue
-
- stats = interface.operational.get_stats()
- cache = interface.operational.load_counters()
- print(formating % (
- interface.ifname,
- get_counter_val(cache['rx_packets'], stats['rx_packets']),
- get_counter_val(cache['rx_bytes'], stats['rx_bytes']),
- get_counter_val(cache['tx_packets'], stats['tx_packets']),
- get_counter_val(cache['tx_bytes'], stats['tx_bytes']),
- ))
-
-
-@register('clear')
-def run_clear_intf(ifnames, iftypes, vif, vrrp):
- for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- print(f'Clearing {interface.ifname}')
- interface.operational.clear_counters()
-
-
-@register('reset')
-def run_reset_intf(ifnames, iftypes, vif, vrrp):
- for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- interface.operational.reset_counters()
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(add_help=False, description='Show interface information')
- parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)')
- parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type')
- parser.add_argument('--action', action="store", type=str, default='show', help='action to perform')
- parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces")
- parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces")
- parser.add_argument('--help', action='store_true', default=False, help="show help")
-
- args = parser.parse_args()
-
- def missing(*args):
- print('Invalid action [{args.action}]')
- usage()
-
- actions.get(args.action, missing)(
- [_ for _ in args.intf.split(' ') if _],
- [_ for _ in args.intf_type.split(' ') if _],
- args.vif,
- args.vrrp
- )
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
index 2392cfe92..b81d1693e 100755
--- a/src/op_mode/vpn_ipsec.py
+++ b/src/op_mode/vpn_ipsec.py
@@ -16,12 +16,12 @@
import re
import argparse
-from subprocess import TimeoutExpired
from vyos.util import call
SWANCTL_CONF = '/etc/swanctl/swanctl.conf'
+
def get_peer_connections(peer, tunnel, return_all = False):
search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*'
matches = []
@@ -34,57 +34,6 @@ def get_peer_connections(peer, tunnel, return_all = False):
matches.append(result[1])
return matches
-def reset_peer(peer, tunnel):
- if not peer:
- print('Invalid peer, aborting')
- return
-
- conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all'))
-
- if not conns:
- print('Tunnel(s) not found, aborting')
- return
-
- result = True
- for conn in conns:
- try:
- call(f'/usr/sbin/ipsec down {conn}{{*}}', timeout = 10)
- call(f'/usr/sbin/ipsec up {conn}', timeout = 10)
- except TimeoutExpired as e:
- print(f'Timed out while resetting {conn}')
- result = False
-
-
- print('Peer reset result: ' + ('success' if result else 'failed'))
-
-def get_profile_connection(profile, tunnel = None):
- search = rf'(dmvpn-{profile}-[\w]+)' if tunnel == 'all' else rf'(dmvpn-{profile}-{tunnel})'
- with open(SWANCTL_CONF, 'r') as f:
- for line in f.readlines():
- result = re.search(search, line)
- if result:
- return result[1]
- return None
-
-def reset_profile(profile, tunnel):
- if not profile:
- print('Invalid profile, aborting')
- return
-
- if not tunnel:
- print('Invalid tunnel, aborting')
- return
-
- conn = get_profile_connection(profile)
-
- if not conn:
- print('Profile not found, aborting')
- return
-
- call(f'/usr/sbin/ipsec down {conn}')
- result = call(f'/usr/sbin/ipsec up {conn}')
-
- print('Profile reset result: ' + ('success' if result == 0 else 'failed'))
def debug_peer(peer, tunnel):
peer = peer.replace(':', '-')
@@ -119,6 +68,7 @@ def debug_peer(peer, tunnel):
for conn in conns:
call(f'/usr/sbin/ipsec statusall | grep {conn}')
+
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--action', help='Control action', required=True)
@@ -127,9 +77,6 @@ if __name__ == '__main__':
args = parser.parse_args()
- if args.action == 'reset-peer':
- reset_peer(args.name, args.tunnel)
- elif args.action == "reset-profile":
- reset_profile(args.name, args.tunnel)
- elif args.action == "vpn-debug":
+
+ if args.action == "vpn-debug":
debug_peer(args.name, args.tunnel)
diff --git a/src/services/api/graphql/generate/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py
index 20fc7cc1d..4ebb47a7e 100644
--- a/src/services/api/graphql/generate/config_session_function.py
+++ b/src/services/api/graphql/generate/config_session_function.py
@@ -28,5 +28,3 @@ mutations = {'save_config_file': save_config_file,
'load_config_file': load_config_file,
'add_system_image': add_system_image,
'delete_system_image': delete_system_image}
-
-
diff --git a/src/services/api/graphql/generate/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py
index 50c5d24f8..06e74032d 100755
--- a/src/services/api/graphql/generate/schema_from_composite.py
+++ b/src/services/api/graphql/generate/schema_from_composite.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -20,8 +20,7 @@
import os
import sys
-import json
-from inspect import signature, getmembers, isfunction, isclass, getmro
+from inspect import signature
from jinja2 import Template
from vyos.defaults import directories
@@ -32,9 +31,9 @@ if __package__ is None or __package__ == '':
else:
from .. libs.op_mode import snake_to_pascal_case, map_type_name
from . composite_function import queries, mutations
- from .. import state
SCHEMA_PATH = directories['api_schema']
+CLIENT_OP_PATH = directories['api_client_op']
schema_data: dict = {'schema_name': '',
'schema_fields': []}
@@ -85,6 +84,30 @@ extend type Mutation {
}
"""
+op_query_template = """
+query {{ op_name }} ({{ op_sig }}) {
+ {{ op_name }} (data: { {{ op_arg }} }) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+"""
+
+op_mutation_template = """
+mutation {{ op_name }} ({{ op_sig }}) {
+ {{ op_name }} (data: { {{ op_arg }} }) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+"""
+
def create_schema(func_name: str, func: callable, template: str) -> str:
sig = signature(func)
@@ -104,19 +127,52 @@ def create_schema(func_name: str, func: callable, template: str) -> str:
return res
+def create_client_op(func_name: str, func: callable, template: str) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ op_sig = ['$key: String']
+ op_arg = ['key: $key']
+ for k,v in field_dict.items():
+ op_sig.append('$'+k+': '+v)
+ op_arg.append(k+': $'+k)
+
+ op_data = {}
+ op_data['op_name'] = snake_to_pascal_case(func_name)
+ op_data['op_sig'] = ', '.join(op_sig)
+ op_data['op_arg'] = ', '.join(op_arg)
+
+ j2_template = Template(template)
+
+ res = j2_template.render(op_data)
+
+ return res
+
def generate_composite_definitions():
- results = []
+ schema = []
+ client_op = []
for name,func in queries.items():
res = create_schema(name, func, query_template)
- results.append(res)
+ schema.append(res)
+ res = create_client_op(name, func, op_query_template)
+ client_op.append(res)
for name,func in mutations.items():
res = create_schema(name, func, mutation_template)
- results.append(res)
+ schema.append(res)
+ res = create_client_op(name, func, op_mutation_template)
+ client_op.append(res)
- out = '\n'.join(results)
+ out = '\n'.join(schema)
with open(f'{SCHEMA_PATH}/composite.graphql', 'w') as f:
f.write(out)
+ out = '\n'.join(client_op)
+ with open(f'{CLIENT_OP_PATH}/composite.graphql', 'w') as f:
+ f.write(out)
+
if __name__ == '__main__':
generate_composite_definitions()
diff --git a/src/services/api/graphql/generate/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py
index 831faa41e..1d5ff1e53 100755
--- a/src/services/api/graphql/generate/schema_from_config_session.py
+++ b/src/services/api/graphql/generate/schema_from_config_session.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -20,8 +20,7 @@
import os
import sys
-import json
-from inspect import signature, getmembers, isfunction, isclass, getmro
+from inspect import signature
from jinja2 import Template
from vyos.defaults import directories
@@ -32,9 +31,9 @@ if __package__ is None or __package__ == '':
else:
from .. libs.op_mode import snake_to_pascal_case, map_type_name
from . config_session_function import queries, mutations
- from .. import state
SCHEMA_PATH = directories['api_schema']
+CLIENT_OP_PATH = directories['api_client_op']
schema_data: dict = {'schema_name': '',
'schema_fields': []}
@@ -85,6 +84,30 @@ extend type Mutation {
}
"""
+op_query_template = """
+query {{ op_name }} ({{ op_sig }}) {
+ {{ op_name }} (data: { {{ op_arg }} }) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+"""
+
+op_mutation_template = """
+mutation {{ op_name }} ({{ op_sig }}) {
+ {{ op_name }} (data: { {{ op_arg }} }) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
+"""
+
def create_schema(func_name: str, func: callable, template: str) -> str:
sig = signature(func)
@@ -104,19 +127,52 @@ def create_schema(func_name: str, func: callable, template: str) -> str:
return res
+def create_client_op(func_name: str, func: callable, template: str) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ op_sig = ['$key: String']
+ op_arg = ['key: $key']
+ for k,v in field_dict.items():
+ op_sig.append('$'+k+': '+v)
+ op_arg.append(k+': $'+k)
+
+ op_data = {}
+ op_data['op_name'] = snake_to_pascal_case(func_name)
+ op_data['op_sig'] = ', '.join(op_sig)
+ op_data['op_arg'] = ', '.join(op_arg)
+
+ j2_template = Template(template)
+
+ res = j2_template.render(op_data)
+
+ return res
+
def generate_config_session_definitions():
- results = []
+ schema = []
+ client_op = []
for name,func in queries.items():
res = create_schema(name, func, query_template)
- results.append(res)
+ schema.append(res)
+ res = create_client_op(name, func, op_query_template)
+ client_op.append(res)
for name,func in mutations.items():
res = create_schema(name, func, mutation_template)
- results.append(res)
+ schema.append(res)
+ res = create_client_op(name, func, op_mutation_template)
+ client_op.append(res)
- out = '\n'.join(results)
+ out = '\n'.join(schema)
with open(f'{SCHEMA_PATH}/configsession.graphql', 'w') as f:
f.write(out)
+ out = '\n'.join(client_op)
+ with open(f'{CLIENT_OP_PATH}/configsession.graphql', 'w') as f:
+ f.write(out)
+
if __name__ == '__main__':
generate_config_session_definitions()
diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py
index cb7b0fd21..229ccf90f 100755
--- a/src/services/api/graphql/generate/schema_from_op_mode.py
+++ b/src/services/api/graphql/generate/schema_from_op_mode.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-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
@@ -35,7 +35,6 @@ if __package__ is None or __package__ == '':
else:
from .. libs.op_mode import is_show_function_name
from .. libs.op_mode import snake_to_pascal_case, map_type_name
- from .. import state
OP_MODE_PATH = directories['op_mode']
SCHEMA_PATH = directories['api_schema']
diff --git a/src/services/api/graphql/graphql/client_op/auth_token.graphql b/src/services/api/graphql/graphql/client_op/auth_token.graphql
new file mode 100644
index 000000000..5ea2ecc1c
--- /dev/null
+++ b/src/services/api/graphql/graphql/client_op/auth_token.graphql
@@ -0,0 +1,10 @@
+
+mutation AuthToken ($username: String!, $password: String!) {
+ AuthToken (data: { username: $username, password: $password }) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index a380f2e66..894f9e24d 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -329,7 +329,7 @@ tag_regex_schema = op_type_schema.extend({
forward_zone_add_schema = op_type_schema.extend({
'data': {
str: {
- 'server': [str],
+ 'name_server': [str],
'addnta': Any({}, None),
'recursion_desired': Any({}, None),
}
diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py
new file mode 100644
index 000000000..f61cbc4a2
--- /dev/null
+++ b/src/tests/test_config_diff.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 vyos.configtree
+
+from unittest import TestCase
+
+class TestConfigDiff(TestCase):
+ def setUp(self):
+ with open('tests/data/config.left', 'r') as f:
+ config_string = f.read()
+ self.config_left = vyos.configtree.ConfigTree(config_string)
+
+ with open('tests/data/config.right', 'r') as f:
+ config_string = f.read()
+ self.config_right = vyos.configtree.ConfigTree(config_string)
+
+ self.config_null = vyos.configtree.ConfigTree('')
+
+ def test_unit(self):
+ diff = vyos.configtree.DiffTree(self.config_left, self.config_null)
+ sub = diff.sub
+ self.assertEqual(sub.to_string(), self.config_left.to_string())
+
+ diff = vyos.configtree.DiffTree(self.config_null, self.config_left)
+ add = diff.add
+ self.assertEqual(add.to_string(), self.config_left.to_string())
+
+ def test_symmetry(self):
+ lr_diff = vyos.configtree.DiffTree(self.config_left,
+ self.config_right)
+ rl_diff = vyos.configtree.DiffTree(self.config_right,
+ self.config_left)
+
+ sub = lr_diff.sub
+ add = rl_diff.add
+ self.assertEqual(sub.to_string(), add.to_string())
+ add = lr_diff.add
+ sub = rl_diff.sub
+ self.assertEqual(add.to_string(), sub.to_string())
+
+ def test_identity(self):
+ lr_diff = vyos.configtree.DiffTree(self.config_left,
+ self.config_right)
+
+ sub = lr_diff.sub
+ inter = lr_diff.inter
+ add = lr_diff.add
+
+ r_union = vyos.configtree.union(add, inter)
+ l_union = vyos.configtree.union(sub, inter)
+
+ self.assertEqual(r_union.to_string(),
+ self.config_right.to_string(ordered_values=True))
+ self.assertEqual(l_union.to_string(),
+ self.config_left.to_string(ordered_values=True))
diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py
index 6e0a071f8..8148aa79b 100644
--- a/src/tests/test_config_parser.py
+++ b/src/tests/test_config_parser.py
@@ -34,8 +34,8 @@ class TestConfigParser(TestCase):
def test_top_level_tag(self):
self.assertTrue(self.config.exists(["top-level-tag-node"]))
- # No sorting is intentional, child order must be preserved
- self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["foo", "bar"])
+ # Sorting is now intentional, during parsing of config
+ self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["bar", "foo"])
def test_copy(self):
self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"])