diff options
-rw-r--r-- | data/templates/chrony/chrony.conf.j2 | 17 | ||||
-rw-r--r-- | data/templates/rsyslog/rsyslog.conf.j2 | 4 | ||||
-rw-r--r-- | interface-definitions/service_ntp.xml.in | 80 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_bridge.py | 34 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_ntp.py | 95 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_system_syslog.py | 33 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_bridge.py | 40 | ||||
-rwxr-xr-x | src/conf_mode/service_ntp.py | 20 | ||||
-rwxr-xr-x | src/conf_mode/system_syslog.py | 11 |
9 files changed, 315 insertions, 19 deletions
diff --git a/data/templates/chrony/chrony.conf.j2 b/data/templates/chrony/chrony.conf.j2 index e3f078fdc..2838f5524 100644 --- a/data/templates/chrony/chrony.conf.j2 +++ b/data/templates/chrony/chrony.conf.j2 @@ -42,7 +42,7 @@ user {{ user }} {% if config.pool is vyos_defined %} {% set association = 'pool' %} {% endif %} -{{ association }} {{ server | replace('_', '-') }} iburst {{ 'nts' if config.nts is vyos_defined }} {{ 'noselect' if config.noselect is vyos_defined }} {{ 'prefer' if config.prefer is vyos_defined }} +{{ association }} {{ server | replace('_', '-') }} iburst {{- ' nts' if config.nts is vyos_defined }} {{- ' noselect' if config.noselect is vyos_defined }} {{- ' prefer' if config.prefer is vyos_defined }} {{- ' xleave' if config.interleave is vyos_defined }} {{- ' port ' ~ ptp.port if ptp.port is vyos_defined and config.ptp is vyos_defined }} {% endfor %} {% endif %} @@ -66,3 +66,18 @@ bindaddress {{ address }} binddevice {{ interface }} {% endif %} {% endif %} + +{% if ptp.timestamp.interface is vyos_defined %} +# Enable hardware timestamping on the specified interfaces +{% for iface, iface_config in ptp.timestamp.interface.items() %} +{% if iface == "all" %} +{% set iface = "*" %} +{% endif %} +hwtimestamp {{ iface }} {{- ' rxfilter ' ~ iface_config.receive_filter if iface_config.receive_filter is vyos_defined }} +{% endfor %} +{% endif %} + +{% if ptp.port is vyos_defined %} +# Enable sending and receiving NTP over PTP packets (PTP transport) +ptpport {{ ptp.port }} +{% endif %} diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2 index effc2ea14..7fd592d1f 100644 --- a/data/templates/rsyslog/rsyslog.conf.j2 +++ b/data/templates/rsyslog/rsyslog.conf.j2 @@ -10,6 +10,10 @@ $MarkMessagePeriod {{ global.marker.interval }} $PreserveFQDN on {% endif %} +{% if global.local_host_name is vyos_defined %} +$LocalHostName {{ global.local_host_name }} +{% endif %} + # We always log to /var/log/messages $outchannel global,/var/log/messages,262144,/usr/sbin/logrotate {{ logrotate }} {% if global.facility is vyos_defined %} diff --git a/interface-definitions/service_ntp.xml.in b/interface-definitions/service_ntp.xml.in index c057b62b5..5dc0cd295 100644 --- a/interface-definitions/service_ntp.xml.in +++ b/interface-definitions/service_ntp.xml.in @@ -13,6 +13,74 @@ #include <include/generic-interface.xml.i> #include <include/listen-address.xml.i> #include <include/interface/vrf.xml.i> + <node name="ptp"> + <properties> + <help>Enable Precision Time Protocol (PTP) transport</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>319</defaultValue> + </leafNode> + <node name="timestamp"> + <properties> + <help>Enable timestamping of packets in the NIC hardware</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface to enable timestamping on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <list>all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>Select all interfaces</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + <regex>all</regex> + </constraint> + </properties> + <children> + <leafNode name="receive-filter"> + <properties> + <help>Selects which inbound packets are timestamped by the NIC</help> + <completionHelp> + <list>all ntp ptp none</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All packets are timestamped</description> + </valueHelp> + <valueHelp> + <format>ntp</format> + <description>Only NTP packets are timestamped</description> + </valueHelp> + <valueHelp> + <format>ptp</format> + <description>Only PTP or NTP packets using the PTP transport are timestamped</description> + </valueHelp> + <valueHelp> + <format>none</format> + <description>No packet is timestamped</description> + </valueHelp> + <constraint> + <regex>(all|ntp|ptp|none)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> <leafNode name="leap-second"> <properties> <help>Leap second behavior</help> @@ -86,6 +154,18 @@ <valueless/> </properties> </leafNode> + <leafNode name="ptp"> + <properties> + <help>Use Precision Time Protocol (PTP) transport for the server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="interleave"> + <properties> + <help>Use the interleaved mode for the server</help> + <valueless/> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 124c1fbcb..54c981adc 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -22,6 +22,7 @@ from base_interfaces_test import BasicInterfaceTest from copy import deepcopy from glob import glob +from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.template import ip_from_cidr from vyos.utils.process import cmd @@ -460,5 +461,38 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_interface_config(interface) self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol']) + def test_bridge_delete_with_vxlan_heighbor_suppress(self): + vxlan_if = 'vxlan0' + vni = '123' + br_if = 'br0' + eth0_addr = '192.0.2.2/30' + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress']) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426']) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-address', ip_from_cidr(eth0_addr)]) + self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni]) + + self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if]) + + self.cli_commit() + + self.assertTrue(interface_exists(vxlan_if)) + self.assertTrue(interface_exists(br_if)) + + # cannot delete bridge interface if "neighbor-suppress" parameter is configured for VXLAN interface + self.cli_delete(['interfaces', 'bridge', br_if]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress']) + + self.cli_commit() + + self.assertFalse(interface_exists(br_if)) + + self.cli_delete(['interfaces', 'vxlan', vxlan_if]) + self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index ae45fe2f4..07af4f5eb 100755 --- a/smoketest/scripts/cli/test_service_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value PROCESS_NAME = 'chronyd' NTP_CONF = '/run/chrony/chrony.conf' @@ -165,5 +166,99 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'maxslewrate 1000', config) self.assertIn(f'smoothtime 400 0.001024 leaponly', config) + def test_interleave_option(self): + # "interleave" option differs from some others in that the + # name is not a 1:1 mapping from VyOS config + servers = ['192.0.2.1', '192.0.2.2'] + options = ['prefer'] + + for server in servers: + for option in options: + self.cli_set(base_path + ['server', server, option]) + self.cli_set(base_path + ['server', server, 'interleave']) + + # commit changes + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst ' + ' '.join(options) + ' xleave', config) + + def test_offload_timestamp_default(self): + # Test offloading of NIC timestamp + servers = ['192.0.2.1', '192.0.2.2'] + ptp_port = '8319' + + for server in servers: + self.cli_set(base_path + ['server', server, 'ptp']) + + self.cli_set(base_path + ['ptp', 'port', ptp_port]) + self.cli_set(base_path + ['ptp', 'timestamp', 'interface', 'all']) + + # commit changes + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst port {ptp_port}', config) + + self.assertIn('hwtimestamp *', config) + + def test_ptp_transport(self): + # Test offloading of NIC timestamp + servers = ['192.0.2.1', '192.0.2.2'] + options = ['prefer'] + + default_ptp_port = default_value(base_path + ['ptp', 'port']) + + for server in servers: + for option in options: + self.cli_set(base_path + ['server', server, option]) + self.cli_set(base_path + ['server', server, 'ptp']) + + # commit changes (expected to fail) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # add the required top-level option and commit + self.cli_set(base_path + ['ptp']) + self.cli_commit() + + # Check generated configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('ntsdumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) + + for server in servers: + self.assertIn(f'server {server} iburst ' + ' '.join(options) + f' port {default_ptp_port}', config) + + self.assertIn(f'ptpport {default_ptp_port}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index 45a5b4087..c802ceeeb 100755 --- a/smoketest/scripts/cli/test_system_syslog.py +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -20,6 +20,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.file import read_file +from vyos.utils.process import cmd from vyos.utils.process import process_named_running PROCESS_NAME = 'rsyslogd' @@ -61,19 +62,45 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err']) self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning']) - self.cli_commit() # verify log level and facilities in config file # *.warning /dev/console # *.* @198.51.100.1:999 # kern.err @192.0.2.1:514 - config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')] + config = [ + get_config_value('\*.\*'), + get_config_value('kern.err'), + get_config_value('\*.warning'), + ] expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console'] - for i in range(0,3): + for i in range(0, 3): self.assertIn(expected[i], config[i]) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_syslog_global(self): + self.cli_set(['system', 'host-name', 'vyos']) + self.cli_set(['system', 'domain-name', 'example.local']) + self.cli_set(base_path + ['global', 'marker', 'interval', '600']) + self.cli_set(base_path + ['global', 'preserve-fqdn']) + self.cli_set(base_path + ['global', 'facility', 'kern', 'level', 'err']) + + self.cli_commit() + + config = cmd(f'sudo cat {RSYSLOG_CONF}') + expected = [ + '$MarkMessagePeriod 600', + '$PreserveFQDN on', + 'kern.err', + '$LocalHostName vyos.example.local', + ] + + for e in expected: + self.assertIn(e, config) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 7b2c1ee0b..637db442a 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -53,20 +53,22 @@ def get_config(config=None): tmp = node_changed(conf, base + [ifname, 'member', 'interface']) if tmp: if 'member' in bridge: - bridge['member'].update({'interface_remove' : tmp }) + bridge['member'].update({'interface_remove': {t: {} for t in tmp}}) else: - bridge.update({'member' : {'interface_remove' : tmp }}) - for interface in tmp: - # When using VXLAN member interfaces that are configured for Single - # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to - # re-create VLAN to VNI mappings if required, but only if the interface - # is already live on the system - this must not be done on first commit - if interface.startswith('vxlan') and interface_exists(interface): - set_dependents('vxlan', conf, interface) - # When using Wireless member interfaces we need to inform hostapd - # to properly set-up the bridge - elif interface.startswith('wlan') and interface_exists(interface): - set_dependents('wlan', conf, interface) + bridge.update({'member': {'interface_remove': {t: {} for t in tmp}}}) + for interface in tmp: + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to + # re-create VLAN to VNI mappings if required, but only if the interface + # is already live on the system - this must not be done on first commit + if interface.startswith('vxlan') and interface_exists(interface): + set_dependents('vxlan', conf, interface) + _, vxlan = get_interface_dict(conf, ['interfaces', 'vxlan'], ifname=interface) + bridge['member']['interface_remove'].update({interface: vxlan}) + # When using Wireless member interfaces we need to inform hostapd + # to properly set-up the bridge + elif interface.startswith('wlan') and interface_exists(interface): + set_dependents('wlan', conf, interface) if dict_search('member.interface', bridge) is not None: for interface in list(bridge['member']['interface']): @@ -118,6 +120,16 @@ def get_config(config=None): return bridge def verify(bridge): + # to delete interface or remove a member interface VXLAN first need to check if + # VXLAN does not require to be a member of a bridge interface + if dict_search('member.interface_remove', bridge): + for iface, iface_config in bridge['member']['interface_remove'].items(): + if iface.startswith('vxlan') and dict_search('parameters.neighbor_suppress', iface_config) != None: + raise ConfigError( + f'To detach interface {iface} from bridge you must first ' + f'disable "neighbor-suppress" parameter in the VXLAN interface {iface}' + ) + if 'deleted' in bridge: return None @@ -192,7 +204,7 @@ def apply(bridge): try: call_dependents() except ConfigError: - raise ConfigError('Error updating member interface configuration after changing bridge!') + raise ConfigError(f'Error updating member interface {interface} configuration after changing bridge!') return None diff --git a/src/conf_mode/service_ntp.py b/src/conf_mode/service_ntp.py index 83880fd72..32563aa0e 100755 --- a/src/conf_mode/service_ntp.py +++ b/src/conf_mode/service_ntp.py @@ -17,6 +17,7 @@ import os from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_interface_exists @@ -42,13 +43,21 @@ def get_config(config=None): if not conf.exists(base): return None - ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_defaults=True) + ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) ntp['config_file'] = config_file ntp['user'] = user_group tmp = is_node_changed(conf, base + ['vrf']) if tmp: ntp.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 = conf.get_config_defaults(**ntp.kwargs, recursive=True) + # Only defined PTP default port, if PTP feature is in use + if 'ptp' not in ntp: + del default_values['ptp'] + + ntp = config_dict_merge(default_values, ntp) return ntp def verify(ntp): @@ -87,6 +96,15 @@ def verify(ntp): if ipv6_addresses > 1: raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ') + if 'server' in ntp: + for host, server in ntp['server'].items(): + if 'ptp' in server: + if 'ptp' not in ntp: + raise ConfigError('PTP must be enabled for the NTP service '\ + f'before it can be used for server "{host}"') + else: + break + return None def generate(ntp): diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py index 2497c5bb6..eb2f02eb3 100755 --- a/src/conf_mode/system_syslog.py +++ b/src/conf_mode/system_syslog.py @@ -53,6 +53,17 @@ def get_config(config=None): if syslog.from_defaults(['global']): del syslog['global'] + if ( + 'global' in syslog + and 'preserve_fqdn' in syslog['global'] + and conf.exists(['system', 'host-name']) + and conf.exists(['system', 'domain-name']) + ): + hostname = conf.return_value(['system', 'host-name']) + domain = conf.return_value(['system', 'domain-name']) + fqdn = f'{hostname}.{domain}' + syslog['global']['local_host_name'] = fqdn + return syslog def verify(syslog): |