diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/configdict.py | 41 | ||||
-rw-r--r-- | python/vyos/configverify.py | 6 | ||||
-rw-r--r-- | python/vyos/firewall.py | 39 | ||||
-rw-r--r-- | python/vyos/ifconfig/bond.py | 12 | ||||
-rw-r--r-- | python/vyos/ifconfig/ethernet.py | 22 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 85 | ||||
-rw-r--r-- | python/vyos/nat.py | 188 | ||||
-rw-r--r-- | python/vyos/template.py | 26 | ||||
-rw-r--r-- | python/vyos/util.py | 8 |
9 files changed, 356 insertions, 71 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 912bc94f2..53decfbf5 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -643,7 +643,9 @@ def get_accel_dict(config, base, chap_secrets): from vyos.util import get_half_cpus from vyos.template import is_ipv4 - dict = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + dict = config.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. @@ -663,6 +665,18 @@ def get_accel_dict(config, base, chap_secrets): # added to individual local users instead - so we can simply delete them if dict_search('client_ipv6_pool.prefix.mask', default_values): del default_values['client_ipv6_pool']['prefix']['mask'] + # delete empty dicts + if len (default_values['client_ipv6_pool']['prefix']) == 0: + del default_values['client_ipv6_pool']['prefix'] + if len (default_values['client_ipv6_pool']) == 0: + del default_values['client_ipv6_pool'] + + # T2665: IPoE only - it has an interface tag node + # added to individual local users instead - so we can simply delete them + if dict_search('authentication.interface', default_values): + del default_values['authentication']['interface'] + if dict_search('interface', default_values): + del default_values['interface'] dict = dict_merge(default_values, dict) @@ -684,11 +698,9 @@ def get_accel_dict(config, base, chap_secrets): dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6}) del dict['name_server'] - # Add individual RADIUS server default values + # T2665: Add individual RADIUS server default values if dict_search('authentication.radius.server', dict): - # T2665 default_values = defaults(base + ['authentication', 'radius', 'server']) - for server in dict_search('authentication.radius.server', dict): dict['authentication']['radius']['server'][server] = dict_merge( default_values, dict['authentication']['radius']['server'][server]) @@ -698,22 +710,31 @@ def get_accel_dict(config, base, chap_secrets): if 'disable_accounting' in dict['authentication']['radius']['server'][server]: dict['authentication']['radius']['server'][server]['acct_port'] = '0' - # Add individual local-user default values + # T2665: Add individual local-user default values if dict_search('authentication.local_users.username', dict): - # T2665 default_values = defaults(base + ['authentication', 'local-users', 'username']) - for username in dict_search('authentication.local_users.username', dict): dict['authentication']['local_users']['username'][username] = dict_merge( default_values, dict['authentication']['local_users']['username'][username]) - # Add individual IPv6 client-pool default mask if required + # T2665: Add individual IPv6 client-pool default mask if required if dict_search('client_ipv6_pool.prefix', dict): - # T2665 default_values = defaults(base + ['client-ipv6-pool', 'prefix']) - for prefix in dict_search('client_ipv6_pool.prefix', dict): dict['client_ipv6_pool']['prefix'][prefix] = dict_merge( default_values, dict['client_ipv6_pool']['prefix'][prefix]) + # T2665: IPoE only - add individual local-user default values + if dict_search('authentication.interface', dict): + default_values = defaults(base + ['authentication', 'interface']) + for interface in dict_search('authentication.interface', dict): + dict['authentication']['interface'][interface] = dict_merge( + default_values, dict['authentication']['interface'][interface]) + + if dict_search('interface', dict): + default_values = defaults(base + ['interface']) + for interface in dict_search('interface', dict): + dict['interface'][interface] = dict_merge(default_values, + dict['interface'][interface]) + return dict diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 447ec795c..afa0c5b33 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -381,14 +381,14 @@ def verify_vlan_config(config): verify_mtu_parent(c_vlan, config) verify_mtu_parent(c_vlan, s_vlan) -def verify_accel_ppp_base_service(config): +def verify_accel_ppp_base_service(config, local_users=True): """ Common helper function which must be used by all Accel-PPP services based on get_config_dict() """ # vertify auth settings - if dict_search('authentication.mode', config) == 'local': - if not dict_search('authentication.local_users', config): + if local_users and dict_search('authentication.mode', config) == 'local': + if dict_search(f'authentication.local_users', config) == None: raise ConfigError('Authentication mode local requires local users to be configured!') for user in dict_search('authentication.local_users.username', config): diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 663c4394a..f9b7222fd 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -52,9 +52,9 @@ def get_ips_domains_dict(list_domains): return ip_dict -def nft_init_set(group_name, table="filter", family="ip"): +def nft_init_set(group_name, table="vyos_filter", family="ip"): """ - table ip filter { + table ip vyos_filter { set GROUP_NAME type ipv4_addr flags interval @@ -63,9 +63,9 @@ def nft_init_set(group_name, table="filter", family="ip"): return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}') -def nft_add_set_elements(group_name, elements, table="filter", family="ip"): +def nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip"): """ - table ip filter { + table ip vyos_filter { set GROUP_NAME { type ipv4_addr flags interval @@ -75,18 +75,18 @@ def nft_add_set_elements(group_name, elements, table="filter", family="ip"): elements = ", ".join(elements) return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ') -def nft_flush_set(group_name, table="filter", family="ip"): +def nft_flush_set(group_name, table="vyos_filter", family="ip"): """ Flush elements of nft set """ return call(f'nft flush set {family} {table} {group_name}') -def nft_update_set_elements(group_name, elements, table="filter", family="ip"): +def nft_update_set_elements(group_name, elements, table="vyos_filter", family="ip"): """ Update elements of nft set """ - flush_set = nft_flush_set(group_name, table="filter", family="ip") - nft_add_set = nft_add_set_elements(group_name, elements, table="filter", family="ip") + flush_set = nft_flush_set(group_name, table="vyos_filter", family="ip") + nft_add_set = nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip") return flush_set, nft_add_set # END firewall group domain-group (sets) @@ -150,7 +150,7 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if suffix[0] == '!': suffix = f'!= {suffix[1:]}' output.append(f'{ip_name} {prefix}addr {suffix}') - + if dict_search_args(side_conf, 'geoip', 'country_code'): operator = '' if dict_search_args(side_conf, 'geoip', 'inverse_match') != None: @@ -265,6 +265,23 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if 'type' in rule_conf[icmp]: output.append(icmp + ' type ' + rule_conf[icmp]['type']) + + if 'packet_length' in rule_conf: + lengths_str = ','.join(rule_conf['packet_length']) + output.append(f'ip{def_suffix} length {{{lengths_str}}}') + + if 'packet_length_exclude' in rule_conf: + negated_lengths_str = ','.join(rule_conf['packet_length_exclude']) + output.append(f'ip{def_suffix} length != {{{negated_lengths_str}}}') + + if 'dscp' in rule_conf: + dscp_str = ','.join(rule_conf['dscp']) + output.append(f'ip{def_suffix} dscp {{{dscp_str}}}') + + if 'dscp_exclude' in rule_conf: + negated_dscp_str = ','.join(rule_conf['dscp_exclude']) + output.append(f'ip{def_suffix} dscp != {{{negated_dscp_str}}}') + if 'ipsec' in rule_conf: if 'match_ipsec' in rule_conf['ipsec']: output.append('meta ipsec == 1') @@ -309,6 +326,10 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if 'action' in rule_conf: output.append(nft_action(rule_conf['action'])) + if 'jump' in rule_conf['action']: + target = rule_conf['jump_target'] + output.append(f'NAME{def_suffix}_{target}') + else: output.append('return') diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 98bf6162b..0edd17055 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2022 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 @@ -405,10 +405,12 @@ class BondIf(Interface): # Remove ALL bond member interfaces for interface in self.get_slaves(): self.del_port(interface) - # Removing an interface from a bond will always place the underlaying - # physical interface in admin-down state! If physical interface is - # not disabled, re-enable it. - if not dict_search(f'member.interface_remove.{interface}.disable', config): + + # Restore correct interface status based on config + if dict_search(f'member.interface.{interface}.disable', config) is not None or \ + dict_search(f'member.interface_remove.{interface}.disable', config) is not None: + Interface(interface).set_admin_state('down') + else: Interface(interface).set_admin_state('up') # Bonding policy/mode - default value, always present diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index b8deb3311..32e667038 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -16,6 +16,7 @@ import os import re +from glob import glob from vyos.ethtool import Ethtool from vyos.ifconfig.interface import Interface from vyos.util import run @@ -74,6 +75,10 @@ class EthernetIf(Interface): 'convert': lambda cpus: cpus if cpus else '0', 'location': '/sys/class/net/{ifname}/queues/rx-0/rps_cpus', }, + 'rfs': { + 'convert': lambda num: num if num else '0', + 'location': '/proc/sys/net/core/rps_sock_flow_entries', + }, }} def __init__(self, ifname, **kargs): @@ -258,6 +263,20 @@ class EthernetIf(Interface): # send bitmask representation as hex string without leading '0x' return self.set_interface('rps', rps_cpus) + def set_rfs(self, state): + rfs_flow = 0 + global_rfs_flow = 0 + queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*')) + if state: + global_rfs_flow = 32768 + rfs_flow = int(global_rfs_flow/queues) + + self.set_interface('rfs', global_rfs_flow) + for i in range(0, queues): + self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_flow_cnt', rfs_flow) + + return True + def set_sg(self, state): """ Enable Scatter-Gather support. State can be either True or False. @@ -342,6 +361,9 @@ class EthernetIf(Interface): # RPS - Receive Packet Steering self.set_rps(dict_search('offload.rps', config) != None) + # RFS - Receive Flow Steering + self.set_rfs(dict_search('offload.rfs', config) != None) + # scatter-gather option self.set_sg(dict_search('offload.sg', config) != None) diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 28b5e2991..9a92c71b8 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -167,12 +167,8 @@ class WireGuardIf(Interface): # remove no longer associated peers first if 'peer_remove' in config: - for tmp in config['peer_remove']: - peer = config['peer_remove'][tmp] - peer['ifname'] = config['ifname'] - - cmd = 'wg set {ifname} peer {public_key} remove' - self._cmd(cmd.format(**peer)) + for peer, public_key in config['peer_remove'].items(): + self._cmd(f'wg set {self.ifname} peer {public_key} remove') config['private_key_file'] = '/tmp/tmp.wireguard.key' with open(config['private_key_file'], 'w') as f: @@ -187,44 +183,49 @@ class WireGuardIf(Interface): base_cmd = base_cmd.format(**config) - for tmp in config['peer']: - peer = config['peer'][tmp] - - # start of with a fresh 'wg' command - cmd = base_cmd + ' peer {public_key}' - - # If no PSK is given remove it by using /dev/null - passing keys via - # the shell (usually bash) is considered insecure, thus we use a file - no_psk_file = '/dev/null' - psk_file = no_psk_file - if 'preshared_key' in peer: - psk_file = '/tmp/tmp.wireguard.psk' - with open(psk_file, 'w') as f: - f.write(peer['preshared_key']) - cmd += f' preshared-key {psk_file}' - - # Persistent keepalive is optional - if 'persistent_keepalive'in peer: - cmd += ' persistent-keepalive {persistent_keepalive}' - - # Multiple allowed-ip ranges can be defined - ensure we are always - # dealing with a list - if isinstance(peer['allowed_ips'], str): - peer['allowed_ips'] = [peer['allowed_ips']] - cmd += ' allowed-ips ' + ','.join(peer['allowed_ips']) - - # Endpoint configuration is optional - if {'address', 'port'} <= set(peer): - if is_ipv6(peer['address']): - cmd += ' endpoint [{address}]:{port}' - else: - cmd += ' endpoint {address}:{port}' + if 'peer' in config: + for peer, peer_config in config['peer'].items(): + # T4702: No need to configure this peer when it was explicitly + # marked as disabled - also active sessions are terminated as + # the public key was already removed when entering this method! + if 'disable' in peer_config: + continue + + # start of with a fresh 'wg' command + cmd = base_cmd + ' peer {public_key}' + + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + no_psk_file = '/dev/null' + psk_file = no_psk_file + if 'preshared_key' in peer_config: + psk_file = '/tmp/tmp.wireguard.psk' + with open(psk_file, 'w') as f: + f.write(peer_config['preshared_key']) + cmd += f' preshared-key {psk_file}' + + # Persistent keepalive is optional + if 'persistent_keepalive'in peer_config: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer_config['allowed_ips'], str): + peer_config['allowed_ips'] = [peer_config['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips']) + + # Endpoint configuration is optional + if {'address', 'port'} <= set(peer_config): + if is_ipv6(peer_config['address']): + cmd += ' endpoint [{address}]:{port}' + else: + cmd += ' endpoint {address}:{port}' - self._cmd(cmd.format(**peer)) + self._cmd(cmd.format(**peer_config)) - # PSK key file is not required to be stored persistently as its backed by CLI - if psk_file != no_psk_file and os.path.exists(psk_file): - os.remove(psk_file) + # PSK key file is not required to be stored persistently as its backed by CLI + if psk_file != no_psk_file and os.path.exists(psk_file): + os.remove(psk_file) # call base class super().update(config) diff --git a/python/vyos/nat.py b/python/vyos/nat.py new file mode 100644 index 000000000..31bbdc386 --- /dev/null +++ b/python/vyos/nat.py @@ -0,0 +1,188 @@ +#!/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 <http://www.gnu.org/licenses/>. + +from vyos.template import is_ip_network +from vyos.util import dict_search_args + +def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): + output = [] + ip_prefix = 'ip6' if ipv6 else 'ip' + log_prefix = ('DST' if nat_type == 'destination' else 'SRC') + f'-NAT-{rule_id}' + log_suffix = '' + + if ipv6: + log_prefix = log_prefix.replace("NAT-", "NAT66-") + + ignore_type_addr = False + translation_str = '' + + if 'inbound_interface' in rule_conf: + ifname = rule_conf['inbound_interface'] + if ifname != 'any': + output.append(f'iifname "{ifname}"') + + if 'outbound_interface' in rule_conf: + ifname = rule_conf['outbound_interface'] + if ifname != 'any': + output.append(f'oifname "{ifname}"') + + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': + protocol = rule_conf['protocol'] + if protocol == 'tcp_udp': + protocol = '{ tcp, udp }' + output.append(f'meta l4proto {protocol}') + + if 'exclude' in rule_conf: + translation_str = 'return' + log_suffix = '-EXCL' + elif 'translation' in rule_conf: + translation_prefix = nat_type[:1] + translation_output = [f'{translation_prefix}nat'] + addr = dict_search_args(rule_conf, 'translation', 'address') + port = dict_search_args(rule_conf, 'translation', 'port') + + if addr and is_ip_network(addr): + if not ipv6: + map_addr = dict_search_args(rule_conf, nat_type, 'address') + translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}') + ignore_type_addr = True + else: + translation_output.append(f'prefix to {addr}') + elif addr == 'masquerade': + if port: + addr = f'{addr} to ' + translation_output = [addr] + log_suffix = '-MASQ' + else: + translation_output.append('to') + if addr: + translation_output.append(addr) + + options = [] + addr_mapping = dict_search_args(rule_conf, 'translation', 'options', 'address_mapping') + port_mapping = dict_search_args(rule_conf, 'translation', 'options', 'port_mapping') + if addr_mapping == 'persistent': + options.append('persistent') + if port_mapping and port_mapping != 'none': + options.append(port_mapping) + + translation_str = " ".join(translation_output) + (f':{port}' if port else '') + + if options: + translation_str += f' {",".join(options)}' + + for target in ['source', 'destination']: + prefix = target[:1] + addr = dict_search_args(rule_conf, target, 'address') + if addr and not (ignore_type_addr and target == nat_type): + operator = '' + if addr[:1] == '!': + operator = '!=' + addr = addr[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} {addr}') + + addr_prefix = dict_search_args(rule_conf, target, 'prefix') + if addr_prefix and ipv6: + operator = '' + if addr_prefix[:1] == '!': + operator = '!=' + addr_prefix = addr[1:] + output.append(f'ip6 {prefix}addr {operator} {addr_prefix}') + + port = dict_search_args(rule_conf, target, 'port') + if port: + protocol = rule_conf['protocol'] + if protocol == 'tcp_udp': + protocol = 'th' + operator = '' + if port[:1] == '!': + operator = '!=' + port = port[1:] + output.append(f'{protocol} {prefix}port {operator} {{ {port} }}') + + output.append('counter') + + if 'log' in rule_conf: + output.append(f'log prefix "[{log_prefix}{log_suffix}]"') + + if translation_str: + output.append(translation_str) + + output.append(f'comment "{log_prefix}"') + + return " ".join(output) + +def parse_nat_static_rule(rule_conf, rule_id, nat_type): + output = [] + log_prefix = ('STATIC-DST' if nat_type == 'destination' else 'STATIC-SRC') + f'-NAT-{rule_id}' + log_suffix = '' + + ignore_type_addr = False + translation_str = '' + + if 'inbound_interface' in rule_conf: + ifname = rule_conf['inbound_interface'] + ifprefix = 'i' if nat_type == 'destination' else 'o' + if ifname != 'any': + output.append(f'{ifprefix}ifname "{ifname}"') + + if 'exclude' in rule_conf: + translation_str = 'return' + log_suffix = '-EXCL' + elif 'translation' in rule_conf: + translation_prefix = nat_type[:1] + translation_output = [f'{translation_prefix}nat'] + addr = dict_search_args(rule_conf, 'translation', 'address') + map_addr = dict_search_args(rule_conf, 'destination', 'address') + + if nat_type == 'source': + addr, map_addr = map_addr, addr # Swap + + if addr and is_ip_network(addr): + translation_output.append(f'ip prefix to ip {translation_prefix}addr map {{ {map_addr} : {addr} }}') + ignore_type_addr = True + elif addr: + translation_output.append(f'to {addr}') + + options = [] + addr_mapping = dict_search_args(rule_conf, 'translation', 'options', 'address_mapping') + port_mapping = dict_search_args(rule_conf, 'translation', 'options', 'port_mapping') + if addr_mapping == 'persistent': + options.append('persistent') + if port_mapping and port_mapping != 'none': + options.append(port_mapping) + + if options: + translation_output.append(",".join(options)) + + translation_str = " ".join(translation_output) + + prefix = nat_type[:1] + addr = dict_search_args(rule_conf, 'translation' if nat_type == 'source' else nat_type, 'address') + if addr and not ignore_type_addr: + output.append(f'ip {prefix}addr {addr}') + + output.append('counter') + + if translation_str: + output.append(translation_str) + + if 'log' in rule_conf: + output.append(f'log prefix "[{log_prefix}{log_suffix}]"') + + output.append(f'comment "{log_prefix}"') + + return " ".join(output) diff --git a/python/vyos/template.py b/python/vyos/template.py index 9804308c1..0870a0523 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -548,7 +548,7 @@ def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): return parse_rule(rule_conf, fw_name, rule_id, ip_name) @register_filter('nft_default_rule') -def nft_default_rule(fw_conf, fw_name): +def nft_default_rule(fw_conf, fw_name, ipv6=False): output = ['counter'] default_action = fw_conf['default_action'] @@ -557,6 +557,11 @@ def nft_default_rule(fw_conf, fw_name): output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}]"') output.append(nft_action(default_action)) + if 'default_jump_target' in fw_conf: + target = fw_conf['default_jump_target'] + def_suffix = '6' if ipv6 else '' + output.append(f'NAME{def_suffix}_{target}') + output.append(f'comment "{fw_name} default-action {default_action}"') return " ".join(output) @@ -611,6 +616,25 @@ def nft_nested_group(out_list, includes, groups, key): add_includes(name) return out_list +@register_filter('nat_rule') +def nat_rule(rule_conf, rule_id, nat_type, ipv6=False): + from vyos.nat import parse_nat_rule + return parse_nat_rule(rule_conf, rule_id, nat_type, ipv6) + +@register_filter('nat_static_rule') +def nat_static_rule(rule_conf, rule_id, nat_type): + from vyos.nat import parse_nat_static_rule + return parse_nat_static_rule(rule_conf, rule_id, nat_type) + +@register_filter('range_to_regex') +def range_to_regex(num_range): + from vyos.range_regex import range_to_regex + if '-' not in num_range: + return num_range + + regex = range_to_regex(num_range) + return f'({regex})' + @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ diff --git a/python/vyos/util.py b/python/vyos/util.py index 325b630bc..461df9a6e 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2022 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 @@ -471,6 +471,12 @@ def process_named_running(name): return p.pid return None +def is_list_equal(first: list, second: list) -> bool: + """ Check if 2 lists are equal and list not empty """ + if len(first) != len(second) or len(first) == 0: + return False + return sorted(first) == sorted(second) + def is_listen_port_bind_service(port: int, service: str) -> bool: """Check if listen port bound to expected program name :param port: Bind port |