diff options
Diffstat (limited to 'src/conf_mode')
84 files changed, 573 insertions, 488 deletions
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py index aac07bd80..1cd8f5451 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,92 +13,62 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys -import os -import re -import syslog as sl +from sys import exit from vyos.config import Config +from vyos.configdict import node_changed from vyos.util import call from vyos import ConfigError - from vyos import airbag airbag.enable() -arp_cmd = '/usr/sbin/arp' - -def get_config(): - c = Config() - if not c.exists('protocols static arp'): - return None - - c.set_level('protocols static') - config_data = {} - - for ip_addr in c.list_nodes('arp'): - config_data.update( - { - ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr') - } - ) +def get_config(config=None): + if config: + conf = config + else: + conf = Config() - return config_data + base = ['protocols', 'static', 'arp'] + arp = conf.get_config_dict(base, get_first_key=True) -def generate(c): - c_eff = Config() - c_eff.set_level('protocols static') - c_eff_cnf = {} - for ip_addr in c_eff.list_effective_nodes('arp'): - c_eff_cnf.update( - { - ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr') - } - ) + if 'interface' in arp: + for interface in arp['interface']: + tmp = node_changed(conf, base + ['interface', interface, 'address'], recursive=True) + if tmp: arp['interface'][interface].update({'address_old' : tmp}) - config_data = { - 'remove' : [], - 'update' : {} - } - ### removal - if c == None: - for ip_addr in c_eff_cnf: - config_data['remove'].append(ip_addr) - else: - for ip_addr in c_eff_cnf: - if not ip_addr in c or c[ip_addr] == None: - config_data['remove'].append(ip_addr) + return arp - ### add/update - if c != None: - for ip_addr in c: - if not ip_addr in c_eff_cnf: - config_data['update'][ip_addr] = c[ip_addr] - if ip_addr in c_eff_cnf: - if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None: - config_data['update'][ip_addr] = c[ip_addr] +def verify(arp): + pass - return config_data +def generate(arp): + pass -def apply(c): - for ip_addr in c['remove']: - sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) - call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') +def apply(arp): + if not arp: + return None - for ip_addr in c['update']: - sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) - updated = c['update'][ip_addr] - call(f'{arp_cmd} -s {ip_addr} {updated}') + if 'interface' in arp: + for interface, interface_config in arp['interface'].items(): + # Delete old static ARP assignments first + if 'address_old' in interface_config: + for address in interface_config['address_old']: + call(f'ip neigh del {address} dev {interface}') + # Add new static ARP entries to interface + if 'address' not in interface_config: + continue + for address, address_config in interface_config['address'].items(): + mac = address_config['mac'] + call(f'ip neigh add {address} lladdr {mac} dev {interface}') if __name__ == '__main__': - try: - c = get_config() - ## syntax verification is done via cli - config = generate(c) - apply(config) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index d93a2a8f4..39a2971ce 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2020 VyOS maintainers and contributors +# Copyright (C) 2017-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -78,7 +78,7 @@ def generate(relay): continue config['instance'] = instance - render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', + render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.j2', config) return None diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index aabf2bdf5..82289526f 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -101,9 +101,9 @@ def verify(conntrack): return None def generate(conntrack): - render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack) - render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack) - render(nftables_ct_file, 'conntrack/nftables-ct.tmpl', conntrack) + render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack) + render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack) + render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack) # dry-run newly generated configuration tmp = run(f'nft -c -f {nftables_ct_file}') diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 34d1f7398..311e01529 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -111,7 +111,7 @@ def generate(conntrack): os.unlink(config_file) return None - render(config_file, 'conntrackd/conntrackd.conf.tmpl', conntrack) + render(config_file, 'conntrackd/conntrackd.conf.j2', conntrack) return None diff --git a/src/conf_mode/containers.py b/src/conf_mode/container.py index 516671844..7e1dc5911 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/container.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -28,7 +28,6 @@ from vyos.configdict import node_changed from vyos.util import call from vyos.util import cmd from vyos.util import run -from vyos.util import read_file from vyos.util import write_file from vyos.template import inc_ip from vyos.template import is_ipv4 @@ -42,6 +41,20 @@ airbag.enable() config_containers_registry = '/etc/containers/registries.conf' config_containers_storage = '/etc/containers/storage.conf' +def _run_rerun(container_cmd): + counter = 0 + while True: + if counter >= 10: + break + try: + _cmd(container_cmd) + break + except: + counter = counter +1 + sleep(0.5) + + return None + def _cmd(command): if os.path.exists('/tmp/vyos.container.debug'): print(command) @@ -77,10 +90,10 @@ def get_config(config=None): container['name'][name] = dict_merge(default_values, container['name'][name]) # Delete container network, delete containers - tmp = node_changed(conf, ['container', 'network']) + tmp = node_changed(conf, base + ['container', 'network']) if tmp: container.update({'network_remove' : tmp}) - tmp = node_changed(conf, ['container', 'name']) + tmp = node_changed(conf, base + ['container', 'name']) if tmp: container.update({'container_remove' : tmp}) return container @@ -93,6 +106,20 @@ def verify(container): # Add new container if 'name' in container: for name, container_config in container['name'].items(): + # Container image is a mandatory option + if 'image' not in container_config: + raise ConfigError(f'Container image for "{name}" is mandatory!') + + # verify container image exists locally + image = container_config['image'] + + # Check if requested container image exists locally. If it does not + # exist locally - inform the user. + if run(f'podman image exists {image}') != 0: + raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\ + f'locally.\nPlease use "add container image {image}" to add it '\ + 'to the system!') + if 'network' in container_config: if len(container_config['network']) > 1: raise ConfigError(f'Only one network can be specified for container "{name}"!') @@ -151,10 +178,6 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') - # Container image is a mandatory option - if 'image' not in container_config: - raise ConfigError(f'Container image for "{name}" is mandatory!') - # If 'allow-host-networks' or 'network' not set. if 'allow_host_networks' not in container_config and 'network' not in container_config: raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!') @@ -194,6 +217,10 @@ def verify(container): def generate(container): # bail out early - looks like removal from running config if not container: + if os.path.exists(config_containers_registry): + os.unlink(config_containers_registry) + if os.path.exists(config_containers_storage): + os.unlink(config_containers_storage) return None if 'network' in container: @@ -227,8 +254,8 @@ def generate(container): write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2)) - render(config_containers_registry, 'containers/registry.tmpl', container) - render(config_containers_storage, 'containers/storage.tmpl', container) + render(config_containers_registry, 'container/registries.conf.j2', container) + render(config_containers_storage, 'container/storage.conf.j2', container) return None @@ -263,13 +290,6 @@ def apply(container): memory = container_config['memory'] restart = container_config['restart'] - # Check if requested container image exists locally. If it does not, we - # pull it. print() is the best way to have a good response from the - # polling process to the user to display progress. If the image exists - # locally, a user can update it running `update container image <name>` - tmp = run(f'podman image exists {image}') - if tmp != 0: print(os.system(f'podman pull {image}')) - # Add capability options. Should be in uppercase cap_add = '' if 'cap_add' in container_config: @@ -318,7 +338,7 @@ def apply(container): f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {device} {port} {volume} {env_opt}' if 'allow_host_networks' in container_config: - run(f'{container_base_cmd} --net host {image}') + _run_rerun(f'{container_base_cmd} --net host {image}') else: for network in container_config['network']: ipparam = '' @@ -326,25 +346,10 @@ def apply(container): address = container_config['network'][network]['address'] ipparam = f'--ip {address}' - run(f'{container_base_cmd} --net {network} {ipparam} {image}') + _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}') return None -def run(container_cmd): - counter = 0 - while True: - if counter >= 10: - break - try: - _cmd(container_cmd) - break - except: - counter = counter +1 - sleep(0.5) - - return None - - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index 6352e0b4a..4de2ca2f3 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -66,18 +66,19 @@ def generate(relay): if not relay: return None - render(config_file, 'dhcp-relay/dhcrelay.conf.tmpl', relay) + render(config_file, 'dhcp-relay/dhcrelay.conf.j2', relay) return None def apply(relay): # bail out early - looks like removal from running config + service_name = 'isc-dhcp-relay.service' if not relay: - call('systemctl stop isc-dhcp-relay.service') + call(f'systemctl stop {service_name}') if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart isc-dhcp-relay.service') + call(f'systemctl restart {service_name}') return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index a8cef5ebf..52b682d6d 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -109,7 +109,7 @@ def get_config(config=None): if not conf.exists(base): return None - dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) # T2665: defaults include lease time per TAG node which need to be added to # individual subnet definitions default_values = defaults(base + ['shared-network-name', 'subnet']) @@ -286,7 +286,7 @@ def generate(dhcp): # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw # parameters we can pass to ISC DHCPd tmp_file = '/tmp/dhcpd.conf' - render(tmp_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) # XXX: as we have the ability for a user to pass in "raw" options via VyOS # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered @@ -299,7 +299,7 @@ def generate(dhcp): # Now that we know that the newly rendered configuration is "good" we can # render the "real" configuration - render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) return None diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index aea2c3b73..c1bd51f62 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -82,19 +82,20 @@ def generate(relay): if not relay: return None - render(config_file, 'dhcp-relay/dhcrelay6.conf.tmpl', relay) + render(config_file, 'dhcp-relay/dhcrelay6.conf.j2', relay) return None def apply(relay): # bail out early - looks like removal from running config + service_name = 'isc-dhcp-relay6.service' if not relay: # DHCPv6 relay support is removed in the commit - call('systemctl stop isc-dhcp-relay6.service') + call(f'systemctl stop {service_name}') if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart isc-dhcp-relay6.service') + call(f'systemctl restart {service_name}') return None diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index e6a2e4486..078ff327c 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -41,7 +41,9 @@ def get_config(config=None): if not conf.exists(base): return None - dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) return dhcpv6 def verify(dhcpv6): @@ -51,7 +53,7 @@ def verify(dhcpv6): # If DHCP is enabled we need one share-network if 'shared_network_name' not in dhcpv6: - raise ConfigError('No DHCPv6 shared networks configured. At least\n' \ + raise ConfigError('No DHCPv6 shared networks configured. At least '\ 'one DHCPv6 shared network must be configured.') # Inspect shared-network/subnet @@ -60,8 +62,9 @@ def verify(dhcpv6): for network, network_config in dhcpv6['shared_network_name'].items(): # A shared-network requires a subnet definition if 'subnet' not in network_config: - raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". At least one\n' \ - 'lease subnet must be configured for each shared network!') + raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". '\ + 'At least one lease subnet must be configured for '\ + 'each shared network!') for subnet, subnet_config in network_config['subnet'].items(): if 'address_range' in subnet_config: @@ -83,20 +86,20 @@ def verify(dhcpv6): # Stop address must be greater or equal to start address if not ip_address(stop) >= ip_address(start): - raise ConfigError(f'address-range stop address "{stop}" must be greater or equal\n' \ + raise ConfigError(f'address-range stop address "{stop}" must be greater then or equal ' \ f'to the range start address "{start}"!') # DHCPv6 range start address must be unique - two ranges can't # start with the same address - makes no sense if start in range6_start: - raise ConfigError(f'Conflicting DHCPv6 lease range:\n' \ + raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool start address "{start}" defined multipe times!') range6_start.append(start) # DHCPv6 range stop address must be unique - two ranges can't # end with the same address - makes no sense if stop in range6_stop: - raise ConfigError(f'Conflicting DHCPv6 lease range:\n' \ + raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool stop address "{stop}" defined multipe times!') range6_stop.append(stop) @@ -112,7 +115,7 @@ def verify(dhcpv6): for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items(): if 'stop' not in prefix_config: - raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}"\n' + raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\ f'must be configured') if 'prefix_length' not in prefix_config: @@ -126,6 +129,10 @@ def verify(dhcpv6): if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet): raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!') + if 'vendor_option' in subnet_config: + if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2: + raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') + # Subnets must be unique if subnet in subnets: raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') @@ -149,8 +156,8 @@ def verify(dhcpv6): raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) if not listen_ok: - raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \ - 'this machine. At least one subnet6 must be connected such that\n' \ + raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\ + 'this machine. At least one subnet6 must be connected such that '\ 'DHCPv6 listens on an interface!') @@ -161,20 +168,20 @@ def generate(dhcpv6): if not dhcpv6 or 'disable' in dhcpv6: return None - render(config_file, 'dhcp-server/dhcpdv6.conf.tmpl', dhcpv6) + render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6) return None def apply(dhcpv6): # bail out early - looks like removal from running config + service_name = 'isc-dhcp-server6.service' if not dhcpv6 or 'disable' in dhcpv6: # DHCP server is removed in the commit - call('systemctl stop isc-dhcp-server6.service') + call(f'systemctl stop {service_name}') if os.path.exists(config_file): os.unlink(config_file) - return None - call('systemctl restart isc-dhcp-server6.service') + call(f'systemctl restart {service_name}') return None if __name__ == '__main__': diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index fa9b21f20..f1c2d1f43 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -279,10 +279,10 @@ def generate(dns): if not dns: return None - render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', + render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2', dns, user=pdns_rec_user, group=pdns_rec_group) - render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', + render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2', dns, user=pdns_rec_user, group=pdns_rec_group) for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'): @@ -290,7 +290,7 @@ def generate(dns): if 'authoritative_zones' in dns: for zone in dns['authoritative_zones']: - render(zone['file'], 'dns-forwarding/recursor.zone.conf.tmpl', + render(zone['file'], 'dns-forwarding/recursor.zone.conf.j2', zone, user=pdns_rec_user, group=pdns_rec_group) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index a31e5ed75..06a2f7e15 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -131,7 +131,7 @@ def generate(dyndns): if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) + render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns) return None def apply(dyndns): diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f33198a49..6924bf555 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -21,6 +21,7 @@ from glob import glob from json import loads from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed @@ -225,7 +226,7 @@ def verify_rule(firewall, rule_conf, ipv6): raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') if not group_obj: - print(f'WARNING: {error_group} "{group_name}" has no members') + Warning(f'{error_group} "{group_name}" has no members!') if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): if 'protocol' not in rule_conf: @@ -326,8 +327,8 @@ def generate(firewall): else: firewall['cleanup_commands'] = cleanup_commands(firewall) - render(nftables_conf, 'firewall/nftables.tmpl', firewall) - render(nftables_defines_conf, 'firewall/nftables-defines.tmpl', firewall) + render(nftables_conf, 'firewall/nftables.j2', firewall) + render(nftables_defines_conf, 'firewall/nftables-defines.j2', firewall) return None def apply_sysfs(firewall): @@ -395,7 +396,7 @@ def resync_policy_route(): # Update policy route as firewall groups were updated tmp = run(policy_route_conf_script) if tmp > 0: - print('Warning: Failed to re-apply policy route configuration') + Warning('Failed to re-apply policy route configuration!') def apply(firewall): if 'first_install' in firewall: diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 25bf54790..7f7a98b04 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -239,8 +239,8 @@ def generate(flow_config): if not flow_config: return None - render(uacctd_conf_path, 'pmacct/uacctd.conf.tmpl', flow_config) - render(systemd_override, 'pmacct/override.conf.tmpl', flow_config) + render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config) + render(systemd_override, 'pmacct/override.conf.j2', flow_config) # Reload systemd manager configuration call('systemctl daemon-reload') diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 7d51bb393..f939f9469 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -152,7 +152,7 @@ def generate(ha): if not ha: return None - render(VRRP.location['config'], 'high-availability/keepalived.conf.tmpl', ha) + render(VRRP.location['config'], 'high-availability/keepalived.conf.j2', ha) return None def apply(ha): diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 87bad0dc6..93f244f42 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -21,13 +21,14 @@ import copy import vyos.util import vyos.hostsd_client -from vyos import ConfigError +from vyos.base import Warning from vyos.config import Config from vyos.ifconfig import Section from vyos.template import is_ip from vyos.util import cmd from vyos.util import call from vyos.util import process_named_running +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -113,7 +114,7 @@ def verify(hosts): for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items(): # Warnin user if interface does not have DHCP or DHCPv6 configured if not set(interface_config).intersection(['dhcp', 'dhcpv6']): - print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!') + Warning(f'"{interface}" is not a DHCP interface but uses DHCP name-server option!') return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 00f3d4f7f..4a7906c17 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -117,7 +117,7 @@ def generate(http_api): with open(api_conf_file, 'w') as f: json.dump(http_api, f, indent=2) - render(systemd_service, 'https/vyos-http-api.service.tmpl', http_api) + render(systemd_service, 'https/vyos-http-api.service.j2', http_api) return None def apply(http_api): diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 37fa36797..3057357fc 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -214,8 +214,8 @@ def generate(https): 'certbot': certbot } - render(config_file, 'https/nginx.default.tmpl', data) - render(systemd_override, 'https/override.conf.tmpl', https) + render(config_file, 'https/nginx.default.j2', data) + render(systemd_override, 'https/override.conf.j2', https) return None def apply(https): diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index fb030c9f3..de6a51c64 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -19,6 +19,7 @@ import os from sys import exit from netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -92,10 +93,10 @@ def generate(igmp_proxy): # bail out early - service is disabled, but inform user if 'disable' in igmp_proxy: - print('WARNING: IGMP Proxy will be deactivated because it is disabled') + Warning('IGMP Proxy will be deactivated because it is disabled') return None - render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) + render(config_file, 'igmp-proxy/igmpproxy.conf.j2', igmp_proxy) return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index ad5a0f499..4167594e3 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -68,7 +68,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'bonding'] - bond = get_interface_dict(conf, base) + ifname, bond = get_interface_dict(conf, base) # To make our own life easier transfor the list of member interfaces # into a dictionary - we will use this to add additional information @@ -81,14 +81,14 @@ def get_config(config=None): if 'mode' in bond: bond['mode'] = get_bond_mode(bond['mode']) - tmp = leaf_node_changed(conf, ['mode']) + tmp = leaf_node_changed(conf, base + [ifname, 'mode']) if tmp: bond.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['lacp-rate']) + tmp = leaf_node_changed(conf, base + [ifname, 'lacp-rate']) if tmp: bond.update({'shutdown_required': {}}) # determine which members have been removed - interfaces_removed = leaf_node_changed(conf, ['member', 'interface']) + interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) if interfaces_removed: bond.update({'shutdown_required': {}}) if 'member' not in bond: diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index b1f7e6d7c..38ae727c1 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -50,15 +50,15 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'bridge'] - bridge = get_interface_dict(conf, base) + ifname, bridge = get_interface_dict(conf, base) # determine which members have been removed - tmp = node_changed(conf, ['member', 'interface'], key_mangling=('-', '_')) + tmp = node_changed(conf, base + [ifname, 'member', 'interface'], key_mangling=('-', '_')) if tmp: if 'member' in bridge: - bridge['member'].update({'interface_remove': tmp }) + bridge['member'].update({'interface_remove' : tmp }) else: - bridge.update({'member': {'interface_remove': tmp }}) + bridge.update({'member' : {'interface_remove' : tmp }}) if dict_search('member.interface', bridge): # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 4a1eb7b93..e771581e1 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -37,7 +37,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'dummy'] - dummy = get_interface_dict(conf, base) + _, dummy = get_interface_dict(conf, base) return dummy def verify(dummy): diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 6aea7a80e..fec4456fb 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -19,6 +19,7 @@ import os from glob import glob from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_address @@ -64,7 +65,7 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) base = ['interfaces', 'ethernet'] - ethernet = get_interface_dict(conf, base) + _, ethernet = get_interface_dict(conf, base) if 'deleted' not in ethernet: if pki: ethernet['pki'] = pki @@ -142,8 +143,8 @@ def verify(ethernet): raise ConfigError('XDP requires additional TX queues, too few available!') if {'is_bond_member', 'mac'} <= set(ethernet): - print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' - f'is a member of bond "{is_bond_member}"'.format(**ethernet)) + Warning(f'changing mac address "{mac}" will be ignored as "{ifname}" ' \ + f'is a member of bond "{is_bond_member}"'.format(**ethernet)) # use common function to verify VLAN configuration verify_vlan_config(ethernet) @@ -152,7 +153,7 @@ def verify(ethernet): def generate(ethernet): if 'eapol' in ethernet: render(wpa_suppl_conf.format(**ethernet), - 'ethernet/wpa_supplicant.conf.tmpl', ethernet) + 'ethernet/wpa_supplicant.conf.j2', ethernet) ifname = ethernet['ifname'] cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 3a668226b..b9cf2fa3c 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -21,6 +21,8 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_bridge_delete @@ -41,7 +43,18 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'geneve'] - geneve = get_interface_dict(conf, base) + ifname, geneve = get_interface_dict(conf, base) + + # GENEVE interfaces are picky and require recreation if certain parameters + # change. But a GENEVE interface should - of course - not be re-created if + # it's description or IP address is adjusted. Feels somehow logic doesn't it? + for cli_option in ['remote', 'vni']: + if leaf_node_changed(conf, base + [ifname, cli_option]): + geneve.update({'rebuild_required': {}}) + + if is_node_changed(conf, base + [ifname, 'parameters']): + geneve.update({'rebuild_required': {}}) + return geneve def verify(geneve): @@ -67,11 +80,12 @@ def generate(geneve): def apply(geneve): # Check if GENEVE interface already exists - if geneve['ifname'] in interfaces(): - g = GeneveIf(geneve['ifname']) - # GENEVE is super picky and the tunnel always needs to be recreated, - # thus we can simply always delete it first. - g.remove() + if 'rebuild_required' in geneve or 'delete' in geneve: + if geneve['ifname'] in interfaces(): + g = GeneveIf(geneve['ifname']) + # GENEVE is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + g.remove() if 'deleted' not in geneve: # Finally create the new interface diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 22256bf4f..6a486f969 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -45,15 +45,15 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'l2tpv3'] - l2tpv3 = get_interface_dict(conf, base) + ifname, l2tpv3 = get_interface_dict(conf, base) # To delete an l2tpv3 interface we need the current tunnel and session-id if 'deleted' in l2tpv3: - tmp = leaf_node_changed(conf, ['tunnel-id']) + tmp = leaf_node_changed(conf, base + [ifname, 'tunnel-id']) # leaf_node_changed() returns a list l2tpv3.update({'tunnel_id': tmp[0]}) - tmp = leaf_node_changed(conf, ['session-id']) + tmp = leaf_node_changed(conf, base + [ifname, 'session-id']) l2tpv3.update({'session_id': tmp[0]}) return l2tpv3 diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index e4bc15bb5..08d34477a 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -36,7 +36,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'loopback'] - loopback = get_interface_dict(conf, base) + _, loopback = get_interface_dict(conf, base) return loopback def verify(loopback): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 96fc1c41c..279dd119b 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -48,7 +48,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'macsec'] - macsec = get_interface_dict(conf, base) + ifname, macsec = get_interface_dict(conf, base) # Check if interface has been removed if 'deleted' in macsec: @@ -98,7 +98,7 @@ def verify(macsec): def generate(macsec): render(wpa_suppl_conf.format(**macsec), - 'macsec/wpa_supplicant.conf.tmpl', macsec) + 'macsec/wpa_supplicant.conf.j2', macsec) return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 83d1c6d9b..4750ca3e8 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -32,7 +32,7 @@ from shutil import rmtree from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect @@ -85,13 +85,12 @@ def get_config(config=None): tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - openvpn = get_interface_dict(conf, base) + ifname, openvpn = get_interface_dict(conf, base) if 'deleted' not in openvpn: openvpn['pki'] = tmp_pki - - tmp = leaf_node_changed(conf, ['openvpn-option']) - if tmp: openvpn['restart_required'] = '' + if is_node_changed(conf, base + [ifname, 'openvpn-option']): + openvpn.update({'restart_required': {}}) # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. @@ -608,7 +607,7 @@ def generate(openvpn): # Generate User/Password authentication file if 'authentication' in openvpn: - render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.tmpl', openvpn, + render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn, user=user, group=group, permission=0o600) else: # delete old auth file if present @@ -624,16 +623,16 @@ def generate(openvpn): # Our client need's to know its subnet mask ... client_config['server_subnet'] = dict_search('server.subnet', openvpn) - render(client_file, 'openvpn/client.conf.tmpl', client_config, + render(client_file, 'openvpn/client.conf.j2', client_config, 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, + render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) # Render 20-override.conf for OpenVPN service - render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn, + render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) # Reload systemd services config to apply an override call(f'systemctl daemon-reload') diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index bfb1fadd5..e2fdc7a42 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -22,7 +22,9 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configdict import leaf_node_changed +from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface from vyos.configverify import verify_interface_exists @@ -47,33 +49,17 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'pppoe'] - pppoe = get_interface_dict(conf, base) + ifname, pppoe = get_interface_dict(conf, base) # We should only terminate the PPPoE session if critical parameters change. # All parameters that can be changed on-the-fly (like interface description) # should not lead to a reconnect! - tmp = leaf_node_changed(conf, ['access-concentrator']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['connect-on-demand']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['service-name']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['source-interface']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['vrf']) - # leaf_node_changed() returns a list, as VRF is a non-multi node, there - # will be only one list element - if tmp: pppoe.update({'vrf_old': tmp[0]}) - - tmp = leaf_node_changed(conf, ['authentication', 'user']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['authentication', 'password']) - if tmp: pppoe.update({'shutdown_required': {}}) + for options in ['access-concentrator', 'connect-on-demand', 'service-name', + 'source-interface', 'vrf', 'no-default-route', 'authentication']: + if is_node_changed(conf, base + [ifname, options]): + pppoe.update({'shutdown_required': {}}) + # bail out early - no need to further process other nodes + break return pppoe @@ -106,7 +92,7 @@ def generate(pppoe): return None # Create PPP configuration files - render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o640) + render(config_pppoe, 'pppoe/peer.j2', pppoe, permission=0o640) return None @@ -120,7 +106,7 @@ def apply(pppoe): return None # reconnect should only be necessary when certain config options change, - # like ACS name, authentication, no-peer-dns, source-interface + # like ACS name, authentication ... (see get_config() for details) if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or 'shutdown_required' in pppoe): @@ -130,6 +116,9 @@ def apply(pppoe): p.remove() call(f'systemctl restart ppp@{ifname}.service') + # When interface comes "live" a hook is called: + # /etc/ppp/ip-up.d/99-vyos-pppoe-callback + # which triggers PPPoEIf.update() else: if os.path.isdir(f'/sys/class/net/{ifname}'): p = PPPoEIf(ifname) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index f2c85554f..1cd3fe276 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -18,7 +18,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -42,14 +42,14 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'pseudo-ethernet'] - peth = get_interface_dict(conf, base) + ifname, peth = get_interface_dict(conf, base) - mode = leaf_node_changed(conf, ['mode']) - if mode: peth.update({'mode_old' : mode}) + mode = is_node_changed(conf, ['mode']) + if mode: peth.update({'shutdown_required' : {}}) if 'source_interface' in peth: - peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], - peth['source_interface']) + _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], + peth['source_interface']) return peth def verify(peth): diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index f4668d976..eff7f373c 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -48,10 +48,10 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'tunnel'] - tunnel = get_interface_dict(conf, base) + ifname, tunnel = get_interface_dict(conf, base) if 'deleted' not in tunnel: - tmp = leaf_node_changed(conf, ['encapsulation']) + tmp = leaf_node_changed(conf, base + [ifname, 'encapsulation']) if tmp: tunnel.update({'encapsulation_changed': {}}) # We also need to inspect other configured tunnels as there are Kernel diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py index f06fdff1b..f4b0436af 100755 --- a/src/conf_mode/interfaces-vti.py +++ b/src/conf_mode/interfaces-vti.py @@ -36,7 +36,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'vti'] - vti = get_interface_dict(conf, base) + _, vti = get_interface_dict(conf, base) return vti def verify(vti): diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 0a9b51cac..f44d754ba 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -19,9 +19,11 @@ import os from sys import exit from netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 @@ -44,18 +46,19 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'vxlan'] - vxlan = get_interface_dict(conf, base) + ifname, vxlan = get_interface_dict(conf, base) # VXLAN interfaces are picky and require recreation if certain parameters # change. But a VXLAN interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? for cli_option in ['external', 'gpe', 'group', 'port', 'remote', - 'source-address', 'source-interface', 'vni', - 'parameters ip dont-fragment', 'parameters ip tos', - 'parameters ip ttl']: - if leaf_node_changed(conf, cli_option.split()): + 'source-address', 'source-interface', 'vni']: + if leaf_node_changed(conf, base + [ifname, cli_option]): vxlan.update({'rebuild_required': {}}) + if is_node_changed(conf, base + [ifname, 'parameters']): + vxlan.update({'rebuild_required': {}}) + # We need to verify that no other VXLAN tunnel is configured when external # mode is in use - Linux Kernel limitation conf.set_level(base) @@ -78,7 +81,7 @@ def verify(vxlan): return None if int(vxlan['mtu']) < 1500: - print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + Warning('RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') if 'group' in vxlan: if 'source_interface' not in vxlan: diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index b404375d6..180ffa507 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -46,17 +46,17 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'wireguard'] - wireguard = get_interface_dict(conf, base) + ifname, wireguard = get_interface_dict(conf, base) # Check if a port was changed - wireguard['port_changed'] = leaf_node_changed(conf, ['port']) + wireguard['port_changed'] = leaf_node_changed(conf, base + [ifname, 'port']) # Determine which Wireguard peer has been removed. # Peers can only be removed with their public key! dict = {} - tmp = node_changed(conf, ['peer'], key_mangling=('-', '_')) + tmp = node_changed(conf, base + [ifname, 'peer'], key_mangling=('-', '_')) for peer in (tmp or []): - public_key = leaf_node_changed(conf, ['peer', peer, 'public_key']) + public_key = leaf_node_changed(conf, base + [ifname, 'peer', peer, 'public_key']) if public_key: dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict) wireguard.update(dict) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 500952df1..d34297063 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -76,15 +76,19 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'wireless'] - wifi = get_interface_dict(conf, base) + ifname, wifi = get_interface_dict(conf, base) # Cleanup "delete" default values when required user selectable values are # not defined at all - tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + tmp = conf.get_config_dict(base + [ifname], key_mangling=('-', '_'), + get_first_key=True) if not (dict_search('security.wpa.passphrase', tmp) or dict_search('security.wpa.radius', tmp)): if 'deleted' not in wifi: del wifi['security']['wpa'] + # if 'security' key is empty, drop it too + if len(wifi['security']) == 0: + del wifi['security'] # defaults include RADIUS server specifics per TAG node which need to be # added to individual RADIUS servers instead - so we can simply delete them @@ -244,11 +248,11 @@ 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', + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi) elif wifi['type'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', wifi) return None diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 9a33039a3..e275ace84 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -21,7 +21,7 @@ from time import sleep from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_authentication from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mirror_redirect @@ -50,42 +50,36 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'wwan'] - wwan = get_interface_dict(conf, base) + ifname, wwan = get_interface_dict(conf, base) # We should only terminate the WWAN session if critical parameters change. # All parameters that can be changed on-the-fly (like interface description) # should not lead to a reconnect! - tmp = leaf_node_changed(conf, ['address']) + tmp = is_node_changed(conf, base + [ifname, 'address']) if tmp: wwan.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['apn']) + tmp = is_node_changed(conf, base + [ifname, 'apn']) if tmp: wwan.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['disable']) + tmp = is_node_changed(conf, base + [ifname, 'disable']) if tmp: wwan.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['vrf']) - # leaf_node_changed() returns a list, as VRF is a non-multi node, there - # will be only one list element - if tmp: wwan.update({'vrf_old': tmp[0]}) - - tmp = leaf_node_changed(conf, ['authentication', 'user']) + tmp = is_node_changed(conf, base + [ifname, 'vrf']) if tmp: wwan.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['authentication', 'password']) + tmp = is_node_changed(conf, base + [ifname, 'authentication']) if tmp: wwan.update({'shutdown_required': {}}) - tmp = leaf_node_changed(conf, ['ipv6', 'address', 'autoconf']) + tmp = is_node_changed(conf, base + [ifname, 'ipv6', 'address', 'autoconf']) if tmp: wwan.update({'shutdown_required': {}}) # We need to know the amount of other WWAN interfaces as ModemManager needs # to be started or stopped. conf.set_level(base) - wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) - ifname = wwan['ifname'] # This if-clause is just to be sure - it will always evaluate to true if ifname in wwan['other_interfaces']: del wwan['other_interfaces'][ifname] diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index db8328259..c703c1fe0 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -18,6 +18,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.validate import is_addr_assigned @@ -84,11 +85,11 @@ def verify(lldp): if 'management_address' in lldp: for address in lldp['management_address']: - message = f'WARNING: LLDP management address "{address}" is invalid' + message = f'LLDP management address "{address}" is invalid' if is_loopback_addr(address): - print(f'{message} - loopback address') + Warning(f'{message} - loopback address') elif not is_addr_assigned(address): - print(f'{message} - not assigned to any interface') + Warning(f'{message} - not assigned to any interface') if 'interface' in lldp: for interface, interface_config in lldp['interface'].items(): @@ -110,8 +111,8 @@ def generate(lldp): if lldp is None: return - render(config_file, 'lldp/lldpd.tmpl', lldp) - render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp) + render(config_file, 'lldp/lldpd.j2', lldp) + render(vyos_config_file, 'lldp/vyos.conf.j2', lldp) def apply(lldp): systemd_service = 'lldpd.service' diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 9f319fc8a..85819a77e 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -23,6 +23,7 @@ from platform import release as kernel_version from sys import exit from netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -142,14 +143,14 @@ def verify(nat): raise ConfigError(f'{err_msg} outbound-interface not specified') 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') + Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') addr = dict_search('translation.address', config) if addr != None: if addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): if not is_addr_assigned(ip): - print(f'WARNING: IP address {ip} does not exist on the system!') + Warning(f'IP address {ip} does not exist on the system!') elif 'exclude' not in config: raise ConfigError(f'{err_msg}\n' \ 'translation address not specified') @@ -167,7 +168,7 @@ def verify(nat): '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') + Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') if dict_search('translation.address', config) == None and 'exclude' not in config: @@ -180,13 +181,13 @@ def verify(nat): return None def generate(nat): - render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat) + render(nftables_nat_config, 'firewall/nftables-nat.j2', nat) # dry-run newly generated configuration tmp = run(f'nft -c -f {nftables_nat_config}') if tmp > 0: - if os.path.exists(nftables_ct_file): - os.unlink(nftables_ct_file) + if os.path.exists(nftables_nat_config): + os.unlink(nftables_nat_config) raise ConfigError('Configuration file errors encountered!') return None diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 8bf2e8073..0972151a0 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -21,6 +21,7 @@ import os from sys import exit from netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -117,12 +118,12 @@ def verify(nat): raise ConfigError(f'{err_msg} outbound-interface not specified') if config['outbound_interface'] not in interfaces(): - raise ConfigError(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + raise ConfigError(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') addr = dict_search('translation.address', config) if addr != None: if addr != 'masquerade' and not is_ipv6(addr): - raise ConfigError(f'Warning: IPv6 address {addr} is not a valid address') + raise ConfigError(f'IPv6 address {addr} is not a valid address') else: raise ConfigError(f'{err_msg} translation address not specified') @@ -140,13 +141,13 @@ def verify(nat): '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') + Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') return None def generate(nat): - render(nftables_nat66_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755) - render(ndppd_config, 'ndppd/ndppd.conf.tmpl', nat, permission=0o755) + render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755) + render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755) return None def apply(nat): diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 52070aabc..0d6ec9ace 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -56,8 +56,8 @@ def generate(ntp): if not ntp: return None - render(config_file, 'ntp/ntpd.conf.tmpl', ntp) - render(systemd_override, 'ntp/override.conf.tmpl', ntp) + render(config_file, 'ntp/ntpd.conf.j2', ntp) + render(systemd_override, 'ntp/override.conf.j2', ntp) return None diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 3d1d7d8c5..5de341beb 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -20,6 +20,7 @@ import re from json import loads from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.template import render from vyos.util import cmd @@ -135,7 +136,7 @@ def verify_rule(policy, name, rule_conf, ipv6): raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule') if not group_obj: - print(f'WARNING: {error_group} "{group_name}" has no members') + Warning(f'{error_group} "{group_name}" has no members') if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): if 'protocol' not in rule_conf: @@ -203,7 +204,7 @@ def generate(policy): else: policy['cleanup_commands'] = cleanup_commands(policy) - render(nftables_conf, 'firewall/nftables-policy.tmpl', policy) + render(nftables_conf, 'firewall/nftables-policy.j2', policy) return None def apply_table_marks(policy): diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 9d8fcfa36..ef6008140 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -177,7 +177,7 @@ def verify(policy): def generate(policy): if not policy: return None - policy['new_frr_config'] = render_to_string('frr/policy.frr.tmpl', policy) + policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy) return None def apply(policy): diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 4ebc0989c..0436abaf9 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -98,7 +98,7 @@ def verify(bfd): def generate(bfd): if not bfd: return None - bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.tmpl', bfd) + bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.j2', bfd) def apply(bfd): bfd_daemon = 'bfdd' diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index dace53d37..cd46cbcb4 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -138,13 +138,20 @@ def verify(bgp): if asn == bgp['local_as']: raise ConfigError('Cannot have local-as same as BGP AS number') + # Neighbor AS specified for local-as and remote-as can not be the same + if dict_search('remote_as', peer_config) == asn: + raise ConfigError(f'Neighbor "{peer}" has local-as specified which is '\ + 'the same as remote-as, this is not allowed!') + # ttl-security and ebgp-multihop can't be used in the same configration if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config: raise ConfigError('You can not set both ebgp-multihop and ttl-security hops') - # Check if neighbor has both override capability and strict capability match configured at the same time. + # Check if neighbor has both override capability and strict capability match + # configured at the same time. if 'override_capability' in peer_config and 'strict_capability_match' in peer_config: - raise ConfigError(f'Neighbor "{peer}" cannot have both override-capability and strict-capability-match configured at the same time!') + raise ConfigError(f'Neighbor "{peer}" cannot have both override-capability and '\ + 'strict-capability-match configured at the same time!') # Check spaces in the password if 'password' in peer_config and ' ' in peer_config['password']: @@ -157,6 +164,22 @@ def verify(bgp): if not verify_remote_as(peer_config, bgp): raise ConfigError(f'Neighbor "{peer}" remote-as must be set!') + # Peer-group member cannot override remote-as of peer-group + if 'peer_group' in peer_config: + peer_group = peer_config['peer_group'] + if 'remote_as' in peer_config and 'remote_as' in bgp['peer_group'][peer_group]: + raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') + if 'interface' in peer_config: + if 'peer_group' in peer_config['interface']: + peer_group = peer_config['interface']['peer_group'] + if 'remote_as' in peer_config['interface'] and 'remote_as' in bgp['peer_group'][peer_group]: + raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') + if 'v6only' in peer_config['interface']: + if 'peer_group' in peer_config['interface']['v6only']: + peer_group = peer_config['interface']['v6only']['peer_group'] + if 'remote_as' in peer_config['interface']['v6only'] and 'remote_as' in bgp['peer_group'][peer_group]: + raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') + # Only checks for ipv4 and ipv6 neighbors # Check if neighbor address is assigned as system interface address vrf = None @@ -297,9 +320,9 @@ def generate(bgp): if not bgp or 'deleted' in bgp: return None - bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.tmpl - bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', bgp) - bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.tmpl', bgp) + bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.j2 + bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', bgp) + bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp) return None diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 28d560d03..65cc2beba 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -108,7 +108,7 @@ def generate(igmp): if igmp is None: return None - render(config_file, 'frr/igmp.frr.tmpl', igmp) + render(config_file, 'frr/igmp.frr.j2', igmp) return None def apply(igmp): diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index f2501e38a..5dafd26d0 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -210,9 +210,9 @@ def generate(isis): if not isis or 'deleted' in isis: return None - isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl - isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', isis) - isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis) + isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.j2 + isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', isis) + isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis) return None def apply(isis): diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 933e23065..5da8e7b06 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -68,7 +68,7 @@ def generate(mpls): if not mpls or 'deleted' in mpls: return None - mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls) + mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.j2', mpls) return None def apply(mpls): diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 7eeb5cd30..92b335085 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -84,7 +84,7 @@ def verify(nhrp): return None def generate(nhrp): - render(opennhrp_conf, 'nhrp/opennhrp.conf.tmpl', nhrp) + render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp) return None def apply(nhrp): @@ -104,7 +104,7 @@ def apply(nhrp): if rule_handle: remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle) - action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' + action = 'reload-or-restart' if nhrp and 'tunnel' in nhrp else 'stop' run(f'systemctl {action} opennhrp') return None diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 26d491838..5b4874ba2 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -160,7 +160,7 @@ def verify(ospf): route_map_name = dict_search('default_information.originate.route_map', ospf) if route_map_name: verify_route_map(route_map_name, ospf) - # Validate if configured Access-list exists + # Validate if configured Access-list exists if 'area' in ospf: for area, area_config in ospf['area'].items(): if 'import_list' in area_config: @@ -204,9 +204,9 @@ def generate(ospf): if not ospf or 'deleted' in ospf: return None - ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.tmpl - ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', ospf) - ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.tmpl', ospf) + ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.j2 + ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', ospf) + ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) return None def apply(ospf): diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index f8e733ba5..ee4eaf59d 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -142,7 +142,7 @@ def generate(ospfv3): if not ospfv3 or 'deleted' in ospfv3: return None - ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.tmpl', ospfv3) + ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3) return None def apply(ospfv3): diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index df2e6f941..78df9b6f8 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -135,7 +135,7 @@ def generate(pim): if pim is None: return None - render(config_file, 'frr/pimd.frr.tmpl', pim) + render(config_file, 'frr/pimd.frr.j2', pim) return None def apply(pim): diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 300f56489..a76c1ce76 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -102,7 +102,7 @@ def generate(rip): if not rip or 'deleted' in rip: return None - rip['new_frr_config'] = render_to_string('frr/ripd.frr.tmpl', rip) + rip['new_frr_config'] = render_to_string('frr/ripd.frr.j2', rip) return None def apply(rip): diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index d9b8c0b30..21ff710b3 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -93,7 +93,7 @@ def generate(ripng): ripng['new_frr_config'] = '' return None - ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.tmpl', ripng) + ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.j2', ripng) return None def apply(ripng): diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 51ad0d315..62ea9c878 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -81,7 +81,7 @@ def verify(rpki): def generate(rpki): if not rpki: return - rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki) + rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki) return None def apply(rpki): diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index f0ec48de4..58e202928 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -22,6 +22,7 @@ from sys import argv from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import get_dhcp_interfaces +from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf from vyos.template import render_to_string @@ -59,7 +60,9 @@ def get_config(config=None): # T3680 - get a list of all interfaces currently configured to use DHCP tmp = get_dhcp_interfaces(conf, vrf) - if tmp: static['dhcp'] = tmp + if tmp: static.update({'dhcp' : tmp}) + tmp = get_pppoe_interfaces(conf, vrf) + if tmp: static.update({'pppoe' : tmp}) return static @@ -91,7 +94,7 @@ def verify(static): def generate(static): if not static: return None - static['new_frr_config'] = render_to_string('frr/staticd.frr.tmpl', static) + static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) return None def apply(static): diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py index 99157835a..6afdf31f3 100755 --- a/src/conf_mode/protocols_static_multicast.py +++ b/src/conf_mode/protocols_static_multicast.py @@ -96,7 +96,7 @@ def generate(mroute): if mroute is None: return None - render(config_file, 'frr/static_mcast.frr.tmpl', mroute) + render(config_file, 'frr/static_mcast.frr.j2', mroute) return None def apply(mroute): diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index 841bf6a39..00b889a11 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,14 +16,18 @@ import os -from copy import deepcopy from socket import gethostname from sys import exit from urllib3 import PoolManager +from vyos.base import Warning from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configverify import verify_interface_exists from vyos.template import render -from vyos.util import call, chown +from vyos.util import call +from vyos.util import chown +from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -32,20 +36,10 @@ airbag.enable() config_file = r'/etc/salt/minion' master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub' -default_config_data = { - 'hash': 'sha256', - 'log_level': 'warning', - 'master' : 'salt', - 'user': 'minion', - 'group': 'vyattacfg', - 'salt_id': gethostname(), - 'mine_interval': '60', - 'verify_master_pubkey_sign': 'false', - 'master_key': '' -} +user='minion' +group='vyattacfg' def get_config(config=None): - salt = deepcopy(default_config_data) if config: conf = config else: @@ -54,44 +48,44 @@ def get_config(config=None): if not conf.exists(base): return None - else: - conf.set_level(base) - if conf.exists(['hash']): - salt['hash'] = conf.return_value(['hash']) + salt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # ID default is dynamic thus we can not use defaults() + if 'id' not in salt: + salt['id'] = gethostname() + # 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) + salt = dict_merge(default_values, salt) - if conf.exists(['master']): - salt['master'] = conf.return_values(['master']) - - if conf.exists(['id']): - salt['salt_id'] = conf.return_value(['id']) + if not conf.exists(base): + return None + else: + conf.set_level(base) - if conf.exists(['user']): - salt['user'] = conf.return_value(['user']) + return salt - if conf.exists(['interval']): - salt['interval'] = conf.return_value(['interval']) +def verify(salt): + if not salt: + return None - if conf.exists(['master-key']): - salt['master_key'] = conf.return_value(['master-key']) - salt['verify_master_pubkey_sign'] = 'true' + if 'hash' in salt and salt['hash'] == 'sha1': + Warning('Do not use sha1 hashing algorithm, upgrade to sha256 or later!') - return salt + if 'source_interface' in salt: + verify_interface_exists(salt['source_interface']) -def verify(salt): return None def generate(salt): if not salt: return None - render(config_file, 'salt-minion/minion.tmpl', salt, - user=salt['user'], group=salt['group']) + render(config_file, 'salt-minion/minion.j2', salt, user=user, group=group) if not os.path.exists(master_keyfile): - if salt['master_key']: + if 'master_key' in salt: req = PoolManager().request('GET', salt['master_key'], preload_content=False) - with open(master_keyfile, 'wb') as f: while True: data = req.read(1024) @@ -100,18 +94,19 @@ def generate(salt): f.write(data) req.release_conn() - chown(master_keyfile, salt['user'], salt['group']) + chown(master_keyfile, user, group) return None def apply(salt): + service_name = 'salt-minion.service' if not salt: # Salt removed from running config - call('systemctl stop salt-minion.service') + call(f'systemctl stop {service_name}') if os.path.exists(config_file): os.unlink(config_file) else: - call('systemctl restart salt-minion.service') + call(f'systemctl restart {service_name}') return None diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index 51050e702..a2e411e49 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -81,7 +81,7 @@ def generate(proxy): if not proxy: return None - render(config_file, 'conserver/conserver.conf.tmpl', proxy) + render(config_file, 'conserver/conserver.conf.j2', proxy) if 'device' in proxy: for device, device_config in proxy['device'].items(): if 'ssh' not in device_config: @@ -92,7 +92,7 @@ def generate(proxy): 'port' : device_config['ssh']['port'], } render(dropbear_systemd_file.format(**tmp), - 'conserver/dropbear@.service.tmpl', tmp) + 'conserver/dropbear@.service.j2', tmp) return None diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index 67edeb630..ae7e582ec 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -67,8 +67,8 @@ def generate(fastnetmon): return - render(config_file, 'ids/fastnetmon.tmpl', fastnetmon) - render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon) + render(config_file, 'ids/fastnetmon.j2', fastnetmon) + render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) return None diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 2ebee8018..559d1bcd5 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -296,10 +296,10 @@ def generate(ipoe): if not ipoe: return None - render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe) + render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) if ipoe['auth_mode'] == 'local': - render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe) + render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe) os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index d31a0c49e..2383a53fb 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2020 VyOS maintainers and contributors +# Copyright (C) 2017-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -92,7 +92,7 @@ def generate(mdns): if len(mdns['interface']) < 2: return None - render(config_file, 'mdns-repeater/avahi-daemon.tmpl', mdns) + render(config_file, 'mdns-repeater/avahi-daemon.j2', mdns) return None def apply(mdns): diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 8a972b9fe..102a87318 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -99,6 +99,15 @@ def get_config(config=None): monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) monitoring['nft_chains'] = get_nft_filter_chains() + if 'authentication' in monitoring or \ + 'url' in monitoring: + monitoring['influxdb_configured'] = True + + # Ignore default XML values if config doesn't exists + # Delete key from dict + if not conf.exists(base + ['prometheus-client']): + del monitoring['prometheus_client'] + return monitoring def verify(monitoring): @@ -106,13 +115,23 @@ def verify(monitoring): if not monitoring: return None - if 'authentication' not in monitoring or \ - 'organization' not in monitoring['authentication'] or \ - 'token' not in monitoring['authentication']: - raise ConfigError(f'Authentication "organization and token" are mandatory!') + if 'influxdb_configured' in monitoring: + if 'authentication' not in monitoring or \ + 'organization' not in monitoring['authentication'] or \ + 'token' not in monitoring['authentication']: + raise ConfigError(f'Authentication "organization and token" are mandatory!') + + if 'url' not in monitoring: + raise ConfigError(f'Monitoring "url" is mandatory!') + + # Verify Splunk + if 'splunk' in monitoring: + if 'authentication' not in monitoring['splunk'] or \ + 'token' not in monitoring['splunk']['authentication']: + raise ConfigError(f'Authentication "organization and token" are mandatory!') - if 'url' not in monitoring: - raise ConfigError(f'Monitoring "url" is mandatory!') + if 'url' not in monitoring['splunk']: + raise ConfigError(f'Monitoring splunk "url" is mandatory!') return None @@ -145,10 +164,10 @@ def generate(monitoring): os.mkdir(custom_scripts_dir) # Render telegraf configuration and systemd override - render(config_telegraf, 'monitoring/telegraf.tmpl', monitoring) - render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.tmpl', monitoring) - render(systemd_override, 'monitoring/override.conf.tmpl', monitoring, permission=0o640) - render(syslog_telegraf, 'monitoring/syslog_telegraf.tmpl', monitoring) + render(config_telegraf, 'monitoring/telegraf.j2', monitoring) + render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.j2', monitoring) + render(systemd_override, 'monitoring/override.conf.j2', monitoring, permission=0o640) + render(syslog_telegraf, 'monitoring/syslog_telegraf.j2', monitoring) chown(base_dir, 'telegraf', 'telegraf') diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 1f31d132d..6086ef859 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -88,10 +88,10 @@ def generate(pppoe): for vlan_range in pppoe['interface'][iface]['vlan_range']: pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range)) - render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe) + render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe) if dict_search('authentication.mode', pppoe) == 'local': - render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', + render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2', pppoe, permission=0o640) else: if os.path.exists(pppoe_chap_secrets): diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 9afcdd63e..71b758399 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -101,7 +101,7 @@ def generate(rtradv): if not rtradv: return None - render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, permission=0o644) + render(config_file, 'router-advert/radvd.conf.j2', rtradv, permission=0o644) return None def apply(rtradv): diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py index d21b31990..36f3e18a7 100755 --- a/src/conf_mode/service_upnp.py +++ b/src/conf_mode/service_upnp.py @@ -135,7 +135,7 @@ def generate(upnpd): if os.path.isfile(config_file): os.unlink(config_file) - render(config_file, 'firewall/upnpd.conf.tmpl', upnpd) + render(config_file, 'firewall/upnpd.conf.j2', upnpd) def apply(upnpd): systemd_service_name = 'miniupnpd.service' diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index a16cc4aeb..32af31bde 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -61,7 +61,7 @@ def generate_sg_localdb(category, list_type, role, proxy): user=user_group, group=user_group) # temporary config file, deleted after generation - render(sg_tmp_file, 'squid/sg_acl.conf.tmpl', tmp, + render(sg_tmp_file, 'squid/sg_acl.conf.j2', tmp, user=user_group, group=user_group) call(f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"') @@ -166,8 +166,8 @@ def generate(proxy): if not proxy: return None - render(squid_config_file, 'squid/squid.conf.tmpl', proxy) - render(squidguard_config_file, 'squid/squidGuard.conf.tmpl', proxy) + render(squid_config_file, 'squid/squid.conf.j2', proxy) + render(squidguard_config_file, 'squid/squidGuard.conf.j2', proxy) cat_dict = { 'local-block' : 'domains', diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 8ce48780b..ae060580d 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -18,6 +18,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf @@ -57,9 +58,6 @@ def get_config(config=None): if conf.exists(['service', 'lldp', 'snmp', 'enable']): snmp.update({'lldp_snmp' : ''}) - if conf.exists(['system', 'ipv6', 'disable']): - snmp.update({'ipv6_disabled' : ''}) - if 'deleted' in snmp: return snmp @@ -100,9 +98,8 @@ def get_config(config=None): snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) if '::1' not in snmp['listen_address']: - if 'ipv6_disabled' not in snmp: - tmp = {'::1': {'port': '161'}} - snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) + tmp = {'::1': {'port': '161'}} + snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) if 'community' in snmp: default_values = defaults(base + ['community']) @@ -153,7 +150,7 @@ def verify(snmp): tmp = extension_opt['script'] if not os.path.isfile(tmp): - print(f'WARNING: script "{tmp}" does not exist!') + Warning(f'script "{tmp}" does not exist!') else: chmod_755(extension_opt['script']) @@ -162,7 +159,7 @@ def verify(snmp): # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist if not is_addr_assigned(address): - print(f'WARNING: SNMP listen address "{address}" not configured!') + Warning(f'SNMP listen address "{address}" not configured!') if 'trap_target' in snmp: for trap, trap_config in snmp['trap_target'].items(): @@ -273,15 +270,15 @@ def generate(snmp): call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null') # Write client config file - render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) + render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp) # Write server config file - render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp) + render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp) # Write access rights config file - render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp) + render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp) # Write access rights config file - render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp) + render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp) # Write daemon configuration file - render(systemd_override, 'snmp/override.conf.tmpl', snmp) + render(systemd_override, 'snmp/override.conf.j2', snmp) return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 67724b043..487e8c229 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -84,8 +84,8 @@ def generate(ssh): syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!') call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') - render(config_file, 'ssh/sshd_config.tmpl', ssh) - render(systemd_override, 'ssh/override.conf.tmpl', ssh) + render(config_file, 'ssh/sshd_config.j2', ssh) + render(systemd_override, 'ssh/override.conf.j2', ssh) # Reload systemd manager configuration call('systemctl daemon-reload') diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index c9c6aa187..c717286ae 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -197,7 +197,7 @@ def generate(login): pass if 'radius' in login: - render(radius_config_file, 'login/pam_radius_auth.conf.tmpl', login, + render(radius_config_file, 'login/pam_radius_auth.conf.j2', login, permission=0o600, user='root', group='root') else: if os.path.isfile(radius_config_file): @@ -241,7 +241,7 @@ def apply(login): # # XXX: Should we deny using root at all? home_dir = getpwnam(user).pw_dir - render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl', + render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2', user_config, permission=0o600, formater=lambda _: _.replace(""", '"'), user=user, group='users') diff --git a/src/conf_mode/system-logs.py b/src/conf_mode/system-logs.py index e6296656d..c71938a79 100755 --- a/src/conf_mode/system-logs.py +++ b/src/conf_mode/system-logs.py @@ -57,13 +57,13 @@ def generate(logs_config): logrotate_atop = dict_search('logrotate.atop', logs_config) # generate new config file for atop syslog.debug('Adding logrotate config for atop') - render(logrotate_atop_file, 'logs/logrotate/vyos-atop.tmpl', logrotate_atop) + render(logrotate_atop_file, 'logs/logrotate/vyos-atop.j2', logrotate_atop) # get configuration for logrotate rsyslog logrotate_rsyslog = dict_search('logrotate.messages', logs_config) # generate new config file for rsyslog syslog.debug('Adding logrotate config for rsyslog') - render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.tmpl', + render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.j2', logrotate_rsyslog) diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index b1c63e316..36dbf155b 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -74,8 +74,8 @@ def verify(options): return None def generate(options): - render(curlrc_config, 'system/curlrc.tmpl', options) - render(ssh_config, 'system/ssh_config.tmpl', options) + render(curlrc_config, 'system/curlrc.j2', options) + render(ssh_config, 'system/ssh_config.j2', options) return None def apply(options): diff --git a/src/conf_mode/system-proxy.py b/src/conf_mode/system-proxy.py index 02536c2ab..079c43e7e 100755 --- a/src/conf_mode/system-proxy.py +++ b/src/conf_mode/system-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,83 +13,59 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import re -from vyos import ConfigError -from vyos.config import Config +from sys import exit +from vyos.config import Config +from vyos.template import render +from vyos import ConfigError from vyos import airbag airbag.enable() proxy_def = r'/etc/profile.d/vyos-system-proxy.sh' - -def get_config(): - c = Config() - if not c.exists('system proxy'): +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'proxy'] + if not conf.exists(base): return None - c.set_level('system proxy') + proxy = conf.get_config_dict(base, get_first_key=True) + return proxy - cnf = { - 'url': None, - 'port': None, - 'usr': None, - 'passwd': None - } +def verify(proxy): + if not proxy: + return - if c.exists('url'): - cnf['url'] = c.return_value('url') - if c.exists('port'): - cnf['port'] = c.return_value('port') - if c.exists('username'): - cnf['usr'] = c.return_value('username') - if c.exists('password'): - cnf['passwd'] = c.return_value('password') + if 'url' not in proxy or 'port' not in proxy: + raise ConfigError('Proxy URL and port require a value') - return cnf + if ('username' in proxy and 'password' not in proxy) or \ + ('username' not in proxy and 'password' in proxy): + raise ConfigError('Both username and password need to be defined!') +def generate(proxy): + if not proxy: + if os.path.isfile(proxy_def): + os.unlink(proxy_def) + return -def verify(c): - if not c: - return None - if not c['url'] or not c['port']: - raise ConfigError("proxy url and port requires a value") - elif c['usr'] and not c['passwd']: - raise ConfigError("proxy password requires a value") - elif not c['usr'] and c['passwd']: - raise ConfigError("proxy username requires a value") - + render(proxy_def, 'system/proxy.j2', proxy, permission=0o755) -def generate(c): - if not c: - return None - if not c['usr']: - return str("export http_proxy={url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy" - .format(url=c['url'], port=c['port'])) - else: - return str("export http_proxy=http://{usr}:{passwd}@{url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy" - .format(url=re.sub('http://', '', c['url']), port=c['port'], usr=c['usr'], passwd=c['passwd'])) - - -def apply(ln): - if not ln and os.path.exists(proxy_def): - os.remove(proxy_def) - else: - open(proxy_def, 'w').write( - "# generated by system-proxy.py\n{}\n".format(ln)) +def apply(proxy): + pass if __name__ == '__main__': try: c = get_config() verify(c) - ln = generate(c) - apply(ln) + generate(c) + apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 309b4bdb0..a9d3bbe31 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -204,7 +204,7 @@ def generate(c): return None conf = '/etc/rsyslog.d/vyos-rsyslog.conf' - render(conf, 'syslog/rsyslog.conf.tmpl', c) + render(conf, 'syslog/rsyslog.conf.j2', c) # cleanup current logrotate config files logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*') @@ -216,7 +216,7 @@ def generate(c): for filename, fileconfig in c.get('files', {}).items(): if fileconfig['log-file'].startswith('/var/log/user/'): conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename - render(conf, 'syslog/logrotate.tmpl', { 'config_render': fileconfig }) + render(conf, 'syslog/logrotate.j2', { 'config_render': fileconfig }) def verify(c): diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 19b252513..86985d765 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -103,7 +103,7 @@ def generate(console): config_file = base_dir + f'/serial-getty@{device}.service' getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' - render(config_file, 'getty/serial-getty.service.tmpl', device_config) + render(config_file, 'getty/serial-getty.service.j2', device_config) os.symlink(config_file, getty_wants_symlink) # GRUB diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py index b5ce32beb..3341dd738 100755 --- a/src/conf_mode/system_lcd.py +++ b/src/conf_mode/system_lcd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -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) + render(lcdd_conf, 'lcd/LCDd.conf.j2', lcd) # Render config file for client lcdproc - render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd) + render(lcdproc_conf, 'lcd/lcdproc.conf.j2', lcd) return None diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py index 4f16d1ed6..2e0004ffa 100755 --- a/src/conf_mode/system_sysctl.py +++ b/src/conf_mode/system_sysctl.py @@ -50,7 +50,7 @@ def generate(sysctl): os.unlink(config_file) return None - render(config_file, 'system/sysctl.conf.tmpl', sysctl) + render(config_file, 'system/sysctl.conf.j2', sysctl) return None def apply(sysctl): diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index ef726670c..c5daccb7f 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -22,6 +22,7 @@ from copy import deepcopy from glob import glob from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf @@ -68,8 +69,8 @@ def verify(tftpd): for address, address_config in tftpd['listen_address'].items(): if not is_addr_assigned(address): - print(f'WARNING: TFTP server listen address "{address}" not ' \ - 'assigned to any interface!') + Warning(f'TFTP server listen address "{address}" not ' \ + 'assigned to any interface!') verify_vrf(address_config) return None @@ -97,7 +98,7 @@ def generate(tftpd): config['vrf'] = address_config['vrf'] file = config_file + str(idx) - render(file, 'tftp-server/default.tmpl', config) + render(file, 'tftp-server/default.j2', config) idx = idx + 1 return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 99b82ca2d..bad9cfbd8 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -503,7 +503,7 @@ def generate(ipsec): charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) - render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes}) + render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes}) return if ipsec['dhcp_no_address']: @@ -553,25 +553,27 @@ def generate(ipsec): if not local_prefixes or not remote_prefixes: continue - passthrough = [] + passthrough = None for local_prefix in local_prefixes: for remote_prefix in remote_prefixes: local_net = ipaddress.ip_network(local_prefix) remote_net = ipaddress.ip_network(remote_prefix) if local_net.overlaps(remote_net): + if passthrough is None: + passthrough = [] passthrough.append(local_prefix) ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough - render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', ipsec) - render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec) - render(charon_conf, 'ipsec/charon.tmpl', ipsec) - render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec) - render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec) - render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec) - render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec) + render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec) + render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec) + render(charon_conf, 'ipsec/charon.j2', ipsec) + render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) + render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) + render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec) + render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec) def resync_nhrp(ipsec): if ipsec and not ipsec['nhrp_exists']: diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 818e8fa0b..fd5a4acd8 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -358,10 +358,10 @@ def generate(l2tp): if not l2tp: return None - render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp) + render(l2tp_conf, 'accel-ppp/l2tp.config.j2', l2tp) if l2tp['auth_mode'] == 'local': - render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp) + render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.j2', l2tp) os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 51ea1f223..8e0e30bbf 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -24,6 +24,7 @@ from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import is_systemd_service_running +from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 @@ -35,6 +36,7 @@ airbag.enable() cfg_dir = '/run/ocserv' ocserv_conf = cfg_dir + '/ocserv.conf' ocserv_passwd = cfg_dir + '/ocpasswd' +ocserv_otp_usr = cfg_dir + '/users.oath' radius_cfg = cfg_dir + '/radiusclient.conf' radius_servers = cfg_dir + '/radius_servers' @@ -54,6 +56,16 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + del ocserv['authentication']['local_users']['username']['otp'] + if not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at least one user') + default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] + for user, params in ocserv['authentication']['local_users']['username'].items(): + # Not every configuration requires OTP settings + if ocserv['authentication']['local_users']['username'][user].get('otp'): + ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) + if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) @@ -63,17 +75,34 @@ def get_config(): def verify(ocserv): if ocserv is None: return None - # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: if "local" in ocserv["authentication"]["mode"]: - if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('openconnect mode local required at leat one user') + if "radius" in ocserv["authentication"]["mode"]: + raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration') + if not ocserv["authentication"]["local_users"]: + raise ConfigError('openconnect mode local required at least one user') + if not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at least one user') else: - for user in ocserv["authentication"]["local_users"]["username"]: - if not "password" in ocserv["authentication"]["local_users"]["username"][user]: - raise ConfigError(f'password required for user {user}') + # For OTP mode: verify that each local user has an OTP key + if "otp" in ocserv["authentication"]["mode"]["local"]: + users_wo_key = [] + for user, user_config in ocserv["authentication"]["local_users"]["username"].items(): + # User has no OTP key defined + if dict_search('otp.key', user_config) == None: + users_wo_key.append(user) + if users_wo_key: + raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}') + # For password (and default) mode: verify that each local user has password + if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]: + users_wo_pswd = [] + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + users_wo_pswd.append(user) + if users_wo_pswd: + raise ConfigError(f'password required for users:\n{users_wo_pswd}') else: raise ConfigError('openconnect authentication mode required') else: @@ -122,22 +151,57 @@ def verify(ocserv): else: raise ConfigError('openconnect network settings required') - def generate(ocserv): if not ocserv: return None if "radius" in ocserv["authentication"]["mode"]: # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"]) + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"]) # Render radius servers - render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"]) + render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) + elif "local" in ocserv["authentication"]["mode"]: + # if mode "OTP", generate OTP users file parameters + if "otp" in ocserv["authentication"]["mode"]["local"]: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + # OTP token type from CLI parameters: + otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval")) + token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type") + otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length")) + if token_type == "hotp-time": + otp_type = "HOTP/T" + otp_interval + elif token_type == "hotp-event": + otp_type = "HOTP/E" + else: + otp_type = "HOTP/T" + otp_interval + ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length + # if there is a password, generate hash + if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]: + 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"]) + + if "password-otp" in ocserv["authentication"]["mode"]["local"]: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) + # Render local users OTP keys + render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"]) + elif "password" in ocserv["authentication"]["mode"]["local"]: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) + elif "otp" in ocserv["authentication"]["mode"]["local"]: + # Render local users OTP keys + render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"]) + else: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) 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"]) + render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) if "ssl" in ocserv: cert_file_path = os.path.join(cfg_dir, 'cert.pem') @@ -163,13 +227,13 @@ def generate(ocserv): f.write(wrap_certificate(pki_ca_cert['certificate'])) # Render config - render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv) + render(ocserv_conf, 'ocserv/ocserv_config.j2', ocserv) def apply(ocserv): if not ocserv: call('systemctl stop ocserv.service') - for file in [ocserv_conf, ocserv_passwd]: + for file in [ocserv_conf, ocserv_passwd, ocserv_otp_usr]: if os.path.exists(file): os.unlink(file) else: diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 30abe4782..7550c411e 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) + render(pptp_conf, 'accel-ppp/pptp.config.j2', pptp) if pptp['local_users']: - render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp) + render(pptp_chap_secrets, 'accel-ppp/chap-secrets.j2', 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 68980e5ab..db53463cf 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -114,7 +114,7 @@ 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) + render(sstp_conf, 'accel-ppp/sstp.config.j2', sstp) cert_name = sstp['ssl']['certificate'] pki_cert = sstp['pki']['certificate'][cert_name] @@ -127,7 +127,7 @@ def generate(sstp): write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate'])) if dict_search('authentication.mode', sstp) == 'local': - render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', + render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2', sstp, permission=0o640) else: if os.path.exists(sstp_chap_secrets): diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index f79c8a21e..972d0289b 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -83,7 +83,8 @@ def get_config(config=None): conf = Config() base = ['vrf'] - vrf = conf.get_config_dict(base, get_first_key=True) + vrf = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, get_first_key=True) # determine which VRF has been removed for name in node_changed(conf, base + ['name']): @@ -133,10 +134,10 @@ def verify(vrf): def generate(vrf): - render(config_file, 'vrf/vrf.conf.tmpl', vrf) + render(config_file, 'vrf/vrf.conf.j2', vrf) # Render nftables zones config - render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf) + render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) return None @@ -152,7 +153,7 @@ def apply(vrf): # set the default VRF global behaviour bind_all = '0' - if 'bind-to-all' in vrf: + if 'bind_to_all' in vrf: bind_all = '1' sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all) sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all) @@ -222,6 +223,15 @@ def apply(vrf): # add VRF description if available vrf_if.set_alias(config.get('description', '')) + # Enable/Disable IPv4 forwarding + tmp = dict_search('ip.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + vrf_if.set_ipv4_forwarding(value) + # Enable/Disable IPv6 forwarding + tmp = dict_search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + vrf_if.set_ipv6_forwarding(value) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 1a7bd1f09..585fdbebf 100755 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -40,7 +40,7 @@ def verify(vrf): return None def generate(vrf): - vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf) + vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.j2', vrf) return None def apply(vrf): diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index dc0617353..070a4deea 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -192,7 +192,7 @@ def generate(zone_policy): if 'local_zone' in zone_conf: zone_conf['from_local'] = get_local_from(data, zone) - render(nftables_conf, 'zone_policy/nftables.tmpl', data) + render(nftables_conf, 'zone_policy/nftables.j2', data) return None def apply(zone_policy): |