diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/interfaces_wireless.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 46 | ||||
-rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 19 | ||||
-rwxr-xr-x | src/conf_mode/system_host-name.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 5 | ||||
-rw-r--r-- | src/conf_mode/vrf_vni.py | 103 | ||||
-rwxr-xr-x | src/helpers/vyos_config_sync.py | 23 | ||||
-rwxr-xr-x | src/migration-scripts/openconnect/2-to-3 | 50 | ||||
-rwxr-xr-x | src/migration-scripts/pppoe-server/9-to-10 | 56 | ||||
-rwxr-xr-x | src/op_mode/firewall.py | 78 | ||||
-rwxr-xr-x | src/services/vyos-configd | 2 |
12 files changed, 252 insertions, 152 deletions
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index 02b4a2500..c0a17c0bc 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -31,8 +31,9 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import WiFiIf from vyos.template import render -from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.utils.kernel import check_kmod +from vyos.utils.process import call from vyos import ConfigError from vyos import airbag airbag.enable() @@ -118,6 +119,10 @@ def verify(wifi): if 'physical_device' not in wifi: raise ConfigError('You must specify a physical-device "phy"') + physical_device = wifi['physical_device'] + if not os.path.exists(f'/sys/class/ieee80211/{physical_device}'): + raise ConfigError(f'Wirelss interface PHY "{physical_device}" does not exist!') + if 'type' not in wifi: raise ConfigError('You must specify a WiFi mode') @@ -266,6 +271,7 @@ def apply(wifi): if __name__ == '__main__': try: + check_kmod('mac80211') c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 4df97d133..44409c0e3 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_vrf from vyos.utils.network import is_addr_assigned from vyos.utils.process import process_named_running +from vyos.utils.process import call from vyos import ConfigError from vyos import frr from vyos import airbag @@ -50,13 +51,8 @@ def get_config(config=None): # eqivalent of the C foo ? 'a' : 'b' statement base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path - bgp = conf.get_config_dict( - base, - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True, - ) + bgp = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], key_mangling=('-', '_'), @@ -75,22 +71,29 @@ def get_config(config=None): if vrf: bgp.update({'vrf' : vrf}) # We can not delete the BGP VRF instance if there is a L3VNI configured + # FRR L3VNI must be deleted first otherwise we will see error: + # "FRR error: Please unconfigure l3vni 3000" tmp = ['vrf', 'name', vrf, 'vni'] - if conf.exists(tmp): - bgp.update({'vni' : conf.return_value(tmp)}) + if conf.exists_effective(tmp): + bgp.update({'vni' : conf.return_effective_value(tmp)}) # We can safely delete ourself from the dependent vrf list if vrf in bgp['dependent_vrfs']: del bgp['dependent_vrfs'][vrf] - bgp['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True)}}}) + bgp['dependent_vrfs'].update({'default': {'protocols': { + 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True)}}}) + if not conf.exists(base): # If bgp instance is deleted then mark it bgp.update({'deleted' : ''}) return bgp + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + bgp = conf.merge_defaults(bgp, recursive=True) + # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). # @@ -242,10 +245,6 @@ def verify(bgp): if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']): raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ 'unconfigure "import vrf" commands!') - # We can not delete the BGP instance if a L3VNI instance exists - if 'vni' in bgp: - raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ - f'unconfigure VNI "{bgp["vni"]}" first!') else: # We are running in the default VRF context, thus we can not delete # our main BGP instance if there are dependent BGP VRF instances. @@ -254,7 +253,11 @@ def verify(bgp): if vrf != 'default': if dict_search('protocols.bgp', vrf_options): raise ConfigError('Cannot delete default BGP instance, ' \ - 'dependent VRF instance(s) exist!') + 'dependent VRF instance(s) exist(s)!') + if 'vni' in vrf_options: + raise ConfigError('Cannot delete default BGP instance, ' \ + 'dependent L3VNI exists!') + return None if 'system_as' not in bgp: @@ -607,6 +610,13 @@ def generate(bgp): return None def apply(bgp): + if 'deleted' in bgp: + # We need to ensure that the L3VNI is deleted first. + # This is not possible with old config backend + # priority bug + if {'vrf', 'vni'} <= set(bgp): + call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp)) + bgp_daemon = 'bgpd' # Save original configuration prior to starting any commit actions diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 11e950782..28b7fb03c 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -66,7 +66,7 @@ def verify(ipoe): raise ConfigError('No IPoE interface configured') for interface, iface_config in ipoe['interface'].items(): - verify_interface_exists(interface) + verify_interface_exists(interface, warning_only=True) if 'client_subnet' in iface_config and 'vlan' in iface_config: raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, ' 'use "client-ip-pool" instead!') diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index b9d174933..c95f976d3 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -84,12 +84,29 @@ def verify_pado_delay(pppoe): pado_delay = pppoe['pado_delay'] delays_without_sessions = pado_delay['delays_without_sessions'] + if 'disable' in delays_without_sessions: + raise ConfigError( + 'Number of sessions must be specified for "pado-delay disable"' + ) + if len(delays_without_sessions) > 1: raise ConfigError( f'Cannot add more then ONE pado-delay without sessions, ' f'but {len(delays_without_sessions)} were set' ) + if 'disable' in [delay[0] for delay in pado_delay['delays_with_sessions']]: + # need to sort delays by sessions to verify if there is no delay + # for sessions after disabling + sorted_pado_delay = sorted(pado_delay['delays_with_sessions'], key=lambda k_v: k_v[1]) + last_delay = sorted_pado_delay[-1] + + if last_delay[0] != 'disable': + raise ConfigError( + f'Cannot add pado-delay after disabled sessions, but ' + f'"pado-delay {last_delay[0]} sessions {last_delay[1]}" was set' + ) + def verify(pppoe): if not pppoe: return None @@ -105,7 +122,7 @@ def verify(pppoe): # Check is interface exists in the system for interface in pppoe['interface']: - verify_interface_exists(interface) + verify_interface_exists(interface, warning_only=True) return None diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py index 8975cadb6..3f245f166 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -22,6 +22,7 @@ import vyos.hostsd_client from vyos.base import Warning from vyos.config import Config +from vyos.configdict import leaf_node_changed from vyos.ifconfig import Section from vyos.template import is_ip from vyos.utils.process import cmd @@ -37,6 +38,7 @@ default_config_data = { 'domain_search': [], 'nameserver': [], 'nameservers_dhcp_interfaces': {}, + 'snmpd_restart_reqired': False, 'static_host_mapping': {} } @@ -52,6 +54,10 @@ def get_config(config=None): hosts['hostname'] = conf.return_value(['system', 'host-name']) + base = ['system'] + if leaf_node_changed(conf, base + ['host-name']) or leaf_node_changed(conf, base + ['domain-name']): + hosts['snmpd_restart_reqired'] = True + # This may happen if the config is not loaded yet, # e.g. if run by cloud-init if not hosts['hostname']: @@ -171,7 +177,7 @@ def apply(config): call("systemctl restart rsyslog.service") # If SNMP is running, restart it too - if process_named_running('snmpd'): + if process_named_running('snmpd') and config['snmpd_restart_reqired']: call('systemctl restart snmpd.service') return None diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 587309005..8d8c234c0 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -130,11 +130,6 @@ def get_config(config=None): tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], get_first_key=True)}} - # L3VNI setup is done via vrf_vni.py as it must be de-configured (on node - # deletetion prior to the BGP process. Tell the Jinja2 template no VNI - # setup is needed - vrf.update({'no_vni' : ''}) - # Merge policy dict into "regular" config dict vrf = dict_merge(tmp, vrf) return vrf diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py deleted file mode 100644 index 8dab164d7..000000000 --- a/src/conf_mode/vrf_vni.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-2024 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sys import argv -from sys import exit - -from vyos.config import Config -from vyos.template import render_to_string -from vyos import ConfigError -from vyos import frr -from vyos import airbag -airbag.enable() - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - vrf_name = None - if len(argv) > 1: - vrf_name = argv[1] - else: - return None - - # Using duplicate L3VNIs makes no sense - it's also forbidden in FRR, - # thus VyOS CLI must deny this, too. Instead of getting only the dict for - # the requested VRF and den comparing it with depenent VRfs to not have any - # duplicate we will just grad ALL VRFs by default but only render/apply - # the configuration for the requested VRF - that makes the code easier and - # hopefully less error prone - vrf = conf.get_config_dict(['vrf'], key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True) - - # Store name of VRF we are interested in for FRR config rendering - vrf.update({'only_vrf' : vrf_name}) - - return vrf - -def verify(vrf): - if not vrf: - return - - if len(argv) < 2: - raise ConfigError('VRF parameter not specified when valling vrf_vni.py') - - if 'name' in vrf: - vni_ids = [] - for name, vrf_config in vrf['name'].items(): - # VRF VNI (Virtual Network Identifier) must be unique on the system - if 'vni' in vrf_config: - if vrf_config['vni'] in vni_ids: - raise ConfigError(f'VRF "{name}" VNI is not unique!') - vni_ids.append(vrf_config['vni']) - - return None - -def generate(vrf): - if not vrf: - return - - vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) - return None - -def apply(vrf): - frr_daemon = 'zebra' - - # add configuration to FRR - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - # There is only one VRF inside the dict as we read only one in get_config() - if vrf and 'only_vrf' in vrf: - vrf_name = vrf['only_vrf'] - frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True) - if vrf and 'new_frr_config' in vrf: - frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 0604b2837..9d9aec376 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -93,7 +93,8 @@ def set_remote_config( key: str, op: str, mask: Dict[str, Any], - config: Dict[str, Any]) -> Optional[Dict[str, Any]]: + config: Dict[str, Any], + port: int) -> Optional[Dict[str, Any]]: """Loads the VyOS configuration in JSON format to a remote host. Args: @@ -102,6 +103,7 @@ def set_remote_config( op (str): The operation to perform (set or load). mask (dict): The dict of paths in sections. config (dict): The dict of masked config data. + port (int): The remote API port Returns: Optional[Dict[str, Any]]: The response from the remote host as a @@ -113,7 +115,7 @@ def set_remote_config( # Disable the InsecureRequestWarning urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - url = f'https://{address}/configure-section' + url = f'https://{address}:{port}/configure-section' data = json.dumps({ 'op': op, 'mask': mask, @@ -138,7 +140,8 @@ def is_section_revised(section: List[str]) -> bool: def config_sync(secondary_address: str, secondary_key: str, sections: List[list[str]], - mode: str): + mode: str, + secondary_port: int): """Retrieve a config section from primary router in JSON format and send it to secondary router """ @@ -158,7 +161,8 @@ def config_sync(secondary_address: str, key=secondary_key, op=mode, mask=mask_dict, - config=config_dict) + config=config_dict, + port=secondary_port) logger.debug(f"Set config for sections '{sections}': {set_config}") @@ -178,14 +182,12 @@ if __name__ == '__main__': secondary_address = config.get('secondary', {}).get('address') secondary_address = bracketize_ipv6(secondary_address) secondary_key = config.get('secondary', {}).get('key') + secondary_port = int(config.get('secondary', {}).get('port', 443)) sections = config.get('section') timeout = int(config.get('secondary', {}).get('timeout')) - if not all([ - mode, secondary_address, secondary_key, sections - ]): - logger.error( - "Missing required configuration data for config synchronization.") + if not all([mode, secondary_address, secondary_key, sections]): + logger.error("Missing required configuration data for config synchronization.") exit(0) # Generate list_sections of sections/subsections @@ -200,5 +202,4 @@ if __name__ == '__main__': else: list_sections.append([section]) - config_sync(secondary_address, secondary_key, - list_sections, mode) + config_sync(secondary_address, secondary_key, list_sections, mode, secondary_port) diff --git a/src/migration-scripts/openconnect/2-to-3 b/src/migration-scripts/openconnect/2-to-3 new file mode 100755 index 000000000..e78fc8a91 --- /dev/null +++ b/src/migration-scripts/openconnect/2-to-3 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T4982: Retain prior default TLS version (v1.0) when upgrading installations with existing openconnect configurations + +import sys + +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + + +config = ConfigTree(config_file) +cfg_base = ['vpn', 'openconnect'] + +# bail out early if service is unconfigured +if not config.exists(cfg_base): + sys.exit(0) + +# new default is TLS 1.2 - set explicit old default value of TLS 1.0 for upgraded configurations to keep compatibility +tls_min_path = cfg_base + ['tls-version-min'] +if not config.exists(tls_min_path): + config.set(tls_min_path, value='1.0') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pppoe-server/9-to-10 b/src/migration-scripts/pppoe-server/9-to-10 new file mode 100755 index 000000000..e0c782f04 --- /dev/null +++ b/src/migration-scripts/pppoe-server/9-to-10 @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Migration of pado-delay options + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server', 'pado-delay'] +if not config.exists(base): + exit(0) + +pado_delay = {} +for delay in config.list_nodes(base): + sessions = config.return_value(base + [delay, 'sessions']) + pado_delay[delay] = sessions + +# need to define delay for latest sessions +sorted_delays = dict(sorted(pado_delay.items(), key=lambda k_v: int(k_v[1]))) +last_delay = list(sorted_delays)[-1] + +# Rename last delay -> disable +tmp = base + [last_delay] +if config.exists(tmp): + config.rename(tmp, 'disable') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 25554b781..442c186cc 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -16,6 +16,7 @@ import argparse import ipaddress +import json import re import tabulate import textwrap @@ -89,10 +90,38 @@ def get_nftables_details(family, hook, priority): out[rule_id] = rule return out -def output_firewall_vertical(rules, headers): +def get_nftables_group_members(family, table, name): + prefix = 'ip6' if family == 'ipv6' else 'ip' + out = [] + + try: + results_str = cmd(f'sudo nft -j list set {prefix} {table} {name}') + results = json.loads(results_str) + except: + return out + + if 'nftables' not in results: + return out + + for obj in results['nftables']: + if 'set' not in obj: + continue + + set_obj = obj['set'] + + if 'elem' in set_obj: + for elem in set_obj['elem']: + if isinstance(elem, str): + out.append(elem) + elif isinstance(elem, dict) and 'elem' in elem: + out.append(elem['elem']) + + return out + +def output_firewall_vertical(rules, headers, adjust=True): for rule in rules: - adjusted_rule = rule + [""] * (len(headers) - len(rule)) # account for different header length, like default-action - transformed_rule = [[header, textwrap.fill(adjusted_rule[i].replace('\n', ' '), 65)] for i, header in enumerate(headers)] # create key-pair list from headers and rules lists; wrap at 100 char + adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action + transformed_rule = [[header, textwrap.fill(adjusted_rule[i].replace('\n', ' '), 65)] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char print(tabulate.tabulate(transformed_rule, tablefmt="presto")) print() @@ -453,6 +482,7 @@ def show_firewall_group(name=None): return out rows = [] + header_tail = [] for group_type, group_type_conf in firewall['group'].items(): ## @@ -479,21 +509,53 @@ def show_firewall_group(name=None): rows.append(row) else: + if not args.detail: + header_tail = ['Timeout', 'Expires'] + for dynamic_type in ['address_group', 'ipv6_address_group']: + family = 'ipv4' if dynamic_type == 'address_group' else 'ipv6' + prefix = 'DA_' if dynamic_type == 'address_group' else 'DA6_' if dynamic_type in firewall['group']['dynamic_group']: for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items(): references = find_references(dynamic_type, dynamic_name) row = [dynamic_name, textwrap.fill(dynamic_conf.get('description') or '', 50), dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D'] - row.append('N/D') - rows.append(row) + + members = get_nftables_group_members(family, 'vyos_filter', f'{prefix}{dynamic_name}') + + if not members: + if args.detail: + row.append('N/D') + else: + row += ["N/D"] * 3 + rows.append(row) + continue + + for idx, member in enumerate(members): + val = member.get('val', 'N/D') + timeout = str(member.get('timeout', 'N/D')) + expires = str(member.get('expires', 'N/D')) + + if args.detail: + row.append(f'{val} (timeout: {timeout}, expires: {expires})') + continue + + if idx > 0: + row = [""] * 4 + + row += [val, timeout, expires] + rows.append(row) + + if args.detail: + header_tail += [""] * (len(members) - 1) + rows.append(row) if rows: print('Firewall Groups\n') if args.detail: - header = ['Name', 'Description','Type', 'References', 'Members'] - output_firewall_vertical(rows, header) + header = ['Name', 'Description', 'Type', 'References', 'Members'] + header_tail + output_firewall_vertical(rows, header, adjust=False) else: - header = ['Name', 'Type', 'References', 'Members'] + header = ['Name', 'Type', 'References', 'Members'] + header_tail for i in rows: rows[rows.index(i)].pop(1) print(tabulate.tabulate(rows, header)) diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 648a017d5..c89c486e5 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -236,7 +236,7 @@ def process_node_data(config, data, last: bool = False) -> int: with stdout_redirected(session_out, session_mode): result = run_script(conf_mode_scripts[script_name], config, args) - if last: + if last and result == R_SUCCESS: call_dependents(dependent_func=config.dependent_func) return result |