diff options
Diffstat (limited to 'src/conf_mode')
37 files changed, 680 insertions, 1418 deletions
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 78daeb6be..d93a2a8f4 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -79,7 +79,7 @@ def generate(relay): config['instance'] = instance render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', - config, trim_blocks=True) + config) return None diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index 352865b9d..6352e0b4a 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -19,81 +19,43 @@ import os from sys import exit from vyos.config import Config +from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call +from vyos.util import dict_search +from vyos.xml import defaults from vyos import ConfigError - from vyos import airbag airbag.enable() -config_file = r'/run/dhcp-relay/dhcp.conf' - -default_config_data = { - 'interface': [], - 'server': [], - 'options': [], - 'hop_count': '10', - 'relay_agent_packets': 'forward' -} +config_file = r'/run/dhcp-relay/dhcrelay.conf' def get_config(config=None): - relay = default_config_data if config: conf = config else: conf = Config() - if not conf.exists(['service', 'dhcp-relay']): + base = ['service', 'dhcp-relay'] + if not conf.exists(base): return None - else: - conf.set_level(['service', 'dhcp-relay']) - - # Network interfaces to listen on - if conf.exists(['interface']): - relay['interface'] = conf.return_values(['interface']) - - # Servers equal to the address of the DHCP server(s) - if conf.exists(['server']): - relay['server'] = conf.return_values(['server']) - - conf.set_level(['service', 'dhcp-relay', 'relay-options']) - - if conf.exists(['hop-count']): - count = '-c ' + conf.return_value(['hop-count']) - relay['options'].append(count) - - # Specify the maximum packet size to send to a DHCPv4/BOOTP server. - # This might be done to allow sufficient space for addition of relay agent - # options while still fitting into the Ethernet MTU size. - # - # Available in DHCPv4 mode only: - if conf.exists(['max-size']): - size = '-A ' + conf.return_value(['max-size']) - relay['options'].append(size) - - # Control the handling of incoming DHCPv4 packets which already contain - # relay agent options. If such a packet does not have giaddr set in its - # header, the DHCP standard requires that the packet be discarded. However, - # if giaddr is set, the relay agent may handle the situation in four ways: - # It may append its own set of relay options to the packet, leaving the - # supplied option field intact; it may replace the existing agent option - # field; it may forward the packet unchanged; or, it may discard it. - # - # Available in DHCPv4 mode only: - if conf.exists(['relay-agents-packets']): - pkt = '-a -m ' + conf.return_value(['relay-agents-packets']) - relay['options'].append(pkt) + + relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + relay = dict_merge(default_values, relay) return relay def verify(relay): # bail out early - looks like removal from running config - if relay is None: + if not relay: return None - if 'lo' in relay['interface']: + if 'lo' in (dict_search('interface', relay) or []): raise ConfigError('DHCP relay does not support the loopback interface.') - if len(relay['server']) == 0: + if 'server' not in relay : raise ConfigError('No DHCP relay server(s) configured.\n' \ 'At least one DHCP relay server required.') @@ -104,17 +66,18 @@ def generate(relay): if not relay: return None - render(config_file, 'dhcp-relay/config.tmpl', relay) + render(config_file, 'dhcp-relay/dhcrelay.conf.tmpl', relay) return None def apply(relay): - if relay: - call('systemctl restart isc-dhcp-relay.service') - else: - # DHCP relay support is removed in the commit + # bail out early - looks like removal from running config + if not relay: call('systemctl stop isc-dhcp-relay.service') if os.path.exists(config_file): os.unlink(config_file) + return None + + call('systemctl restart isc-dhcp-relay.service') return None diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index d4212b8be..cf8a26674 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -17,90 +17,84 @@ import os from sys import exit -from copy import deepcopy from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +from vyos.configdict import dict_merge +from vyos.ifconfig import Interface from vyos.template import render - +from vyos.util import call +from vyos.util import dict_search +from vyos.validate import is_ipv6_link_local +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/run/dhcp-relay/dhcpv6.conf' - -default_config_data = { - 'listen_addr': [], - 'upstream_addr': [], - 'options': [], -} +config_file = '/run/dhcp-relay/dhcrelay6.conf' def get_config(config=None): - relay = deepcopy(default_config_data) if config: conf = config else: conf = Config() - if not conf.exists('service dhcpv6-relay'): + base = ['service', 'dhcpv6-relay'] + if not conf.exists(base): return None - else: - conf.set_level('service dhcpv6-relay') - - # Network interfaces/address to listen on for DHCPv6 query(s) - if conf.exists('listen-interface'): - interfaces = conf.list_nodes('listen-interface') - for intf in interfaces: - if conf.exists('listen-interface {0} address'.format(intf)): - addr = conf.return_value('listen-interface {0} address'.format(intf)) - listen = addr + '%' + intf - relay['listen_addr'].append(listen) - - # Upstream interface/address for remote DHCPv6 server - if conf.exists('upstream-interface'): - interfaces = conf.list_nodes('upstream-interface') - for intf in interfaces: - addresses = conf.return_values('upstream-interface {0} address'.format(intf)) - for addr in addresses: - server = addr + '%' + intf - relay['upstream_addr'].append(server) - - # Maximum hop count. When forwarding packets, dhcrelay discards packets - # which have reached a hop count of COUNT. Default is 10. Maximum is 255. - if conf.exists('max-hop-count'): - count = '-c ' + conf.return_value('max-hop-count') - relay['options'].append(count) - - if conf.exists('use-interface-id-option'): - relay['options'].append('-I') + + relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + relay = dict_merge(default_values, relay) return relay def verify(relay): # bail out early - looks like removal from running config - if relay is None: + if not relay: return None - if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0: - raise ConfigError('Must set at least one listen and upstream interface addresses.') + if 'upstream_interface' not in relay: + raise ConfigError('At least one upstream interface required!') + for interface, config in relay['upstream_interface'].items(): + if 'address' not in config: + raise ConfigError('DHCPv6 server required for upstream ' \ + f'interface {interface}!') + + if 'listen_interface' not in relay: + raise ConfigError('At least one listen interface required!') + + # DHCPv6 relay requires at least one global unicat address assigned to the + # interface + for interface in relay['listen_interface']: + has_global = False + for addr in Interface(interface).get_addr(): + if not is_ipv6_link_local(addr.split('/')[0]): + has_global = True + if not has_global: + raise ConfigError(f'Interface {interface} does not have global '\ + 'IPv6 address assigned!') return None def generate(relay): # bail out early - looks like removal from running config - if relay is None: + if not relay: return None - render(config_file, 'dhcpv6-relay/config.tmpl', relay) + render(config_file, 'dhcp-relay/dhcrelay6.conf.tmpl', relay) return None def apply(relay): - if relay is not None: - call('systemctl restart isc-dhcp-relay6.service') - else: + # bail out early - looks like removal from running config + if not relay: # DHCPv6 relay support is removed in the commit call('systemctl stop isc-dhcp-relay6.service') if os.path.exists(config_file): os.unlink(config_file) + return None + + call('systemctl restart isc-dhcp-relay6.service') return None diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index ef52cbfd3..c44e6c974 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -114,10 +114,10 @@ def generate(dns): return None render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', - dns, trim_blocks=True, user=pdns_rec_user, group=pdns_rec_group) + dns, user=pdns_rec_user, group=pdns_rec_group) render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', - dns, trim_blocks=True, user=pdns_rec_user, group=pdns_rec_group) + dns, user=pdns_rec_user, group=pdns_rec_group) # if vyos-hostsd didn't create its files yet, create them (empty) for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 93e995b78..6d39c6644 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -131,7 +131,9 @@ def generate(dyndns): if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, trim_blocks=True, permission=0o600) + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, + permission=0o600) + return None def apply(dyndns): diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index de228f0f8..a6e2d9c8c 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -159,7 +159,7 @@ def generate(https): if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True) + render(config_file, 'https/nginx.default.tmpl', https) return None diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index 754f46566..fb030c9f3 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -17,90 +17,65 @@ import os from sys import exit -from copy import deepcopy - from netifaces import interfaces + from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +from vyos.configdict import dict_merge from vyos.template import render - +from vyos.util import call +from vyos.util import dict_search +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/igmpproxy.conf' -default_config_data = { - 'disable': False, - 'disable_quickleave': False, - 'interfaces': [], -} - def get_config(config=None): - igmp_proxy = deepcopy(default_config_data) if config: conf = config else: conf = Config() - base = ['protocols', 'igmp-proxy'] - if not conf.exists(base): - return None - else: - conf.set_level(base) - - # Network interfaces to listen on - if conf.exists(['disable']): - igmp_proxy['disable'] = True - - # Option to disable "quickleave" - if conf.exists(['disable-quickleave']): - igmp_proxy['disable_quickleave'] = True - for intf in conf.list_nodes(['interface']): - conf.set_level(base + ['interface', intf]) - interface = { - 'name': intf, - 'alt_subnet': [], - 'role': 'downstream', - 'threshold': '1', - 'whitelist': [] - } - - if conf.exists(['alt-subnet']): - interface['alt_subnet'] = conf.return_values(['alt-subnet']) + base = ['protocols', 'igmp-proxy'] + igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - if conf.exists(['role']): - interface['role'] = conf.return_value(['role']) + if 'interface' in igmp_proxy: + # T2665: we must add the tagNode defaults individually until this is + # moved to the base class + default_values = defaults(base + ['interface']) + for interface in igmp_proxy['interface']: + igmp_proxy['interface'][interface] = dict_merge(default_values, + igmp_proxy['interface'][interface]) - if conf.exists(['threshold']): - interface['threshold'] = conf.return_value(['threshold']) - if conf.exists(['whitelist']): - interface['whitelist'] = conf.return_values(['whitelist']) + if conf.exists(['protocols', 'igmp']): + igmp_proxy.update({'igmp_configured': ''}) - # Append interface configuration to global configuration list - igmp_proxy['interfaces'].append(interface) + if conf.exists(['protocols', 'pim']): + igmp_proxy.update({'pim_configured': ''}) return igmp_proxy def verify(igmp_proxy): # bail out early - looks like removal from running config - if igmp_proxy is None: + if not igmp_proxy or 'disable' in igmp_proxy: return None - # bail out early - service is disabled - if igmp_proxy['disable']: - return None + if 'igmp_configured' in igmp_proxy or 'pim_configured' in igmp_proxy: + raise ConfigError('Can not configure both IGMP proxy and PIM '\ + 'at the same time') # at least two interfaces are required, one upstream and one downstream - if len(igmp_proxy['interfaces']) < 2: - raise ConfigError('Must define an upstream and at least 1 downstream interface!') + if 'interface' not in igmp_proxy or len(igmp_proxy['interface']) < 2: + raise ConfigError('Must define exactly one upstream and at least one ' \ + 'downstream interface!') upstream = 0 - for interface in igmp_proxy['interfaces']: - if interface['name'] not in interfaces(): - raise ConfigError('Interface "{}" does not exist'.format(interface['name'])) - if "upstream" == interface['role']: + for interface, config in igmp_proxy['interface'].items(): + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist') + if dict_search('role', config) == 'upstream': upstream += 1 if upstream == 0: @@ -112,19 +87,20 @@ def verify(igmp_proxy): def generate(igmp_proxy): # bail out early - looks like removal from running config - if igmp_proxy is None: + if not igmp_proxy: return None # bail out early - service is disabled, but inform user - if igmp_proxy['disable']: - print('Warning: IGMP Proxy will be deactivated because it is disabled') + if 'disable' in igmp_proxy: + print('WARNING: IGMP Proxy will be deactivated because it is disabled') return None render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) + return None def apply(igmp_proxy): - if igmp_proxy is None or igmp_proxy['disable']: + if not igmp_proxy or 'disable' in igmp_proxy: # IGMP Proxy support is removed in the commit call('systemctl stop igmpproxy.service') if os.path.exists(config_file): diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 076bdb63e..7af3e3d7c 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -123,12 +123,12 @@ def get_config(config=None): # VLAN-aware bridge members must not have VLAN interface configuration if 'native_vlan' in interface_config: - if 'disable' not in interface_config['native_vlan']: - vlan_aware = True + vlan_aware = True if 'allowed_vlan' in interface_config: vlan_aware = True + if vlan_aware: tmp = has_vlan_subinterface_configured(conf,interface) if tmp: @@ -142,6 +142,8 @@ def verify(bridge): verify_dhcpv6(bridge) verify_vrf(bridge) + + vlan_aware = False if dict_search('member.interface', bridge): for interface, interface_config in bridge['member']['interface'].items(): @@ -168,6 +170,16 @@ def verify(bridge): if 'has_vlan' in interface_config: raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + # VLAN-aware bridge members must not have VLAN interface configuration + if 'native_vlan' in interface_config: + vlan_aware = True + + if 'allowed_vlan' in interface_config: + vlan_aware = True + + if vlan_aware and 'wlan' in interface: + raise ConfigError(error_msg + 'VLAN aware cannot be set!') + if 'allowed_vlan' in interface_config: for vlan in interface_config['allowed_vlan']: if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index c23e79948..25920f893 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -120,7 +120,7 @@ def verify(openvpn): # OpenVPN site-to-site - VERIFY # elif openvpn['mode'] == 'site-to-site': - if not 'local_address' in openvpn: + if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: @@ -166,15 +166,16 @@ def verify(openvpn): if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn): raise ConfigError('"remote-address" and "remote-host" can not be the same') - - if 'local_address' in openvpn: + if openvpn['device_type'] == 'tap': # we can only have one local_address, this is ensured above v4addr = None for laddr in openvpn['local_address']: - if is_ipv4(laddr): v4addr = laddr + if is_ipv4(laddr): + v4addr = laddr + break - if 'remote_address' not in openvpn and (v4addr not in openvpn['local_address'] or 'subnet_mask' not in openvpn['local_address'][v4addr]): - raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"') + if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]: + raise ConfigError('Must specify IPv4 "subnet-mask" for local-address') if dict_search('encryption.ncp_ciphers', openvpn): raise ConfigError('NCP ciphers can only be used in client or server mode') @@ -464,12 +465,9 @@ def generate(openvpn): if tmp: fix_permissions.append(tmp) # Generate User/Password authentication file - if 'auth' in openvpn: - with open(openvpn['auth_user_pass_file'], 'w') as f: - f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) - # also change permission on auth file - fix_permissions.append(openvpn['auth_user_pass_file']) - + if 'authentication' in openvpn: + render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.tmpl', openvpn, + user=user, group=group, permission=0o600) else: # delete old auth file if present if os.path.isfile(openvpn['auth_user_pass_file']): @@ -483,17 +481,13 @@ def generate(openvpn): # Our client need's to know its subnet mask ... client_config['server_subnet'] = dict_search('server.subnet', openvpn) - import pprint - pprint.pprint(client_config) - render(client_file, 'openvpn/client.conf.tmpl', client_config, - trim_blocks=True, user=user, group=group) + user=user, group=group) # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, - trim_blocks=True, formater=lambda _: _.replace(""", '"'), - user=user, group=group) + formater=lambda _: _.replace(""", '"'), user=user, group=group) # Fixup file permissions for file in fix_permissions: diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index ee3b142c8..c31e49574 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -93,25 +93,25 @@ def generate(pppoe): return None # Create PPP configuration files - render(config_pppoe, 'pppoe/peer.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o755) + # Create script for ip-pre-up.d - render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', pppoe, + permission=0o755) # Create script for ip-up.d - render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', pppoe, + permission=0o755) # Create script for ip-down.d - render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', pppoe, + permission=0o755) # Create script for ipv6-up.d - render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', pppoe, + permission=0o755) if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']: # ipv6.tmpl relies on ifname - this should be made consitent in the # future better then double key-ing the same value - render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True) + render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe) return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index f1217b62d..1a7e9a96d 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,354 +15,124 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import netifaces from sys import exit -from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.configdict import is_member -from vyos.configdict import list_diff -from vyos.dicts import FixedDict -from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf -from vyos.ifconfig.afi import IP4, IP6 +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_mtu_ipv6 +from vyos.ifconfig import Interface +from vyos.ifconfig import GREIf +from vyos.ifconfig import GRETapIf +from vyos.ifconfig import IPIPIf +from vyos.ifconfig import IP6GREIf +from vyos.ifconfig import IPIP6If +from vyos.ifconfig import IP6IP6If +from vyos.ifconfig import SitIf +from vyos.ifconfig import Sit6RDIf from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.util import dict_search from vyos import ConfigError - - from vyos import airbag airbag.enable() - -class ConfigurationState(object): +def get_config(config=None): """ - The current API require a dict to be generated by get_config() - which is then consumed by verify(), generate() and apply() - - ConfiguartionState is an helper class wrapping Config and providing - an common API to this dictionary structure - - Its to_api() function return a dictionary containing three fields, - each a dict, called options, changes, actions. - - options: - - contains the configuration options for the dict and its value - {'options': {'commment': 'test'}} will be set if - 'set interface dummy dum1 description test' was used and - the key 'commment' is used to index the description info. - - changes: - - per key, let us know how the data was modified using one of the action - a special key called 'section' is used to indicate what happened to the - section. for example: - - 'set interface dummy dum1 description test' when no interface was setup - will result in the following changes - {'changes': {'section': 'create', 'comment': 'create'}} - - on an existing interface, depending if there was a description - 'set interface dummy dum1 description test' will result in one of - {'changes': {'comment': 'create'}} (not present before) - {'changes': {'comment': 'static'}} (unchanged) - {'changes': {'comment': 'modify'}} (changed from half) - - and 'delete interface dummy dummy1 description' will result in: - {'changes': {'comment': 'delete'}} - - actions: - - for each action list the configuration key which were changes - in our example if we added the 'description' and added an IP we would have - {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} - - the actions are: - 'create': it did not exist previously and was created - 'modify': it did exist previously but its content changed - 'static': it did exist and did not change - 'delete': it was present but was removed from the configuration - 'absent': it was not and is not present - which for each field represent how it was modified since the last commit + Retrive CLI config as dictionary. Dictionary can never be empty, as at least + the interface name will be added or a deleted flag """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'tunnel'] + tunnel = get_interface_dict(conf, base) - def __init__(self, configuration, section, default): - """ - initialise the class for a given configuration path: - - >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1') - all further references to get_value(s) and get_effective(s) - will be for this part of the configuration (eth1) - """ - self._conf = configuration - - self.default = deepcopy(default) - self.options = FixedDict(**default) - self.actions = { - 'create': [], # the key did not exist and was added - 'static': [], # the key exists and its value was not modfied - 'modify': [], # the key exists and its value was modified - 'absent': [], # the key is not present - 'delete': [], # the key was present and was deleted - } - self.changes = {} - if not self._conf.exists(section): - self.changes['section'] = 'delete' - elif self._conf.exists_effective(section): - self.changes['section'] = 'modify' - else: - self.changes['section'] = 'create' - - self.set_level(section) - - def set_level(self, lpath): - self.section = lpath - self._conf.set_level(lpath) - - def _act(self, section): - """ - Returns for a given configuration field determine what happened to it - - 'create': it did not exist previously and was created - 'modify': it did exist previously but its content changed - 'static': it did exist and did not change - 'delete': it was present but was removed from the configuration - 'absent': it was not and is not present - """ - if self._conf.exists(section): - if self._conf.exists_effective(section): - if self._conf.return_value(section) != self._conf.return_effective_value(section): - return 'modify' - return 'static' - return 'create' - else: - if self._conf.exists_effective(section): - return 'delete' - return 'absent' - - def _action(self, name, key): - action = self._act(key) - self.changes[name] = action - self.actions[action].append(name) - return action - - def _get(self, name, key, default, getter): - value = getter(key) - if not value: - if default: - self.options[name] = default - return - self.options[name] = self.default[name] - return - self.options[name] = value - - def get_value(self, name, key, default=None): - """ - >>> conf.get_value('comment', 'description') - will place the string of 'interface dummy description test' - into the dictionnary entry 'comment' using Config.return_value - (the data in the configuration to apply) - """ - if self._action(name, key) in ('delete', 'absent'): - return - return self._get(name, key, default, self._conf.return_value) - - def get_values(self, name, key, default=None): - """ - >>> conf.get_values('addresses', 'address') - will place a list of the new IP present in 'interface dummy dum1 address' - into the dictionnary entry "-add" (here 'addresses-add') using - Config.return_values and will add the the one which were removed in into - the entry "-del" (here addresses-del') - """ - add_name = f'{name}-add' - - if self._action(add_name, key) in ('delete', 'absent'): - return - - self._get(add_name, key, default, self._conf.return_values) - - # get the effective values to determine which data is no longer valid - self.options['addresses-del'] = list_diff( - self._conf.return_effective_values('address'), - self.options['addresses-add'] - ) - - def get_effective(self, name, key, default=None): - """ - >>> conf.get_value('comment', 'description') - will place the string of 'interface dummy description test' - into the dictionnary entry 'comment' using Config.return_effective_value - (the data in the configuration to apply) - """ - self._action(name, key) - return self._get(name, key, default, self._conf.return_effective_value) - - def get_effectives(self, name, key, default=None): - """ - >>> conf.get_effectives('addresses-add', 'address') - will place a list made of the IP present in 'interface ethernet eth1 address' - into the dictionnary entry 'addresses-add' using Config.return_effectives_value - (the data in the un-modified configuration) - """ - self._action(name, key) - return self._get(name, key, default, self._conf.return_effectives_value) + # Wireguard is "special" the default MTU is 1420 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + tunnel['mtu'] = '1476' - def load(self, mapping): - """ - load will take a dictionary defining how we wish the configuration - to be parsed and apply this definition to set the data. + tmp = leaf_node_changed(conf, ['encapsulation']) + if tmp: tunnel.update({'encapsulation_changed': {}}) - >>> mapping = { - 'addresses-add' : ('address', True, None), - 'comment' : ('description', False, 'auto'), - } - >>> conf.load(mapping) + # We must check if our interface is configured to be a DMVPN member + nhrp_base = ['protocols', 'nhrp', 'tunnel'] + conf.set_level(nhrp_base) + nhrp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if nhrp: tunnel.update({'nhrp' : list(nhrp.keys())}) - mapping is a dictionary where each key represents the name we wish - to have (such as 'addresses-add'), with a list a content representing - how the data should be parsed: - - the configuration section name - such as 'address' under 'interface ethernet eth1' - - boolean indicating if this data can have multiple values - for 'address', True, as multiple IPs can be set - for 'description', False, as it is a single string - - default represent the default value if absent from the configuration - 'None' indicate that no default should be set if the configuration - does not have the configuration section + return tunnel - """ - for local_name, (config_name, multiple, default) in mapping.items(): - if multiple: - self.get_values(local_name, config_name, default) - else: - self.get_value(local_name, config_name, default) +def verify(tunnel): + if 'deleted' in tunnel: + verify_bridge_delete(tunnel) - def remove_default(self,*options): - """ - remove all the values which were not changed from the default - """ - for option in options: - if not self._conf.exists(option): - del self.options[option] - continue + if 'nhrp' in tunnel and tunnel['ifname'] in tunnel['nhrp']: + raise ConfigError('Tunnel used for NHRP, it can not be deleted!') - if self._conf.return_value(option) == self.default[option]: - del self.options[option] - continue + return None - if self._conf.return_values(option) == self.default[option]: - del self.options[option] - continue + if 'encapsulation' not in tunnel: + raise ConfigError('Must configure the tunnel encapsulation for '\ + '{ifname}!'.format(**tunnel)) - def as_dict(self, lpath): - l = self._conf.get_level() - self._conf.set_level([]) - d = self._conf.get_config_dict(lpath) - # XXX: that not what I would have expected from get_config_dict - if lpath: - d = d[lpath[-1]] - # XXX: it should have provided me the content and not the key - self._conf.set_level(l) - return d + verify_mtu_ipv6(tunnel) + verify_address(tunnel) + verify_vrf(tunnel) - def to_api(self): - """ - provide a dictionary with the generated data for the configuration - options: the configuration value for the key - changes: per key how they changed from the previous configuration - actions: per changes all the options which were changed - """ - # as we have to use a dict() for the API for verify and apply the options - return { - 'options': self.options, - 'changes': self.changes, - 'actions': self.actions, - } + if 'local_ip' not in tunnel and 'dhcp_interface' not in tunnel: + raise ConfigError('local-ip is mandatory for tunnel') + if 'remote_ip' not in tunnel and tunnel['encapsulation'] != 'gre': + raise ConfigError('remote-ip is mandatory for tunnel') -default_config_data = { - # interface definition - 'vrf': '', - 'addresses-add': [], - 'addresses-del': [], - 'state': 'up', - 'dhcp-interface': '', - 'link_detect': 1, - 'ip': False, - 'ipv6': False, - 'nhrp': [], - 'arp_filter': 1, - 'arp_accept': 0, - 'arp_announce': 0, - 'arp_ignore': 0, - 'ipv6_accept_ra': 1, - 'ipv6_autoconf': 0, - 'ipv6_forwarding': 1, - 'ipv6_dad_transmits': 1, - # internal - 'interfaces': [], - 'tunnel': {}, - 'bridge': '', - # the following names are exactly matching the name - # for the ip command and must not be changed - 'ifname': '', - 'type': '', - 'alias': '', - 'mtu': '1476', - 'local': '', - 'remote': '', - 'dev': '', - 'multicast': 'disable', - 'allmulticast': 'disable', - 'ttl': '255', - 'tos': 'inherit', - 'key': '', - 'encaplimit': '4', - 'flowlabel': 'inherit', - 'hoplimit': '64', - 'tclass': 'inherit', - '6rd-prefix': '', - '6rd-relay-prefix': '', -} + if {'local_ip', 'dhcp_interface'} <= set(tunnel): + raise ConfigError('Can not use both local-ip and dhcp-interface') + if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + error_ipv6 = 'Encapsulation mode requires IPv6' + if 'local_ip' in tunnel and not is_ipv6(tunnel['local_ip']): + raise ConfigError(f'{error_ipv6} local-ip') -# dict name -> config name, multiple values, default -mapping = { - 'type': ('encapsulation', False, None), - 'alias': ('description', False, None), - 'mtu': ('mtu', False, None), - 'local': ('local-ip', False, None), - 'remote': ('remote-ip', False, None), - 'multicast': ('multicast', False, None), - 'dev': ('source-interface', False, None), - 'ttl': ('parameters ip ttl', False, None), - 'tos': ('parameters ip tos', False, None), - 'key': ('parameters ip key', False, None), - 'encaplimit': ('parameters ipv6 encaplimit', False, None), - 'flowlabel': ('parameters ipv6 flowlabel', False, None), - 'hoplimit': ('parameters ipv6 hoplimit', False, None), - 'tclass': ('parameters ipv6 tclass', False, None), - '6rd-prefix': ('6rd-prefix', False, None), - '6rd-relay-prefix': ('6rd-relay-prefix', False, None), - 'dhcp-interface': ('dhcp-interface', False, None), - 'state': ('disable', False, 'down'), - 'link_detect': ('disable-link-detect', False, 2), - 'vrf': ('vrf', False, None), - 'addresses': ('address', True, None), - 'arp_filter': ('ip disable-arp-filter', False, 0), - 'arp_accept': ('ip enable-arp-accept', False, 1), - 'arp_announce': ('ip enable-arp-announce', False, 1), - 'arp_ignore': ('ip enable-arp-ignore', False, 1), - 'ipv6_autoconf': ('ipv6 address autoconf', False, 1), - 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0), - 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None) -} + if 'remote_ip' in tunnel and not is_ipv6(tunnel['remote_ip']): + raise ConfigError(f'{error_ipv6} remote-ip') + else: + error_ipv4 = 'Encapsulation mode requires IPv4' + if 'local_ip' in tunnel and not is_ipv4(tunnel['local_ip']): + raise ConfigError(f'{error_ipv4} local-ip') + + if 'remote_ip' in tunnel and not is_ipv4(tunnel['remote_ip']): + raise ConfigError(f'{error_ipv4} remote-ip') + + if tunnel['encapsulation'] in ['sit', 'gre-bridge']: + if 'source_interface' in tunnel: + raise ConfigError('Option source-interface can not be used with ' \ + 'encapsulation "sit" or "gre-bridge"') + elif tunnel['encapsulation'] == 'gre': + if 'local_ip' in tunnel and is_ipv6(tunnel['local_ip']): + raise ConfigError('Can not use local IPv6 address is for mGRE tunnels') + +def generate(tunnel): + return None +def apply(tunnel): + if 'deleted' in tunnel or 'encapsulation_changed' in tunnel: + if tunnel['ifname'] in interfaces(): + tmp = Interface(tunnel['ifname']) + tmp.remove() + if 'deleted' in tunnel: + return None -def get_class (options): dispatch = { 'gre': GREIf, 'gre-bridge': GRETapIf, @@ -373,353 +143,52 @@ def get_class (options): 'sit': SitIf, } - kls = dispatch[options['type']] - if options['type'] == 'gre' and not options['remote'] \ - and not options['key'] and not options['multicast']: - # will use GreTapIf on GreIf deletion but it does not matter - return GRETapIf - elif options['type'] == 'sit' and options['6rd-prefix']: - # will use SitIf on Sit6RDIf deletion but it does not matter - return Sit6RDIf - return kls - -def get_interface_ip (ifname): - if not ifname: - return '' - try: - addrs = Interface(ifname).get_addr() - if addrs: - return addrs[0].split('/')[0] - except Exception: - return '' - -def get_afi (ip): - return IP6 if is_ipv6(ip) else IP4 - -def ip_proto (afi): - return 6 if afi == IP6 else 4 - - -def get_config(config=None): - ifname = os.environ.get('VYOS_TAGNODE_VALUE','') - if not ifname: - raise ConfigError('Interface not specified') - - if config: - config = config - else: - config = Config() - - conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data) - options = conf.options - changes = conf.changes - options['ifname'] = ifname - - if changes['section'] == 'delete': - conf.get_effective('type', mapping['type'][0]) - config.set_level(['protocols', 'nhrp', 'tunnel']) - options['nhrp'] = config.list_nodes('') - return conf.to_api() - - # load all the configuration option according to the mapping - conf.load(mapping) - - # remove default value if not set and not required - afi_local = get_afi(options['local']) - if afi_local == IP6: - conf.remove_default('ttl', 'tos', 'key') - if afi_local == IP4: - conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass') - - # if the local-ip is not set, pick one from the interface ! - # hopefully there is only one, otherwise it will not be very deterministic - # at time of writing the code currently returns ipv4 before ipv6 in the list - - # XXX: There is no way to trigger an update of the interface source IP if - # XXX: the underlying interface IP address does change, I believe this - # XXX: limit/issue is present in vyatta too - - if not options['local'] and options['dhcp-interface']: - # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if - # XXX: the interface was not DHCP. As there is no easy way to find if an - # XXX: interface is using DHCP, and using this feature to get 127.0.0.1 - # XXX: makes little sense, I feel the change in behaviour is acceptable - picked = get_interface_ip(options['dhcp-interface']) - if picked == '': - picked = '127.0.0.1' - print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead') - options['local'] = picked - options['dhcp-interface'] = '' - - # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, - # accept_ra must be 2 - if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']: - options['ipv6_accept_ra'] = 2 - - # allmulticast fate is linked to multicast - options['allmulticast'] = options['multicast'] - - # check that per encapsulation all local-remote pairs are unique - ct = conf.as_dict(['interfaces', 'tunnel']) - options['tunnel'] = {} - - # check for bridges - tmp = is_member(config, ifname, 'bridge') - if tmp: options['bridge'] = next(iter(tmp)) - options['interfaces'] = interfaces() - - for name in ct: - tunnel = ct[name] - encap = tunnel.get('encapsulation', '') - local = tunnel.get('local-ip', '') - if not local: - local = get_interface_ip(tunnel.get('dhcp-interface', '')) - remote = tunnel.get('remote-ip', '<unset>') - pair = f'{local}-{remote}' - options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1 - - return conf.to_api() - - -def verify(conf): - options = conf['options'] - changes = conf['changes'] - actions = conf['actions'] - - ifname = options['ifname'] - iftype = options['type'] - - if changes['section'] == 'delete': - if ifname in options['nhrp']: - raise ConfigError(( - f'Cannot delete interface tunnel {iftype} {ifname}, ' - 'it is used by NHRP')) - - if options['bridge']: - raise ConfigError(( - f'Cannot delete interface "{options["ifname"]}" as it is a ' - f'member of bridge "{options["bridge"]}"!')) - - # done, bail out early - return None - - # tunnel encapsulation checks - - if not iftype: - raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}') - - if changes['type'] in ('modify', 'delete'): - # TODO: we could now deal with encapsulation modification by deleting / recreating - raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}') - - if iftype != 'sit' and options['6rd-prefix']: - # XXX: should be able to remove this and let the definition catch it - print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}') - - # what are the tunnel options we can set / modified / deleted - - kls = get_class(options) - valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state'] - valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore'] - valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'] - - if changes['section'] == 'create': - valid.extend(['type',]) - valid.extend([o for o in kls.options if o not in kls.updates]) - - for create in actions['create']: - if create not in valid: - raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation') - - for modify in actions['modify']: - if modify not in valid: - raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation') - - for delete in actions['delete']: - if delete in kls.required: - raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}') - - # tunnel information - - tun_local = options['local'] - afi_local = get_afi(tun_local) - tun_remote = options['remote'] or tun_local - afi_remote = get_afi(tun_remote) - tun_ismgre = iftype == 'gre' and not options['remote'] - tun_is6rd = iftype == 'sit' and options['6rd-prefix'] - tun_dev = options['dev'] - - # incompatible options - - if not tun_local and not options['dhcp-interface'] and not tun_is6rd: - raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}') - - if tun_local and options['dhcp-interface']: - raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}') - - if tun_dev and iftype in ('gre-bridge', 'sit'): - raise ConfigError(f'source interface can not be used with {iftype} {ifname}') - - # tunnel endpoint - - if afi_local != afi_remote: - raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}') - - if afi_local != kls.tunnel: - version = 4 if tun_local == IP4 else 6 - raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}') - - ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)]) - ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)]) - - if tun_ismgre and afi_local == IP6: - raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}') - - # check address family use - # checks are not enforced (but ip command failing) for backward compatibility - - if ipv4_count and not IP4 in kls.ip: - print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}') - - if ipv6_count and not IP6 in kls.ip: - print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}') - - # vrf check - if options['vrf']: - if options['vrf'] not in options['interfaces']: - raise ConfigError(f'VRF "{options["vrf"]}" does not exist') - - if options['bridge']: - raise ConfigError(( - f'Interface "{options["ifname"]}" cannot be member of VRF ' - f'"{options["vrf"]}" and bridge {options["bridge"]} ' - f'at the same time!')) - - # bridge and address check - if ( options['bridge'] - and ( options['addresses-add'] - or options['ipv6_autoconf'] ) ): - raise ConfigError(( - f'Cannot assign address to interface "{options["name"]}" ' - f'as it is a member of bridge "{options["bridge"]}"!')) - - # source-interface check - - if tun_dev and tun_dev not in options['interfaces']: - raise ConfigError(f'device "{tun_dev}" does not exist') - - # tunnel encapsulation check - - convert = { - (6, 4, 'gre'): 'ip6gre', - (6, 6, 'gre'): 'ip6gre', - (4, 6, 'ipip'): 'ipip6', - (6, 6, 'ipip'): 'ip6ip6', + # We need to re-map the tunnel encapsulation proto to a valid interface class + encap = tunnel['encapsulation'] + klass = dispatch[encap] + + # This is a special type of interface which needs additional parameters + # when created using iproute2. Instead of passing a ton of arguments, + # use a dictionary provided by the interface class which holds all the + # options necessary. + conf = klass.get_config() + + # Copy/re-assign our dictionary values to values understood by the + # derived _Tunnel classes + mapping = { + # this : get_config() + 'local_ip' : 'local', + 'remote_ip' : 'remote', + 'source_interface' : 'dev', + 'parameters.ip.ttl' : 'ttl', + 'parameters.ip.tos' : 'tos', + 'parameters.ip.key' : 'key', + 'parameters.ipv6.encaplimit' : 'encaplimit' } - iprotos = [] - if ipv4_count: - iprotos.append(4) - if ipv6_count: - iprotos.append(6) - - for iproto in iprotos: - replace = convert.get((kls.tunnel, iproto, iftype), '') - if replace: - raise ConfigError( - f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' + - f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.' - ) - - # tunnel options - - incompatible = [] - if afi_local == IP6: - incompatible.extend(['ttl', 'tos', 'key',]) - if afi_local == IP4: - incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass']) - - for option in incompatible: - if option in options: - # TODO: raise converted to print as not enforced by vyatta - # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}') - print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}') - - # duplicate tunnel pairs - - pair = '{}-{}'.format(options['local'], options['remote']) - if options['tunnel'].get(iftype, {}).get(pair, 0) > 1: - raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}') + # Add additional IPv6 options if tunnel is IPv6 aware + if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']: + mappingv6 = { + # this : get_config() + 'parameters.ipv6.encaplimit' : 'encaplimit' + } + mapping.update(mappingv6) - return None + for our_key, their_key in mapping.items(): + if dict_search(our_key, tunnel) and their_key in conf: + conf[their_key] = dict_search(our_key, tunnel) + tun = klass(tunnel['ifname'], **conf) + tun.change_options() + tun.update(tunnel) -def generate(gre): return None -def apply(conf): - options = conf['options'] - changes = conf['changes'] - actions = conf['actions'] - kls = get_class(options) - - # extract ifname as otherwise it is duplicated on the interface creation - ifname = options.pop('ifname') - - # only the valid keys for creation of a Interface - config = dict((k, options[k]) for k in kls.options if options[k]) - - # setup or create the tunnel interface if it does not exist - tunnel = kls(ifname, **config) - - if changes['section'] == 'delete': - tunnel.remove() - # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup - # which identified tunnels type which were not used anymore to remove them - # (ie: gre0, gretap0, etc.) The perl code did however nothing - # This feature is also not implemented yet - return - - # A GRE interface without remote will be mGRE - # if the interface does not suppor the option, it skips the change - for option in tunnel.updates: - if changes['section'] in 'create' and option in tunnel.options: - # it was setup at creation - continue - if not options[option]: - # remote can be set to '' and it would generate an invalide command - continue - tunnel.set_interface(option, options[option]) - - # set other interface properties - for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', - 'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore', - 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): - if not options[option]: - # should never happen but better safe - continue - tunnel.set_interface(option, options[option]) - - # assign/remove VRF (ONLY when not a member of a bridge, - # otherwise 'nomaster' removes it from it) - if not options['bridge']: - tunnel.set_vrf(options['vrf']) - - # Configure interface address(es) - for addr in options['addresses-del']: - tunnel.del_addr(addr) - for addr in options['addresses-add']: - tunnel.add_addr(addr) - - # now bring it up (or not) - tunnel.set_admin_state(options['state']) - - if __name__ == '__main__': try: c = get_config() - verify(c) generate(c) + verify(c) apply(c) except ConfigError as e: print(e) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 9bda35d0a..7cfc76aa0 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -80,9 +80,6 @@ def verify(wireguard): raise ConfigError('Wireguard private-key not found! Execute: ' \ '"run generate wireguard [default-keypair|named-keypairs]"') - if 'address' not in wireguard: - raise ConfigError('IP address required!') - if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 5d723bbfd..b25fcd4e0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -241,10 +241,12 @@ def generate(wifi): # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': - render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', + wifi) elif wifi['type'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True) + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', + wifi) return None @@ -261,7 +263,6 @@ def apply(wifi): # Assign WiFi instance configuration parameters to config dict conf['phy'] = wifi['physical_device'] - conf['wds'] = 'on' if 'wds' in wifi else 'off' # Finally create the new interface w = WiFiIf(interface, **conf) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index bce3405d0..976953b31 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -91,21 +91,21 @@ def generate(wwan): wwan['device'] = find_device_file(wwan['device']) # Create PPP configuration files - render(config_wwan, 'wwan/peer.tmpl', wwan, trim_blocks=True) + render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script - render(config_wwan_chat, 'wwan/chat.tmpl', wwan, trim_blocks=True) + render(config_wwan_chat, 'wwan/chat.tmpl', wwan) # generated script file must be executable # Create script for ip-pre-up.d render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', - wwan, trim_blocks=True, permission=0o755) + wwan, permission=0o755) # Create script for ip-up.d render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', - wwan, trim_blocks=True, permission=0o755) + wwan, permission=0o755) # Create script for ip-down.d render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', - wwan, trim_blocks=True, permission=0o755) + wwan, permission=0o755) return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 11a5b7aaa..a65e8b567 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -170,12 +170,12 @@ def verify(data): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) + render(charon_conf_file, 'ipsec/charon.tmpl', data) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) # old_umask = os.umask(0o077) - # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True) + # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_secrets(data) @@ -186,12 +186,12 @@ def generate(data): if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) - render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True) + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data) os.umask(old_umask) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) # old_umask = os.umask(0o077) - # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True) + # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_conf(data) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index b66cd370a..f5c023b81 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -18,18 +18,19 @@ import jmespath import json import os -from copy import deepcopy from distutils.version import LooseVersion from platform import release as kernel_version from sys import exit from netifaces import interfaces from vyos.config import Config +from vyos.configdict import dict_merge from vyos.template import render -from vyos.util import call from vyos.util import cmd from vyos.util import check_kmod +from vyos.util import dict_search from vyos.validate import is_addr_assigned +from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -40,17 +41,6 @@ if LooseVersion(kernel_version()) > LooseVersion('5.1'): else: k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] -default_config_data = { - 'deleted': False, - 'destination': [], - 'helper_functions': None, - 'pre_ct_helper': '', - 'pre_ct_conntrack': '', - 'out_ct_helper': '', - 'out_ct_conntrack': '', - 'source': [] -} - iptables_nat_config = '/tmp/vyos-nat-rules.nft' def get_handler(json, chain, target): @@ -66,114 +56,43 @@ def get_handler(json, chain, target): return None -def verify_rule(rule, err_msg): +def verify_rule(config, err_msg): """ Common verify steps used for both source and destination NAT """ - if rule['translation_port'] or rule['dest_port'] or rule['source_port']: - if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: - proto = rule['protocol'] - raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') - if '/' in rule['translation_address']: + if (dict_search('translation.port', config) != None or + dict_search('destination.port', config) != None or + dict_search('source.port', config)): + + if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError(f'{err_msg}\n' \ + 'ports can only be specified when protocol is '\ + 'either tcp, udp or tcp_udp!') + + if '/' in (dict_search('translation.address', config) or []): raise ConfigError(f'{err_msg}\n' \ 'Cannot use ports with an IPv4net type translation address as it\n' \ 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') - -def parse_configuration(conf, source_dest): - """ Common wrapper to read in both NAT source and destination CLI """ - ruleset = [] - base_level = ['nat', source_dest] - conf.set_level(base_level) - for number in conf.list_nodes(['rule']): - rule = { - 'description': '', - 'dest_address': '', - 'dest_port': '', - 'disabled': False, - 'exclude': False, - 'interface_in': '', - 'interface_out': '', - 'log': False, - 'protocol': 'all', - 'number': number, - 'source_address': '', - 'source_prefix': '', - 'source_port': '', - 'translation_address': '', - 'translation_prefix': '', - 'translation_port': '' - } - conf.set_level(base_level + ['rule', number]) - - if conf.exists(['description']): - rule['description'] = conf.return_value(['description']) - - if conf.exists(['destination', 'address']): - tmp = conf.return_value(['destination', 'address']) - if tmp.startswith('!'): - tmp = tmp.replace('!', '!=') - rule['dest_address'] = tmp - - if conf.exists(['destination', 'port']): - tmp = conf.return_value(['destination', 'port']) - if tmp.startswith('!'): - tmp = tmp.replace('!', '!=') - rule['dest_port'] = tmp - - if conf.exists(['disable']): - rule['disabled'] = True - - if conf.exists(['exclude']): - rule['exclude'] = True - - if conf.exists(['inbound-interface']): - rule['interface_in'] = conf.return_value(['inbound-interface']) - - if conf.exists(['outbound-interface']): - rule['interface_out'] = conf.return_value(['outbound-interface']) - - if conf.exists(['log']): - rule['log'] = True - - if conf.exists(['protocol']): - rule['protocol'] = conf.return_value(['protocol']) - - if conf.exists(['source', 'address']): - tmp = conf.return_value(['source', 'address']) - if tmp.startswith('!'): - tmp = tmp.replace('!', '!=') - rule['source_address'] = tmp - - if conf.exists(['source', 'prefix']): - rule['source_prefix'] = conf.return_value(['source', 'prefix']) - - if conf.exists(['source', 'port']): - tmp = conf.return_value(['source', 'port']) - if tmp.startswith('!'): - tmp = tmp.replace('!', '!=') - rule['source_port'] = tmp - - if conf.exists(['translation', 'address']): - rule['translation_address'] = conf.return_value(['translation', 'address']) - - if conf.exists(['translation', 'prefix']): - rule['translation_prefix'] = conf.return_value(['translation', 'prefix']) - - if conf.exists(['translation', 'port']): - rule['translation_port'] = conf.return_value(['translation', 'port']) - - ruleset.append(rule) - - return ruleset - def get_config(config=None): - nat = deepcopy(default_config_data) if config: conf = config else: conf = Config() + base = ['nat'] + nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # T2665: we must add the tagNode defaults individually until this is + # moved to the base class + for direction in ['source', 'destination']: + if direction in nat: + default_values = defaults(base + [direction, 'rule']) + for rule in nat[direction]['rule']: + nat[direction]['rule'][rule] = dict_merge(default_values, + nat[direction]['rule'][rule]) + + # read in current nftable (once) for further processing tmp = cmd('nft -j list table raw') nftable_json = json.loads(tmp) @@ -182,7 +101,7 @@ def get_config(config=None): pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' condensed_json = jmespath.search(pattern, nftable_json) - if not conf.exists(['nat']): + if not conf.exists(base): nat['helper_functions'] = 'remove' # Retrieve current table handler positions @@ -190,9 +109,7 @@ def get_config(config=None): nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') - - nat['deleted'] = True - + nat['deleted'] = '' return nat # check if NAT connection tracking helpers need to be set up - this has to @@ -206,19 +123,10 @@ def get_config(config=None): nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') - # set config level for parsing in NAT configuration - conf.set_level(['nat']) - - # use a common wrapper function to read in the source / destination - # tree from the config - thus we do not need to replicate almost the - # same code :-) - for tgt in ['source', 'destination', 'nptv6']: - nat[tgt] = parse_configuration(conf, tgt) - return nat def verify(nat): - if nat['deleted']: + if not nat or 'deleted' in nat: # no need to verify the CLI as NAT is going to be deactivated return None @@ -226,49 +134,55 @@ def verify(nat): if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): raise Exception('could not determine nftable ruleset handlers') - for rule in nat['source']: - interface = rule['interface_out'] - err_msg = f'Source NAT configuration error in rule "{rule["number"]}":' - - if interface and interface not in 'any' and interface not in interfaces(): - print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') + if dict_search('source.rule', nat): + for rule, config in dict_search('source.rule', nat).items(): + err_msg = f'Source NAT configuration error in rule {rule}:' + if 'outbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'outbound-interface not specified') + else: + if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces(): + print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') - if not rule['interface_out']: - raise ConfigError(f'{err_msg} outbound-interface not specified') - if rule['translation_address']: - addr = rule['translation_address'] - if addr != 'masquerade': - for ip in addr.split('-'): - if not is_addr_assigned(ip): - print(f'Warning: IP address {ip} does not exist on the system!') + addr = dict_search('translation.address', config) + if addr != None: + if addr != 'masquerade': + for ip in addr.split('-'): + if not is_addr_assigned(ip): + print(f'WARNING: IP address {ip} does not exist on the system!') + elif 'exclude' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'translation address not specified') - elif not rule['exclude']: - raise ConfigError(f'{err_msg} translation address not specified') + # common rule verification + verify_rule(config, err_msg) - # common rule verification - verify_rule(rule, err_msg) - for rule in nat['destination']: - interface = rule['interface_in'] - err_msg = f'Destination NAT configuration error in rule "{rule["number"]}":' + if dict_search('destination.rule', nat): + for rule, config in dict_search('destination.rule', nat).items(): + err_msg = f'Destination NAT configuration error in rule {rule}:' - if interface and interface not in 'any' and interface not in interfaces(): - print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') + if 'inbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'inbound-interface not specified') + else: + if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): + print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') - if not rule['interface_in']: - raise ConfigError(f'{err_msg} inbound-interface not specified') - if not rule['translation_address'] and not rule['exclude']: - raise ConfigError(f'{err_msg} translation address not specified') + if dict_search('translation.address', config) == None and 'exclude' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'translation address not specified') - # common rule verification - verify_rule(rule, err_msg) + # common rule verification + verify_rule(config, err_msg) return None def generate(nat): - render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) + render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, + permission=0o755) return None def apply(nat): diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index d6453ec83..b102b3e9e 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -53,8 +53,8 @@ def generate(ntp): if not ntp: return None - render(config_file, 'ntp/ntp.conf.tmpl', ntp, trim_blocks=True) - render(systemd_override, 'ntp/override.conf.tmpl', ntp, trim_blocks=True) + render(config_file, 'ntp/ntp.conf.tmpl', ntp) + render(systemd_override, 'ntp/override.conf.tmpl', ntp) return None diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py new file mode 100755 index 000000000..c4024dce4 --- /dev/null +++ b/src/conf_mode/policy-local-route.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +def get_config(config=None): + + if config: + conf = config + else: + conf = Config() + base = ['policy', 'local-route'] + pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # delete policy local-route + dict = {} + tmp = node_changed(conf, ['policy', 'local-route', 'rule']) + if tmp: + for rule in (tmp or []): + src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) + if src: + dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) + pbr.update(dict) + + # delete policy local-route rule x source x.x.x.x + if 'rule' in pbr: + for rule in pbr['rule']: + src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) + if src: + dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) + pbr.update(dict) + + return pbr + +def verify(pbr): + # bail out early - looks like removal from running config + if not pbr: + return None + + if 'rule' in pbr: + for rule in pbr['rule']: + if 'source' not in pbr['rule'][rule]: + raise ConfigError('Source address required!') + else: + if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']: + raise ConfigError('Table set is required!') + + return None + +def generate(pbr): + if not pbr: + return None + + return None + +def apply(pbr): + if not pbr: + return None + + # Delete old rule if needed + if 'rule_remove' in pbr: + for rule in pbr['rule_remove']: + for src in pbr['rule_remove'][rule]['source']: + call(f'ip rule del prio {rule} from {src}') + + # Generate new config + if 'rule' in pbr: + for rule in pbr['rule']: + table = pbr['rule'][rule]['set']['table'] + if pbr['rule'][rule]['source']: + for src in pbr['rule'][rule]['source']: + call(f'ip rule add prio {rule} from {src} lookup {table}') + + 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/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 654874232..642738b09 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -14,16 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit from vyos.config import Config from vyos.util import call +from vyos.util import dict_search from vyos.template import render from vyos.template import render_to_string +from vyos import ConfigError from vyos import frr -from vyos import ConfigError, airbag +from vyos import airbag airbag.enable() config_file = r'/tmp/bgp.frr' @@ -31,8 +31,10 @@ config_file = r'/tmp/bgp.frr' def get_config(): conf = Config() base = ['protocols', 'nbgp'] - bgp = conf.get_config_dict(base, key_mangling=('-', '_')) + bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # XXX: any reason we can not move this into the FRR template? + # we shall not call vtysh directly, especially not in get_config() if not conf.exists(base): bgp = {} call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ') @@ -40,9 +42,6 @@ def get_config(): if not conf.exists(base + ['route-map']): call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ') - from pprint import pprint - pprint(bgp) - return bgp def verify(bgp): @@ -50,9 +49,23 @@ def verify(bgp): return None # Check if declared more than one ASN - for asn in bgp['nbgp'].items(): - if len(bgp['nbgp']) > 1: - raise ConfigError('Only one bgp ASN process can be definded') + if len(bgp) > 1: + raise ConfigError('Only one BGP AS can be defined!') + + for asn, asn_config in bgp.items(): + # Common verification for both peer-group and neighbor statements + for neigh in ['neighbor', 'peer_group']: + # bail out early if there is no neighbor or peer-group statement + # this also saves one indention level + if neigh not in asn_config: + continue + + for neighbor, config in asn_config[neigh].items(): + if 'remote_as' not in config and 'peer_group' not in config: + raise ConfigError(f'BGP remote-as must be specified for "{neighbor}"!') + + if 'remote_as' in config and 'peer_group' in config: + raise ConfigError(f'BGP peer-group member "{neighbor}" cannot override remote-as of peer-group!') return None @@ -61,33 +74,40 @@ def generate(bgp): bgp['new_frr_config'] = '' return None - # render(config) not needed, its only for debug - render(config_file, 'frr/bgp.frr.tmpl', bgp) + # only one BGP AS is supported, so we can directly send the first key + # of the config dict + asn = list(bgp.keys())[0] + bgp[asn]['asn'] = asn - bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp) + # render(config) not needed, its only for debug + render(config_file, 'frr/bgp.frr.tmpl', bgp[asn]) + bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp[asn]) return None def apply(bgp): - # Save original configration prior to starting any commit actions - bgp['original_config'] = frr.get_configuration(daemon='bgpd') - bgp['modified_config'] = frr.replace_section(bgp['original_config'], bgp['new_frr_config'], from_re='router bgp .*') + # Save original configuration prior to starting any commit actions + frr_cfg = {} + frr_cfg['original_config'] = frr.get_configuration(daemon='bgpd') + frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], bgp['new_frr_config'], from_re='router bgp .*') # Debugging + print('') print('--------- DEBUGGING ----------') - print(f'Existing config:\n{bgp["original_config"]}\n\n') + print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n') - print(f'Modified config:\n{bgp["modified_config"]}\n\n') + print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') - # Frr Mark configuration will test for syntax errors and exception out if any syntax errors are detected - frr.mark_configuration(bgp['modified_config']) + # FRR mark configuration will test for syntax errors and throws an + # exception if any syntax errors is detected + frr.mark_configuration(frr_cfg['modified_config']) - # Commit the resulting new configuration to frr, this will render an frr.CommitError() Exception on fail - frr.reload_configuration(bgp['modified_config'], daemon='bgpd') + # Commit resulting configuration to FRR, this will throw CommitError + # on failure + frr.reload_configuration(frr_cfg['modified_config'], daemon='bgpd') return None - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 6f4fc784d..8606e7364 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -21,8 +21,9 @@ from sys import exit from vyos import ConfigError from vyos.config import Config -from vyos.util import call +from vyos.util import call, process_named_running from vyos.template import render +from signal import SIGTERM from vyos import airbag airbag.enable() @@ -36,12 +37,20 @@ def get_config(config=None): conf = Config() igmp_conf = { 'igmp_conf' : False, + 'pim_conf' : False, + 'igmp_proxy_conf' : False, 'old_ifaces' : {}, 'ifaces' : {} } if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')): return None + if conf.exists('protocols igmp-proxy'): + igmp_conf['igmp_proxy_conf'] = True + + if conf.exists('protocols pim'): + igmp_conf['pim_conf'] = True + if conf.exists('protocols igmp'): igmp_conf['igmp_conf'] = True @@ -79,6 +88,10 @@ def verify(igmp): return None if igmp['igmp_conf']: + # Check conflict with IGMP-Proxy + if igmp['igmp_proxy_conf']: + raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") + # Check interfaces if not igmp['ifaces']: raise ConfigError(f"IGMP require defined interfaces!") @@ -99,9 +112,16 @@ def apply(igmp): if igmp is None: return None - if os.path.exists(config_file): - call(f'vtysh -d pimd -f {config_file}') - os.remove(config_file) + pim_pid = process_named_running('pimd') + if igmp['igmp_conf'] or igmp['pim_conf']: + if not pim_pid: + call(f'pimd -d -F traditional --daemon -A 127.0.0.1') + + if os.path.exists(config_file): + call(f'vtysh -d pimd -f {config_file}') + os.remove(config_file) + elif pim_pid: + os.kill(int(pim_pid), SIGTERM) return None diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index b0b8d705b..bd372a7b3 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -107,16 +107,15 @@ def generate(isis): isis[process]['process'] = process # render(config) not needed, its only for debug - render(config_file, 'frr/isis.frr.tmpl', isis[process], trim_blocks=True) + render(config_file, 'frr/isis.frr.tmpl', isis[process]) isis['new_frr_config'] = render_to_string('frr/isis.frr.tmpl', - isis[process], trim_blocks=True) + isis[process]) return None def apply(isis): - - # Save original configration prior to starting any commit actions + # Save original configuration prior to starting any commit actions frr_cfg = {} frr_cfg['original_config'] = frr.get_configuration(daemon='isisd') frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], isis['new_frr_config'], from_re='interface .*') diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 84948baf4..791b18110 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2020 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 @@ -16,351 +16,124 @@ import os +from sys import exit + from vyos.config import Config -from vyos import ConfigError +from vyos.configdict import node_changed +from vyos.template import render_to_string from vyos.util import call -from vyos.template import render - +from vyos.util import dict_search +from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() config_file = r'/tmp/ldpd.frr' -def sysctl(name, value): - call('sysctl -wq {}={}'.format(name, value)) - def get_config(config=None): if config: conf = config else: conf = Config() - mpls_conf = { - 'router_id' : None, - 'mpls_ldp' : False, - 'old_parameters' : { - 'no_ttl_propagation' : False, - 'maximum_ttl' : None - }, - 'parameters' : { - 'no_ttl_propagation' : False, - 'maximum_ttl' : None - }, - 'old_ldp' : { - 'interfaces' : [], - 'neighbors' : {}, - 'd_transp_ipv4' : None, - 'd_transp_ipv6' : None, - 'hello_ipv4_holdtime' : None, - 'hello_ipv4_interval' : None, - 'hello_ipv6_holdtime' : None, - 'hello_ipv6_interval' : None, - 'ses_ipv4_hold' : None, - 'ses_ipv6_hold' : None, - 'export_ipv4_exp' : False, - 'export_ipv6_exp' : False, - 'cisco_interop_tlv' : False, - 'transport_prefer_ipv4' : False, - 'target_ipv4_addresses' : [], - 'target_ipv6_addresses' : [], - 'target_ipv4_enable' : False, - 'target_ipv6_enable' : False, - 'target_ipv4_hello_int' : None, - 'target_ipv6_hello_int' : None, - 'target_ipv4_hello_hold' : None, - 'target_ipv6_hello_hold' : None - }, - 'ldp' : { - 'interfaces' : [], - 'neighbors' : {}, - 'd_transp_ipv4' : None, - 'd_transp_ipv6' : None, - 'hello_ipv4_holdtime' : None, - 'hello_ipv4_interval' : None, - 'hello_ipv6_holdtime' : None, - 'hello_ipv6_interval' : None, - 'ses_ipv4_hold' : None, - 'ses_ipv6_hold' : None, - 'export_ipv4_exp' : False, - 'export_ipv6_exp' : False, - 'cisco_interop_tlv' : False, - 'transport_prefer_ipv4' : False, - 'target_ipv4_addresses' : [], - 'target_ipv6_addresses' : [], - 'target_ipv4_enable' : False, - 'target_ipv6_enable' : False, - 'target_ipv4_hello_int' : None, - 'target_ipv6_hello_int' : None, - 'target_ipv4_hello_hold' : None, - 'target_ipv6_hello_hold' : None - } - } - if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')): - return None - - # If LDP is defined then enable LDP portion of code - if conf.exists('protocols mpls ldp'): - mpls_conf['mpls_ldp'] = True - - # Set to MPLS hierarchy configuration level - conf.set_level('protocols mpls') - - # Get no_ttl_propagation - if conf.exists_effective('parameters no-propagate-ttl'): - mpls_conf['old_parameters']['no_ttl_propagation'] = True - - if conf.exists('parameters no-propagate-ttl'): - mpls_conf['parameters']['no_ttl_propagation'] = True - - # Get maximum_ttl - if conf.exists_effective('parameters maximum-ttl'): - mpls_conf['old_parameters']['maximum_ttl'] = conf.return_effective_value('parameters maximum-ttl') - - if conf.exists('parameters maximum-ttl'): - mpls_conf['parameters']['maximum_ttl'] = conf.return_value('parameters maximum-ttl') - - # Set to LDP hierarchy configuration level - conf.set_level('protocols mpls ldp') - - # Get router-id - if conf.exists_effective('router-id'): - mpls_conf['old_router_id'] = conf.return_effective_value('router-id') - - if conf.exists('router-id'): - mpls_conf['router_id'] = conf.return_value('router-id') - - # Get hello-ipv4-holdtime - if conf.exists_effective('discovery hello-ipv4-holdtime'): - mpls_conf['old_ldp']['hello_ipv4_holdtime'] = conf.return_effective_value('discovery hello-ipv4-holdtime') - - if conf.exists('discovery hello-ipv4-holdtime'): - mpls_conf['ldp']['hello_ipv4_holdtime'] = conf.return_value('discovery hello-ipv4-holdtime') - - # Get hello-ipv4-interval - if conf.exists_effective('discovery hello-ipv4-interval'): - mpls_conf['old_ldp']['hello_ipv4_interval'] = conf.return_effective_value('discovery hello-ipv4-interval') - - if conf.exists('discovery hello-ipv4-interval'): - mpls_conf['ldp']['hello_ipv4_interval'] = conf.return_value('discovery hello-ipv4-interval') - - # Get hello-ipv6-holdtime - if conf.exists_effective('discovery hello-ipv6-holdtime'): - mpls_conf['old_ldp']['hello_ipv6_holdtime'] = conf.return_effective_value('discovery hello-ipv6-holdtime') - - if conf.exists('discovery hello-ipv6-holdtime'): - mpls_conf['ldp']['hello_ipv6_holdtime'] = conf.return_value('discovery hello-ipv6-holdtime') - - # Get hello-ipv6-interval - if conf.exists_effective('discovery hello-ipv6-interval'): - mpls_conf['old_ldp']['hello_ipv6_interval'] = conf.return_effective_value('discovery hello-ipv6-interval') - - if conf.exists('discovery hello-ipv6-interval'): - mpls_conf['ldp']['hello_ipv6_interval'] = conf.return_value('discovery hello-ipv6-interval') - - # Get session-ipv4-holdtime - if conf.exists_effective('discovery session-ipv4-holdtime'): - mpls_conf['old_ldp']['ses_ipv4_hold'] = conf.return_effective_value('discovery session-ipv4-holdtime') - - if conf.exists('discovery session-ipv4-holdtime'): - mpls_conf['ldp']['ses_ipv4_hold'] = conf.return_value('discovery session-ipv4-holdtime') - - # Get session-ipv6-holdtime - if conf.exists_effective('discovery session-ipv6-holdtime'): - mpls_conf['old_ldp']['ses_ipv6_hold'] = conf.return_effective_value('discovery session-ipv6-holdtime') - - if conf.exists('discovery session-ipv6-holdtime'): - mpls_conf['ldp']['ses_ipv6_hold'] = conf.return_value('discovery session-ipv6-holdtime') - - # Get discovery transport-ipv4-address - if conf.exists_effective('discovery transport-ipv4-address'): - mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address') - - if conf.exists('discovery transport-ipv4-address'): - mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address') - - # Get discovery transport-ipv6-address - if conf.exists_effective('discovery transport-ipv6-address'): - mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address') - - if conf.exists('discovery transport-ipv6-address'): - mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address') - - # Get export ipv4 explicit-null - if conf.exists_effective('export ipv4 explicit-null'): - mpls_conf['old_ldp']['export_ipv4_exp'] = True - - if conf.exists('export ipv4 explicit-null'): - mpls_conf['ldp']['export_ipv4_exp'] = True - - # Get export ipv6 explicit-null - if conf.exists_effective('export ipv6 explicit-null'): - mpls_conf['old_ldp']['export_ipv6_exp'] = True - - if conf.exists('export ipv6 explicit-null'): - mpls_conf['ldp']['export_ipv6_exp'] = True - - # Get target_ipv4_addresses - if conf.exists_effective('targeted-neighbor ipv4 address'): - mpls_conf['old_ldp']['target_ipv4_addresses'] = conf.return_effective_values('targeted-neighbor ipv4 address') + base = ['protocols', 'mpls'] - if conf.exists('targeted-neighbor ipv4 address'): - mpls_conf['ldp']['target_ipv4_addresses'] = conf.return_values('targeted-neighbor ipv4 address') - - # Get target_ipv4_enable - if conf.exists_effective('targeted-neighbor ipv4 enable'): - mpls_conf['old_ldp']['target_ipv4_enable'] = True - - if conf.exists('targeted-neighbor ipv4 enable'): - mpls_conf['ldp']['target_ipv4_enable'] = True - - # Get target_ipv4_hello_int - if conf.exists_effective('targeted-neighbor ipv4 hello-interval'): - mpls_conf['old_ldp']['target_ipv4_hello_int'] = conf.return_effective_value('targeted-neighbor ipv4 hello-interval') - - if conf.exists('targeted-neighbor ipv4 hello-interval'): - mpls_conf['ldp']['target_ipv4_hello_int'] = conf.return_value('targeted-neighbor ipv4 hello-interval') - - # Get target_ipv4_hello_hold - if conf.exists_effective('targeted-neighbor ipv4 hello-holdtime'): - mpls_conf['old_ldp']['target_ipv4_hello_hold'] = conf.return_effective_value('targeted-neighbor ipv4 hello-holdtime') - - if conf.exists('targeted-neighbor ipv4 hello-holdtime'): - mpls_conf['ldp']['target_ipv4_hello_hold'] = conf.return_value('targeted-neighbor ipv4 hello-holdtime') - - # Get target_ipv6_addresses - if conf.exists_effective('targeted-neighbor ipv6 address'): - mpls_conf['old_ldp']['target_ipv6_addresses'] = conf.return_effective_values('targeted-neighbor ipv6 address') - - if conf.exists('targeted-neighbor ipv6 address'): - mpls_conf['ldp']['target_ipv6_addresses'] = conf.return_values('targeted-neighbor ipv6 address') - - # Get target_ipv6_enable - if conf.exists_effective('targeted-neighbor ipv6 enable'): - mpls_conf['old_ldp']['target_ipv6_enable'] = True - - if conf.exists('targeted-neighbor ipv6 enable'): - mpls_conf['ldp']['target_ipv6_enable'] = True - - # Get target_ipv6_hello_int - if conf.exists_effective('targeted-neighbor ipv6 hello-interval'): - mpls_conf['old_ldp']['target_ipv6_hello_int'] = conf.return_effective_value('targeted-neighbor ipv6 hello-interval') - - if conf.exists('targeted-neighbor ipv6 hello-interval'): - mpls_conf['ldp']['target_ipv6_hello_int'] = conf.return_value('targeted-neighbor ipv6 hello-interval') - - # Get target_ipv6_hello_hold - if conf.exists_effective('targeted-neighbor ipv6 hello-holdtime'): - mpls_conf['old_ldp']['target_ipv6_hello_hold'] = conf.return_effective_value('targeted-neighbor ipv6 hello-holdtime') - - if conf.exists('targeted-neighbor ipv6 hello-holdtime'): - mpls_conf['ldp']['target_ipv6_hello_hold'] = conf.return_value('targeted-neighbor ipv6 hello-holdtime') - - # Get parameters cisco-interop-tlv - if conf.exists_effective('parameters cisco-interop-tlv'): - mpls_conf['old_ldp']['cisco_interop_tlv'] = True - - if conf.exists('parameters cisco-interop-tlv'): - mpls_conf['ldp']['cisco_interop_tlv'] = True - - # Get parameters transport-prefer-ipv4 - if conf.exists_effective('parameters transport-prefer-ipv4'): - mpls_conf['old_ldp']['transport_prefer_ipv4'] = True - - if conf.exists('parameters transport-prefer-ipv4'): - mpls_conf['ldp']['transport_prefer_ipv4'] = True - - # Get interfaces - if conf.exists_effective('interface'): - mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface') - - if conf.exists('interface'): - mpls_conf['ldp']['interfaces'] = conf.return_values('interface') - - # Get neighbors - for neighbor in conf.list_effective_nodes('neighbor'): - mpls_conf['old_ldp']['neighbors'].update({ - neighbor : { - 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor), default=''), - 'ttl_security' : conf.return_effective_value('neighbor {0} ttl-security'.format(neighbor), default=''), - 'session_holdtime' : conf.return_effective_value('neighbor {0} session-holdtime'.format(neighbor), default='') - } - }) - - for neighbor in conf.list_nodes('neighbor'): - mpls_conf['ldp']['neighbors'].update({ - neighbor : { - 'password' : conf.return_value('neighbor {0} password'.format(neighbor), default=''), - 'ttl_security' : conf.return_value('neighbor {0} ttl-security'.format(neighbor), default=''), - 'session_holdtime' : conf.return_value('neighbor {0} session-holdtime'.format(neighbor), default='') - } - }) - - return mpls_conf - -def operate_mpls_on_intfc(interfaces, action): - rp_filter = 0 - if action == 1: - rp_filter = 2 - for iface in interfaces: - sysctl('net.mpls.conf.{0}.input'.format(iface), action) - # Operate rp filter - sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter) + mpls = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return mpls def verify(mpls): - if mpls is None: + # If no config, then just bail out early. + if not mpls: return None - if mpls['mpls_ldp']: - # Require router-id - if not mpls['router_id']: - raise ConfigError(f"MPLS ldp router-id is mandatory!") + # Checks to see if LDP is properly configured + if 'ldp' in mpls: + # If router ID not defined + if 'router_id' not in mpls['ldp']: + raise ConfigError('Router ID missing. An LDP router id is mandatory!') - # Require discovery transport-address - if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']: - raise ConfigError(f"MPLS ldp discovery transport address is mandatory!") + # If interface not set + if 'interface' not in mpls['ldp']: + raise ConfigError('LDP interfaces are missing. An LDP interface is mandatory!') - # Require interface - if not mpls['ldp']['interfaces']: - raise ConfigError(f"MPLS ldp interface is mandatory!") + # If transport addresses are not set + if not dict_search('ldp.discovery.transport_ipv4_address', mpls) and \ + not dict_search('ldp.discovery.transport_ipv6_address', mpls): + raise ConfigError('LDP transport address missing!') + + return None def generate(mpls): - if mpls is None: + # If there's no MPLS config generated, create dictionary key with no value. + if not mpls: + mpls['new_frr_config'] = '' return None - render(config_file, 'frr/ldpd.frr.tmpl', mpls) + mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls) return None def apply(mpls): - if mpls is None: - return None - - # Set number of entries in the platform label table - if mpls['mpls_ldp']: - sysctl('net.mpls.platform_labels', '1048575') + # Define dictionary that will load FRR config + frr_cfg = {} + # Save original configuration prior to starting any commit actions + frr_cfg['original_config'] = frr.get_configuration(daemon='ldpd') + frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], mpls['new_frr_config'], from_re='mpls.*') + + # If FRR config is blank, rerun the blank commit three times due to frr-reload + # behavior/bug not properly clearing out on one commit. + if mpls['new_frr_config'] == '': + for x in range(3): + frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') + elif not 'ldp' in mpls: + for x in range(3): + frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') else: - sysctl('net.mpls.platform_labels', '0') - - # Choose whether to copy IP TTL to MPLS header TTL - if mpls['parameters']['no_ttl_propagation']: - sysctl('net.mpls.ip_ttl_propagate', '0') + # FRR mark configuration will test for syntax errors and throws an + # exception if any syntax errors is detected + frr.mark_configuration(frr_cfg['modified_config']) + + # Commit resulting configuration to FRR, this will throw CommitError + # on failure + frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') + + # Set number of entries in the platform label tables + labels = '0' + if 'interface' in mpls: + labels = '1048575' + call(f'sysctl -wq net.mpls.platform_labels={labels}') + + # Check for changes in global MPLS options + if 'parameters' in mpls: + # Choose whether to copy IP TTL to MPLS header TTL + if 'no_propagate_ttl' in mpls['parameters']: + call('sysctl -wq net.mpls.ip_ttl_propagate=0') + # Choose whether to limit maximum MPLS header TTL + if 'maximum_ttl' in mpls['parameters']: + ttl = mpls['parameters']['maximum_ttl'] + call(f'sysctl -wq net.mpls.default_ttl={ttl}') else: - sysctl('net.mpls.ip_ttl_propagate', '1') - - # Choose whether to limit maximum MPLS header TTL - if mpls['parameters']['maximum_ttl']: - sysctl('net.mpls.default_ttl', '%s' %(mpls['parameters']['maximum_ttl'])) + # Set default global MPLS options if not defined. + call('sysctl -wq net.mpls.ip_ttl_propagate=1') + call('sysctl -wq net.mpls.default_ttl=255') + + # Enable and disable MPLS processing on interfaces per configuration + if 'interface' in mpls: + system_interfaces = [] + system_interfaces.append(((os.popen('sysctl net.mpls.conf').read()).split('\n'))) + del system_interfaces[0][-1] + for configured_interface in mpls['interface']: + for system_interface in system_interfaces[0]: + if configured_interface in system_interface: + call(f'sysctl -wq net.mpls.conf.{configured_interface}.input=1') + elif system_interface.endswith(' = 1'): + system_interface = system_interface.replace(' = 1', '=0') + call(f'sysctl -wq {system_interface}') else: - sysctl('net.mpls.default_ttl', '255') - - # Allow mpls on interfaces - operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1) - - # Disable mpls on deleted interfaces - diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces']) - operate_mpls_on_intfc(diactive_ifaces, 0) - - if os.path.exists(config_file): - call(f'vtysh -d ldpd -f {config_file}') - os.remove(config_file) + # If MPLS interfaces are not configured, set MPLS processing disabled + system_interfaces = [] + system_interfaces.append(((os.popen('sysctl net.mpls.conf').read()).replace(" = 1", "=0")).split('\n')) + del system_interfaces[0][-1] + for interface in (system_interfaces[0]): + call(f'sysctl -wq {interface}') return None diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 6d333e19a..8a9f034d5 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -21,8 +21,9 @@ from sys import exit from vyos.config import Config from vyos import ConfigError -from vyos.util import call +from vyos.util import call, process_named_running from vyos.template import render +from signal import SIGTERM from vyos import airbag airbag.enable() @@ -36,6 +37,8 @@ def get_config(config=None): conf = Config() pim_conf = { 'pim_conf' : False, + 'igmp_conf' : False, + 'igmp_proxy_conf' : False, 'old_pim' : { 'ifaces' : {}, 'rp' : {} @@ -48,6 +51,12 @@ def get_config(config=None): if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')): return None + if conf.exists('protocols igmp-proxy'): + pim_conf['igmp_proxy_conf'] = True + + if conf.exists('protocols igmp'): + pim_conf['igmp_conf'] = True + if conf.exists('protocols pim'): pim_conf['pim_conf'] = True @@ -92,6 +101,10 @@ def verify(pim): return None if pim['pim_conf']: + # Check conflict with IGMP-Proxy + if pim['igmp_proxy_conf']: + raise ConfigError(f"IGMP proxy and PIM cannot be both configured at the same time") + # Check interfaces if not pim['pim']['ifaces']: raise ConfigError(f"PIM require defined interfaces!") @@ -126,9 +139,16 @@ def apply(pim): if pim is None: return None - if os.path.exists(config_file): - call("vtysh -d pimd -f " + config_file) - os.remove(config_file) + pim_pid = process_named_running('pimd') + if pim['igmp_conf'] or pim['pim_conf']: + if not pim_pid: + call(f'pimd -d -F traditional --daemon -A 127.0.0.1') + + if os.path.exists(config_file): + call("vtysh -d pimd -f " + config_file) + os.remove(config_file) + elif pim_pid: + os.kill(int(pim_pid), SIGTERM) return None diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index 27d0ee60c..67edeb630 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -56,7 +56,7 @@ def verify(fastnetmon): if not os.access(fastnetmon["alert_script"], os.X_OK): raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"])) else: - raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) + raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) def generate(fastnetmon): if not fastnetmon: @@ -67,8 +67,8 @@ def generate(fastnetmon): return - render(config_file, 'ids/fastnetmon.tmpl', fastnetmon, trim_blocks=True) - render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon, trim_blocks=True) + render(config_file, 'ids/fastnetmon.tmpl', fastnetmon) + render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon) return None diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 68c554360..f676fdbbe 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -283,7 +283,7 @@ def generate(ipoe): if not ipoe: return None - render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True) + render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe) if ipoe['auth_mode'] == 'local': render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 2260b3fe1..9fbd531da 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -73,11 +73,11 @@ def generate(pppoe): if not pppoe: return None - render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True) + render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe) if dict_search('authentication.mode', pppoe) == 'local': render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', - pppoe, trim_blocks=True, permission=0o640) + pppoe, permission=0o640) else: if os.path.exists(pppoe_chap_secrets): os.unlink(pppoe_chap_secrets) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 687d7068f..65eb11ce3 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -93,7 +93,7 @@ def generate(rtradv): if not rtradv: return None - render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644) + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, permission=0o644) return None def apply(rtradv): diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index e07745963..8f99053d2 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -66,8 +66,8 @@ def generate(ssh): return None - render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True) - render(systemd_override, 'ssh/override.conf.tmpl', ssh, trim_blocks=True) + render(config_file, 'ssh/sshd_config.tmpl', ssh) + render(systemd_override, 'ssh/override.conf.tmpl', ssh) return None diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 2c0bbd4f7..39bad717d 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -256,7 +256,7 @@ def generate(login): if len(login['radius_server']) > 0: render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', - login, trim_blocks=True) + login) uid = getpwnam('root').pw_uid gid = getpwnam('root').pw_gid diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-option.py index 1061b90ac..910c14474 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-option.py @@ -39,7 +39,7 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['system', 'options'] + base = ['system', 'option'] options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default @@ -73,13 +73,13 @@ def verify(options): return None def generate(options): - render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True) - render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True) + render(curlrc_config, 'system/curlrc.tmpl', options) + render(ssh_config, 'system/ssh_config.tmpl', options) return None def apply(options): # System bootup beep - if 'beep_if_fully_booted' in options: + if 'startup_beep' in options: cmd('systemctl enable vyos-beep.service') else: cmd('systemctl disable vyos-beep.service') @@ -87,10 +87,10 @@ def apply(options): # Ctrl-Alt-Delete action if os.path.exists(systemd_action_file): os.unlink(systemd_action_file) - if 'ctrl_alt_del_action' in options: - if options['ctrl_alt_del_action'] == 'reboot': + if 'ctrl_alt_del' in options: + if options['ctrl_alt_del'] == 'reboot': os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) - elif options['ctrl_alt_del_action'] == 'poweroff': + elif options['ctrl_alt_del'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) # Configure HTTP client @@ -123,7 +123,7 @@ def apply(options): # Keyboard layout - there will be always the default key inside the dict # but we check for key existence anyway if 'keyboard_layout' in options: - cmd('loadkeys -C /dev/console {keyboard_layout}'.format(**options)) + cmd('loadkeys {keyboard_layout}'.format(**options)) if __name__ == '__main__': try: @@ -134,4 +134,3 @@ if __name__ == '__main__': except ConfigError as e: print(e) exit(1) - diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index b1daf7a82..3d8a51cd8 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -203,12 +203,12 @@ def generate(c): return None conf = '/etc/rsyslog.d/vyos-rsyslog.conf' - render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True) + render(conf, 'syslog/rsyslog.conf.tmpl', c) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter conf = '/etc/logrotate.d/vyos-rsyslog' - render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True) + render(conf, 'syslog/logrotate.tmpl', c) def verify(c): diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py index a540d1b9e..b5ce32beb 100755 --- a/src/conf_mode/system_lcd.py +++ b/src/conf_mode/system_lcd.py @@ -61,9 +61,9 @@ def generate(lcd): lcd['device'] = find_device_file(lcd['device']) # Render config file for daemon LCDd - render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd, trim_blocks=True) + render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd) # Render config file for client lcdproc - render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True) + render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd) return None diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 56e195b6a..2409eec1f 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -92,7 +92,7 @@ def generate(tftpd): config['listen_address'] = f'[{address}]:{port} -6' file = config_file + str(idx) - render(file, 'tftp-server/default.tmpl', config, trim_blocks=True) + render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 return None diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 80eb8daf2..e970d2ef5 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -357,7 +357,7 @@ def generate(l2tp): if not l2tp: return None - render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True) + render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp) if l2tp['auth_mode'] == 'local': render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index af8604972..b2aa13c0d 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -34,12 +34,10 @@ ocserv_passwd = cfg_dir + '/ocpasswd' radius_cfg = cfg_dir + '/radiusclient.conf' radius_servers = cfg_dir + '/radius_servers' - # Generate hash from user cleartext password def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) - def get_config(): conf = Config() base = ['vpn', 'openconnect'] @@ -47,10 +45,12 @@ def get_config(): return None ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - return ocserv + return ocserv def verify(ocserv): if ocserv is None: @@ -88,7 +88,7 @@ def verify(ocserv): ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") ocserv["network_settings"]["push_route"].append("default") else: - ocserv["network_settings"]["push_route"] = "default" + ocserv["network_settings"]["push_route"] = "default" else: raise ConfigError('openconnect network settings required') @@ -99,19 +99,18 @@ def generate(ocserv): if "radius" in ocserv["authentication"]["mode"]: # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"]) # Render radius servers - render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"]) else: if "local_users" in ocserv["authentication"]: for user in ocserv["authentication"]["local_users"]["username"]: ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) # Render local users - render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) # Render config - render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) - + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv) def apply(ocserv): diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 3125ee9d0..30abe4782 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -264,10 +264,10 @@ def generate(pptp): if not pptp: return None - render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True) + render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp) if pptp['local_users']: - render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True) + render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp) os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: if os.path.exists(pptp_chap_secrets): diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 1b2b80ce5..47367f125 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -82,11 +82,11 @@ def generate(sstp): return None # accel-cmd reload doesn't work so any change results in a restart of the daemon - render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True) + render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp) if dict_search('authentication.mode', sstp) == 'local': render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', - sstp, trim_blocks=True, permission=0o640) + sstp, permission=0o640) else: if os.path.exists(sstp_chap_secrets): os.unlink(sstp_chap_secrets) |