# Copyright 2019-2024 VyOS maintainers and contributors # # 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 . import os import json import signal from time import time from tabulate import tabulate from vyos.configquery import ConfigTreeQuery from vyos.utils.convert import seconds_to_human 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', } _signal = { 'state': signal.SIGUSR1, 'stats': signal.SIGUSR2, 'json': signal.SIGRTMIN + 2, } _name = { 'state': 'information', 'stats': 'statistics', 'json': 'data', } state = { 0: 'INIT', 1: 'BACKUP', 2: 'MASTER', 3: 'FAULT', # UNKNOWN } def __init__(self, ifname): self.ifname = ifname def enabled(self): return self.ifname in self.active_interfaces() @classmethod def active_interfaces(cls): if not os.path.exists(cls.location['pid']): return [] data = cls.collect('json') return [group['data']['ifp_ifname'] for group in json.loads(data)] @classmethod def decode_state(cls, code): return cls.state.get(code, 'UNKNOWN') # used in conf mode @classmethod def is_running(cls): if not os.path.exists(cls.location['pid']): return False return process_running(cls.location['pid']) @classmethod def collect(cls, what): fname = cls.location[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, ) 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 Exception: name = cls._name[what] raise VRRPError(f'VRRP {name} is not available') finally: if os.path.exists(fname): os.remove(fname) @classmethod def disabled(cls): disabled = [] base = ['high-availability', 'vrrp'] 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 ) # 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', '', ] ) # return list with disabled instances return disabled @classmethod def format(cls, data): headers = ['Name', 'Interface', 'VRID', 'State', 'Priority', 'Last Transition'] groups = [] data = json.loads(data) if isinstance(data, str) else data for group in data: data = group['data'] name = data['iname'] intf = data['ifp_ifname'] vrid = data['vrid'] state = cls.decode_state(data['state']) priority = data['effective_priority'] since = int(time() - float(data['last_transition'])) last = seconds_to_human(since) groups.append([name, intf, vrid, state, priority, last]) # add to the active list disabled instances groups.extend(cls.disabled()) return tabulate(groups, headers)