diff options
Diffstat (limited to 'src/conf_mode')
59 files changed, 522 insertions, 1226 deletions
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 2a77540f7..9c43640a9 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -20,7 +20,6 @@ import re from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.firewall import find_nftables_rule from vyos.firewall import remove_nftables_rule from vyos.utils.process import process_named_running @@ -28,7 +27,6 @@ from vyos.utils.dict import dict_search from vyos.utils.process import cmd from vyos.utils.process import run from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -77,16 +75,8 @@ def get_config(config=None): base = ['system', 'conntrack'] conntrack = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if 'timeout' in default_values and 'custom' in default_values['timeout']: - del default_values['timeout']['custom'] - conntrack = dict_merge(default_values, conntrack) + get_first_key=True, + with_recursive_defaults=True) return conntrack diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 6a4d102f7..4fb2ce27f 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -18,7 +18,6 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configverify import verify_interface_exists from vyos.utils.dict import dict_search from vyos.utils.process import process_named_running @@ -28,7 +27,6 @@ from vyos.utils.process import run from vyos.template import render from vyos.template import get_ipv4 from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -50,11 +48,7 @@ def get_config(config=None): return None conntrack = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - conntrack = dict_merge(default_values, conntrack) + get_first_key=True, with_defaults=True) conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize') conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max') diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 3378aac63..478868a9a 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -33,11 +33,12 @@ from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.process import rc_cmd +from vyos.template import bracketize_ipv6 from vyos.template import inc_ip from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.template import render -from vyos.xml import defaults +from vyos.xml_ref import default_value from vyos import ConfigError from vyos import airbag airbag.enable() @@ -66,58 +67,26 @@ def get_config(config=None): base = ['container'] container = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - # container base default values can not be merged here - remove and add them later - if 'name' in default_values: - del default_values['name'] - # registry will be handled below - if 'registry' in default_values: - del default_values['registry'] - container = dict_merge(default_values, container) - - # Merge per-container default values - if 'name' in container: - default_values = defaults(base + ['name']) - if 'port' in default_values: - del default_values['port'] - if 'volume' in default_values: - del default_values['volume'] - for name in container['name']: - container['name'][name] = dict_merge(default_values, container['name'][name]) - - # T5047: Any container related configuration changed? We only - # wan't to restart the required containers and not all of them ... - tmp = is_node_changed(conf, base + ['name', name]) - if tmp: - if 'container_restart' not in container: - container['container_restart'] = [name] - else: - container['container_restart'].append(name) - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if 'port' in container['name'][name]: - for port in container['name'][name]['port']: - default_values_port = defaults(base + ['name', 'port']) - container['name'][name]['port'][port] = dict_merge( - default_values_port, container['name'][name]['port'][port]) - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if 'volume' in container['name'][name]: - for volume in container['name'][name]['volume']: - default_values_volume = defaults(base + ['name', 'volume']) - container['name'][name]['volume'][volume] = dict_merge( - default_values_volume, container['name'][name]['volume'][volume]) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) + + for name in container.get('name', []): + # T5047: Any container related configuration changed? We only + # wan't to restart the required containers and not all of them ... + tmp = is_node_changed(conf, base + ['name', name]) + if tmp: + if 'container_restart' not in container: + container['container_restart'] = [name] + else: + container['container_restart'].append(name) # registry is a tagNode with default values - merge the list from # default_values['registry'] into the tagNode variables if 'registry' not in container: container.update({'registry' : {}}) - default_values = defaults(base) - for registry in default_values['registry'].split(): + default_values = default_value(base + ['registry']) + for registry in default_values: tmp = {registry : {}} container['registry'] = dict_merge(tmp, container['registry']) @@ -312,6 +281,14 @@ def generate_run_arguments(name, container_config): protocol = container_config['port'][portmap]['protocol'] sport = container_config['port'][portmap]['source'] dport = container_config['port'][portmap]['destination'] + listen_addresses = container_config['port'][portmap].get('listen_address', []) + + # If listen_addresses is not empty, include them in the publish command + if listen_addresses: + for listen_address in listen_addresses: + port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}' + else: + # If listen_addresses is empty, just include the standard publish command port += f' --publish {sport}:{dport}/{protocol}' # Bind volume diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index fd39bd9fe..37d708847 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -20,12 +20,10 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.base import Warning from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -41,11 +39,9 @@ def get_config(config=None): if not conf.exists(base): return None - relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - relay = dict_merge(default_values, relay) + relay = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return relay diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 3ea708902..c4c72aae9 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -23,14 +23,12 @@ from netaddr import IPRange from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.process import call from vyos.utils.process import run from vyos.utils.network import is_subnet_connected from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -109,19 +107,15 @@ def get_config(config=None): if not conf.exists(base): return None - 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']) + dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) if 'shared_network_name' in dhcp: for network, network_config in dhcp['shared_network_name'].items(): if 'subnet' in network_config: for subnet, subnet_config in network_config['subnet'].items(): - if 'lease' not in subnet_config: - dhcp['shared_network_name'][network]['subnet'][subnet] = dict_merge( - default_values, dhcp['shared_network_name'][network]['subnet'][subnet]) - # If exclude IP addresses are defined we need to slice them out of # the defined ranges if {'exclude', 'range'} <= set(subnet_config): @@ -302,6 +296,10 @@ def generate(dhcp): render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) + # Clean up configuration test file + if os.path.exists(tmp_file): + os.unlink(tmp_file) + return None def apply(dhcp): diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index d912611b3..6537ca3c2 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -19,14 +19,11 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.ifconfig import Interface from vyos.template import render from vyos.template import is_ipv6 from vyos.utils.process import call -from vyos.utils.dict import dict_search from vyos.utils.network import is_ipv6_link_local -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -42,11 +39,9 @@ def get_config(config=None): if not conf.exists(base): return None - relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - relay = dict_merge(default_values, relay) + relay = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return relay diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 97d46148a..ab80defe8 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -19,10 +19,8 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -50,8 +48,9 @@ def get_config(config=None): return None dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True, - with_defaults=True, with_recursive_defaults=True) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) dyndns['config_file'] = config_file return dyndns diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 2d98bffe3..c186f47af 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -21,14 +21,12 @@ from sys import exit from glob import glob from vyos.config import Config -from vyos.configdict import dict_merge from vyos.hostsd_client import Client as hostsd_client from vyos.template import render from vyos.template import bracketize_ipv6 from vyos.utils.process import call from vyos.utils.permission import chown from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -52,31 +50,10 @@ def get_config(config=None): if not conf.exists(base): return None - dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrieved. - default_values = defaults(base) - # T2665 due to how defaults under tag nodes work, we must clear these out before we merge - del default_values['authoritative_domain'] - del default_values['name_server'] - del default_values['domain']['name_server'] - dns = dict_merge(default_values, dns) - - # T2665: we cleared default values for tag node 'name_server' above. - # We now need to add them back back in a granular way. - if 'name_server' in dns: - default_values = defaults(base + ['name-server']) - for server in dns['name_server']: - dns['name_server'][server] = dict_merge(default_values, dns['name_server'][server]) - - # T2665: we cleared default values for tag node 'domain' above. - # We now need to add them back back in a granular way. - if 'domain' in dns: - default_values = defaults(base + ['domain', 'name-server']) - for domain in dns['domain'].keys(): - for server in dns['domain'][domain]['name_server']: - dns['domain'][domain]['name_server'][server] = dict_merge( - default_values, dns['domain'][domain]['name_server'][server]) + dns = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) # some additions to the default dictionary if 'system' in dns: @@ -109,9 +86,6 @@ def get_config(config=None): rdata = recorddata[rtype][subnode] if rtype in [ 'a', 'aaaa' ]: - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - rdata = dict_merge(rdefaults, rdata) - if not 'address' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required') continue @@ -127,9 +101,6 @@ def get_config(config=None): 'value': address }) elif rtype in ['cname', 'ptr', 'ns']: - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - rdata = dict_merge(rdefaults, rdata) - if not 'target' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required') continue @@ -141,18 +112,12 @@ def get_config(config=None): 'value': '{}.'.format(rdata['target']) }) elif rtype == 'mx': - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - del rdefaults['server'] - rdata = dict_merge(rdefaults, rdata) - if not 'server' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required') continue for servername in rdata['server']: serverdata = rdata['server'][servername] - serverdefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'server']) # T2665 - serverdata = dict_merge(serverdefaults, serverdata) zone['records'].append({ 'name': subnode, 'type': rtype.upper(), @@ -160,9 +125,6 @@ def get_config(config=None): 'value': '{} {}.'.format(serverdata['priority'], servername) }) elif rtype == 'txt': - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - rdata = dict_merge(rdefaults, rdata) - if not 'value' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required') continue @@ -175,9 +137,6 @@ def get_config(config=None): 'value': "\"{}\"".format(value.replace("\"", "\\\"")) }) elif rtype == 'spf': - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - rdata = dict_merge(rdefaults, rdata) - if not 'value' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required') continue @@ -189,19 +148,12 @@ def get_config(config=None): 'value': '"{}"'.format(rdata['value'].replace("\"", "\\\"")) }) elif rtype == 'srv': - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - del rdefaults['entry'] - rdata = dict_merge(rdefaults, rdata) - if not 'entry' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required') continue for entryno in rdata['entry']: entrydata = rdata['entry'][entryno] - entrydefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'entry']) # T2665 - entrydata = dict_merge(entrydefaults, entrydata) - if not 'hostname' in entrydata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}') continue @@ -217,19 +169,12 @@ def get_config(config=None): 'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname']) }) elif rtype == 'naptr': - rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 - del rdefaults['rule'] - rdata = dict_merge(rdefaults, rdata) - - if not 'rule' in rdata: dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required') continue for ruleno in rdata['rule']: ruledata = rdata['rule'][ruleno] - ruledefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'rule']) # T2665 - ruledata = dict_merge(ruledefaults, ruledata) flags = "" if 'lookup-srv' in ruledata: flags += "S" diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 07166d457..8ad3f27fc 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -23,7 +23,6 @@ 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 from vyos.configdiff import get_config_diff, Diff from vyos.configdep import set_dependents, call_dependents @@ -37,7 +36,6 @@ from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.process import process_named_running from vyos.utils.process import rc_cmd -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -97,19 +95,22 @@ def geoip_updated(conf, firewall): updated = False for key, path in dict_search_recursive(firewall, 'geoip'): - set_name = f'GEOIP_CC_{path[1]}_{path[3]}' - if path[0] == 'name': + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' + if (path[0] == 'ipv4'): out['name'].append(set_name) - elif path[0] == 'ipv6_name': + elif (path[0] == 'ipv6'): + set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' out['ipv6_name'].append(set_name) + updated = True if 'delete' in node_diff: for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): - set_name = f'GEOIP_CC_{path[1]}_{path[3]}' - if path[0] == 'name': + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' + if (path[0] == 'ipv4'): out['deleted_name'].append(set_name) - elif path[0] == 'ipv6-name': + elif (path[0] == 'ipv6'): + set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' out['deleted_ipv6_name'].append(set_name) updated = True @@ -125,54 +126,17 @@ def get_config(config=None): conf = Config() base = ['firewall'] - firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary retrived. - # XXX: T2665: we currently have no nice way for defaults under tag - # nodes, thus we load the defaults "by hand" - default_values = defaults(base) - for tmp in ['name', 'ipv6_name']: - if tmp in default_values: - del default_values[tmp] - - if 'zone' in default_values: - del default_values['zone'] - - firewall = dict_merge(default_values, firewall) - - # Merge in defaults for IPv4 ruleset - if 'name' in firewall: - default_values = defaults(base + ['name']) - for name in firewall['name']: - firewall['name'][name] = dict_merge(default_values, - firewall['name'][name]) - - # Merge in defaults for IPv6 ruleset - if 'ipv6_name' in firewall: - default_values = defaults(base + ['ipv6-name']) - for ipv6_name in firewall['ipv6_name']: - firewall['ipv6_name'][ipv6_name] = dict_merge(default_values, - firewall['ipv6_name'][ipv6_name]) - - if 'zone' in firewall: - default_values = defaults(base + ['zone']) - for zone in firewall['zone']: - firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) + firewall = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) + firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) if firewall['group_resync']: # Update nat and policy-route as firewall groups were updated set_dependents('group_resync', conf) - if 'config_trap' in firewall and firewall['config_trap'] == 'enable': - diff = get_config_diff(conf) - firewall['trap_diff'] = diff.get_child_nodes_diff_str(base) - firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'], - key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - firewall['geoip_updated'] = geoip_updated(conf, firewall) fqdn_config_parse(firewall) @@ -191,11 +155,11 @@ def verify_rule(firewall, rule_conf, ipv6): raise ConfigError('jump-target defined, but action jump needed and it is not defined') target = rule_conf['jump_target'] if not ipv6: - if target not in dict_search_args(firewall, 'name'): + if target not in dict_search_args(firewall, 'ipv4', 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') else: - if target not in dict_search_args(firewall, 'ipv6_name'): - raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system') + if target not in dict_search_args(firewall, 'ipv6', 'name'): + raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system') if 'queue_options' in rule_conf: if 'queue' not in rule_conf['action']: @@ -312,10 +276,6 @@ def verify_nested_group(group_name, group, groups, seen): verify_nested_group(g, groups[g], groups, seen) def verify(firewall): - if 'config_trap' in firewall and firewall['config_trap'] == 'enable': - if not firewall['trap_targets']: - raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') - if 'group' in firewall: for group_type in nested_group_types: if group_type in firewall['group']: @@ -323,95 +283,45 @@ def verify(firewall): for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) - for name in ['name', 'ipv6_name']: - if name in firewall: - for name_id, name_conf in firewall[name].items(): - if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: - raise ConfigError('default-action set to jump, but no default-jump-target specified') - if 'default_jump_target' in name_conf: - target = name_conf['default_jump_target'] - if 'jump' not in name_conf['default_action']: - raise ConfigError('default-jump-target defined,but default-action jump needed and it is not defined') - if name_conf['default_jump_target'] == name_id: - raise ConfigError(f'Loop detected on default-jump-target.') - ## Now need to check that default-jump-target exists (other firewall chain/name) - if target not in dict_search_args(firewall, name): - raise ConfigError(f'Invalid jump-target. Firewall {name} {target} does not exist on the system') - - if 'rule' in name_conf: - for rule_id, rule_conf in name_conf['rule'].items(): - verify_rule(firewall, rule_conf, name == 'ipv6_name') - - if 'interface' in firewall: - for ifname, if_firewall in firewall['interface'].items(): - # verify ifname needs to be disabled, dynamic devices come up later - # verify_interface_exists(ifname) - - for direction in ['in', 'out', 'local']: - name = dict_search_args(if_firewall, direction, 'name') - ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') - - if name and dict_search_args(firewall, 'name', name) == None: - raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}') - - if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: - raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}') - - local_zone = False - zone_interfaces = [] - - if 'zone' in firewall: - for zone, zone_conf in firewall['zone'].items(): - if 'local_zone' not in zone_conf and 'interface' not in zone_conf: - raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') - - if 'local_zone' in zone_conf: - if local_zone: - raise ConfigError('There cannot be multiple local zones') - if 'interface' in zone_conf: - raise ConfigError('Local zone cannot have interfaces assigned') - if 'intra_zone_filtering' in zone_conf: - raise ConfigError('Local zone cannot use intra-zone-filtering') - local_zone = True - - if 'interface' in zone_conf: - found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] - - if found_duplicates: - raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - - zone_interfaces += zone_conf['interface'] - - if 'intra_zone_filtering' in zone_conf: - intra_zone = zone_conf['intra_zone_filtering'] - - if len(intra_zone) > 1: - raise ConfigError('Only one intra-zone-filtering action must be specified') - - if 'firewall' in intra_zone: - v4_name = dict_search_args(intra_zone, 'firewall', 'name') - if v4_name and not dict_search_args(firewall, 'name', v4_name): - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name') - if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - - if not v4_name and not v6_name: - raise ConfigError('No firewall names specified for intra-zone-filtering') - - if 'from' in zone_conf: - for from_zone, from_conf in zone_conf['from'].items(): - if from_zone not in firewall['zone']: - raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') - - v4_name = dict_search_args(from_conf, 'firewall', 'name') - if v4_name and not dict_search_args(firewall, 'name', v4_name): - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') - if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + if 'ipv4' in firewall: + for name in ['name','forward','input','output']: + if name in firewall['ipv4']: + for name_id, name_conf in firewall['ipv4'][name].items(): + if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: + raise ConfigError('default-action set to jump, but no default-jump-target specified') + if 'default_jump_target' in name_conf: + target = name_conf['default_jump_target'] + if 'jump' not in name_conf['default_action']: + raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') + if name_conf['default_jump_target'] == name_id: + raise ConfigError(f'Loop detected on default-jump-target.') + ## Now need to check that default-jump-target exists (other firewall chain/name) + if target not in dict_search_args(firewall['ipv4'], 'name'): + raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + + if 'rule' in name_conf: + for rule_id, rule_conf in name_conf['rule'].items(): + verify_rule(firewall, rule_conf, False) + + if 'ipv6' in firewall: + for name in ['name','forward','input','output']: + if name in firewall['ipv6']: + for name_id, name_conf in firewall['ipv6'][name].items(): + if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: + raise ConfigError('default-action set to jump, but no default-jump-target specified') + if 'default_jump_target' in name_conf: + target = name_conf['default_jump_target'] + if 'jump' not in name_conf['default_action']: + raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') + if name_conf['default_jump_target'] == name_id: + raise ConfigError(f'Loop detected on default-jump-target.') + ## Now need to check that default-jump-target exists (other firewall chain/name) + if target not in dict_search_args(firewall['ipv6'], 'name'): + raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + + if 'rule' in name_conf: + for rule_id, rule_conf in name_conf['rule'].items(): + verify_rule(firewall, rule_conf, True) return None @@ -419,19 +329,6 @@ def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True - if 'zone' in firewall: - for local_zone, local_zone_conf in firewall['zone'].items(): - if 'local_zone' not in local_zone_conf: - continue - - local_zone_conf['from_local'] = {} - - for zone, zone_conf in firewall['zone'].items(): - if zone == local_zone or 'from' not in zone_conf: - continue - if local_zone in zone_conf['from']: - local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] - render(nftables_conf, 'firewall/nftables.j2', firewall) return None @@ -440,9 +337,8 @@ def apply_sysfs(firewall): paths = glob(conf['sysfs']) value = None - if name in firewall: - conf_value = firewall[name] - + if name in firewall['global_options']: + conf_value = firewall['global_options'][name] if conf_value in conf: value = conf[conf_value] elif conf_value == 'enable': @@ -455,42 +351,6 @@ def apply_sysfs(firewall): with open(path, 'w') as f: f.write(value) -def post_apply_trap(firewall): - if 'first_install' in firewall: - return None - - if 'config_trap' not in firewall or firewall['config_trap'] != 'enable': - return None - - if not process_named_running('snmpd'): - return None - - trap_username = os.getlogin() - - for host, target_conf in firewall['trap_targets'].items(): - community = target_conf['community'] if 'community' in target_conf else 'public' - port = int(target_conf['port']) if 'port' in target_conf else 162 - - base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} ' - - for change_type, changes in firewall['trap_diff'].items(): - for path_str, value in changes.items(): - objects = [ - f'mgmtEventUser s "{trap_username}"', - f'mgmtEventSource i {snmp_event_source}', - f'mgmtEventType i {snmp_change_type[change_type]}' - ] - - if change_type == 'add': - objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"') - elif change_type == 'delete': - objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"') - elif change_type == 'change': - objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"') - objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"') - - cmd(base_cmd + ' '.join(objects)) - def apply(firewall): install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: @@ -515,8 +375,6 @@ def apply(firewall): print('Updating GeoIP. Please wait...') geoip_update(firewall) - post_apply_trap(firewall) - return None if __name__ == '__main__': diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 372bb0da7..71acd69fa 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -22,14 +22,13 @@ from ipaddress import ip_address from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.config import config_dict_merge from vyos.configverify import verify_vrf from vyos.ifconfig import Section from vyos.template import render from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -128,30 +127,19 @@ def get_config(config=None): flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) + # We have gathered the dict representation of the CLI, but there are + # default values which we need to conditionally update into the + # dictionary retrieved. + default_values = conf.get_config_defaults(**flow_accounting.kwargs, + recursive=True) - # delete individual flow type default - should only be added if user uses - # this feature + # delete individual flow type defaults - should only be added if user + # sets this feature for flow_type in ['sflow', 'netflow']: - if flow_type in default_values: + if flow_type not in flow_accounting and flow_type in default_values: del default_values[flow_type] - flow_accounting = dict_merge(default_values, flow_accounting) - for flow_type in ['sflow', 'netflow']: - if flow_type in flow_accounting: - default_values = defaults(base + [flow_type]) - # we need to merge individual server configurations - if 'server' in default_values: - del default_values['server'] - flow_accounting[flow_type] = dict_merge(default_values, flow_accounting[flow_type]) - - if 'server' in flow_accounting[flow_type]: - default_values = defaults(base + [flow_type, 'server']) - for server in flow_accounting[flow_type]['server']: - flow_accounting[flow_type]['server'][server] = dict_merge( - default_values,flow_accounting[flow_type]['server'][server]) + flow_accounting = config_dict_merge(default_values, flow_accounting) return flow_accounting diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 7bdf448a3..793a90d88 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -24,12 +24,9 @@ from copy import deepcopy import vyos.defaults from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdep import set_dependents, call_dependents from vyos.template import render -from vyos.utils.process import cmd from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -72,8 +69,9 @@ def get_config(config=None): return None api_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) # One needs to 'flatten' the keys dict from the config into the # http-api.conf format for api_keys: @@ -93,8 +91,8 @@ def get_config(config=None): if 'api_keys' in api_dict: keys_added = True - if 'graphql' in api_dict: - api_dict = dict_merge(defaults(base), api_dict) + if api_dict.from_defaults(['graphql']): + del api_dict['graphql'] http_api.update(api_dict) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index 4ec2f1835..40db417dd 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -21,11 +21,9 @@ 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 from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -39,16 +37,9 @@ def get_config(config=None): conf = Config() base = ['protocols', 'igmp-proxy'] - igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - if 'interface' in igmp_proxy: - # T2665: we must add the tagNode defaults individually until this is - # moved to the base class - default_values = defaults(base + ['interface']) - for interface in igmp_proxy['interface']: - igmp_proxy['interface'][interface] = dict_merge(default_values, - igmp_proxy['interface'][interface]) - + igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_defaults=True) if conf.exists(['protocols', 'igmp']): igmp_proxy.update({'igmp_configured': ''}) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 1bdd61eca..c82f01e53 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -14,10 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit -from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict @@ -25,16 +22,13 @@ from vyos.configdict import node_changed from vyos.configdict import is_member from vyos.configdict import is_source_interface from vyos.configdict import has_vlan_subinterface_configured -from vyos.configdict import dict_merge from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf from vyos.ifconfig import BridgeIf from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured -from vyos.xml import defaults -from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos import ConfigError @@ -61,22 +55,8 @@ def get_config(config=None): else: bridge.update({'member' : {'interface_remove' : tmp }}) - if dict_search('member.interface', bridge) != None: - # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: - # RuntimeError: dictionary changed size during iteration + if dict_search('member.interface', bridge) is not None: for interface in list(bridge['member']['interface']): - for key in ['cost', 'priority']: - if interface == key: - del bridge['member']['interface'][key] - continue - - # the default dictionary is not properly paged into the dict (see T2665) - # thus we will ammend it ourself - default_member_values = defaults(base + ['member', 'interface']) - for interface,interface_config in bridge['member']['interface'].items(): - bridge['member']['interface'][interface] = dict_merge( - default_member_values, bridge['member']['interface'][interface]) - # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') if tmp and bridge['ifname'] not in tmp: diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 3f86e2638..0a927ac88 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -43,6 +43,14 @@ airbag.enable() # XXX: wpa_supplicant works on the source interface wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' +# Constants +## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit +GCM_AES_128_LEN: int = 32 +GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!' +## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit +GCM_AES_256_LEN: int = 64 +GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!' + def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -89,18 +97,54 @@ def verify(macsec): raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) if dict_search('security.encrypt', macsec) != None: - if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: - raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') + # Check that only static or MKA config is present + if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None): + raise ConfigError('Only static or MKA can be used!') + + # Logic to check static configuration + if dict_search('security.static', macsec) != None: + # tx-key must be defined + if dict_search('security.static.key', macsec) == None: + raise ConfigError('Static MACsec tx-key must be defined.') + + tx_len = len(dict_search('security.static.key', macsec)) + + if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN: + raise ConfigError(GCM_128_KEY_ERROR) + + if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN: + raise ConfigError(GCM_256_KEY_ERROR) + + # Make sure at least one peer is defined + if 'peer' not in macsec['security']['static']: + raise ConfigError('Must have at least one peer defined for static MACsec') + + # For every enabled peer, make sure a MAC and rx-key is defined + for peer, peer_config in macsec['security']['static']['peer'].items(): + if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config): + raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.') + + # check rx-key length against cipher suite + rx_len = len(peer_config['key']) + + if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN: + raise ConfigError(GCM_128_KEY_ERROR) + + if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN: + raise ConfigError(GCM_256_KEY_ERROR) + + # Logic to check MKA configuration + else: + if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: + raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') - cak_len = len(dict_search('security.mka.cak', macsec)) + cak_len = len(dict_search('security.mka.cak', macsec)) - if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32: - # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit - raise ConfigError('gcm-aes-128 requires a 128bit long key!') + if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN: + raise ConfigError(GCM_128_KEY_ERROR) - elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64: - # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit - raise ConfigError('gcm-aes-128 requires a 256bit long key!') + elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN: + raise ConfigError(GCM_256_KEY_ERROR) if 'source_interface' in macsec: # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -115,7 +159,9 @@ def verify(macsec): def generate(macsec): - render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) + # Only generate wpa_supplicant config if using MKA + if dict_search('security.mka.cak', macsec): + render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) return None @@ -142,8 +188,10 @@ def apply(macsec): i = MACsecIf(**macsec) i.update(macsec) - if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: - call(f'systemctl reload-or-restart {systemd_service}') + # Only reload/restart if using MKA + if dict_search('security.mka.cak', macsec): + if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 2a9b43f9b..1d0feb56f 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -166,17 +166,23 @@ def verify_pki(openvpn): raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}') if tls: - if 'ca_certificate' not in tls: - raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') + if (mode in ['server', 'client']) and ('ca_certificate' not in tls): + raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\ + it is required in server and client modes') + else: + if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls): + raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\ + on openvpn interface {interface} in site-to-site mode') - for ca_name in tls['ca_certificate']: - if ca_name not in pki['ca']: - raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + if 'ca_certificate' in tls: + for ca_name in tls['ca_certificate']: + if ca_name not in pki['ca']: + raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') - if len(tls['ca_certificate']) > 1: - sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) - if not verify_ca_chain(sorted_chain, pki['ca']): - raise ConfigError(f'CA certificates are not a valid chain') + if len(tls['ca_certificate']) > 1: + sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) + if not verify_ca_chain(sorted_chain, pki['ca']): + raise ConfigError(f'CA certificates are not a valid chain') if mode != 'client' and 'auth_key' not in tls: if 'certificate' not in tls: @@ -189,16 +195,7 @@ def verify_pki(openvpn): if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None: raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') - if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']): - raise ConfigError('Must specify "tls dh-params" when not using EC keys in server mode') - if 'dh_params' in tls: - if 'dh' not in pki: - raise ConfigError('There are no DH parameters in PKI configuration') - - if tls['dh_params'] not in pki['dh']: - raise ConfigError(f'Invalid dh-params on openvpn interface {interface}') - pki_dh = pki['dh'][tls['dh_params']] dh_params = load_dh_parameters(pki_dh['parameters']) dh_numbers = dh_params.parameter_numbers() @@ -207,6 +204,7 @@ def verify_pki(openvpn): if dh_bits < 2048: raise ConfigError(f'Minimum DH key-size is 2048 bits') + if 'auth_key' in tls or 'crypt_key' in tls: if not dict_search_args(pki, 'openvpn', 'shared_secret'): raise ConfigError('There are no openvpn shared-secrets in PKI configuration') @@ -495,9 +493,6 @@ def verify(openvpn): if openvpn['protocol'] == 'tcp-active': raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') - if not dict_search('tls.dh_params', openvpn): - raise ConfigError('Must specify "tls dh-params" when "tls role" is "passive"') - if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']): if 'dh_params' in openvpn['tls']: print('Warning: using dh-params and EC keys simultaneously will ' \ diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 6a075970e..91aed9cc3 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -55,6 +55,9 @@ def get_config(config=None): tmp = is_node_changed(conf, base + [ifname, 'encapsulation']) if tmp: tunnel.update({'encapsulation_changed': {}}) + tmp = is_node_changed(conf, base + [ifname, 'parameters', 'ip', 'key']) + if tmp: tunnel.update({'key_changed': {}}) + # We also need to inspect other configured tunnels as there are Kernel # restrictions where we need to comply. E.g. GRE tunnel key can't be used # twice, or with multiple GRE tunnels to the same location we must specify @@ -197,7 +200,8 @@ def apply(tunnel): remote = dict_search('linkinfo.info_data.remote', tmp) if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or encap in - ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any']): + ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any'] or + 'key_changed' in tunnel): if interface in interfaces(): tmp = Interface(interface) tmp.remove() diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 446399255..122d9589a 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -90,7 +90,6 @@ def verify(wireguard): # run checks on individual configured WireGuard peer public_keys = [] - for tmp in wireguard['peer']: peer = wireguard['peer'][tmp] @@ -107,8 +106,9 @@ def verify(wireguard): if peer['public_key'] in public_keys: raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') - if 'disable' not in peer and is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): - raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"') + if 'disable' not in peer: + if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): + raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"') public_keys.append(peer['public_key']) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 42326bea0..02b4a2500 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -25,8 +25,6 @@ from vyos.configdict import get_interface_dict from vyos.configdict import dict_merge from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete -from vyos.configverify import verify_dhcpv6 -from vyos.configverify import verify_source_interface from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf @@ -42,6 +40,8 @@ airbag.enable() # XXX: wpa_supplicant works on the source interface wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' hostapd_conf = '/run/hostapd/{ifname}.conf' +hostapd_accept_station_conf = '/run/hostapd/{ifname}_station_accept.conf' +hostapd_deny_station_conf = '/run/hostapd/{ifname}_station_deny.conf' def find_other_stations(conf, base, ifname): """ @@ -79,30 +79,14 @@ def get_config(config=None): 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(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: + if 'deleted' not in wifi: + # then get_interface_dict provides default keys + if wifi.from_defaults(['security', 'wep']): # if not set by user + del wifi['security']['wep'] + if wifi.from_defaults(['security', 'wpa']): # if not set by user 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 - if dict_search('security.wpa.radius.server.port', wifi) != None: - del wifi['security']['wpa']['radius']['server']['port'] - if not len(wifi['security']['wpa']['radius']['server']): - del wifi['security']['wpa']['radius'] - if not len(wifi['security']['wpa']): - del wifi['security']['wpa'] - if not len(wifi['security']): - del wifi['security'] - if 'security' in wifi and 'wpa' in wifi['security']: + if dict_search('security.wpa', wifi) != None: wpa_cipher = wifi['security']['wpa'].get('cipher') wpa_mode = wifi['security']['wpa'].get('mode') if not wpa_cipher: @@ -120,13 +104,9 @@ def get_config(config=None): tmp = find_other_stations(conf, base, wifi['ifname']) if tmp: wifi['station_interfaces'] = tmp - # Add individual RADIUS server default values - if dict_search('security.wpa.radius.server', wifi): - default_values = defaults(base + ['security', 'wpa', 'radius', 'server']) - - for server in dict_search('security.wpa.radius.server', wifi): - wifi['security']['wpa']['radius']['server'][server] = dict_merge( - default_values, wifi['security']['wpa']['radius']['server'][server]) + # used in hostapt.conf.j2 + wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi) + wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi) return wifi @@ -142,7 +122,7 @@ def verify(wifi): raise ConfigError('You must specify a WiFi mode') if 'ssid' not in wifi and wifi['type'] != 'monitor': - raise ConfigError('SSID must be configured') + raise ConfigError('SSID must be configured unless type is set to "monitor"!') if wifi['type'] == 'access-point': if 'country_code' not in wifi: @@ -215,7 +195,10 @@ def generate(wifi): if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): os.unlink(hostapd_conf.format(**wifi)) - + if os.path.isfile(hostapd_accept_station_conf.format(**wifi)): + os.unlink(hostapd_accept_station_conf.format(**wifi)) + if os.path.isfile(hostapd_deny_station_conf.format(**wifi)): + os.unlink(hostapd_deny_station_conf.format(**wifi)) if os.path.isfile(wpa_suppl_conf.format(**wifi)): os.unlink(wpa_suppl_conf.format(**wifi)) @@ -250,12 +233,12 @@ def generate(wifi): # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': - render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', - wifi) + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi) + render(hostapd_accept_station_conf.format(**wifi), 'wifi/hostapd_accept_station.conf.j2', wifi) + render(hostapd_deny_station_conf.format(**wifi), 'wifi/hostapd_deny_station.conf.j2', wifi) elif wifi['type'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', - wifi) + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', wifi) return None diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index c8f341327..c2e87d171 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -20,13 +20,11 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge from vyos.utils.network import is_addr_assigned from vyos.utils.network import is_loopback_addr from vyos.version import get_version_data from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos.template import render from vyos import ConfigError from vyos import airbag @@ -46,7 +44,9 @@ def get_config(config=None): return {} lldp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) if conf.exists(['service', 'snmp']): lldp['system_snmp_enabled'] = '' @@ -54,27 +54,12 @@ def get_config(config=None): version_data = get_version_data() lldp['version'] = version_data['version'] - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - # location coordinates have a default value - if 'interface' in lldp: - for interface, interface_config in lldp['interface'].items(): - default_values = defaults(base + ['interface']) - if dict_search('location.coordinate_based', interface_config) == None: - # no location specified - no need to add defaults - del default_values['location']['coordinate_based']['datum'] - del default_values['location']['coordinate_based']['altitude'] - - # cleanup default_values dictionary from inner to outer - # this might feel overkill here, but it does support easy extension - # in the future with additional default values - if len(default_values['location']['coordinate_based']) == 0: - del default_values['location']['coordinate_based'] - if len(default_values['location']) == 0: - del default_values['location'] - - lldp['interface'][interface] = dict_merge(default_values, - lldp['interface'][interface]) + # prune location information if not set by user + for interface in lldp.get('interface', []): + if lldp.from_defaults(['interface', interface, 'location']): + del lldp['interface'][interface]['location'] + elif lldp.from_defaults(['interface', interface, 'location','coordinate_based']): + del lldp['interface'][interface]['location']['coordinate_based'] return lldp diff --git a/src/conf_mode/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py index 2fb0edf8e..8fe429653 100755 --- a/src/conf_mode/load-balancing-haproxy.py +++ b/src/conf_mode/load-balancing-haproxy.py @@ -20,14 +20,12 @@ from sys import exit from shutil import rmtree from vyos.config import Config -from vyos.configdict import dict_merge from vyos.utils.process import call from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -54,18 +52,8 @@ def get_config(config=None): lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - if 'backend' in default_values: - del default_values['backend'] if lb: - lb = dict_merge(default_values, lb) - - if 'backend' in lb: - for backend in lb['backend']: - default_balues_backend = defaults(base + ['backend']) - lb['backend'][backend] = dict_merge(default_balues_backend, lb['backend'][backend]) + lb = conf.merge_defaults(lb, recursive=True) return lb diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py index 3533a5a04..ad9c80d72 100755 --- a/src/conf_mode/load-balancing-wan.py +++ b/src/conf_mode/load-balancing-wan.py @@ -21,10 +21,8 @@ from shutil import rmtree from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge from vyos.utils.process import cmd from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -41,48 +39,15 @@ def get_config(config=None): conf = Config() base = ['load-balancing', 'wan'] - lb = conf.get_config_dict(base, + lb = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, get_first_key=True, - key_mangling=('-', '_'), - no_tag_node_value_mangle=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - # lb base default values can not be merged here - remove and add them later - if 'interface_health' in default_values: - del default_values['interface_health'] - if 'rule' in default_values: - del default_values['rule'] - lb = dict_merge(default_values, lb) - - if 'interface_health' in lb: - for iface in lb.get('interface_health'): - default_values_iface = defaults(base + ['interface-health']) - if 'test' in default_values_iface: - del default_values_iface['test'] - lb['interface_health'][iface] = dict_merge( - default_values_iface, lb['interface_health'][iface]) - if 'test' in lb['interface_health'][iface]: - for node_test in lb['interface_health'][iface]['test']: - default_values_test = defaults(base + - ['interface-health', 'test']) - lb['interface_health'][iface]['test'][node_test] = dict_merge( - default_values_test, - lb['interface_health'][iface]['test'][node_test]) - - if 'rule' in lb: - for rule in lb.get('rule'): - default_values_rule = defaults(base + ['rule']) - if 'interface' in default_values_rule: - del default_values_rule['interface'] - lb['rule'][rule] = dict_merge(default_values_rule, lb['rule'][rule]) - if not conf.exists(base + ['rule', rule, 'limit']): - del lb['rule'][rule]['limit'] - if 'interface' in lb['rule'][rule]: - for iface in lb['rule'][rule]['interface']: - default_values_rule_iface = defaults(base + ['rule', 'interface']) - lb['rule'][rule]['interface'][iface] = dict_merge(default_values_rule_iface, lb['rule'][rule]['interface'][iface]) + with_recursive_defaults=True) + + # prune limit key if not set by user + for rule in lb.get('rule', []): + if lb.from_defaults(['rule', rule, 'limit']): + del lb['rule'][rule]['limit'] return lb diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 8e3a11ff4..9da7fbe80 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -25,7 +25,6 @@ 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 from vyos.template import is_ip_network from vyos.utils.kernel import check_kmod @@ -34,7 +33,6 @@ from vyos.utils.dict import dict_search_args from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -145,16 +143,9 @@ def get_config(config=None): conf = Config() base = ['nat'] - nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # T2665: we must add the tagNode defaults individually until this is - # moved to the base class - for direction in ['source', 'destination', 'static']: - if direction in nat: - default_values = defaults(base + [direction, 'rule']) - for rule in dict_search(f'{direction}.rule', nat) or []: - nat[direction]['rule'][rule] = dict_merge(default_values, - nat[direction]['rule'][rule]) + nat = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) # read in current nftable (once) for further processing tmp = cmd('nft -j list table raw') @@ -233,7 +224,7 @@ def verify(nat): elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') - if not dict_search('translation.address', config) and not dict_search('translation.port', config) and not dict_search('translation.redirect.port', config): + if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']: if 'exclude' not in config and 'backend' not in config['load_balance']: raise ConfigError(f'{err_msg} translation requires address and/or port') diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 25f625b84..4c12618bc 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -23,13 +23,11 @@ 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 from vyos.utils.process import cmd from vyos.utils.kernel import check_kmod from vyos.utils.dict import dict_search from vyos.template import is_ipv6 -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -60,16 +58,6 @@ def get_config(config=None): base = ['nat66'] nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # T2665: we must add the tagNode defaults individually until this is - # moved to the base class - for direction in ['source', 'destination']: - if direction in nat: - default_values = defaults(base + [direction, 'rule']) - if 'rule' in nat[direction]: - for rule in nat[direction]['rule']: - nat[direction]['rule'][rule] = dict_merge(default_values, - nat[direction]['rule'][rule]) - # read in current nftable (once) for further processing tmp = cmd('nft -j list table ip6 raw') nftable_json = json.loads(tmp) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index eb8cb3940..34ba2fe69 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -18,7 +18,6 @@ from sys import exit from vyos.config import Config from vyos.configdep import set_dependents, call_dependents -from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.pki import is_ca_certificate from vyos.pki import load_certificate @@ -28,7 +27,6 @@ from vyos.pki import load_crl from vyos.pki import load_dh_parameters from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -113,8 +111,7 @@ def get_config(config=None): # We only merge on the defaults of there is a configuration at all if conf.exists(base): - default_values = defaults(base) - pki = dict_merge(default_values, pki) + pki = conf.merge_defaults(pki, recursive=True) # We need to get the entire system configuration to verify that we are not # deleting a certificate that is still referenced somewhere! diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py index f5ac56f65..104711b55 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -19,13 +19,13 @@ import os from sys import exit from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos.template import render_to_string from vyos import ConfigError from vyos import frr @@ -38,7 +38,8 @@ def get_config(config=None): else: conf = Config() base = ['protocols', 'babel'] - babel = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + babel = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # FRR has VRF support for different routing daemons. As interfaces belong # to VRFs - or the global VRF, we need to check for changed interfaces so @@ -54,15 +55,13 @@ def get_config(config=None): return babel # 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) + # values which we need to update into the dictionary retrieved. + default_values = conf.get_config_defaults(base, key_mangling=('-', '_'), + get_first_key=True, + recursive=True) - # XXX: T2665: we currently have no nice way for defaults under tag nodes, - # clean them out and add them manually :( - del default_values['interface'] - - # merge in remaining default values - babel = dict_merge(default_values, babel) + # merge in default values + babel = config_dict_merge(default_values, babel) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index b8d2d65ee..dab784662 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -17,12 +17,10 @@ import os from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configverify import verify_vrf from vyos.template import is_ipv6 from vyos.template import render_to_string from vyos.utils.network import is_ipv6_link_local -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -41,18 +39,7 @@ def get_config(config=None): if not conf.exists(base): return bfd - # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary retrived. - # XXX: T2665: we currently have no nice way for defaults under tag - # nodes, thus we load the defaults "by hand" - default_values = defaults(base + ['peer']) - if 'peer' in bfd: - for peer in bfd['peer']: - bfd['peer'][peer] = dict_merge(default_values, bfd['peer'][peer]) - - if 'profile' in bfd: - for profile in bfd['profile']: - bfd['profile'][profile] = dict_merge(default_values, bfd['profile'][profile]) + bfd = conf.merge_defaults(bfd, recursive=True) return bfd diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py index faf56d741..e7e44db84 100755 --- a/src/conf_mode/protocols_failover.py +++ b/src/conf_mode/protocols_failover.py @@ -19,10 +19,8 @@ import json from pathlib import Path from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -42,15 +40,12 @@ def get_config(config=None): conf = Config() base = ['protocols', 'failover'] - failover = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + failover = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # Set default values only if we set config - if failover.get('route'): - for route, route_config in failover.get('route').items(): - for next_hop, next_hop_config in route_config.get('next_hop').items(): - default_values = defaults(base + ['route']) - failover['route'][route]['next_hop'][next_hop] = dict_merge( - default_values['next_hop'], failover['route'][route]['next_hop'][next_hop]) + if failover.get('route') is not None: + failover = conf.merge_defaults(failover, recursive=True) return failover diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 4c637a99f..e00c58ee4 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -28,7 +28,6 @@ from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos.template import render_to_string -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -64,19 +63,14 @@ def get_config(config=None): if interfaces_removed: isis['interface_removed'] = list(interfaces_removed) - # Bail out early if configuration tree does not exist + # Bail out early if configuration tree does no longer exist. this must + # be done after retrieving the list of interfaces to be removed. if not conf.exists(base): isis.update({'deleted' : ''}) return isis - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - # XXX: Note that we can not call defaults(base), as defaults does not work - # on an instance of a tag node. As we use the exact same CLI definition for - # both the non-vrf and vrf version this is absolutely safe! - default_values = defaults(base_path) # merge in default values - isis = dict_merge(default_values, isis) + isis = conf.merge_defaults(isis, recursive=True) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). @@ -254,7 +248,7 @@ def apply(isis): if key not in isis: continue for interface in isis[key]: - frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) if 'frr_isisd_config' in isis: frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config']) diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index f2075d25b..cddd3765e 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -20,6 +20,7 @@ from sys import exit from sys import argv from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps @@ -29,7 +30,6 @@ from vyos.configverify import verify_access_list from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -65,17 +65,15 @@ def get_config(config=None): if interfaces_removed: ospf['interface_removed'] = list(interfaces_removed) - # Bail out early if configuration tree does not exist + # Bail out early if configuration tree does no longer exist. this must + # be done after retrieving the list of interfaces to be removed. if not conf.exists(base): ospf.update({'deleted' : ''}) return ospf # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. - # XXX: Note that we can not call defaults(base), as defaults does not work - # on an instance of a tag node. As we use the exact same CLI definition for - # both the non-vrf and vrf version this is absolutely safe! - default_values = defaults(base_path) + default_values = conf.get_config_defaults(**ospf.kwargs, recursive=True) # We have to cleanup the default dict, as default values could enable features # which are not explicitly enabled on the CLI. Example: default-information @@ -84,62 +82,27 @@ def get_config(config=None): # need to check this first and probably drop that key. if dict_search('default_information.originate', ospf) is None: del default_values['default_information'] - if dict_search('area.area_type.nssa', ospf) is None: - del default_values['area']['area_type']['nssa'] if 'mpls_te' not in ospf: del default_values['mpls_te'] if 'graceful_restart' not in ospf: del default_values['graceful_restart'] + for area_num in default_values.get('area', []): + if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: + del default_values['area'][area_num]['area_type']['nssa'] - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']: - # table is a tagNode thus we need to clean out all occurances for the - # default values and load them in later individually - if protocol == 'table': - del default_values['redistribute']['table'] - continue + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: if dict_search(f'redistribute.{protocol}', ospf) is None: del default_values['redistribute'][protocol] - # XXX: T2665: we currently have no nice way for defaults under tag nodes, - # clean them out and add them manually :( - del default_values['neighbor'] - del default_values['area']['virtual_link'] - del default_values['interface'] - - # merge in remaining default values - ospf = dict_merge(default_values, ospf) - - if 'neighbor' in ospf: - default_values = defaults(base + ['neighbor']) - for neighbor in ospf['neighbor']: - ospf['neighbor'][neighbor] = dict_merge(default_values, ospf['neighbor'][neighbor]) + for interface in ospf.get('interface', []): + # We need to reload the defaults on every pass b/c of + # hello-multiplier dependency on dead-interval + # If hello-multiplier is set, we need to remove the default from + # dead-interval. + if 'hello_multiplier' in ospf['interface'][interface]: + del default_values['interface'][interface]['dead_interval'] - if 'area' in ospf: - default_values = defaults(base + ['area', 'virtual-link']) - for area, area_config in ospf['area'].items(): - if 'virtual_link' in area_config: - for virtual_link in area_config['virtual_link']: - ospf['area'][area]['virtual_link'][virtual_link] = dict_merge( - default_values, ospf['area'][area]['virtual_link'][virtual_link]) - - if 'interface' in ospf: - for interface in ospf['interface']: - # We need to reload the defaults on every pass b/c of - # hello-multiplier dependency on dead-interval - default_values = defaults(base + ['interface']) - # If hello-multiplier is set, we need to remove the default from - # dead-interval. - if 'hello_multiplier' in ospf['interface'][interface]: - del default_values['dead_interval'] - - ospf['interface'][interface] = dict_merge(default_values, - ospf['interface'][interface]) - - if 'redistribute' in ospf and 'table' in ospf['redistribute']: - default_values = defaults(base + ['redistribute', 'table']) - for table in ospf['redistribute']['table']: - ospf['redistribute']['table'][table] = dict_merge(default_values, - ospf['redistribute']['table'][table]) + ospf = config_dict_merge(default_values, ospf) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). @@ -287,7 +250,7 @@ def apply(ospf): if key not in ospf: continue for interface in ospf[key]: - frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) if 'frr_ospfd_config' in ospf: frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index fbea51f56..5b1adce30 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -20,6 +20,7 @@ from sys import exit from sys import argv from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps @@ -29,7 +30,6 @@ from vyos.template import render_to_string from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -64,17 +64,16 @@ def get_config(config=None): if interfaces_removed: ospfv3['interface_removed'] = list(interfaces_removed) - # Bail out early if configuration tree does not exist + # Bail out early if configuration tree does no longer exist. this must + # be done after retrieving the list of interfaces to be removed. if not conf.exists(base): ospfv3.update({'deleted' : ''}) return ospfv3 # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. - # XXX: Note that we can not call defaults(base), as defaults does not work - # on an instance of a tag node. As we use the exact same CLI definition for - # both the non-vrf and vrf version this is absolutely safe! - default_values = defaults(base_path) + default_values = conf.get_config_defaults(**ospfv3.kwargs, + recursive=True) # We have to cleanup the default dict, as default values could enable features # which are not explicitly enabled on the CLI. Example: default-information @@ -86,12 +85,10 @@ def get_config(config=None): if 'graceful_restart' not in ospfv3: del default_values['graceful_restart'] - # XXX: T2665: we currently have no nice way for defaults under tag nodes, - # clean them out and add them manually :( - del default_values['interface'] + default_values.pop('interface', {}) # merge in remaining default values - ospfv3 = dict_merge(default_values, ospfv3) + ospfv3 = config_dict_merge(default_values, ospfv3) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). @@ -170,7 +167,7 @@ def apply(ospfv3): if key not in ospfv3: continue for interface in ospfv3[key]: - frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) if 'new_frr_config' in ospfv3: frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config']) diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 5661dc377..bd47dfd00 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -25,7 +25,6 @@ from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos.template import render_to_string from vyos import ConfigError from vyos import frr @@ -55,9 +54,7 @@ def get_config(config=None): # 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) - # merge in remaining default values - rip = dict_merge(default_values, rip) + rip = conf.merge_defaults(rip, recursive=True) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index e3c904e33..dd1550033 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -24,7 +24,6 @@ from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos.template import render_to_string from vyos import ConfigError from vyos import frr @@ -45,9 +44,7 @@ def get_config(config=None): # 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) - # merge in remaining default values - ripng = dict_merge(default_values, ripng) + ripng = conf.merge_defaults(ripng, recursive=True) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 035b7db05..05e876f3b 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -19,10 +19,8 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render_to_string from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -43,8 +41,7 @@ def get_config(config=None): # 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) - rpki = dict_merge(default_values, rpki) + rpki = conf.merge_defaults(rpki, recursive=True) return rpki diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 53e9ff50d..5536adda2 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -38,7 +38,6 @@ from vyos.qos import TrafficShaper from vyos.qos import TrafficShaperHFSC from vyos.utils.process import call from vyos.utils.dict import dict_search_recursive -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -97,63 +96,32 @@ def get_config(config=None): type_node = path.split(" ")[1] # return only interface type node set_dependents(type_node, conf, ifname) - if 'policy' in qos: - for policy in qos['policy']: - # when calling defaults() we need to use the real CLI node, thus we - # need a hyphen - policy_hyphen = policy.replace('_', '-') - - if policy in ['random_detect']: - for rd_name, rd_config in qos['policy'][policy].items(): - # There are eight precedence levels - ensure all are present - # to be filled later down with the appropriate default values - default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, - '4' : {}, '5' : {}, '6' : {}, '7' : {} }} - qos['policy']['random_detect'][rd_name] = dict_merge( - default_precedence, qos['policy']['random_detect'][rd_name]) - - for p_name, p_config in qos['policy'][policy].items(): - default_values = defaults(base + ['policy', policy_hyphen]) - - if policy in ['priority_queue']: - if 'default' not in p_config: - raise ConfigError(f'QoS policy {p_name} misses "default" class!') - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if 'class' in default_values: - del default_values['class'] - if 'precedence' in default_values: - del default_values['precedence'] - - qos['policy'][policy][p_name] = dict_merge( - default_values, qos['policy'][policy][p_name]) - - # class is another tag node which requires individual handling - if 'class' in p_config: - default_values = defaults(base + ['policy', policy_hyphen, 'class']) - for p_class in p_config['class']: - qos['policy'][policy][p_name]['class'][p_class] = dict_merge( - default_values, qos['policy'][policy][p_name]['class'][p_class]) - - if 'precedence' in p_config: - default_values = defaults(base + ['policy', policy_hyphen, 'precedence']) - # precedence values are a bit more complex as they are calculated - # under specific circumstances - thus we need to iterate two times. - # first blend in the defaults from XML / CLI - for precedence in p_config['precedence']: - qos['policy'][policy][p_name]['precedence'][precedence] = dict_merge( - default_values, qos['policy'][policy][p_name]['precedence'][precedence]) - # second calculate defaults based on actual dictionary - for precedence in p_config['precedence']: - max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) - if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: - qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( - int((9 + int(precedence)) * max_thr) // 18); - - if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: - qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ - str(int(4 * max_thr)) + for policy in qos.get('policy', []): + if policy in ['random_detect']: + for rd_name in list(qos['policy'][policy]): + # There are eight precedence levels - ensure all are present + # to be filled later down with the appropriate default values + default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, + '4' : {}, '5' : {}, '6' : {}, '7' : {} }} + qos['policy']['random_detect'][rd_name] = dict_merge( + default_precedence, qos['policy']['random_detect'][rd_name]) + + qos = conf.merge_defaults(qos, recursive=True) + + for policy in qos.get('policy', []): + for p_name, p_config in qos['policy'][policy].items(): + if 'precedence' in p_config: + # precedence settings are a bit more complex as they are + # calculated under specific circumstances: + for precedence in p_config['precedence']: + max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) + if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( + int((9 + int(precedence)) * max_thr) // 18); + + if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ + str(int(4 * max_thr)) return qos @@ -202,7 +170,9 @@ def verify(qos): queue_lim = int(precedence_config['queue_limit']) if queue_lim < max_tr: raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') - + if policy_type in ['priority_queue']: + if 'default' not in policy_config: + raise ConfigError(f'Policy {policy} misses "default" class!') if 'default' in policy_config: if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']: raise ConfigError('Bandwidth not defined for default traffic!') diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index 3ff7880b2..a8fce8e01 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -22,12 +22,10 @@ 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.utils.process import call from vyos.utils.permission import chown -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -55,8 +53,7 @@ def get_config(config=None): 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) + salt = conf.merge_defaults(salt, recursive=True) if not conf.exists(base): return None diff --git a/src/conf_mode/service_config_sync.py b/src/conf_mode/service_config_sync.py index 5cde735a1..4b8a7f6ee 100755 --- a/src/conf_mode/service_config_sync.py +++ b/src/conf_mode/service_config_sync.py @@ -19,8 +19,6 @@ import json from pathlib import Path from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -42,12 +40,8 @@ def get_config(config=None): base = ['service', 'config-sync'] if not conf.exists(base): return None - config = conf.get_config_dict(base, - get_first_key=True, - no_tag_node_value_mangle=True) - - default_values = defaults(base) - config = dict_merge(default_values, config) + config = conf.get_config_dict(base, get_first_key=True, + with_recursive_defaults=True) return config diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index 7eb41ea87..b112add3f 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -20,10 +20,8 @@ from sys import exit from psutil import process_iter from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError config_file = '/run/conserver/conserver.cf' @@ -49,11 +47,7 @@ def get_config(config=None): # 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 + ['device']) - if 'device' in proxy: - for device in proxy['device']: - tmp = dict_merge(default_values, proxy['device'][device]) - proxy['device'][device] = tmp + proxy = conf.merge_defaults(proxy, recursive=True) return proxy diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index f6b80552b..276a71fcb 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -19,10 +19,8 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -41,11 +39,9 @@ def get_config(config=None): if not conf.exists(base): return None - fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - fastnetmon = dict_merge(default_values, fastnetmon) + fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return fastnetmon diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 0269bedd9..40eb13e23 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -22,7 +22,6 @@ from sys import exit from shutil import rmtree from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.ifconfig import Section @@ -30,7 +29,6 @@ from vyos.template import render from vyos.utils.process import call from vyos.utils.permission import chown from vyos.utils.process import cmd -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -83,8 +81,7 @@ def get_config(config=None): # 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) - monitoring = dict_merge(default_values, monitoring) + monitoring = conf.merge_defaults(monitoring, recursive=True) monitoring['custom_scripts_dir'] = custom_scripts_dir monitoring['hostname'] = get_hostname() diff --git a/src/conf_mode/service_monitoring_zabbix-agent.py b/src/conf_mode/service_monitoring_zabbix-agent.py new file mode 100755 index 000000000..98d8a32ca --- /dev/null +++ b/src/conf_mode/service_monitoring_zabbix-agent.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.config import Config +from vyos.template import render +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +service_name = 'zabbix-agent2' +service_conf = f'/run/zabbix/{service_name}.conf' +systemd_override = r'/run/systemd/system/zabbix-agent2.service.d/10-override.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['service', 'monitoring', 'zabbix-agent'] + + if not conf.exists(base): + return None + + config = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True) + + # Cut the / from the end, /tmp/ => /tmp + if 'directory' in config and config['directory'].endswith('/'): + config['directory'] = config['directory'][:-1] + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if config is None: + return + + if 'server' not in config: + raise ConfigError('Server is required!') + + +def generate(config): + # bail out early - looks like removal from running config + if config is None: + # Remove old config and return + config_files = [service_conf, systemd_override] + for file in config_files: + if os.path.isfile(file): + os.unlink(file) + + return None + + # Write configuration file + render(service_conf, 'zabbix-agent/zabbix-agent.conf.j2', config) + render(systemd_override, 'zabbix-agent/10-override.conf.j2', config) + + return None + + +def apply(config): + call('systemctl daemon-reload') + if config: + call(f'systemctl restart {service_name}.service') + else: + call(f'systemctl stop {service_name}.service') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index fe33c43ea..dbb47de4e 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -19,10 +19,8 @@ import os from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -35,40 +33,9 @@ def get_config(config=None): else: conf = Config() base = ['service', 'router-advert'] - rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_interface_values = defaults(base + ['interface']) - # we deal with prefix, route defaults later on - if 'prefix' in default_interface_values: - del default_interface_values['prefix'] - if 'route' in default_interface_values: - del default_interface_values['route'] - - default_prefix_values = defaults(base + ['interface', 'prefix']) - default_route_values = defaults(base + ['interface', 'route']) - - if 'interface' in rtradv: - for interface in rtradv['interface']: - rtradv['interface'][interface] = dict_merge( - default_interface_values, rtradv['interface'][interface]) - - if 'prefix' in rtradv['interface'][interface]: - for prefix in rtradv['interface'][interface]['prefix']: - rtradv['interface'][interface]['prefix'][prefix] = dict_merge( - default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) - - if 'route' in rtradv['interface'][interface]: - for route in rtradv['interface'][interface]['route']: - rtradv['interface'][interface]['route'][route] = dict_merge( - default_route_values, rtradv['interface'][interface]['route'][route]) - - if 'name_server' in rtradv['interface'][interface]: - # always use a list when dealing with nameservers - eases the template generation - if isinstance(rtradv['interface'][interface]['name_server'], str): - rtradv['interface'][interface]['name_server'] = [ - rtradv['interface'][interface]['name_server']] + rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return rtradv diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py index 54b72e029..ba5e645f0 100755 --- a/src/conf_mode/service_sla.py +++ b/src/conf_mode/service_sla.py @@ -19,10 +19,8 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -44,11 +42,9 @@ def get_config(config=None): if not conf.exists(base): return None - sla = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - sla = dict_merge(default_values, sla) + sla = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) # Ignore default XML values if config doesn't exists # Delete key from dict diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py index b37d502c2..cf26bf9ce 100755 --- a/src/conf_mode/service_upnp.py +++ b/src/conf_mode/service_upnp.py @@ -23,12 +23,10 @@ from ipaddress import IPv4Network from ipaddress import IPv6Network from vyos.config import Config -from vyos.configdict import dict_merge from vyos.utils.process import call from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -47,10 +45,7 @@ def get_config(config=None): if not upnpd: return None - if 'rule' in upnpd: - default_member_values = defaults(base + ['rule']) - for rule,rule_config in upnpd['rule'].items(): - upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule]) + upnpd = conf.merge_defaults(upnpd, recursive=True) uuidgen = uuid.uuid1() upnpd.update({'uuid': uuidgen}) diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index db4066572..12ae4135e 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -20,14 +20,13 @@ from shutil import rmtree from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.config import config_dict_merge from vyos.template import render from vyos.utils.process import call from vyos.utils.permission import chmod_755 from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos.base import Warning from vyos import ConfigError from vyos import airbag @@ -125,7 +124,8 @@ def get_config(config=None): get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. - default_values = defaults(base) + default_values = conf.get_config_defaults(**proxy.kwargs, + recursive=True) # if no authentication method is supplied, no need to add defaults if not dict_search('authentication.method', proxy): @@ -138,16 +138,7 @@ def get_config(config=None): proxy['squidguard_conf'] = squidguard_config_file proxy['squidguard_db_dir'] = squidguard_db_dir - # XXX: T2665: blend in proper cache-peer default values later - default_values.pop('cache_peer') - proxy = dict_merge(default_values, proxy) - - # XXX: T2665: blend in proper cache-peer default values - if 'cache_peer' in proxy: - default_values = defaults(base + ['cache-peer']) - for peer in proxy['cache_peer']: - proxy['cache_peer'][peer] = dict_merge(default_values, - proxy['cache_peer'][peer]) + proxy = config_dict_merge(default_values, proxy) return proxy diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 4bf67f079..7882f8510 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -31,7 +31,6 @@ from vyos.utils.permission import chmod_755 from vyos.utils.dict import dict_search from vyos.utils.network import is_addr_assigned from vyos.version import get_version_data -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -70,26 +69,9 @@ def get_config(config=None): # 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) - - # We can not merge defaults for tagNodes - those need to be blended in - # per tagNode instance - if 'listen_address' in default_values: - del default_values['listen_address'] - if 'community' in default_values: - del default_values['community'] - if 'trap_target' in default_values: - del default_values['trap_target'] - if 'v3' in default_values: - del default_values['v3'] - snmp = dict_merge(default_values, snmp) + snmp = conf.merge_defaults(snmp, recursive=True) if 'listen_address' in snmp: - default_values = defaults(base + ['listen-address']) - for address in snmp['listen_address']: - snmp['listen_address'][address] = dict_merge( - default_values, snmp['listen_address'][address]) - # Always listen on localhost if an explicit address has been configured # This is a safety measure to not end up with invalid listen addresses # that are not configured on this system. See https://vyos.dev/T850 @@ -101,41 +83,6 @@ def get_config(config=None): tmp = {'::1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) - if 'community' in snmp: - default_values = defaults(base + ['community']) - if 'network' in default_values: - # convert multiple default networks to list - default_values['network'] = default_values['network'].split() - for community in snmp['community']: - snmp['community'][community] = dict_merge( - default_values, snmp['community'][community]) - - if 'trap_target' in snmp: - default_values = defaults(base + ['trap-target']) - for trap in snmp['trap_target']: - snmp['trap_target'][trap] = dict_merge( - default_values, snmp['trap_target'][trap]) - - if 'v3' in snmp: - default_values = defaults(base + ['v3']) - # tagNodes need to be merged in individually later on - for tmp in ['user', 'group', 'trap_target']: - del default_values[tmp] - snmp['v3'] = dict_merge(default_values, snmp['v3']) - - for user_group in ['user', 'group']: - if user_group in snmp['v3']: - default_values = defaults(base + ['v3', user_group]) - for tmp in snmp['v3'][user_group]: - snmp['v3'][user_group][tmp] = dict_merge( - default_values, snmp['v3'][user_group][tmp]) - - if 'trap_target' in snmp['v3']: - default_values = defaults(base + ['v3', 'trap-target']) - for trap in snmp['v3']['trap_target']: - snmp['v3']['trap_target'][trap] = dict_merge( - default_values, snmp['v3']['trap_target'][trap]) - return snmp def verify(snmp): diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 3b63fcb7d..ee5e1eca2 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -21,12 +21,10 @@ from syslog import syslog from syslog import LOG_INFO from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.utils.process import call from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -57,8 +55,8 @@ def get_config(config=None): # 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) - ssh = dict_merge(default_values, ssh) + ssh = conf.merge_defaults(ssh, recursive=True) + # pass config file path - used in override template ssh['config_file'] = config_file diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index c89267afc..9ed34c735 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -24,7 +24,6 @@ from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.system import sysctl_write -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -37,11 +36,9 @@ def get_config(config=None): conf = Config() base = ['system', 'ip'] - opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - opt = dict_merge(default_values, opt) + opt = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) # When working with FRR we need to know the corresponding address-family opt['afi'] = 'ip' diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 22210c27a..8a4df11fa 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -24,7 +24,6 @@ from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.system import sysctl_write from vyos.utils.file import write_file -from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag @@ -37,12 +36,9 @@ def get_config(config=None): conf = Config() base = ['system', 'ipv6'] - opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - opt = dict_merge(default_values, opt) + opt = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) # When working with FRR we need to know the corresponding address-family opt['afi'] = 'ipv6' diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 82941e0c0..02c97afaa 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -24,7 +24,6 @@ from sys import exit from time import sleep from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configverify import verify_vrf from vyos.defaults import directories from vyos.template import render @@ -35,7 +34,6 @@ from vyos.utils.process import call from vyos.utils.process import rc_cmd from vyos.utils.process import run from vyos.utils.process import DEVNULL -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -93,7 +91,9 @@ def get_config(config=None): conf = Config() base = ['system', 'login'] login = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, get_first_key=True) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) # users no longer existing in the running configuration need to be deleted local_users = get_local_users() @@ -101,27 +101,9 @@ def get_config(config=None): if 'user' in login: cli_users = list(login['user']) - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - default_values = defaults(base + ['user']) - for user in login['user']: - login['user'][user] = dict_merge(default_values, login['user'][user]) - - # Add TACACS global defaults - if 'tacacs' in login: - default_values = defaults(base + ['tacacs']) - if 'server' in default_values: - del default_values['server'] - login['tacacs'] = dict_merge(default_values, login['tacacs']) - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - for backend in ['radius', 'tacacs']: - default_values = defaults(base + [backend, 'server']) - for server in dict_search(f'{backend}.server', login) or []: - login[backend]['server'][server] = dict_merge(default_values, - login[backend]['server'][server]) - + # prune TACACS global defaults if not set by user + if login.from_defaults(['tacacs']): + del login['tacacs'] # create a list of all users, cli and users all_users = list(set(local_users + cli_users)) diff --git a/src/conf_mode/system-logs.py b/src/conf_mode/system-logs.py index 12145d641..8ad4875d4 100755 --- a/src/conf_mode/system-logs.py +++ b/src/conf_mode/system-logs.py @@ -19,11 +19,9 @@ from sys import exit from vyos import ConfigError from vyos import airbag from vyos.config import Config -from vyos.configdict import dict_merge from vyos.logger import syslog from vyos.template import render from vyos.utils.dict import dict_search -from vyos.xml import defaults airbag.enable() # path to logrotate configs @@ -38,11 +36,9 @@ def get_config(config=None): conf = Config() base = ['system', 'logs'] - default_values = defaults(base) - logs_config = conf.get_config_dict(base, - key_mangling=('-', '_'), - get_first_key=True) - logs_config = dict_merge(default_values, logs_config) + logs_config = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return logs_config diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 917013651..d92121b3d 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -21,14 +21,12 @@ from sys import exit from time import sleep from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configverify import verify_source_interface from vyos.template import render from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running from vyos.utils.network import is_addr_assigned from vyos.utils.network import is_intf_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -48,12 +46,9 @@ def get_config(config=None): else: conf = Config() base = ['system', 'option'] - options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - options = dict_merge(default_values, options) + options = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return options diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 19c87bcee..07fbb0734 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -19,12 +19,10 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.utils.process import call from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -50,43 +48,9 @@ def get_config(config=None): tmp = is_node_changed(conf, base + ['vrf']) if tmp: syslog.update({'restart_required': {}}) - # 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) - # XXX: some syslog default values can not be merged here (originating from - # a tagNode - remove and add them later per individual tagNode instance - if 'console' in default_values: - del default_values['console'] - for entity in ['global', 'user', 'host', 'file']: - if entity in default_values: - del default_values[entity] - - syslog = dict_merge(default_values, syslog) - - # XXX: add defaults for "console" tree - if 'console' in syslog and 'facility' in syslog['console']: - default_values = defaults(base + ['console', 'facility']) - for facility in syslog['console']['facility']: - syslog['console']['facility'][facility] = dict_merge(default_values, - syslog['console']['facility'][facility]) - - # XXX: add defaults for "host" tree - for syslog_type in ['host', 'user', 'file']: - # Bail out early if there is nothing to do - if syslog_type not in syslog: - continue - - default_values_host = defaults(base + [syslog_type]) - if 'facility' in default_values_host: - del default_values_host['facility'] - - for tmp, tmp_config in syslog[syslog_type].items(): - syslog[syslog_type][tmp] = dict_merge(default_values_host, syslog[syslog_type][tmp]) - if 'facility' in tmp_config: - default_values_facility = defaults(base + [syslog_type, 'facility']) - for facility in tmp_config['facility']: - syslog[syslog_type][tmp]['facility'][facility] = dict_merge(default_values_facility, - syslog[syslog_type][tmp]['facility'][facility]) + syslog = conf.merge_defaults(syslog, recursive=True) + if syslog.from_defaults(['global']): + del syslog['global'] return syslog diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 87d587959..ebf9a113b 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -19,12 +19,10 @@ import re from pathlib import Path from vyos.config import Config -from vyos.configdict import dict_merge from vyos.utils.process import call from vyos.utils.file import read_file from vyos.utils.file import write_file from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -45,16 +43,12 @@ def get_config(config=None): if 'device' not in console: return console - # convert CLI values to system values - default_values = defaults(base + ['device']) for device, device_config in console['device'].items(): if 'speed' not in device_config and device.startswith('hvc'): # XEN console has a different default console speed console['device'][device]['speed'] = 38400 - else: - # Merge in XML defaults - the proper way to do it - console['device'][device] = dict_merge(default_values, - console['device'][device]) + + console = conf.merge_defaults(console, recursive=True) return console diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index eae869a6d..2df1bbb7a 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -19,11 +19,9 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -42,26 +40,9 @@ def get_config(config=None): if not conf.exists(base): return None - sflow = conf.get_config_dict(base, - key_mangling=('-', '_'), - get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - - sflow = dict_merge(default_values, sflow) - - # Ignore default XML values if config doesn't exists - # Delete key from dict - if 'port' in sflow['server']: - del sflow['server']['port'] - - # Set default values per server - if 'server' in sflow: - for server in sflow['server']: - default_values = defaults(base + ['server']) - sflow['server'][server] = dict_merge(default_values, sflow['server'][server]) + sflow = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return sflow diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 32882fc12..3ad346e2e 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -24,14 +24,12 @@ 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 from vyos.template import render from vyos.template import is_ipv4 from vyos.utils.process import call from vyos.utils.permission import chmod_755 from vyos.utils.network import is_addr_assigned -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -48,11 +46,9 @@ def get_config(config=None): if not conf.exists(base): return None - tftpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - tftpd = dict_merge(default_values, tftpd) + tftpd = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) return tftpd def verify(tftpd): diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 9a27a44bf..fa271cbdb 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -27,7 +27,6 @@ from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists -from vyos.configdict import dict_merge from vyos.defaults import directories from vyos.ifconfig import Interface from vyos.pki import encode_public_key @@ -45,7 +44,6 @@ from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import call from vyos.utils.process import run -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() @@ -84,88 +82,23 @@ def get_config(config=None): # retrieve common dictionary keys ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - # XXX: T2665: we must safely remove default values for tag nodes, those are - # added in a more fine grained way later on - del default_values['esp_group'] - del default_values['ike_group'] - del default_values['remote_access'] - del default_values['site_to_site'] - ipsec = dict_merge(default_values, ipsec) - - if 'esp_group' in ipsec: - default_values = defaults(base + ['esp-group']) - for group in ipsec['esp_group']: - ipsec['esp_group'][group] = dict_merge(default_values, - ipsec['esp_group'][group]) - if 'ike_group' in ipsec: - default_values = defaults(base + ['ike-group']) - # proposal is a tag node which may come with individual defaults per node - if 'proposal' in default_values: - del default_values['proposal'] - - for group in ipsec['ike_group']: - ipsec['ike_group'][group] = dict_merge(default_values, - ipsec['ike_group'][group]) - - if 'proposal' in ipsec['ike_group'][group]: - default_values = defaults(base + ['ike-group', 'proposal']) - for proposal in ipsec['ike_group'][group]['proposal']: - ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values, - ipsec['ike_group'][group]['proposal'][proposal]) - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if dict_search('remote_access.connection', ipsec): - default_values = defaults(base + ['remote-access', 'connection']) - for rw in ipsec['remote_access']['connection']: - ipsec['remote_access']['connection'][rw] = dict_merge(default_values, - ipsec['remote_access']['connection'][rw]) - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if dict_search('remote_access.radius.server', ipsec): - # Fist handle the "base" stuff like RADIUS timeout - default_values = defaults(base + ['remote-access', 'radius']) - if 'server' in default_values: - del default_values['server'] - ipsec['remote_access']['radius'] = dict_merge(default_values, - ipsec['remote_access']['radius']) - - # Take care about individual RADIUS servers implemented as tagNodes - this - # requires special treatment - default_values = defaults(base + ['remote-access', 'radius', 'server']) - for server in ipsec['remote_access']['radius']['server']: - ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values, - ipsec['remote_access']['radius']['server'][server]) - - # XXX: T2665: we can not safely rely on the defaults() when there are - # tagNodes in place, it is better to blend in the defaults manually. - if dict_search('site_to_site.peer', ipsec): - default_values = defaults(base + ['site-to-site', 'peer']) - for peer in ipsec['site_to_site']['peer']: - ipsec['site_to_site']['peer'][peer] = dict_merge(default_values, - ipsec['site_to_site']['peer'][peer]) + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True) tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True) if tmp: - ipsec['l2tp'] = tmp - l2tp_defaults = defaults(l2tp_base) - ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp']) + ipsec['l2tp'] = conf.merge_defaults(tmp, recursive=True) ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address']) ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024' ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1' diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index e82862fa3..a039172c4 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -19,7 +19,6 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render @@ -28,7 +27,6 @@ from vyos.utils.network import check_port_availability from vyos.utils.process import is_systemd_service_running from vyos.utils.network import is_listen_port_bind_service from vyos.utils.dict import dict_search -from vyos.xml import defaults from vyos import ConfigError from passlib.hash import sha512_crypt from time import sleep @@ -47,66 +45,6 @@ radius_servers = cfg_dir + '/radius_servers' def get_hash(password): return sha512_crypt.hash(password) - - -def _default_dict_cleanup(origin: dict, default_values: dict) -> dict: - """ - https://vyos.dev/T2665 - Clear unnecessary key values in merged config by dict_merge function - :param origin: config - :type origin: dict - :param default_values: default values - :type default_values: dict - :return: merged dict - :rtype: dict - """ - if 'mode' in origin["authentication"] and "local" in \ - origin["authentication"]["mode"]: - del origin['authentication']['local_users']['username']['otp'] - if not origin["authentication"]["local_users"]["username"]: - raise ConfigError( - 'Openconnect authentication mode local requires at least one user') - default_ocserv_usr_values = \ - default_values['authentication']['local_users']['username']['otp'] - for user, params in origin['authentication']['local_users'][ - 'username'].items(): - # Not every configuration requires OTP settings - if origin['authentication']['local_users']['username'][user].get( - 'otp'): - origin['authentication']['local_users']['username'][user][ - 'otp'] = dict_merge(default_ocserv_usr_values, - origin['authentication'][ - 'local_users']['username'][user][ - 'otp']) - - if 'mode' in origin["authentication"] and "radius" in \ - origin["authentication"]["mode"]: - del origin['authentication']['radius']['server']['port'] - if not origin["authentication"]['radius']['server']: - raise ConfigError( - 'Openconnect authentication mode radius requires at least one RADIUS server') - default_values_radius_port = \ - default_values['authentication']['radius']['server']['port'] - for server, params in origin['authentication']['radius'][ - 'server'].items(): - if 'port' not in params: - params['port'] = default_values_radius_port - - if 'mode' in origin["accounting"] and "radius" in \ - origin["accounting"]["mode"]: - del origin['accounting']['radius']['server']['port'] - if not origin["accounting"]['radius']['server']: - raise ConfigError( - 'Openconnect accounting mode radius requires at least one RADIUS server') - default_values_radius_port = \ - default_values['accounting']['radius']['server']['port'] - for server, params in origin['accounting']['radius'][ - 'server'].items(): - if 'port' not in params: - params['port'] = default_values_radius_port - return origin - - def get_config(config=None): if config: conf = config @@ -116,16 +54,14 @@ def get_config(config=None): if not conf.exists(base): return None - ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - ocserv = dict_merge(default_values, ocserv) - # workaround a "know limitation" - https://vyos.dev/T2665 - ocserv = _default_dict_cleanup(ocserv, default_values) + ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True) return ocserv @@ -142,6 +78,8 @@ def verify(ocserv): # Check accounting if "accounting" in ocserv: if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: + if not origin["accounting"]['radius']['server']: + raise ConfigError('Openconnect accounting mode radius requires at least one RADIUS server') if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: raise ConfigError('Accounting depends on OpenConnect authentication configuration') elif "radius" not in ocserv["authentication"]["mode"]: @@ -150,9 +88,13 @@ def verify(ocserv): # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: - if "local" in ocserv["authentication"]["mode"]: - if "radius" in ocserv["authentication"]["mode"]: + if ("local" in ocserv["authentication"]["mode"] and + "radius" in ocserv["authentication"]["mode"]): raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration') + if "radius" in ocserv["authentication"]["mode"]: + if not ocserv["authentication"]['radius']['server']: + raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server') + if "local" in ocserv["authentication"]["mode"]: if not ocserv["authentication"]["local_users"]: raise ConfigError('openconnect mode local required at least one user') if not ocserv["authentication"]["local_users"]["username"]: diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py index 80ce1e8e3..82c2f236e 100755 --- a/src/conf_mode/vpp.py +++ b/src/conf_mode/vpp.py @@ -22,7 +22,6 @@ from re import search as re_search, MULTILINE as re_M from vyos.config import Config from vyos.configdep import set_dependents, call_dependents -from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete @@ -31,7 +30,6 @@ from vyos.utils.process import rc_cmd from vyos.utils.system import sysctl_read from vyos.utils.system import sysctl_apply from vyos.template import render -from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -94,28 +92,18 @@ def get_config(config=None): if not conf.exists(base): return {'removed_ifaces': removed_ifaces} - config = conf.get_config_dict(base, + config = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, get_first_key=True, - key_mangling=('-', '_'), - no_tag_node_value_mangle=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - if 'interface' in default_values: - del default_values['interface'] - config = dict_merge(default_values, config) + with_recursive_defaults=True) if 'interface' in config: for iface, iface_config in config['interface'].items(): - default_values_iface = defaults(base + ['interface']) - config['interface'][iface] = dict_merge(default_values_iface, config['interface'][iface]) # add an interface to a list of interfaces that need # to be reinitialized after the commit set_dependents('ethernet', conf, iface) - # Get PCI address auto - for iface, iface_config in config['interface'].items(): + # Get PCI address auto if iface_config['pci'] == 'auto': config['interface'][iface]['pci'] = _get_pci_address_by_interface(iface) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index be867b208..37625142c 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -28,6 +28,8 @@ from vyos.template import render from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.network import get_vrf_members +from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import popen @@ -143,7 +145,7 @@ def verify(vrf): raise ConfigError(f'VRF "{name}" table id is mandatory!') # routing table id can't be changed - OS restriction - if os.path.isdir(f'/sys/class/net/{name}'): + if interface_exists(name): tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name))) if tmp and tmp != vrf_config['table']: raise ConfigError(f'VRF "{name}" table id modification not possible!') @@ -195,12 +197,23 @@ def apply(vrf): sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all) for tmp in (dict_search('vrf_remove', vrf) or []): - if os.path.isdir(f'/sys/class/net/{tmp}'): - call(f'ip link delete dev {tmp}') + if interface_exists(tmp): + # T5492: deleting a VRF instance may leafe processes running + # (e.g. dhclient) as there is a depedency ordering issue in the CLI. + # We need to ensure that we stop the dhclient processes first so + # a proper DHCLP RELEASE message is sent + for interface in get_vrf_members(tmp): + vrf_iface = Interface(interface) + vrf_iface.set_dhcp(False) + vrf_iface.set_dhcpv6(False) + # Remove nftables conntrack zone map item nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' cmd(f'nft {nft_del_element}') + # Delete the VRF Kernel interface + call(f'ip link delete dev {tmp}') + if 'name' in vrf: # Separate VRFs in conntrack table # check if table already exists @@ -245,7 +258,7 @@ def apply(vrf): for name, config in vrf['name'].items(): table = config['table'] - if not os.path.isdir(f'/sys/class/net/{name}'): + if not interface_exists(name): # For each VRF apart from your default context create a VRF # interface with a separate routing table call(f'ip link add {name} type vrf table {table}') |