summaryrefslogtreecommitdiff
path: root/tools/frr-reload.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/frr-reload.py')
-rwxr-xr-xtools/frr-reload.py334
1 files changed, 201 insertions, 133 deletions
diff --git a/tools/frr-reload.py b/tools/frr-reload.py
index 0e0aec9839..ef92e8b59f 100755
--- a/tools/frr-reload.py
+++ b/tools/frr-reload.py
@@ -25,6 +25,7 @@ from collections import OrderedDict
from ipaddress import IPv6Address, ip_network
from pprint import pformat
+
# Python 3
def iteritems(d):
return iter(d.items())
@@ -219,6 +220,53 @@ def get_normalized_mac_ip_line(line):
return line
+# This dictionary contains a tree of all commands that we know start a
+# new multi-line context. All other commands are treated either as
+# commands inside a multi-line context or as single-line contexts. This
+# dictionary should be updated whenever a new node is added to FRR.
+ctx_keywords = {
+ "router bgp ": {
+ "address-family ": {
+ "vni ": {},
+ },
+ "vnc defaults": {},
+ "vnc nve-group ": {},
+ "vnc l2-group ": {},
+ "vrf-policy ": {},
+ "bmp targets ": {},
+ "segment-routing srv6": {},
+ },
+ "router rip": {},
+ "router ripng": {},
+ "router isis ": {},
+ "router openfabric ": {},
+ "router ospf": {},
+ "router ospf6": {},
+ "router eigrp ": {},
+ "router babel": {},
+ "mpls ldp": {"address-family ": {"interface ": {}}},
+ "l2vpn ": {"member pseudowire ": {}},
+ "key chain ": {"key ": {}},
+ "vrf ": {},
+ "interface ": {"link-params": {}},
+ "pseudowire ": {},
+ "segment-routing": {
+ "traffic-eng": {
+ "segment-list ": {},
+ "policy ": {"candidate-path ": {}},
+ "pcep": {"pcc": {}, "pce ": {}, "pce-config ": {}},
+ },
+ "srv6": {"locators": {"locator ": {}}},
+ },
+ "nexthop-group ": {},
+ "route-map ": {},
+ "pbr-map ": {},
+ "rpki": {},
+ "bfd": {"peer ": {}, "profile ": {}},
+ "line vty": {},
+}
+
+
class Config(object):
"""
A frr configuration is stored in a Config object. A Config object
@@ -315,7 +363,7 @@ class Config(object):
"""
Return the parsed context as strings for display, log etc.
"""
- for (_, ctx) in sorted(iteritems(self.contexts)):
+ for _, ctx in sorted(iteritems(self.contexts)):
print(str(ctx))
def save_contexts(self, key, lines):
@@ -490,54 +538,7 @@ class Config(object):
key of the context. So "router bgp 10" is the key for the non-address
family part of bgp, "router bgp 10, address-family ipv6 unicast" is
the key for the subcontext and so on.
-
- This dictionary contains a tree of all commands that we know start a
- new multi-line context. All other commands are treated either as
- commands inside a multi-line context or as single-line contexts. This
- dictionary should be updated whenever a new node is added to FRR.
"""
- ctx_keywords = {
- "router bgp ": {
- "address-family ": {
- "vni ": {},
- },
- "vnc defaults": {},
- "vnc nve-group ": {},
- "vnc l2-group ": {},
- "vrf-policy ": {},
- "bmp targets ": {},
- "segment-routing srv6": {},
- },
- "router rip": {},
- "router ripng": {},
- "router isis ": {},
- "router openfabric ": {},
- "router ospf": {},
- "router ospf6": {},
- "router eigrp ": {},
- "router babel": {},
- "mpls ldp": {"address-family ": {"interface ": {}}},
- "l2vpn ": {"member pseudowire ": {}},
- "key chain ": {"key ": {}},
- "vrf ": {},
- "interface ": {"link-params": {}},
- "pseudowire ": {},
- "segment-routing": {
- "traffic-eng": {
- "segment-list ": {},
- "policy ": {"candidate-path ": {}},
- "pcep": {"pcc": {}, "pce ": {}, "pce-config ": {}},
- },
- "srv6": {"locators": {"locator ": {}}},
- },
- "nexthop-group ": {},
- "route-map ": {},
- "pbr-map ": {},
- "rpki": {},
- "bfd": {"peer ": {}, "profile ": {}},
- "line vty": {},
- }
-
# stack of context keys
ctx_keys = []
# stack of context keywords
@@ -546,7 +547,6 @@ class Config(object):
cur_ctx_lines = []
for line in self.lines:
-
if not line:
continue
@@ -632,8 +632,22 @@ def lines_to_config(ctx_keys, line, delete):
"""
cmd = []
+ # If there's no `line` and `ctx_keys` length is 1, then it may be a single-line command.
+ # In this case, we should treat it as a single command in an empty context.
+ if len(ctx_keys) == 1 and not line:
+ single = True
+
+ for k, v in ctx_keywords.items():
+ if ctx_keys[0].startswith(k):
+ single = False
+ break
+
+ if single:
+ line = ctx_keys[0]
+ ctx_keys = []
+
if line:
- for (i, ctx_key) in enumerate(ctx_keys):
+ for i, ctx_key in enumerate(ctx_keys):
cmd.append(" " * i + ctx_key)
line = line.lstrip()
@@ -652,6 +666,9 @@ def lines_to_config(ctx_keys, line, delete):
else:
cmd.append(indent + line)
+ for i in reversed(range(len(ctx_keys))):
+ cmd.append(" " * i + "exit")
+
# If line is None then we are typically deleting an entire
# context ('no router ospf' for example)
else:
@@ -666,6 +683,10 @@ def lines_to_config(ctx_keys, line, delete):
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("%sexit" % (" " * (len(ctx_keys) - 1)))
+
+ for i in reversed(range(len(ctx_keys) - 1)):
+ cmd.append(" " * i + "exit")
return cmd
@@ -704,7 +725,7 @@ def get_normalized_ipv6_line(line):
def line_exist(lines, target_ctx_keys, target_line, exact_match=True):
- for (ctx_keys, line) in lines:
+ for ctx_keys, line in lines:
if ctx_keys == target_ctx_keys:
if exact_match:
if line == target_line:
@@ -715,38 +736,6 @@ 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 != "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 bgp_delete_inst_move_line(lines_to_del):
# Deletion of bgp default inst followed by
# bgp vrf inst leads to issue of default
@@ -755,7 +744,7 @@ def bgp_delete_inst_move_line(lines_to_del):
bgp_defult_inst = False
bgp_vrf_inst = False
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
# Find bgp default inst
if (
ctx_keys[0].startswith("router bgp")
@@ -768,7 +757,7 @@ def bgp_delete_inst_move_line(lines_to_del):
bgp_vrf_inst = True
if bgp_defult_inst and bgp_vrf_inst:
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
# move bgp default inst to end
if (
ctx_keys[0].startswith("router bgp")
@@ -864,7 +853,6 @@ def bgp_delete_nbr_remote_as_line(lines_to_add):
def bgp_remove_neighbor_cfg(lines_to_del, del_nbr_dict):
-
# This method handles deletion of bgp neighbor configs,
# if there is neighbor to peer-group cmd is in delete list.
# As 'no neighbor .* peer-group' deletes the neighbor,
@@ -872,7 +860,7 @@ def bgp_remove_neighbor_cfg(lines_to_del, del_nbr_dict):
# in error.
lines_to_del_to_del = []
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
if (
ctx_keys[0].startswith("router bgp")
and line
@@ -887,11 +875,11 @@ def bgp_remove_neighbor_cfg(lines_to_del, del_nbr_dict):
if re_nb:
lines_to_del_to_del.append((ctx_keys, line))
- for (ctx_keys, line) in lines_to_del_to_del:
+ for ctx_keys, line in lines_to_del_to_del:
lines_to_del.remove((ctx_keys, line))
-def delete_move_lines(lines_to_add, lines_to_del):
+def bgp_delete_move_lines(lines_to_add, lines_to_del):
# This method handles deletion of bgp peer group config.
# The objective is to delete config lines related to peers
# associated with the peer-group and move the peer-group
@@ -953,7 +941,7 @@ def delete_move_lines(lines_to_add, lines_to_del):
# "router bgp 200 no neighbor uplink1 interface remote-as internal"
# "router bgp 200 no neighbor underlay peer-group"
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
if (
ctx_keys[0].startswith("router bgp")
and line
@@ -1004,19 +992,20 @@ def delete_move_lines(lines_to_add, lines_to_del):
del_dict[ctx_keys[0]][re_pg.group(1)] = list()
found_pg_del_cmd = True
+ # move neighbor remote-as lines at the end
+ for ctx_keys, line in lines_to_del_to_app:
+ lines_to_del.remove((ctx_keys, line))
+ lines_to_del.append((ctx_keys, line))
+
if found_pg_del_cmd == False:
bgp_delete_inst_move_line(lines_to_del)
if del_nbr_dict:
bgp_remove_neighbor_cfg(lines_to_del, del_nbr_dict)
return (lines_to_add, lines_to_del)
- for (ctx_keys, line) in lines_to_del_to_app:
- lines_to_del.remove((ctx_keys, line))
- lines_to_del.append((ctx_keys, line))
-
# {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
# 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
if (
ctx_keys[0].startswith("router bgp")
and line
@@ -1034,7 +1023,7 @@ def delete_move_lines(lines_to_add, lines_to_del):
del_dict[ctx_keys[0]][pg_key].append(re_nbr_pg.group(1))
lines_to_del_to_app = []
- for (ctx_keys, line) in lines_to_del:
+ for ctx_keys, line in lines_to_del:
if (
ctx_keys[0].startswith("router bgp")
and line
@@ -1054,10 +1043,10 @@ def delete_move_lines(lines_to_add, lines_to_del):
if re_pg:
lines_to_del_to_app.append((ctx_keys, line))
- for (ctx_keys, line) in lines_to_del_to_del:
+ for ctx_keys, line in lines_to_del_to_del:
lines_to_del.remove((ctx_keys, line))
- for (ctx_keys, line) in lines_to_del_to_app:
+ for ctx_keys, line in lines_to_del_to_app:
lines_to_del.remove((ctx_keys, line))
lines_to_del.append((ctx_keys, line))
@@ -1066,19 +1055,73 @@ def delete_move_lines(lines_to_add, lines_to_del):
return (lines_to_add, lines_to_del)
-def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
+def pim_delete_move_lines(lines_to_add, lines_to_del):
+ # Under interface context, if 'no ip pim' is present
+ # remove subsequent 'no ip pim <blah>' options as it
+ # they are implicitly deleted by 'no ip pim'.
+ # Remove all such depdendent options from delete
+ # pending list.
+ pim_disable = False
+
+ for ctx_keys, line in lines_to_del:
+ if ctx_keys[0].startswith("interface") and line and line == "ip pim":
+ pim_disable = True
+
+ if pim_disable:
+ for ctx_keys, line in lines_to_del:
+ if (
+ ctx_keys[0].startswith("interface")
+ and line
+ and line.startswith("ip pim ")
+ ):
+ lines_to_del.remove((ctx_keys, line))
+
+ return (lines_to_add, lines_to_del)
+
+
+def delete_move_lines(lines_to_add, lines_to_del):
+ lines_to_add, lines_to_del = bgp_delete_move_lines(lines_to_add, lines_to_del)
+ lines_to_add, lines_to_del = pim_delete_move_lines(lines_to_add, lines_to_del)
+
+ 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
lines_to_add_to_del = []
lines_to_del_to_del = []
- for (ctx_keys, line) in lines_to_del:
+ index = -1
+ for ctx_keys, line in lines_to_del:
deleted = False
+ # no form of route-map description command only
+ # accept 'no description', replace 'no description blah'
+ # to just 'no description'.
+ index = index + 1
+ if (
+ ctx_keys[0].startswith("route-map")
+ and line
+ and line.startswith("description ")
+ ):
+ lines_to_del.remove((ctx_keys, line))
+ lines_to_del.insert(index, (ctx_keys, "description"))
+
+ # interface x ; description blah
+ # no form of description does not accept any argument,
+ # strip arg before rendering
+ if (
+ ctx_keys[0].startswith("interface ")
+ and line
+ and line.startswith("description ")
+ ):
+ lines_to_del.remove((ctx_keys, line))
+ lines_to_del.insert(index, (ctx_keys, "description"))
+
# If there is a change in the segment routing block ranges, do it
# in-place, to avoid requesting spurious label chunks which might fail
if line and "segment-routing global-block" in line:
- for (add_key, add_line) in lines_to_add:
+ for add_key, add_line in lines_to_add:
if (
ctx_keys[0] == add_key[0]
and add_line
@@ -1089,7 +1132,6 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
continue
if ctx_keys[0].startswith("router bgp") and line:
-
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:
@@ -1171,7 +1213,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
bfd_nbr = "neighbor %s" % nbr
bfd_search_string = bfd_nbr + r" bfd (\S+) (\S+) (\S+)"
- for (ctx_keys, add_line) in lines_to_add:
+ 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
@@ -1201,7 +1243,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
dir = re_nbr_rm.group(3)
search = "neighbor%sroute-map(.*)%s" % (neighbor_name, dir)
save_line = "EMPTY"
- for (ctx_keys_al, add_line) in lines_to_add:
+ for ctx_keys_al, add_line in lines_to_add:
if ctx_keys_al[0].startswith("router bgp"):
if add_line:
rm_match = re.search(search, add_line)
@@ -1221,7 +1263,7 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
lines_to_del_to_del.append((ctx_keys_al, line))
if adjust_for_bgp_node == 1:
- for (ctx_keys_dl, dl_line) in lines_to_del:
+ for ctx_keys_dl, dl_line in lines_to_del:
if (
ctx_keys_dl[0].startswith("router bgp")
and len(ctx_keys_dl) > 1
@@ -1403,7 +1445,6 @@ def ignore_delete_re_add_lines(lines_to_add, lines_to_del):
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
@@ -1479,13 +1520,17 @@ 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((tmp_ctx_keys, line))
- for (ctx_keys, line) in lines_to_del_to_del:
- if line is not None:
+ for ctx_keys, line in lines_to_del_to_del:
+ try:
lines_to_del.remove((ctx_keys, line))
+ except ValueError:
+ pass
- for (ctx_keys, line) in lines_to_add_to_del:
- if line is not None:
+ for ctx_keys, line in lines_to_add_to_del:
+ try:
lines_to_add.remove((ctx_keys, line))
+ except ValueError:
+ pass
return (lines_to_add, lines_to_del)
@@ -1497,8 +1542,7 @@ def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
"""
lines_to_del_to_del = []
- for (ctx_keys, line) in lines_to_del:
-
+ for ctx_keys, line in lines_to_del:
# The integrated-vtysh-config one 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.
@@ -1519,7 +1563,7 @@ def ignore_unconfigurable_lines(lines_to_add, lines_to_del):
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:
+ for ctx_keys, line in lines_to_del_to_del:
lines_to_del.remove((ctx_keys, line))
return (lines_to_add, lines_to_del)
@@ -1539,13 +1583,32 @@ def compare_context_objects(newconf, running):
pcclist_to_del = []
candidates_to_add = []
delete_bgpd = False
+ area_stub_no_sum = "area (\S+) stub no-summary"
+ deleted_keychains = []
# Find contexts that are in newconf but not in running
# Find contexts that are in running but not in newconf
- for (running_ctx_keys, running_ctx) in iteritems(running.contexts):
+ for running_ctx_keys, running_ctx in iteritems(running.contexts):
+ if running_ctx_keys in newconf.contexts:
+ newconf_ctx = newconf.contexts[running_ctx_keys]
- if running_ctx_keys not in newconf.contexts:
+ for line in running_ctx.lines:
+ # ospf area <> stub no-summary line removal requires
+ # to remoe area <> stub as no form of original
+ # retains the stub form.
+ # lines_to_del will contain:
+ # no area <x> stub no-summary and
+ # no area <x> stub
+ if (
+ running_ctx_keys[0].startswith("router ospf")
+ and line not in newconf_ctx.dlines
+ ):
+ re_area_stub_no_sum = re.search(area_stub_no_sum, line)
+ if re_area_stub_no_sum:
+ new_del_line = "area %s stub" % re_area_stub_no_sum.group(1)
+ lines_to_del.append((running_ctx_keys, new_del_line))
+ if running_ctx_keys not in newconf.contexts:
# We check that the len is 1 here so that we only look at ('router bgp 10')
# and not ('router bgp 10', 'address-family ipv4 unicast'). The
# latter could cause a false delete_bgpd positive if ipv4 unicast is in
@@ -1570,6 +1633,22 @@ def compare_context_objects(newconf, running):
):
continue
+ # Check if key chain is being deleted:
+ # - If it is being deleted then avoid deleting its contexts
+ # - Else delete its configuration without removing the root node
+ elif (
+ running_ctx_keys[0].startswith("key chain ")
+ and len(running_ctx_keys) == 1
+ ):
+ deleted_keychains.append(running_ctx_keys[0])
+ lines_to_del.append((running_ctx_keys, None))
+ elif (
+ running_ctx_keys[0].startswith("key chain ")
+ and len(running_ctx_keys) > 1
+ and running_ctx_keys[0] in deleted_keychains
+ ):
+ continue
+
# Delete an entire vni sub-context under "address-family l2vpn evpn"
elif (
"router bgp" in running_ctx_keys[0]
@@ -1700,14 +1779,12 @@ def compare_context_objects(newconf, running):
# Find the lines within each context to add
# Find the lines within each context to del
- for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
-
+ for newconf_ctx_keys, newconf_ctx in iteritems(newconf.contexts):
if newconf_ctx_keys in running.contexts:
running_ctx = running.contexts[newconf_ctx_keys]
for line in newconf_ctx.lines:
if line not in running_ctx.dlines:
-
# candidate paths can only be added after the policy and segment list,
# so add them to a separate array that is going to be appended at the end
if (
@@ -1725,10 +1802,8 @@ def compare_context_objects(newconf, running):
if line not in newconf_ctx.dlines:
lines_to_del.append((newconf_ctx_keys, line))
- for (newconf_ctx_keys, newconf_ctx) in iteritems(newconf.contexts):
-
+ for newconf_ctx_keys, newconf_ctx in iteritems(newconf.contexts):
if newconf_ctx_keys not in running.contexts:
-
# candidate paths can only be added after the policy and segment list,
# so add them to a separate array that is going to be appended at the end
if (
@@ -1750,7 +1825,6 @@ def compare_context_objects(newconf, running):
if len(candidates_to_add) > 0:
lines_to_add.extend(candidates_to_add)
- (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
)
@@ -1964,7 +2038,6 @@ if __name__ == "__main__":
reload_ok = False
if args.test:
-
# Create a Config object from the running config
running = Config(vtysh)
@@ -1980,8 +2053,7 @@ if __name__ == "__main__":
print("\nLines To Delete")
print("===============")
- for (ctx_keys, line) in lines_to_del:
-
+ for ctx_keys, line in lines_to_del:
if line == "!":
continue
@@ -2007,8 +2079,7 @@ if __name__ == "__main__":
print("\nLines To Add")
print("============")
- for (ctx_keys, line) in lines_to_add:
-
+ for ctx_keys, line in lines_to_add:
if line == "!":
continue
@@ -2096,8 +2167,7 @@ if __name__ == "__main__":
# apply to other scenarios as well where configuring FOO adds BAR
# to the config.
if lines_to_del and x == 0:
- for (ctx_keys, line) in lines_to_del:
-
+ for ctx_keys, line in lines_to_del:
if line == "!":
continue
@@ -2127,12 +2197,11 @@ if __name__ == "__main__":
vtysh(["configure"] + cmd, stdouts)
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))
+ log.error("Failed to execute %s", " ".join(cmd))
last_arg = cmd[-1].split(" ")
if len(last_arg) <= 2:
@@ -2155,8 +2224,7 @@ if __name__ == "__main__":
if lines_to_add:
lines_to_configure = []
- for (ctx_keys, line) in lines_to_add:
-
+ for ctx_keys, line in lines_to_add:
if line == "!":
continue