diff options
Diffstat (limited to 'tools/frr-reload.py')
| -rwxr-xr-x | tools/frr-reload.py | 1162 |
1 files changed, 729 insertions, 433 deletions
diff --git a/tools/frr-reload.py b/tools/frr-reload.py index 45843faf13..951383beb2 100755 --- a/tools/frr-reload.py +++ b/tools/frr-reload.py @@ -32,13 +32,14 @@ from __future__ import print_function, unicode_literals import argparse import copy import logging -import os +import os, os.path import random import re import string import subprocess import sys from collections import OrderedDict + try: from ipaddress import IPv6Address, ip_network except ImportError: @@ -51,18 +52,137 @@ except AttributeError: # Python 3 def iteritems(d): return iter(d.items()) + + else: # Python 2 def iteritems(d): return d.iteritems() + log = logging.getLogger(__name__) -class VtyshMarkException(Exception): +class VtyshException(Exception): pass +class Vtysh(object): + def __init__(self, bindir=None, confdir=None, sockdir=None, pathspace=None): + self.bindir = bindir + self.confdir = confdir + self.pathspace = pathspace + self.common_args = [os.path.join(bindir or "", "vtysh")] + if confdir: + self.common_args.extend(["--config_dir", confdir]) + if sockdir: + self.common_args.extend(["--vty_socket", sockdir]) + if pathspace: + self.common_args.extend(["-N", pathspace]) + + def _call(self, args, stdin=None, stdout=None, stderr=None): + kwargs = {} + if stdin is not None: + kwargs["stdin"] = stdin + if stdout is not None: + kwargs["stdout"] = stdout + if stderr is not None: + kwargs["stderr"] = stderr + return subprocess.Popen(self.common_args + args, **kwargs) + + def _call_cmd(self, command, stdin=None, stdout=None, stderr=None): + if isinstance(command, list): + args = [item for sub in command for item in ["-c", sub]] + else: + args = ["-c", command] + return self._call(args, stdin, stdout, stderr) + + def __call__(self, command): + """ + Call a CLI command (e.g. "show running-config") + + Output text is automatically redirected, decoded and returned. + Multiple commands may be passed as list. + """ + proc = self._call_cmd(command, stdout=subprocess.PIPE) + stdout, stderr = proc.communicate() + if proc.wait() != 0: + raise VtyshException( + 'vtysh returned status %d for command "%s"' % (proc.returncode, command) + ) + return stdout.decode("UTF-8") + + def is_config_available(self): + """ + Return False if no frr daemon is running or some other vtysh session is + in 'configuration terminal' mode which will prevent us from making any + configuration changes. + """ + + output = self("configure") + + if "VTY configuration is locked by other VTY" in output: + log.error("vtysh 'configure' returned\n%s\n" % (output)) + return False + + return True + + def exec_file(self, filename): + child = self._call(["-f", filename]) + if child.wait() != 0: + raise VtyshException( + "vtysh (exec file) exited with status %d" % (child.returncode) + ) + + def mark_file(self, filename, stdin=None): + child = self._call( + ["-m", "-f", filename], + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + stdout, stderr = child.communicate() + except subprocess.TimeoutExpired: + child.kill() + stdout, stderr = child.communicate() + raise VtyshException("vtysh call timed out!") + + if child.wait() != 0: + raise VtyshException( + "vtysh (mark file) exited with status %d:\n%s" + % (child.returncode, stderr) + ) + + return stdout.decode("UTF-8") + + def mark_show_run(self, daemon=None): + cmd = "show running-config" + if daemon: + cmd += " %s" % daemon + cmd += " no-header" + show_run = self._call_cmd(cmd, stdout=subprocess.PIPE) + mark = self._call( + ["-m", "-f", "-"], stdin=show_run.stdout, stdout=subprocess.PIPE + ) + + show_run.wait() + stdout, stderr = mark.communicate() + mark.wait() + + if show_run.returncode != 0: + raise VtyshException( + "vtysh (show running-config) exited with status %d:" + % (show_run.returncode) + ) + if mark.returncode != 0: + raise VtyshException( + "vtysh (mark running-config) exited with status %d" % (mark.returncode) + ) + + return stdout.decode("UTF-8") + + class Context(object): """ @@ -110,33 +230,28 @@ class Config(object): ('router ospf' for example) are our dictionary key. """ - def __init__(self): + def __init__(self, vtysh): self.lines = [] self.contexts = OrderedDict() + self.vtysh = vtysh - def load_from_file(self, filename, bindir, confdir): + def load_from_file(self, filename): """ Read configuration from specified file and slurp it into internal memory The internal representation has been marked appropriately by passing it through vtysh with the -m parameter """ - log.info('Loading Config object from file %s', filename) + log.info("Loading Config object from file %s", filename) - try: - file_output = subprocess.check_output([str(bindir + '/vtysh'), '-m', '-f', filename, '--config_dir', confdir], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - ve = VtyshMarkException(e) - ve.output = e.output - raise ve - - for line in file_output.decode('utf-8').split('\n'): + file_output = self.vtysh.mark_file(filename) + + for line in file_output.split("\n"): line = line.strip() # Compress duplicate whitespaces - line = ' '.join(line.split()) + line = " ".join(line.split()) - if ":" in line: + if ":" in line and not "ipv6 add": qv6_line = get_normalized_ipv6_line(line) self.lines.append(qv6_line) else: @@ -144,29 +259,24 @@ class Config(object): self.load_contexts() - def load_from_show_running(self, bindir, confdir, daemon): + def load_from_show_running(self, daemon): """ Read running configuration and slurp it into internal memory The internal representation has been marked appropriately by passing it through vtysh with the -m parameter """ - log.info('Loading Config object from vtysh show running') + log.info("Loading Config object from vtysh show running") - try: - config_text = subprocess.check_output( - bindir + "/vtysh --config_dir " + confdir + " -c 'show run " + daemon + "' | /usr/bin/tail -n +4 | " + bindir + "/vtysh --config_dir " + confdir + " -m -f -", - shell=True) - except subprocess.CalledProcessError as e: - ve = VtyshMarkException(e) - ve.output = e.output - raise ve - - for line in config_text.decode('utf-8').split('\n'): + config_text = self.vtysh.mark_show_run(daemon) + + for line in config_text.split("\n"): line = line.strip() - if (line == 'Building configuration...' or - line == 'Current configuration:' or - not line): + if ( + line == "Building configuration..." + or line == "Current configuration:" + or not line + ): continue self.lines.append(line) @@ -178,7 +288,7 @@ class Config(object): Return the lines read in from the configuration """ - return '\n'.join(self.lines) + return "\n".join(self.lines) def get_contexts(self): """ @@ -186,7 +296,7 @@ class Config(object): """ for (_, ctx) in sorted(iteritems(self.contexts)): - print(str(ctx) + '\n') + print(str(ctx) + "\n") def save_contexts(self, key, lines): """ @@ -196,99 +306,116 @@ class Config(object): if not key: return - ''' + """ IP addresses specified in "network" statements, "ip prefix-lists" etc. can differ in the host part of the specification the user provides and what the running config displays. For example, user can specify 11.1.1.1/24, and the running config displays this as 11.1.1.0/24. Ensure we don't do a needless operation for such lines. IS-IS & OSPFv3 have no "network" support. - ''' - re_key_rt = re.match(r'(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$', key[0]) + """ + re_key_rt = re.match(r"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key[0]) if re_key_rt: addr = re_key_rt.group(2) - if '/' in addr: + if "/" in addr: try: - if 'ipaddress' not in sys.modules: + if "ipaddress" not in sys.modules: newaddr = IPNetwork(addr) - key[0] = '%s route %s/%s%s' % (re_key_rt.group(1), - newaddr.network, - newaddr.prefixlen, - re_key_rt.group(3)) + key[0] = "%s route %s/%s%s" % ( + re_key_rt.group(1), + newaddr.network, + newaddr.prefixlen, + re_key_rt.group(3), + ) else: newaddr = ip_network(addr, strict=False) - key[0] = '%s route %s/%s%s' % (re_key_rt.group(1), - str(newaddr.network_address), - newaddr.prefixlen, - re_key_rt.group(3)) + key[0] = "%s route %s/%s%s" % ( + re_key_rt.group(1), + str(newaddr.network_address), + newaddr.prefixlen, + re_key_rt.group(3), + ) except ValueError: pass re_key_rt = re.match( - r'(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$', - key[0] + r"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key[0] ) if re_key_rt: addr = re_key_rt.group(4) - if '/' in addr: + if "/" in addr: try: - if 'ipaddress' not in sys.modules: - newaddr = '%s/%s' % (IPNetwork(addr).network, - IPNetwork(addr).prefixlen) + if "ipaddress" not in sys.modules: + newaddr = "%s/%s" % ( + IPNetwork(addr).network, + IPNetwork(addr).prefixlen, + ) else: network_addr = ip_network(addr, strict=False) - newaddr = '%s/%s' % (str(network_addr.network_address), - network_addr.prefixlen) + newaddr = "%s/%s" % ( + str(network_addr.network_address), + network_addr.prefixlen, + ) except ValueError: newaddr = addr else: newaddr = addr legestr = re_key_rt.group(5) - re_lege = re.search(r'(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)', legestr) + re_lege = re.search(r"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr) if re_lege: - legestr = '%sge %s le %s%s' % (re_lege.group(1), - re_lege.group(3), - re_lege.group(2), - re_lege.group(4)) - re_lege = re.search(r'(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)', legestr) - - if (re_lege and ((re_key_rt.group(1) == "ip" and - re_lege.group(3) == "32") or - (re_key_rt.group(1) == "ipv6" and - re_lege.group(3) == "128"))): - legestr = '%sge %s%s' % (re_lege.group(1), - re_lege.group(2), - re_lege.group(4)) - - key[0] = '%s prefix-list%s%s %s%s' % (re_key_rt.group(1), - re_key_rt.group(2), - re_key_rt.group(3), - newaddr, - legestr) - - if lines and key[0].startswith('router bgp'): + legestr = "%sge %s le %s%s" % ( + re_lege.group(1), + re_lege.group(3), + re_lege.group(2), + re_lege.group(4), + ) + re_lege = re.search(r"(.*)ge\s+(\d+)\s+le\s+(\d+)(.*)", legestr) + + if re_lege and ( + (re_key_rt.group(1) == "ip" and re_lege.group(3) == "32") + or (re_key_rt.group(1) == "ipv6" and re_lege.group(3) == "128") + ): + legestr = "%sge %s%s" % ( + re_lege.group(1), + re_lege.group(2), + re_lege.group(4), + ) + + key[0] = "%s prefix-list%s%s %s%s" % ( + re_key_rt.group(1), + re_key_rt.group(2), + re_key_rt.group(3), + newaddr, + legestr, + ) + + if lines and key[0].startswith("router bgp"): newlines = [] for line in lines: - re_net = re.match(r'network\s+([A-Fa-f:.0-9/]+)(.*)$', line) + re_net = re.match(r"network\s+([A-Fa-f:.0-9/]+)(.*)$", line) if re_net: addr = re_net.group(1) - if '/' not in addr and key[0].startswith('router bgp'): + if "/" not in addr and key[0].startswith("router bgp"): # This is most likely an error because with no # prefixlen, BGP treats the prefixlen as 8 - addr = addr + '/8' + addr = addr + "/8" try: - if 'ipaddress' not in sys.modules: + if "ipaddress" not in sys.modules: newaddr = IPNetwork(addr) - line = 'network %s/%s %s' % (newaddr.network, - newaddr.prefixlen, - re_net.group(2)) + line = "network %s/%s %s" % ( + newaddr.network, + newaddr.prefixlen, + re_net.group(2), + ) else: network_addr = ip_network(addr, strict=False) - line = 'network %s/%s %s' % (str(network_addr.network_address), - network_addr.prefixlen, - re_net.group(2)) + line = "network %s/%s %s" % ( + str(network_addr.network_address), + network_addr.prefixlen, + re_net.group(2), + ) newlines.append(line) except ValueError: # Really this should be an error. Whats a network @@ -298,13 +425,16 @@ class Config(object): newlines.append(line) lines = newlines - ''' + """ More fixups in user specification and what running config shows. "null0" in routes must be replaced by Null0. - ''' - if (key[0].startswith('ip route') or key[0].startswith('ipv6 route') and - 'null0' in key[0]): - key[0] = re.sub(r'\s+null0(\s*$)', ' Null0', key[0]) + """ + if ( + key[0].startswith("ip route") + or key[0].startswith("ipv6 route") + and "null0" in key[0] + ): + key[0] = re.sub(r"\s+null0(\s*$)", " Null0", key[0]) if lines: if tuple(key) not in self.contexts: @@ -327,7 +457,7 @@ class Config(object): current_context_lines = [] ctx_keys = [] - ''' + """ The end of a context is flagged via the 'end' keyword: ! @@ -371,7 +501,7 @@ router ospf timers throttle spf 0 50 5000 ! end - ''' + """ # The code assumes that its working on the output from the "vtysh -m" # command. That provides the appropriate markers to signify end of @@ -391,37 +521,40 @@ end # the keywords that we know are single line contexts. bgp in this case # is not the main router bgp block, but enabling multi-instance - oneline_ctx_keywords = ("access-list ", - "agentx", - "allow-external-route-update", - "bgp ", - "debug ", - "domainname ", - "dump ", - "enable ", - "frr ", - "hostname ", - "ip ", - "ipv6 ", - "log ", - "mpls lsp", - "mpls label", - "no ", - "password ", - "ptm-enable", - "router-id ", - "service ", - "table ", - "username ", - "zebra ", - "vrrp autoconfigure") + oneline_ctx_keywords = ( + "access-list ", + "agentx", + "allow-external-route-update", + "bgp ", + "debug ", + "domainname ", + "dump ", + "enable ", + "frr ", + "hostname ", + "ip ", + "ipv6 ", + "log ", + "mpls lsp", + "mpls label", + "no ", + "password ", + "ptm-enable", + "router-id ", + "service ", + "table ", + "username ", + "zebra ", + "vrrp autoconfigure", + "evpn mh", + ) for line in self.lines: if not line: continue - if line.startswith('!') or line.startswith('#'): + if line.startswith("!") or line.startswith("#"): continue # one line contexts @@ -429,22 +562,31 @@ end # as part of its 'mpls ldp' config context. If we are processing # ldp configuration and encounter a router-id we should NOT switch # to a new context - if new_ctx is True and any(line.startswith(keyword) for keyword in oneline_ctx_keywords) and not ( - ctx_keys and ctx_keys[0].startswith("mpls ldp") and line.startswith("router-id ")): + if ( + new_ctx is True + and any(line.startswith(keyword) for keyword in oneline_ctx_keywords) + and not ( + ctx_keys + and ctx_keys[0].startswith("mpls ldp") + and line.startswith("router-id ") + ) + ): self.save_contexts(ctx_keys, current_context_lines) # Start a new context main_ctx_key = [] - ctx_keys = [line, ] + ctx_keys = [ + line, + ] current_context_lines = [] - log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) + log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys) self.save_contexts(ctx_keys, current_context_lines) new_ctx = True elif line == "end": self.save_contexts(ctx_keys, current_context_lines) - log.debug('LINE %-50s: exiting old context, %-50s', line, ctx_keys) + log.debug("LINE %-50s: exiting old context, %-50s", line, ctx_keys) # Start a new context new_ctx = True @@ -455,9 +597,11 @@ end elif line == "exit-vrf": self.save_contexts(ctx_keys, current_context_lines) current_context_lines.append(line) - log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) + log.debug( + "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys + ) - #Start a new context + # Start a new context new_ctx = True main_ctx_key = [] ctx_keys = [] @@ -471,7 +615,11 @@ end # Start a new context ctx_keys = copy.deepcopy(main_ctx_key) current_context_lines = [] - log.debug('LINE %-50s: popping from subcontext to ctx%-50s', line, ctx_keys) + log.debug( + "LINE %-50s: popping from subcontext to ctx%-50s", + line, + ctx_keys, + ) elif line in ["exit-vni", "exit-ldp-if"]: if sub_main_ctx_key: @@ -480,127 +628,98 @@ end # Start a new context ctx_keys = copy.deepcopy(sub_main_ctx_key) current_context_lines = [] - log.debug('LINE %-50s: popping from sub-subcontext to ctx%-50s', line, ctx_keys) + log.debug( + "LINE %-50s: popping from sub-subcontext to ctx%-50s", + line, + ctx_keys, + ) elif new_ctx is True: if not main_ctx_key: - ctx_keys = [line, ] + ctx_keys = [ + line, + ] else: ctx_keys = copy.deepcopy(main_ctx_key) main_ctx_key = [] current_context_lines = [] new_ctx = False - log.debug('LINE %-50s: entering new context, %-50s', line, ctx_keys) - elif (line.startswith("address-family ") or - line.startswith("vnc defaults") or - line.startswith("vnc l2-group") or - line.startswith("vnc nve-group") or - line.startswith("member pseudowire")): + log.debug("LINE %-50s: entering new context, %-50s", line, ctx_keys) + elif ( + line.startswith("address-family ") + or line.startswith("vnc defaults") + or line.startswith("vnc l2-group") + or line.startswith("vnc nve-group") + or line.startswith("peer") + or line.startswith("key ") + or line.startswith("member pseudowire") + ): main_ctx_key = [] # Save old context first self.save_contexts(ctx_keys, current_context_lines) current_context_lines = [] main_ctx_key = copy.deepcopy(ctx_keys) - log.debug('LINE %-50s: entering sub-context, append to ctx_keys', line) + log.debug("LINE %-50s: entering sub-context, append to ctx_keys", line) - if line == "address-family ipv6" and not ctx_keys[0].startswith("mpls ldp"): + if line == "address-family ipv6" and not ctx_keys[0].startswith( + "mpls ldp" + ): ctx_keys.append("address-family ipv6 unicast") - elif line == "address-family ipv4" and not ctx_keys[0].startswith("mpls ldp"): + elif line == "address-family ipv4" and not ctx_keys[0].startswith( + "mpls ldp" + ): ctx_keys.append("address-family ipv4 unicast") elif line == "address-family evpn": ctx_keys.append("address-family l2vpn evpn") else: ctx_keys.append(line) - elif ((line.startswith("vni ") and - len(ctx_keys) == 2 and - ctx_keys[0].startswith('router bgp') and - ctx_keys[1] == 'address-family l2vpn evpn')): + elif ( + line.startswith("vni ") + and len(ctx_keys) == 2 + and ctx_keys[0].startswith("router bgp") + and ctx_keys[1] == "address-family l2vpn evpn" + ): # Save old context first self.save_contexts(ctx_keys, current_context_lines) current_context_lines = [] sub_main_ctx_key = copy.deepcopy(ctx_keys) - log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line) + log.debug( + "LINE %-50s: entering sub-sub-context, append to ctx_keys", line + ) ctx_keys.append(line) - - elif ((line.startswith("interface ") and - len(ctx_keys) == 2 and - ctx_keys[0].startswith('mpls ldp') and - ctx_keys[1].startswith('address-family'))): + + elif ( + line.startswith("interface ") + and len(ctx_keys) == 2 + and ctx_keys[0].startswith("mpls ldp") + and ctx_keys[1].startswith("address-family") + ): # Save old context first self.save_contexts(ctx_keys, current_context_lines) current_context_lines = [] sub_main_ctx_key = copy.deepcopy(ctx_keys) - log.debug('LINE %-50s: entering sub-sub-context, append to ctx_keys', line) + log.debug( + "LINE %-50s: entering sub-sub-context, append to ctx_keys", line + ) ctx_keys.append(line) else: # Continuing in an existing context, add non-commented lines to it current_context_lines.append(line) - log.debug('LINE %-50s: append to current_context_lines, %-50s', line, ctx_keys) + log.debug( + "LINE %-50s: append to current_context_lines, %-50s", line, ctx_keys + ) # Save the context of the last one self.save_contexts(ctx_keys, current_context_lines) -def line_to_vtysh_conft(ctx_keys, line, delete, bindir, confdir): - """ - Return the vtysh command for the specified context line - """ - - cmd = [] - cmd.append(str(bindir + '/vtysh')) - cmd.append('--config_dir') - cmd.append(confdir) - cmd.append('-c') - cmd.append('conf t') - - if line: - for ctx_key in ctx_keys: - cmd.append('-c') - cmd.append(ctx_key) - - line = line.lstrip() - - if delete: - cmd.append('-c') - - if line.startswith('no '): - cmd.append('%s' % line[3:]) - else: - cmd.append('no %s' % line) - - else: - cmd.append('-c') - cmd.append(line) - - # If line is None then we are typically deleting an entire - # context ('no router ospf' for example) - else: - - if delete: - - # Only put the 'no' on the last sub-context - for ctx_key in ctx_keys: - cmd.append('-c') - - if ctx_key == ctx_keys[-1]: - cmd.append('no %s' % ctx_key) - else: - cmd.append('%s' % ctx_key) - else: - for ctx_key in ctx_keys: - cmd.append('-c') - cmd.append(ctx_key) - - return cmd - - -def line_for_vtysh_file(ctx_keys, line, delete): +def lines_to_config(ctx_keys, line, delete): """ Return the command as it would appear in frr.conf """ @@ -608,16 +727,20 @@ def line_for_vtysh_file(ctx_keys, line, delete): if line: for (i, ctx_key) in enumerate(ctx_keys): - cmd.append(' ' * i + ctx_key) + cmd.append(" " * i + ctx_key) line = line.lstrip() - indent = len(ctx_keys) * ' ' + indent = len(ctx_keys) * " " + # There are some commands that are on by default so their "no" form will be + # displayed in the config. "no bgp default ipv4-unicast" is one of these. + # If we need to remove this line we do so by adding "bgp default ipv4-unicast", + # not by doing a "no no bgp default ipv4-unicast" if delete: - if line.startswith('no '): - cmd.append('%s%s' % (indent, line[3:])) + if line.startswith("no "): + cmd.append("%s%s" % (indent, line[3:])) else: - cmd.append('%sno %s' % (indent, line)) + cmd.append("%sno %s" % (indent, line)) else: cmd.append(indent + line) @@ -625,26 +748,17 @@ def line_for_vtysh_file(ctx_keys, line, delete): # If line is None then we are typically deleting an entire # context ('no router ospf' for example) else: - if delete: - - # Only put the 'no' on the last sub-context - for ctx_key in ctx_keys: + for i, ctx_key in enumerate(ctx_keys[:-1]): + cmd.append("%s%s" % (" " * i, ctx_key)) - if ctx_key == ctx_keys[-1]: - cmd.append('no %s' % ctx_key) - else: - cmd.append('%s' % ctx_key) + # Only put the 'no' on the last sub-context + if delete: + if ctx_keys[-1].startswith("no "): + cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1][3:])) + else: + cmd.append("%sno %s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1])) else: - for ctx_key in ctx_keys: - cmd.append(ctx_key) - - cmd = '\n' + '\n'.join(cmd) - - # There are some commands that are on by default so their "no" form will be - # displayed in the config. "no bgp default ipv4-unicast" is one of these. - # If we need to remove this line we do so by adding "bgp default ipv4-unicast", - # not by doing a "no no bgp default ipv4-unicast" - cmd = cmd.replace('no no ', '') + cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1])) return cmd @@ -657,23 +771,26 @@ def get_normalized_ipv6_line(line): the IPv6 word is a network """ norm_line = "" - words = line.split(' ') + words = line.split(" ") for word in words: if ":" in word: norm_word = None if "/" in word: try: - if 'ipaddress' not in sys.modules: + if "ipaddress" not in sys.modules: v6word = IPNetwork(word) - norm_word = '%s/%s' % (v6word.network, v6word.prefixlen) + norm_word = "%s/%s" % (v6word.network, v6word.prefixlen) else: v6word = ip_network(word, strict=False) - norm_word = '%s/%s' % (str(v6word.network_address), v6word.prefixlen) + norm_word = "%s/%s" % ( + str(v6word.network_address), + v6word.prefixlen, + ) except ValueError: pass if not norm_word: try: - norm_word = '%s' % IPv6Address(word) + norm_word = "%s" % IPv6Address(word) except ValueError: norm_word = word else: @@ -695,6 +812,38 @@ def line_exist(lines, target_ctx_keys, target_line, exact_match=True): return False +def check_for_exit_vrf(lines_to_add, lines_to_del): + + # exit-vrf is a bit tricky. If the new config is missing it but we + # have configs under a vrf, we need to add it at the end to do the + # right context changes. If exit-vrf exists in both the running and + # new config, we cannot delete it or it will break context changes. + add_exit_vrf = False + index = 0 + + for (ctx_keys, line) in lines_to_add: + if add_exit_vrf == True: + if ctx_keys[0] != prior_ctx_key: + insert_key = ((prior_ctx_key),) + lines_to_add.insert(index, ((insert_key, "exit-vrf"))) + add_exit_vrf = False + + if ctx_keys[0].startswith("vrf") and line: + if line is not "exit-vrf": + add_exit_vrf = True + prior_ctx_key = ctx_keys[0] + else: + add_exit_vrf = False + index += 1 + + for (ctx_keys, line) in lines_to_del: + if line == "exit-vrf": + if line_exist(lines_to_add, ctx_keys, line): + lines_to_del.remove((ctx_keys, line)) + + return (lines_to_add, lines_to_del) + + def ignore_delete_re_add_lines(lines_to_add, lines_to_del): # Quite possibly the most confusing (while accurate) variable names in history @@ -704,10 +853,10 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): for (ctx_keys, line) in lines_to_del: deleted = False - if ctx_keys[0].startswith('router bgp') and line: + if ctx_keys[0].startswith("router bgp") and line: - if line.startswith('neighbor '): - ''' + if line.startswith("neighbor "): + """ BGP changed how it displays swpX peers that are part of peer-group. Older versions of frr would display these on separate lines: neighbor swp1 interface @@ -724,10 +873,14 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): neighbor swp1 peer-group FOO If so then chop the del line and the corresponding add lines - ''' + """ - re_swpx_int_peergroup = re.search('neighbor (\S+) interface peer-group (\S+)', line) - re_swpx_int_v6only_peergroup = re.search('neighbor (\S+) interface v6only peer-group (\S+)', line) + re_swpx_int_peergroup = re.search( + "neighbor (\S+) interface peer-group (\S+)", line + ) + re_swpx_int_v6only_peergroup = re.search( + "neighbor (\S+) interface v6only peer-group (\S+)", line + ) if re_swpx_int_peergroup or re_swpx_int_v6only_peergroup: swpx_interface = None @@ -743,21 +896,29 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): swpx_interface = "neighbor %s interface v6only" % swpx swpx_peergroup = "neighbor %s peer-group %s" % (swpx, peergroup) - found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) - found_add_swpx_peergroup = line_exist(lines_to_add, ctx_keys, swpx_peergroup) + found_add_swpx_interface = line_exist( + lines_to_add, ctx_keys, swpx_interface + ) + found_add_swpx_peergroup = line_exist( + lines_to_add, ctx_keys, swpx_peergroup + ) tmp_ctx_keys = tuple(list(ctx_keys)) if not found_add_swpx_peergroup: tmp_ctx_keys = list(ctx_keys) - tmp_ctx_keys.append('address-family ipv4 unicast') + tmp_ctx_keys.append("address-family ipv4 unicast") tmp_ctx_keys = tuple(tmp_ctx_keys) - found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) + found_add_swpx_peergroup = line_exist( + lines_to_add, tmp_ctx_keys, swpx_peergroup + ) if not found_add_swpx_peergroup: tmp_ctx_keys = list(ctx_keys) - tmp_ctx_keys.append('address-family ipv6 unicast') + tmp_ctx_keys.append("address-family ipv6 unicast") tmp_ctx_keys = tuple(tmp_ctx_keys) - found_add_swpx_peergroup = line_exist(lines_to_add, tmp_ctx_keys, swpx_peergroup) + found_add_swpx_peergroup = line_exist( + lines_to_add, tmp_ctx_keys, swpx_peergroup + ) if found_add_swpx_interface and found_add_swpx_peergroup: deleted = True @@ -765,29 +926,36 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): lines_to_add_to_del.append((ctx_keys, swpx_interface)) lines_to_add_to_del.append((tmp_ctx_keys, swpx_peergroup)) - ''' + """ Changing the bfd timers on neighbors is allowed without doing a delete/add process. Since doing a "no neighbor blah bfd ..." will cause the peer to bounce unnecessarily, just skip the delete and just do the add. - ''' - re_nbr_bfd_timers = re.search(r'neighbor (\S+) bfd (\S+) (\S+) (\S+)', line) + """ + re_nbr_bfd_timers = re.search( + r"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line + ) if re_nbr_bfd_timers: nbr = re_nbr_bfd_timers.group(1) bfd_nbr = "neighbor %s" % nbr - bfd_search_string = bfd_nbr + r' bfd (\S+) (\S+) (\S+)' + bfd_search_string = bfd_nbr + r" bfd (\S+) (\S+) (\S+)" for (ctx_keys, add_line) in lines_to_add: - re_add_nbr_bfd_timers = re.search(bfd_search_string, add_line) + if ctx_keys[0].startswith("router bgp"): + re_add_nbr_bfd_timers = re.search( + bfd_search_string, add_line + ) - if re_add_nbr_bfd_timers: - found_add_bfd_nbr = line_exist(lines_to_add, ctx_keys, bfd_nbr, False) + if re_add_nbr_bfd_timers: + found_add_bfd_nbr = line_exist( + lines_to_add, ctx_keys, bfd_nbr, False + ) - if found_add_bfd_nbr: - lines_to_del_to_del.append((ctx_keys, line)) + if found_add_bfd_nbr: + lines_to_del_to_del.append((ctx_keys, line)) - ''' + """ We changed how we display the neighbor interface command. Older versions of frr would display the following: neighbor swp1 interface @@ -809,9 +977,13 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): neighbor swp1 capability extended-nexthop If so then chop the del line and the corresponding add lines - ''' - re_swpx_int_remoteas = re.search('neighbor (\S+) interface remote-as (\S+)', line) - re_swpx_int_v6only_remoteas = re.search('neighbor (\S+) interface v6only remote-as (\S+)', line) + """ + re_swpx_int_remoteas = re.search( + "neighbor (\S+) interface remote-as (\S+)", line + ) + re_swpx_int_v6only_remoteas = re.search( + "neighbor (\S+) interface v6only remote-as (\S+)", line + ) if re_swpx_int_remoteas or re_swpx_int_v6only_remoteas: swpx_interface = None @@ -827,8 +999,12 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): swpx_interface = "neighbor %s interface v6only" % swpx swpx_remoteas = "neighbor %s remote-as %s" % (swpx, remoteas) - found_add_swpx_interface = line_exist(lines_to_add, ctx_keys, swpx_interface) - found_add_swpx_remoteas = line_exist(lines_to_add, ctx_keys, swpx_remoteas) + found_add_swpx_interface = line_exist( + lines_to_add, ctx_keys, swpx_interface + ) + found_add_swpx_remoteas = line_exist( + lines_to_add, ctx_keys, swpx_remoteas + ) tmp_ctx_keys = tuple(list(ctx_keys)) if found_add_swpx_interface and found_add_swpx_remoteas: @@ -837,7 +1013,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): lines_to_add_to_del.append((ctx_keys, swpx_interface)) lines_to_add_to_del.append((tmp_ctx_keys, swpx_remoteas)) - ''' + """ We made the 'bgp bestpath as-path multipath-relax' command automatically assume 'no-as-set' since the lack of this option caused weird routing problems. When the running config is shown in @@ -845,10 +1021,12 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): is the default. This causes frr-reload to unnecessarily unapply this option only to apply it back again, causing unnecessary session resets. - ''' - if 'multipath-relax' in line: - re_asrelax_new = re.search('^bgp\s+bestpath\s+as-path\s+multipath-relax$', line) - old_asrelax_cmd = 'bgp bestpath as-path multipath-relax no-as-set' + """ + if "multipath-relax" in line: + re_asrelax_new = re.search( + "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line + ) + old_asrelax_cmd = "bgp bestpath as-path multipath-relax no-as-set" found_asrelax_old = line_exist(lines_to_add, ctx_keys, old_asrelax_cmd) if re_asrelax_new and found_asrelax_old: @@ -856,34 +1034,36 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): lines_to_del_to_del.append((ctx_keys, line)) lines_to_add_to_del.append((ctx_keys, old_asrelax_cmd)) - ''' + """ If we are modifying the BGP table-map we need to avoid a del/add and instead modify the table-map in place via an add. This is needed to avoid installing all routes in the RIB the second the 'no table-map' is issued. - ''' - if line.startswith('table-map'): - found_table_map = line_exist(lines_to_add, ctx_keys, 'table-map', False) + """ + if line.startswith("table-map"): + found_table_map = line_exist(lines_to_add, ctx_keys, "table-map", False) if found_table_map: lines_to_del_to_del.append((ctx_keys, line)) - ''' + """ More old-to-new config handling. ip import-table no longer accepts distance, but we honor the old syntax. But 'show running' shows only the new syntax. This causes an unnecessary 'no import-table' followed by the same old 'ip import-table' which causes perturbations in announced routes leading to traffic blackholes. Fix this issue. - ''' - re_importtbl = re.search('^ip\s+import-table\s+(\d+)$', ctx_keys[0]) + """ + re_importtbl = re.search("^ip\s+import-table\s+(\d+)$", ctx_keys[0]) if re_importtbl: table_num = re_importtbl.group(1) for ctx in lines_to_add: - if ctx[0][0].startswith('ip import-table %s distance' % table_num): - lines_to_del_to_del.append((('ip import-table %s' % table_num,), None)) + if ctx[0][0].startswith("ip import-table %s distance" % table_num): + lines_to_del_to_del.append( + (("ip import-table %s" % table_num,), None) + ) lines_to_add_to_del.append((ctx[0], None)) - ''' + """ ip/ipv6 prefix-list can be specified without a seq number. However, the running config always adds 'seq x', where x is a number incremented by 5 for every element, to the prefix list. So, ignore such lines as @@ -891,24 +1071,36 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32 ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32 ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64 - ''' - re_ip_pfxlst = re.search('^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$', - ctx_keys[0]) + """ + re_ip_pfxlst = re.search( + "^(ip|ipv6)(\s+prefix-list\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$", + ctx_keys[0], + ) if re_ip_pfxlst: - tmpline = (re_ip_pfxlst.group(1) + re_ip_pfxlst.group(2) + - re_ip_pfxlst.group(3) + re_ip_pfxlst.group(5) + - re_ip_pfxlst.group(6)) + tmpline = ( + re_ip_pfxlst.group(1) + + re_ip_pfxlst.group(2) + + re_ip_pfxlst.group(3) + + re_ip_pfxlst.group(5) + + re_ip_pfxlst.group(6) + ) for ctx in lines_to_add: if ctx[0][0] == tmpline: lines_to_del_to_del.append((ctx_keys, None)) lines_to_add_to_del.append(((tmpline,), None)) - if (len(ctx_keys) == 3 and - ctx_keys[0].startswith('router bgp') and - ctx_keys[1] == 'address-family l2vpn evpn' and - ctx_keys[2].startswith('vni')): + if ( + len(ctx_keys) == 3 + and ctx_keys[0].startswith("router bgp") + and ctx_keys[1] == "address-family l2vpn evpn" + and ctx_keys[2].startswith("vni") + ): - re_route_target = re.search('^route-target import (.*)$', line) if line is not None else False + re_route_target = ( + re.search("^route-target import (.*)$", line) + if line is not None + else False + ) if re_route_target: rt = re_route_target.group(1).strip() @@ -916,10 +1108,14 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): route_target_export_line = "route-target export %s" % rt route_target_both_line = "route-target both %s" % rt - found_route_target_export_line = line_exist(lines_to_del, ctx_keys, route_target_export_line) - found_route_target_both_line = line_exist(lines_to_add, ctx_keys, route_target_both_line) + found_route_target_export_line = line_exist( + lines_to_del, ctx_keys, route_target_export_line + ) + found_route_target_both_line = line_exist( + lines_to_add, ctx_keys, route_target_both_line + ) - ''' + """ If the running configs has route-target import 1:1 route-target export 1:1 @@ -928,7 +1124,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): route-target both 1:1 then we can ignore deleting the import/export and ignore adding the 'both' - ''' + """ if found_route_target_export_line and found_route_target_both_line: lines_to_del_to_del.append((ctx_keys, route_target_import_line)) lines_to_del_to_del.append((ctx_keys, route_target_export_line)) @@ -937,10 +1133,9 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): # Deleting static routes under a vrf can lead to time-outs if each is sent # as separate vtysh -c commands. Change them from being in lines_to_del and # put the "no" form in lines_to_add - if ctx_keys[0].startswith('vrf ') and line: - if (line.startswith('ip route') or - line.startswith('ipv6 route')): - add_cmd = ('no ' + line) + if ctx_keys[0].startswith("vrf ") and line: + if line.startswith("ip route") or line.startswith("ipv6 route"): + add_cmd = "no " + line lines_to_add.append((ctx_keys, add_cmd)) lines_to_del_to_del.append((ctx_keys, line)) @@ -951,7 +1146,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): lines_to_del_to_del.append((ctx_keys, line)) lines_to_add_to_del.append((ctx_keys, line)) else: - ''' + """ We have commands that used to be displayed in the global part of 'router bgp' that are now displayed under 'address-family ipv4 unicast' @@ -967,8 +1162,12 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del): neighbor ISL advertisement-interval 0 Look to see if we are deleting it in one format just to add it back in the other - ''' - if ctx_keys[0].startswith('router bgp') and len(ctx_keys) > 1 and ctx_keys[1] == 'address-family ipv4 unicast': + """ + if ( + ctx_keys[0].startswith("router bgp") + and len(ctx_keys) > 1 + and ctx_keys[1] == "address-family ipv4 unicast" + ): tmp_ctx_keys = list(ctx_keys)[:-1] tmp_ctx_keys = tuple(tmp_ctx_keys) @@ -996,17 +1195,20 @@ def ignore_unconfigurable_lines(lines_to_add, lines_to_del): for (ctx_keys, line) in lines_to_del: - if (ctx_keys[0].startswith('frr version') or - ctx_keys[0].startswith('frr defaults') or - ctx_keys[0].startswith('password') or - ctx_keys[0].startswith('line vty') or - + if ( + ctx_keys[0].startswith("frr version") + or ctx_keys[0].startswith("frr defaults") + or ctx_keys[0].startswith("username") + or ctx_keys[0].startswith("password") + or ctx_keys[0].startswith("line vty") + or # This is technically "no"able but if we did so frr-reload would # stop working so do not let the user shoot themselves in the foot # by removing this. - ctx_keys[0].startswith('service integrated-vtysh-config')): + ctx_keys[0].startswith("service integrated-vtysh-config") + ): - log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line)) + log.info('"%s" cannot be removed' % (ctx_keys[-1],)) lines_to_del_to_del.append((ctx_keys, line)) for (ctx_keys, line) in lines_to_del_to_del: @@ -1040,25 +1242,35 @@ def compare_context_objects(newconf, running): lines_to_del.append((running_ctx_keys, None)) # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it - elif running_ctx_keys[0].startswith('interface') or running_ctx_keys[0].startswith('vrf'): + elif running_ctx_keys[0].startswith("interface") or running_ctx_keys[ + 0 + ].startswith("vrf"): for line in running_ctx.lines: lines_to_del.append((running_ctx_keys, line)) # If this is an address-family under 'router bgp' and we are already deleting the # entire 'router bgp' context then ignore this sub-context - elif "router bgp" in running_ctx_keys[0] and len(running_ctx_keys) > 1 and delete_bgpd: + elif ( + "router bgp" in running_ctx_keys[0] + and len(running_ctx_keys) > 1 + and delete_bgpd + ): continue # Delete an entire vni sub-context under "address-family l2vpn evpn" - elif ("router bgp" in running_ctx_keys[0] and - len(running_ctx_keys) > 2 and - running_ctx_keys[1].startswith('address-family l2vpn evpn') and - running_ctx_keys[2].startswith('vni ')): + elif ( + "router bgp" in running_ctx_keys[0] + and len(running_ctx_keys) > 2 + and running_ctx_keys[1].startswith("address-family l2vpn evpn") + and running_ctx_keys[2].startswith("vni ") + ): lines_to_del.append((running_ctx_keys, None)) - elif ("router bgp" in running_ctx_keys[0] and - len(running_ctx_keys) > 1 and - running_ctx_keys[1].startswith('address-family')): + elif ( + "router bgp" in running_ctx_keys[0] + and len(running_ctx_keys) > 1 + and running_ctx_keys[1].startswith("address-family") + ): # There's no 'no address-family' support and so we have to # delete each line individually again for line in running_ctx.lines: @@ -1068,17 +1280,31 @@ def compare_context_objects(newconf, running): # doing vtysh -c inefficient (and can time out.) For # these commands, instead of adding them to lines_to_del, # add the "no " version to lines_to_add. - elif (running_ctx_keys[0].startswith('ip route') or - running_ctx_keys[0].startswith('ipv6 route') or - running_ctx_keys[0].startswith('access-list') or - running_ctx_keys[0].startswith('ipv6 access-list') or - running_ctx_keys[0].startswith('ip prefix-list') or - running_ctx_keys[0].startswith('ipv6 prefix-list')): - add_cmd = ('no ' + running_ctx_keys[0],) + elif ( + running_ctx_keys[0].startswith("ip route") + or running_ctx_keys[0].startswith("ipv6 route") + or running_ctx_keys[0].startswith("access-list") + or running_ctx_keys[0].startswith("ipv6 access-list") + or running_ctx_keys[0].startswith("ip prefix-list") + or running_ctx_keys[0].startswith("ipv6 prefix-list") + ): + add_cmd = ("no " + running_ctx_keys[0],) lines_to_add.append((add_cmd, None)) + # if this an interface sub-subcontext in an address-family block in ldpd and + # we are already deleting the whole context, then ignore this + elif ( + len(running_ctx_keys) > 2 + and running_ctx_keys[0].startswith("mpls ldp") + and running_ctx_keys[1].startswith("address-family") + and (running_ctx_keys[:2], None) in lines_to_del + ): + continue + # Non-global context - elif running_ctx_keys and not any("address-family" in key for key in running_ctx_keys): + elif running_ctx_keys and not any( + "address-family" in key for key in running_ctx_keys + ): lines_to_del.append((running_ctx_keys, None)) elif running_ctx_keys and not any("vni" in key for key in running_ctx_keys): @@ -1112,53 +1338,79 @@ def compare_context_objects(newconf, running): for line in newconf_ctx.lines: lines_to_add.append((newconf_ctx_keys, line)) - (lines_to_add, lines_to_del) = ignore_delete_re_add_lines(lines_to_add, lines_to_del) - (lines_to_add, lines_to_del) = ignore_unconfigurable_lines(lines_to_add, lines_to_del) + (lines_to_add, lines_to_del) = check_for_exit_vrf(lines_to_add, lines_to_del) + (lines_to_add, lines_to_del) = ignore_delete_re_add_lines( + lines_to_add, lines_to_del + ) + (lines_to_add, lines_to_del) = ignore_unconfigurable_lines( + lines_to_add, lines_to_del + ) return (lines_to_add, lines_to_del) - -def vtysh_config_available(bindir, confdir): - """ - Return False if no frr daemon is running or some other vtysh session is - in 'configuration terminal' mode which will prevent us from making any - configuration changes. - """ - - try: - cmd = [str(bindir + '/vtysh'), '--config_dir', confdir, '-c', 'conf t'] - output = subprocess.check_output(cmd).strip() - - if 'VTY configuration is locked by other VTY' in output.decode('utf-8'): - print(output) - log.error("'%s' returned\n%s\n" % (' '.join(cmd), output)) - return False - - except subprocess.CalledProcessError as e: - msg = "vtysh could not connect with any frr daemons" - print(msg) - log.error(msg) - return False - - return True - - -if __name__ == '__main__': +if __name__ == "__main__": # Command line options - parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs') - parser.add_argument('--input', help='Read running config from file instead of "show running"') + parser = argparse.ArgumentParser( + description="Dynamically apply diff in frr configs" + ) + parser.add_argument( + "--input", help='Read running config from file instead of "show running"' + ) group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--reload', action='store_true', help='Apply the deltas', default=False) - group.add_argument('--test', action='store_true', help='Show the deltas', default=False) - parser.add_argument('--debug', action='store_true', help='Enable debugs', default=False) - parser.add_argument('--stdout', action='store_true', help='Log to STDOUT', default=False) - parser.add_argument('filename', help='Location of new frr config file') - parser.add_argument('--overwrite', action='store_true', help='Overwrite frr.conf with running config output', default=False) - parser.add_argument('--bindir', help='path to the vtysh executable', default='/usr/bin') - parser.add_argument('--confdir', help='path to the daemon config files', default='/etc/frr') - parser.add_argument('--rundir', help='path for the temp config file', default='/var/run/frr') - parser.add_argument('--daemon', help='daemon for which want to replace the config', default='') + group.add_argument( + "--reload", action="store_true", help="Apply the deltas", default=False + ) + group.add_argument( + "--test", action="store_true", help="Show the deltas", default=False + ) + level_group = parser.add_mutually_exclusive_group() + level_group.add_argument( + "--debug", + action="store_true", + help="Enable debugs (synonym for --log-level=debug)", + default=False, + ) + level_group.add_argument( + "--log-level", + help="Log level", + default="info", + choices=("critical", "error", "warning", "info", "debug"), + ) + parser.add_argument( + "--stdout", action="store_true", help="Log to STDOUT", default=False + ) + parser.add_argument( + "--pathspace", + "-N", + metavar="NAME", + help="Reload specified path/namespace", + default=None, + ) + parser.add_argument("filename", help="Location of new frr config file") + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite frr.conf with running config output", + default=False, + ) + parser.add_argument( + "--bindir", help="path to the vtysh executable", default="/usr/bin" + ) + parser.add_argument( + "--confdir", help="path to the daemon config files", default="/etc/frr" + ) + parser.add_argument( + "--rundir", help="path for the temp config file", default="/var/run/frr" + ) + parser.add_argument( + "--vty_socket", + help="socket to be used by vtysh to connect to the daemons", + default=None, + ) + parser.add_argument( + "--daemon", help="daemon for which want to replace the config", default="" + ) args = parser.parse_args() @@ -1166,98 +1418,136 @@ if __name__ == '__main__': # For --test log to stdout # For --reload log to /var/log/frr/frr-reload.log if args.test or args.stdout: - logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(levelname)5s: %(message)s') + logging.basicConfig(format="%(asctime)s %(levelname)5s: %(message)s") # Color the errors and warnings in red - logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)) - logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING)) + logging.addLevelName( + logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR) + ) + logging.addLevelName( + logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING) + ) elif args.reload: - if not os.path.isdir('/var/log/frr/'): - os.makedirs('/var/log/frr/') + if not os.path.isdir("/var/log/frr/"): + os.makedirs("/var/log/frr/") - logging.basicConfig(filename='/var/log/frr/frr-reload.log', - level=logging.INFO, - format='%(asctime)s %(levelname)5s: %(message)s') + logging.basicConfig( + filename="/var/log/frr/frr-reload.log", + format="%(asctime)s %(levelname)5s: %(message)s", + ) # argparse should prevent this from happening but just to be safe... else: - raise Exception('Must specify --reload or --test') + raise Exception("Must specify --reload or --test") log = logging.getLogger(__name__) + if args.debug: + log.setLevel(logging.DEBUG) + else: + log.setLevel(args.log_level.upper()) + + if args.reload and not args.stdout: + # Additionally send errors and above to STDOUT, with no metadata, + # when we are logging to a file. This specifically does not follow + # args.log_level, and is analagous to behaviour in earlier versions + # which additionally logged most errors using print(). + + stdout_hdlr = logging.StreamHandler(sys.stdout) + stdout_hdlr.setLevel(logging.ERROR) + stdout_hdlr.setFormatter(logging.Formatter()) + log.addHandler(stdout_hdlr) + # Verify the new config file is valid if not os.path.isfile(args.filename): - msg = "Filename %s does not exist" % args.filename - print(msg) - log.error(msg) + log.error("Filename %s does not exist" % args.filename) sys.exit(1) if not os.path.getsize(args.filename): - msg = "Filename %s is an empty file" % args.filename - print(msg) - log.error(msg) + log.error("Filename %s is an empty file" % args.filename) sys.exit(1) # Verify that confdir is correct if not os.path.isdir(args.confdir): - msg = "Confdir %s is not a valid path" % args.confdir - print(msg) - log.error(msg) + log.error("Confdir %s is not a valid path" % args.confdir) sys.exit(1) # Verify that bindir is correct - if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + '/vtysh'): - msg = "Bindir %s is not a valid path to vtysh" % args.bindir - print(msg) - log.error(msg) + if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + "/vtysh"): + log.error("Bindir %s is not a valid path to vtysh" % args.bindir) + sys.exit(1) + + # verify that the vty_socket, if specified, is valid + if args.vty_socket and not os.path.isdir(args.vty_socket): + log.error("vty_socket %s is not a valid path" % args.vty_socket) sys.exit(1) # verify that the daemon, if specified, is valid - if args.daemon and args.daemon not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']: - msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon - print(msg) - log.error(msg) + if args.daemon and args.daemon not in [ + "zebra", + "bgpd", + "fabricd", + "isisd", + "ospf6d", + "ospfd", + "pbrd", + "pimd", + "ripd", + "ripngd", + "sharpd", + "staticd", + "vrrpd", + "ldpd", + ]: + log.error( + "Daemon %s is not a valid option for 'show running-config'" % args.daemon + ) sys.exit(1) + vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket, args.pathspace) + # Verify that 'service integrated-vtysh-config' is configured - vtysh_filename = args.confdir + '/vtysh.conf' + if args.pathspace: + vtysh_filename = args.confdir + "/" + args.pathspace + "/vtysh.conf" + else: + vtysh_filename = args.confdir + "/vtysh.conf" service_integrated_vtysh_config = True if os.path.isfile(vtysh_filename): - with open(vtysh_filename, 'r') as fh: + with open(vtysh_filename, "r") as fh: for line in fh.readlines(): line = line.strip() - if line == 'no service integrated-vtysh-config': + if line == "no service integrated-vtysh-config": service_integrated_vtysh_config = False break if not service_integrated_vtysh_config and not args.daemon: - msg = "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'" - print(msg) - log.error(msg) + log.error( + "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'" + ) sys.exit(1) - if args.debug: - log.setLevel(logging.DEBUG) - log.info('Called via "%s"', str(args)) # Create a Config object from the config generated by newconf - newconf = Config() - newconf.load_from_file(args.filename, args.bindir, args.confdir) - reload_ok = True + newconf = Config(vtysh) + try: + newconf.load_from_file(args.filename) + reload_ok = True + except VtyshException as ve: + log.error("vtysh failed to process new configuration: {}".format(ve)) + reload_ok = False if args.test: # Create a Config object from the running config - running = Config() + running = Config(vtysh) if args.input: - running.load_from_file(args.input, args.bindir, args.confdir) + running.load_from_file(args.input) else: - running.load_from_show_running(args.bindir, args.confdir, args.daemon) + running.load_from_show_running(args.daemon) (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) lines_to_configure = [] @@ -1268,10 +1558,10 @@ if __name__ == '__main__': for (ctx_keys, line) in lines_to_del: - if line == '!': + if line == "!": continue - cmd = line_for_vtysh_file(ctx_keys, line, True) + cmd = "\n".join(lines_to_config(ctx_keys, line, True)) lines_to_configure.append(cmd) print(cmd) @@ -1281,20 +1571,20 @@ if __name__ == '__main__': for (ctx_keys, line) in lines_to_add: - if line == '!': + if line == "!": continue - cmd = line_for_vtysh_file(ctx_keys, line, False) + cmd = "\n".join(lines_to_config(ctx_keys, line, False)) lines_to_configure.append(cmd) print(cmd) elif args.reload: # We will not be able to do anything, go ahead and exit(1) - if not vtysh_config_available(args.bindir, args.confdir): + if not vtysh.is_config_available(): sys.exit(1) - log.debug('New Frr Config\n%s', newconf.get_lines()) + log.debug("New Frr Config\n%s", newconf.get_lines()) # This looks a little odd but we have to do this twice...here is why # If the user had this running bgp config: @@ -1334,9 +1624,9 @@ if __name__ == '__main__': lines_to_add_first_pass = [] for x in range(2): - running = Config() - running.load_from_show_running(args.bindir, args.confdir, args.daemon) - log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines()) + running = Config(vtysh) + running.load_from_show_running(args.daemon) + log.debug("Running Frr Config (Pass #%d)\n%s", x, running.get_lines()) (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) @@ -1361,13 +1651,13 @@ if __name__ == '__main__': if lines_to_del and x == 0: for (ctx_keys, line) in lines_to_del: - if line == '!': + if line == "!": continue # 'no' commands are tricky, we can't just put them in a file and # vtysh -f that file. See the next comment for an explanation # of their quirks - cmd = line_to_vtysh_conft(ctx_keys, line, True, args.bindir, args.confdir) + cmd = lines_to_config(ctx_keys, line, True) original_cmd = cmd # Some commands in frr are picky about taking a "no" of the entire line. @@ -1386,25 +1676,28 @@ if __name__ == '__main__': while True: try: - _ = subprocess.check_output(cmd) + vtysh(["configure"] + cmd) - except subprocess.CalledProcessError: + except VtyshException: # - Pull the last entry from cmd (this would be # 'no ip ospf authentication message-digest 1.1.1.1' in # our example above # - Split that last entry by whitespace and drop the last word - log.info('Failed to execute %s', ' '.join(cmd)) - last_arg = cmd[-1].split(' ') + log.info("Failed to execute %s", " ".join(cmd)) + last_arg = cmd[-1].split(" ") if len(last_arg) <= 2: - log.error('"%s" we failed to remove this command', original_cmd) + log.error( + '"%s" we failed to remove this command', + " -- ".join(original_cmd), + ) break new_last_arg = last_arg[0:-1] - cmd[-1] = ' '.join(new_last_arg) + cmd[-1] = " ".join(new_last_arg) else: - log.info('Executed "%s"', ' '.join(cmd)) + log.info('Executed "%s"', " ".join(cmd)) break if lines_to_add: @@ -1412,40 +1705,43 @@ if __name__ == '__main__': for (ctx_keys, line) in lines_to_add: - if line == '!': + if line == "!": continue # Don't run "no" commands twice since they can error # out the second time due to first deletion - if x == 1 and ctx_keys[0].startswith('no '): + if x == 1 and ctx_keys[0].startswith("no "): continue - cmd = line_for_vtysh_file(ctx_keys, line, False) + cmd = "\n".join(lines_to_config(ctx_keys, line, False)) + "\n" lines_to_configure.append(cmd) if lines_to_configure: - random_string = ''.join(random.SystemRandom().choice( - string.ascii_uppercase + - string.digits) for _ in range(6)) + random_string = "".join( + random.SystemRandom().choice( + string.ascii_uppercase + string.digits + ) + for _ in range(6) + ) filename = args.rundir + "/reload-%s.txt" % random_string log.info("%s content\n%s" % (filename, pformat(lines_to_configure))) - with open(filename, 'w') as fh: + with open(filename, "w") as fh: for line in lines_to_configure: - fh.write(line + '\n') + fh.write(line + "\n") try: - subprocess.check_output([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-f', filename]) - except subprocess.CalledProcessError as e: - log.warning("frr-reload.py failed due to\n%s" % e.output) + vtysh.exec_file(filename) + except VtyshException as e: + log.warning("frr-reload.py failed due to\n%s" % e.args) reload_ok = False os.unlink(filename) # Make these changes persistent - target = str(args.confdir + '/frr.conf') + target = str(args.confdir + "/frr.conf") if args.overwrite or (not args.daemon and args.filename != target): - subprocess.call([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-c', 'write']) + vtysh("write") if not reload_ok: sys.exit(1) |
