diff options
-rw-r--r-- | python/vyos/ifconfig/vrrp.py | 145 | ||||
-rw-r--r-- | python/vyos/keepalived.py | 140 | ||||
-rwxr-xr-x | src/conf_mode/vrrp.py | 21 | ||||
-rwxr-xr-x | src/op_mode/vrrp.py | 87 |
4 files changed, 161 insertions, 232 deletions
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py new file mode 100644 index 000000000..29b10dd9e --- /dev/null +++ b/python/vyos/ifconfig/vrrp.py @@ -0,0 +1,145 @@ +# Copyright 2019 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/>. + +import os +import json +import signal +from time import time +from time import sleep + +from tabulate import tabulate + +from vyos import airbag +from vyos import util + + +class VRRPError(Exception): + pass + +class VRRP(object): + _vrrp_prefix = '00:00:5E:00:01:' + location = { + 'pid': '/run/keepalived.pid', + 'fifo': '/run/keepalived_notify_fifo', + 'state': '/tmp/keepalived.data', + 'stats': '/tmp/keepalived.stats', + 'json': '/tmp/keepalived.json', + 'daemon': '/etc/default/keepalived', + 'config': '/etc/keepalived/keepalived.conf', + 'vyos': '/run/keepalived_config.dict', + } + + _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 util.process_running(cls.location['pid']) + + @classmethod + def collect(cls, what): + fname = cls.location[what] + try: + # send signal to generate the configuration file + pid = util.read_file(cls.location['pid']) + os.kill(int(pid), cls._signal[what]) + + # shoud look for file size change ? + sleep(0.2) + return util.read_file(fname) + 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): + if not os.path.exists(cls.location['vyos']): + return [] + + disabled = [] + config = json.loads(util.readfile(cls.location['vyos'])) + + # add disabled groups to the list + for group in config['vrrp_groups']: + if group['disable']: + disabled.append( + [group['name'], group['interface'], group['vrid'], 'DISABLED', '']) + + # return list with disabled instances + return disabled + + @classmethod + def format (cls, data): + headers = ["Name", "Interface", "VRID", "State", "Last Transition"] + groups = [] + + data = json.loads(data) + for group in data: + data = group['data'] + + name = data['iname'] + intf = data['ifp_ifname'] + vrid = data['vrid'] + state = cls.decode_state(data["state"]) + + since = int(time() - float(data['last_transition'])) + last = util.seconds_to_human(since) + + groups.append([name, intf, vrid, state, last]) + + # add to the active list disabled instances + groups.extend(cls.disabled()) + return(tabulate(groups, headers)) + diff --git a/python/vyos/keepalived.py b/python/vyos/keepalived.py deleted file mode 100644 index 3984ca792..000000000 --- a/python/vyos/keepalived.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2018 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/>. - -import re -import os -import signal -import json - -import vyos.util - - -pid_file = '/var/run/keepalived.pid' -state_file = '/tmp/keepalived.data' -stats_file = '/tmp/keepalived.stats' -json_file = '/tmp/keepalived.json' - -def vrrp_running(): - if not os.path.exists(vyos.keepalived.pid_file) \ - or not vyos.util.process_running(vyos.keepalived.pid_file): - return False - else: - return True - -def keepalived_running(): - return vyos.util.process_running(pid_file) - -## Clear VRRP data after showing -def remove_vrrp_data(data_file): - if data_file == "json" and os.path.exists(json_file): - os.remove(json_file) - elif data_file == "stats" and os.path.exists(stats_file): - os.remove(stats_file) - elif data_file == "state" and os.path.exists(state_file): - os.remove(state_file) - -def force_state_data_dump(): - pid = vyos.util.read_file(pid_file) - os.kill(int(pid), signal.SIGUSR1) - -def force_stats_dump(): - pid = vyos.util.read_file(pid_file) - os.kill(int(pid), signal.SIGUSR2) - -def force_json_dump(): - pid = vyos.util.read_file(pid_file) - os.kill(int(pid), signal.SIGRTMIN+2) - -def get_json_data(): - with open(json_file, 'r') as f: - j = json.load(f) - return j - -def get_statistics(): - return vyos.util.read_file(stats_file) - -def get_state_data(): - return vyos.util.read_file(state_file) - -def decode_state(code): - state = None - if code == 0: - state = "INIT" - elif code == 1: - state = "BACKUP" - elif code == 2: - state = "MASTER" - elif code == 3: - state = "FAULT" - else: - state = "UNKNOWN" - - return state - -## These functions are for the old, and hopefully obsolete plaintext -## (non machine-readable) data format introduced by Vyatta back in the days -## They are kept here just in case, if JSON output option turns out or becomes -## insufficient. - -def read_state_data(): - with open(state_file, 'r') as f: - lines = f.readlines() - return lines - -def parse_keepalived_data(data_lines): - vrrp_groups = {} - - # Scratch variable - group_name = None - - # Sadly there is no explicit end marker in that format, so we have - # only two states, one before the first VRRP instance is encountered - # and one after an instance/"group" was encountered - # We'll set group_name once the first group is encountered, - # and assume we are inside a group if it's set afterwards - # - # It may not be necessary since the keywords found inside groups and before - # the VRRP Topology section seem to have no intersection, - # but better safe than sorry. - - for line in data_lines: - if re.match(r'^\s*VRRP Instance', line, re.IGNORECASE): - # Example: "VRRP Instance = Foo" - name = re.match(r'^\s*VRRP Instance\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() - group_name = name - vrrp_groups[name] = {} - elif re.match(r'^\s*State', line, re.IGNORECASE) and group_name: - # Example: " State = MASTER" - group_state = re.match(r'^\s*State\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() - vrrp_groups[group_name]["state"] = group_state - elif re.match(r'^\s*Last transition', line, re.IGNORECASE) and group_name: - # Example: " Last transition = 1532043820 (Thu Jul 19 23:43:40 2018)" - trans_time = re.match(r'^\s*Last transition\s+=\s+(\d+)\s', line, re.IGNORECASE).groups()[0] - vrrp_groups[group_name]["last_transition"] = trans_time - elif re.match(r'^\s*Interface', line, re.IGNORECASE) and group_name: - # Example: " Interface = eth0.30" - interface = re.match(r'\s*Interface\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() - vrrp_groups[group_name]["interface"] = interface - elif re.match(r'^\s*Virtual Router ID', line, re.IGNORECASE) and group_name: - # Example: " Virtual Router ID = 14" - vrid = re.match(r'^\s*Virtual Router ID\s+=\s+(.*)$', line, re.IGNORECASE).groups()[0].strip() - vrrp_groups[group_name]["vrid"] = vrid - elif re.match(r'^\s*------< Interfaces', line, re.IGNORECASE): - # Interfaces section appears to always be present, - # and there's nothing of interest for us below that section, - # so we use it as an end of input marker - break - - return vrrp_groups diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index b9b0405e2..f358891a5 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -22,16 +22,13 @@ from json import dumps from pathlib import Path import vyos.config -import vyos.keepalived from vyos import ConfigError from vyos.util import call from vyos.template import render +from vyos.ifconfig.vrrp import VRRP -daemon_file = "/etc/default/keepalived" -config_file = "/etc/keepalived/keepalived.conf" -config_dict_path = "/run/keepalived_config.dict" def get_config(): vrrp_groups = [] @@ -127,7 +124,7 @@ def get_config(): sync_groups.append(sync_group) # create a file with dict with proposed configuration - with open("{}.temp".format(config_dict_path), 'w') as dict_file: + with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file: dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) return (vrrp_groups, sync_groups) @@ -212,9 +209,9 @@ def generate(data): # Filter out disabled groups vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - render(config_file, 'vrrp/keepalived.conf.tmpl', - {"groups": vrrp_groups, "sync_groups": sync_groups}) - render(daemon_file, 'vrrp/daemon.tmpl', {}) + render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', + {"groups": vrrp_groups, "sync_groups": sync_groups}) + render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {}) return None @@ -223,12 +220,12 @@ def apply(data): if vrrp_groups: # safely rename a temporary file with configuration dict try: - dict_file = Path("{}.temp".format(config_dict_path)) - dict_file.rename(Path(config_dict_path)) + dict_file = Path("{}.temp".format(VRRP.location['vyos'])) + dict_file.rename(Path(VRRP.location['vyos'])) except Exception as err: print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) - if not vyos.keepalived.vrrp_running(): + if not VRRP.is_running(): print("Starting the VRRP process") ret = call("sudo systemctl restart keepalived.service") else: @@ -241,7 +238,7 @@ def apply(data): # VRRP is removed in the commit print("Stopping the VRRP process") call("sudo systemctl stop keepalived.service") - os.unlink(config_file) + os.unlink(VRRP.location['daemon']) return None diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 8a993f92c..923cfa4d4 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -24,81 +24,8 @@ import tabulate import vyos.keepalived import vyos.util -config_dict_path = '/run/keepalived_config.dict' - - -# get disabled instances from a config -def vrrp_get_disabled(): - # read the dictionary file with configuration - with open(config_dict_path, 'r') as dict_file: - vrrp_config_dict = json.load(dict_file) - vrrp_disabled = [] - # add disabled groups to the list - for vrrp_group in vrrp_config_dict['vrrp_groups']: - if vrrp_group['disable']: - vrrp_disabled.append([vrrp_group['name'], vrrp_group['interface'], vrrp_group['vrid'], 'DISABLED', '']) - # return list with disabled instances - return vrrp_disabled - - -def print_summary(): - try: - vyos.keepalived.force_json_dump() - # Wait for keepalived to produce the data - # Replace with inotify or similar if it proves problematic - time.sleep(0.2) - json_data = vyos.keepalived.get_json_data() - vyos.keepalived.remove_vrrp_data("json") - except: - print("VRRP information is not available") - sys.exit(1) - - groups = [] - for group in json_data: - data = group["data"] - - name = data["iname"] - - ltrans_timestamp = float(data["last_transition"]) - ltrans_time = vyos.util.seconds_to_human(int(time.time() - ltrans_timestamp)) - - interface = data["ifp_ifname"] - vrid = data["vrid"] - - state = vyos.keepalived.decode_state(data["state"]) - - row = [name, interface, vrid, state, ltrans_time] - groups.append(row) - - # add to the active list disabled instances - groups.extend(vrrp_get_disabled()) - headers = ["Name", "Interface", "VRID", "State", "Last Transition"] - output = tabulate.tabulate(groups, headers) - print(output) - - -def print_statistics(): - try: - vyos.keepalived.force_stats_dump() - time.sleep(0.2) - output = vyos.keepalived.get_statistics() - print(output) - vyos.keepalived.remove_vrrp_data("stats") - except: - print("VRRP statistics are not available") - sys.exit(1) - - -def print_state_data(): - try: - vyos.keepalived.force_state_data_dump() - time.sleep(0.2) - output = vyos.keepalived.get_state_data() - print(output) - vyos.keepalived.remove_vrrp_data("state") - except: - print("VRRP information is not available") - sys.exit(1) +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.vrrp import VRRPError parser = argparse.ArgumentParser() @@ -110,16 +37,16 @@ group.add_argument("-d", "--data", action="store_true", help="Print detailed VRR args = parser.parse_args() # Exit early if VRRP is dead or not configured -if not vyos.keepalived.vrrp_running(): - print("VRRP is not running") +if not VRRP.is_running(): + print('VRRP is not running') sys.exit(0) if args.summary: - print_summary() + print(VRRP.format(VRRP.collect('json'))) elif args.statistics: - print_statistics() + print(VRRP.collect('stats')) elif args.data: - print_state_data() + print(VRRP.collect('state')) else: parser.print_help() sys.exit(1) |