From e2259e25029a142ba607b5865337b38ff8a482aa Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Thu, 4 Aug 2022 13:04:30 +0000 Subject: utils: T4594: Add convert_data util Convert multiple types of data to types usable in CLI For example 'vici' returns values in bytestring/bytes and we can decode them all at once --- python/vyos/util.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'python') diff --git a/python/vyos/util.py b/python/vyos/util.py index b86b1949c..8df9ef7d6 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -800,6 +800,32 @@ def dict_search_recursive(dict_object, key, path=[]): for x in dict_search_recursive(j, key, new_path): yield x +def convert_data(data): + """Convert multiple types of data to types usable in CLI + + Args: + data (str | bytes | list | OrderedDict): input data + + Returns: + str | list | dict: converted data + """ + from collections import OrderedDict + + if isinstance(data, str): + return data + if isinstance(data, bytes): + return data.decode() + if isinstance(data, list): + list_tmp = [] + for item in data: + list_tmp.append(convert_data(item)) + return list_tmp + if isinstance(data, OrderedDict): + dict_tmp = {} + for key, value in data.items(): + dict_tmp[key] = convert_data(value) + return dict_tmp + def get_bridge_fdb(interface): """ Returns the forwarding database entries for a given interface """ if not os.path.exists(f'/sys/class/net/{interface}'): -- cgit v1.2.3 From fd15f9d2ab6a7e5bbc07ff2e8b10c064984492ce Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Thu, 18 Aug 2022 17:09:17 +0000 Subject: firewall: T4622: Add TCP MSS option Ability to drop|accept packets based on TCP MSS size set firewall name rule tcp mss '501-1460' --- interface-definitions/include/firewall/tcp-flags.xml.i | 17 +++++++++++++++++ python/vyos/firewall.py | 5 +++++ smoketest/scripts/cli/test_firewall.py | 8 +++++++- 3 files changed, 29 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/interface-definitions/include/firewall/tcp-flags.xml.i b/interface-definitions/include/firewall/tcp-flags.xml.i index b99896687..5a7b5a8d3 100644 --- a/interface-definitions/include/firewall/tcp-flags.xml.i +++ b/interface-definitions/include/firewall/tcp-flags.xml.i @@ -114,6 +114,23 @@ + + + Maximum segment size (MSS) + + u32:1-16384 + Maximum segment size + + + <min>-<max> + TCP MSS range (use '-' as delimiter) + + + + + + + diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 3e2de4c3f..663c4394a 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -297,6 +297,11 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if tcp_flags: output.append(parse_tcp_flags(tcp_flags)) + # TCP MSS + tcp_mss = dict_search_args(rule_conf, 'tcp', 'mss') + if tcp_mss: + output.append(f'tcp option maxseg size {tcp_mss}') + output.append('counter') if 'set' in rule_conf: diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 4de90e1ec..684a07681 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -177,6 +177,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip filter') def test_basic_rules(self): + mss_range = '501-1460' self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) self.cli_set(['firewall', 'name', 'smoketest', 'enable-default-log']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) @@ -203,6 +204,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'destination', 'port', '22']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'count', '10']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'time', 'minute']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'protocol', 'tcp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'flags', 'syn']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'mss', mss_range]) self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) @@ -214,7 +219,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'log prefix "[smoketest-2-R]" level err', 'ip ttl > 102', 'reject'], ['tcp dport { 22 }', 'limit rate 5/minute', 'return'], ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'], - ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'] + ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'], + [f'tcp flags & syn == syn tcp option maxseg size {mss_range}'] ] self.verify_nftables(nftables_search, 'ip filter') -- cgit v1.2.3 From c0f5d00d92667f2a45896180cd05747c3ba82782 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Sat, 20 Aug 2022 13:48:30 +0000 Subject: ocserv: T4597: Fix check bounded port by service itself We check listen port before commit service if is port available and not bounded, but when we start openconnect our own port starts be bounded by "ocserv-main" process and next commit will be fail as port is already bound To fix it, extend check if port already bonded and it is not our self process "ocserv-main" --- python/vyos/util.py | 23 +++++++++++++++++++++++ src/conf_mode/vpn_openconnect.py | 5 ++++- 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/util.py b/python/vyos/util.py index b86b1949c..c1459f02a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -471,6 +471,29 @@ def process_named_running(name): return p.pid return None +def is_listen_port_bind_service(port: int, service: str) -> bool: + """Check if listen port bound to expected program name + :param port: Bind port + :param service: Program name + :return: bool + + Example: + % is_listen_port_bind_service(443, 'nginx') + True + % is_listen_port_bind_service(443, 'ocservr-main') + False + """ + from psutil import net_connections as connections + from psutil import Process as process + for connection in connections(): + addr = connection.laddr + pid = connection.pid + pid_name = process(pid).name() + pid_port = addr.port + if service == pid_name and port == pid_port: + return True + return False + def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index a3e774678..240546817 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -25,6 +25,7 @@ from vyos.template import render from vyos.util import call from vyos.util import check_port_availability from vyos.util import is_systemd_service_running +from vyos.util import is_listen_port_bind_service from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError @@ -77,8 +78,10 @@ def verify(ocserv): if ocserv is None: return None # Check if listen-ports not binded other services + # It can be only listen by 'ocserv-main' for proto, port in ocserv.get('listen_ports').items(): - if check_port_availability('0.0.0.0', int(port), proto) is not True: + if check_port_availability('0.0.0.0', int(port), proto) is not True and \ + not is_listen_port_bind_service(int(port), 'ocserv-main'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') # Check authentication if "authentication" in ocserv: -- cgit v1.2.3 From f60d0e1ce029925b843f635b36154c90049b9577 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 22 Aug 2022 17:52:58 +0200 Subject: bridge: T4632: vlan aware bridge lacks CPU forwarding The VLAN aware bridge was forwarding traffic between member ports, but traffic destined torwards the CPU was dropped. This resulted in a gateway not reachable or DHCP leases that could not be handed out. Tested via: VyOS set interfaces bridge br0 enable-vlan set interfaces bridge br0 member interface eth1 allowed-vlan '10' set interfaces bridge br0 member interface eth1 allowed-vlan '20' set interfaces bridge br0 member interface eth1 allowed-vlan '30' set interfaces bridge br0 member interface eth1 allowed-vlan '40' set interfaces bridge br0 member interface eth1 native-vlan '40' set interfaces bridge br0 member interface eth2 allowed-vlan '30' set interfaces bridge br0 member interface eth2 allowed-vlan '20' set interfaces bridge br0 member interface eth2 allowed-vlan '10' set interfaces bridge br0 member interface eth2 allowed-vlan '40' set interfaces bridge br0 vif 10 address '10.0.10.1/24' set interfaces bridge br0 vif 20 address '10.0.20.1/24' set interfaces bridge br0 vif 30 address '10.0.30.1/24' set interfaces bridge br0 vif 40 address '10.0.40.1/24' Arista vEOS vlan 10,20,30,40 interface Ethernet1 switchport trunk allowed vlan 10,20,30,40 interface Vlan10 ip address 10.0.10.2/24 interface Vlan20 ip address 10.0.20.2/24 interface Vlan30 ip address 10.0.30.2/24 interface Vlan40 ip address 10.0.40.2/24 interface Ethernet1 switchport trunk allowed vlan 10,20,30,40 switchport mode trunk spanning-tree portfast Cisco vIOS interface GigabitEthernet0/0 ip address 10.0.40.3 255.255.255.0 duplex auto speed auto media-type rj45 ! interface GigabitEthernet0/0.10 encapsulation dot1Q 10 ip address 10.0.10.3 255.255.255.0 ! interface GigabitEthernet0/0.20 encapsulation dot1Q 20 ip address 10.0.20.3 255.255.255.0 ! interface GigabitEthernet0/0.30 encapsulation dot1Q 30 ip address 10.0.30.3 255.255.255.0 ! --- python/vyos/ifconfig/bridge.py | 20 +++- smoketest/scripts/cli/test_interfaces_bridge.py | 137 ++++++++++++------------ 2 files changed, 88 insertions(+), 69 deletions(-) (limited to 'python') diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 758967fbc..aa818bc5f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -295,8 +295,24 @@ class BridgeIf(Interface): self.del_port(member) # enable/disable Vlan Filter - vlan_filter = '1' if 'enable_vlan' in config else '0' - self.set_vlan_filter(vlan_filter) + tmp = '1' if 'enable_vlan' in config else '0' + self.set_vlan_filter(tmp) + + # add VLAN interfaces to local 'parent' bridge to allow forwarding + if 'enable_vlan' in config: + for vlan in config.get('vif_remove', {}): + # Remove old VLANs from the bridge + cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self' + self._cmd(cmd) + + for vlan in config.get('vif', {}): + cmd = f'bridge vlan add dev {self.ifname} vid {vlan} self' + self._cmd(cmd) + + # VLAN of bridge parent interface is always 1. VLAN 1 is the default + # VLAN for all unlabeled packets + cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self' + self._cmd(cmd) tmp = dict_search('member.interface', config) if tmp: diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 8f711af20..6d7af78eb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -19,6 +19,7 @@ import json import unittest from base_interfaces_test import BasicInterfaceTest +from copy import deepcopy from glob import glob from netifaces import interfaces @@ -224,85 +225,78 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): super().test_vif_8021q_mtu_limits() def test_bridge_vlan_filter(self): - def _verify_members() -> None: - # check member interfaces are added on the bridge - for interface in self._interfaces: - bridge_members = [] - for tmp in glob(f'/sys/class/net/{interface}/lower_*'): - bridge_members.append(os.path.basename(tmp).replace('lower_', '')) - - # We can not use assertListEqual() b/c the position of the interface - # names within the list is not fixed - self.assertEqual(len(self._members), len(bridge_members)) - for member in self._members: - self.assertIn(member, bridge_members) - - def _check_vlan_filter() -> None: - for interface in self._interfaces: - tmp = cmd(f'bridge -j vlan show dev {interface}') - tmp = json.loads(tmp) - self.assertIsNotNone(tmp) - - for interface_status in tmp: - ifname = interface_status['ifname'] - for interface in self._members: - vlan_success = 0; - if interface == ifname: - vlans_status = interface_status['vlans'] - for vlan_status in vlans_status: - vlan_id = vlan_status['vlan'] - flag_num = 0 - if 'flags' in vlan_status: - flags = vlan_status['flags'] - for flag in flags: - flag_num = flag_num +1 - if vlan_id == 2: - if flag_num == 0: - vlan_success = vlan_success + 1 - else: - for id in range(4,10): - if vlan_id == id: - if flag_num == 0: - vlan_success = vlan_success + 1 - if vlan_id >= 101: - if flag_num == 2: - vlan_success = vlan_success + 1 - self.assertGreaterEqual(vlan_success, 7) - - vif_vlan = 2 + vifs = ['10', '20', '30', '40'] + native_vlan = '20' + # Add member interface to bridge and set VLAN filter for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) self.cli_set(base + ['address', '192.0.2.1/24']) - self.cli_set(base + ['vif', str(vif_vlan), 'address', '192.0.3.1/24']) - self.cli_set(base + ['vif', str(vif_vlan), 'mtu', self._mtu]) - vlan_id = 101 - allowed_vlan = 2 - allowed_vlan_range = '4-9' - # assign members to bridge interface + for vif in vifs: + self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24']) + self.cli_set(base + ['vif', vif, 'mtu', self._mtu]) + for member in self._members: base_member = base + ['member', 'interface', member] - self.cli_set(base_member + ['allowed-vlan', str(allowed_vlan)]) - self.cli_set(base_member + ['allowed-vlan', allowed_vlan_range]) - self.cli_set(base_member + ['native-vlan', str(vlan_id)]) - vlan_id += 1 + self.cli_set(base_member + ['native-vlan', native_vlan]) + for vif in vifs: + self.cli_set(base_member + ['allowed-vlan', vif]) # commit config self.cli_commit() + def _verify_members(interface, members) -> None: + # check member interfaces are added on the bridge + bridge_members = [] + for tmp in glob(f'/sys/class/net/{interface}/lower_*'): + bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + + self.assertListEqual(sorted(members), sorted(bridge_members)) + + def _check_vlan_filter(interface, vifs) -> None: + configured_vlan_ids = [] + + bridge_json = cmd(f'bridge -j vlan show dev {interface}') + bridge_json = json.loads(bridge_json) + self.assertIsNotNone(bridge_json) + + for tmp in bridge_json: + self.assertIn('vlans', tmp) + + for vlan in tmp['vlans']: + self.assertIn('vlan', vlan) + configured_vlan_ids.append(str(vlan['vlan'])) + + # Verify native VLAN ID has 'PVID' flag set on individual member ports + if not interface.startswith('br') and str(vlan['vlan']) == native_vlan: + self.assertIn('flags', vlan) + self.assertIn('PVID', vlan['flags']) + + self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs)) + # Verify correct setting of VLAN filter function for interface in self._interfaces: tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering') self.assertEqual(tmp, '1') - # Execute the program to obtain status information and verify proper - # VLAN filter setup - _check_vlan_filter() + # Obtain status information and verify proper VLAN filter setup. + # First check if all members are present, second check if all VLANs + # are assigned on the parend bridge interface, third verify all the + # VLANs are properly setup on the downstream "member" ports + for interface in self._interfaces: + # check member interfaces are added on the bridge + _verify_members(interface, self._members) - # check member interfaces are added on the bridge - _verify_members() + # Check if all VLAN ids are properly set up. Bridge interface always + # has native VLAN 1 + tmp = deepcopy(vifs) + tmp.append('1') + _check_vlan_filter(interface, tmp) + + for member in self._members: + _check_vlan_filter(member, vifs) # change member interface description to trigger config update, # VLANs must still exist (T4565) @@ -313,12 +307,22 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # commit config self.cli_commit() - # check member interfaces are added on the bridge - _verify_members() + # Obtain status information and verify proper VLAN filter setup. + # First check if all members are present, second check if all VLANs + # are assigned on the parend bridge interface, third verify all the + # VLANs are properly setup on the downstream "member" ports + for interface in self._interfaces: + # check member interfaces are added on the bridge + _verify_members(interface, self._members) + + # Check if all VLAN ids are properly set up. Bridge interface always + # has native VLAN 1 + tmp = deepcopy(vifs) + tmp.append('1') + _check_vlan_filter(interface, tmp) - # Execute the program to obtain status information and verify proper - # VLAN filter setup - _check_vlan_filter() + for member in self._members: + _check_vlan_filter(member, vifs) # delete all members for interface in self._interfaces: @@ -337,7 +341,6 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): for member in self._members: self.assertNotIn(member, bridge_members) - def test_bridge_vif_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] -- cgit v1.2.3 From f66ad001e153ee42bc46edbe7df55145b7971544 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 22 Aug 2022 12:03:56 -0500 Subject: graphql: T3993: reorganize/rename directory structure --- python/vyos/defaults.py | 2 +- src/services/api/graphql/graphql/mutations.py | 4 +- src/services/api/graphql/graphql/queries.py | 4 +- src/services/api/graphql/recipes/__init__.py | 0 .../api/graphql/recipes/queries/system_status.py | 38 ---- .../remove_firewall_address_group_members.py | 35 ---- src/services/api/graphql/recipes/session.py | 207 --------------------- .../recipes/templates/create_dhcp_server.tmpl | 9 - .../templates/create_firewall_address_group.tmpl | 4 - .../create_firewall_address_ipv_6_group.tmpl | 4 - .../templates/create_interface_ethernet.tmpl | 5 - .../remove_firewall_address_group_members.tmpl | 3 - ...emove_firewall_address_ipv_6_group_members.tmpl | 3 - .../update_firewall_address_group_members.tmpl | 3 - ...pdate_firewall_address_ipv_6_group_members.tmpl | 3 - src/services/api/graphql/session/__init__.py | 0 .../api/graphql/session/composite/system_status.py | 38 ++++ .../remove_firewall_address_group_members.py | 35 ++++ src/services/api/graphql/session/session.py | 207 +++++++++++++++++++++ .../session/templates/create_dhcp_server.tmpl | 9 + .../templates/create_firewall_address_group.tmpl | 4 + .../create_firewall_address_ipv_6_group.tmpl | 4 + .../templates/create_interface_ethernet.tmpl | 5 + .../remove_firewall_address_group_members.tmpl | 3 + ...emove_firewall_address_ipv_6_group_members.tmpl | 3 + .../update_firewall_address_group_members.tmpl | 3 + ...pdate_firewall_address_ipv_6_group_members.tmpl | 3 + 27 files changed, 319 insertions(+), 319 deletions(-) delete mode 100644 src/services/api/graphql/recipes/__init__.py delete mode 100755 src/services/api/graphql/recipes/queries/system_status.py delete mode 100644 src/services/api/graphql/recipes/remove_firewall_address_group_members.py delete mode 100644 src/services/api/graphql/recipes/session.py delete mode 100644 src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl delete mode 100644 src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl create mode 100644 src/services/api/graphql/session/__init__.py create mode 100755 src/services/api/graphql/session/composite/system_status.py create mode 100644 src/services/api/graphql/session/override/remove_firewall_address_group_members.py create mode 100644 src/services/api/graphql/session/session.py create mode 100644 src/services/api/graphql/session/templates/create_dhcp_server.tmpl create mode 100644 src/services/api/graphql/session/templates/create_firewall_address_group.tmpl create mode 100644 src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl create mode 100644 src/services/api/graphql/session/templates/create_interface_ethernet.tmpl create mode 100644 src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl create mode 100644 src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl create mode 100644 src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl create mode 100644 src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl (limited to 'python') diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 09ae73eac..6894fc4da 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -26,7 +26,7 @@ directories = { "templates": "/usr/share/vyos/templates/", "certbot": "/config/auth/letsencrypt", "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", - "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/", + "api_templates": "/usr/libexec/vyos/services/api/graphql/session/templates/", "vyos_udev_dir": "/run/udev/vyos" } diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 3e89fb239..c8ae0f516 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -21,7 +21,7 @@ from makefun import with_signature from .. import state from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session mutation = ObjectType("Mutation") @@ -71,7 +71,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): # one may override the session functions with a local subclass try: - mod = import_module(f'api.graphql.recipes.{func_base_name}') + mod = import_module(f'api.graphql.session.override.{func_base_name}') klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index f6544709e..921a66274 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -21,7 +21,7 @@ from makefun import with_signature from .. import state from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session query = ObjectType("Query") @@ -71,7 +71,7 @@ def make_query_resolver(query_name, class_name, session_func): # one may override the session functions with a local subclass try: - mod = import_module(f'api.graphql.recipes.{func_base_name}') + mod = import_module(f'api.graphql.session.override.{func_base_name}') klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/recipes/queries/system_status.py deleted file mode 100755 index 8dadcc9f3..000000000 --- a/src/services/api/graphql/recipes/queries/system_status.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# - -import os -import sys -import json -import importlib.util - -from vyos.defaults import directories - -from api.graphql.utils.util import load_op_mode_as_module - -def get_system_version() -> dict: - show_version = load_op_mode_as_module('version.py') - return show_version.show(raw=True, funny=False) - -def get_system_uptime() -> dict: - show_uptime = load_op_mode_as_module('show_uptime.py') - return show_uptime.get_raw_data() - -def get_system_ram_usage() -> dict: - show_ram = load_op_mode_as_module('memory.py') - return show_ram.show(raw=True) diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py deleted file mode 100644 index b91932e14..000000000 --- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2021 VyOS maintainers and contributors -# -# 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 . - -from . session import Session - -class RemoveFirewallAddressGroupMembers(Session): - def __init__(self, session, data): - super().__init__(session, data) - - # Define any custom processing of parameters here by overriding - # configure: - # - # def configure(self): - # self._data = transform_data(self._data) - # super().configure() - # self.clean_up() - - def configure(self): - super().configure() - - group_name = self._data['name'] - path = ['firewall', 'group', 'address-group', group_name] - self.delete_path_if_childless(path) diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py deleted file mode 100644 index ac185beb7..000000000 --- a/src/services/api/graphql/recipes/session.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright 2021-2022 VyOS maintainers and contributors -# -# 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 . - -import os -import json - -from ariadne import convert_camel_case_to_snake - -from vyos.config import Config -from vyos.configtree import ConfigTree -from vyos.defaults import directories -from vyos.template import render - -from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name - -op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') - -class Session: - """ - Wrapper for calling configsession functions based on GraphQL requests. - Non-nullable fields in the respective schema allow avoiding a key check - in 'data'. - """ - def __init__(self, session, data): - self._session = session - self._data = data - self._name = convert_camel_case_to_snake(type(self).__name__) - - try: - with open(op_mode_include_file) as f: - self._op_mode_list = json.loads(f.read()) - except Exception: - self._op_mode_list = None - - def configure(self): - session = self._session - data = self._data - func_base_name = self._name - - tmpl_file = f'{func_base_name}.tmpl' - cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = directories['api_templates'] - - try: - render(cmd_file, tmpl_file, data, location=tmpl_dir) - commands = [] - with open(cmd_file) as f: - lines = f.readlines() - for line in lines: - commands.append(line.split()) - for cmd in commands: - if cmd[0] == 'set': - session.set(cmd[1:]) - elif cmd[0] == 'delete': - session.delete(cmd[1:]) - else: - raise ValueError('Operation must be "set" or "delete"') - session.commit() - except Exception as error: - raise error - - def delete_path_if_childless(self, path): - session = self._session - config = Config(session.get_session_env()) - if not config.list_nodes(path): - session.delete(path) - session.commit() - - def show_config(self): - session = self._session - data = self._data - out = '' - - try: - out = session.show_config(data['path']) - if data.get('config_format', '') == 'json': - config_tree = vyos.configtree.ConfigTree(out) - out = json.loads(config_tree.to_json()) - except Exception as error: - raise error - - return out - - def save(self): - session = self._session - data = self._data - if 'file_name' not in data or not data['file_name']: - data['file_name'] = '/config/config.boot' - - try: - session.save_config(data['file_name']) - except Exception as error: - raise error - - def load(self): - session = self._session - data = self._data - - try: - session.load_config(data['file_name']) - session.commit() - except Exception as error: - raise error - - def show(self): - session = self._session - data = self._data - out = '' - - try: - out = session.show(data['path']) - except Exception as error: - raise error - - return out - - def add(self): - session = self._session - data = self._data - - try: - res = session.install_image(data['location']) - except Exception as error: - raise error - - return res - - def delete(self): - session = self._session - data = self._data - - try: - res = session.remove_image(data['name']) - except Exception as error: - raise error - - return res - - def system_status(self): - import api.graphql.recipes.queries.system_status as system_status - - session = self._session - data = self._data - - status = {} - status['host_name'] = session.show(['host', 'name']).strip() - status['version'] = system_status.get_system_version() - status['uptime'] = system_status.get_system_uptime() - status['ram'] = system_status.get_system_ram_usage() - - return status - - def gen_op_query(self): - session = self._session - data = self._data - name = self._name - op_mode_list = self._op_mode_list - - # handle the case that the op-mode file contains underscores: - if op_mode_list is None: - raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") - (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) - if scriptname == '': - raise FileNotFoundError(f"No op-mode file named in string '{name}'") - - mod = load_op_mode_as_module(f'{scriptname}') - func = getattr(mod, func_name) - if len(list(data)) > 0: - res = func(True, **data) - else: - res = func(True) - - return res - - def gen_op_mutation(self): - session = self._session - data = self._data - name = self._name - op_mode_list = self._op_mode_list - - # handle the case that the op-mode file name contains underscores: - if op_mode_list is None: - raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") - (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) - if scriptname == '': - raise FileNotFoundError(f"No op-mode file named in string '{name}'") - - mod = load_op_mode_as_module(f'{scriptname}') - func = getattr(mod, func_name) - if len(list(data)) > 0: - res = func(**data) - else: - res = func() - - return res diff --git a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl deleted file mode 100644 index 70de43183..000000000 --- a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }} -set service dns forwarding allow-from {{ dns_forwarding_allow_from }} -set service dns forwarding cache-size {{ dns_forwarding_cache_size }} -set service dns forwarding listen-address {{ dns_forwarding_listen_address }} diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl deleted file mode 100644 index a890d0086..000000000 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -set firewall group address-group {{ name }} -{% for add in address %} -set firewall group address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl deleted file mode 100644 index e9b660722..000000000 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -set firewall group ipv6-address-group {{ name }} -{% for add in address %} -set firewall group ipv6-address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl deleted file mode 100644 index d9d7ed691..000000000 --- a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -{% if replace %} -delete interfaces ethernet {{ interface }} address -{% endif %} -set interfaces ethernet {{ interface }} address {{ address }} -set interfaces ethernet {{ interface }} description {{ description }} diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl deleted file mode 100644 index 458f3e5fc..000000000 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{% for add in address %} -delete firewall group address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl deleted file mode 100644 index 0efa0b226..000000000 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{% for add in address %} -delete firewall group ipv6-address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl deleted file mode 100644 index f56c61231..000000000 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{% for add in address %} -set firewall group address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl deleted file mode 100644 index f98a5517c..000000000 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{% for add in address %} -set firewall group ipv6-address-group {{ name }} address {{ add }} -{% endfor %} diff --git a/src/services/api/graphql/session/__init__.py b/src/services/api/graphql/session/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py new file mode 100755 index 000000000..8dadcc9f3 --- /dev/null +++ b/src/services/api/graphql/session/composite/system_status.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import json +import importlib.util + +from vyos.defaults import directories + +from api.graphql.utils.util import load_op_mode_as_module + +def get_system_version() -> dict: + show_version = load_op_mode_as_module('version.py') + return show_version.show(raw=True, funny=False) + +def get_system_uptime() -> dict: + show_uptime = load_op_mode_as_module('show_uptime.py') + return show_uptime.get_raw_data() + +def get_system_ram_usage() -> dict: + show_ram = load_op_mode_as_module('memory.py') + return show_ram.show(raw=True) diff --git a/src/services/api/graphql/session/override/remove_firewall_address_group_members.py b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py new file mode 100644 index 000000000..b91932e14 --- /dev/null +++ b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py @@ -0,0 +1,35 @@ +# Copyright 2021 VyOS maintainers and contributors +# +# 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 . + +from . session import Session + +class RemoveFirewallAddressGroupMembers(Session): + def __init__(self, session, data): + super().__init__(session, data) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self._data = transform_data(self._data) + # super().configure() + # self.clean_up() + + def configure(self): + super().configure() + + group_name = self._data['name'] + path = ['firewall', 'group', 'address-group', group_name] + self.delete_path_if_childless(path) diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py new file mode 100644 index 000000000..23bc7154c --- /dev/null +++ b/src/services/api/graphql/session/session.py @@ -0,0 +1,207 @@ +# Copyright 2021-2022 VyOS maintainers and contributors +# +# 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 . + +import os +import json + +from ariadne import convert_camel_case_to_snake + +from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.template import render + +from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name + +op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') + +class Session: + """ + Wrapper for calling configsession functions based on GraphQL requests. + Non-nullable fields in the respective schema allow avoiding a key check + in 'data'. + """ + def __init__(self, session, data): + self._session = session + self._data = data + self._name = convert_camel_case_to_snake(type(self).__name__) + + try: + with open(op_mode_include_file) as f: + self._op_mode_list = json.loads(f.read()) + except Exception: + self._op_mode_list = None + + def configure(self): + session = self._session + data = self._data + func_base_name = self._name + + tmpl_file = f'{func_base_name}.tmpl' + cmd_file = f'/tmp/{func_base_name}.cmds' + tmpl_dir = directories['api_templates'] + + try: + render(cmd_file, tmpl_file, data, location=tmpl_dir) + commands = [] + with open(cmd_file) as f: + lines = f.readlines() + for line in lines: + commands.append(line.split()) + for cmd in commands: + if cmd[0] == 'set': + session.set(cmd[1:]) + elif cmd[0] == 'delete': + session.delete(cmd[1:]) + else: + raise ValueError('Operation must be "set" or "delete"') + session.commit() + except Exception as error: + raise error + + def delete_path_if_childless(self, path): + session = self._session + config = Config(session.get_session_env()) + if not config.list_nodes(path): + session.delete(path) + session.commit() + + def show_config(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show_config(data['path']) + if data.get('config_format', '') == 'json': + config_tree = vyos.configtree.ConfigTree(out) + out = json.loads(config_tree.to_json()) + except Exception as error: + raise error + + return out + + def save(self): + session = self._session + data = self._data + if 'file_name' not in data or not data['file_name']: + data['file_name'] = '/config/config.boot' + + try: + session.save_config(data['file_name']) + except Exception as error: + raise error + + def load(self): + session = self._session + data = self._data + + try: + session.load_config(data['file_name']) + session.commit() + except Exception as error: + raise error + + def show(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show(data['path']) + except Exception as error: + raise error + + return out + + def add(self): + session = self._session + data = self._data + + try: + res = session.install_image(data['location']) + except Exception as error: + raise error + + return res + + def delete(self): + session = self._session + data = self._data + + try: + res = session.remove_image(data['name']) + except Exception as error: + raise error + + return res + + def system_status(self): + import api.graphql.session.composite.system_status as system_status + + session = self._session + data = self._data + + status = {} + status['host_name'] = session.show(['host', 'name']).strip() + status['version'] = system_status.get_system_version() + status['uptime'] = system_status.get_system_uptime() + status['ram'] = system_status.get_system_ram_usage() + + return status + + def gen_op_query(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(True, **data) + else: + res = func(True) + + return res + + def gen_op_mutation(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file name contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(**data) + else: + res = func() + + return res diff --git a/src/services/api/graphql/session/templates/create_dhcp_server.tmpl b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl new file mode 100644 index 000000000..70de43183 --- /dev/null +++ b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl @@ -0,0 +1,9 @@ +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }} +set service dns forwarding allow-from {{ dns_forwarding_allow_from }} +set service dns forwarding cache-size {{ dns_forwarding_cache_size }} +set service dns forwarding listen-address {{ dns_forwarding_listen_address }} diff --git a/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl new file mode 100644 index 000000000..a890d0086 --- /dev/null +++ b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl @@ -0,0 +1,4 @@ +set firewall group address-group {{ name }} +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl new file mode 100644 index 000000000..e9b660722 --- /dev/null +++ b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl @@ -0,0 +1,4 @@ +set firewall group ipv6-address-group {{ name }} +{% for add in address %} +set firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl new file mode 100644 index 000000000..d9d7ed691 --- /dev/null +++ b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl @@ -0,0 +1,5 @@ +{% if replace %} +delete interfaces ethernet {{ interface }} address +{% endif %} +set interfaces ethernet {{ interface }} address {{ address }} +set interfaces ethernet {{ interface }} description {{ description }} diff --git a/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl new file mode 100644 index 000000000..458f3e5fc --- /dev/null +++ b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl new file mode 100644 index 000000000..0efa0b226 --- /dev/null +++ b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl new file mode 100644 index 000000000..f56c61231 --- /dev/null +++ b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl new file mode 100644 index 000000000..f98a5517c --- /dev/null +++ b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} -- cgit v1.2.3 From eb4a7ee3afc0765671ce0fa379ab5e3518e9e49e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 24 Aug 2022 21:43:10 +0200 Subject: T4630: can not use same source-interface for macsec and pseudo-ethernet A macsec interface requires a dedicated source interface, it can not be shared with another macsec or a pseudo-ethernet interface. set interfaces macsec macsec10 address '192.168.2.1/30' set interfaces macsec macsec10 security cipher 'gcm-aes-256' set interfaces macsec macsec10 security encrypt set interfaces macsec macsec10 security mka cak '232e44b7fda6f8e2d88a07bf78a7aff4232e44b7fda6f8e2d88a07bf78a7aff4' set interfaces macsec macsec10 security mka ckn '09924585a6f3010208cf5222ef24c821405b0e34f4b4f63b1f0ced474b9bb6e6' set interfaces macsec macsec10 source-interface 'eth1' commit set interfaces pseudo-ethernet peth0 source-interface eth1 commit Reuslts in FileNotFoundError: [Errno 2] failed to run command: ip link add peth0 link eth1 type macvlan mode private returned: exit code: 2 noteworthy: cmd 'ip link add peth0 link eth1 type macvlan mode private' returned (out): returned (err): RTNETLINK answers: Device or resource busy [[interfaces pseudo-ethernet peth0]] failed Commit failed --- python/vyos/configdict.py | 11 +++++++++-- python/vyos/configverify.py | 6 ++++++ src/conf_mode/interfaces-macsec.py | 8 +------- src/conf_mode/interfaces-pseudo-ethernet.py | 7 ++++++- 4 files changed, 22 insertions(+), 10 deletions(-) (limited to 'python') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 8f822a97d..912bc94f2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -295,11 +295,18 @@ def is_source_interface(conf, interface, intftype=None): """ ret_val = None intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan'] - if intftype not in intftypes + [None]: + if not intftype: + intftype = intftypes + + if isinstance(intftype, str): + intftype = [intftype] + elif not isinstance(intftype, list): + raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!') + + if not all(x in intftypes for x in intftype): raise ValueError(f'unknown interface type "{intftype}" or it can not ' 'have a source-interface') - intftype = intftypes if intftype == None else [intftype] for it in intftype: base = ['interfaces', it] for intf in conf.list_nodes(base): diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 2ab3cb408..447ec795c 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -295,6 +295,12 @@ def verify_source_interface(config): raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface ' f'is already a member of bond "{bond_name}"!') + if 'is_source_interface' in config: + tmp = config['is_source_interface'] + src_ifname = config['source_interface'] + raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ + f'belongs to interface "{tmp}"!') + def verify_dhcpv6(config): """ Common helper function used by interface implementations to perform diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 870049a88..649ea8d50 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -67,7 +67,7 @@ def get_config(config=None): macsec.update({'shutdown_required': {}}) if 'source_interface' in macsec: - tmp = is_source_interface(conf, macsec['source_interface'], 'macsec') + tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet']) if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp}) return macsec @@ -102,12 +102,6 @@ def verify(macsec): # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit raise ConfigError('gcm-aes-128 requires a 256bit long key!') - if 'is_source_interface' in macsec: - tmp = macsec['is_source_interface'] - src_ifname = macsec['source_interface'] - raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ - f'belongs to interface "{tmp}"!') - if 'source_interface' in macsec: # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad # and 802.1q) - we need to check the underlaying MTU if our configured diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index f26a50a0e..20f2b1975 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 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 @@ -19,6 +19,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -51,6 +52,10 @@ def get_config(config=None): if 'source_interface' in peth: _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], peth['source_interface']) + # test if source-interface is maybe already used by another interface + tmp = is_source_interface(conf, peth['source_interface'], ['macsec']) + if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp}) + return peth def verify(peth): -- cgit v1.2.3 From cfde4b4986347d4b050b38c7dc50db9179894a81 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 25 Aug 2022 18:56:25 +0200 Subject: ifconfig: T2223: add vlan switch for Section.interfaces() Sometimes we are only interested in the parent interfaces without any VLAN subinterfaces. Extend the API with a vlan argument that defaults to True to keep the current behavior in place. --- python/vyos/ifconfig/section.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'python') diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 91f667b65..5e98cd510 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -88,7 +88,7 @@ class Section: raise ValueError(f'No type found for interface name: {name}') @classmethod - def _intf_under_section (cls,section=''): + def _intf_under_section (cls,section='',vlan=True): """ return a generator with the name of the configured interface which are under a section @@ -103,6 +103,9 @@ class Section: if section and ifsection != section: continue + if vlan == False and '.' in ifname: + continue + yield ifname @classmethod @@ -135,13 +138,14 @@ class Section: return l @classmethod - def interfaces(cls, section=''): + def interfaces(cls, section='', vlan=True): """ return a list of the name of the configured interface which are under a section - if no section is provided, then it returns all configured interfaces + if no section is provided, then it returns all configured interfaces. + If vlan is True, also Vlan subinterfaces will be returned """ - return cls._sort_interfaces(cls._intf_under_section(section)) + return cls._sort_interfaces(cls._intf_under_section(section, vlan)) @classmethod def _intf_with_feature(cls, feature=''): -- cgit v1.2.3 From 9126170f0b09285cf79f8c40584312bccd67c3e8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 27 Aug 2022 08:49:04 +0200 Subject: pppoe: T4648: do not install IPv6 default route from RA is no-default-route is set Adds a sysctl parameter to ignore the default router obtained from router advertisements when pppoe no-default-route is set. --- python/vyos/ifconfig/pppoe.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index 63ffc8069..437fe0cae 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors +# Copyright 2020-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,6 +14,7 @@ # License along with this library. If not, see . from vyos.ifconfig.interface import Interface +from vyos.validate import assert_range from vyos.util import get_interface_config @Interface.register @@ -27,6 +28,21 @@ class PPPoEIf(Interface): }, } + _sysfs_get = { + **Interface._sysfs_get,**{ + 'accept_ra_defrtr': { + 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', + } + } + } + + _sysfs_set = {**Interface._sysfs_set, **{ + 'accept_ra_defrtr': { + 'validate': lambda value: assert_range(value, 0, 2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', + }, + }} + def _remove_routes(self, vrf=None): # Always delete default routes when interface is removed vrf_cmd = '' @@ -70,6 +86,21 @@ class PPPoEIf(Interface): """ Get a synthetic MAC address. """ return self.get_mac_synthetic() + def set_accept_ra_defrtr(self, enable): + """ + Learn default router in Router Advertisement. + 1: enabled + 0: disable + + Example: + >>> from vyos.ifconfig import PPPoEIf + >>> PPPoEIf('pppoe1').set_accept_ra_defrtr(0) + """ + tmp = self.get_interface('accept_ra_defrtr') + if tmp == enable: + return None + self.set_interface('accept_ra_defrtr', enable) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -107,6 +138,10 @@ class PPPoEIf(Interface): tmp = config['vrf'] vrf = f'-c "vrf {tmp}"' + # learn default router in Router Advertisement. + tmp = '0' if 'no_default_route' in config else '1' + self.set_accept_ra_defrtr(tmp) + if 'no_default_route' not in config: # Set default route(s) pointing to PPPoE interface distance = config['default_route_distance'] -- cgit v1.2.3 From 0cc7e0a49094be809cccff9fb44288d883e6ef05 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 29 Aug 2022 14:55:32 +0000 Subject: firewall: T4655: Fix default action 'drop' for the firewall For some reason after firewall rewriting we are having default action 'accept' for 1.4 and default action 'drop' for 1.3 Fix this issue, set default action 'drop' --- python/vyos/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python') diff --git a/python/vyos/template.py b/python/vyos/template.py index eb7f06480..62303bd55 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors +# Copyright 2019-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -550,7 +550,7 @@ def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): @register_filter('nft_default_rule') def nft_default_rule(fw_conf, fw_name): output = ['counter'] - default_action = fw_conf.get('default_action', 'accept') + default_action = fw_conf.get('default_action', 'drop') if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() -- cgit v1.2.3 From b01f27b3bb3f4cbc6096011856d83009d0440313 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 29 Aug 2022 20:36:20 +0200 Subject: ethernet: T4653: bugfix copy-paste when processing NIC offloading Commit 31169fa8a763e ("vyos.ifconfig: T3619: only set offloading options if supported by NIC") added the new implementation which handles NIC offloading. Unfortunately every single implementation was copied from "gro" which resulted in a change to gro for each offloading option - thus options like lro, sg, tso had no effect at all. It all comes down to copy/paste errors ... one way or another. --- python/vyos/ifconfig/ethernet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'python') diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 1280fc238..b8deb3311 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -236,7 +236,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_large_receive_offload() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('lro', 'on' if state else 'off') else: print('Adapter does not support changing large-receive-offload settings!') return False @@ -273,7 +273,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_scatter_gather() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('sg', 'on' if state else 'off') else: print('Adapter does not support changing scatter-gather settings!') return False @@ -293,7 +293,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_tcp_segmentation_offload() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('tso', 'on' if state else 'off') else: print('Adapter does not support changing tcp-segmentation-offload settings!') return False @@ -359,5 +359,5 @@ class EthernetIf(Interface): for rx_tx, size in config['ring_buffer'].items(): self.set_ring_buffer(rx_tx, size) - # call base class first + # call base class last super().update(config) -- cgit v1.2.3 From 8792e13f80f4150fbe2dae5d8eedf7f40703411e Mon Sep 17 00:00:00 2001 From: zsdc Date: Tue, 30 Aug 2022 15:10:36 +0300 Subject: opmode: T4657: fixed opmode with return type hints This commit excludes `return` from `typing.get_type_hints()` output, which allows generate argparse arguments for function properly. --- python/vyos/opmode.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 628f7b3a2..7e3545c87 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -105,6 +105,8 @@ def run(module): subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__) type_hints = typing.get_type_hints(functions[function_name]) + if 'return' in type_hints: + del type_hints['return'] for opt in type_hints: th = type_hints[opt] -- cgit v1.2.3 From 69f79beee2070906b68f2b910296c362e7216278 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 30 Aug 2022 17:36:19 +0200 Subject: firewall: T4655: implement XML defaultValue for name and ipv6-name This extends the implementation of commit 0cc7e0a49094 ("firewall: T4655: Fix default action 'drop' for the firewall") in a way that we can now also use the XML node under "firewall name" and "firewall ipv6-name". This is a much cleaner approach which also adds the default value automatically to the CLIs completion helper ("?"). --- .../include/firewall/default-action.xml.i | 1 + python/vyos/template.py | 2 +- src/conf_mode/firewall.py | 30 +++++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) (limited to 'python') diff --git a/interface-definitions/include/firewall/default-action.xml.i b/interface-definitions/include/firewall/default-action.xml.i index b11dfd2e8..92a2fcaaf 100644 --- a/interface-definitions/include/firewall/default-action.xml.i +++ b/interface-definitions/include/firewall/default-action.xml.i @@ -21,5 +21,6 @@ (drop|reject|accept) + drop diff --git a/python/vyos/template.py b/python/vyos/template.py index 62303bd55..9804308c1 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -550,7 +550,7 @@ def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): @register_filter('nft_default_rule') def nft_default_rule(fw_conf, fw_name): output = ['counter'] - default_action = fw_conf.get('default_action', 'drop') + default_action = fw_conf['default_action'] if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 07eca722f..f0ea1a1e5 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -206,9 +206,31 @@ def get_config(config=None): firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary retrived. + # XXX: T2665: we currently have no nice way for defaults under tag + # nodes, thus we load the defaults "by hand" default_values = defaults(base) + for tmp in ['name', 'ipv6_name']: + if tmp in default_values: + del default_values[tmp] + firewall = dict_merge(default_values, firewall) + # Merge in defaults for IPv4 ruleset + if 'name' in firewall: + default_values = defaults(base + ['name']) + for name in firewall['name']: + firewall['name'][name] = dict_merge(default_values, + firewall['name'][name]) + + # Merge in defaults for IPv6 ruleset + if 'ipv6_name' in firewall: + default_values = defaults(base + ['ipv6-name']) + for ipv6_name in firewall['ipv6_name']: + firewall['ipv6_name'][ipv6_name] = dict_merge(default_values, + firewall['ipv6_name'][ipv6_name]) + firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) firewall['interfaces'] = get_firewall_interfaces(conf) firewall['zone_policy'] = get_firewall_zones(conf) @@ -315,7 +337,7 @@ def verify_nested_group(group_name, group, groups, seen): if g in seen: raise ConfigError(f'Group "{group_name}" has a circular reference') - + seen.append(g) if 'include' in groups[g]: @@ -378,7 +400,7 @@ def cleanup_commands(firewall): if firewall['geoip_updated']: geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] - + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) @@ -420,7 +442,7 @@ def cleanup_commands(firewall): if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: commands_sets.append(f'delete set {table} {set_name}') continue - + if set_name.startswith("RECENT_"): commands_sets.append(f'delete set {table} {set_name}') continue @@ -520,7 +542,7 @@ def apply(firewall): if install_result == 1: raise ConfigError('Failed to apply firewall') - # set fireall group domain-group xxx + # set firewall group domain-group xxx if 'group' in firewall: if 'domain_group' in firewall['group']: # T970 Enable a resolver (systemd daemon) that checks -- cgit v1.2.3