summaryrefslogtreecommitdiff
path: root/tools/frr-reload.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/frr-reload.py')
-rwxr-xr-xtools/frr-reload.py891
1 files changed, 560 insertions, 331 deletions
diff --git a/tools/frr-reload.py b/tools/frr-reload.py
index 88873da904..951383beb2 100755
--- a/tools/frr-reload.py
+++ b/tools/frr-reload.py
@@ -39,6 +39,7 @@ import string
import subprocess
import sys
from collections import OrderedDict
+
try:
from ipaddress import IPv6Address, ip_network
except ImportError:
@@ -51,45 +52,49 @@ 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 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')]
+ self.common_args = [os.path.join(bindir or "", "vtysh")]
if confdir:
- self.common_args.extend(['--config_dir', confdir])
+ self.common_args.extend(["--config_dir", confdir])
if sockdir:
- self.common_args.extend(['--vty_socket', sockdir])
+ self.common_args.extend(["--vty_socket", sockdir])
if pathspace:
- self.common_args.extend(['-N', 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
+ kwargs["stdin"] = stdin
if stdout is not None:
- kwargs['stdout'] = stdout
+ kwargs["stdout"] = stdout
if stderr is not None:
- kwargs['stderr'] = stderr
+ 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]]
+ args = [item for sub in command for item in ["-c", sub]]
else:
- args = ['-c', command]
+ args = ["-c", command]
return self._call(args, stdin, stdout, stderr)
def __call__(self, command):
@@ -102,9 +107,10 @@ class Vtysh(object):
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')
+ raise VtyshException(
+ 'vtysh returned status %d for command "%s"' % (proc.returncode, command)
+ )
+ return stdout.decode("UTF-8")
def is_config_available(self):
"""
@@ -113,56 +119,69 @@ class Vtysh(object):
configuration changes.
"""
- output = self('configure')
+ output = self("configure")
- if 'VTY configuration is locked by other VTY' in output:
+ 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])
+ child = self._call(["-f", filename])
if child.wait() != 0:
- raise VtyshException('vtysh (exec file) exited with status %d'
- % (child.returncode))
+ 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)
+ 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!')
+ raise VtyshException("vtysh call timed out!")
if child.wait() != 0:
- raise VtyshException('vtysh (mark file) exited with status %d:\n%s'
- % (child.returncode, stderr))
+ raise VtyshException(
+ "vtysh (mark file) exited with status %d:\n%s"
+ % (child.returncode, stderr)
+ )
- return stdout.decode('UTF-8')
+ return stdout.decode("UTF-8")
- def mark_show_run(self, daemon = None):
- cmd = 'show running-config'
+ def mark_show_run(self, daemon=None):
+ cmd = "show running-config"
if daemon:
- cmd += ' %s' % daemon
- cmd += ' no-header'
+ 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)
+ 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))
+ 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))
+ raise VtyshException(
+ "vtysh (mark running-config) exited with status %d" % (mark.returncode)
+ )
+
+ return stdout.decode("UTF-8")
- return stdout.decode('UTF-8')
class Context(object):
@@ -222,15 +241,15 @@ class Config(object):
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)
file_output = self.vtysh.mark_file(filename)
- for line in file_output.split('\n'):
+ for line in file_output.split("\n"):
line = line.strip()
# Compress duplicate whitespaces
- line = ' '.join(line.split())
+ line = " ".join(line.split())
if ":" in line and not "ipv6 add":
qv6_line = get_normalized_ipv6_line(line)
@@ -246,16 +265,18 @@ class Config(object):
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")
config_text = self.vtysh.mark_show_run(daemon)
- for line in config_text.split('\n'):
+ 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)
@@ -267,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):
"""
@@ -275,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):
"""
@@ -285,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
@@ -387,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:
@@ -416,7 +457,7 @@ class Config(object):
current_context_lines = []
ctx_keys = []
- '''
+ """
The end of a context is flagged via the 'end' keyword:
!
@@ -460,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
@@ -480,38 +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",
- "evpn mh")
+ 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
@@ -519,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
@@ -545,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 = []
@@ -561,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:
@@ -570,70 +628,92 @@ 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("peer") or
- line.startswith("key ") 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)
@@ -647,20 +727,20 @@ def lines_to_config(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)
@@ -669,16 +749,16 @@ def lines_to_config(ctx_keys, line, delete):
# context ('no router ospf' for example)
else:
for i, ctx_key in enumerate(ctx_keys[:-1]):
- cmd.append('%s%s' % (' ' * i, ctx_key))
+ cmd.append("%s%s" % (" " * i, 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:]))
+ 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]))
+ cmd.append("%sno %s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
else:
- cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1]))
+ cmd.append("%s%s" % (" " * (len(ctx_keys) - 1), ctx_keys[-1]))
return cmd
@@ -691,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:
@@ -728,6 +811,7 @@ def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
return 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
@@ -740,25 +824,26 @@ def check_for_exit_vrf(lines_to_add, lines_to_del):
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),
+ 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 ctx_keys[0].startswith("vrf") and line:
if line is not "exit-vrf":
add_exit_vrf = True
- prior_ctx_key = (ctx_keys[0])
+ prior_ctx_key = ctx_keys[0]
else:
add_exit_vrf = False
- index+=1
+ index += 1
for (ctx_keys, line) in lines_to_del:
if line == "exit-vrf":
- if (line_exist(lines_to_add, ctx_keys, line)):
+ 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
@@ -768,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
@@ -788,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
@@ -807,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
@@ -829,30 +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:
- if ctx_keys[0].startswith('router bgp'):
- 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)
+ 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))
- '''
+ """
We changed how we display the neighbor interface command. Older
versions of frr would display the following:
neighbor swp1 interface
@@ -874,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
@@ -892,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:
@@ -902,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
@@ -910,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:
@@ -921,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
@@ -956,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()
@@ -981,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
@@ -993,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))
@@ -1002,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))
@@ -1016,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'
@@ -1032,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)
@@ -1061,16 +1195,18 @@ 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('username') 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" cannot be removed' % (ctx_keys[-1],))
lines_to_del_to_del.append((ctx_keys, line))
@@ -1106,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:
@@ -1134,24 +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):
+ 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):
@@ -1186,33 +1339,78 @@ def compare_context_objects(newconf, running):
lines_to_add.append((newconf_ctx_keys, line))
(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)
+ (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)
-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)
+ 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='')
+ 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()
@@ -1220,22 +1418,28 @@ 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(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',
- 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:
@@ -1269,40 +1473,59 @@ if __name__ == '__main__':
sys.exit(1)
# Verify that bindir is correct
- if not os.path.isdir(args.bindir) or not os.path.isfile(args.bindir + '/vtysh'):
+ 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)
+ 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']:
- log.error("Daemon %s is not a valid option for 'show running-config'" % args.daemon)
+ 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
if args.pathspace:
- vtysh_filename = args.confdir + '/' + args.pathspace + '/vtysh.conf'
+ vtysh_filename = args.confdir + "/" + args.pathspace + "/vtysh.conf"
else:
- vtysh_filename = args.confdir + '/vtysh.conf'
+ 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:
- log.error("'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'")
+ log.error(
+ "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
+ )
sys.exit(1)
log.info('Called via "%s"', str(args))
@@ -1335,10 +1558,10 @@ if __name__ == '__main__':
for (ctx_keys, line) in lines_to_del:
- if line == '!':
+ if line == "!":
continue
- cmd = '\n'.join(lines_to_config(ctx_keys, line, True))
+ cmd = "\n".join(lines_to_config(ctx_keys, line, True))
lines_to_configure.append(cmd)
print(cmd)
@@ -1348,10 +1571,10 @@ if __name__ == '__main__':
for (ctx_keys, line) in lines_to_add:
- if line == '!':
+ if line == "!":
continue
- cmd = '\n'.join(lines_to_config(ctx_keys, line, False))
+ cmd = "\n".join(lines_to_config(ctx_keys, line, False))
lines_to_configure.append(cmd)
print(cmd)
@@ -1361,7 +1584,7 @@ if __name__ == '__main__':
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:
@@ -1403,7 +1626,7 @@ if __name__ == '__main__':
for x in range(2):
running = Config(vtysh)
running.load_from_show_running(args.daemon)
- log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines())
+ log.debug("Running Frr Config (Pass #%d)\n%s", x, running.get_lines())
(lines_to_add, lines_to_del) = compare_context_objects(newconf, running)
@@ -1428,7 +1651,7 @@ 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
@@ -1453,7 +1676,7 @@ if __name__ == '__main__':
while True:
try:
- vtysh(['configure'] + cmd)
+ vtysh(["configure"] + cmd)
except VtyshException:
@@ -1461,17 +1684,20 @@ if __name__ == '__main__':
# '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', ' -- '.join(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:
@@ -1479,28 +1705,31 @@ 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 = '\n'.join(lines_to_config(ctx_keys, line, False)) + '\n'
+ 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:
vtysh.exec_file(filename)
@@ -1510,9 +1739,9 @@ if __name__ == '__main__':
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):
- vtysh('write')
+ vtysh("write")
if not reload_ok:
sys.exit(1)