diff options
19 files changed, 393 insertions, 426 deletions
diff --git a/data/templates/frr_exporter/frr_exporter.service.j2 b/data/templates/prometheus/frr_exporter.service.j2 index c3892e42b..c3892e42b 100644 --- a/data/templates/frr_exporter/frr_exporter.service.j2 +++ b/data/templates/prometheus/frr_exporter.service.j2 diff --git a/data/templates/node_exporter/node_exporter.service.j2 b/data/templates/prometheus/node_exporter.service.j2 index 62e7e6774..62e7e6774 100644 --- a/data/templates/node_exporter/node_exporter.service.j2 +++ b/data/templates/prometheus/node_exporter.service.j2 diff --git a/debian/control b/debian/control index 76ca83dcd..08b86356a 100644 --- a/debian/control +++ b/debian/control @@ -235,12 +235,12 @@ Depends: squidclient, squidguard, # End "service webproxy" -# For "service monitoring node-exporter" +# For "service monitoring prometheus node-exporter" node-exporter, -# End "service monitoring node-exporter" -# For "service monitoring frr-exporter" +# End "service monitoring prometheus node-exporter" +# For "service monitoring prometheus frr-exporter" frr-exporter, -# End "service monitoring frr-exporter" +# End "service monitoring prometheus frr-exporter" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" diff --git a/interface-definitions/include/version/monitoring-version.xml.i b/interface-definitions/include/version/monitoring-version.xml.i index 6a275a5d8..2e2e0116e 100644 --- a/interface-definitions/include/version/monitoring-version.xml.i +++ b/interface-definitions/include/version/monitoring-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/monitoring-version.xml.i --> -<syntaxVersion component='monitoring' version='1'></syntaxVersion> +<syntaxVersion component='monitoring' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/service_monitoring_frr_exporter.xml.in b/interface-definitions/service_monitoring_frr_exporter.xml.in deleted file mode 100644 index 96aee3ab4..000000000 --- a/interface-definitions/service_monitoring_frr_exporter.xml.in +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="monitoring"> - <children> - <node name="frr-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_frr-exporter.py"> - <properties> - <help>Prometheus exporter for FRR metrics</help> - <priority>1280</priority> - </properties> - <children> - #include <include/listen-address.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>9342</defaultValue> - </leafNode> - #include <include/interface/vrf.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/service_monitoring_node_exporter.xml.in b/interface-definitions/service_monitoring_node_exporter.xml.in deleted file mode 100644 index a11d2304f..000000000 --- a/interface-definitions/service_monitoring_node_exporter.xml.in +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="service"> - <children> - <node name="monitoring"> - <children> - <node name="node-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_node-exporter.py"> - <properties> - <help>Prometheus exporter for hardware and operating system metrics</help> - <priority>1280</priority> - </properties> - <children> - #include <include/listen-address.xml.i> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>9100</defaultValue> - </leafNode> - #include <include/interface/vrf.xml.i> - </children> - </node> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/service_monitoring_prometheus.xml.in b/interface-definitions/service_monitoring_prometheus.xml.in new file mode 100644 index 000000000..24f31e15c --- /dev/null +++ b/interface-definitions/service_monitoring_prometheus.xml.in @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="monitoring"> + <children> + <node name="prometheus" owner="${vyos_conf_scripts_dir}/service_monitoring_prometheus.py"> + <properties> + <help>Prometheus metric exporter</help> + <priority>1280</priority> + </properties> + <children> + <node name="node-exporter"> + <properties> + <help>Prometheus exporter for hardware and operating system metrics</help> + </properties> + <children> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>9100</defaultValue> + </leafNode> + #include <include/interface/vrf.xml.i> + </children> + </node> + <node name="frr-exporter"> + <properties> + <help>Prometheus exporter for FRR metrics</help> + </properties> + <children> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>9342</defaultValue> + </leafNode> + #include <include/interface/vrf.xml.i> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index f6b70be32..c9dc49b3a 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -9,13 +9,13 @@ <properties> <help>Monitor last lines of messages file</help> </properties> - <command>SYSTEMD_LOG_COLOR=false journalctl --no-hostname --follow --boot</command> + <command>SYSTEMD_COLORS=false journalctl --no-hostname --follow --boot</command> <children> <node name="color"> <properties> <help>Output log in a colored fashion</help> </properties> - <command>SYSTEMD_LOG_COLOR=false grc journalctl --no-hostname --follow --boot</command> + <command>SYSTEMD_COLORS=false grc journalctl --no-hostname --follow --boot</command> </node> <node name="ids"> <properties> diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index eac9f61f5..cad1685a9 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -29,6 +29,7 @@ from netifaces import AF_INET6 from netaddr import EUI from netaddr import mac_unix_expanded +from vyos.base import ConfigError from vyos.configdict import list_diff from vyos.configdict import dict_merge from vyos.configdict import get_vlan_ids @@ -43,6 +44,7 @@ from vyos.template import render from vyos.utils.network import mac2eui64 from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.network import get_interface_address from vyos.utils.network import get_interface_namespace from vyos.utils.network import get_vrf_tableid from vyos.utils.network import is_netns_interface @@ -62,7 +64,6 @@ from vyos.ifconfig.control import Control from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational from vyos.ifconfig import Section -from vyos import ConfigError link_local_prefix = 'fe80::/64' @@ -1375,12 +1376,11 @@ class Interface(Control): if enable not in [True, False]: raise ValueError() - ifname = self.ifname config_base = directories['isc_dhclient_dir'] + '/dhclient' - dhclient_config_file = f'{config_base}_{ifname}.conf' - dhclient_lease_file = f'{config_base}_{ifname}.leases' - systemd_override_file = f'/run/systemd/system/dhclient@{ifname}.service.d/10-override.conf' - systemd_service = f'dhclient@{ifname}.service' + dhclient_config_file = f'{config_base}_{self.ifname}.conf' + dhclient_lease_file = f'{config_base}_{self.ifname}.leases' + systemd_override_file = f'/run/systemd/system/dhclient@{self.ifname}.service.d/10-override.conf' + systemd_service = f'dhclient@{self.ifname}.service' # Rendered client configuration files require the apsolute config path self.config['isc_dhclient_dir'] = directories['isc_dhclient_dir'] @@ -1414,6 +1414,21 @@ class Interface(Control): else: if is_systemd_service_active(systemd_service): self._cmd(f'systemctl stop {systemd_service}') + + # Smoketests occationally fail if the lease is not removed from the Kernel fast enough: + # AssertionError: 2 unexpectedly found in {17: [{'addr': '52:54:00:00:00:00', + # 'broadcast': 'ff:ff:ff:ff:ff:ff'}], 2: [{'addr': '192.0.2.103', 'netmask': '255.255.255.0', + # + # We will force removal of any dynamic IPv4 address from the interface + tmp = get_interface_address(self.ifname) + if tmp and 'addr_info' in tmp: + for address_dict in tmp['addr_info']: + # Only remove dynamic assigned addresses + if 'dynamic' not in address_dict: + continue + address = address_dict['local'] + self.del_addr(address) + # cleanup old config files for file in [dhclient_config_file, systemd_override_file, dhclient_lease_file]: if os.path.isfile(file): diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index febf1452d..f80a68d4f 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -115,14 +115,6 @@ class PPPoEIf(Interface): # before this is done by the base class. self._config = config - # remove old routes from an e.g. old VRF assignment - if 'shutdown_required': - vrf = None - tmp = get_interface_config(self.ifname) - if 'master' in tmp: - vrf = tmp['master'] - self._remove_routes(vrf) - # DHCPv6 PD handling is a bit different on PPPoE interfaces, as we do # not require an 'address dhcpv6' CLI option as with other interfaces if 'dhcpv6_options' in config and 'pd' in config['dhcpv6_options']: diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py index 107e262cc..12f6ecf38 100755 --- a/smoketest/scripts/cli/test_protocols_babel.py +++ b/smoketest/scripts/cli/test_protocols_babel.py @@ -50,54 +50,30 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): # check process health and continuity self.assertEqual(self.daemon_pid, process_named_running(babel_daemon)) - def test_babel_interfaces(self): - def_update_interval = default_value(base_path + ['interface', 'eth0', 'update-interval']) - channel = '20' - hello_interval = '1000' - max_rtt_penalty = '100' - rtt_decay = '23' - rtt_max = '119' - rtt_min = '11' - rxcost = '40000' - type = 'wired' + def test_01_basic(self): + diversity_factor = '64' + resend_delay = '100' + smoothing_half_life = '400' - for interface in self._interfaces: - self.cli_set(base_path + ['interface', interface]) - self.cli_set(base_path + ['interface', interface, 'channel', channel]) - self.cli_set(base_path + ['interface', interface, 'enable-timestamps']) - self.cli_set(base_path + ['interface', interface, 'hello-interval', hello_interval]) - self.cli_set(base_path + ['interface', interface, 'max-rtt-penalty', max_rtt_penalty]) - self.cli_set(base_path + ['interface', interface, 'rtt-decay', rtt_decay]) - self.cli_set(base_path + ['interface', interface, 'rtt-max', rtt_max]) - self.cli_set(base_path + ['interface', interface, 'rtt-min', rtt_min]) - self.cli_set(base_path + ['interface', interface, 'enable-timestamps']) - self.cli_set(base_path + ['interface', interface, 'rxcost', rxcost]) - self.cli_set(base_path + ['interface', interface, 'split-horizon', 'disable']) - self.cli_set(base_path + ['interface', interface, 'type', type]) + self.cli_set(base_path + ['parameters', 'diversity']) + self.cli_set(base_path + ['parameters', 'diversity-factor', diversity_factor]) + self.cli_set(base_path + ['parameters', 'resend-delay', resend_delay]) + self.cli_set(base_path + ['parameters', 'smoothing-half-life', smoothing_half_life]) self.cli_commit() frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) - for interface in self._interfaces: - self.assertIn(f' network {interface}', frrconfig) - - iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit', daemon=babel_daemon) - self.assertIn(f' babel channel {channel}', iface_config) - self.assertIn(f' babel enable-timestamps', iface_config) - self.assertIn(f' babel update-interval {def_update_interval}', iface_config) - self.assertIn(f' babel hello-interval {hello_interval}', iface_config) - self.assertIn(f' babel rtt-decay {rtt_decay}', iface_config) - self.assertIn(f' babel rtt-max {rtt_max}', iface_config) - self.assertIn(f' babel rtt-min {rtt_min}', iface_config) - self.assertIn(f' babel rxcost {rxcost}', iface_config) - self.assertIn(f' babel max-rtt-penalty {max_rtt_penalty}', iface_config) - self.assertIn(f' no babel split-horizon', iface_config) - self.assertIn(f' babel {type}', iface_config) + self.assertIn(f' babel diversity', frrconfig) + self.assertIn(f' babel diversity-factor {diversity_factor}', frrconfig) + self.assertIn(f' babel resend-delay {resend_delay}', frrconfig) + self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig) - def test_babel_redistribute(self): + def test_02_redistribute(self): ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'rip', 'static'] ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static'] + self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps']) + for protocol in ipv4_protos: self.cli_set(base_path + ['redistribute', 'ipv4', protocol]) for protocol in ipv6_protos: @@ -113,25 +89,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): protocol = 'ospf6' self.assertIn(f' redistribute ipv6 {protocol}', frrconfig) - def test_babel_basic(self): - diversity_factor = '64' - resend_delay = '100' - smoothing_half_life = '400' - - self.cli_set(base_path + ['parameters', 'diversity']) - self.cli_set(base_path + ['parameters', 'diversity-factor', diversity_factor]) - self.cli_set(base_path + ['parameters', 'resend-delay', resend_delay]) - self.cli_set(base_path + ['parameters', 'smoothing-half-life', smoothing_half_life]) - - self.cli_commit() - - frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) - self.assertIn(f' babel diversity', frrconfig) - self.assertIn(f' babel diversity-factor {diversity_factor}', frrconfig) - self.assertIn(f' babel resend-delay {resend_delay}', frrconfig) - self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig) - - def test_babel_distribute_list(self): + def test_03_distribute_list(self): access_list_in4 = '40' access_list_out4 = '50' access_list_in4_iface = '44' @@ -214,5 +172,48 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in6}-{interface} in {interface}', frrconfig) self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out6}-{interface} out {interface}', frrconfig) + def test_04_interfaces(self): + def_update_interval = default_value(base_path + ['interface', 'eth0', 'update-interval']) + channel = '20' + hello_interval = '1000' + max_rtt_penalty = '100' + rtt_decay = '23' + rtt_max = '119' + rtt_min = '11' + rxcost = '40000' + type = 'wired' + + for interface in self._interfaces: + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', interface, 'channel', channel]) + self.cli_set(base_path + ['interface', interface, 'enable-timestamps']) + self.cli_set(base_path + ['interface', interface, 'hello-interval', hello_interval]) + self.cli_set(base_path + ['interface', interface, 'max-rtt-penalty', max_rtt_penalty]) + self.cli_set(base_path + ['interface', interface, 'rtt-decay', rtt_decay]) + self.cli_set(base_path + ['interface', interface, 'rtt-max', rtt_max]) + self.cli_set(base_path + ['interface', interface, 'rtt-min', rtt_min]) + self.cli_set(base_path + ['interface', interface, 'rxcost', rxcost]) + self.cli_set(base_path + ['interface', interface, 'split-horizon', 'disable']) + self.cli_set(base_path + ['interface', interface, 'type', type]) + + self.cli_commit() + + frrconfig = self.getFRRconfig('router babel', endsection='^exit', daemon=babel_daemon) + for interface in self._interfaces: + self.assertIn(f' network {interface}', frrconfig) + + iface_config = self.getFRRconfig(f'interface {interface}', endsection='^exit', daemon=babel_daemon) + self.assertIn(f' babel channel {channel}', iface_config) + self.assertIn(f' babel enable-timestamps', iface_config) + self.assertIn(f' babel update-interval {def_update_interval}', iface_config) + self.assertIn(f' babel hello-interval {hello_interval}', iface_config) + self.assertIn(f' babel rtt-decay {rtt_decay}', iface_config) + self.assertIn(f' babel rtt-max {rtt_max}', iface_config) + self.assertIn(f' babel rtt-min {rtt_min}', iface_config) + self.assertIn(f' babel rxcost {rxcost}', iface_config) + self.assertIn(f' babel max-rtt-penalty {max_rtt_penalty}', iface_config) + self.assertIn(f' no babel split-horizon', iface_config) + self.assertIn(f' babel {type}', iface_config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 3e14976fc..3ce459e44 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -14,7 +14,6 @@ # 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 import unittest import tempfile diff --git a/smoketest/scripts/cli/test_service_monitoring_node-exporter.py b/smoketest/scripts/cli/test_service_monitoring_node-exporter.py deleted file mode 100755 index e18a3f7a2..000000000 --- a/smoketest/scripts/cli/test_service_monitoring_node-exporter.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 unittest - -from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.utils.process import process_named_running -from vyos.utils.file import read_file - -PROCESS_NAME = 'node_exporter' -base_path = ['service', 'monitoring', 'node-exporter'] -service_file = '/etc/systemd/system/node_exporter.service' -listen_if = 'dum3421' -listen_ip = '192.0.2.1' - - -class TestMonitoringNodeExporter(VyOSUnitTestSHIM.TestCase): - @classmethod - def setUpClass(cls): - # call base-classes classmethod - super(TestMonitoringNodeExporter, cls).setUpClass() - # create a test interfaces - cls.cli_set( - cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] - ) - - @classmethod - def tearDownClass(cls): - cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) - super(TestMonitoringNodeExporter, cls).tearDownClass() - - def tearDown(self): - self.cli_delete(base_path) - self.cli_commit() - self.assertFalse(process_named_running(PROCESS_NAME)) - - def test_01_basic_config(self): - self.cli_set(base_path + ['listen-address', listen_ip]) - - # commit changes - self.cli_commit() - - file_content = read_file(service_file) - self.assertIn(f'{listen_ip}:9100', file_content) - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py b/smoketest/scripts/cli/test_service_monitoring_prometheus.py index 230171c11..dae103e4b 100755 --- a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py +++ b/smoketest/scripts/cli/test_service_monitoring_prometheus.py @@ -20,18 +20,21 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.process import process_named_running from vyos.utils.file import read_file -PROCESS_NAME = 'frr_exporter' -base_path = ['service', 'monitoring', 'frr-exporter'] -service_file = '/etc/systemd/system/frr_exporter.service' +NODE_EXPORTER_PROCESS_NAME = 'node_exporter' +FRR_EXPORTER_PROCESS_NAME = 'frr_exporter' + +base_path = ['service', 'monitoring', 'prometheus'] listen_if = 'dum3421' listen_ip = '192.0.2.1' +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' -class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase): +class TestMonitoringPrometheus(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): # call base-classes classmethod - super(TestMonitoringFrrExporter, cls).setUpClass() + super(TestMonitoringPrometheus, cls).setUpClass() # create a test interfaces cls.cli_set( cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'] @@ -40,24 +43,37 @@ class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) - super(TestMonitoringFrrExporter, cls).tearDownClass() + super(TestMonitoringPrometheus, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) self.cli_commit() - self.assertFalse(process_named_running(PROCESS_NAME)) + self.assertFalse(process_named_running(NODE_EXPORTER_PROCESS_NAME)) + self.assertFalse(process_named_running(FRR_EXPORTER_PROCESS_NAME)) + + def test_01_node_exporter(self): + self.cli_set(base_path + ['node-exporter', 'listen-address', listen_ip]) + + # commit changes + self.cli_commit() + + file_content = read_file(node_exporter_service_file) + self.assertIn(f'{listen_ip}:9100', file_content) + + # Check for running process + self.assertTrue(process_named_running(NODE_EXPORTER_PROCESS_NAME)) - def test_01_basic_config(self): - self.cli_set(base_path + ['listen-address', listen_ip]) + def test_02_frr_exporter(self): + self.cli_set(base_path + ['frr-exporter', 'listen-address', listen_ip]) # commit changes self.cli_commit() - file_content = read_file(service_file) + file_content = read_file(frr_exporter_service_file) self.assertIn(f'{listen_ip}:9342', file_content) # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(process_named_running(FRR_EXPORTER_PROCESS_NAME)) if __name__ == '__main__': diff --git a/src/conf_mode/service_monitoring_frr-exporter.py b/src/conf_mode/service_monitoring_frr-exporter.py deleted file mode 100755 index 01527d579..000000000 --- a/src/conf_mode/service_monitoring_frr-exporter.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 is_node_changed -from vyos.configverify import verify_vrf -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag - - -airbag.enable() - -service_file = '/etc/systemd/system/frr_exporter.service' -systemd_service = 'frr_exporter.service' - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'monitoring', 'frr-exporter'] - if not conf.exists(base): - return None - - config_data = conf.get_config_dict( - base, key_mangling=('-', '_'), get_first_key=True - ) - config_data = conf.merge_defaults(config_data, recursive=True) - - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: - config_data.update({'restart_required': {}}) - - return config_data - - -def verify(config_data): - # bail out early - looks like removal from running config - if not config_data: - return None - - verify_vrf(config_data) - return None - - -def generate(config_data): - if not config_data: - # Delete systemd files - if os.path.isfile(service_file): - os.unlink(service_file) - return None - - # Render frr_exporter service_file - render(service_file, 'frr_exporter/frr_exporter.service.j2', config_data) - return None - - -def apply(config_data): - # Reload systemd manager configuration - call('systemctl daemon-reload') - if not config_data: - call(f'systemctl stop {systemd_service}') - return - - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in config_data: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_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_node-exporter.py b/src/conf_mode/service_monitoring_node-exporter.py deleted file mode 100755 index db34bb5d0..000000000 --- a/src/conf_mode/service_monitoring_node-exporter.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2024 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 is_node_changed -from vyos.configverify import verify_vrf -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag - - -airbag.enable() - -service_file = '/etc/systemd/system/node_exporter.service' -systemd_service = 'node_exporter.service' - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'monitoring', 'node-exporter'] - if not conf.exists(base): - return None - - config_data = conf.get_config_dict( - base, key_mangling=('-', '_'), get_first_key=True - ) - config_data = conf.merge_defaults(config_data, recursive=True) - - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: - config_data.update({'restart_required': {}}) - - return config_data - - -def verify(config_data): - # bail out early - looks like removal from running config - if not config_data: - return None - - verify_vrf(config_data) - return None - - -def generate(config_data): - if not config_data: - # Delete systemd files - if os.path.isfile(service_file): - os.unlink(service_file) - return None - - # Render node_exporter service_file - render(service_file, 'node_exporter/node_exporter.service.j2', config_data) - return None - - -def apply(config_data): - # Reload systemd manager configuration - call('systemctl daemon-reload') - if not config_data: - call(f'systemctl stop {systemd_service}') - return - - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in config_data: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_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_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py new file mode 100755 index 000000000..e0a9fc4ef --- /dev/null +++ b/src/conf_mode/service_monitoring_prometheus.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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 is_node_changed +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag + + +airbag.enable() + +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +node_exporter_systemd_service = 'node_exporter.service' + +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' +frr_exporter_systemd_service = 'frr_exporter.service' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'prometheus'] + if not conf.exists(base): + return None + + monitoring = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True + ) + + tmp = is_node_changed(conf, base + ['node-exporter', 'vrf']) + if tmp: + monitoring.update({'node_exporter_restart_required': {}}) + + tmp = is_node_changed(conf, base + ['frr-exporter', 'vrf']) + if tmp: + monitoring.update({'frr_exporter_restart_required': {}}) + + return monitoring + + +def verify(monitoring): + if not monitoring: + return None + + if 'node_exporter' in monitoring: + verify_vrf(monitoring['node_exporter']) + + if 'frr_exporter' in monitoring: + verify_vrf(monitoring['frr_exporter']) + + return None + + +def generate(monitoring): + if not monitoring or 'node_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(node_exporter_service_file): + os.unlink(node_exporter_service_file) + + if not monitoring or 'frr_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(frr_exporter_service_file): + os.unlink(frr_exporter_service_file) + + if not monitoring: + return None + + if 'node_exporter' in monitoring: + # Render node_exporter node_exporter_service_file + render( + node_exporter_service_file, + 'prometheus/node_exporter.service.j2', + monitoring['node_exporter'], + ) + + if 'frr_exporter' in monitoring: + # Render frr_exporter service_file + render( + frr_exporter_service_file, + 'prometheus/frr_exporter.service.j2', + monitoring['frr_exporter'], + ) + + return None + + +def apply(monitoring): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not monitoring or 'node_exporter' not in monitoring: + call(f'systemctl stop {node_exporter_systemd_service}') + if not monitoring or 'frr_exporter' not in monitoring: + call(f'systemctl stop {frr_exporter_systemd_service}') + + if not monitoring: + return + + if 'node_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'node_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {node_exporter_systemd_service}') + + if 'frr_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'frr_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {frr_exporter_systemd_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/migration-scripts/monitoring/1-to-2 b/src/migration-scripts/monitoring/1-to-2 new file mode 100644 index 000000000..8bdaebae9 --- /dev/null +++ b/src/migration-scripts/monitoring/1-to-2 @@ -0,0 +1,50 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T6953: merge node and frr exporter under prometheus section + +from vyos.configtree import ConfigTree + +old_base = ['service', 'monitoring'] +new_base = ['service', 'monitoring', 'prometheus'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(old_base): + # Nothing to do + return + + if config.exists(old_base + ['node-exporter']): + if config.exists(old_base + ['node-exporter', 'listen-address']): + tmp = config.return_value(old_base + ['node-exporter', 'listen-address']) + config.set(new_base + ['node-exporter', 'listen-address'], value=tmp) + if config.exists(old_base + ['node-exporter', 'port']): + tmp = config.return_value(old_base + ['node-exporter', 'port']) + config.set(new_base + ['node-exporter', 'port'], value=tmp) + if config.exists(old_base + ['node-exporter', 'vrf']): + tmp = config.return_value(old_base + ['node-exporter', 'vrf']) + config.set(new_base + ['node-exporter', 'vrf'], value=tmp) + config.delete(old_base + ['node-exporter']) + + if config.exists(old_base + ['frr-exporter']): + if config.exists(old_base + ['frr-exporter', 'listen-address']): + tmp = config.return_value(old_base + ['frr-exporter', 'listen-address']) + config.set(new_base + ['frr-exporter', 'listen-address'], value=tmp) + if config.exists(old_base + ['frr-exporter', 'port']): + tmp = config.return_value(old_base + ['frr-exporter', 'port']) + config.set(new_base + ['frr-exporter', 'port'], value=tmp) + if config.exists(old_base + ['frr-exporter', 'vrf']): + tmp = config.return_value(old_base + ['frr-exporter', 'vrf']) + config.set(new_base + ['frr-exporter', 'vrf'], value=tmp) + config.delete(old_base + ['frr-exporter']) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index e5455c8af..1429fd7b1 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -19,6 +19,7 @@ import sys import typing from datetime import datetime +from datetime import timezone from glob import glob from ipaddress import ip_address from tabulate import tabulate @@ -62,7 +63,7 @@ def _format_hex_string(in_str): return out_str -def _find_list_of_dict_index(lst, key='ip', value='') -> int: +def _find_list_of_dict_index(lst, key='ip', value=''): """ Find the index entry of list of dict matching the dict value Exampe: @@ -82,7 +83,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig inet_suffix = '6' if family == 'inet6' else '4' try: leases = kea_get_leases(inet_suffix) - except: + except Exception: raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server lease information') if pool is None: @@ -92,7 +93,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig try: active_config = kea_get_active_config(inet_suffix) - except: + except Exception: raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration') data = [] @@ -110,11 +111,14 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet-id']) if active_config else '-' data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None data_lease['origin'] = 'local' # TODO: Determine remote in HA + data_lease['hostname'] = lease.get('hostname', '-') + # remove trailing dot to ensure consistency for `vyos-hostsd-client` + if data_lease['hostname'][-1] == '.': + data_lease['hostname'] = data_lease['hostname'][:-1] if family == 'inet': data_lease['mac'] = lease['hw-address'] data_lease['start'] = lease['start_timestamp'].timestamp() - data_lease['hostname'] = lease['hostname'] if family == 'inet6': data_lease['last_communication'] = lease['start_timestamp'].timestamp() @@ -128,12 +132,12 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig data_lease['remaining'] = '-' if lease['valid-lft'] > 0: - data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() + data_lease['remaining'] = lease['expire_timestamp'] - datetime.now(timezone.utc) if data_lease['remaining'].days >= 0: # substraction gives us a timedelta object which can't be formatted with strftime # so we use str(), split gets rid of the microseconds - data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + data_lease['remaining'] = str(data_lease['remaining']).split('.')[0] # Do not add old leases if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': @@ -148,7 +152,8 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig checked.append(addr) else: idx = _find_list_of_dict_index(data, key='ip', value=addr) - data.pop(idx) + if idx is not None: + data.pop(idx) if sorted: if sorted == 'ip': @@ -279,22 +284,36 @@ def _get_raw_server_static_mappings(family='inet', pool=None, sorted=None): if sorted: if sorted == 'ip': - data.sort(key = lambda x:ip_address(x['ip-address'])) + if family == 'inet6': + mappings.sort(key = lambda x:ip_address(x['ipv6-address'])) + else: + mappings.sort(key = lambda x:ip_address(x['ip-address'])) else: - data.sort(key = lambda x:x[sorted]) + mappings.sort(key = lambda x:x[sorted]) return mappings def _get_formatted_server_static_mappings(raw_data, family='inet'): data_entries = [] - for entry in raw_data: - pool = entry.get('pool') - subnet = entry.get('subnet') - name = entry.get('name') - ip_addr = entry.get('ip-address', 'N/A') - mac_addr = entry.get('mac', 'N/A') - duid = entry.get('duid', 'N/A') - description = entry.get('description', 'N/A') - data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description]) + if family == 'inet': + for entry in raw_data: + pool = entry.get('pool') + subnet = entry.get('subnet') + name = entry.get('name') + ip_addr = entry.get('ip-address', 'N/A') + mac_addr = entry.get('mac', 'N/A') + duid = entry.get('duid', 'N/A') + description = entry.get('description', 'N/A') + data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description]) + elif family == 'inet6': + for entry in raw_data: + pool = entry.get('pool') + subnet = entry.get('subnet') + name = entry.get('name') + ip_addr = entry.get('ipv6-address', 'N/A') + mac_addr = entry.get('mac', 'N/A') + duid = entry.get('duid', 'N/A') + description = entry.get('description', 'N/A') + data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description]) headers = ['Pool', 'Subnet', 'Name', 'IP Address', 'MAC Address', 'DUID', 'Description'] output = tabulate(data_entries, headers, numalign='left') @@ -445,7 +464,8 @@ def _get_raw_client_leases(family='inet', interface=None): if 'interface' in tmp: vrf = get_interface_vrf(tmp['interface']) - if vrf: tmp.update({'vrf' : vrf}) + if vrf: + tmp.update({'vrf' : vrf}) lease_data.append(tmp) |