summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py5
-rwxr-xr-xsrc/conf_mode/container.py27
-rwxr-xr-xsrc/conf_mode/firewall.py28
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py16
-rwxr-xr-xsrc/conf_mode/high-availability.py8
-rwxr-xr-xsrc/conf_mode/pki.py129
-rwxr-xr-xsrc/conf_mode/policy.py10
-rwxr-xr-xsrc/conf_mode/protocols_eigrp.py123
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py11
-rwxr-xr-xsrc/conf_mode/protocols_rip.py2
-rwxr-xr-xsrc/conf_mode/service_event_handler.py91
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py35
-rwxr-xr-xsrc/conf_mode/service_sla.py113
-rwxr-xr-xsrc/conf_mode/snmp.py10
-rwxr-xr-xsrc/conf_mode/ssh.py19
-rwxr-xr-xsrc/conf_mode/system_frr.py91
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py8
17 files changed, 681 insertions, 45 deletions
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index 311e01529..c4b2bb488 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -116,6 +116,7 @@ def generate(conntrack):
return None
def apply(conntrack):
+ systemd_service = 'conntrackd.service'
if not conntrack:
# Failover mechanism daemon should be indicated that it no longer needs
# to execute conntrackd actions on transition. This is only required
@@ -123,7 +124,7 @@ def apply(conntrack):
if process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl stop conntrackd.service')
+ call(f'systemctl stop {systemd_service}')
return None
# Failover mechanism daemon should be indicated that it needs to execute
@@ -132,7 +133,7 @@ def apply(conntrack):
if not process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl restart conntrackd.service')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 7e1dc5911..2110fd9e0 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -15,13 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import json
from ipaddress import ip_address
from ipaddress import ip_network
from time import sleep
from json import dumps as json_write
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
@@ -110,15 +110,21 @@ def verify(container):
if 'image' not in container_config:
raise ConfigError(f'Container image for "{name}" is mandatory!')
- # verify container image exists locally
- image = container_config['image']
-
# Check if requested container image exists locally. If it does not
- # exist locally - inform the user.
+ # exist locally - inform the user. This is required as there is a
+ # shared container image storage accross all VyOS images. A user can
+ # delete a container image from the system, boot into another version
+ # of VyOS and then it would fail to boot. This is to prevent any
+ # configuration error when container images are deleted from the
+ # global storage. A per image local storage would be a super waste
+ # of diskspace as there will be a full copy (up tu several GB/image)
+ # on upgrade. This is the "cheapest" and fastest solution in terms
+ # of image upgrade and deletion.
+ image = container_config['image']
if run(f'podman image exists {image}') != 0:
- raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\
- f'locally.\nPlease use "add container image {image}" to add it '\
- 'to the system!')
+ Warning(f'Image "{image}" used in contianer "{name}" does not exist '\
+ f'locally. Please use "add container image {image}" to add it '\
+ f'to the system! Container "{name}" will not be started!')
if 'network' in container_config:
if len(container_config['network']) > 1:
@@ -279,6 +285,11 @@ def apply(container):
for name, container_config in container['name'].items():
image = container_config['image']
+ if run(f'podman image exists {image}') != 0:
+ # container image does not exist locally - user already got
+ # informed by a WARNING in verfiy() - bail out early
+ continue
+
if 'disable' in container_config:
# check if there is a container by that name running
tmp = _cmd('podman ps -a --format "{{.Names}}"')
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 6924bf555..335098bf1 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -26,7 +26,13 @@ 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.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
from vyos.template import render
+from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search_args
from vyos.util import process_named_running
@@ -408,6 +414,26 @@ def apply(firewall):
if install_result == 1:
raise ConfigError('Failed to apply firewall')
+ # set fireall group domain-group xxx
+ if 'group' in firewall:
+ if 'domain_group' in firewall['group']:
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ call('systemctl restart vyos-domain-group-resolve.service')
+ for group, group_config in firewall['group']['domain_group'].items():
+ domains = []
+ for address in group_config['address']:
+ domains.append(address)
+ # Add elements to domain-group, try to resolve domain => ip
+ # and add elements to nft set
+ ip_dict = get_ips_domains_dict(domains)
+ elements = sum(ip_dict.values(), [])
+ nft_init_set(group)
+ nft_add_set_elements(group, elements)
+ else:
+ call('systemctl stop vyos-domain-group-resolve.service')
+
if 'state_policy' in firewall and not state_policy_rule_exists():
for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 7f7a98b04..7750c1247 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -22,6 +22,7 @@ import ipaddress
from ipaddress import ip_address
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.ifconfig import Section
@@ -109,6 +110,9 @@ def _nftables_config(configured_ifaces, direction, length=None):
iface_prefix = "o" if direction == "egress" else "i"
rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
+ # Also add IPv6 ingres logging
+ if nftables_table == nftables_nflog_table:
+ nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
# change nftables
for command in nftable_commands:
@@ -172,7 +176,7 @@ def verify(flow_config):
if interface not in Section.interfaces():
# Changed from error to warning to allow adding dynamic interfaces
# and interface templates
- print(f'Warning: Interface "{interface}" is not presented in the system')
+ Warning(f'Interface "{interface}" is not presented in the system')
# check sFlow configuration
if 'sflow' in flow_config:
@@ -200,7 +204,13 @@ def verify(flow_config):
if 'agent_address' in flow_config['sflow']:
tmp = flow_config['sflow']['agent_address']
if not is_addr_assigned(tmp):
- print(f'Warning: Configured "sflow agent-address {tmp}" does not exist in the system!')
+ raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
+
+ # Check if configured netflow source-address exist in the system
+ if 'source_address' in flow_config['sflow']:
+ if not is_addr_assigned(flow_config['sflow']['source_address']):
+ tmp = flow_config['sflow']['source_address']
+ raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
# check NetFlow configuration
if 'netflow' in flow_config:
@@ -212,7 +222,7 @@ def verify(flow_config):
if 'source_address' in flow_config['netflow']:
if not is_addr_assigned(flow_config['netflow']['source_address']):
tmp = flow_config['netflow']['source_address']
- print(f'Warning: Configured "netflow source-address {tmp}" does not exist on the system!')
+ raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
# Check if engine-id compatible with selected protocol version
if 'engine_id' in flow_config['netflow']:
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index f939f9469..e14050dd3 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -28,7 +28,6 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
-from vyos.util import is_systemd_service_running
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -161,12 +160,7 @@ def apply(ha):
call(f'systemctl stop {service_name}')
return None
- # XXX: T3944 - reload keepalived configuration if service is already running
- # to not cause any service disruption when applying changes.
- if is_systemd_service_running(service_name):
- call(f'systemctl reload {service_name}')
- else:
- call(f'systemctl restart {service_name}')
+ call(f'systemctl reload-or-restart {service_name}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index efa3578b4..29ed7b1b7 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -29,12 +29,60 @@ from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
from vyos.util import ask_input
+from vyos.util import call
+from vyos.util import dict_search_args
from vyos.util import dict_search_recursive
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
+# keys to recursively search for under specified path, script to call if update required
+sync_search = [
+ {
+ 'keys': ['certificate'],
+ 'path': ['service', 'https'],
+ 'script': '/usr/libexec/vyos/conf_mode/https.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['interfaces', 'ethernet'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-ethernet.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'],
+ 'path': ['interfaces', 'openvpn'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'],
+ 'path': ['vpn', 'ipsec'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'openconnect'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'sstp'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py'
+ }
+]
+
+# key from other config nodes -> key in pki['changed'] and pki
+sync_translate = {
+ 'certificate': 'certificate',
+ 'ca_certificate': 'ca',
+ 'dh_params': 'dh',
+ 'local_key': 'key_pair',
+ 'remote_key': 'key_pair',
+ 'shared_secret_key': 'openvpn',
+ 'auth_key': 'openvpn',
+ 'crypt_key': 'openvpn'
+}
+
def get_config(config=None):
if config:
conf = config
@@ -47,12 +95,21 @@ def get_config(config=None):
no_tag_node_value_mangle=True)
pki['changed'] = {}
- tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'ca' : tmp})
- tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'certificate' : tmp})
+ tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'dh' : tmp})
+
+ tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'key_pair' : tmp})
+
+ tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'openvpn' : tmp})
+
# We only merge on the defaults of there is a configuration at all
if conf.exists(base):
default_values = defaults(base)
@@ -164,17 +221,30 @@ def verify(pki):
if 'changed' in pki:
# if the list is getting longer, we can move to a dict() and also embed the
# search key as value from line 173 or 176
- for cert_type in ['ca', 'certificate']:
- if not cert_type in pki['changed']:
- continue
- for certificate in pki['changed'][cert_type]:
- if cert_type not in pki or certificate not in pki['changed'][cert_type]:
- if cert_type == 'ca':
- if certificate in dict_search_recursive(pki['system'], 'ca_certificate'):
- raise ConfigError(f'CA certificate "{certificate}" is still in use!')
- elif cert_type == 'certificate':
- if certificate in dict_search_recursive(pki['system'], 'certificate'):
- raise ConfigError(f'Certificate "{certificate}" is still in use!')
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if not node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = " ".join(search['path'] + found_path)
+ raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
return None
@@ -188,7 +258,38 @@ def apply(pki):
if not pki:
return None
- # XXX: restart services if the content of a certificate changes
+ if 'changed' in pki:
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = ' '.join(search['path'] + found_path)
+ print(f'pki: Updating config: {path_str} {found_name}')
+
+ script = search['script']
+ if found_path[0] == 'interfaces':
+ ifname = found_path[2]
+ call(f'VYOS_TAGNODE_VALUE={ifname} {script}')
+ else:
+ call(script)
return None
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index ef6008140..3008a20e0 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -150,6 +150,16 @@ def verify(policy):
tmp = dict_search('match.ipv6.address.prefix_list', rule_config)
if tmp and tmp not in policy.get('prefix_list6', []):
raise ConfigError(f'prefix-list6 {tmp} does not exist!')
+
+ # Specified access_list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.access_list', rule_config)
+ if tmp and tmp not in policy.get('access_list6', []):
+ raise ConfigError(f'access_list6 {tmp} does not exist!')
+
+ # Specified prefix-list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.prefix_list', rule_config)
+ if tmp and tmp not in policy.get('prefix_list6', []):
+ raise ConfigError(f'prefix-list6 {tmp} does not exist!')
# When routing protocols are active some use prefix-lists, route-maps etc.
# to apply the systems routing policy to the learned or redistributed routes.
diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py
new file mode 100755
index 000000000..c1a1a45e1
--- /dev/null
+++ b/src/conf_mode/protocols_eigrp.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+from sys import argv
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'eigrp']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path
+ eigrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: eigrp.update({'vrf' : vrf})
+
+ if not conf.exists(base):
+ eigrp.update({'deleted' : ''})
+ if not vrf:
+ # We are running in the default VRF context, thus we can not delete
+ # our main EIGRP instance if there are dependent EIGRP VRF instances.
+ eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return eigrp
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ eigrp = dict_merge(tmp, eigrp)
+
+ import pprint
+ pprint.pprint(eigrp)
+ return eigrp
+
+def verify(eigrp):
+ pass
+
+def generate(eigrp):
+ if not eigrp or 'deleted' in eigrp:
+ return None
+
+ eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2
+ eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp)
+ eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp)
+
+def apply(eigrp):
+ eigrp_daemon = 'eigrpd'
+ zebra_daemon = 'zebra'
+
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ # Generate empty helper string which can be ammended to FRR commands, it
+ # will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in eigrp:
+ vrf = ' vrf ' + eigrp['vrf']
+
+ frr_cfg.load_configuration(eigrp_daemon)
+ frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_eigrpd_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config'])
+ frr_cfg.commit_configuration(eigrp_daemon)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 92b335085..56939955d 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -81,6 +81,11 @@ def verify(nhrp):
for map_name, map_conf in nhrp_conf['dynamic_map'].items():
if 'nbma_domain_name' not in map_conf:
raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+
+ if 'cisco_authentication' in nhrp_conf:
+ if len(nhrp_conf['cisco_authentication']) > 8:
+ raise ConfigError('Maximum length of the secret is 8 characters!')
+
return None
def generate(nhrp):
@@ -104,8 +109,8 @@ def apply(nhrp):
if rule_handle:
remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle)
- action = 'reload-or-restart' if nhrp and 'tunnel' in nhrp else 'stop'
- run(f'systemctl {action} opennhrp')
+ action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
+ run(f'systemctl {action} opennhrp.service')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index a76c1ce76..c78d90396 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py
new file mode 100755
index 000000000..5440d1056
--- /dev/null
+++ b/src/conf_mode/service_event_handler.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos.util import call, dict_search
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+service_name = 'vyos-event-handler'
+service_conf = Path(f'/run/{service_name}.conf')
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'event-handler', 'event']
+ config = conf.get_config_dict(base,
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config:
+ return None
+
+ for name, event_config in config.items():
+ if not dict_search('filter.pattern', event_config) or not dict_search(
+ 'script.path', event_config):
+ raise ConfigError(
+ 'Event-handler: both pattern and script path items are mandatory'
+ )
+
+ if dict_search('script.environment.message', event_config):
+ raise ConfigError(
+ 'Event-handler: "message" environment variable is reserved for log message text'
+ )
+
+
+def generate(config):
+ if not config:
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ # Write configuration file
+ conf_json = json.dumps(config, indent=4)
+ service_conf.write_text(conf_json)
+
+ return None
+
+
+def apply(config):
+ 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_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 102a87318..daf75d740 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -103,11 +103,28 @@ def get_config(config=None):
'url' in monitoring:
monitoring['influxdb_configured'] = True
+ # Redefine azure group-metrics 'single-table' and 'table-per-metric'
+ if 'azure_data_explorer' in monitoring:
+ if 'single-table' in monitoring['azure_data_explorer']['group_metrics']:
+ monitoring['azure_data_explorer']['group_metrics'] = 'SingleTable'
+ else:
+ monitoring['azure_data_explorer']['group_metrics'] = 'TablePerMetric'
+ # Set azure env
+ if 'authentication' in monitoring['azure_data_explorer']:
+ auth_config = monitoring['azure_data_explorer']['authentication']
+ if {'client_id', 'client_secret', 'tenant_id'} <= set(auth_config):
+ os.environ['AZURE_CLIENT_ID'] = auth_config['client_id']
+ os.environ['AZURE_CLIENT_SECRET'] = auth_config['client_secret']
+ os.environ['AZURE_TENANT_ID'] = auth_config['tenant_id']
+
# Ignore default XML values if config doesn't exists
# Delete key from dict
if not conf.exists(base + ['prometheus-client']):
del monitoring['prometheus_client']
+ if not conf.exists(base + ['azure-data-explorer']):
+ del monitoring['azure_data_explorer']
+
return monitoring
def verify(monitoring):
@@ -124,6 +141,24 @@ def verify(monitoring):
if 'url' not in monitoring:
raise ConfigError(f'Monitoring "url" is mandatory!')
+ # Verify azure-data-explorer
+ if 'azure_data_explorer' in monitoring:
+ if 'authentication' not in monitoring['azure_data_explorer'] or \
+ 'client_id' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'client_secret' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'tenant_id' not in monitoring['azure_data_explorer']['authentication']:
+ raise ConfigError(f'Authentication "client-id, client-secret and tenant-id" are mandatory!')
+
+ if 'database' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "database" is mandatory!')
+
+ if 'url' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "url" is mandatory!')
+
+ if monitoring['azure_data_explorer']['group_metrics'] == 'SingleTable' and \
+ 'table' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "table" name for single-table mode is mandatory!')
+
# Verify Splunk
if 'splunk' in monitoring:
if 'authentication' not in monitoring['splunk'] or \
diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py
new file mode 100755
index 000000000..e7c3ca59c
--- /dev/null
+++ b/src/conf_mode/service_sla.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+owamp_config_dir = '/etc/owamp-server'
+owamp_config_file = f'{owamp_config_dir}/owamp-server.conf'
+systemd_override_owamp = r'/etc/systemd/system/owamp-server.d/20-override.conf'
+
+twamp_config_dir = '/etc/twamp-server'
+twamp_config_file = f'{twamp_config_dir}/twamp-server.conf'
+systemd_override_twamp = r'/etc/systemd/system/twamp-server.d/20-override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'sla']
+ 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)
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['owamp-server']):
+ del sla['owamp_server']
+ if not conf.exists(base + ['twamp-server']):
+ del sla['twamp_server']
+
+ return sla
+
+def verify(sla):
+ if not sla:
+ return None
+
+def generate(sla):
+ if not sla:
+ return None
+
+ render(owamp_config_file, 'sla/owamp-server.conf.j2', sla)
+ render(systemd_override_owamp, 'sla/owamp-override.conf.j2', sla)
+
+ render(twamp_config_file, 'sla/twamp-server.conf.j2', sla)
+ render(systemd_override_twamp, 'sla/twamp-override.conf.j2', sla)
+
+ return None
+
+def apply(sla):
+ owamp_service = 'owamp-server.service'
+ twamp_service = 'twamp-server.service'
+
+ call('systemctl daemon-reload')
+
+ if not sla or 'owamp_server' not in sla:
+ call(f'systemctl stop {owamp_service}')
+
+ if os.path.exists(owamp_config_file):
+ os.unlink(owamp_config_file)
+
+ if not sla or 'twamp_server' not in sla:
+ call(f'systemctl stop {twamp_service}')
+ if os.path.exists(twamp_config_file):
+ os.unlink(twamp_config_file)
+
+ if sla and 'owamp_server' in sla:
+ call(f'systemctl reload-or-restart {owamp_service}')
+
+ if sla and 'twamp_server' in sla:
+ call(f'systemctl reload-or-restart {twamp_service}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index ae060580d..5cd24db32 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -293,7 +293,15 @@ def apply(snmp):
call(f'systemctl restart {systemd_service}')
# Enable AgentX in FRR
- call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+ # This should be done for each daemon individually because common command
+ # works only if all the daemons started with SNMP support
+ frr_daemons_list = [
+ 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra'
+ ]
+ for frr_daemon in frr_daemons_list:
+ call(
+ f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null'
+ )
return None
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 487e8c229..28669694b 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -33,6 +33,9 @@ airbag.enable()
config_file = r'/run/sshd/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
+sshguard_config_file = '/etc/sshguard/sshguard.conf'
+sshguard_whitelist = '/etc/sshguard/whitelist'
+
key_rsa = '/etc/ssh/ssh_host_rsa_key'
key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
@@ -54,6 +57,11 @@ def get_config(config=None):
# pass config file path - used in override template
ssh['config_file'] = config_file
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['dynamic-protection']):
+ del ssh['dynamic_protection']
+
return ssh
def verify(ssh):
@@ -86,6 +94,10 @@ def generate(ssh):
render(config_file, 'ssh/sshd_config.j2', ssh)
render(systemd_override, 'ssh/override.conf.j2', ssh)
+
+ if 'dynamic_protection' in ssh:
+ render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh)
+ render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh)
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -95,7 +107,12 @@ def apply(ssh):
if not ssh:
# SSH access is removed in the commit
call('systemctl stop ssh.service')
+ call('systemctl stop sshguard.service')
return None
+ if 'dynamic_protection' not in ssh:
+ call('systemctl stop sshguard.service')
+ else:
+ call('systemctl restart sshguard.service')
call('systemctl restart ssh.service')
return None
diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py
new file mode 100755
index 000000000..1af0055f6
--- /dev/null
+++ b/src/conf_mode/system_frr.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from pathlib import Path
+from sys import exit
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.logger import syslog
+from vyos.template import render_to_string
+from vyos.util import read_file, write_file, run
+airbag.enable()
+
+# path to daemons config and config status files
+config_file = '/etc/frr/daemons'
+vyos_status_file = '/tmp/vyos-config-status'
+# path to watchfrr for FRR control
+watchfrr = '/usr/lib/frr/watchfrr.sh'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['system', 'frr']
+ frr_config = conf.get_config_dict(base, get_first_key=True)
+
+ return frr_config
+
+
+def verify(frr_config):
+ # Nothing to verify here
+ pass
+
+
+def generate(frr_config):
+ # read daemons config file
+ daemons_config_current = read_file(config_file)
+ # generate new config file
+ daemons_config_new = render_to_string('frr/daemons.frr.tmpl', frr_config)
+ # update configuration file if this is necessary
+ if daemons_config_new != daemons_config_current:
+ syslog.warning('FRR daemons configuration file need to be changed')
+ write_file(config_file, daemons_config_new)
+ frr_config['config_file_changed'] = True
+
+
+def apply(frr_config):
+ # check if this is initial commit during boot or intiated by CLI
+ # if the file exists, this must be CLI commit
+ commit_type_cli = Path(vyos_status_file).exists()
+ # display warning to user
+ if commit_type_cli and frr_config.get('config_file_changed'):
+ # Since FRR restart is not safe thing, better to give
+ # control over this to users
+ print('''
+ You need to reboot a router (preferred) or restart FRR
+ to apply changes in modules settings
+ ''')
+ # restart FRR automatically. DUring the initial boot this should be
+ # safe in most cases
+ if not commit_type_cli and frr_config.get('config_file_changed'):
+ syslog.warning('Restarting FRR to apply changes in modules')
+ run(f'{watchfrr} restart')
+
+
+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/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index db53463cf..23e5162ba 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
+from vyos.configdict import dict_merge
from vyos.configverify import verify_accel_ppp_base_service
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
@@ -50,10 +51,10 @@ def get_config(config=None):
# retrieve common dictionary keys
sstp = get_accel_dict(conf, base, sstp_chap_secrets)
-
if sstp:
sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
return sstp
@@ -121,7 +122,6 @@ def generate(sstp):
ca_cert_name = sstp['ssl']['ca_certificate']
pki_ca = sstp['pki']['ca'][ca_cert_name]
-
write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate']))