2 # SPDX-License-Identifier: GPL-2.0-or-later
4 # Copyright (C) 2014 Cumulus Networks, Inc.
8 - reads a frr configuration text file
9 - reads frr's current running configuration via "vtysh -c 'show running'"
10 - compares the two configs and determines what commands to execute to
11 synchronize frr's running configuration with the configuation in the
15 from __future__
import print_function
, unicode_literals
24 from collections
import OrderedDict
25 from ipaddress
import IPv6Address
, ip_network
26 from pprint
import pformat
30 return iter(d
.items())
33 log
= logging
.getLogger(__name__
)
36 class VtyshException(Exception):
41 def __init__(self
, bindir
=None, confdir
=None, sockdir
=None, pathspace
=None):
43 self
.confdir
= confdir
44 self
.pathspace
= pathspace
45 self
.common_args
= [os
.path
.join(bindir
or "", "vtysh")]
47 self
.common_args
.extend(["--config_dir", confdir
])
49 self
.common_args
.extend(["--vty_socket", sockdir
])
51 self
.common_args
.extend(["-N", pathspace
])
53 def _call(self
, args
, stdin
=None, stdout
=None, stderr
=None):
56 kwargs
["stdin"] = stdin
57 if stdout
is not None:
58 kwargs
["stdout"] = stdout
59 if stderr
is not None:
60 kwargs
["stderr"] = stderr
61 return subprocess
.Popen(self
.common_args
+ args
, **kwargs
)
63 def _call_cmd(self
, command
, stdin
=None, stdout
=None, stderr
=None):
64 if isinstance(command
, list):
65 args
= [item
for sub
in command
for item
in ["-c", sub
]]
67 args
= ["-c", command
]
68 return self
._call
(args
, stdin
, stdout
, stderr
)
70 def __call__(self
, command
, stdouts
=None):
72 Call a CLI command (e.g. "show running-config")
74 Output text is automatically redirected, decoded and returned.
75 Multiple commands may be passed as list.
77 proc
= self
._call
_cmd
(command
, stdout
=subprocess
.PIPE
)
78 stdout
, stderr
= proc
.communicate()
80 if stdouts
is not None:
81 stdouts
.append(stdout
.decode("UTF-8"))
83 'vtysh returned status %d for command "%s"' % (proc
.returncode
, command
)
85 return stdout
.decode("UTF-8")
87 def is_config_available(self
):
89 Return False if no frr daemon is running or some other vtysh session is
90 in 'configuration terminal' mode which will prevent us from making any
91 configuration changes.
94 output
= self("configure")
96 if "VTY configuration is locked by other VTY" in output
:
97 log
.error("vtysh 'configure' returned\n%s\n" % (output
))
102 def exec_file(self
, filename
):
103 child
= self
._call
(["-f", filename
])
104 if child
.wait() != 0:
105 raise VtyshException(
106 "vtysh (exec file) exited with status %d" % (child
.returncode
)
109 def mark_file(self
, filename
, stdin
=None):
111 ["-m", "-f", filename
],
112 stdout
=subprocess
.PIPE
,
113 stdin
=subprocess
.PIPE
,
114 stderr
=subprocess
.PIPE
,
117 stdout
, stderr
= child
.communicate()
118 except subprocess
.TimeoutExpired
:
120 stdout
, stderr
= child
.communicate()
121 raise VtyshException("vtysh call timed out!")
123 if child
.wait() != 0:
124 raise VtyshException(
125 "vtysh (mark file) exited with status %d:\n%s"
126 % (child
.returncode
, stderr
)
129 return stdout
.decode("UTF-8")
131 def mark_show_run(self
, daemon
=None):
132 cmd
= "show running-config"
134 cmd
+= " %s" % daemon
136 show_run
= self
._call
_cmd
(cmd
, stdout
=subprocess
.PIPE
)
138 ["-m", "-f", "-"], stdin
=show_run
.stdout
, stdout
=subprocess
.PIPE
142 stdout
, stderr
= mark
.communicate()
145 if show_run
.returncode
!= 0:
146 raise VtyshException(
147 "vtysh (show running-config) exited with status %d:"
148 % (show_run
.returncode
)
150 if mark
.returncode
!= 0:
151 raise VtyshException(
152 "vtysh (mark running-config) exited with status %d" % (mark
.returncode
)
155 return stdout
.decode("UTF-8")
158 class Context(object):
160 A Context object represents a section of frr configuration such as:
163 description swp3 -> r8's swp1
168 or a single line context object such as this:
174 def __init__(self
, keys
, lines
):
178 # Keep a dictionary of the lines, this is to make it easy to tell if a
179 # line exists in this Context
180 self
.dlines
= OrderedDict()
183 self
.dlines
[ligne
] = True
186 return str(self
.keys
) + " : " + str(self
.lines
)
188 def add_lines(self
, lines
):
190 Add lines to specified context
193 self
.lines
.extend(lines
)
196 self
.dlines
[ligne
] = True
199 def get_normalized_es_id(line
):
201 The es-id or es-sys-mac need to be converted to lower case
203 sub_strs
= ["evpn mh es-id", "evpn mh es-sys-mac"]
204 for sub_str
in sub_strs
:
205 obj
= re
.match(sub_str
+ " (?P<esi>\S*)", line
)
207 line
= "%s %s" % (sub_str
, obj
.group("esi").lower())
212 def get_normalized_mac_ip_line(line
):
213 if line
.startswith("evpn mh es"):
214 return get_normalized_es_id(line
)
216 if not "ipv6 add" in line
:
217 return get_normalized_ipv6_line(line
)
222 class Config(object):
224 A frr configuration is stored in a Config object. A Config object
225 contains a dictionary of Context objects where the Context keys
226 ('router ospf' for example) are our dictionary key.
229 def __init__(self
, vtysh
):
231 self
.contexts
= OrderedDict()
234 def load_from_file(self
, filename
):
236 Read configuration from specified file and slurp it into internal memory
237 The internal representation has been marked appropriately by passing it
238 through vtysh with the -m parameter
240 log
.info("Loading Config object from file %s", filename
)
242 file_output
= self
.vtysh
.mark_file(filename
)
244 for line
in file_output
.split("\n"):
247 # Compress duplicate whitespaces
248 line
= " ".join(line
.split())
251 line
= get_normalized_mac_ip_line(line
)
253 # vrf static routes can be added in two ways. The old way is:
255 # "ip route x.x.x.x/x y.y.y.y vrf <vrfname>"
257 # but it's rendered in the configuration as the new way::
260 # ip route x.x.x.x/x y.y.y.y
263 # this difference causes frr-reload to not consider them a
264 # match and delete vrf static routes incorrectly.
265 # fix the old way to match new "show running" output so a
266 # proper match is found.
268 line
.startswith("ip route ") or line
.startswith("ipv6 route ")
269 ) and " vrf " in line
:
270 newline
= line
.split(" ")
271 vrf_index
= newline
.index("vrf")
272 vrf_ctx
= newline
[vrf_index
] + " " + newline
[vrf_index
+ 1]
273 del newline
[vrf_index
: vrf_index
+ 2]
274 newline
= " ".join(newline
)
275 self
.lines
.append(vrf_ctx
)
276 self
.lines
.append(newline
)
277 self
.lines
.append("exit-vrf")
280 self
.lines
.append(line
)
284 def load_from_show_running(self
, daemon
):
286 Read running configuration and slurp it into internal memory
287 The internal representation has been marked appropriately by passing it
288 through vtysh with the -m parameter
290 log
.info("Loading Config object from vtysh show running")
292 config_text
= self
.vtysh
.mark_show_run(daemon
)
294 for line
in config_text
.split("\n"):
298 line
== "Building configuration..."
299 or line
== "Current configuration:"
304 self
.lines
.append(line
)
310 Return the lines read in from the configuration
312 return "\n".join(self
.lines
)
314 def get_contexts(self
):
316 Return the parsed context as strings for display, log etc.
318 for (_
, ctx
) in sorted(iteritems(self
.contexts
)):
321 def save_contexts(self
, key
, lines
):
323 Save the provided key and lines as a context
328 # IP addresses specified in "network" statements, "ip prefix-lists"
329 # etc. can differ in the host part of the specification the user
330 # provides and what the running config displays. For example, user can
331 # specify 11.1.1.1/24, and the running config displays this as
332 # 11.1.1.0/24. Ensure we don't do a needless operation for such lines.
333 # IS-IS & OSPFv3 have no "network" support.
334 re_key_rt
= re
.match(r
"(ip|ipv6)\s+route\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0])
336 addr
= re_key_rt
.group(2)
339 newaddr
= ip_network(addr
, strict
=False)
340 key
[0] = "%s route %s/%s%s" % (
342 str(newaddr
.network_address
),
349 re_key_rt
= re
.match(
350 r
"(ip|ipv6)\s+prefix-list(.*)(permit|deny)\s+([A-Fa-f:.0-9/]+)(.*)$", key
[0]
353 addr
= re_key_rt
.group(4)
356 network_addr
= ip_network(addr
, strict
=False)
357 newaddr
= "%s/%s" % (
358 str(network_addr
.network_address
),
359 network_addr
.prefixlen
,
366 legestr
= re_key_rt
.group(5)
367 re_lege
= re
.search(r
"(.*)le\s+(\d+)\s+ge\s+(\d+)(.*)", legestr
)
369 legestr
= "%sge %s le %s%s" % (
376 key
[0] = "%s prefix-list%s%s %s%s" % (
384 if lines
and key
[0].startswith("router bgp"):
387 re_net
= re
.match(r
"network\s+([A-Fa-f:.0-9/]+)(.*)$", line
)
389 addr
= re_net
.group(1)
390 if "/" not in addr
and key
[0].startswith("router bgp"):
391 # This is most likely an error because with no
392 # prefixlen, BGP treats the prefixlen as 8
396 network_addr
= ip_network(addr
, strict
=False)
397 line
= "network %s/%s %s" % (
398 str(network_addr
.network_address
),
399 network_addr
.prefixlen
,
402 newlines
.append(line
)
404 # Really this should be an error. Whats a network
405 # without an IP Address following it ?
406 newlines
.append(line
)
408 newlines
.append(line
)
411 # More fixups in user specification and what running config shows.
412 # "null0" in routes must be replaced by Null0.
414 key
[0].startswith("ip route")
415 or key
[0].startswith("ipv6 route")
416 and "null0" in key
[0]
418 key
[0] = re
.sub(r
"\s+null0(\s*$)", " Null0", key
[0])
420 if lines
and key
[0].startswith("vrf "):
423 if line
.startswith("ip route ") or line
.startswith("ipv6 route "):
425 line
= re
.sub(r
"\s+null0(\s*$)", " Null0", line
)
426 newlines
.append(line
)
428 newlines
.append(line
)
432 if tuple(key
) not in self
.contexts
:
433 ctx
= Context(tuple(key
), lines
)
434 self
.contexts
[tuple(key
)] = ctx
436 ctx
= self
.contexts
[tuple(key
)]
440 if tuple(key
) not in self
.contexts
:
441 ctx
= Context(tuple(key
), [])
442 self
.contexts
[tuple(key
)] = ctx
444 def load_contexts(self
):
446 Parse the configuration and create contexts for each appropriate block
448 The end of a context is flagged via the 'end' keyword:
457 bgp router-id 10.0.0.1
458 bgp log-neighbor-changes
459 no bgp default ipv4-unicast
460 neighbor EBGP peer-group
461 neighbor EBGP advertisement-interval 1
462 neighbor EBGP timers connect 10
463 neighbor 2001:40:1:4::6 remote-as 40
464 neighbor 2001:40:1:8::a remote-as 40
468 neighbor IBGPv6 activate
469 neighbor 2001:10::2 peer-group IBGPv6
470 neighbor 2001:10::3 peer-group IBGPv6
475 ospf router-id 10.0.0.1
476 log-adjacency-changes detail
477 timers throttle spf 0 50 5000
481 The code assumes that its working on the output from the "vtysh -m"
482 command. That provides the appropriate markers to signify end of
483 a context. This routine uses that to build the contexts for the
486 There are single line contexts such as "log file /media/node/zebra.log"
487 and multi-line contexts such as "router ospf" and subcontexts
488 within a context such as "address-family" within "router bgp"
489 In each of these cases, the first line of the context becomes the
490 key of the context. So "router bgp 10" is the key for the non-address
491 family part of bgp, "router bgp 10, address-family ipv6 unicast" is
492 the key for the subcontext and so on.
494 This dictionary contains a tree of all commands that we know start a
495 new multi-line context. All other commands are treated either as
496 commands inside a multi-line context or as single-line contexts. This
497 dictionary should be updated whenever a new node is added to FRR.
505 "vnc nve-group ": {},
509 "segment-routing srv6": {},
514 "router openfabric ": {},
519 "mpls ldp": {"address-family ": {"interface ": {}}},
520 "l2vpn ": {"member pseudowire ": {}},
521 "key chain ": {"key ": {}},
523 "interface ": {"link-params": {}},
528 "policy ": {"candidate-path ": {}},
529 "pcep": {"pcc": {}, "pce ": {}, "pce-config ": {}},
531 "srv6": {"locators": {"locator ": {}}},
533 "nexthop-group ": {},
537 "bfd": {"peer ": {}, "profile ": {}},
541 # stack of context keys
543 # stack of context keywords
544 cur_ctx_keywords
= [ctx_keywords
]
545 # list of stored commands
548 for line
in self
.lines
:
553 if line
.startswith("!") or line
.startswith("#"):
556 if line
.startswith("exit"):
557 # ignore on top level
558 if len(ctx_keys
) == 0:
561 # save current context
562 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
564 # exit current context
565 log
.debug("LINE %-50s: exit context %-50s", line
, ctx_keys
)
568 cur_ctx_keywords
.pop()
573 if line
.startswith("end"):
575 while len(ctx_keys
) > 0:
576 # save current context
577 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
579 # exit current context
580 log
.debug("LINE %-50s: exit context %-50s", line
, ctx_keys
)
583 cur_ctx_keywords
.pop()
590 # check if the line is a context-entering keyword
591 for k
, v
in cur_ctx_keywords
[-1].items():
592 if line
.startswith(k
):
593 # candidate-path is a special case. It may be a node and
594 # may be a single-line command. The distinguisher is the
595 # word "dynamic" or "explicit" at the middle of the line.
596 # It was perhaps not the best choice by the pathd authors
597 # but we have what we have.
598 if k
== "candidate-path " and "explicit" in line
:
599 # this is a single-line command
602 # save current context
603 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
607 ctx_keys
.append(line
)
608 cur_ctx_keywords
.append(v
)
611 log
.debug("LINE %-50s: enter context %-50s", line
, ctx_keys
)
617 if len(ctx_keys
) == 0:
618 log
.debug("LINE %-50s: single-line context", line
)
619 self
.save_contexts([line
], [])
621 log
.debug("LINE %-50s: add to current context %-50s", line
, ctx_keys
)
622 cur_ctx_lines
.append(line
)
624 # Save the context of the last one
625 if len(ctx_keys
) > 0:
626 self
.save_contexts(ctx_keys
, cur_ctx_lines
)
629 def lines_to_config(ctx_keys
, line
, delete
):
631 Return the command as it would appear in frr.conf
636 for (i
, ctx_key
) in enumerate(ctx_keys
):
637 cmd
.append(" " * i
+ ctx_key
)
640 indent
= len(ctx_keys
) * " "
642 # There are some commands that are on by default so their "no" form will be
643 # displayed in the config. "no bgp default ipv4-unicast" is one of these.
644 # If we need to remove this line we do so by adding "bgp default ipv4-unicast",
645 # not by doing a "no no bgp default ipv4-unicast"
647 if line
.startswith("no "):
648 cmd
.append("%s%s" % (indent
, line
[3:]))
650 cmd
.append("%sno %s" % (indent
, line
))
653 cmd
.append(indent
+ line
)
655 # If line is None then we are typically deleting an entire
656 # context ('no router ospf' for example)
658 for i
, ctx_key
in enumerate(ctx_keys
[:-1]):
659 cmd
.append("%s%s" % (" " * i
, ctx_key
))
661 # Only put the 'no' on the last sub-context
663 if ctx_keys
[-1].startswith("no "):
664 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1][3:]))
666 cmd
.append("%sno %s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
668 cmd
.append("%s%s" % (" " * (len(ctx_keys
) - 1), ctx_keys
[-1]))
673 def get_normalized_ipv6_line(line
):
675 Return a normalized IPv6 line as produced by frr,
676 with all letters in lower case and trailing and leading
677 zeros removed, and only the network portion present if
678 the IPv6 word is a network
681 words
= line
.split(" ")
687 v6word
= ip_network(word
, strict
=False)
688 norm_word
= "%s/%s" % (
689 str(v6word
.network_address
),
696 norm_word
= "%s" % IPv6Address(word
)
701 norm_line
= norm_line
+ " " + norm_word
703 return norm_line
.strip()
706 def line_exist(lines
, target_ctx_keys
, target_line
, exact_match
=True):
707 for (ctx_keys
, line
) in lines
:
708 if ctx_keys
== target_ctx_keys
:
710 if line
== target_line
:
713 if line
.startswith(target_line
):
718 def check_for_exit_vrf(lines_to_add
, lines_to_del
):
720 # exit-vrf is a bit tricky. If the new config is missing it but we
721 # have configs under a vrf, we need to add it at the end to do the
722 # right context changes. If exit-vrf exists in both the running and
723 # new config, we cannot delete it or it will break context changes.
727 for (ctx_keys
, line
) in lines_to_add
:
728 if add_exit_vrf
== True:
729 if ctx_keys
[0] != prior_ctx_key
:
730 insert_key
= ((prior_ctx_key
),)
731 lines_to_add
.insert(index
, ((insert_key
, "exit-vrf")))
734 if ctx_keys
[0].startswith("vrf") and line
:
735 if line
!= "exit-vrf":
737 prior_ctx_key
= ctx_keys
[0]
742 for (ctx_keys
, line
) in lines_to_del
:
743 if line
== "exit-vrf":
744 if line_exist(lines_to_add
, ctx_keys
, line
):
745 lines_to_del
.remove((ctx_keys
, line
))
747 return (lines_to_add
, lines_to_del
)
750 def bgp_delete_inst_move_line(lines_to_del
):
751 # Deletion of bgp default inst followed by
752 # bgp vrf inst leads to issue of default
753 # instance can not be removed.
754 # Move the bgp default instance line to end.
755 bgp_defult_inst
= False
758 for (ctx_keys
, line
) in lines_to_del
:
759 # Find bgp default inst
761 ctx_keys
[0].startswith("router bgp")
763 and "vrf" not in ctx_keys
[0]
765 bgp_defult_inst
= True
767 if ctx_keys
[0].startswith("router bgp") and not line
and "vrf" in ctx_keys
[0]:
770 if bgp_defult_inst
and bgp_vrf_inst
:
771 for (ctx_keys
, line
) in lines_to_del
:
772 # move bgp default inst to end
774 ctx_keys
[0].startswith("router bgp")
776 and "vrf" not in ctx_keys
[0]
778 lines_to_del
.remove((ctx_keys
, line
))
779 lines_to_del
.append((ctx_keys
, line
))
782 def bgp_delete_nbr_remote_as_line(lines_to_add
):
783 # Handle deletion of neighbor <nbr> remote-as line from
784 # lines_to_add if the nbr is configured with peer-group and
785 # peer-group has remote-as config present.
786 # 'neighbor <nbr> remote-as change on peer is not allowed
787 # if the peer is part of peer-group and peer-group has
791 # Find all peer-group commands; create dict of each peer-group
792 # to store assoicated neighbor as value
793 for ctx_keys
, line
in lines_to_add
:
795 ctx_keys
[0].startswith("router bgp")
797 and line
.startswith("neighbor ")
799 # {'router bgp 65001': {'PG': [], 'PG1': []},
800 # 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
801 if ctx_keys
[0] not in pg_dict
:
802 pg_dict
[ctx_keys
[0]] = dict()
803 # find 'neighbor <pg_name> peer-group'
804 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
805 if re_pg
and re_pg
.group(1) not in pg_dict
[ctx_keys
[0]]:
806 pg_dict
[ctx_keys
[0]][re_pg
.group(1)] = {
812 # Find peer-group with remote-as command, also search neighbor
813 # associated to peer-group and store into peer-group dict
814 for ctx_keys
, line
in lines_to_add
:
816 ctx_keys
[0].startswith("router bgp")
818 and line
.startswith("neighbor ")
820 if ctx_keys
[0] in pg_dict
:
821 for pg_key
in pg_dict
[ctx_keys
[0]]:
822 # Find 'neighbor <pg_name> remote-as'
823 pg_rmtas
= "neighbor %s remote-as (\S+)" % pg_key
824 re_pg_rmtas
= re
.search(pg_rmtas
, line
)
826 pg_dict
[ctx_keys
[0]][pg_key
]["remoteas"] = True
828 # Find 'neighbor <peer> [interface] peer-group <pg_name>'
829 nb_pg
= "neighbor (\S+) peer-group %s$" % pg_key
830 re_nbr_pg
= re
.search(nb_pg
, line
)
833 and re_nbr_pg
.group(1) not in pg_dict
[ctx_keys
[0]][pg_key
]
835 pg_dict
[ctx_keys
[0]][pg_key
]["nbr"].append(re_nbr_pg
.group(1))
837 # Find any neighbor <nbr> remote-as config line check if the nbr
838 # is in the peer group's list of nbrs. Remove 'neighbor <nbr> remote-as <>'
840 lines_to_del_from_add
= []
841 for ctx_keys
, line
in lines_to_add
:
843 ctx_keys
[0].startswith("router bgp")
845 and line
.startswith("neighbor ")
847 nbr_rmtas
= "neighbor (\S+) remote-as.*"
848 re_nbr_rmtas
= re
.search(nbr_rmtas
, line
)
849 if re_nbr_rmtas
and ctx_keys
[0] in pg_dict
:
850 for pg
in pg_dict
[ctx_keys
[0]]:
851 if pg_dict
[ctx_keys
[0]][pg
]["remoteas"] == True:
852 for nbr
in pg_dict
[ctx_keys
[0]][pg
]["nbr"]:
853 if re_nbr_rmtas
.group(1) in nbr
:
854 lines_to_del_from_add
.append((ctx_keys
, line
))
856 for ctx_keys
, line
in lines_to_del_from_add
:
857 lines_to_add
.remove((ctx_keys
, line
))
860 def bgp_remove_neighbor_cfg(lines_to_del
, del_nbr_dict
):
862 # This method handles deletion of bgp neighbor configs,
863 # if there is neighbor to peer-group cmd is in delete list.
864 # As 'no neighbor .* peer-group' deletes the neighbor,
865 # subsequent neighbor speciic config line deletion results
867 lines_to_del_to_del
= []
869 for (ctx_keys
, line
) in lines_to_del
:
871 ctx_keys
[0].startswith("router bgp")
873 and line
.startswith("neighbor ")
875 if ctx_keys
[0] in del_nbr_dict
:
876 for nbr
in del_nbr_dict
[ctx_keys
[0]]:
877 re_nbr_pg
= re
.search("neighbor (\S+) .*peer-group (\S+)", line
)
878 nb_exp
= "neighbor %s .*" % nbr
880 re_nb
= re
.search(nb_exp
, line
)
882 lines_to_del_to_del
.append((ctx_keys
, line
))
884 for (ctx_keys
, line
) in lines_to_del_to_del
:
885 lines_to_del
.remove((ctx_keys
, line
))
888 def delete_move_lines(lines_to_add
, lines_to_del
):
889 # This method handles deletion of bgp peer group config.
890 # The objective is to delete config lines related to peers
891 # associated with the peer-group and move the peer-group
892 # config line to the end of the lines_to_del list.
894 bgp_delete_nbr_remote_as_line(lines_to_add
)
897 del_nbr_dict
= dict()
898 # Stores the lines to move to the end of the pending list.
899 lines_to_del_to_del
= []
900 # Stores the lines to move to end of the pending list.
901 lines_to_del_to_app
= []
902 found_pg_del_cmd
= False
904 # When "neighbor <pg_name> peer-group" under a bgp instance is removed,
905 # it also deletes the associated peer config. Any config line below no form of
906 # peer-group related to a peer are errored out as the peer no longer exists.
907 # To cleanup peer-group and associated peer(s) configs:
908 # - Remove all the peers config lines from the pending list (lines_to_del list).
909 # - Move peer-group deletion line to the end of the pending list, to allow
910 # removal of any of the peer-group specific configs.
912 # Create a dictionary of config context (i.e. router bgp vrf x).
913 # Under each context node, create a dictionary of a peer-group name.
914 # Append a peer associated to the peer-group into a list under a peer-group node.
915 # Remove all of the peer associated config lines from the pending list.
916 # Append peer-group deletion line to end of the pending list.
919 # neighbor underlay peer-group
920 # neighbor underlay remote-as external
921 # neighbor underlay advertisement-interval 0
922 # neighbor underlay timers 3 9
923 # neighbor underlay timers connect 10
924 # neighbor swp1 interface peer-group underlay
925 # neighbor swp1 advertisement-interval 0
926 # neighbor swp1 timers 3 9
927 # neighbor swp1 timers connect 10
928 # neighbor swp2 interface peer-group underlay
929 # neighbor swp2 advertisement-interval 0
930 # neighbor swp2 timers 3 9
931 # neighbor swp2 timers connect 10
932 # neighbor swp3 interface peer-group underlay
933 # neighbor uplink1 interface remote-as internal
934 # neighbor uplink1 advertisement-interval 0
935 # neighbor uplink1 timers 3 9
936 # neighbor uplink1 timers connect 10
939 # "router bgp 200 no bgp bestpath as-path multipath-relax"
940 # "router bgp 200 no neighbor underlay advertisement-interval 0"
941 # "router bgp 200 no neighbor underlay timers 3 9"
942 # "router bgp 200 no neighbor underlay timers connect 10"
943 # "router bgp 200 no neighbor uplink1 advertisement-interval 0"
944 # "router bgp 200 no neighbor uplink1 timers 3 9"
945 # "router bgp 200 no neighbor uplink1 timers connect 10"
946 # "router bgp 200 no neighbor underlay remote-as external"
947 # "router bgp 200 no neighbor uplink1 interface remote-as internal"
948 # "router bgp 200 no neighbor underlay peer-group"
950 for (ctx_keys
, line
) in lines_to_del
:
952 ctx_keys
[0].startswith("router bgp")
954 and line
.startswith("neighbor ")
956 # When 'neighbor <peer> remote-as <>' is removed it deletes the peer,
957 # there might be a peer associated config which also needs to be removed
959 # Append the 'neighbor <peer> remote-as <>' to the lines_to_del.
962 # neighbor uplink1 interface remote-as internal
963 # neighbor uplink1 advertisement-interval 0
964 # neighbor uplink1 timers 3 9
965 # neighbor uplink1 timers connect 10
968 # neighbor uplink1 advertisement-interval 0
969 # neighbor uplink1 timers 3 9
970 # neighbor uplink1 timers connect 10
973 # neighbor uplink1 interface remote-as internal
975 # 'no neighbor peer [interface] remote-as <>'
976 nb_remoteas
= "neighbor (\S+) .*remote-as (\S+)"
977 re_nb_remoteas
= re
.search(nb_remoteas
, line
)
979 lines_to_del_to_app
.append((ctx_keys
, line
))
981 # 'no neighbor peer [interface] peer-group <>' is in lines_to_del
982 # copy the neighbor and look for all config removal lines associated
983 # to neighbor and delete them from the lines_to_del
984 re_nbr_pg
= re
.search("neighbor (\S+) .*peer-group (\S+)", line
)
986 if ctx_keys
[0] not in del_nbr_dict
:
987 del_nbr_dict
[ctx_keys
[0]] = list()
988 if re_nbr_pg
.group(1) not in del_nbr_dict
[ctx_keys
[0]]:
989 del_nbr_dict
[ctx_keys
[0]].append(re_nbr_pg
.group(1))
991 # {'router bgp 65001': {'PG': [], 'PG1': []},
992 # 'router bgp 65001 vrf vrf1': {'PG': [], 'PG1': []}}
993 if ctx_keys
[0] not in del_dict
:
994 del_dict
[ctx_keys
[0]] = dict()
995 # find 'no neighbor <pg_name> peer-group'
996 re_pg
= re
.match("neighbor (\S+) peer-group$", line
)
997 if re_pg
and re_pg
.group(1) not in del_dict
[ctx_keys
[0]]:
998 del_dict
[ctx_keys
[0]][re_pg
.group(1)] = list()
999 found_pg_del_cmd
= True
1001 if found_pg_del_cmd
== False:
1002 bgp_delete_inst_move_line(lines_to_del
)
1004 bgp_remove_neighbor_cfg(lines_to_del
, del_nbr_dict
)
1005 return (lines_to_add
, lines_to_del
)
1007 for (ctx_keys
, line
) in lines_to_del_to_app
:
1008 lines_to_del
.remove((ctx_keys
, line
))
1009 lines_to_del
.append((ctx_keys
, line
))
1011 # {'router bgp 65001': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']},
1012 # 'router bgp 65001 vrf vrf1': {'PG': ['10.1.1.2'], 'PG1': ['10.1.1.21']}}
1013 for (ctx_keys
, line
) in lines_to_del
:
1015 ctx_keys
[0].startswith("router bgp")
1017 and line
.startswith("neighbor ")
1019 if ctx_keys
[0] in del_dict
:
1020 for pg_key
in del_dict
[ctx_keys
[0]]:
1021 # 'neighbor <peer> [interface] peer-group <pg_name>'
1022 nb_pg
= "neighbor (\S+) .*peer-group %s$" % pg_key
1023 re_nbr_pg
= re
.search(nb_pg
, line
)
1026 and re_nbr_pg
.group(1) not in del_dict
[ctx_keys
[0]][pg_key
]
1028 del_dict
[ctx_keys
[0]][pg_key
].append(re_nbr_pg
.group(1))
1030 lines_to_del_to_app
= []
1031 for (ctx_keys
, line
) in lines_to_del
:
1033 ctx_keys
[0].startswith("router bgp")
1035 and line
.startswith("neighbor ")
1037 if ctx_keys
[0] in del_dict
:
1038 for pg
in del_dict
[ctx_keys
[0]]:
1039 for nbr
in del_dict
[ctx_keys
[0]][pg
]:
1040 nb_exp
= "neighbor %s .*" % nbr
1041 re_nb
= re
.search(nb_exp
, line
)
1042 # add peer configs to delete list.
1043 if re_nb
and line
not in lines_to_del_to_del
:
1044 lines_to_del_to_del
.append((ctx_keys
, line
))
1046 pg_exp
= "neighbor %s peer-group$" % pg
1047 re_pg
= re
.match(pg_exp
, line
)
1049 lines_to_del_to_app
.append((ctx_keys
, line
))
1051 for (ctx_keys
, line
) in lines_to_del_to_del
:
1052 lines_to_del
.remove((ctx_keys
, line
))
1054 for (ctx_keys
, line
) in lines_to_del_to_app
:
1055 lines_to_del
.remove((ctx_keys
, line
))
1056 lines_to_del
.append((ctx_keys
, line
))
1058 bgp_delete_inst_move_line(lines_to_del
)
1060 return (lines_to_add
, lines_to_del
)
1063 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
1065 # Quite possibly the most confusing (while accurate) variable names in history
1066 lines_to_add_to_del
= []
1067 lines_to_del_to_del
= []
1069 for (ctx_keys
, line
) in lines_to_del
:
1072 # If there is a change in the segment routing block ranges, do it
1073 # in-place, to avoid requesting spurious label chunks which might fail
1074 if line
and "segment-routing global-block" in line
:
1075 for (add_key
, add_line
) in lines_to_add
:
1077 ctx_keys
[0] == add_key
[0]
1079 and "segment-routing global-block" in add_line
1081 lines_to_del_to_del
.append((ctx_keys
, line
))
1085 if ctx_keys
[0].startswith("router bgp") and line
:
1087 if line
.startswith("neighbor "):
1088 # BGP changed how it displays swpX peers that are part of peer-group. Older
1089 # versions of frr would display these on separate lines:
1090 # neighbor swp1 interface
1091 # neighbor swp1 peer-group FOO
1093 # but today we display via a single line
1094 # neighbor swp1 interface peer-group FOO
1096 # This change confuses frr-reload.py so check to see if we are deleting
1097 # neighbor swp1 interface peer-group FOO
1100 # neighbor swp1 interface
1101 # neighbor swp1 peer-group FOO
1103 # If so then chop the del line and the corresponding add lines
1104 re_swpx_int_peergroup
= re
.search(
1105 "neighbor (\S+) interface peer-group (\S+)", line
1107 re_swpx_int_v6only_peergroup
= re
.search(
1108 "neighbor (\S+) interface v6only peer-group (\S+)", line
1111 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
1112 swpx_interface
= None
1113 swpx_peergroup
= None
1115 if re_swpx_int_peergroup
:
1116 swpx
= re_swpx_int_peergroup
.group(1)
1117 peergroup
= re_swpx_int_peergroup
.group(2)
1118 swpx_interface
= "neighbor %s interface" % swpx
1119 elif re_swpx_int_v6only_peergroup
:
1120 swpx
= re_swpx_int_v6only_peergroup
.group(1)
1121 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
1122 swpx_interface
= "neighbor %s interface v6only" % swpx
1124 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
1125 found_add_swpx_interface
= line_exist(
1126 lines_to_add
, ctx_keys
, swpx_interface
1128 found_add_swpx_peergroup
= line_exist(
1129 lines_to_add
, ctx_keys
, swpx_peergroup
1131 tmp_ctx_keys
= tuple(list(ctx_keys
))
1133 if not found_add_swpx_peergroup
:
1134 tmp_ctx_keys
= list(ctx_keys
)
1135 tmp_ctx_keys
.append("address-family ipv4 unicast")
1136 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1137 found_add_swpx_peergroup
= line_exist(
1138 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1141 if not found_add_swpx_peergroup
:
1142 tmp_ctx_keys
= list(ctx_keys
)
1143 tmp_ctx_keys
.append("address-family ipv6 unicast")
1144 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1145 found_add_swpx_peergroup
= line_exist(
1146 lines_to_add
, tmp_ctx_keys
, swpx_peergroup
1149 if found_add_swpx_interface
and found_add_swpx_peergroup
:
1151 lines_to_del_to_del
.append((ctx_keys
, line
))
1152 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1153 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
1155 # Changing the bfd timers on neighbors is allowed without doing
1156 # a delete/add process. Since doing a "no neighbor blah bfd
1157 # ..." will cause the peer to bounce unnecessarily, just skip
1158 # the delete and just do the add.
1159 re_nbr_bfd_timers
= re
.search(
1160 r
"neighbor (\S+) bfd (\S+) (\S+) (\S+)", line
1163 if re_nbr_bfd_timers
:
1164 nbr
= re_nbr_bfd_timers
.group(1)
1165 bfd_nbr
= "neighbor %s" % nbr
1166 bfd_search_string
= bfd_nbr
+ r
" bfd (\S+) (\S+) (\S+)"
1168 for (ctx_keys
, add_line
) in lines_to_add
:
1169 if ctx_keys
[0].startswith("router bgp"):
1170 re_add_nbr_bfd_timers
= re
.search(
1171 bfd_search_string
, add_line
1174 if re_add_nbr_bfd_timers
:
1175 found_add_bfd_nbr
= line_exist(
1176 lines_to_add
, ctx_keys
, bfd_nbr
, False
1179 if found_add_bfd_nbr
:
1180 lines_to_del_to_del
.append((ctx_keys
, line
))
1182 # Neighbor changes of route-maps need to be accounted for in
1183 # that we do not want to do a `no route-map...` `route-map
1184 # ....` when changing a route-map. This is bad mojo as that we
1185 # will send/receive data we don't want. Additionally we need
1186 # to ensure that if we have different afi/safi variants that
1187 # they actually match and if we are going from a very old style
1188 # command such that the neighbor command is under the `router
1189 # bgp ..` node that we need to handle that appropriately
1190 re_nbr_rm
= re
.search("neighbor(.*)route-map(.*)(in|out)$", line
)
1192 adjust_for_bgp_node
= 0
1193 neighbor_name
= re_nbr_rm
.group(1)
1194 rm_name_del
= re_nbr_rm
.group(2)
1195 dir = re_nbr_rm
.group(3)
1196 search
= "neighbor%sroute-map(.*)%s" % (neighbor_name
, dir)
1198 for (ctx_keys_al
, add_line
) in lines_to_add
:
1199 if ctx_keys_al
[0].startswith("router bgp"):
1201 rm_match
= re
.search(search
, add_line
)
1203 rm_name_add
= rm_match
.group(1)
1204 if rm_name_add
== rm_name_del
:
1206 if len(ctx_keys_al
) == 1:
1208 adjust_for_bgp_node
= 1
1212 and len(ctx_keys_al
) > 1
1213 and ctx_keys
[1] == ctx_keys_al
[1]
1215 lines_to_del_to_del
.append((ctx_keys_al
, line
))
1217 if adjust_for_bgp_node
== 1:
1218 for (ctx_keys_dl
, dl_line
) in lines_to_del
:
1220 ctx_keys_dl
[0].startswith("router bgp")
1221 and len(ctx_keys_dl
) > 1
1222 and ctx_keys_dl
[1] == "address-family ipv4 unicast"
1224 if save_line
== dl_line
:
1225 lines_to_del_to_del
.append((ctx_keys_dl
, save_line
))
1227 # We changed how we display the neighbor interface command. Older
1228 # versions of frr would display the following:
1229 # neighbor swp1 interface
1230 # neighbor swp1 remote-as external
1231 # neighbor swp1 capability extended-nexthop
1233 # but today we display via a single line
1234 # neighbor swp1 interface remote-as external
1236 # and capability extended-nexthop is no longer needed because we
1237 # automatically enable it when the neighbor is of type interface.
1239 # This change confuses frr-reload.py so check to see if we are deleting
1240 # neighbor swp1 interface remote-as (external|internal|ASNUM)
1243 # neighbor swp1 interface
1244 # neighbor swp1 remote-as (external|internal|ASNUM)
1245 # neighbor swp1 capability extended-nexthop
1247 # If so then chop the del line and the corresponding add lines
1248 re_swpx_int_remoteas
= re
.search(
1249 "neighbor (\S+) interface remote-as (\S+)", line
1251 re_swpx_int_v6only_remoteas
= re
.search(
1252 "neighbor (\S+) interface v6only remote-as (\S+)", line
1255 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
1256 swpx_interface
= None
1257 swpx_remoteas
= None
1259 if re_swpx_int_remoteas
:
1260 swpx
= re_swpx_int_remoteas
.group(1)
1261 remoteas
= re_swpx_int_remoteas
.group(2)
1262 swpx_interface
= "neighbor %s interface" % swpx
1263 elif re_swpx_int_v6only_remoteas
:
1264 swpx
= re_swpx_int_v6only_remoteas
.group(1)
1265 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
1266 swpx_interface
= "neighbor %s interface v6only" % swpx
1268 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
1269 found_add_swpx_interface
= line_exist(
1270 lines_to_add
, ctx_keys
, swpx_interface
1272 found_add_swpx_remoteas
= line_exist(
1273 lines_to_add
, ctx_keys
, swpx_remoteas
1275 tmp_ctx_keys
= tuple(list(ctx_keys
))
1277 if found_add_swpx_interface
and found_add_swpx_remoteas
:
1279 lines_to_del_to_del
.append((ctx_keys
, line
))
1280 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
1281 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
1283 # We made the 'bgp bestpath as-path multipath-relax' command
1284 # automatically assume 'no-as-set' since the lack of this option
1285 # caused weird routing problems. When the running config is shown
1286 # in releases with this change, the no-as-set keyword is not shown
1287 # as it is the default. This causes frr-reload to unnecessarily
1288 # unapply this option only to apply it back again, causing
1289 # unnecessary session resets.
1290 if "multipath-relax" in line
:
1291 re_asrelax_new
= re
.search(
1292 "^bgp\s+bestpath\s+as-path\s+multipath-relax$", line
1294 old_asrelax_cmd
= "bgp bestpath as-path multipath-relax no-as-set"
1295 found_asrelax_old
= line_exist(lines_to_add
, ctx_keys
, old_asrelax_cmd
)
1297 if re_asrelax_new
and found_asrelax_old
:
1299 lines_to_del_to_del
.append((ctx_keys
, line
))
1300 lines_to_add_to_del
.append((ctx_keys
, old_asrelax_cmd
))
1302 # If we are modifying the BGP table-map we need to avoid a del/add
1303 # and instead modify the table-map in place via an add. This is
1304 # needed to avoid installing all routes in the RIB the second the
1305 # 'no table-map' is issued.
1306 if line
.startswith("table-map"):
1307 found_table_map
= line_exist(lines_to_add
, ctx_keys
, "table-map", False)
1310 lines_to_del_to_del
.append((ctx_keys
, line
))
1312 # More old-to-new config handling. ip import-table no longer accepts
1313 # distance, but we honor the old syntax. But 'show running' shows only
1314 # the new syntax. This causes an unnecessary 'no import-table' followed
1315 # by the same old 'ip import-table' which causes perturbations in
1316 # announced routes leading to traffic blackholes. Fix this issue.
1317 re_importtbl
= re
.search("^ip\s+import-table\s+(\d+)$", ctx_keys
[0])
1319 table_num
= re_importtbl
.group(1)
1320 for ctx
in lines_to_add
:
1321 if ctx
[0][0].startswith("ip import-table %s distance" % table_num
):
1322 lines_to_del_to_del
.append(
1323 (("ip import-table %s" % table_num
,), None)
1325 lines_to_add_to_del
.append((ctx
[0], None))
1327 # ip/ipv6 prefix-lists and access-lists can be specified without a seq
1328 # number. However, the running config always adds 'seq x', where x is
1329 # a number incremented by 5 for every element of the prefix/access
1330 # list. So, ignore such lines as well. Sample prefix-list and
1332 # ip prefix-list PR-TABLE-2 seq 5 permit 20.8.2.0/24 le 32
1333 # ip prefix-list PR-TABLE-2 seq 10 permit 20.8.2.0/24 le 32
1334 # ipv6 prefix-list vrfdev6-12 permit 2000:9:2::/64 gt 64
1335 # access-list FOO seq 5 permit 2.2.2.2/32
1336 # ipv6 access-list BAR seq 5 permit 2:2:2::2/128
1337 re_acl_pfxlst
= re
.search(
1338 "^(ip |ipv6 |)(prefix-list|access-list)(\s+\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1344 re_acl_pfxlst
.group(1)
1345 + re_acl_pfxlst
.group(2)
1346 + re_acl_pfxlst
.group(3)
1347 + re_acl_pfxlst
.group(5)
1348 + re_acl_pfxlst
.group(6)
1350 for ctx
in lines_to_add
:
1351 if ctx
[0][0] == tmpline
:
1352 lines_to_del_to_del
.append((ctx_keys
, None))
1353 lines_to_add_to_del
.append(((tmpline
,), None))
1355 # If prefix-lists or access-lists are being deleted and not added
1356 # (see comment above), add command with 'no' to lines_to_add and
1357 # remove from lines_to_del to improve scaling performance.
1359 add_cmd
= ("no " + ctx_keys
[0],)
1360 lines_to_add
.append((add_cmd
, None))
1361 lines_to_del_to_del
.append((ctx_keys
, None))
1363 # bgp community-list, large-community-list, extcommunity-list can be
1364 # specified without a seq number. However, the running config always
1365 # adds `seq X` (sequence number). So, ignore such lines as well.
1367 # bgp community-list standard clist seq 5 permit 222:213
1368 # bgp large-community-list standard llist seq 5 permit 65001:65001:1
1369 # bgp extcommunity-list standard elist seq 5 permit soo 123:123
1370 re_bgp_lists
= re
.search(
1371 "^(bgp )(community-list|large-community-list|extcommunity-list)(\s+\S+\s+)(\S+\s+)(seq \d+\s+)(permit|deny)(.*)$",
1377 re_bgp_lists
.group(1)
1378 + re_bgp_lists
.group(2)
1379 + re_bgp_lists
.group(3)
1380 + re_bgp_lists
.group(4)
1381 + re_bgp_lists
.group(6)
1382 + re_bgp_lists
.group(7)
1384 for ctx
in lines_to_add
:
1385 if ctx
[0][0] == tmpline
:
1386 lines_to_del_to_del
.append((ctx_keys
, None))
1387 lines_to_add_to_del
.append(((tmpline
,), None))
1390 add_cmd
= ("no " + ctx_keys
[0],)
1391 lines_to_add
.append((add_cmd
, None))
1392 lines_to_del_to_del
.append((ctx_keys
, None))
1396 and ctx_keys
[0].startswith("router bgp")
1397 and ctx_keys
[1] == "address-family l2vpn evpn"
1398 and ctx_keys
[2].startswith("vni")
1402 re
.search("^route-target import (.*)$", line
)
1408 rt
= re_route_target
.group(1).strip()
1409 route_target_import_line
= line
1410 route_target_export_line
= "route-target export %s" % rt
1411 route_target_both_line
= "route-target both %s" % rt
1413 found_route_target_export_line
= line_exist(
1414 lines_to_del
, ctx_keys
, route_target_export_line
1416 found_route_target_both_line
= line_exist(
1417 lines_to_add
, ctx_keys
, route_target_both_line
1420 # If the running configs has
1421 # route-target import 1:1
1422 # route-target export 1:1
1423 # and the config we are reloading against has
1424 # route-target both 1:1
1425 # then we can ignore deleting the import/export and ignore adding the 'both'
1426 if found_route_target_export_line
and found_route_target_both_line
:
1427 lines_to_del_to_del
.append((ctx_keys
, route_target_import_line
))
1428 lines_to_del_to_del
.append((ctx_keys
, route_target_export_line
))
1429 lines_to_add_to_del
.append((ctx_keys
, route_target_both_line
))
1431 # Deleting static routes under a vrf can lead to time-outs if each is sent
1432 # as separate vtysh -c commands. Change them from being in lines_to_del and
1433 # put the "no" form in lines_to_add
1434 if ctx_keys
[0].startswith("vrf ") and line
:
1435 if line
.startswith("ip route") or line
.startswith("ipv6 route"):
1436 add_cmd
= "no " + line
1437 lines_to_add
.append((ctx_keys
, add_cmd
))
1438 lines_to_del_to_del
.append((ctx_keys
, line
))
1441 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
1444 lines_to_del_to_del
.append((ctx_keys
, line
))
1445 lines_to_add_to_del
.append((ctx_keys
, line
))
1447 # We have commands that used to be displayed in the global part
1448 # of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
1452 # neighbor ISL advertisement-interval 0
1458 # address-family ipv4 unicast
1459 # neighbor ISL advertisement-interval 0
1461 # Look to see if we are deleting it in one format just to add it back in the other
1463 ctx_keys
[0].startswith("router bgp")
1464 and len(ctx_keys
) > 1
1465 and ctx_keys
[1] == "address-family ipv4 unicast"
1467 tmp_ctx_keys
= list(ctx_keys
)[:-1]
1468 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
1470 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
1473 lines_to_del_to_del
.append((ctx_keys
, line
))
1474 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
1476 for (ctx_keys
, line
) in lines_to_del_to_del
:
1477 if line
is not None:
1478 lines_to_del
.remove((ctx_keys
, line
))
1480 for (ctx_keys
, line
) in lines_to_add_to_del
:
1481 if line
is not None:
1482 lines_to_add
.remove((ctx_keys
, line
))
1484 return (lines_to_add
, lines_to_del
)
1487 def ignore_unconfigurable_lines(lines_to_add
, lines_to_del
):
1489 There are certain commands that cannot be removed. Remove
1490 those commands from lines_to_del.
1492 lines_to_del_to_del
= []
1494 for (ctx_keys
, line
) in lines_to_del
:
1496 # The integrated-vtysh-config one is technically "no"able but if we did
1497 # so frr-reload would stop working so do not let the user shoot
1498 # themselves in the foot by removing this.
1501 ctx_keys
[0].startswith(x
)
1509 "service integrated-vtysh-config",
1513 log
.info('"%s" cannot be removed' % (ctx_keys
[-1],))
1514 lines_to_del_to_del
.append((ctx_keys
, line
))
1516 for (ctx_keys
, line
) in lines_to_del_to_del
:
1517 lines_to_del
.remove((ctx_keys
, line
))
1519 return (lines_to_add
, lines_to_del
)
1522 def compare_context_objects(newconf
, running
):
1524 Create a context diff for the two specified contexts
1527 # Compare the two Config objects to find the lines that we need to add/del
1534 candidates_to_add
= []
1537 # Find contexts that are in newconf but not in running
1538 # Find contexts that are in running but not in newconf
1539 for (running_ctx_keys
, running_ctx
) in iteritems(running
.contexts
):
1541 if running_ctx_keys
not in newconf
.contexts
:
1543 # We check that the len is 1 here so that we only look at ('router bgp 10')
1544 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
1545 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
1546 # running but not in newconf.
1547 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
1549 lines_to_del
.append((running_ctx_keys
, None))
1551 # We cannot do 'no interface' or 'no vrf' in FRR, and so deal with it
1552 elif running_ctx_keys
[0].startswith("interface") or running_ctx_keys
[
1554 ].startswith("vrf"):
1555 for line
in running_ctx
.lines
:
1556 lines_to_del
.append((running_ctx_keys
, line
))
1558 # If this is an address-family under 'router bgp' and we are already deleting the
1559 # entire 'router bgp' context then ignore this sub-context
1561 "router bgp" in running_ctx_keys
[0]
1562 and len(running_ctx_keys
) > 1
1567 # Delete an entire vni sub-context under "address-family l2vpn evpn"
1569 "router bgp" in running_ctx_keys
[0]
1570 and len(running_ctx_keys
) > 2
1571 and running_ctx_keys
[1].startswith("address-family l2vpn evpn")
1572 and running_ctx_keys
[2].startswith("vni ")
1574 lines_to_del
.append((running_ctx_keys
, None))
1577 "router bgp" in running_ctx_keys
[0]
1578 and len(running_ctx_keys
) > 1
1579 and running_ctx_keys
[1].startswith("address-family")
1581 # There's no 'no address-family' support and so we have to
1582 # delete each line individually again
1583 for line
in running_ctx
.lines
:
1584 lines_to_del
.append((running_ctx_keys
, line
))
1586 # Some commands can happen at higher counts that make
1587 # doing vtysh -c inefficient (and can time out.) For
1588 # these commands, instead of adding them to lines_to_del,
1589 # add the "no " version to lines_to_add.
1590 elif running_ctx_keys
[0].startswith("ip route") or running_ctx_keys
[
1592 ].startswith("ipv6 route"):
1593 add_cmd
= ("no " + running_ctx_keys
[0],)
1594 lines_to_add
.append((add_cmd
, None))
1596 # if this an interface sub-subcontext in an address-family block in ldpd and
1597 # we are already deleting the whole context, then ignore this
1599 len(running_ctx_keys
) > 2
1600 and running_ctx_keys
[0].startswith("mpls ldp")
1601 and running_ctx_keys
[1].startswith("address-family")
1602 and (running_ctx_keys
[:2], None) in lines_to_del
1606 # same thing for a pseudowire sub-context inside an l2vpn context
1608 len(running_ctx_keys
) > 1
1609 and running_ctx_keys
[0].startswith("l2vpn")
1610 and running_ctx_keys
[1].startswith("member pseudowire")
1611 and (running_ctx_keys
[:1], None) in lines_to_del
1615 # Segment routing and traffic engineering never need to be deleted
1617 running_ctx_keys
[0].startswith("segment-routing")
1618 and len(running_ctx_keys
) < 3
1622 # Neither the pcep command
1624 len(running_ctx_keys
) == 3
1625 and running_ctx_keys
[0].startswith("segment-routing")
1626 and running_ctx_keys
[2].startswith("pcep")
1630 # Segment lists can only be deleted after we removed all the candidate paths that
1631 # use them, so add them to a separate array that is going to be appended at the end
1633 len(running_ctx_keys
) == 3
1634 and running_ctx_keys
[0].startswith("segment-routing")
1635 and running_ctx_keys
[2].startswith("segment-list")
1637 seglist_to_del
.append((running_ctx_keys
, None))
1639 # Policies must be deleted after there candidate path, to be sure
1640 # we add them to a separate array that is going to be appended at the end
1642 len(running_ctx_keys
) == 3
1643 and running_ctx_keys
[0].startswith("segment-routing")
1644 and running_ctx_keys
[2].startswith("policy")
1646 pollist_to_del
.append((running_ctx_keys
, None))
1648 # pce-config must be deleted after the pce, to be sure we add them
1649 # to a separate array that is going to be appended at the end
1651 len(running_ctx_keys
) >= 4
1652 and running_ctx_keys
[0].startswith("segment-routing")
1653 and running_ctx_keys
[3].startswith("pce-config")
1655 pceconf_to_del
.append((running_ctx_keys
, None))
1657 # pcc must be deleted after the pce and pce-config too
1659 len(running_ctx_keys
) >= 4
1660 and running_ctx_keys
[0].startswith("segment-routing")
1661 and running_ctx_keys
[3].startswith("pcc")
1663 pcclist_to_del
.append((running_ctx_keys
, None))
1665 # Non-global context
1666 elif running_ctx_keys
and not any(
1667 "address-family" in key
for key
in running_ctx_keys
1669 lines_to_del
.append((running_ctx_keys
, None))
1671 elif running_ctx_keys
and not any("vni" in key
for key
in running_ctx_keys
):
1672 lines_to_del
.append((running_ctx_keys
, None))
1676 for line
in running_ctx
.lines
:
1677 lines_to_del
.append((running_ctx_keys
, line
))
1679 # if we have some policies commands to delete, append them to lines_to_del
1680 if len(pollist_to_del
) > 0:
1681 lines_to_del
.extend(pollist_to_del
)
1683 # if we have some segment list commands to delete, append them to lines_to_del
1684 if len(seglist_to_del
) > 0:
1685 lines_to_del
.extend(seglist_to_del
)
1687 # if we have some pce list commands to delete, append them to lines_to_del
1688 if len(pceconf_to_del
) > 0:
1689 lines_to_del
.extend(pceconf_to_del
)
1691 # if we have some pcc list commands to delete, append them to lines_to_del
1692 if len(pcclist_to_del
) > 0:
1693 lines_to_del
.extend(pcclist_to_del
)
1695 # Find the lines within each context to add
1696 # Find the lines within each context to del
1697 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1699 if newconf_ctx_keys
in running
.contexts
:
1700 running_ctx
= running
.contexts
[newconf_ctx_keys
]
1702 for line
in newconf_ctx
.lines
:
1703 if line
not in running_ctx
.dlines
:
1705 # candidate paths can only be added after the policy and segment list,
1706 # so add them to a separate array that is going to be appended at the end
1708 len(newconf_ctx_keys
) == 3
1709 and newconf_ctx_keys
[0].startswith("segment-routing")
1710 and newconf_ctx_keys
[2].startswith("policy ")
1711 and line
.startswith("candidate-path ")
1713 candidates_to_add
.append((newconf_ctx_keys
, line
))
1716 lines_to_add
.append((newconf_ctx_keys
, line
))
1718 for line
in running_ctx
.lines
:
1719 if line
not in newconf_ctx
.dlines
:
1720 lines_to_del
.append((newconf_ctx_keys
, line
))
1722 for (newconf_ctx_keys
, newconf_ctx
) in iteritems(newconf
.contexts
):
1724 if newconf_ctx_keys
not in running
.contexts
:
1726 # candidate paths can only be added after the policy and segment list,
1727 # so add them to a separate array that is going to be appended at the end
1729 len(newconf_ctx_keys
) == 4
1730 and newconf_ctx_keys
[0].startswith("segment-routing")
1731 and newconf_ctx_keys
[3].startswith("candidate-path")
1733 candidates_to_add
.append((newconf_ctx_keys
, None))
1734 for line
in newconf_ctx
.lines
:
1735 candidates_to_add
.append((newconf_ctx_keys
, line
))
1738 lines_to_add
.append((newconf_ctx_keys
, None))
1740 for line
in newconf_ctx
.lines
:
1741 lines_to_add
.append((newconf_ctx_keys
, line
))
1743 # if we have some candidate paths commands to add, append them to lines_to_add
1744 if len(candidates_to_add
) > 0:
1745 lines_to_add
.extend(candidates_to_add
)
1747 (lines_to_add
, lines_to_del
) = check_for_exit_vrf(lines_to_add
, lines_to_del
)
1748 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(
1749 lines_to_add
, lines_to_del
1751 (lines_to_add
, lines_to_del
) = delete_move_lines(lines_to_add
, lines_to_del
)
1752 (lines_to_add
, lines_to_del
) = ignore_unconfigurable_lines(
1753 lines_to_add
, lines_to_del
1756 return (lines_to_add
, lines_to_del
)
1759 if __name__
== "__main__":
1760 # Command line options
1761 parser
= argparse
.ArgumentParser(
1762 description
="Dynamically apply diff in frr configs"
1764 parser
.add_argument(
1765 "--input", help='Read running config from file instead of "show running"'
1767 group
= parser
.add_mutually_exclusive_group(required
=True)
1769 "--reload", action
="store_true", help="Apply the deltas", default
=False
1772 "--test", action
="store_true", help="Show the deltas", default
=False
1774 level_group
= parser
.add_mutually_exclusive_group()
1775 level_group
.add_argument(
1777 action
="store_true",
1778 help="Enable debugs (synonym for --log-level=debug)",
1781 level_group
.add_argument(
1785 choices
=("critical", "error", "warning", "info", "debug"),
1787 parser
.add_argument(
1788 "--stdout", action
="store_true", help="Log to STDOUT", default
=False
1790 parser
.add_argument(
1794 help="Reload specified path/namespace",
1797 parser
.add_argument("filename", help="Location of new frr config file")
1798 parser
.add_argument(
1800 action
="store_true",
1801 help="Overwrite frr.conf with running config output",
1804 parser
.add_argument(
1805 "--bindir", help="path to the vtysh executable", default
="/usr/bin"
1807 parser
.add_argument(
1808 "--confdir", help="path to the daemon config files", default
="/etc/frr"
1810 parser
.add_argument(
1811 "--rundir", help="path for the temp config file", default
="/var/run/frr"
1813 parser
.add_argument(
1815 help="socket to be used by vtysh to connect to the daemons",
1818 parser
.add_argument(
1819 "--daemon", help="daemon for which want to replace the config", default
=""
1821 parser
.add_argument(
1823 action
="store_true",
1824 help="Used by topotest to not delete debug or log file commands",
1827 args
= parser
.parse_args()
1830 # For --test log to stdout
1831 # For --reload log to /var/log/frr/frr-reload.log
1832 if args
.test
or args
.stdout
:
1833 logging
.basicConfig(format
="%(asctime)s %(levelname)5s: %(message)s")
1835 # Color the errors and warnings in red
1836 logging
.addLevelName(
1837 logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
)
1839 logging
.addLevelName(
1840 logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
)
1844 if not os
.path
.isdir("/var/log/frr/"):
1845 os
.makedirs("/var/log/frr/", mode
=0o0755)
1847 logging
.basicConfig(
1848 filename
="/var/log/frr/frr-reload.log",
1849 format
="%(asctime)s %(levelname)5s: %(message)s",
1852 # argparse should prevent this from happening but just to be safe...
1854 raise Exception("Must specify --reload or --test")
1855 log
= logging
.getLogger(__name__
)
1858 log
.setLevel(logging
.DEBUG
)
1860 log
.setLevel(args
.log_level
.upper())
1862 if args
.reload and not args
.stdout
:
1863 # Additionally send errors and above to STDOUT, with no metadata,
1864 # when we are logging to a file. This specifically does not follow
1865 # args.log_level, and is analagous to behaviour in earlier versions
1866 # which additionally logged most errors using print().
1868 stdout_hdlr
= logging
.StreamHandler(sys
.stdout
)
1869 stdout_hdlr
.setLevel(logging
.ERROR
)
1870 stdout_hdlr
.setFormatter(logging
.Formatter())
1871 log
.addHandler(stdout_hdlr
)
1873 # Verify the new config file is valid
1874 if not os
.path
.isfile(args
.filename
):
1875 log
.error("Filename %s does not exist" % args
.filename
)
1878 if not os
.path
.getsize(args
.filename
):
1879 log
.error("Filename %s is an empty file" % args
.filename
)
1882 # Verify that confdir is correct
1883 if not os
.path
.isdir(args
.confdir
):
1884 log
.error("Confdir %s is not a valid path" % args
.confdir
)
1887 # Verify that bindir is correct
1888 if not os
.path
.isdir(args
.bindir
) or not os
.path
.isfile(args
.bindir
+ "/vtysh"):
1889 log
.error("Bindir %s is not a valid path to vtysh" % args
.bindir
)
1892 # verify that the vty_socket, if specified, is valid
1893 if args
.vty_socket
and not os
.path
.isdir(args
.vty_socket
):
1894 log
.error("vty_socket %s is not a valid path" % args
.vty_socket
)
1897 # verify that the daemon, if specified, is valid
1898 if args
.daemon
and args
.daemon
not in [
1920 msg
= "Daemon %s is not a valid option for 'show running-config'" % args
.daemon
1925 vtysh
= Vtysh(args
.bindir
, args
.confdir
, args
.vty_socket
, args
.pathspace
)
1927 # Verify that 'service integrated-vtysh-config' is configured
1929 vtysh_filename
= args
.confdir
+ "/" + args
.pathspace
+ "/vtysh.conf"
1931 vtysh_filename
= args
.confdir
+ "/vtysh.conf"
1932 service_integrated_vtysh_config
= True
1934 if os
.path
.isfile(vtysh_filename
):
1935 with
open(vtysh_filename
, "r") as fh
:
1936 for line
in fh
.readlines():
1939 if line
== "no service integrated-vtysh-config":
1940 service_integrated_vtysh_config
= False
1943 if not args
.test
and not service_integrated_vtysh_config
and not args
.daemon
:
1945 "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
1949 log
.info('Called via "%s"', str(args
))
1951 # Create a Config object from the config generated by newconf
1952 newconf
= Config(vtysh
)
1954 newconf
.load_from_file(args
.filename
)
1956 except VtyshException
as ve
:
1957 log
.error("vtysh failed to process new configuration: {}".format(ve
))
1962 # Create a Config object from the running config
1963 running
= Config(vtysh
)
1966 running
.load_from_file(args
.input)
1968 running
.load_from_show_running(args
.daemon
)
1970 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
1973 if not args
.test_reset
:
1974 print("\nLines To Delete")
1975 print("===============")
1977 for (ctx_keys
, line
) in lines_to_del
:
1982 nolines
= lines_to_config(ctx_keys
, line
, True)
1985 # For topotests the original code stripped the lines, and ommitted blank lines
1986 # after, do that here
1987 nolines
= [x
.strip() for x
in nolines
]
1988 # For topotests leave these lines in (don't delete them)
1989 # [chopps: why is "log file" more special than other "log" commands?]
1991 x
for x
in nolines
if "debug" not in x
and "log file" not in x
1996 cmd
= "\n".join(nolines
)
2000 if not args
.test_reset
:
2001 print("\nLines To Add")
2002 print("============")
2004 for (ctx_keys
, line
) in lines_to_add
:
2009 lines
= lines_to_config(ctx_keys
, line
, False)
2012 # For topotests the original code stripped the lines, and ommitted blank lines
2013 # after, do that here
2014 lines
= [x
.strip() for x
in lines
if x
.strip()]
2018 cmd
= "\n".join(lines
)
2022 lines_to_configure
= []
2024 # We will not be able to do anything, go ahead and exit(1)
2025 if not vtysh
.is_config_available() or not reload_ok
:
2028 log
.debug("New Frr Config\n%s", newconf
.get_lines())
2030 # This looks a little odd but we have to do this twice...here is why
2031 # If the user had this running bgp config:
2034 # neighbor 1.1.1.1 remote-as 50
2035 # neighbor 1.1.1.1 route-map FOO out
2037 # and this config in the newconf config file
2040 # neighbor 1.1.1.1 remote-as 999
2041 # neighbor 1.1.1.1 route-map FOO out
2044 # Then the script will do
2045 # - no neighbor 1.1.1.1 remote-as 50
2046 # - neighbor 1.1.1.1 remote-as 999
2048 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
2049 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
2050 # configs again to put this line back.
2052 # There are many keywords in FRR that can only appear one time under
2053 # a context, take "bgp router-id" for example. If the config that we are
2054 # reloading against has the following:
2057 # bgp router-id 1.1.1.1
2058 # bgp router-id 2.2.2.2
2060 # The final config needs to contain "bgp router-id 2.2.2.2". On the
2061 # first pass we will add "bgp router-id 2.2.2.2" but then on the second
2062 # pass we will see that "bgp router-id 1.1.1.1" is missing and add that
2063 # back which cancels out the "bgp router-id 2.2.2.2". The fix is for the
2064 # second pass to include all of the "adds" from the first pass.
2065 lines_to_add_first_pass
= []
2068 running
= Config(vtysh
)
2069 running
.load_from_show_running(args
.daemon
)
2070 log
.debug("Running Frr Config (Pass #%d)\n%s", x
, running
.get_lines())
2072 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
2075 lines_to_add_first_pass
= lines_to_add
2077 lines_to_add
.extend(lines_to_add_first_pass
)
2079 # Only do deletes on the first pass. The reason being if we
2080 # configure a bgp neighbor via "neighbor swp1 interface" FRR
2081 # will automatically add:
2084 # ipv6 nd ra-interval 10
2085 # no ipv6 nd suppress-ra
2088 # but those lines aren't in the config we are reloading against so
2089 # on the 2nd pass they will show up in lines_to_del. This could
2090 # apply to other scenarios as well where configuring FOO adds BAR
2092 if lines_to_del
and x
== 0:
2093 for (ctx_keys
, line
) in lines_to_del
:
2098 # 'no' commands are tricky, we can't just put them in a file and
2099 # vtysh -f that file. See the next comment for an explanation
2101 cmd
= lines_to_config(ctx_keys
, line
, True)
2104 # Some commands in frr are picky about taking a "no" of the entire line.
2105 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
2106 # only the beginning. If we hit one of these command an exception will be
2107 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
2110 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
2111 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
2112 # % Unknown command.
2113 # frr(config-if)# no ip ospf authentication message-digest
2114 # % Unknown command.
2115 # frr(config-if)# no ip ospf authentication
2121 vtysh(["configure"] + cmd
, stdouts
)
2123 except VtyshException
:
2125 # - Pull the last entry from cmd (this would be
2126 # 'no ip ospf authentication message-digest 1.1.1.1' in
2128 # - Split that last entry by whitespace and drop the last word
2129 log
.info("Failed to execute %s", " ".join(cmd
))
2130 last_arg
= cmd
[-1].split(" ")
2132 if len(last_arg
) <= 2:
2134 '"%s" we failed to remove this command',
2135 " -- ".join(original_cmd
),
2137 # Log first error msg for original_cmd
2139 log
.error(stdouts
[0])
2143 new_last_arg
= last_arg
[0:-1]
2144 cmd
[-1] = " ".join(new_last_arg
)
2146 log
.info('Executed "%s"', " ".join(cmd
))
2150 lines_to_configure
= []
2152 for (ctx_keys
, line
) in lines_to_add
:
2157 # Don't run "no" commands twice since they can error
2158 # out the second time due to first deletion
2159 if x
== 1 and ctx_keys
[0].startswith("no "):
2162 cmd
= "\n".join(lines_to_config(ctx_keys
, line
, False)) + "\n"
2163 lines_to_configure
.append(cmd
)
2165 if lines_to_configure
:
2166 random_string
= "".join(
2167 random
.SystemRandom().choice(
2168 string
.ascii_uppercase
+ string
.digits
2173 filename
= args
.rundir
+ "/reload-%s.txt" % random_string
2174 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
2176 with
open(filename
, "w") as fh
:
2177 for line
in lines_to_configure
:
2178 fh
.write(line
+ "\n")
2181 vtysh
.exec_file(filename
)
2182 except VtyshException
as e
:
2183 log
.warning("frr-reload.py failed due to\n%s" % e
.args
)
2187 # Make these changes persistent
2188 target
= str(args
.confdir
+ "/frr.conf")
2189 if args
.overwrite
or (not args
.daemon
and args
.filename
!= target
):