From e42e32d48489329686ce6642848b189c0d262196 Mon Sep 17 00:00:00 2001
From: khramshinr <khramshinr@gmail.com>
Date: Tue, 15 Oct 2024 17:37:13 +0600
Subject: 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 158e7093e..fb777b2e4 100644
--- a/op-mode-definitions/vrrp.xml.in
+++ b/op-mode-definitions/vrrp.xml.in
@@ -2,23 +2,42 @@
 <interfaceDefinition>
   <node name="show">
     <children>
+      <tagNode name="vrrp">
+        <properties>
+          <help>Show specified VRRP (Virtual Router Redundancy Protocol) group information</help>
+        </properties>
+        <children>
+          <node name="statistics">
+            <properties>
+              <help>Show VRRP statistics</help>
+            </properties>
+            <command>sudo ${vyos_op_scripts_dir}/vrrp.py show_statistics --group-name="$3"</command>
+          </node>
+          <node name="detail">
+            <properties>
+              <help>Show detailed VRRP state information</help>
+            </properties>
+            <command>sudo ${vyos_op_scripts_dir}/vrrp.py show_detail --group-name="$3"</command>
+          </node>
+        </children>
+      </tagNode>
       <node name="vrrp">
         <properties>
           <help>Show VRRP (Virtual Router Redundancy Protocol) information</help>
         </properties>
-        <command>sudo ${vyos_op_scripts_dir}/vrrp.py --summary</command>
+        <command>sudo ${vyos_op_scripts_dir}/vrrp.py show_summary</command>
         <children>
           <node name="statistics">
             <properties>
               <help>Show VRRP statistics</help>
             </properties>
-            <command>sudo ${vyos_op_scripts_dir}/vrrp.py --statistics</command>
+            <command>sudo ${vyos_op_scripts_dir}/vrrp.py show_statistics</command>
           </node>
           <node name="detail">
             <properties>
               <help>Show detailed VRRP state information</help>
             </properties>
-            <command>sudo ${vyos_op_scripts_dir}/vrrp.py --data</command>
+            <command>sudo ${vyos_op_scripts_dir}/vrrp.py show_detail</command>
           </node>
         </children>
       </node>
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
index ee9336d1a..d3d31cc07 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -99,11 +99,11 @@ class VRRP(object):
               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 @@ class VRRP(object):
         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 60be86065..ef1338e23 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 <http://www.gnu.org/licenses/>.
-
+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)
-- 
cgit v1.2.3


From a5a484a50f976b4df7abd0a00a89dfb1512d84cb Mon Sep 17 00:00:00 2001
From: khramshinr <khramshinr@gmail.com>
Date: Thu, 17 Oct 2024 15:30:28 +0600
Subject: 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 d3d31cc07..a3657370f 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -26,34 +26,37 @@ from vyos.utils.file import read_file
 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 @@ class VRRP(object):
 
     @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 @@ class VRRP(object):
         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 @@ class VRRP(object):
         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 @@ class VRRP(object):
             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 @@ class VRRP(object):
 
         # add to the active list disabled instances
         groups.extend(cls.disabled())
-        return(tabulate(groups, headers))
+        return tabulate(groups, headers)
-- 
cgit v1.2.3