From e42e32d48489329686ce6642848b189c0d262196 Mon Sep 17 00:00:00 2001 From: khramshinr Date: Tue, 15 Oct 2024 17:37:13 +0600 Subject: [PATCH 1/2] T4583: Rewrite VRRP op-mode to vyos.opmode format --- op-mode-definitions/vrrp.xml.in | 25 ++- python/vyos/ifconfig/vrrp.py | 6 +- src/op_mode/vrrp.py | 355 ++++++++++++++++++++++++++++---- 3 files changed, 341 insertions(+), 45 deletions(-) diff --git a/op-mode-definitions/vrrp.xml.in b/op-mode-definitions/vrrp.xml.in index 158e7093e0..fb777b2e4c 100644 --- a/op-mode-definitions/vrrp.xml.in +++ b/op-mode-definitions/vrrp.xml.in @@ -2,23 +2,42 @@ + + + Show specified VRRP (Virtual Router Redundancy Protocol) group information + + + + + Show VRRP statistics + + sudo ${vyos_op_scripts_dir}/vrrp.py show_statistics --group-name="$3" + + + + Show detailed VRRP state information + + sudo ${vyos_op_scripts_dir}/vrrp.py show_detail --group-name="$3" + + + Show VRRP (Virtual Router Redundancy Protocol) information - sudo ${vyos_op_scripts_dir}/vrrp.py --summary + sudo ${vyos_op_scripts_dir}/vrrp.py show_summary Show VRRP statistics - sudo ${vyos_op_scripts_dir}/vrrp.py --statistics + sudo ${vyos_op_scripts_dir}/vrrp.py show_statistics Show detailed VRRP state information - sudo ${vyos_op_scripts_dir}/vrrp.py --data + sudo ${vyos_op_scripts_dir}/vrrp.py show_detail diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index ee9336d1a3..d3d31cc073 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -99,11 +99,11 @@ def collect(cls, what): timeout=30) return read_file(fname) + except FileNotFoundError: + raise VRRPNoData("VRRP data is not available (process not running or no active groups)") except OSError: # raised by vyos.utils.file.read_file raise VRRPNoData("VRRP data is not available (wait time exceeded)") - except FileNotFoundError: - raise VRRPNoData("VRRP data is not available (process not running or no active groups)") except Exception: name = cls._name[what] raise VRRPError(f'VRRP {name} is not available') @@ -136,7 +136,7 @@ def format(cls, data): headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"] groups = [] - data = json.loads(data) + data = json.loads(data) if isinstance(data, str) else data for group in data: data = group['data'] diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 60be860651..ef1338e23f 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -13,47 +13,324 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +import json import sys -import argparse +import typing + +from jinja2 import Template -from vyos.configquery import ConfigTreeQuery -from vyos.ifconfig.vrrp import VRRP +import vyos.opmode +from vyos.ifconfig import VRRP from vyos.ifconfig.vrrp import VRRPNoData -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary") -group.add_argument("-t", "--statistics", action="store_true", help="Print VRRP statistics") -group.add_argument("-d", "--data", action="store_true", help="Print detailed VRRP data") - -args = parser.parse_args() - -def is_configured(): - """ Check if VRRP is configured """ - config = ConfigTreeQuery() - if not config.exists(['high-availability', 'vrrp', 'group']): - return False - return True - -# Exit early if VRRP is dead or not configured -if is_configured() == False: - print('VRRP not configured!') - exit(0) -if not VRRP.is_running(): - print('VRRP is not running') - sys.exit(0) - -try: - if args.summary: - print(VRRP.format(VRRP.collect('json'))) - elif args.statistics: - print(VRRP.collect('stats')) - elif args.data: - print(VRRP.collect('state')) - else: - parser.print_help() + +stat_template = Template(""" +{% for rec in instances %} +VRRP Instance: {{rec.instance}} + Advertisements: + Received: {{rec.advert_rcvd}} + Sent: {{rec.advert_sent}} + Became master: {{rec.become_master}} + Released master: {{rec.release_master}} + Packet Errors: + Length: {{rec.packet_len_err}} + TTL: {{rec.ip_ttl_err}} + Invalid Type: {{rec.invalid_type_rcvd}} + Advertisement Interval: {{rec.advert_interval_err}} + Address List: {{rec.addr_list_err}} + Authentication Errors: + Invalid Type: {{rec.invalid_authtype}} + Type Mismatch: {{rec.authtype_mismatch}} + Failure: {{rec.auth_failure}} + Priority Zero: + Received: {{rec.pri_zero_rcvd}} + Sent: {{rec.pri_zero_sent}} +{% endfor %} +""") + +detail_template = Template(""" +{%- for rec in instances %} + VRRP Instance: {{rec.iname}} + VRRP Version: {{rec.version}} + State: {{rec.state}} + {% if rec.state == 'BACKUP' -%} + Master priority: {{ rec.master_priority }} + {% if rec.version == 3 -%} + Master advert interval: {{ rec.master_adver_int }} + {% endif -%} + {% endif -%} + Wantstate: {{rec.wantstate}} + Last transition: {{rec.last_transition}} + Interface: {{rec.ifp_ifname}} + {% if rec.dont_track_primary > 0 -%} + VRRP interface tracking disabled + {% endif -%} + {% if rec.skip_check_adv_addr > 0 -%} + Skip checking advert IP addresses + {% endif -%} + {% if rec.strict_mode > 0 -%} + Enforcing strict VRRP compliance + {% endif -%} + Gratuitous ARP delay: {{rec.garp_delay}} + Gratuitous ARP repeat: {{rec.garp_rep}} + Gratuitous ARP refresh: {{rec.garp_refresh}} + Gratuitous ARP refresh repeat: {{rec.garp_refresh_rep}} + Gratuitous ARP lower priority delay: {{rec.garp_lower_prio_delay}} + Gratuitous ARP lower priority repeat: {{rec.garp_lower_prio_rep}} + Send advert after receive lower priority advert: {{rec.lower_prio_no_advert}} + Send advert after receive higher priority advert: {{rec.higher_prio_send_advert}} + Virtual Router ID: {{rec.vrid}} + Priority: {{rec.base_priority}} + Effective priority: {{rec.effective_priority}} + Advert interval: {{rec.adver_int}} sec + Accept: {{rec.accept}} + Preempt: {{rec.nopreempt}} + {% if rec.preempt_delay -%} + Preempt delay: {{rec.preempt_delay}} + {% endif -%} + Promote secondaries: {{rec.promote_secondaries}} + Authentication type: {{rec.auth_type}} + {% if rec.vips %} + Virtual IP ({{ rec.vips | length }}): + {% for ip in rec.vips -%} + {{ip}} + {% endfor -%} + {% endif -%} + {% if rec.evips %} + Virtual IP Excluded: + {% for ip in rec.evips -%} + {{ip}} + {% endfor -%} + {% endif -%} + {% if rec.vroutes %} + Virtual Routes: + {% for route in rec.vroutes -%} + {{route}} + {% endfor -%} + {% endif -%} + {% if rec.vrules %} + Virtual Rules: + {% for rule in rec.vrules -%} + {{rule}} + {% endfor -%} + {% endif -%} + {% if rec.track_ifp %} + Tracked interfaces: + {% for ifp in rec.track_ifp -%} + {{ifp}} + {% endfor -%} + {% endif -%} + {% if rec.track_script %} + Tracked scripts: + {% for script in rec.track_script -%} + {{script}} + {% endfor -%} + {% endif %} + Using smtp notification: {{rec.smtp_alert}} + Notify deleted: {{rec.notify_deleted}} +{% endfor %} +""") + +# https://github.com/acassen/keepalived/blob/59c39afe7410f927c9894a1bafb87e398c6f02be/keepalived/include/vrrp.h#L126 +VRRP_AUTH_NONE = 0 +VRRP_AUTH_PASS = 1 +VRRP_AUTH_AH = 2 + +# https://github.com/acassen/keepalived/blob/59c39afe7410f927c9894a1bafb87e398c6f02be/keepalived/include/vrrp.h#L417 +VRRP_STATE_INIT = 0 +VRRP_STATE_BACK = 1 +VRRP_STATE_MAST = 2 +VRRP_STATE_FAULT = 3 + +VRRP_AUTH_TO_NAME = { + VRRP_AUTH_NONE: 'NONE', + VRRP_AUTH_PASS: 'SIMPLE_PASSWORD', + VRRP_AUTH_AH: 'IPSEC_AH', +} + +VRRP_STATE_TO_NAME = { + VRRP_STATE_INIT: 'INIT', + VRRP_STATE_BACK: 'BACKUP', + VRRP_STATE_MAST: 'MASTER', + VRRP_STATE_FAULT: 'FAULT', +} + + +def _get_raw_data(group_name: str = None) -> list: + """ + Retrieve raw JSON data for all VRRP groups. + + Args: + group_name (str, optional): If provided, filters the data to only + include the specified vrrp group. + + Returns: + list: A list of raw JSON data for VRRP groups, filtered by group_name + if specified. + """ + try: + output = VRRP.collect('json') + except VRRPNoData as e: + raise vyos.opmode.DataUnavailable(f'{e}') + + data = json.loads(output) + + if not data: + return [] + + if group_name is not None: + for rec in data: + if rec['data'].get('iname') == group_name: + return [rec] + return [] + return data + + +def _get_formatted_statistics_output(data: list) -> str: + """ + Prepare formatted statistics output from the given data. + + Args: + data (list): A list of dictionaries containing vrrp grop information + and statistics. + + Returns: + str: Rendered statistics output based on the provided data. + """ + instances = list() + for instance in data: + instances.append( + {'instance': instance['data'].get('iname'), **instance['stats']} + ) + + return stat_template.render(instances=instances) + + +def _process_field(data: dict, field: str, true_value: str, false_value: str): + """ + Updates the given field in the data dictionary with a specified value based + on its truthiness. + + Args: + data (dict): The dictionary containing the field to be processed. + field (str): The key representing the field in the dictionary. + true_value (str): The value to set if the field's value is truthy. + false_value (str): The value to set if the field's value is falsy. + + Returns: + None: The function modifies the dictionary in place. + """ + data[field] = true_value if data.get(field) else false_value + + +def _get_formatted_detail_output(data: list) -> str: + """ + Prepare formatted detail information output from the given data. + + Args: + data (list): A list of dictionaries containing vrrp grop information + and statistics. + + Returns: + str: Rendered detail info output based on the provided data. + """ + instances = list() + for instance in data: + instance['data']['state'] = VRRP_STATE_TO_NAME.get( + instance['data'].get('state'), 'unknown' + ) + instance['data']['wantstate'] = VRRP_STATE_TO_NAME.get( + instance['data'].get('wantstate'), 'unknown' + ) + instance['data']['auth_type'] = VRRP_AUTH_TO_NAME.get( + instance['data'].get('auth_type'), 'unknown' + ) + _process_field(instance['data'], 'lower_prio_no_advert', 'false', 'true') + _process_field(instance['data'], 'higher_prio_send_advert', 'true', 'false') + _process_field(instance['data'], 'accept', 'Enabled', 'Disabled') + _process_field(instance['data'], 'notify_deleted', 'Deleted', 'Fault') + _process_field(instance['data'], 'smtp_alert', 'yes', 'no') + _process_field(instance['data'], 'nopreempt', 'Disabled', 'Enabled') + _process_field(instance['data'], 'promote_secondaries', 'Enabled', 'Disabled') + instance['data']['vips'] = instance['data'].get('vips', False) + instance['data']['evips'] = instance['data'].get('evips', False) + instance['data']['vroutes'] = instance['data'].get('vroutes', False) + instance['data']['vrules'] = instance['data'].get('vrules', False) + + instances.append(instance['data']) + + return detail_template.render(instances=instances) + + +def show_detail( + raw: bool, group_name: typing.Optional[str] = None +) -> typing.Union[list, str]: + """ + Display detailed information about the VRRP group. + + Args: + raw (bool): If True, return raw data instead of formatted output. + group_name (str, optional): Filter the data by a specific group name, + if provided. + + Returns: + list or str: Raw data if `raw` is True, otherwise a formatted detail + output. + """ + data = _get_raw_data(group_name) + + if raw: + return data + + return _get_formatted_detail_output(data) + + +def show_statistics( + raw: bool, group_name: typing.Optional[str] = None +) -> typing.Union[list, str]: + """ + Display VRRP group statistics. + + Args: + raw (bool): If True, return raw data instead of formatted output. + group_name (str, optional): Filter the data by a specific group name, + if provided. + + Returns: + list or str: Raw data if `raw` is True, otherwise a formatted statistic + output. + """ + data = _get_raw_data(group_name) + + if raw: + return data + + return _get_formatted_statistics_output(data) + + +def show_summary(raw: bool) -> typing.Union[list, str]: + """ + Display a summary of VRRP group. + + Args: + raw (bool): If True, return raw data instead of formatted output. + + Returns: + list or str: Raw data if `raw` is True, otherwise a formatted summary output. + """ + data = _get_raw_data() + + if raw: + return data + + return VRRP.format(data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) sys.exit(1) -except VRRPNoData as e: - print(e) - sys.exit(1) From a5a484a50f976b4df7abd0a00a89dfb1512d84cb Mon Sep 17 00:00:00 2001 From: khramshinr Date: Thu, 17 Oct 2024 15:30:28 +0600 Subject: [PATCH 2/2] T4583: Rewrite VRRP op-mode to vyos.opmode format reformat file by linter rules --- python/vyos/ifconfig/vrrp.py | 64 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index d3d31cc073..a3657370f5 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -26,34 +26,37 @@ from vyos.utils.file import wait_for_file_write_complete from vyos.utils.process import process_running + class VRRPError(Exception): pass + class VRRPNoData(VRRPError): pass + class VRRP(object): _vrrp_prefix = '00:00:5E:00:01:' location = { - 'pid': '/run/keepalived/keepalived.pid', - 'fifo': '/run/keepalived/keepalived_notify_fifo', - 'state': '/tmp/keepalived.data', - 'stats': '/tmp/keepalived.stats', - 'json': '/tmp/keepalived.json', - 'daemon': '/etc/default/keepalived', - 'config': '/run/keepalived/keepalived.conf', + 'pid': '/run/keepalived/keepalived.pid', + 'fifo': '/run/keepalived/keepalived_notify_fifo', + 'state': '/tmp/keepalived.data', + 'stats': '/tmp/keepalived.stats', + 'json': '/tmp/keepalived.json', + 'daemon': '/etc/default/keepalived', + 'config': '/run/keepalived/keepalived.conf', } _signal = { - 'state': signal.SIGUSR1, - 'stats': signal.SIGUSR2, - 'json': signal.SIGRTMIN + 2, + 'state': signal.SIGUSR1, + 'stats': signal.SIGUSR2, + 'json': signal.SIGRTMIN + 2, } _name = { 'state': 'information', 'stats': 'statistics', - 'json': 'data', + 'json': 'data', } state = { @@ -64,7 +67,7 @@ class VRRP(object): # UNKNOWN } - def __init__(self,ifname): + def __init__(self, ifname): self.ifname = ifname def enabled(self): @@ -79,7 +82,7 @@ def active_interfaces(cls): @classmethod def decode_state(cls, code): - return cls.state.get(code,'UNKNOWN') + return cls.state.get(code, 'UNKNOWN') # used in conf mode @classmethod @@ -94,16 +97,20 @@ def collect(cls, what): try: # send signal to generate the configuration file pid = read_file(cls.location['pid']) - wait_for_file_write_complete(fname, - pre_hook=(lambda: os.kill(int(pid), cls._signal[what])), - timeout=30) + wait_for_file_write_complete( + fname, + pre_hook=(lambda: os.kill(int(pid), cls._signal[what])), + timeout=30, + ) return read_file(fname) except FileNotFoundError: - raise VRRPNoData("VRRP data is not available (process not running or no active groups)") + raise VRRPNoData( + 'VRRP data is not available (process not running or no active groups)' + ) except OSError: # raised by vyos.utils.file.read_file - raise VRRPNoData("VRRP data is not available (wait time exceeded)") + raise VRRPNoData('VRRP data is not available (wait time exceeded)') except Exception: name = cls._name[what] raise VRRPError(f'VRRP {name} is not available') @@ -118,22 +125,31 @@ def disabled(cls): conf = ConfigTreeQuery() if conf.exists(base): # Read VRRP configuration directly from CLI - vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) + vrrp_config_dict = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True + ) # add disabled groups to the list if 'group' in vrrp_config_dict: for group, group_config in vrrp_config_dict['group'].items(): if 'disable' not in group_config: continue - disabled.append([group, group_config['interface'], group_config['vrid'], 'DISABLED', '']) + disabled.append( + [ + group, + group_config['interface'], + group_config['vrid'], + 'DISABLED', + '', + ] + ) # return list with disabled instances return disabled @classmethod def format(cls, data): - headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"] + headers = ['Name', 'Interface', 'VRID', 'State', 'Priority', 'Last Transition'] groups = [] data = json.loads(data) if isinstance(data, str) else data @@ -143,7 +159,7 @@ def format(cls, data): name = data['iname'] intf = data['ifp_ifname'] vrid = data['vrid'] - state = cls.decode_state(data["state"]) + state = cls.decode_state(data['state']) priority = data['effective_priority'] since = int(time() - float(data['last_transition'])) @@ -153,4 +169,4 @@ def format(cls, data): # add to the active list disabled instances groups.extend(cls.disabled()) - return(tabulate(groups, headers)) + return tabulate(groups, headers)